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:
1 parent
a49725d365
commit
6ee433efa5
4 files changed
+657
-68
No files matched your search
+2
-1
@@ -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. |
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user