wip(collision gate): stop the fall instead of the player -- UNVALIDATED, after-change run never happened

The round was stopped when Windows began refusing to start processes for it (0xC0000142) under memory pressure, before any after-change test run. Treat as a draft: the baseline run happened, the verification did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-22 15:19:12 +02:00
1 parent 0dd45ca410
commit 88f3e44402
3 files changed
+719 -109

No files matched your search

+621 -97
View File
@@ -60,6 +60,12 @@
namespace namespace
{ {
int32 GVoxelForgeCollisionGate = 1;
FAutoConsoleVariableRef CVarVoxelForgeCollisionGate(
TEXT("voxel.CollisionGate"),
GVoxelForgeCollisionGate,
TEXT("Protect the pawn from falling through unresolved ground: 0 off, 1 on."));
int32 GVoxelForgeProfileTileGeneration = 0; int32 GVoxelForgeProfileTileGeneration = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration( FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
TEXT("voxel.ProfileTileGeneration"), TEXT("voxel.ProfileTileGeneration"),
@@ -1436,6 +1442,7 @@ void AVoxelWorld::BeginPlay()
int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode; int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode;
int32 CommandLineSealedSolidProof = GVoxelForgeSealedSolidProof; int32 CommandLineSealedSolidProof = GVoxelForgeSealedSolidProof;
int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; int32 CommandLineSampleStacks = GVoxelForgeSampleStacks;
int32 CommandLineCollisionGate = GVoxelForgeCollisionGate;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile)) if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile))
{ {
GVoxelForgeProfileDensity = CommandLineProfile; GVoxelForgeProfileDensity = CommandLineProfile;
@@ -1471,6 +1478,10 @@ void AVoxelWorld::BeginPlay()
{ {
GVoxelForgeSampleStacks = CommandLineSampleStacks; GVoxelForgeSampleStacks = CommandLineSampleStacks;
} }
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CollisionGate="), CommandLineCollisionGate))
{
GVoxelForgeCollisionGate = CommandLineCollisionGate;
}
GVoxelForgeProfileTileGeneration = GVoxelForgeProfileTileGeneration != 0 ? 1 : 0; GVoxelForgeProfileTileGeneration = GVoxelForgeProfileTileGeneration != 0 ? 1 : 0;
GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0; GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0;
GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
@@ -1479,6 +1490,11 @@ void AVoxelWorld::BeginPlay()
GVoxelForgeSampleStacks, GVoxelForgeSampleStacks,
0, 0,
static_cast<int32>(FVoxelStackSampler::MaxIntervalUs)); static_cast<int32>(FVoxelStackSampler::MaxIntervalUs));
GVoxelForgeCollisionGate = GVoxelForgeCollisionGate != 0 ? 1 : 0;
bCollisionGateEnabled = GVoxelForgeCollisionGate != 0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] startup enabled=%d"),
bCollisionGateEnabled ? 1 : 0);
if (GVoxelForgePerfAttribution != 0) if (GVoxelForgePerfAttribution != 0)
{ {
@@ -1744,6 +1760,31 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessStreamingTestLastActualPosition = FVector::ZeroVector; HeadlessStreamingTestLastActualPosition = FVector::ZeroVector;
HeadlessStreamingTestBeginSeconds = 0.0; HeadlessStreamingTestBeginSeconds = 0.0;
HeadlessStreamingTestLastElapsedSeconds = 0.0; HeadlessStreamingTestLastElapsedSeconds = 0.0;
bHeadlessCollisionGateStressTest = false;
bHeadlessCollisionGateStressTestStartPlaced = false;
bHeadlessCollisionGateStressTestStarted = false;
bHeadlessCollisionGateStressTestPassed = false;
bHeadlessCollisionGateStressTestFailed = false;
bHeadlessCollisionGateStressTestExitRequested = false;
bHeadlessCollisionGateStressTestGateWasEngaged = false;
bHeadlessCollisionGateStressTestFullStop = false;
HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
HeadlessCollisionGateStressTestStartDelaySeconds = 0.1f;
HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
HeadlessCollisionGateStressTestBeginSeconds = 0.0;
HeadlessCollisionGateStressTestStartSeconds = 0.0;
HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
HeadlessCollisionGateStressTestDistanceCm = 0.0;
HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0;
HeadlessCollisionGateStressTestGateHoldCount = 0;
HeadlessCollisionGateStressTestFullStopCount = 0;
HeadlessCollisionGateStressTestDirection = FVector::XAxisVector;
HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector;
HeadlessCollisionGateStressTestFailureReason.Reset();
bHeadlessSurfaceFallTest = false; bHeadlessSurfaceFallTest = false;
bHeadlessSurfaceFallTestStartPlaced = false; bHeadlessSurfaceFallTestStartPlaced = false;
bHeadlessSurfaceFallTestPassed = false; bHeadlessSurfaceFallTestPassed = false;
@@ -1776,6 +1817,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessStrateCrossingTestExitRequested = false; bHeadlessStrateCrossingTestExitRequested = false;
bHeadlessStrateCrossingTestGateWasEngaged = false; bHeadlessStrateCrossingTestGateWasEngaged = false;
bHeadlessStrateCrossingTestInitialSupportReported = false; bHeadlessStrateCrossingTestInitialSupportReported = false;
bHeadlessStrateCrossingTestPendingFloorOverlap = false;
HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE; HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
HeadlessStrateCrossingTestPassageIndex = INDEX_NONE; HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE; HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
@@ -1976,6 +2018,51 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessStreamingTestDirection.Y, HeadlessStreamingTestDirection.Y,
HeadlessStreamingTestDirection.Z); HeadlessStreamingTestDirection.Z);
} }
int32 CollisionGateStressValue = 0;
bHeadlessCollisionGateStressTest =
(FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStress="), CollisionGateStressValue)
&& CollisionGateStressValue != 0)
|| FParse::Param(CommandLine, TEXT("voxel.TestCollisionGateStress"));
if (bHeadlessCollisionGateStressTest)
{
if (bHeadlessStrateCrossingTest || bHeadlessSurfaceFallTest)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("conflicting_headless_tests");
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeCollisionGateStressTest] result=FAIL reason=%s"),
*HeadlessCollisionGateStressTestFailureReason);
}
float StressSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond;
float StressStartDelay = HeadlessCollisionGateStressTestStartDelaySeconds;
float StressDuration = HeadlessCollisionGateStressTestDurationSeconds;
float StressTimeout = HeadlessCollisionGateStressTestTimeoutSeconds;
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpeedCmPerSecond="),
StressSpeed);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressStartDelaySeconds="),
StressStartDelay);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDurationSeconds="),
StressDuration);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressTimeoutSeconds="),
StressTimeout);
HeadlessCollisionGateStressTestSpeedCmPerSecond = FMath::Max(StressSpeed, 1.0f);
HeadlessCollisionGateStressTestStartDelaySeconds = FMath::Max(StressStartDelay, 0.0f);
HeadlessCollisionGateStressTestDurationSeconds = FMath::Max(StressDuration, 0.25f);
HeadlessCollisionGateStressTestTimeoutSeconds = FMath::Max(StressTimeout, 1.0f);
// The stress path owns movement and keeps the streamer centred at the launch tile. Do
// not let the older synthetic TestMove path run in parallel with it.
bHeadlessStreamingTestMovement = false;
HeadlessStreamingTestExitSeconds = 0.0f;
HeadlessCollisionGateStressTestBeginSeconds = 0.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] configured=1 speed_cm_s=%.1f "
"start_delay_s=%.3f duration_s=%.3f timeout_s=%.3f"),
HeadlessCollisionGateStressTestSpeedCmPerSecond,
HeadlessCollisionGateStressTestStartDelaySeconds,
HeadlessCollisionGateStressTestDurationSeconds,
HeadlessCollisionGateStressTestTimeoutSeconds);
}
} }
void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
@@ -2469,6 +2556,22 @@ void AVoxelWorld::ObserveHeadlessStrateCrossingTest(
const FVoxelPassage& Passage = const FVoxelPassage& Passage =
StrateManager->GetPassages()[HeadlessStrateCrossingTestPassageIndex]; StrateManager->GetPassages()[HeadlessStrateCrossingTestPassageIndex];
const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition); const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition);
FVoxelTileKey PendingFloorTile;
if (IsPawnCapsuleOverlappingPendingFloor(
PlayerPawn, PlayerPosition, &PendingFloorTile))
{
bHeadlessStrateCrossingTestPendingFloorOverlap = true;
bHeadlessStrateCrossingTestFailed = true;
HeadlessStrateCrossingTestFailureReason = TEXT("capsule_overlaps_pending_floor");
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeStrateCrossingTest] assertion_failed=%s tile=(%d,%d,%d) "
"position_vox=(%.2f,%.2f,%.2f) gate=%d"),
*HeadlessStrateCrossingTestFailureReason,
PendingFloorTile.Coord.X, PendingFloorTile.Coord.Y, PendingFloorTile.Coord.Z,
LocalPosition.X, LocalPosition.Y, LocalPosition.Z,
bPawnCollisionGateEngaged ? 1 : 0);
return;
}
float PassageFloorZ = 0.0f; float PassageFloorZ = 0.0f;
if (VF_ProjectHeadlessPassageFloor(Passage, LocalPosition, PassageFloorZ)) if (VF_ProjectHeadlessPassageFloor(Passage, LocalPosition, PassageFloorZ))
{ {
@@ -2570,7 +2673,8 @@ void AVoxelWorld::MaybeFinishHeadlessStrateCrossingTest()
TEXT("[VoxelForgeStrateCrossingTest] result=%s reason=%s passage=%d upper_strate=%d " TEXT("[VoxelForgeStrateCrossingTest] result=%s reason=%s passage=%d upper_strate=%d "
"lower_strate=%d elapsed_s=%.6f distance_m=%.6f route=%d/%d " "lower_strate=%d elapsed_s=%.6f distance_m=%.6f route=%d/%d "
"max_gate_s=%.6f gate_limit_s=%.3f gate_total_s=%.6f " "max_gate_s=%.6f gate_limit_s=%.3f gate_total_s=%.6f "
"min_floor_clearance_vox=%.6f final_position=(%.1f,%.1f,%.1f)"), "min_floor_clearance_vox=%.6f pending_floor_overlap=%d "
"final_position=(%.1f,%.1f,%.1f)"),
bPassed ? TEXT("PASS") : TEXT("FAIL"), bPassed ? TEXT("PASS") : TEXT("FAIL"),
bPassed ? TEXT("reached_lower_strate") : *HeadlessStrateCrossingTestFailureReason, bPassed ? TEXT("reached_lower_strate") : *HeadlessStrateCrossingTestFailureReason,
HeadlessStrateCrossingTestPassageIndex, HeadlessStrateCrossingTestPassageIndex,
@@ -2584,6 +2688,7 @@ void AVoxelWorld::MaybeFinishHeadlessStrateCrossingTest()
HeadlessStrateCrossingTestMaxGateSeconds, HeadlessStrateCrossingTestMaxGateSeconds,
HeadlessStrateCrossingTestTotalGateDurationSeconds, HeadlessStrateCrossingTestTotalGateDurationSeconds,
HeadlessStrateCrossingTestMinFloorClearanceVoxels, HeadlessStrateCrossingTestMinFloorClearanceVoxels,
bHeadlessStrateCrossingTestPendingFloorOverlap ? 1 : 0,
HeadlessStrateCrossingTestLastActualPosition.X, HeadlessStrateCrossingTestLastActualPosition.X,
HeadlessStrateCrossingTestLastActualPosition.Y, HeadlessStrateCrossingTestLastActualPosition.Y,
HeadlessStrateCrossingTestLastActualPosition.Z); HeadlessStrateCrossingTestLastActualPosition.Z);
@@ -2652,6 +2757,176 @@ void AVoxelWorld::AdvanceHeadlessStreamingTest(
} }
} }
void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn)
{
if (!bHeadlessCollisionGateStressTest || bHeadlessCollisionGateStressTestFailed
|| bHeadlessCollisionGateStressTestPassed || !IsValid(PlayerPawn))
{
return;
}
const double Now = FPlatformTime::Seconds();
if (!bHeadlessCollisionGateStressTestStartPlaced)
{
FVoxelTileKey SupportTile;
const TCHAR* SupportUnavailableReason = nullptr;
const bool bSupportReady = IsPlayerSupportCollisionReady(
PlayerPawn, InOutPlayerPosition, &SupportTile, &SupportUnavailableReason);
const FVector ActualPosition = PlayerPawn->GetActorLocation();
const FVector LaunchVoxel = WorldToLocalVoxel(ActualPosition);
// Freeze only the streaming centre, not the pawn. This deliberately leaves the desired
// clipmap behind the run while the launch support is already ready.
bHeadlessStreamingTestCenterOverride = true;
HeadlessStreamingTestCenterVoxel = LaunchVoxel;
InOutPlayerPosition = ActualPosition;
InOutPlayerHeading = FVector::ZeroVector;
HeadlessCollisionGateStressTestLastActualPosition = ActualPosition;
bHeadlessCollisionGateStressTestStartPlaced = true;
HeadlessCollisionGateStressTestStartSeconds = Now;
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{
if (UCharacterMovementComponent* CharacterMovement =
Character->GetCharacterMovement())
{
CharacterMovement->MaxWalkSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond;
CharacterMovement->MaxAcceleration = FMath::Max(
CharacterMovement->MaxAcceleration,
HeadlessCollisionGateStressTestSpeedCmPerSecond * 8.0f);
CharacterMovement->BrakingDecelerationWalking = FMath::Max(
CharacterMovement->BrakingDecelerationWalking,
HeadlessCollisionGateStressTestSpeedCmPerSecond * 8.0f);
CharacterMovement->StopMovementImmediately();
CharacterMovement->SetMovementMode(MOVE_Walking);
}
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] start_placed support=(%d,%d,%d) ready=%d "
"reason=%s "
"position=(%.1f,%.1f,%.1f) launch_vox=(%.2f,%.2f,%.2f)"),
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z,
bSupportReady ? 1 : 0,
SupportUnavailableReason != nullptr ? SupportUnavailableReason : TEXT("pending"),
ActualPosition.X, ActualPosition.Y, ActualPosition.Z,
LaunchVoxel.X, LaunchVoxel.Y, LaunchVoxel.Z);
return;
}
const double SinceLaunch = Now - HeadlessCollisionGateStressTestStartSeconds;
if (!bHeadlessCollisionGateStressTestStarted
&& SinceLaunch < static_cast<double>(HeadlessCollisionGateStressTestStartDelaySeconds))
{
return;
}
if (!bHeadlessCollisionGateStressTestStarted)
{
bHeadlessCollisionGateStressTestStarted = true;
HeadlessCollisionGateStressTestBeginSeconds = Now;
HeadlessCollisionGateStressTestStartSeconds = Now;
HeadlessCollisionGateStressTestLastActualPosition = InOutPlayerPosition;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] movement_started speed_cm_s=%.1f"),
HeadlessCollisionGateStressTestSpeedCmPerSecond);
}
const FVector WorldDirection = GetActorTransform().TransformVectorNoScale(
HeadlessCollisionGateStressTestDirection).GetSafeNormal();
if (WorldDirection.IsNearlyZero())
{
return;
}
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{
if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
{
// The baseline gate changes MOVE_Walking to MOVE_None. Do not undo that here; the
// test must observe the resulting full stop. The fall-only gate keeps this input live.
if (!bPawnCollisionGateEngaged
&& CharacterMovement->MovementMode == MOVE_None)
{
CharacterMovement->SetMovementMode(MOVE_Walking);
}
PlayerPawn->AddMovementInput(WorldDirection, 1.0f, false);
}
}
else
{
PlayerPawn->AddMovementInput(WorldDirection, 1.0f, false);
}
InOutPlayerHeading = WorldDirection * HeadlessCollisionGateStressTestSpeedCmPerSecond;
}
void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
const FVector& PlayerPosition, APawn* PlayerPawn)
{
if (!bHeadlessCollisionGateStressTest || !bHeadlessCollisionGateStressTestStarted
|| !IsValid(PlayerPawn) || bHeadlessCollisionGateStressTestExitRequested)
{
return;
}
const double Now = FPlatformTime::Seconds();
const float HorizontalDelta = FVector::Dist2D(
HeadlessCollisionGateStressTestLastActualPosition, PlayerPosition);
HeadlessCollisionGateStressTestDistanceCm += HorizontalDelta;
HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition;
const bool bGateEngaged = bPawnCollisionGateEngaged;
if (bGateEngaged && !bHeadlessCollisionGateStressTestGateWasEngaged)
{
++HeadlessCollisionGateStressTestGateHoldCount;
HeadlessCollisionGateStressTestGateStartSeconds = Now;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] gate=engaged elapsed_s=%.6f"),
Now - HeadlessCollisionGateStressTestStartSeconds);
}
else if (!bGateEngaged && bHeadlessCollisionGateStressTestGateWasEngaged)
{
const double GateDuration = FMath::Max(
0.0, Now - HeadlessCollisionGateStressTestGateStartSeconds);
HeadlessCollisionGateStressTestTotalGateDurationSeconds += GateDuration;
HeadlessCollisionGateStressTestMaxGateDurationSeconds = FMath::Max(
HeadlessCollisionGateStressTestMaxGateDurationSeconds, GateDuration);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] gate=released duration_s=%.6f "
"total_s=%.6f"),
GateDuration,
HeadlessCollisionGateStressTestTotalGateDurationSeconds);
}
bHeadlessCollisionGateStressTestGateWasEngaged = bGateEngaged;
// At 6000 cm/s a healthy frame advances well beyond one centimetre. A zero horizontal delta
// while the collision gate is engaged is therefore a real full stop, not a low-rate sample.
const bool bFullStop = bGateEngaged && HorizontalDelta <= 0.01f;
if (bFullStop && !bHeadlessCollisionGateStressTestFullStop)
{
++HeadlessCollisionGateStressTestFullStopCount;
HeadlessCollisionGateStressTestFullStopStartSeconds = Now;
}
else if (!bFullStop && bHeadlessCollisionGateStressTestFullStop)
{
HeadlessCollisionGateStressTestFullStopDurationSeconds += FMath::Max(
0.0, Now - HeadlessCollisionGateStressTestFullStopStartSeconds);
}
bHeadlessCollisionGateStressTestFullStop = bFullStop;
if (Now - HeadlessCollisionGateStressTestStartSeconds
>= static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds))
{
return;
}
if (Now - HeadlessCollisionGateStressTestBeginSeconds
>= static_cast<double>(HeadlessCollisionGateStressTestTimeoutSeconds))
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("stress_timeout");
}
}
void AVoxelWorld::AdvanceHeadlessSurfaceFallTest( void AVoxelWorld::AdvanceHeadlessSurfaceFallTest(
FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn) FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn)
{ {
@@ -2965,6 +3240,109 @@ void AVoxelWorld::MaybeFinishHeadlessSurfaceFallTest()
false, bPassed ? 0 : 1, TEXT("VoxelForge surface fall test complete")); false, bPassed ? 0 : 1, TEXT("VoxelForge surface fall test complete"));
} }
void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
{
if (!bHeadlessCollisionGateStressTest || bHeadlessCollisionGateStressTestExitRequested
|| HeadlessCollisionGateStressTestBeginSeconds <= 0.0)
{
return;
}
const double Now = FPlatformTime::Seconds();
const double SinceBegin = Now - HeadlessCollisionGateStressTestBeginSeconds;
if (!bHeadlessCollisionGateStressTestStartPlaced
&& !bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed
&& SinceBegin >= static_cast<double>(HeadlessCollisionGateStressTestTimeoutSeconds))
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("stress_start_not_observed");
}
else if (bHeadlessCollisionGateStressTestStarted
&& !bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed
&& Now - HeadlessCollisionGateStressTestStartSeconds
>= static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds))
{
const double ExpectedDistanceCm =
static_cast<double>(HeadlessCollisionGateStressTestSpeedCmPerSecond)
* static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds);
if (HeadlessCollisionGateStressTestFullStopCount > 0)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("full_horizontal_stop");
}
else if (HeadlessCollisionGateStressTestDistanceCm < ExpectedDistanceCm * 0.65)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("horizontal_progress_shortfall");
}
else
{
bHeadlessCollisionGateStressTestPassed = true;
}
}
else if (!bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed
&& SinceBegin >= static_cast<double>(HeadlessCollisionGateStressTestTimeoutSeconds))
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("stress_timeout");
}
if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed)
{
return;
}
if (bHeadlessCollisionGateStressTestGateWasEngaged
&& HeadlessCollisionGateStressTestGateStartSeconds > 0.0)
{
const double GateDuration = FMath::Max(
0.0, Now - HeadlessCollisionGateStressTestGateStartSeconds);
HeadlessCollisionGateStressTestTotalGateDurationSeconds += GateDuration;
HeadlessCollisionGateStressTestMaxGateDurationSeconds = FMath::Max(
HeadlessCollisionGateStressTestMaxGateDurationSeconds, GateDuration);
}
if (bHeadlessCollisionGateStressTestFullStop
&& HeadlessCollisionGateStressTestFullStopStartSeconds > 0.0)
{
HeadlessCollisionGateStressTestFullStopDurationSeconds += FMath::Max(
0.0, Now - HeadlessCollisionGateStressTestFullStopStartSeconds);
}
bHeadlessCollisionGateStressTestExitRequested = true;
const bool bPassed = bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed;
const double ExpectedDistanceCm =
static_cast<double>(HeadlessCollisionGateStressTestSpeedCmPerSecond)
* static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f "
"distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f "
"gate_holds=%d gate_total_s=%.6f max_gate_s=%.6f "
"full_stops=%d full_stop_total_s=%.6f collision_gate=%d "
"final_position=(%.1f,%.1f,%.1f)"),
bPassed ? TEXT("PASS") : TEXT("FAIL"),
bPassed ? TEXT("continuous_horizontal_progress")
: *HeadlessCollisionGateStressTestFailureReason,
Now - HeadlessCollisionGateStressTestBeginSeconds,
HeadlessCollisionGateStressTestDistanceCm / 100.0,
ExpectedDistanceCm / 100.0,
HeadlessCollisionGateStressTestSpeedCmPerSecond,
HeadlessCollisionGateStressTestGateHoldCount,
HeadlessCollisionGateStressTestTotalGateDurationSeconds,
HeadlessCollisionGateStressTestMaxGateDurationSeconds,
HeadlessCollisionGateStressTestFullStopCount,
HeadlessCollisionGateStressTestFullStopDurationSeconds,
bCollisionGateEnabled ? 1 : 0,
HeadlessCollisionGateStressTestLastActualPosition.X,
HeadlessCollisionGateStressTestLastActualPosition.Y,
HeadlessCollisionGateStressTestLastActualPosition.Z);
FPlatformMisc::RequestExitWithStatus(
false, bPassed ? 0 : 1, TEXT("VoxelForge collision-gate stress test complete"));
}
void AVoxelWorld::MaybeFinishHeadlessStreamingTest() void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
{ {
if (bHeadlessStrateCrossingTest) if (bHeadlessStrateCrossingTest)
@@ -2979,6 +3357,12 @@ void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
return; return;
} }
if (bHeadlessCollisionGateStressTest)
{
MaybeFinishHeadlessCollisionGateStressTest();
return;
}
if (bHeadlessStreamingTestExitRequested || HeadlessStreamingTestExitSeconds <= 0.0f if (bHeadlessStreamingTestExitRequested || HeadlessStreamingTestExitSeconds <= 0.0f
|| HeadlessStreamingTestBeginSeconds <= 0.0) || HeadlessStreamingTestBeginSeconds <= 0.0)
{ {
@@ -3018,10 +3402,12 @@ void AVoxelWorld::Tick(float DeltaTime)
AdvanceHeadlessStreamingTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessStreamingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
AdvanceHeadlessStrateCrossingTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessStrateCrossingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
AdvanceHeadlessSurfaceFallTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessSurfaceFallTest(PlayerLastPos, PlayerHeading, PlayerPawn);
AdvanceHeadlessCollisionGateStressTest(PlayerLastPos, PlayerHeading, PlayerPawn);
PeakObservedPawnSpeedCmPerSecond = FMath::Max( PeakObservedPawnSpeedCmPerSecond = FMath::Max(
PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D()); PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D());
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime); UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn); ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn);
ObserveHeadlessCollisionGateStressTest(PlayerLastPos, PlayerPawn);
const FVector StreamingCenterPosition = bHeadlessStreamingTestCenterOverride const FVector StreamingCenterPosition = bHeadlessStreamingTestCenterOverride
? LocalVoxelToWorld(HeadlessStreamingTestCenterVoxel) ? LocalVoxelToWorld(HeadlessStreamingTestCenterVoxel)
: PlayerLastPos; : PlayerLastPos;
@@ -3420,15 +3806,6 @@ bool AVoxelWorld::IsCollisionTileUnavailable(const FVoxelTileKey& Tile,
} }
} }
// DesiredStamp is zero only during the initial possession/setup tick, before the first
// desired set has been built. Do not classify that transient state as "outside desired";
// otherwise the initial safety gate could release before streaming has any authority.
if (DesiredStamp > 0 && !IsDesired(Tile))
{
OutReason = TEXT("outside_desired_set");
return true;
}
return false; return false;
} }
@@ -3507,14 +3884,167 @@ bool AVoxelWorld::IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& Play
CandidateTile, OutSupportTile, OutUnavailableReason); CandidateTile, OutSupportTile, OutUnavailableReason);
} }
bool AVoxelWorld::IsPawnCapsuleOverlappingPendingFloor(
APawn* Pawn, const FVector& PlayerPosition, FVoxelTileKey* OutPendingTile) const
{
if (OutPendingTile != nullptr)
{
*OutPendingTile = FVoxelTileKey();
}
if (!IsValid(Pawn))
{
return false;
}
FVoxelTileKey SupportTile;
const TCHAR* SupportUnavailableReason = nullptr;
if (IsPlayerSupportCollisionReady(
Pawn, PlayerPosition, &SupportTile, &SupportUnavailableReason)
|| SupportUnavailableReason != nullptr
|| SupportTile.Level != 0
|| CollisionNotRequiredTiles.Contains(SupportTile))
{
return false;
}
float HalfHeightCm = 0.0f;
float RadiusCm = 0.0f;
if (const ACharacter* Character = Cast<ACharacter>(Pawn))
{
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
{
HalfHeightCm = Capsule->GetScaledCapsuleHalfHeight();
RadiusCm = Capsule->GetScaledCapsuleRadius();
}
}
else
{
FVector BoundsOrigin = FVector::ZeroVector;
FVector BoundsExtent = FVector::ZeroVector;
Pawn->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent);
HalfHeightCm = FMath::Max(0.0f, BoundsExtent.Z);
RadiusCm = FMath::Max(BoundsExtent.X, BoundsExtent.Y);
}
if (!VoxelMath::IsFinite(HalfHeightCm) || !VoxelMath::IsFinite(RadiusCm)
|| HalfHeightCm <= 0.0f || RadiusCm < 0.0f)
{
return false;
}
const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition);
const float HalfHeightVoxels = HalfHeightCm / VOXEL_SIZE;
const float RadiusVoxels = RadiusCm / VOXEL_SIZE;
const float TileMinX = static_cast<float>(SupportTile.Coord.X * CHUNK_SIZE);
const float TileMaxX = static_cast<float>((SupportTile.Coord.X + 1) * CHUNK_SIZE);
const float TileMinY = static_cast<float>(SupportTile.Coord.Y * CHUNK_SIZE);
const float TileMaxY = static_cast<float>((SupportTile.Coord.Y + 1) * CHUNK_SIZE);
const float TileMinZ = static_cast<float>(SupportTile.Coord.Z * CHUNK_SIZE);
const float TileMaxZ = static_cast<float>((SupportTile.Coord.Z + 1) * CHUNK_SIZE);
const float CapsuleMinZ = LocalPosition.Z - HalfHeightVoxels;
const float CapsuleMaxZ = LocalPosition.Z + HalfHeightVoxels;
const bool bHorizontalOverlap = LocalPosition.X + RadiusVoxels > TileMinX
&& LocalPosition.X - RadiusVoxels < TileMaxX
&& LocalPosition.Y + RadiusVoxels > TileMinY
&& LocalPosition.Y - RadiusVoxels < TileMaxY;
const bool bVerticalOverlap = CapsuleMaxZ > TileMinZ && CapsuleMinZ < TileMaxZ;
if (!bHorizontalOverlap || !bVerticalOverlap)
{
return false;
}
if (OutPendingTile != nullptr)
{
*OutPendingTile = SupportTile;
}
return true;
}
void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile)
{
if (SupportTile.Level != 0
|| IsTileCollisionReady(SupportTile)
|| CollisionNotRequiredTiles.Contains(SupportTile))
{
return;
}
if (!bPawnGateSupportRequestReported
|| PawnGateRequestedSupportTile != SupportTile)
{
PawnGateRequestedSupportTile = SupportTile;
bPawnGateSupportRequestReported = false;
}
// The gate runs before UpdateChunksAroundPosition. If the floor has just moved outside the
// old desired stamp, ask that normal rebuild to make it the critical floor request rather than
// generating an orphan tile that the streaming policy could immediately cull.
if (DesiredStamp == 0 || !IsDesired(SupportTile))
{
bForceDesiredRebuild = true;
if (!bPawnGateSupportRequestReported)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] support_request_deferred tile=(%d,%d,%d) "
"reason=outside_desired_set"),
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z);
bPawnGateSupportRequestReported = true;
}
return;
}
// Preserve the existing critical floor ordering for the next submit pass, and make an
// immediate game-thread request when no mesh/cook is already in flight. LoadTile's true flag
// is the established BackgroundHigh streaming priority.
CriticalDesiredTiles.Remove(SupportTile);
CriticalDesiredTiles.Insert(SupportTile, 0);
if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile))
{
if (!bPawnGateSupportRequestReported)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] support_request_present tile=(%d,%d,%d) "
"pending=%d loaded=%d"),
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z,
PendingTiles.Contains(SupportTile) ? 1 : 0,
LoadedTiles.Contains(SupportTile) ? 1 : 0);
bPawnGateSupportRequestReported = true;
}
return;
}
const int32 PendingBefore = PendingTiles.Num();
LoadTile(SupportTile, /*bHighPriority*/ true);
if (PendingTiles.Num() > PendingBefore)
{
bAllChunksLoaded = false;
}
if (!bPawnGateSupportRequestReported)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] support_request tile=(%d,%d,%d) "
"submitted=%d priority=high"),
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z,
PendingTiles.Contains(SupportTile) ? 1 : 0);
bPawnGateSupportRequestReported = true;
}
}
void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition,
float DeltaTime) float DeltaTime)
{ {
static_cast<void>(DeltaTime);
if (!IsValid(Pawn) || bShuttingDown.load(std::memory_order_relaxed)) if (!IsValid(Pawn) || bShuttingDown.load(std::memory_order_relaxed))
{ {
return; return;
} }
// This is a startup snapshot of voxel.CollisionGate. The hot path never reads the CVar.
if (!bCollisionGateEnabled)
{
ReleasePawnCollisionGate();
return;
}
// Tick prerequisites are kept even while the gate is open. That makes the next movement // Tick prerequisites are kept even while the gate is open. That makes the next movement
// decision happen before pawn physics, including the first frame after a tile-boundary crossing. // decision happen before pawn physics, including the first frame after a tile-boundary crossing.
if (PawnTickPrerequisite.Get() != Pawn) if (PawnTickPrerequisite.Get() != Pawn)
@@ -3554,7 +4084,7 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
&& CollisionGatedCharacterMovement.Get() != CurrentMovement && CollisionGatedCharacterMovement.Get() != CurrentMovement
&& CollisionGatedPawnMovement.Get() != CurrentMovement) && CollisionGatedPawnMovement.Get() != CurrentMovement)
{ {
// The component was replaced while the pawn was stopped. Restore the old component (if it // The component was replaced while the pawn was held. Restore the old component (if it
// still exists) and let the normal path engage the replacement against the same readiness // still exists) and let the normal path engage the replacement against the same readiness
// decision below. // decision below.
ReleasePawnCollisionGate(); ReleasePawnCollisionGate();
@@ -3569,75 +4099,49 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
const TCHAR* CurrentSupportUnavailableReason = nullptr; const TCHAR* CurrentSupportUnavailableReason = nullptr;
const bool bCurrentSupportReady = IsPlayerSupportCollisionReady( const bool bCurrentSupportReady = IsPlayerSupportCollisionReady(
Pawn, PlayerPosition, &CurrentSupportTile, &CurrentSupportUnavailableReason); Pawn, PlayerPosition, &CurrentSupportTile, &CurrentSupportUnavailableReason);
bool bCanRelease = bCurrentSupportReady; if (CurrentSupportUnavailableReason != nullptr)
bool bWaitingForPredictedSupport = false;
FVoxelTileKey PredictedSupportTile;
const TCHAR* PredictedSupportUnavailableReason = nullptr;
// Check the movement path ahead while the gate is open. Without this, a pawn could leave a
// ready tile on this frame and enter an un-cooked tile before the next world tick observes it.
if (bCurrentSupportReady && DeltaTime > 0.0f)
{
const FVector Velocity = Pawn->GetVelocity();
if (!Velocity.IsNearlyZero(1.0f))
{
constexpr float SupportProbeSpacingCm = CHUNK_SIZE * VOXEL_SIZE * 0.5f;
constexpr int32 MaxPredictedSupportProbes = 64;
const int32 NumPredictedProbes = FMath::Clamp(
FMath::CeilToInt(Velocity.Size() * DeltaTime / SupportProbeSpacingCm),
1, MaxPredictedSupportProbes);
for (int32 ProbeIndex = 1; ProbeIndex <= NumPredictedProbes; ++ProbeIndex)
{
const float Alpha = static_cast<float>(ProbeIndex)
/ static_cast<float>(NumPredictedProbes);
const FVector PredictedPosition = PlayerPosition + Velocity * DeltaTime * Alpha;
const bool bPredictedSupportReady = IsPlayerSupportCollisionReady(
Pawn, PredictedPosition, &PredictedSupportTile,
&PredictedSupportUnavailableReason);
if (!bPredictedSupportReady)
{
bCanRelease = false;
bWaitingForPredictedSupport = true;
break;
}
}
}
}
const TCHAR* NonBlockingReason = CurrentSupportUnavailableReason != nullptr
? CurrentSupportUnavailableReason : PredictedSupportUnavailableReason;
if (NonBlockingReason != nullptr)
{ {
if (!bPawnGateUnavailableReported) if (!bPawnGateUnavailableReported)
{ {
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] nonblocking pawn=%s reason=%s " TEXT("[VoxelForgeCollisionGate] nonblocking pawn=%s reason=%s "
"current_reason=%s predicted_reason=%s current_support=(%d,%d,%d) " "support=(%d,%d,%d); tile will not be produced, no collision to wait for"),
"predicted_support=(%d,%d,%d); tile will not be produced, no collision to wait for"),
*Pawn->GetName(), *Pawn->GetName(),
NonBlockingReason, CurrentSupportUnavailableReason,
CurrentSupportUnavailableReason != nullptr
? CurrentSupportUnavailableReason : TEXT("none"),
PredictedSupportUnavailableReason != nullptr
? PredictedSupportUnavailableReason : TEXT("none"),
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y,
CurrentSupportTile.Coord.Z, CurrentSupportTile.Coord.Z);
PredictedSupportTile.Coord.X, PredictedSupportTile.Coord.Y,
PredictedSupportTile.Coord.Z);
} }
bPawnGateUnavailableReported = true; bPawnGateUnavailableReported = true;
ReleasePawnCollisionGate();
return;
} }
else else
{ {
bPawnGateUnavailableReported = false; bPawnGateUnavailableReported = false;
} }
if (bCanRelease) if (bCurrentSupportReady)
{ {
ReleasePawnCollisionGate(); ReleasePawnCollisionGate();
return; return;
} }
// An unresolved tile above the pawn is not a landing hazard. Let upward movement continue;
// only a downward/standing pawn needs its fall cancelled until the current support resolves.
if (const ACharacter* Character = Cast<ACharacter>(Pawn))
{
if (const UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
{
if (CharacterMovement->Velocity.Z > KINDA_SMALL_NUMBER)
{
ReleasePawnCollisionGate();
return;
}
}
}
RequestCollisionGateSupportTile(CurrentSupportTile);
constexpr double MaxCollisionGateWaitSeconds = 2.0; constexpr double MaxCollisionGateWaitSeconds = 2.0;
if (bPawnCollisionGateEngaged && PawnGateEngagedAtSeconds > 0.0) if (bPawnCollisionGateEngaged && PawnGateEngagedAtSeconds > 0.0)
{ {
@@ -3646,16 +4150,12 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
{ {
UE_LOG(LogTemp, Warning, UE_LOG(LogTemp, Warning,
TEXT("[VoxelForgeCollisionGate] wait_timeout pawn=%s waited_s=%.3f " TEXT("[VoxelForgeCollisionGate] wait_timeout pawn=%s waited_s=%.3f "
"current_ready=%d predicted_wait=%d current_support=(%d,%d,%d) " "current_ready=%d support=(%d,%d,%d); releasing safety gate"),
"predicted_support=(%d,%d,%d); releasing safety gate"),
*Pawn->GetName(), *Pawn->GetName(),
WaitSeconds, WaitSeconds,
bCurrentSupportReady ? 1 : 0, bCurrentSupportReady ? 1 : 0,
bWaitingForPredictedSupport ? 1 : 0,
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y,
CurrentSupportTile.Coord.Z, CurrentSupportTile.Coord.Z);
PredictedSupportTile.Coord.X, PredictedSupportTile.Coord.Y,
PredictedSupportTile.Coord.Z);
ReleasePawnCollisionGate(); ReleasePawnCollisionGate();
return; return;
} }
@@ -3665,11 +4165,10 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
{ {
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] waiting pawn=%s current_support=(%d,%d,%d) ready=%d " TEXT("[VoxelForgeCollisionGate] waiting pawn=%s current_support=(%d,%d,%d) ready=%d "
"predicted_wait=%d"), "hold_fall=1"),
*Pawn->GetName(), *Pawn->GetName(),
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z, CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z,
bCurrentSupportReady ? 1 : 0, bCurrentSupportReady ? 1 : 0);
bWaitingForPredictedSupport ? 1 : 0);
} }
EngagePawnCollisionGate(Pawn); EngagePawnCollisionGate(Pawn);
} }
@@ -3682,6 +4181,29 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
} }
if (bPawnCollisionGateEngaged) if (bPawnCollisionGateEngaged)
{ {
// Re-apply the vertical hold every game-thread sample. Movement input and horizontal
// velocity are intentionally untouched.
if (UCharacterMovementComponent* CharacterMovement =
CollisionGatedCharacterMovement.Get())
{
FVector Velocity = CharacterMovement->Velocity;
if (Velocity.Z < 0.0f)
{
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
CharacterMovement->UpdateComponentVelocity();
}
}
else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get())
{
FVector Velocity = PawnMovement->Velocity;
if (Velocity.Z < 0.0f)
{
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
}
}
return; return;
} }
@@ -3692,12 +4214,17 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
{ {
CollisionGatedCharacterMovement = CharacterMovement; CollisionGatedCharacterMovement = CharacterMovement;
SavedCharacterMovementMode = static_cast<uint8>(CharacterMovement->MovementMode); SavedCharacterGravityScale = CharacterMovement->GravityScale;
SavedCharacterCustomMovementMode = CharacterMovement->CustomMovementMode; bSavedCharacterGravityScale = true;
bSavedCharacterMovementMode = true;
CharacterMovement->StopMovementImmediately(); FVector Velocity = CharacterMovement->Velocity;
CharacterMovement->SetMovementMode(MOVE_None); if (Velocity.Z < 0.0f)
{
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
}
CharacterMovement->GravityScale = 0.0f;
CharacterMovement->UpdateComponentVelocity();
bPawnCollisionGateEngaged = true; bPawnCollisionGateEngaged = true;
PawnGateEngagedAtSeconds = FPlatformTime::Seconds(); PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
return; return;
@@ -3707,9 +4234,13 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
if (UPawnMovementComponent* PawnMovement = Pawn->GetMovementComponent()) if (UPawnMovementComponent* PawnMovement = Pawn->GetMovementComponent())
{ {
CollisionGatedPawnMovement = PawnMovement; CollisionGatedPawnMovement = PawnMovement;
bSavedPawnMovementActive = PawnMovement->IsActive(); FVector Velocity = PawnMovement->Velocity;
PawnMovement->StopMovementImmediately(); if (Velocity.Z < 0.0f)
PawnMovement->Deactivate(); {
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
}
bPawnCollisionGateEngaged = true; bPawnCollisionGateEngaged = true;
PawnGateEngagedAtSeconds = FPlatformTime::Seconds(); PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
return; return;
@@ -3732,44 +4263,37 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedPawn.Reset(); CollisionGatedPawn.Reset();
CollisionGatedPawnMovement.Reset(); CollisionGatedPawnMovement.Reset();
CollisionGatedCharacterMovement.Reset(); CollisionGatedCharacterMovement.Reset();
bSavedCharacterMovementMode = false; bSavedCharacterGravityScale = false;
bSavedPawnMovementActive = false; bPawnGateSupportRequestReported = false;
PawnGateEngagedAtSeconds = 0.0; PawnGateEngagedAtSeconds = 0.0;
return; return;
} }
if (UCharacterMovementComponent* CharacterMovement = CollisionGatedCharacterMovement.Get()) if (UCharacterMovementComponent* CharacterMovement = CollisionGatedCharacterMovement.Get())
{ {
if (bSavedCharacterMovementMode) if (bSavedCharacterGravityScale)
{ {
CharacterMovement->SetMovementMode( CharacterMovement->GravityScale = SavedCharacterGravityScale;
static_cast<EMovementMode>(SavedCharacterMovementMode), CharacterMovement->UpdateComponentVelocity();
SavedCharacterCustomMovementMode);
}
}
else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get())
{
if (bSavedPawnMovementActive)
{
PawnMovement->Activate(true);
}
else
{
PawnMovement->Deactivate();
} }
} }
if (APawn* Pawn = CollisionGatedPawn.Get()) if (APawn* Pawn = CollisionGatedPawn.Get())
{ {
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] released pawn=%s"), *Pawn->GetName()); const double HoldSeconds = PawnGateEngagedAtSeconds > 0.0
? FMath::Max(0.0, FPlatformTime::Seconds() - PawnGateEngagedAtSeconds)
: 0.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] released pawn=%s hold_s=%.6f"),
*Pawn->GetName(), HoldSeconds);
} }
CollisionGatedPawn.Reset(); CollisionGatedPawn.Reset();
CollisionGatedPawnMovement.Reset(); CollisionGatedPawnMovement.Reset();
CollisionGatedCharacterMovement.Reset(); CollisionGatedCharacterMovement.Reset();
bSavedCharacterMovementMode = false; bSavedCharacterGravityScale = false;
bSavedPawnMovementActive = false;
bPawnCollisionGateEngaged = false; bPawnCollisionGateEngaged = false;
PawnGateEngagedAtSeconds = 0.0; PawnGateEngagedAtSeconds = 0.0;
bPawnGateSupportRequestReported = false;
} }
void AVoxelWorld::BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh) void AVoxelWorld::BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh)
+46 -9
View File
@@ -522,6 +522,9 @@ private:
void ConfigureHeadlessStreamingTest(); void ConfigureHeadlessStreamingTest();
void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition, void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn); FVector& InOutPlayerHeading, APawn* PlayerPawn);
void AdvanceHeadlessCollisionGateStressTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn);
void ObserveHeadlessCollisionGateStressTest(const FVector& PlayerPosition, APawn* PlayerPawn);
void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition, void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn); FVector& InOutPlayerHeading, APawn* PlayerPawn);
void InitializeHeadlessStrateCrossingTest(); void InitializeHeadlessStrateCrossingTest();
@@ -530,6 +533,7 @@ private:
void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn); void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn);
void MaybeFinishHeadlessStrateCrossingTest(); void MaybeFinishHeadlessStrateCrossingTest();
void MaybeFinishHeadlessSurfaceFallTest(); void MaybeFinishHeadlessSurfaceFallTest();
void MaybeFinishHeadlessCollisionGateStressTest();
void MaybeFinishHeadlessStreamingTest(); void MaybeFinishHeadlessStreamingTest();
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi, int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
@@ -835,9 +839,12 @@ public:
bool IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition, bool IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition,
FVoxelTileKey* OutSupportTile = nullptr, FVoxelTileKey* OutSupportTile = nullptr,
const TCHAR** OutUnavailableReason = nullptr) const; const TCHAR** OutUnavailableReason = nullptr) const;
bool IsPawnCapsuleOverlappingPendingFloor(APawn* Pawn, const FVector& PlayerPosition,
FVoxelTileKey* OutPendingTile = nullptr) const;
void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile);
/** Gate pawn movement until its current and, when moving, next support tile have a completed /** Cancel only downward pawn motion while the tile beneath its feet is unresolved. Horizontal
* level-0 RMC collision body. The world tick is installed as the pawn prerequisite in the * input and movement remain live. The world tick is installed as the pawn prerequisite in the
* implementation (and directly on its movement component) so this check runs before * implementation (and directly on its movement component) so this check runs before
* CharacterMovementComponent physics. */ * CharacterMovementComponent physics. */
void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime); void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime);
@@ -1004,6 +1011,35 @@ public:
double HeadlessStreamingTestBeginSeconds = 0.0; double HeadlessStreamingTestBeginSeconds = 0.0;
double HeadlessStreamingTestLastElapsedSeconds = 0.0; double HeadlessStreamingTestLastElapsedSeconds = 0.0;
// Explicit high-speed collision-gate repro. The streamer stays centred at the launch tile while
// the character uses ordinary swept movement across the still-pending desired set. The test
// distinguishes a gate hold from a full horizontal stop so a fall-only gate can be validated.
bool bHeadlessCollisionGateStressTest = false;
bool bHeadlessCollisionGateStressTestStartPlaced = false;
bool bHeadlessCollisionGateStressTestStarted = false;
bool bHeadlessCollisionGateStressTestPassed = false;
bool bHeadlessCollisionGateStressTestFailed = false;
bool bHeadlessCollisionGateStressTestExitRequested = false;
bool bHeadlessCollisionGateStressTestGateWasEngaged = false;
bool bHeadlessCollisionGateStressTestFullStop = false;
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
float HeadlessCollisionGateStressTestStartDelaySeconds = 0.1f;
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
double HeadlessCollisionGateStressTestBeginSeconds = 0.0;
double HeadlessCollisionGateStressTestStartSeconds = 0.0;
double HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
double HeadlessCollisionGateStressTestDistanceCm = 0.0;
double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
double HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0;
int32 HeadlessCollisionGateStressTestGateHoldCount = 0;
int32 HeadlessCollisionGateStressTestFullStopCount = 0;
FVector HeadlessCollisionGateStressTestDirection = FVector::XAxisVector;
FVector HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector;
FString HeadlessCollisionGateStressTestFailureReason;
// Explicit owner-layout collision-gate repro. This is a command-line-only harness path: it // Explicit owner-layout collision-gate repro. This is a command-line-only harness path: it
// places the possessed character above the authored single-strate surface and lets ordinary // places the possessed character above the authored single-strate surface and lets ordinary
// CharacterMovement gravity carry it to the generated ground. // CharacterMovement gravity carry it to the generated ground.
@@ -1046,6 +1082,7 @@ public:
bool bHeadlessStrateCrossingTestExitRequested = false; bool bHeadlessStrateCrossingTestExitRequested = false;
bool bHeadlessStrateCrossingTestGateWasEngaged = false; bool bHeadlessStrateCrossingTestGateWasEngaged = false;
bool bHeadlessStrateCrossingTestInitialSupportReported = false; bool bHeadlessStrateCrossingTestInitialSupportReported = false;
bool bHeadlessStrateCrossingTestPendingFloorOverlap = false;
int32 HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
int32 HeadlessStrateCrossingTestPassageIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
int32 HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE; int32 HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
@@ -1068,22 +1105,22 @@ public:
FString HeadlessStrateCrossingTestFailureReason; FString HeadlessStrateCrossingTestFailureReason;
TArray<FVector> HeadlessStrateCrossingTestRoute; TArray<FVector> HeadlessStrateCrossingTestRoute;
// PLAYER COLLISION GATE — the pawn is allowed to enter/leave a tile only when the level-0 // PLAYER COLLISION GATE — only the downward component is held while the level-0 collision body
// collision body covering its feet has completed the RMC cook. The movement component state is // beneath the pawn's feet is unresolved. Horizontal movement and player control remain live.
// saved so a temporary stream wait does not permanently change a character's movement mode.
TWeakObjectPtr<APawn> PawnTickPrerequisite; TWeakObjectPtr<APawn> PawnTickPrerequisite;
TWeakObjectPtr<UPawnMovementComponent> PawnMovementTickPrerequisite; TWeakObjectPtr<UPawnMovementComponent> PawnMovementTickPrerequisite;
TWeakObjectPtr<APawn> CollisionGatedPawn; TWeakObjectPtr<APawn> CollisionGatedPawn;
TWeakObjectPtr<UPawnMovementComponent> CollisionGatedPawnMovement; TWeakObjectPtr<UPawnMovementComponent> CollisionGatedPawnMovement;
TWeakObjectPtr<UCharacterMovementComponent> CollisionGatedCharacterMovement; TWeakObjectPtr<UCharacterMovementComponent> CollisionGatedCharacterMovement;
uint8 SavedCharacterMovementMode = 0; bool bCollisionGateEnabled = true;
uint8 SavedCharacterCustomMovementMode = 0; float SavedCharacterGravityScale = 1.0f;
bool bSavedCharacterMovementMode = false; bool bSavedCharacterGravityScale = false;
bool bSavedPawnMovementActive = false;
bool bPawnCollisionGateEngaged = false; bool bPawnCollisionGateEngaged = false;
double PawnGateEngagedAtSeconds = 0.0; double PawnGateEngagedAtSeconds = 0.0;
bool bPawnGateUnavailableReported = false; bool bPawnGateUnavailableReported = false;
bool bPawnGateUnsupportedReported = false; bool bPawnGateUnsupportedReported = false;
FVoxelTileKey PawnGateRequestedSupportTile;
bool bPawnGateSupportRequestReported = false;
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée. // Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
// BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp // BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp
+52 -3
View File
@@ -1,6 +1,6 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'surface-fall', 'crossing', 'parity')] [ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity')]
[string]$Scenario = 'canonical', [string]$Scenario = 'canonical',
[switch]$Build, [switch]$Build,
[ValidateSet('owner', 'default')] [ValidateSet('owner', 'default')]
@@ -45,6 +45,7 @@ $CommandletMetrics = [ordered]@{}
$GameMetrics = [ordered]@{} $GameMetrics = [ordered]@{}
$SurfaceFallMetrics = [ordered]@{ requested = $Scenario -in @('tests', 'surface-fall'); status = 'not_run' } $SurfaceFallMetrics = [ordered]@{ requested = $Scenario -in @('tests', 'surface-fall'); status = 'not_run' }
$StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' } $StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' }
$CollisionGateStressMetrics = [ordered]@{ requested = $Scenario -in @('tests', 'gate-stress'); status = 'not_run' }
$TestMetrics = @() $TestMetrics = @()
$ParityEvidence = [ordered]@{ requested = $Scenario -eq 'parity'; status = 'not_run' } $ParityEvidence = [ordered]@{ requested = $Scenario -eq 'parity'; status = 'not_run' }
$DllEvidence = [ordered]@{ $DllEvidence = [ordered]@{
@@ -426,7 +427,7 @@ function Sync-OwnerAssets {
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru $null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
[void]$rows.Add([ordered]@{ [void]$rows.Add([ordered]@{
name = $assetName name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing') used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress')
source_path = $source source_path = $source
source_sha256 = Get-Sha256 $source source_sha256 = Get-Sha256 $source
staged_path = $target staged_path = $target
@@ -474,7 +475,7 @@ function Get-AssetEvidence {
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset" $target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
[void]$rows.Add([ordered]@{ [void]$rows.Add([ordered]@{
name = $assetName name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing') used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress')
source_path = $source source_path = $source
source_sha256 = Get-Sha256 $source source_sha256 = Get-Sha256 $source
staged_path = $target staged_path = $target
@@ -691,6 +692,23 @@ function Get-StrateCrossingMetrics([string]$LogPath) {
return $metrics return $metrics
} }
function Get-CollisionGateStressMetrics([string]$LogPath) {
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeCollisionGateStressTest\] result=' -ErrorAction SilentlyContinue |
Select-Object -Last 1)
if ($lineMatch.Count -eq 0) { return [ordered]@{ status = 'missing'; log = $LogPath } }
$line = [string]$lineMatch[0].Line
$metrics = [ordered]@{ status = 'failed'; log = $LogPath; raw = $line }
$result = [regex]::Match($line, 'result=(?<v>PASS|FAIL)')
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
foreach ($key in @('reason', 'elapsed_s', 'distance_m', 'expected_distance_m', 'speed_cm_s',
'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops',
'full_stop_total_s', 'collision_gate')) {
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
if ($match.Success) { $metrics[$key] = $match.Groups['v'].Value }
}
return $metrics
}
function Get-TestMetrics($Ledger) { function Get-TestMetrics($Ledger) {
if ($null -eq $Ledger) { return @() } if ($null -eq $Ledger) { return @() }
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object { $logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
@@ -980,6 +998,20 @@ try {
Add-Failure "strate-crossing test did not pass: $($StrateCrossingMetrics.status) $($StrateCrossingMetrics.reason)" Add-Failure "strate-crossing test did not pass: $($StrateCrossingMetrics.status) $($StrateCrossingMetrics.reason)"
} }
} }
'gate-stress' {
$baseCvars['voxel.TestCollisionGateStress'] = 1
$baseCvars['voxel.TestCollisionGateStressSpeedCmPerSecond'] = 6000
$baseCvars['voxel.TestCollisionGateStressStartDelaySeconds'] = 0.1
$baseCvars['voxel.TestCollisionGateStressDurationSeconds'] = 2
$baseCvars['voxel.TestCollisionGateStressTimeoutSeconds'] = 30
$args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\gate-stress.log') (Merge-Cvars $baseCvars)
$gameLedger = Invoke-UnrealLaunch 'collision-gate-stress-game' $args `
(Join-Path $RunRoot 'Logs\gate-stress.log') (Join-Path $RunRoot 'Logs\gate-stress.stdout.log') (Join-Path $RunRoot 'Logs\gate-stress.stderr.log')
$CollisionGateStressMetrics = Get-CollisionGateStressMetrics (Join-Path $RunRoot 'Logs\gate-stress.log')
if ($CollisionGateStressMetrics.status -ne 'passed') {
Add-Failure "collision-gate stress repro did not pass: $($CollisionGateStressMetrics.status) $($CollisionGateStressMetrics.reason)"
}
}
'tests' { 'tests' {
$testPath = if ([string]::IsNullOrWhiteSpace($TestFilter)) { 'VoxelForge' } else { $TestFilter } $testPath = if ([string]::IsNullOrWhiteSpace($TestFilter)) { 'VoxelForge' } else { $TestFilter }
$baseCvars['ExecCmds'] = "Automation RunTests $testPath; Quit" $baseCvars['ExecCmds'] = "Automation RunTests $testPath; Quit"
@@ -1018,6 +1050,21 @@ try {
if ($SurfaceFallMetrics.status -ne 'passed') { if ($SurfaceFallMetrics.status -ne 'passed') {
Add-Failure "surface-fall repro did not pass: $($SurfaceFallMetrics.status) $($SurfaceFallMetrics.reason)" Add-Failure "surface-fall repro did not pass: $($SurfaceFallMetrics.status) $($SurfaceFallMetrics.reason)"
} }
$stressBaseCvars = [ordered]@{
'voxel.TestCollisionGateStress' = 1
'voxel.TestCollisionGateStressSpeedCmPerSecond' = 6000
'voxel.TestCollisionGateStressStartDelaySeconds' = 0.1
'voxel.TestCollisionGateStressDurationSeconds' = 2
'voxel.TestCollisionGateStressTimeoutSeconds' = 30
}
$stressArgs = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\gate-stress.log') (Merge-Cvars $stressBaseCvars)
$stressLedger = Invoke-UnrealLaunch 'collision-gate-stress-game' $stressArgs `
(Join-Path $RunRoot 'Logs\gate-stress.log') (Join-Path $RunRoot 'Logs\gate-stress.stdout.log') (Join-Path $RunRoot 'Logs\gate-stress.stderr.log')
$CollisionGateStressMetrics = Get-CollisionGateStressMetrics (Join-Path $RunRoot 'Logs\gate-stress.log')
if ($CollisionGateStressMetrics.status -ne 'passed') {
Add-Failure "collision-gate stress repro did not pass: $($CollisionGateStressMetrics.status) $($CollisionGateStressMetrics.reason)"
}
} }
'parity' { 'parity' {
$parityArgs = New-CommonArguments 'commandlet' (Join-Path $RunRoot 'Logs\parity-commandlet.log') (Merge-Cvars $baseCvars) $parityArgs = New-CommonArguments 'commandlet' (Join-Path $RunRoot 'Logs\parity-commandlet.log') (Merge-Cvars $baseCvars)
@@ -1110,6 +1157,7 @@ $result = [ordered]@{
game = $GameMetrics game = $GameMetrics
surface_fall = $SurfaceFallMetrics surface_fall = $SurfaceFallMetrics
strate_crossing = $StrateCrossingMetrics strate_crossing = $StrateCrossingMetrics
collision_gate_stress = $CollisionGateStressMetrics
worker_seconds = [ordered]@{ worker_seconds = [ordered]@{
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null } commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
game = if ($GameMetrics.Contains('worker_generation_sum_s')) { $GameMetrics.worker_generation_sum_s } else { $null } game = if ($GameMetrics.Contains('worker_generation_sum_s')) { $GameMetrics.worker_generation_sum_s } else { $null }
@@ -1141,6 +1189,7 @@ try {
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)") [void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)") [void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)") [void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
[void]$summaryLines.Add("collision_gate_stress=$(ConvertTo-CompactJson $result.collision_gate_stress)")
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)") [void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)") [void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")
[void]$summaryLines.Add("p50_p95=$(ConvertTo-CompactJson $result.p50_p95)") [void]$summaryLines.Add("p50_p95=$(ConvertTo-CompactJson $result.p50_p95)")