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:
1 parent
0dd45ca410
commit
88f3e44402
3 files changed
+719
-109
No files matched your search
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)")
|
||||||
|
|||||||
Reference in new issue
Block a user