test(gate): stress repro judges terrain penetration -- gate on 10/10 clear, gate off 3/3 falls through

The old criteria (travel distance) could not tell a working gate from a broken one: every run,
gate on or off, ended at (455.7, 0, -58.1). That spot is measured air ~1.2 m above a real floor
(density zero-crossing at world Z ~ -275.7 cm), so it is a terrain trap, not a fall-through.

The stress test now drops the pawn fast onto an unready support tile (0,0,-1) and samples the
density at the capsule feet every frame, sweeping at <= half a voxel between positions. PASS =
no measured clearance below -5 cm.

Owner config (radius 3, max level 6, distance 768, sheets off):
- gate on: 10/10 PASS, min clearance +120.5..+120.9 cm, one hold of 0.43-0.47 s each,
  support submitted in ~0.7 ms, collision ready in 0.43-0.47 s
- gate off: 3/3 FAIL terrain_penetration, clearance -5.7..-8.1 cm
Edit-ghost, clipmap no-overlap sweep: pass. Test-harness code only; no production path changed.
This validates a49725d.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-25 06:11:08 +02:00
1 parent a49725d365
commit 6ee433efa5
4 files changed
+657 -68

No files matched your search

+2 -1
View File
@@ -114,7 +114,7 @@ remains `0`, and lateral regions remain gated off.
|------|------| |------|------|
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. | | `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|parity|horizon|horizon-render|determinism`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, and `-SharedDDC`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, and emits compact `result.json`/`summary.txt` plus full logs. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`. | | `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|parity|horizon|horizon-render|determinism`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, and `-SharedDDC`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, and emits compact `result.json`/`summary.txt` plus full logs. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`. |
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | | `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. | | `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. |
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. | | `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
@@ -311,6 +311,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. | | `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. | | `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
| `UpdateChunksAroundPosition` | 362 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. | | `UpdateChunksAroundPosition` | 362 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The `GetDensityAt` column at capsule feet defines the local air/solid surface; each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; support submission/readiness, travel, and holds are diagnostics. Result logs include the terminal density column and the first crossing below spawn height. |
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. | | `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. |
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. | | `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. | | `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
+628 -63
View File
@@ -34,6 +34,7 @@
#include "GameFramework/Character.h" #include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/CharacterMovementComponent.h"
#include "Components/CapsuleComponent.h" #include "Components/CapsuleComponent.h"
#include "Components/PrimitiveComponent.h"
#include "Async/Async.h" #include "Async/Async.h"
#include "Materials/MaterialInstanceDynamic.h" #include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialParameterCollection.h" #include "Materials/MaterialParameterCollection.h"
@@ -1890,12 +1891,17 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessCollisionGateStressTestExitRequested = false; bHeadlessCollisionGateStressTestExitRequested = false;
bHeadlessCollisionGateStressTestGateWasEngaged = false; bHeadlessCollisionGateStressTestGateWasEngaged = false;
bHeadlessCollisionGateStressTestFullStop = false; bHeadlessCollisionGateStressTestFullStop = false;
bHeadlessCollisionGateStressTestStartHeightCrossed = false;
bHeadlessCollisionGateStressTestTerrainViolation = false;
bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false;
bHeadlessCollisionGateStressTestLanded = false;
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false;
HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f; HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
HeadlessCollisionGateStressTestStartDelaySeconds = 0.1f;
HeadlessCollisionGateStressTestDurationSeconds = 2.0f; HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f; HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
HeadlessCollisionGateStressTestBeginSeconds = 0.0; HeadlessCollisionGateStressTestBeginSeconds = 0.0;
HeadlessCollisionGateStressTestStartSeconds = 0.0; HeadlessCollisionGateStressTestStartSeconds = 0.0;
HeadlessCollisionGateStressTestLandedSeconds = 0.0;
HeadlessCollisionGateStressTestGateStartSeconds = 0.0; HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
HeadlessCollisionGateStressTestDistanceCm = 0.0; HeadlessCollisionGateStressTestDistanceCm = 0.0;
@@ -1903,10 +1909,21 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0; HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0; HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0; HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0;
HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = 0.0;
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = 0.0;
HeadlessCollisionGateStressTestLaunchSupportReadySeconds = 0.0;
HeadlessCollisionGateStressTestMinClearanceCm = 1.0e30;
HeadlessCollisionGateStressTestMinFeetDensity = 0.0f;
HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = 0.0f;
HeadlessCollisionGateStressTestTerrainSampleCount = 0;
HeadlessCollisionGateStressTestTraceFrame = 0;
HeadlessCollisionGateStressTestGateHoldCount = 0; HeadlessCollisionGateStressTestGateHoldCount = 0;
HeadlessCollisionGateStressTestFullStopCount = 0; HeadlessCollisionGateStressTestFullStopCount = 0;
HeadlessCollisionGateStressTestDirection = FVector::XAxisVector; HeadlessCollisionGateStressTestDirection = FVector::ZeroVector;
HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector; HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector;
HeadlessCollisionGateStressTestStartPosition = FVector::ZeroVector;
HeadlessCollisionGateStressTestMinClearancePosition = FVector::ZeroVector;
HeadlessCollisionGateStressTestLaunchSupportTile = FVoxelTileKey();
HeadlessCollisionGateStressTestFailureReason.Reset(); HeadlessCollisionGateStressTestFailureReason.Reset();
bHeadlessSurfaceFallTest = false; bHeadlessSurfaceFallTest = false;
bHeadlessSurfaceFallTestStartPlaced = false; bHeadlessSurfaceFallTestStartPlaced = false;
@@ -2188,19 +2205,15 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
*HeadlessCollisionGateStressTestFailureReason); *HeadlessCollisionGateStressTestFailureReason);
} }
float StressSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond; float StressSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond;
float StressStartDelay = HeadlessCollisionGateStressTestStartDelaySeconds;
float StressDuration = HeadlessCollisionGateStressTestDurationSeconds; float StressDuration = HeadlessCollisionGateStressTestDurationSeconds;
float StressTimeout = HeadlessCollisionGateStressTestTimeoutSeconds; float StressTimeout = HeadlessCollisionGateStressTestTimeoutSeconds;
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpeedCmPerSecond="), FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpeedCmPerSecond="),
StressSpeed); StressSpeed);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressStartDelaySeconds="),
StressStartDelay);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDurationSeconds="), FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDurationSeconds="),
StressDuration); StressDuration);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressTimeoutSeconds="), FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressTimeoutSeconds="),
StressTimeout); StressTimeout);
HeadlessCollisionGateStressTestSpeedCmPerSecond = FMath::Max(StressSpeed, 1.0f); HeadlessCollisionGateStressTestSpeedCmPerSecond = FMath::Max(StressSpeed, 1.0f);
HeadlessCollisionGateStressTestStartDelaySeconds = FMath::Max(StressStartDelay, 0.0f);
HeadlessCollisionGateStressTestDurationSeconds = FMath::Max(StressDuration, 0.25f); HeadlessCollisionGateStressTestDurationSeconds = FMath::Max(StressDuration, 0.25f);
HeadlessCollisionGateStressTestTimeoutSeconds = FMath::Max(StressTimeout, 1.0f); HeadlessCollisionGateStressTestTimeoutSeconds = FMath::Max(StressTimeout, 1.0f);
// The stress path owns movement and keeps the streamer centred at the launch tile. Do // The stress path owns movement and keeps the streamer centred at the launch tile. Do
@@ -2210,9 +2223,8 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessCollisionGateStressTestBeginSeconds = 0.0; HeadlessCollisionGateStressTestBeginSeconds = 0.0;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] configured=1 speed_cm_s=%.1f " TEXT("[VoxelForgeCollisionGateStressTest] configured=1 speed_cm_s=%.1f "
"start_delay_s=%.3f duration_s=%.3f timeout_s=%.3f"), "duration_after_landing_s=%.3f timeout_s=%.3f"),
HeadlessCollisionGateStressTestSpeedCmPerSecond, HeadlessCollisionGateStressTestSpeedCmPerSecond,
HeadlessCollisionGateStressTestStartDelaySeconds,
HeadlessCollisionGateStressTestDurationSeconds, HeadlessCollisionGateStressTestDurationSeconds,
HeadlessCollisionGateStressTestTimeoutSeconds); HeadlessCollisionGateStressTestTimeoutSeconds);
} }
@@ -2926,6 +2938,72 @@ void AVoxelWorld::AdvanceHeadlessStreamingTest(
} }
} }
bool AVoxelWorld::MeasureHeadlessCollisionGateStressTerrain(
const FVector& WorldPosition, float CapsuleHalfHeightCm,
float& OutFeetDensity, float& OutCenterDensity,
float& OutSurfaceLocalVoxelZ, float& OutClearanceCm) const
{
OutFeetDensity = 0.0f;
OutCenterDensity = 0.0f;
OutSurfaceLocalVoxelZ = 0.0f;
OutClearanceCm = 0.0f;
if (Generator == nullptr || !VoxelMath::IsFinite(CapsuleHalfHeightCm)
|| CapsuleHalfHeightCm < 0.0f)
{
return false;
}
const FVector LocalPosition = WorldToLocalVoxel(WorldPosition);
const float FeetZ = LocalPosition.Z - CapsuleHalfHeightCm / VOXEL_SIZE;
OutCenterDensity = Generator->GetDensityAt(
LocalPosition.X, LocalPosition.Y, LocalPosition.Z);
OutFeetDensity = Generator->GetDensityAt(
LocalPosition.X, LocalPosition.Y, FeetZ);
if (!VoxelMath::IsFinite(OutCenterDensity) || !VoxelMath::IsFinite(OutFeetDensity))
{
return false;
}
// The density field is the authoritative terrain oracle (negative = solid). Find the
// nearest air/solid zero crossing adjacent to the capsule's lowest point, then linearly
// interpolate it. Searching up when the feet sample is solid reports negative clearance
// instead of silently choosing a lower floor after a fall-through.
constexpr float SearchStepVoxels = 0.5f;
constexpr int32 MaxSearchSteps = 256; // 128 voxels, enough to cover the local landing band.
float LastZ = FeetZ;
float LastDensity = OutFeetDensity;
const bool bSearchDown = OutFeetDensity >= 0.0f;
for (int32 Step = 1; Step <= MaxSearchSteps; ++Step)
{
const float SampleZ = FeetZ + (bSearchDown ? -1.0f : 1.0f)
* SearchStepVoxels * static_cast<float>(Step);
const float SampleDensity = Generator->GetDensityAt(
LocalPosition.X, LocalPosition.Y, SampleZ);
if (!VoxelMath::IsFinite(SampleDensity))
{
return false;
}
const bool bCrossedSurface = bSearchDown
? (LastDensity >= 0.0f && SampleDensity < 0.0f)
: (LastDensity <= 0.0f && SampleDensity > 0.0f);
if (bCrossedSurface)
{
const float Denominator = LastDensity - SampleDensity;
const float Alpha = FMath::IsNearlyZero(Denominator)
? 0.5f
: FMath::Clamp(LastDensity / Denominator, 0.0f, 1.0f);
OutSurfaceLocalVoxelZ = FMath::Lerp(LastZ, SampleZ, Alpha);
OutClearanceCm = (FeetZ - OutSurfaceLocalVoxelZ) * VOXEL_SIZE;
return true;
}
LastZ = SampleZ;
LastDensity = SampleDensity;
}
return false;
}
void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest( void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn) FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn)
{ {
@@ -2938,22 +3016,121 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
const double Now = FPlatformTime::Seconds(); const double Now = FPlatformTime::Seconds();
if (!bHeadlessCollisionGateStressTestStartPlaced) if (!bHeadlessCollisionGateStressTestStartPlaced)
{ {
FVoxelTileKey SupportTile; UE_LOG(LogTemp, Display,
const TCHAR* SupportUnavailableReason = nullptr; TEXT("[VoxelForgeCollisionGateStressTest] owner_settings clip_radius=%d "
const bool bSupportReady = IsPlayerSupportCollisionReady( "max_clip_level=%d render_distance_chunks=%d far_sheet_ring=%d"),
PlayerPawn, InOutPlayerPosition, &SupportTile, &SupportUnavailableReason); Settings != nullptr ? Settings->ClipRadius : -1,
Settings != nullptr ? Settings->MaxClipLevel : -1,
Settings != nullptr ? Settings->RenderDistanceChunks : -1,
Settings != nullptr && Settings->bFarSheetRing ? 1 : 0);
float CapsuleHalfHeightCm = 0.0f;
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
{
CapsuleHalfHeightCm = Capsule->GetScaledCapsuleHalfHeight();
}
}
// The earlier horizontal route ran through an open origin drop and ended more than a
// metre above its floor. Start above the measured density surface at this XY so the pawn
// must fall onto a freshly requested level-0 tile before the horizontal diagnostic.
constexpr float LaunchLocalVoxelX = 18.2f;
constexpr float LaunchLocalVoxelY = 0.0f;
constexpr float LaunchHeightAboveGroundVoxels = 10.0f;
const FVector SurfaceProbeWorld = LocalVoxelToWorld(
FVector(LaunchLocalVoxelX, LaunchLocalVoxelY, 0.0f));
float ProbeFeetDensity = 0.0f;
float ProbeCenterDensity = 0.0f;
float SurfaceLocalVoxelZ = 0.0f;
float ProbeClearanceCm = 0.0f;
const bool bSurfaceFound = MeasureHeadlessCollisionGateStressTerrain(
SurfaceProbeWorld, CapsuleHalfHeightCm, ProbeFeetDensity,
ProbeCenterDensity, SurfaceLocalVoxelZ, ProbeClearanceCm);
if (!bSurfaceFound)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason =
TEXT("stress_launch_surface_unavailable");
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed "
"reason=stress_launch_surface_unavailable probe=(%.2f,%.2f,%.2f) "
"feet_density=%.6f"),
SurfaceProbeWorld.X, SurfaceProbeWorld.Y, SurfaceProbeWorld.Z,
ProbeFeetDensity);
return;
}
const FVector LaunchVoxel(
LaunchLocalVoxelX,
LaunchLocalVoxelY,
SurfaceLocalVoxelZ + CapsuleHalfHeightCm / VOXEL_SIZE
+ LaunchHeightAboveGroundVoxels);
const FVector RequestedLaunchWorld = LocalVoxelToWorld(LaunchVoxel);
const bool bPlaced = PlayerPawn->SetActorLocation(
RequestedLaunchWorld, false, nullptr, ETeleportType::TeleportPhysics);
if (!bPlaced)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("stress_launch_placement_failed");
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed "
"reason=stress_launch_placement_failed requested=(%.2f,%.2f,%.2f)"),
RequestedLaunchWorld.X, RequestedLaunchWorld.Y, RequestedLaunchWorld.Z);
return;
}
const FVector ActualPosition = PlayerPawn->GetActorLocation(); const FVector ActualPosition = PlayerPawn->GetActorLocation();
const FVector LaunchVoxel = WorldToLocalVoxel(ActualPosition); const FVector ActualLaunchVoxel = WorldToLocalVoxel(ActualPosition);
// Freeze only the streaming centre, not the pawn. This deliberately leaves the desired // Keep streaming centred on the launch footprint while the character drops and later
// clipmap behind the run while the launch support is already ready. // moves horizontally. The support tile below the capsule is still not cooked at launch.
bHeadlessStreamingTestCenterOverride = true; bHeadlessStreamingTestCenterOverride = true;
HeadlessStreamingTestCenterVoxel = LaunchVoxel; HeadlessStreamingTestCenterVoxel = ActualLaunchVoxel;
InOutPlayerPosition = ActualPosition; InOutPlayerPosition = ActualPosition;
InOutPlayerHeading = FVector::ZeroVector; InOutPlayerHeading = FVector::ZeroVector;
HeadlessCollisionGateStressTestLastActualPosition = ActualPosition; HeadlessCollisionGateStressTestLastActualPosition = ActualPosition;
HeadlessCollisionGateStressTestStartPosition = ActualPosition;
HeadlessCollisionGateStressTestDirection = FVector::ZeroVector;
bHeadlessCollisionGateStressTestLanded = false;
FVoxelTileKey FootTile;
const bool bHaveFootTile = GetPlayerSupportTile(PlayerPawn, ActualPosition, FootTile);
FVoxelTileKey GateSupportTile;
const TCHAR* SupportUnavailableReason = nullptr;
const bool bSupportReady = IsPlayerSupportCollisionReady(
PlayerPawn, ActualPosition, &GateSupportTile, &SupportUnavailableReason);
const bool bFootTileReady = bHaveFootTile && IsTileCollisionReady(FootTile);
if (!bHaveFootTile)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("stress_launch_support_missing");
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed "
"reason=stress_launch_support_missing"));
return;
}
HeadlessCollisionGateStressTestLaunchSupportTile = FootTile;
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = true;
HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = Now;
if (bFootTileReady)
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now;
HeadlessCollisionGateStressTestLaunchSupportReadySeconds = Now;
}
else if (PendingTiles.Contains(FootTile) || LoadedTiles.Contains(FootTile))
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now;
}
bHeadlessCollisionGateStressTestStartPlaced = true; bHeadlessCollisionGateStressTestStartPlaced = true;
HeadlessCollisionGateStressTestStartSeconds = Now; HeadlessCollisionGateStressTestStartSeconds = Now;
HeadlessCollisionGateStressTestBeginSeconds = Now;
bHeadlessCollisionGateStressTestStarted = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] drop_started position=(%.1f,%.1f,%.1f) "
"support=(%d,%d,%d)"),
ActualPosition.X, ActualPosition.Y, ActualPosition.Z,
FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z);
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn)) if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{ {
@@ -2968,43 +3145,73 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
CharacterMovement->BrakingDecelerationWalking, CharacterMovement->BrakingDecelerationWalking,
HeadlessCollisionGateStressTestSpeedCmPerSecond * 8.0f); HeadlessCollisionGateStressTestSpeedCmPerSecond * 8.0f);
CharacterMovement->StopMovementImmediately(); CharacterMovement->StopMovementImmediately();
CharacterMovement->SetMovementMode(MOVE_Walking); CharacterMovement->SetMovementMode(MOVE_Falling);
// Exercise the gate's actual hazard: a pawn descending onto a just-streamed
// floor fast enough to cross the surface in one movement frame if uncooked.
CharacterMovement->Velocity = FVector(
0.0f, 0.0f, -HeadlessCollisionGateStressTestSpeedCmPerSecond);
CharacterMovement->UpdateComponentVelocity();
} }
} }
float FeetDensity = 0.0f;
float CenterDensity = 0.0f;
float ActualSurfaceLocalVoxelZ = 0.0f;
float ClearanceCm = 0.0f;
const bool bLaunchSurfaceFound = MeasureHeadlessCollisionGateStressTerrain(
ActualPosition, CapsuleHalfHeightCm, FeetDensity, CenterDensity,
ActualSurfaceLocalVoxelZ, ClearanceCm);
if (bLaunchSurfaceFound)
{
HeadlessCollisionGateStressTestMinClearanceCm = ClearanceCm;
HeadlessCollisionGateStressTestMinClearancePosition = ActualPosition;
HeadlessCollisionGateStressTestMinFeetDensity = FeetDensity;
HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = ActualSurfaceLocalVoxelZ;
++HeadlessCollisionGateStressTestTerrainSampleCount;
}
else
{
bHeadlessCollisionGateStressTestTerrainMeasurementMissing = true;
}
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] start_placed support=(%d,%d,%d) ready=%d " TEXT("[VoxelForgeCollisionGateStressTest] start_placed moved=%d "
"reason=%s " "support=(%d,%d,%d) tile_collision_ready=%d gate_support=(%d,%d,%d) "
"position=(%.1f,%.1f,%.1f) launch_vox=(%.2f,%.2f,%.2f)"), "gate_support_ready=%d unavailable=%s surface_vox_z=%.4f "
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, "spawn_height_vox=%.2f position=(%.1f,%.1f,%.1f) "
"launch_vox=(%.3f,%.3f,%.3f)"),
bPlaced ? 1 : 0,
FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z,
bFootTileReady ? 1 : 0,
GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z,
bSupportReady ? 1 : 0, bSupportReady ? 1 : 0,
SupportUnavailableReason != nullptr ? SupportUnavailableReason : TEXT("pending"), SupportUnavailableReason != nullptr ? SupportUnavailableReason : TEXT("none"),
SurfaceLocalVoxelZ, LaunchHeightAboveGroundVoxels,
ActualPosition.X, ActualPosition.Y, ActualPosition.Z, ActualPosition.X, ActualPosition.Y, ActualPosition.Z,
LaunchVoxel.X, LaunchVoxel.Y, LaunchVoxel.Z); ActualLaunchVoxel.X, ActualLaunchVoxel.Y, ActualLaunchVoxel.Z);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] start_density feet=%.6f center=%.6f "
"surface_found=%d surface_vox_z=%.4f clearance_cm=%.3f "
"half_height_cm=%.2f probe_feet_density=%.6f"),
FeetDensity, CenterDensity, bLaunchSurfaceFound ? 1 : 0,
ActualSurfaceLocalVoxelZ, ClearanceCm, CapsuleHalfHeightCm, ProbeFeetDensity);
return; return;
} }
const double SinceLaunch = Now - HeadlessCollisionGateStressTestStartSeconds; // The measured interval starts with the vertical drop. Horizontal movement begins only after
if (!bHeadlessCollisionGateStressTestStarted // CharacterMovement reports a landing, so the test observes the entire fall and then checks
&& SinceLaunch < static_cast<double>(HeadlessCollisionGateStressTestStartDelaySeconds)) // that support gating leaves horizontal movement available.
if (!bHeadlessCollisionGateStressTestLanded)
{ {
InOutPlayerHeading = FVector::ZeroVector;
return; 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( const FVector WorldDirection = GetActorTransform().TransformVectorNoScale(
HeadlessCollisionGateStressTestDirection).GetSafeNormal(); HeadlessCollisionGateStressTestDirection).GetSafeNormal();
if (WorldDirection.IsNearlyZero()) if (WorldDirection.IsNearlyZero())
{ {
InOutPlayerHeading = FVector::ZeroVector;
return; return;
} }
@@ -3033,14 +3240,16 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
const FVector& PlayerPosition, APawn* PlayerPawn) const FVector& PlayerPosition, APawn* PlayerPawn)
{ {
if (!bHeadlessCollisionGateStressTest || !bHeadlessCollisionGateStressTestStarted if (!bHeadlessCollisionGateStressTest || !bHeadlessCollisionGateStressTestStarted
|| !IsValid(PlayerPawn) || bHeadlessCollisionGateStressTestExitRequested) || !IsValid(PlayerPawn) || bHeadlessCollisionGateStressTestExitRequested
|| bHeadlessCollisionGateStressTestFailed)
{ {
return; return;
} }
const double Now = FPlatformTime::Seconds(); const double Now = FPlatformTime::Seconds();
const FVector PreviousPosition = HeadlessCollisionGateStressTestLastActualPosition;
const float HorizontalDelta = FVector::Dist2D( const float HorizontalDelta = FVector::Dist2D(
HeadlessCollisionGateStressTestLastActualPosition, PlayerPosition); PreviousPosition, PlayerPosition);
HeadlessCollisionGateStressTestDistanceCm += HorizontalDelta; HeadlessCollisionGateStressTestDistanceCm += HorizontalDelta;
HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition; HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition;
@@ -3077,7 +3286,8 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
// interval before calling it a full stop; a permanent stop still fails through the distance // interval before calling it a full stop; a permanent stop still fails through the distance
// shortfall check below, while repeated gate holds remain visible in gate_holds/total_s. // shortfall check below, while repeated gate holds remain visible in gate_holds/total_s.
constexpr double FullStopConfirmationSeconds = 0.05; constexpr double FullStopConfirmationSeconds = 0.05;
const bool bNoHorizontalProgress = bGateEngaged && HorizontalDelta <= 0.01f; const bool bNoHorizontalProgress = bGateEngaged
&& bHeadlessCollisionGateStressTestLanded && HorizontalDelta <= 0.01f;
if (bNoHorizontalProgress) if (bNoHorizontalProgress)
{ {
if (HeadlessCollisionGateStressTestFullStopStartSeconds <= 0.0) if (HeadlessCollisionGateStressTestFullStopStartSeconds <= 0.0)
@@ -3104,7 +3314,260 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
} }
if (Now - HeadlessCollisionGateStressTestStartSeconds float CapsuleHalfHeightCm = 0.0f;
FVector PawnVelocity = PlayerPawn->GetVelocity();
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
{
CapsuleHalfHeightCm = Capsule->GetScaledCapsuleHalfHeight();
}
if (const UCharacterMovementComponent* CharacterMovement =
Character->GetCharacterMovement())
{
PawnVelocity = CharacterMovement->Velocity;
}
}
const FVoxelTileKey& LaunchSupportTile =
HeadlessCollisionGateStressTestLaunchSupportTile;
const bool bLaunchTileMatches =
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured
&& LaunchSupportTile.Level == 0;
if (bLaunchTileMatches)
{
const bool bPending = PendingTiles.Contains(LaunchSupportTile);
const bool bLoaded = LoadedTiles.Contains(LaunchSupportTile);
if (HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0
&& (bPending || bLoaded))
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now;
}
if (HeadlessCollisionGateStressTestLaunchSupportReadySeconds <= 0.0
&& IsTileCollisionReady(LaunchSupportTile))
{
if (HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0)
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now;
}
HeadlessCollisionGateStressTestLaunchSupportReadySeconds = Now;
}
}
if (!bHeadlessCollisionGateStressTestLanded)
{
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{
if (const UCharacterMovementComponent* CharacterMovement =
Character->GetCharacterMovement();
CharacterMovement != nullptr && CharacterMovement->IsMovingOnGround())
{
bHeadlessCollisionGateStressTestLanded = true;
HeadlessCollisionGateStressTestLandedSeconds = Now;
HeadlessCollisionGateStressTestDirection = FVector::XAxisVector;
// Follow the character after landing so the horizontal diagnostic exercises the
// normal desired-set path while the support gate remains active.
bHeadlessStreamingTestCenterOverride = false;
UPrimitiveComponent* MovementBase = CharacterMovement->GetMovementBase();
const FVector MovementBaseLocation = IsValid(MovementBase)
? MovementBase->GetComponentLocation() : FVector::ZeroVector;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] landed elapsed_s=%.6f "
"position=(%.2f,%.2f,%.2f) velocity=(%.2f,%.2f,%.2f) "
"movement_mode=%d base=%s base_location=(%.2f,%.2f,%.2f) "
"launch_support_ready=%d"),
Now - HeadlessCollisionGateStressTestBeginSeconds,
PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z,
PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z,
static_cast<int32>(CharacterMovement->MovementMode),
*GetNameSafe(MovementBase),
MovementBaseLocation.X, MovementBaseLocation.Y,
MovementBaseLocation.Z,
bLaunchTileMatches && IsTileCollisionReady(LaunchSupportTile) ? 1 : 0);
}
}
}
float FeetDensity = 0.0f;
float CenterDensity = 0.0f;
float SurfaceLocalVoxelZ = 0.0f;
float ClearanceCm = 0.0f;
const bool bSurfaceFound = MeasureHeadlessCollisionGateStressTerrain(
PlayerPosition, CapsuleHalfHeightCm, FeetDensity, CenterDensity,
SurfaceLocalVoxelZ, ClearanceCm);
const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition);
FVoxelTileKey FootTile;
const bool bHaveFootTile = GetPlayerSupportTile(PlayerPawn, PlayerPosition, FootTile);
const bool bFootTileCollisionReady = bHaveFootTile && IsTileCollisionReady(FootTile);
FVoxelTileKey GateSupportTile;
const TCHAR* GateSupportUnavailableReason = nullptr;
const bool bGateSupportReady = IsPlayerSupportCollisionReady(
PlayerPawn, PlayerPosition, &GateSupportTile, &GateSupportUnavailableReason);
if (bSurfaceFound)
{
++HeadlessCollisionGateStressTestTerrainSampleCount;
if (ClearanceCm < HeadlessCollisionGateStressTestMinClearanceCm)
{
HeadlessCollisionGateStressTestMinClearanceCm = ClearanceCm;
HeadlessCollisionGateStressTestMinClearancePosition = PlayerPosition;
HeadlessCollisionGateStressTestMinFeetDensity = FeetDensity;
HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = SurfaceLocalVoxelZ;
}
}
else
{
bHeadlessCollisionGateStressTestTerrainMeasurementMissing = true;
}
if (!bHeadlessCollisionGateStressTestStartHeightCrossed
&& PlayerPosition.Z < HeadlessCollisionGateStressTestStartPosition.Z - 0.5f)
{
bHeadlessCollisionGateStressTestStartHeightCrossed = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] start_height_crossed elapsed_s=%.6f "
"position=(%.2f,%.2f,%.2f) velocity=(%.2f,%.2f,%.2f) gate=%d "
"foot_tile=(%d,%d,%d) foot_tile_ready=%d gate_support=(%d,%d,%d) "
"gate_support_ready=%d unavailable=%s feet_density=%.6f "
"surface_found=%d surface_vox_z=%.4f clearance_cm=%.3f"),
Now - HeadlessCollisionGateStressTestStartSeconds,
PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z,
PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z,
bPawnCollisionGateEngaged ? 1 : 0,
FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z,
bFootTileCollisionReady ? 1 : 0,
GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z,
bGateSupportReady ? 1 : 0,
GateSupportUnavailableReason != nullptr
? GateSupportUnavailableReason : TEXT("none"),
FeetDensity, bSurfaceFound ? 1 : 0, SurfaceLocalVoxelZ, ClearanceCm);
}
constexpr double AllowedSurfacePenetrationCm = 5.0;
bool bSweptIntoSolid = false;
FVector FirstSolidSample = PlayerPosition;
float FirstSolidDensity = FeetDensity;
const float SegmentLengthCm = FVector::Distance(PreviousPosition, PlayerPosition);
const int32 SegmentSamples = FMath::Clamp(
FMath::CeilToInt(SegmentLengthCm / (VOXEL_SIZE * 0.5f)), 1, 256);
for (int32 SampleIndex = 1; SampleIndex < SegmentSamples; ++SampleIndex)
{
const float Alpha = static_cast<float>(SampleIndex)
/ static_cast<float>(SegmentSamples);
const FVector SampleWorldPosition = FMath::Lerp(PreviousPosition, PlayerPosition, Alpha);
float SampleFeetDensity = 0.0f;
float SampleCenterDensity = 0.0f;
float SampleSurfaceLocalVoxelZ = 0.0f;
float SampleClearanceCm = 0.0f;
const bool bSampleSurfaceFound = MeasureHeadlessCollisionGateStressTerrain(
SampleWorldPosition, CapsuleHalfHeightCm, SampleFeetDensity,
SampleCenterDensity, SampleSurfaceLocalVoxelZ, SampleClearanceCm);
if (bSampleSurfaceFound)
{
++HeadlessCollisionGateStressTestTerrainSampleCount;
if (SampleClearanceCm < HeadlessCollisionGateStressTestMinClearanceCm)
{
HeadlessCollisionGateStressTestMinClearanceCm = SampleClearanceCm;
HeadlessCollisionGateStressTestMinClearancePosition = SampleWorldPosition;
HeadlessCollisionGateStressTestMinFeetDensity = SampleFeetDensity;
HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ =
SampleSurfaceLocalVoxelZ;
}
}
else
{
bHeadlessCollisionGateStressTestTerrainMeasurementMissing = true;
}
if ((bSampleSurfaceFound
&& SampleClearanceCm < -AllowedSurfacePenetrationCm)
|| (!bSampleSurfaceFound && SampleFeetDensity < -0.25f))
{
bSweptIntoSolid = true;
FirstSolidSample = SampleWorldPosition;
FirstSolidDensity = SampleFeetDensity;
break;
}
}
const bool bBelowSurface = bSurfaceFound
? ClearanceCm < -AllowedSurfacePenetrationCm
: FeetDensity < -0.25f;
FVector ViolationPosition = bSweptIntoSolid ? FirstSolidSample : PlayerPosition;
float ViolationFeetDensity = bSweptIntoSolid ? FirstSolidDensity : FeetDensity;
float ViolationCenterDensity = CenterDensity;
float ViolationSurfaceLocalVoxelZ = SurfaceLocalVoxelZ;
float ViolationClearanceCm = ClearanceCm;
const bool bViolationSurfaceFound = bSweptIntoSolid
? MeasureHeadlessCollisionGateStressTerrain(
ViolationPosition, CapsuleHalfHeightCm, ViolationFeetDensity,
ViolationCenterDensity, ViolationSurfaceLocalVoxelZ,
ViolationClearanceCm)
: bSurfaceFound;
const bool bSweptPenetrationExceedsTolerance = bSweptIntoSolid
&& (!bViolationSurfaceFound
|| ViolationClearanceCm < -AllowedSurfacePenetrationCm);
if (bBelowSurface || bSweptPenetrationExceedsTolerance)
{
if (bViolationSurfaceFound
&& ViolationClearanceCm < HeadlessCollisionGateStressTestMinClearanceCm)
{
HeadlessCollisionGateStressTestMinClearanceCm = ViolationClearanceCm;
HeadlessCollisionGateStressTestMinClearancePosition = ViolationPosition;
HeadlessCollisionGateStressTestMinFeetDensity = ViolationFeetDensity;
HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ =
ViolationSurfaceLocalVoxelZ;
}
bHeadlessCollisionGateStressTestTerrainViolation = true;
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("terrain_penetration");
FVoxelTileKey ViolationFootTile;
const bool bHaveViolationFootTile = GetPlayerSupportTile(
PlayerPawn, ViolationPosition, ViolationFootTile);
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeCollisionGateStressTest] first_terrain_penetration "
"elapsed_s=%.6f position=(%.2f,%.2f,%.2f) local_vox=(%.3f,%.3f,%.3f) "
"velocity=(%.2f,%.2f,%.2f) gate=%d foot_tile=(%d,%d,%d) "
"foot_tile_ready=%d feet_density=%.6f surface_found=%d "
"surface_vox_z=%.4f clearance_cm=%.3f swept=%d"),
Now - HeadlessCollisionGateStressTestStartSeconds,
ViolationPosition.X, ViolationPosition.Y, ViolationPosition.Z,
WorldToLocalVoxel(ViolationPosition).X,
WorldToLocalVoxel(ViolationPosition).Y,
WorldToLocalVoxel(ViolationPosition).Z,
PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z,
bPawnCollisionGateEngaged ? 1 : 0,
ViolationFootTile.Coord.X, ViolationFootTile.Coord.Y,
ViolationFootTile.Coord.Z,
bHaveViolationFootTile && IsTileCollisionReady(ViolationFootTile) ? 1 : 0,
ViolationFeetDensity, bViolationSurfaceFound ? 1 : 0,
ViolationSurfaceLocalVoxelZ, ViolationClearanceCm,
bSweptIntoSolid ? 1 : 0);
}
++HeadlessCollisionGateStressTestTraceFrame;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] trace frame=%d elapsed_s=%.6f "
"position=(%.2f,%.2f,%.2f) local_vox=(%.3f,%.3f,%.3f) "
"velocity=(%.2f,%.2f,%.2f) gate=%d foot_tile=(%d,%d,%d) "
"foot_tile_ready=%d gate_support=(%d,%d,%d) gate_support_ready=%d "
"feet_density=%.6f center_density=%.6f surface_found=%d "
"surface_vox_z=%.4f clearance_cm=%.3f landed=%d segment_cm=%.2f"),
HeadlessCollisionGateStressTestTraceFrame,
Now - HeadlessCollisionGateStressTestStartSeconds,
PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z,
LocalPosition.X, LocalPosition.Y, LocalPosition.Z,
PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z,
bPawnCollisionGateEngaged ? 1 : 0,
FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z,
bFootTileCollisionReady ? 1 : 0,
GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z,
bGateSupportReady ? 1 : 0,
FeetDensity, CenterDensity, bSurfaceFound ? 1 : 0,
SurfaceLocalVoxelZ, ClearanceCm,
bHeadlessCollisionGateStressTestLanded ? 1 : 0, SegmentLengthCm);
if (bHeadlessCollisionGateStressTestLanded
&& Now - HeadlessCollisionGateStressTestLandedSeconds
>= static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds)) >= static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds))
{ {
return; return;
@@ -3449,37 +3912,28 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
HeadlessCollisionGateStressTestFailureReason = TEXT("stress_start_not_observed"); HeadlessCollisionGateStressTestFailureReason = TEXT("stress_start_not_observed");
} }
else if (bHeadlessCollisionGateStressTestStarted else if (bHeadlessCollisionGateStressTestStarted
&& bHeadlessCollisionGateStressTestLanded
&& !bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed && !bHeadlessCollisionGateStressTestFailed
&& Now - HeadlessCollisionGateStressTestStartSeconds && Now - HeadlessCollisionGateStressTestLandedSeconds
>= static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds)) >= static_cast<double>(HeadlessCollisionGateStressTestDurationSeconds))
{ {
// Total travel also reflects authored terrain along this deterministic path. Keep it as // The gate's job is to protect the density surface. Travel, holds, and support timing
// diagnostic output; judge the gate on support readiness and progress during a real hold. // stay in the report, but only measured capsule clearance decides PASS/FAIL.
if (!bStartupCollisionGateTimingCaptured) if (bHeadlessCollisionGateStressTestTerrainViolation)
{ {
bHeadlessCollisionGateStressTestFailed = true; // The first penetration was recorded at the sample that observed it.
HeadlessCollisionGateStressTestFailureReason = TEXT("support_not_requested");
} }
else if (StartupCollisionGateSubmittedSeconds <= 0.0) else if (HeadlessCollisionGateStressTestTerrainSampleCount == 0
|| bHeadlessCollisionGateStressTestTerrainMeasurementMissing)
{ {
bHeadlessCollisionGateStressTestFailed = true; bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("support_not_submitted"); HeadlessCollisionGateStressTestFailureReason = TEXT("terrain_surface_unavailable");
} }
else if (StartupCollisionGateReadySeconds <= 0.0) else if (HeadlessCollisionGateStressTestMinClearanceCm < -5.0)
{ {
bHeadlessCollisionGateStressTestFailed = true; bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("support_never_collision_ready"); HeadlessCollisionGateStressTestFailureReason = TEXT("terrain_clearance_below_tolerance");
}
else if (HeadlessCollisionGateStressTestGateHoldCount == 0)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("gate_not_engaged");
}
else if (HeadlessCollisionGateStressTestGateDistanceCm < 10.0)
{
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("horizontal_progress_during_gate_shortfall");
} }
else else
{ {
@@ -3531,6 +3985,66 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
? FMath::Max(0.0, StartupCollisionGateReadySeconds ? FMath::Max(0.0, StartupCollisionGateReadySeconds
- StartupCollisionGateFirstCheckSeconds) - StartupCollisionGateFirstCheckSeconds)
: -1.0; : -1.0;
const double StressSupportSubmitLatencySeconds =
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured
&& HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds > 0.0
? FMath::Max(0.0,
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds
- HeadlessCollisionGateStressTestLaunchSupportCheckSeconds)
: -1.0;
const double StressSupportReadyLatencySeconds =
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured
&& HeadlessCollisionGateStressTestLaunchSupportReadySeconds > 0.0
? FMath::Max(0.0,
HeadlessCollisionGateStressTestLaunchSupportReadySeconds
- HeadlessCollisionGateStressTestLaunchSupportCheckSeconds)
: -1.0;
float FinalFeetDensity = 0.0f;
float FinalCenterDensity = 0.0f;
float FinalSurfaceLocalVoxelZ = 0.0f;
float FinalClearanceCm = 0.0f;
float FinalHalfHeightCm = 0.0f;
FVector IgnoredCurrentPosition;
APawn* StressPawn = nullptr;
TryGetPlayerPosition(IgnoredCurrentPosition, &StressPawn);
if (const ACharacter* Character = Cast<ACharacter>(StressPawn))
{
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
{
FinalHalfHeightCm = Capsule->GetScaledCapsuleHalfHeight();
}
}
const bool bFinalSurfaceFound = MeasureHeadlessCollisionGateStressTerrain(
HeadlessCollisionGateStressTestLastActualPosition, FinalHalfHeightCm,
FinalFeetDensity, FinalCenterDensity, FinalSurfaceLocalVoxelZ, FinalClearanceCm);
const FVector FinalLocalPosition = WorldToLocalVoxel(
HeadlessCollisionGateStressTestLastActualPosition);
const float ColumnCenterZ = bFinalSurfaceFound
? FinalSurfaceLocalVoxelZ : FinalLocalPosition.Z - FinalHalfHeightCm / VOXEL_SIZE;
FString DensityColumnSamples;
for (int32 Offset = 12; Offset >= -12; --Offset)
{
const float SampleZ = ColumnCenterZ + static_cast<float>(Offset);
const float Density = Generator != nullptr
? Generator->GetDensityAt(FinalLocalPosition.X, FinalLocalPosition.Y, SampleZ)
: 0.0f;
if (!DensityColumnSamples.IsEmpty())
{
DensityColumnSamples += TEXT(",");
}
DensityColumnSamples += FString::Printf(TEXT("%.2f:%.5f"), SampleZ, Density);
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] density_column world_xy=(%.2f,%.2f) "
"local_xy_vox=(%.4f,%.4f) surface_found=%d surface_vox_z=%.4f "
"feet_density=%.6f clearance_cm=%.3f samples_zvox_density=%s"),
HeadlessCollisionGateStressTestLastActualPosition.X,
HeadlessCollisionGateStressTestLastActualPosition.Y,
FinalLocalPosition.X, FinalLocalPosition.Y,
bFinalSurfaceFound ? 1 : 0, FinalSurfaceLocalVoxelZ,
FinalFeetDensity, FinalClearanceCm, *DensityColumnSamples);
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f " TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f "
"distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f " "distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f "
@@ -3538,9 +4052,15 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
"full_stops=%d full_stop_total_s=%.6f collision_gate=%d " "full_stops=%d full_stop_total_s=%.6f collision_gate=%d "
"support_tile=(%d,%d,%d) support_first_check_to_submit_s=%.6f " "support_tile=(%d,%d,%d) support_first_check_to_submit_s=%.6f "
"support_first_check_to_collision_ready_s=%.6f " "support_first_check_to_collision_ready_s=%.6f "
"final_position=(%.1f,%.1f,%.1f)"), "test_support_tile=(%d,%d,%d) "
"test_support_first_check_to_submit_s=%.6f "
"test_support_first_check_to_collision_ready_s=%.6f landed=%d "
"terrain_samples=%d terrain_measurement_missing=%d "
"start_height_crossed=%d min_clearance_cm=%.3f "
"min_clearance_position=(%.2f,%.2f,%.2f) min_feet_density=%.6f "
"min_surface_vox_z=%.4f final_position=(%.1f,%.1f,%.1f)"),
bPassed ? TEXT("PASS") : TEXT("FAIL"), bPassed ? TEXT("PASS") : TEXT("FAIL"),
bPassed ? TEXT("continuous_horizontal_progress") bPassed ? TEXT("capsule_never_below_density_surface")
: *HeadlessCollisionGateStressTestFailureReason, : *HeadlessCollisionGateStressTestFailureReason,
Now - HeadlessCollisionGateStressTestBeginSeconds, Now - HeadlessCollisionGateStressTestBeginSeconds,
HeadlessCollisionGateStressTestDistanceCm / 100.0, HeadlessCollisionGateStressTestDistanceCm / 100.0,
@@ -3558,6 +4078,21 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
StartupCollisionGateSupportTile.Coord.Z, StartupCollisionGateSupportTile.Coord.Z,
SupportSubmitLatencySeconds, SupportSubmitLatencySeconds,
SupportReadyLatencySeconds, SupportReadyLatencySeconds,
HeadlessCollisionGateStressTestLaunchSupportTile.Coord.X,
HeadlessCollisionGateStressTestLaunchSupportTile.Coord.Y,
HeadlessCollisionGateStressTestLaunchSupportTile.Coord.Z,
StressSupportSubmitLatencySeconds,
StressSupportReadyLatencySeconds,
bHeadlessCollisionGateStressTestLanded ? 1 : 0,
HeadlessCollisionGateStressTestTerrainSampleCount,
bHeadlessCollisionGateStressTestTerrainMeasurementMissing ? 1 : 0,
bHeadlessCollisionGateStressTestStartHeightCrossed ? 1 : 0,
HeadlessCollisionGateStressTestMinClearanceCm,
HeadlessCollisionGateStressTestMinClearancePosition.X,
HeadlessCollisionGateStressTestMinClearancePosition.Y,
HeadlessCollisionGateStressTestMinClearancePosition.Z,
HeadlessCollisionGateStressTestMinFeetDensity,
HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ,
HeadlessCollisionGateStressTestLastActualPosition.X, HeadlessCollisionGateStressTestLastActualPosition.X,
HeadlessCollisionGateStressTestLastActualPosition.Y, HeadlessCollisionGateStressTestLastActualPosition.Y,
HeadlessCollisionGateStressTestLastActualPosition.Z); HeadlessCollisionGateStressTestLastActualPosition.Z);
@@ -4535,6 +5070,26 @@ bool AVoxelWorld::IsPawnCapsuleOverlappingPendingFloor(
void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile) void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile)
{ {
const double RequestCheckSeconds = FPlatformTime::Seconds();
const bool bIsStressLaunchSupport =
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured
&& HeadlessCollisionGateStressTestLaunchSupportTile.Coord == SupportTile.Coord
&& HeadlessCollisionGateStressTestLaunchSupportTile.Level == SupportTile.Level;
if (bIsStressLaunchSupport)
{
const bool bPending = PendingTiles.Contains(SupportTile);
const bool bLoaded = LoadedTiles.Contains(SupportTile);
if (HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0
&& (bPending || bLoaded || IsTileCollisionReady(SupportTile)))
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = RequestCheckSeconds;
}
if (HeadlessCollisionGateStressTestLaunchSupportReadySeconds <= 0.0
&& IsTileCollisionReady(SupportTile))
{
HeadlessCollisionGateStressTestLaunchSupportReadySeconds = RequestCheckSeconds;
}
}
if (SupportTile.Level != 0 if (SupportTile.Level != 0
|| IsTileCollisionReady(SupportTile) || IsTileCollisionReady(SupportTile)
|| CollisionNotRequiredTiles.Contains(SupportTile)) || CollisionNotRequiredTiles.Contains(SupportTile))
@@ -4542,7 +5097,6 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
return; return;
} }
const double RequestCheckSeconds = FPlatformTime::Seconds();
if (!bStartupCollisionGateTimingCaptured) if (!bStartupCollisionGateTimingCaptured)
{ {
StartupCollisionGateSupportTile = SupportTile; StartupCollisionGateSupportTile = SupportTile;
@@ -4578,6 +5132,11 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
} }
if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile)) if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile))
{ {
if (bIsStressLaunchSupport
&& HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0)
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = RequestCheckSeconds;
}
if (bStartupCollisionGateTimingCaptured if (bStartupCollisionGateTimingCaptured
&& StartupCollisionGateSupportTile.Coord == SupportTile.Coord && StartupCollisionGateSupportTile.Coord == SupportTile.Coord
&& StartupCollisionGateSupportTile.Level == SupportTile.Level && StartupCollisionGateSupportTile.Level == SupportTile.Level
@@ -4615,6 +5174,12 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
if (bSubmitted) if (bSubmitted)
{ {
bAllChunksLoaded = false; bAllChunksLoaded = false;
if (bIsStressLaunchSupport
&& HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0)
{
HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds =
FPlatformTime::Seconds();
}
} }
if (!bPawnGateSupportRequestReported || bSubmitted) if (!bPawnGateSupportRequestReported || bSubmitted)
{ {
+21 -1
View File
@@ -548,6 +548,10 @@ private:
void AdvanceHeadlessCollisionGateStressTest(FVector& InOutPlayerPosition, void AdvanceHeadlessCollisionGateStressTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn); FVector& InOutPlayerHeading, APawn* PlayerPawn);
void ObserveHeadlessCollisionGateStressTest(const FVector& PlayerPosition, APawn* PlayerPawn); void ObserveHeadlessCollisionGateStressTest(const FVector& PlayerPosition, APawn* PlayerPawn);
bool MeasureHeadlessCollisionGateStressTerrain(
const FVector& WorldPosition, float CapsuleHalfHeightCm,
float& OutFeetDensity, float& OutCenterDensity,
float& OutSurfaceLocalVoxelZ, float& OutClearanceCm) const;
void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition, void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn); FVector& InOutPlayerHeading, APawn* PlayerPawn);
void InitializeHeadlessStrateCrossingTest(); void InitializeHeadlessStrateCrossingTest();
@@ -1109,12 +1113,17 @@ public:
bool bHeadlessCollisionGateStressTestExitRequested = false; bool bHeadlessCollisionGateStressTestExitRequested = false;
bool bHeadlessCollisionGateStressTestGateWasEngaged = false; bool bHeadlessCollisionGateStressTestGateWasEngaged = false;
bool bHeadlessCollisionGateStressTestFullStop = false; bool bHeadlessCollisionGateStressTestFullStop = false;
bool bHeadlessCollisionGateStressTestStartHeightCrossed = false;
bool bHeadlessCollisionGateStressTestTerrainViolation = false;
bool bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false;
bool bHeadlessCollisionGateStressTestLanded = false;
bool bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false;
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f; float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
float HeadlessCollisionGateStressTestStartDelaySeconds = 0.1f;
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f; float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f; float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
double HeadlessCollisionGateStressTestBeginSeconds = 0.0; double HeadlessCollisionGateStressTestBeginSeconds = 0.0;
double HeadlessCollisionGateStressTestStartSeconds = 0.0; double HeadlessCollisionGateStressTestStartSeconds = 0.0;
double HeadlessCollisionGateStressTestLandedSeconds = 0.0;
double HeadlessCollisionGateStressTestGateStartSeconds = 0.0; double HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
double HeadlessCollisionGateStressTestDistanceCm = 0.0; double HeadlessCollisionGateStressTestDistanceCm = 0.0;
@@ -1122,10 +1131,21 @@ public:
double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0; double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
double HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0; double HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0; double HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0;
double HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = 0.0;
double HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = 0.0;
double HeadlessCollisionGateStressTestLaunchSupportReadySeconds = 0.0;
double HeadlessCollisionGateStressTestMinClearanceCm = 1.0e30;
float HeadlessCollisionGateStressTestMinFeetDensity = 0.0f;
float HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = 0.0f;
int32 HeadlessCollisionGateStressTestTerrainSampleCount = 0;
int32 HeadlessCollisionGateStressTestTraceFrame = 0;
int32 HeadlessCollisionGateStressTestGateHoldCount = 0; int32 HeadlessCollisionGateStressTestGateHoldCount = 0;
int32 HeadlessCollisionGateStressTestFullStopCount = 0; int32 HeadlessCollisionGateStressTestFullStopCount = 0;
FVector HeadlessCollisionGateStressTestDirection = FVector::XAxisVector; FVector HeadlessCollisionGateStressTestDirection = FVector::XAxisVector;
FVector HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector; FVector HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector;
FVector HeadlessCollisionGateStressTestStartPosition = FVector::ZeroVector;
FVector HeadlessCollisionGateStressTestMinClearancePosition = FVector::ZeroVector;
FVoxelTileKey HeadlessCollisionGateStressTestLaunchSupportTile;
FString HeadlessCollisionGateStressTestFailureReason; 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
+6 -3
View File
@@ -994,7 +994,12 @@ function Get-CollisionGateStressMetrics([string]$LogPath) {
'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops', 'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops',
'full_stop_total_s', 'collision_gate', 'gate_progress_m', 'support_tile', 'full_stop_total_s', 'collision_gate', 'gate_progress_m', 'support_tile',
'support_first_check_to_submit_s', 'support_first_check_to_submit_s',
'support_first_check_to_collision_ready_s')) { 'support_first_check_to_collision_ready_s', 'test_support_tile',
'test_support_first_check_to_submit_s',
'test_support_first_check_to_collision_ready_s', 'landed', 'terrain_samples',
'terrain_measurement_missing', 'start_height_crossed',
'min_clearance_cm', 'min_clearance_position', 'min_feet_density',
'min_surface_vox_z', 'final_position')) {
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key)) $match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
if ($match.Success) { $metrics[$key] = $match.Groups['v'].Value } if ($match.Success) { $metrics[$key] = $match.Groups['v'].Value }
} }
@@ -1451,7 +1456,6 @@ try {
'gate-stress' { 'gate-stress' {
$baseCvars['voxel.TestCollisionGateStress'] = 1 $baseCvars['voxel.TestCollisionGateStress'] = 1
$baseCvars['voxel.TestCollisionGateStressSpeedCmPerSecond'] = 6000 $baseCvars['voxel.TestCollisionGateStressSpeedCmPerSecond'] = 6000
$baseCvars['voxel.TestCollisionGateStressStartDelaySeconds'] = 0.1
$baseCvars['voxel.TestCollisionGateStressDurationSeconds'] = 2 $baseCvars['voxel.TestCollisionGateStressDurationSeconds'] = 2
$baseCvars['voxel.TestCollisionGateStressTimeoutSeconds'] = 30 $baseCvars['voxel.TestCollisionGateStressTimeoutSeconds'] = 30
$args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\gate-stress.log') (Merge-Cvars $baseCvars) $args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\gate-stress.log') (Merge-Cvars $baseCvars)
@@ -1506,7 +1510,6 @@ try {
$stressBaseCvars = [ordered]@{ $stressBaseCvars = [ordered]@{
'voxel.TestCollisionGateStress' = 1 'voxel.TestCollisionGateStress' = 1
'voxel.TestCollisionGateStressSpeedCmPerSecond' = 6000 'voxel.TestCollisionGateStressSpeedCmPerSecond' = 6000
'voxel.TestCollisionGateStressStartDelaySeconds' = 0.1
'voxel.TestCollisionGateStressDurationSeconds' = 2 'voxel.TestCollisionGateStressDurationSeconds' = 2
'voxel.TestCollisionGateStressTimeoutSeconds' = 30 'voxel.TestCollisionGateStressTimeoutSeconds' = 30
} }