fix(collision gate): stop freezing the player on a stale prediction
The owner fell through a surface-only world and hovered in mid-air: log showed 'ready=1 predicted_wait=1' for 50 s. Two causes, both fixed: (1) engaging the gate zeroes velocity, so the predicted-support probe never re-ran and the gate kept waiting on the tile remembered from when the pawn was still moving -- the prediction is now re-derived every tick from the pawn's current pose; (2) a tile the streamer never requests (out of layout, or outside the desired set) could never become ready, so the wait was unbounded -- such tiles are now logged and treated as nonblocking, and any wait is bounded to 2 s with a warning. Repro added to the harness (surface-fall scenario): before, the pawn stops at Z=1315.9 with collision_gate_prediction_deadlock; after, it reaches the ground in 2.25 s at Z=-45.6. Strate crossing still protected: 145.49 s route, max gate wait 0.109 s (limit 10 s). Perf S/M/S/M 24.265/54.262 and 23.947/53.057 s, within repeat noise. Owner and canonical geometry unchanged (44E7C813 / 94B47A1C). Known red, pre-existing and unrelated: VoxelForge.Composer.ParameterRoll still reports false box verdicts in the owner's current single-strate surface layout (5 violations, identical in the baseline). Next round. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
7af8d0e870
commit
0dd45ca410
3 files changed
+697
-35
No files matched your search
@@ -1514,6 +1514,52 @@ void AVoxelWorld::BeginPlay()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The owner's current DA_Settings is intentionally a one-strate surface layout, so it cannot
|
||||||
|
// exercise the separate inter-strate traversal regression. The explicit headless crossing
|
||||||
|
// switch uses a transient copy of the authored four-definition corpus; no package is modified
|
||||||
|
// or saved, and ordinary gameplay keeps the assigned settings untouched.
|
||||||
|
if (bHeadlessStrateCrossingTest)
|
||||||
|
{
|
||||||
|
UVoxelSettings* CrossingSettings = DuplicateObject<UVoxelSettings>(Settings, this);
|
||||||
|
TArray<UVoxelStrateDefinition*> CrossingDefinitions;
|
||||||
|
for (int32 DefinitionIndex = 1; DefinitionIndex <= 4; ++DefinitionIndex)
|
||||||
|
{
|
||||||
|
const FString AssetPath = FString::Printf(
|
||||||
|
TEXT("/Game/VoxelForge/DA_Strate%d.DA_Strate%d"),
|
||||||
|
DefinitionIndex, DefinitionIndex);
|
||||||
|
if (UVoxelStrateDefinition* Definition =
|
||||||
|
LoadObject<UVoxelStrateDefinition>(nullptr, *AssetPath))
|
||||||
|
{
|
||||||
|
CrossingDefinitions.Add(Definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CrossingSettings != nullptr && CrossingDefinitions.Num() == 4)
|
||||||
|
{
|
||||||
|
CrossingSettings->Season.Reset();
|
||||||
|
CrossingSettings->StratePool.Reset();
|
||||||
|
CrossingSettings->FixedStrates.Reset();
|
||||||
|
CrossingSettings->TotalStrates = CrossingDefinitions.Num();
|
||||||
|
for (int32 DefinitionIndex = 0; DefinitionIndex < CrossingDefinitions.Num();
|
||||||
|
++DefinitionIndex)
|
||||||
|
{
|
||||||
|
CrossingSettings->FixedStrates.Add(
|
||||||
|
DefinitionIndex, CrossingDefinitions[DefinitionIndex]);
|
||||||
|
}
|
||||||
|
Settings = CrossingSettings;
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeStrateCrossingTest] fixture=transient_authored_corpus "
|
||||||
|
"strates=%d assets_not_saved=1"),
|
||||||
|
CrossingDefinitions.Num());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[VoxelForgeStrateCrossingTest] fixture=transient_authored_corpus "
|
||||||
|
"unavailable definitions=%d; using assigned settings"),
|
||||||
|
CrossingDefinitions.Num());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (VoxelForgeStartupTrace::IsActive())
|
if (VoxelForgeStartupTrace::IsActive())
|
||||||
{
|
{
|
||||||
FString SettingsPath = Settings->GetPathName();
|
FString SettingsPath = Settings->GetPathName();
|
||||||
@@ -1698,6 +1744,26 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
|||||||
HeadlessStreamingTestLastActualPosition = FVector::ZeroVector;
|
HeadlessStreamingTestLastActualPosition = FVector::ZeroVector;
|
||||||
HeadlessStreamingTestBeginSeconds = 0.0;
|
HeadlessStreamingTestBeginSeconds = 0.0;
|
||||||
HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||||
|
bHeadlessSurfaceFallTest = false;
|
||||||
|
bHeadlessSurfaceFallTestStartPlaced = false;
|
||||||
|
bHeadlessSurfaceFallTestPassed = false;
|
||||||
|
bHeadlessSurfaceFallTestFailed = false;
|
||||||
|
bHeadlessSurfaceFallTestExitRequested = false;
|
||||||
|
bHeadlessSurfaceFallTestPredictionProbeInjected = false;
|
||||||
|
bHeadlessSurfaceFallTestPredictionProbeCleared = false;
|
||||||
|
HeadlessSurfaceFallTestSpawnHeightVoxels = 20.0f;
|
||||||
|
HeadlessSurfaceFallTestGroundToleranceVoxels = 4.0f;
|
||||||
|
HeadlessSurfaceFallTestTimeoutSeconds = 8.0f;
|
||||||
|
HeadlessSurfaceFallTestPlayerHalfHeightVoxels =
|
||||||
|
VoxelPassageGeometry::PlayerHalfHeightVoxels;
|
||||||
|
HeadlessSurfaceFallTestTerrainZ = 0.0f;
|
||||||
|
HeadlessSurfaceFallTestBeginSeconds = 0.0;
|
||||||
|
HeadlessSurfaceFallTestStartSeconds = 0.0;
|
||||||
|
HeadlessSurfaceFallTestDistanceCm = 0.0;
|
||||||
|
HeadlessSurfaceFallTestStartPosition = FVector::ZeroVector;
|
||||||
|
HeadlessSurfaceFallTestLastActualPosition = FVector::ZeroVector;
|
||||||
|
HeadlessSurfaceFallTestPredictionProbePosition = FVector::ZeroVector;
|
||||||
|
HeadlessSurfaceFallTestFailureReason.Reset();
|
||||||
bTileHashDumpEnabled = false;
|
bTileHashDumpEnabled = false;
|
||||||
TileHashDumpPath.Reset();
|
TileHashDumpPath.Reset();
|
||||||
TileHashDumpRecords.Reset();
|
TileHashDumpRecords.Reset();
|
||||||
@@ -1709,6 +1775,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
|||||||
bHeadlessStrateCrossingTestFailed = false;
|
bHeadlessStrateCrossingTestFailed = false;
|
||||||
bHeadlessStrateCrossingTestExitRequested = false;
|
bHeadlessStrateCrossingTestExitRequested = false;
|
||||||
bHeadlessStrateCrossingTestGateWasEngaged = false;
|
bHeadlessStrateCrossingTestGateWasEngaged = false;
|
||||||
|
bHeadlessStrateCrossingTestInitialSupportReported = false;
|
||||||
HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
|
HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
|
||||||
HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
|
HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
|
||||||
HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
|
HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
|
||||||
@@ -1806,6 +1873,41 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
|||||||
HeadlessStrateCrossingTestRequestedPassageIndex);
|
HeadlessStrateCrossingTestRequestedPassageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 SurfaceFallValue = 0;
|
||||||
|
bHeadlessSurfaceFallTest =
|
||||||
|
(FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFall="), SurfaceFallValue)
|
||||||
|
&& SurfaceFallValue != 0)
|
||||||
|
|| FParse::Param(CommandLine, TEXT("voxel.TestSurfaceFall"));
|
||||||
|
if (bHeadlessSurfaceFallTest)
|
||||||
|
{
|
||||||
|
if (bHeadlessStrateCrossingTest)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=conflicting_headless_tests"));
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("conflicting_headless_tests");
|
||||||
|
}
|
||||||
|
FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFallSpawnHeightVoxels="),
|
||||||
|
HeadlessSurfaceFallTestSpawnHeightVoxels);
|
||||||
|
FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFallGroundToleranceVoxels="),
|
||||||
|
HeadlessSurfaceFallTestGroundToleranceVoxels);
|
||||||
|
FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFallTimeoutSeconds="),
|
||||||
|
HeadlessSurfaceFallTestTimeoutSeconds);
|
||||||
|
HeadlessSurfaceFallTestSpawnHeightVoxels = FMath::Max(
|
||||||
|
HeadlessSurfaceFallTestSpawnHeightVoxels, 1.0f);
|
||||||
|
HeadlessSurfaceFallTestGroundToleranceVoxels = FMath::Max(
|
||||||
|
HeadlessSurfaceFallTestGroundToleranceVoxels, 0.5f);
|
||||||
|
HeadlessSurfaceFallTestTimeoutSeconds = FMath::Max(
|
||||||
|
HeadlessSurfaceFallTestTimeoutSeconds, 1.0f);
|
||||||
|
HeadlessSurfaceFallTestBeginSeconds = FPlatformTime::Seconds();
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] configured=1 spawn_height_vox=%.2f "
|
||||||
|
"ground_tolerance_vox=%.2f timeout_s=%.3f"),
|
||||||
|
HeadlessSurfaceFallTestSpawnHeightVoxels,
|
||||||
|
HeadlessSurfaceFallTestGroundToleranceVoxels,
|
||||||
|
HeadlessSurfaceFallTestTimeoutSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
int32 TraceThroughCrossingValue = 0;
|
int32 TraceThroughCrossingValue = 0;
|
||||||
if (FParse::Value(
|
if (FParse::Value(
|
||||||
CommandLine,
|
CommandLine,
|
||||||
@@ -2151,6 +2253,27 @@ void AVoxelWorld::AdvanceHeadlessStrateCrossingTest(
|
|||||||
StrateManager->GetPassages()[HeadlessStrateCrossingTestPassageIndex];
|
StrateManager->GetPassages()[HeadlessStrateCrossingTestPassageIndex];
|
||||||
if (!bHeadlessStrateCrossingTestStartPlaced)
|
if (!bHeadlessStrateCrossingTestStartPlaced)
|
||||||
{
|
{
|
||||||
|
FVoxelTileKey InitialSupportTile;
|
||||||
|
const TCHAR* InitialSupportUnavailableReason = nullptr;
|
||||||
|
if (!IsPlayerSupportCollisionReady(
|
||||||
|
PlayerPawn, InOutPlayerPosition, &InitialSupportTile,
|
||||||
|
&InitialSupportUnavailableReason))
|
||||||
|
{
|
||||||
|
if (!bHeadlessStrateCrossingTestInitialSupportReported)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeStrateCrossingTest] waiting_for_initial_support "
|
||||||
|
"tile=(%d,%d,%d) reason=%s"),
|
||||||
|
InitialSupportTile.Coord.X, InitialSupportTile.Coord.Y,
|
||||||
|
InitialSupportTile.Coord.Z,
|
||||||
|
InitialSupportUnavailableReason != nullptr
|
||||||
|
? InitialSupportUnavailableReason : TEXT("collision_pending"));
|
||||||
|
bHeadlessStrateCrossingTestInitialSupportReported = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bHeadlessStrateCrossingTestInitialSupportReported = false;
|
||||||
|
|
||||||
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
{
|
{
|
||||||
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
|
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
|
||||||
@@ -2529,6 +2652,319 @@ void AVoxelWorld::AdvanceHeadlessStreamingTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::AdvanceHeadlessSurfaceFallTest(
|
||||||
|
FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn)
|
||||||
|
{
|
||||||
|
if (!bHeadlessSurfaceFallTest || bHeadlessSurfaceFallTestFailed
|
||||||
|
|| bHeadlessSurfaceFallTestPassed || !IsValid(PlayerPawn)
|
||||||
|
|| HeadlessSurfaceFallTestBeginSeconds <= 0.0 || !Generator || !StrateManager)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Now = FPlatformTime::Seconds();
|
||||||
|
if (!bHeadlessSurfaceFallTestStartPlaced)
|
||||||
|
{
|
||||||
|
if (StrateManager->GetNumStrates() != 1
|
||||||
|
|| StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, 0))
|
||||||
|
!= ECaveGeneratorType::SurfaceWorld)
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("layout_is_not_one_surface_strate");
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s strates=%d"),
|
||||||
|
*HeadlessSurfaceFallTestFailureReason, StrateManager->GetNumStrates());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float HeightfieldTerrainZ = 0.0f;
|
||||||
|
float CeilingZ = 0.0f;
|
||||||
|
if (!Generator->GetSurfaceHeightAt(
|
||||||
|
0.0f, 0.0f, 0, HeightfieldTerrainZ, CeilingZ))
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("surface_height_oracle_unavailable");
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"),
|
||||||
|
*HeadlessSurfaceFallTestFailureReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
|
||||||
|
{
|
||||||
|
const float HalfHeightCm = Capsule->GetScaledCapsuleHalfHeight();
|
||||||
|
if (VoxelMath::IsFinite(HalfHeightCm) && HalfHeightCm > 0.0f)
|
||||||
|
{
|
||||||
|
HeadlessSurfaceFallTestPlayerHalfHeightVoxels =
|
||||||
|
HalfHeightCm / VOXEL_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 TopChunkZ = 0;
|
||||||
|
int32 BottomChunkZ = 0;
|
||||||
|
if (!StrateManager->GetStrateChunkZBounds(0, TopChunkZ, BottomChunkZ))
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("surface_strate_bounds_unavailable");
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"),
|
||||||
|
*HeadlessSurfaceFallTestFailureReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The authored surface entry is at the top of the one-strate band. The procedural
|
||||||
|
// heightfield can be far below that opening, so use the higher of the two as the spawn
|
||||||
|
// floor; this reproduces a character falling through the surface-only world instead of
|
||||||
|
// accidentally starting next to the eventual terrain.
|
||||||
|
const float SurfaceEntryTopZ = static_cast<float>(TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
if (!StrateManager->GetLayout().IsValidIndex(0)
|
||||||
|
|| StrateManager->GetLayout()[0].Definition == nullptr
|
||||||
|
|| Settings == nullptr)
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("surface_entry_geometry_unavailable");
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"),
|
||||||
|
*HeadlessSurfaceFallTestFailureReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UVoxelStrateDefinition* TopDefinition =
|
||||||
|
StrateManager->GetLayout()[0].Definition;
|
||||||
|
const float TopSeal = TopDefinition->SurfaceParams.BoundarySealThickness;
|
||||||
|
const float OriginRadius = Settings->GetEffectiveOriginSpineRadius();
|
||||||
|
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||||
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
|
SurfaceEntryTopZ,
|
||||||
|
static_cast<float>(BottomChunkZ) * CHUNK_SIZE,
|
||||||
|
TopSeal,
|
||||||
|
OriginRadius);
|
||||||
|
if (!OriginLanding.bValid)
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("surface_entry_landing_unavailable");
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"),
|
||||||
|
*HeadlessSurfaceFallTestFailureReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HeadlessSurfaceFallTestTerrainZ = OriginLanding.FloorZ;
|
||||||
|
const float GroundHeightfieldZ = HeightfieldTerrainZ;
|
||||||
|
const float SpawnBaseZ = FMath::Max(HeightfieldTerrainZ, SurfaceEntryTopZ);
|
||||||
|
const FVector StartVoxel(
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
SpawnBaseZ
|
||||||
|
+ HeadlessSurfaceFallTestPlayerHalfHeightVoxels
|
||||||
|
+ HeadlessSurfaceFallTestSpawnHeightVoxels);
|
||||||
|
HeadlessSurfaceFallTestStartPosition = LocalVoxelToWorld(StartVoxel);
|
||||||
|
const bool bPlaced = PlayerPawn->SetActorLocation(
|
||||||
|
HeadlessSurfaceFallTestStartPosition,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
ETeleportType::TeleportPhysics);
|
||||||
|
const FVector ActualPosition = PlayerPawn->GetActorLocation();
|
||||||
|
InOutPlayerPosition = ActualPosition;
|
||||||
|
InOutPlayerHeading = FVector::ZeroVector;
|
||||||
|
HeadlessSurfaceFallTestStartPosition = ActualPosition;
|
||||||
|
HeadlessSurfaceFallTestLastActualPosition = ActualPosition;
|
||||||
|
HeadlessSurfaceFallTestDistanceCm = 0.0;
|
||||||
|
bHeadlessSurfaceFallTestStartPlaced = true;
|
||||||
|
HeadlessSurfaceFallTestStartSeconds = Now;
|
||||||
|
|
||||||
|
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
CharacterMovement->StopMovementImmediately();
|
||||||
|
CharacterMovement->SetMovementMode(MOVE_Falling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] start_placed moved=%d ground_vox=%.3f "
|
||||||
|
"heightfield_vox=%.3f entry_top_vox=%.3f "
|
||||||
|
"spawn_vox=(%.2f,%.2f,%.2f) world=(%.1f,%.1f,%.1f)"),
|
||||||
|
bPlaced ? 1 : 0,
|
||||||
|
HeadlessSurfaceFallTestTerrainZ,
|
||||||
|
GroundHeightfieldZ,
|
||||||
|
SurfaceEntryTopZ,
|
||||||
|
StartVoxel.X, StartVoxel.Y, StartVoxel.Z,
|
||||||
|
ActualPosition.X, ActualPosition.Y, ActualPosition.Z);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce the gate's original stale-prediction path while the pawn is still above the
|
||||||
|
// landing: the current support is valid, but a deliberately fast one-frame horizontal probe
|
||||||
|
// points at a tile outside the current desired set. The old gate remembers that probe after
|
||||||
|
// stopping the pawn and never re-derives it. The probe is cleared on the next tick only when
|
||||||
|
// the gate has correctly released, after which ordinary gravity continues the fall.
|
||||||
|
if (!bHeadlessSurfaceFallTestPredictionProbeInjected
|
||||||
|
&& Now - HeadlessSurfaceFallTestStartSeconds >= 0.25)
|
||||||
|
{
|
||||||
|
FVoxelTileKey ProbeSupportTile;
|
||||||
|
const FVector LocalPosition = WorldToLocalVoxel(InOutPlayerPosition);
|
||||||
|
const bool bProbeSupportReady =
|
||||||
|
IsPlayerSupportCollisionReady(PlayerPawn, InOutPlayerPosition, &ProbeSupportTile);
|
||||||
|
int32 ProbeTopChunkZ = 0;
|
||||||
|
int32 ProbeBottomChunkZ = 0;
|
||||||
|
const bool bProbeSupportInLayout = bProbeSupportReady
|
||||||
|
&& StrateManager->GetStrateChunkZBounds(
|
||||||
|
ProbeSupportTile.Coord.Z, ProbeTopChunkZ, ProbeBottomChunkZ);
|
||||||
|
if (bProbeSupportReady
|
||||||
|
&& bProbeSupportInLayout
|
||||||
|
&& LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels
|
||||||
|
> HeadlessSurfaceFallTestTerrainZ + 2.0f)
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestPredictionProbeInjected = true;
|
||||||
|
HeadlessSurfaceFallTestPredictionProbePosition = InOutPlayerPosition;
|
||||||
|
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (UCharacterMovementComponent* CharacterMovement =
|
||||||
|
Character->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
CharacterMovement->Velocity = FVector(1000000.0f, 0.0f, 0.0f);
|
||||||
|
CharacterMovement->SetMovementMode(MOVE_Falling);
|
||||||
|
CharacterMovement->UpdateComponentVelocity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] prediction_probe injected=1 "
|
||||||
|
"current_support=(%d,%d,%d) local=(%.2f,%.2f,%.2f) "
|
||||||
|
"velocity_cm_s=1000000 pawn_velocity=(%.1f,%.1f,%.1f)"),
|
||||||
|
ProbeSupportTile.Coord.X, ProbeSupportTile.Coord.Y, ProbeSupportTile.Coord.Z,
|
||||||
|
LocalPosition.X, LocalPosition.Y, LocalPosition.Z,
|
||||||
|
PlayerPawn->GetVelocity().X, PlayerPawn->GetVelocity().Y,
|
||||||
|
PlayerPawn->GetVelocity().Z);
|
||||||
|
// Use a stable test-frame delta so this command-line probe exercises the same
|
||||||
|
// prediction branch even when an uncapped -nullrhi tick reports a near-zero delta.
|
||||||
|
UpdatePawnCollisionGate(PlayerPawn, InOutPlayerPosition, 1.0f / 60.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (bHeadlessSurfaceFallTestPredictionProbeInjected
|
||||||
|
&& !bHeadlessSurfaceFallTestPredictionProbeCleared
|
||||||
|
&& !bPawnCollisionGateEngaged)
|
||||||
|
{
|
||||||
|
const bool bRestored = PlayerPawn->SetActorLocation(
|
||||||
|
HeadlessSurfaceFallTestPredictionProbePosition,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
ETeleportType::TeleportPhysics);
|
||||||
|
InOutPlayerPosition = PlayerPawn->GetActorLocation();
|
||||||
|
InOutPlayerHeading = FVector::ZeroVector;
|
||||||
|
bHeadlessSurfaceFallTestPredictionProbeCleared = true;
|
||||||
|
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (UCharacterMovementComponent* CharacterMovement =
|
||||||
|
Character->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
CharacterMovement->StopMovementImmediately();
|
||||||
|
CharacterMovement->SetMovementMode(MOVE_Falling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] prediction_probe cleared=1 restored=%d"),
|
||||||
|
bRestored ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessSurfaceFallTestDistanceCm += FVector::Dist(
|
||||||
|
HeadlessSurfaceFallTestLastActualPosition, InOutPlayerPosition);
|
||||||
|
HeadlessSurfaceFallTestLastActualPosition = InOutPlayerPosition;
|
||||||
|
|
||||||
|
const FVector LocalPosition = WorldToLocalVoxel(InOutPlayerPosition);
|
||||||
|
const float FeetZ = LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels;
|
||||||
|
bool bOnGround = false;
|
||||||
|
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (const UCharacterMovementComponent* CharacterMovement =
|
||||||
|
Character->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
bOnGround = CharacterMovement->IsMovingOnGround();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bOnGround
|
||||||
|
&& bHeadlessSurfaceFallTestPredictionProbeCleared
|
||||||
|
&& FeetZ <= HeadlessSurfaceFallTestTerrainZ
|
||||||
|
+ HeadlessSurfaceFallTestGroundToleranceVoxels
|
||||||
|
&& FeetZ >= HeadlessSurfaceFallTestTerrainZ
|
||||||
|
- HeadlessSurfaceFallTestGroundToleranceVoxels)
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestPassed = true;
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] reached_ground elapsed_s=%.6f "
|
||||||
|
"distance_m=%.6f feet_vox=%.3f terrain_vox=%.3f gate=%d probe=1"),
|
||||||
|
Now - HeadlessSurfaceFallTestStartSeconds,
|
||||||
|
HeadlessSurfaceFallTestDistanceCm / 100.0,
|
||||||
|
FeetZ,
|
||||||
|
HeadlessSurfaceFallTestTerrainZ,
|
||||||
|
bPawnCollisionGateEngaged ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::MaybeFinishHeadlessSurfaceFallTest()
|
||||||
|
{
|
||||||
|
if (!bHeadlessSurfaceFallTest || bHeadlessSurfaceFallTestExitRequested
|
||||||
|
|| HeadlessSurfaceFallTestBeginSeconds <= 0.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Now = FPlatformTime::Seconds();
|
||||||
|
if (!bHeadlessSurfaceFallTestStartPlaced
|
||||||
|
&& Now - HeadlessSurfaceFallTestBeginSeconds
|
||||||
|
>= static_cast<double>(HeadlessSurfaceFallTestTimeoutSeconds))
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("pawn_start_not_observed");
|
||||||
|
}
|
||||||
|
else if (!bHeadlessSurfaceFallTestPredictionProbeInjected
|
||||||
|
&& !bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed
|
||||||
|
&& Now - HeadlessSurfaceFallTestStartSeconds
|
||||||
|
>= static_cast<double>(HeadlessSurfaceFallTestTimeoutSeconds))
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("surface_prediction_probe_not_exercised");
|
||||||
|
}
|
||||||
|
else if (bHeadlessSurfaceFallTestPredictionProbeInjected
|
||||||
|
&& !bHeadlessSurfaceFallTestPredictionProbeCleared
|
||||||
|
&& !bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed
|
||||||
|
&& Now - HeadlessSurfaceFallTestStartSeconds
|
||||||
|
>= static_cast<double>(HeadlessSurfaceFallTestTimeoutSeconds))
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("collision_gate_prediction_deadlock");
|
||||||
|
}
|
||||||
|
else if (!bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed
|
||||||
|
&& Now - HeadlessSurfaceFallTestStartSeconds
|
||||||
|
>= static_cast<double>(HeadlessSurfaceFallTestTimeoutSeconds))
|
||||||
|
{
|
||||||
|
bHeadlessSurfaceFallTestFailed = true;
|
||||||
|
HeadlessSurfaceFallTestFailureReason = TEXT("surface_ground_timeout");
|
||||||
|
}
|
||||||
|
if (!bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bHeadlessSurfaceFallTestExitRequested = true;
|
||||||
|
const FVector FinalPosition = HeadlessSurfaceFallTestLastActualPosition;
|
||||||
|
const bool bPassed = bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed;
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeSurfaceFallTest] result=%s reason=%s elapsed_s=%.6f "
|
||||||
|
"distance_m=%.6f final_position=(%.1f,%.1f,%.1f) terrain_vox=%.3f"),
|
||||||
|
bPassed ? TEXT("PASS") : TEXT("FAIL"),
|
||||||
|
bPassed ? TEXT("reached_surface_ground") : *HeadlessSurfaceFallTestFailureReason,
|
||||||
|
Now - HeadlessSurfaceFallTestBeginSeconds,
|
||||||
|
HeadlessSurfaceFallTestDistanceCm / 100.0,
|
||||||
|
FinalPosition.X, FinalPosition.Y, FinalPosition.Z,
|
||||||
|
HeadlessSurfaceFallTestTerrainZ);
|
||||||
|
FPlatformMisc::RequestExitWithStatus(
|
||||||
|
false, bPassed ? 0 : 1, TEXT("VoxelForge surface fall test complete"));
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
|
void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
|
||||||
{
|
{
|
||||||
if (bHeadlessStrateCrossingTest)
|
if (bHeadlessStrateCrossingTest)
|
||||||
@@ -2537,6 +2973,12 @@ void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bHeadlessSurfaceFallTest)
|
||||||
|
{
|
||||||
|
MaybeFinishHeadlessSurfaceFallTest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bHeadlessStreamingTestExitRequested || HeadlessStreamingTestExitSeconds <= 0.0f
|
if (bHeadlessStreamingTestExitRequested || HeadlessStreamingTestExitSeconds <= 0.0f
|
||||||
|| HeadlessStreamingTestBeginSeconds <= 0.0)
|
|| HeadlessStreamingTestBeginSeconds <= 0.0)
|
||||||
{
|
{
|
||||||
@@ -2575,6 +3017,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
FVector PlayerHeading = PlayerPawn ? PlayerPawn->GetVelocity() : FVector::ZeroVector;
|
FVector PlayerHeading = PlayerPawn ? PlayerPawn->GetVelocity() : FVector::ZeroVector;
|
||||||
AdvanceHeadlessStreamingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
|
AdvanceHeadlessStreamingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
|
||||||
AdvanceHeadlessStrateCrossingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
|
AdvanceHeadlessStrateCrossingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
|
||||||
|
AdvanceHeadlessSurfaceFallTest(PlayerLastPos, PlayerHeading, PlayerPawn);
|
||||||
PeakObservedPawnSpeedCmPerSecond = FMath::Max(
|
PeakObservedPawnSpeedCmPerSecond = FMath::Max(
|
||||||
PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D());
|
PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D());
|
||||||
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
|
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
|
||||||
@@ -2955,9 +3398,49 @@ bool AVoxelWorld::IsTileCollisionReady(const FVoxelTileKey& Tile) const
|
|||||||
return Mesh != nullptr && Mesh->GetBodySetup() != nullptr;
|
return Mesh != nullptr && Mesh->GetBodySetup() != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AVoxelWorld::IsCollisionReadyFromSupportTile(
|
bool AVoxelWorld::IsCollisionTileUnavailable(const FVoxelTileKey& Tile,
|
||||||
const FVoxelTileKey& InitialTile, FVoxelTileKey* OutSupportTile) const
|
const TCHAR*& OutReason) const
|
||||||
{
|
{
|
||||||
|
OutReason = nullptr;
|
||||||
|
|
||||||
|
// A tile outside every authored strate is open air. The streamer deliberately does not
|
||||||
|
// request it, so treating its absent collision as pending would make a one-strate world an
|
||||||
|
// impossible gate target. Inter-strate gap chunks are different: they are authored solid
|
||||||
|
// bedrock and must remain blocking until their collision is ready.
|
||||||
|
if (StrateManager != nullptr)
|
||||||
|
{
|
||||||
|
int32 TopChunkZ = 0;
|
||||||
|
int32 BottomChunkZ = 0;
|
||||||
|
const bool bInStrate = StrateManager->GetStrateChunkZBounds(
|
||||||
|
Tile.Coord.Z, TopChunkZ, BottomChunkZ);
|
||||||
|
if (!bInStrate && !StrateManager->IsGapChunk(Tile.Coord))
|
||||||
|
{
|
||||||
|
OutReason = TEXT("out_of_layout");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DesiredStamp is zero only during the initial possession/setup tick, before the first
|
||||||
|
// desired set has been built. Do not classify that transient state as "outside desired";
|
||||||
|
// otherwise the initial safety gate could release before streaming has any authority.
|
||||||
|
if (DesiredStamp > 0 && !IsDesired(Tile))
|
||||||
|
{
|
||||||
|
OutReason = TEXT("outside_desired_set");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AVoxelWorld::IsCollisionReadyFromSupportTile(
|
||||||
|
const FVoxelTileKey& InitialTile, FVoxelTileKey* OutSupportTile,
|
||||||
|
const TCHAR** OutUnavailableReason) const
|
||||||
|
{
|
||||||
|
if (OutUnavailableReason != nullptr)
|
||||||
|
{
|
||||||
|
*OutUnavailableReason = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
FVoxelTileKey CandidateTile = InitialTile;
|
FVoxelTileKey CandidateTile = InitialTile;
|
||||||
const int32 Direction = CollisionSolidTiles.Contains(InitialTile) ? 1 : -1;
|
const int32 Direction = CollisionSolidTiles.Contains(InitialTile) ? 1 : -1;
|
||||||
|
|
||||||
@@ -2975,6 +3458,19 @@ bool AVoxelWorld::IsCollisionReadyFromSupportTile(
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const TCHAR* UnavailableReason = nullptr;
|
||||||
|
if (IsCollisionTileUnavailable(CandidateTile, UnavailableReason))
|
||||||
|
{
|
||||||
|
if (OutSupportTile != nullptr)
|
||||||
|
{
|
||||||
|
*OutSupportTile = CandidateTile;
|
||||||
|
}
|
||||||
|
if (OutUnavailableReason != nullptr)
|
||||||
|
{
|
||||||
|
*OutUnavailableReason = UnavailableReason;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!CollisionNotRequiredTiles.Contains(CandidateTile))
|
if (!CollisionNotRequiredTiles.Contains(CandidateTile))
|
||||||
{
|
{
|
||||||
if (OutSupportTile != nullptr)
|
if (OutSupportTile != nullptr)
|
||||||
@@ -2993,15 +3489,22 @@ bool AVoxelWorld::IsCollisionReadyFromSupportTile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool AVoxelWorld::IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition,
|
bool AVoxelWorld::IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition,
|
||||||
FVoxelTileKey* OutSupportTile) const
|
FVoxelTileKey* OutSupportTile,
|
||||||
|
const TCHAR** OutUnavailableReason) const
|
||||||
{
|
{
|
||||||
|
if (OutUnavailableReason != nullptr)
|
||||||
|
{
|
||||||
|
*OutUnavailableReason = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
FVoxelTileKey CandidateTile;
|
FVoxelTileKey CandidateTile;
|
||||||
if (!GetPlayerSupportTile(Pawn, PlayerPosition, CandidateTile))
|
if (!GetPlayerSupportTile(Pawn, PlayerPosition, CandidateTile))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsCollisionReadyFromSupportTile(CandidateTile, OutSupportTile);
|
return IsCollisionReadyFromSupportTile(
|
||||||
|
CandidateTile, OutSupportTile, OutUnavailableReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition,
|
void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition,
|
||||||
@@ -3027,6 +3530,7 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
|
|||||||
PawnTickPrerequisite = Pawn;
|
PawnTickPrerequisite = Pawn;
|
||||||
Pawn->AddTickPrerequisiteActor(this);
|
Pawn->AddTickPrerequisiteActor(this);
|
||||||
bPawnGateUnsupportedReported = false;
|
bPawnGateUnsupportedReported = false;
|
||||||
|
bPawnGateUnavailableReported = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The actor prerequisite covers the pawn tick; attach the same prerequisite directly to its
|
// The actor prerequisite covers the pawn tick; attach the same prerequisite directly to its
|
||||||
@@ -3062,11 +3566,13 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
|
|||||||
}
|
}
|
||||||
|
|
||||||
FVoxelTileKey CurrentSupportTile;
|
FVoxelTileKey CurrentSupportTile;
|
||||||
|
const TCHAR* CurrentSupportUnavailableReason = nullptr;
|
||||||
const bool bCurrentSupportReady = IsPlayerSupportCollisionReady(
|
const bool bCurrentSupportReady = IsPlayerSupportCollisionReady(
|
||||||
Pawn, PlayerPosition, &CurrentSupportTile);
|
Pawn, PlayerPosition, &CurrentSupportTile, &CurrentSupportUnavailableReason);
|
||||||
bool bCanRelease = bCurrentSupportReady;
|
bool bCanRelease = bCurrentSupportReady;
|
||||||
bool bWaitingForPredictedSupport = false;
|
bool bWaitingForPredictedSupport = false;
|
||||||
FVoxelTileKey PredictedSupportTile;
|
FVoxelTileKey PredictedSupportTile;
|
||||||
|
const TCHAR* PredictedSupportUnavailableReason = nullptr;
|
||||||
|
|
||||||
// Check the movement path ahead while the gate is open. Without this, a pawn could leave a
|
// Check the movement path ahead while the gate is open. Without this, a pawn could leave a
|
||||||
// ready tile on this frame and enter an un-cooked tile before the next world tick observes it.
|
// ready tile on this frame and enter an un-cooked tile before the next world tick observes it.
|
||||||
@@ -3085,48 +3591,74 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
|
|||||||
const float Alpha = static_cast<float>(ProbeIndex)
|
const float Alpha = static_cast<float>(ProbeIndex)
|
||||||
/ static_cast<float>(NumPredictedProbes);
|
/ static_cast<float>(NumPredictedProbes);
|
||||||
const FVector PredictedPosition = PlayerPosition + Velocity * DeltaTime * Alpha;
|
const FVector PredictedPosition = PlayerPosition + Velocity * DeltaTime * Alpha;
|
||||||
const bool bHavePredictedSupport = GetPlayerSupportTile(
|
const bool bPredictedSupportReady = IsPlayerSupportCollisionReady(
|
||||||
Pawn, PredictedPosition, PredictedSupportTile);
|
Pawn, PredictedPosition, &PredictedSupportTile,
|
||||||
const bool bPredictedSupportReady = bHavePredictedSupport
|
&PredictedSupportUnavailableReason);
|
||||||
&& IsPlayerSupportCollisionReady(
|
|
||||||
Pawn, PredictedPosition, &PredictedSupportTile);
|
|
||||||
if (!bPredictedSupportReady)
|
if (!bPredictedSupportReady)
|
||||||
{
|
{
|
||||||
bCanRelease = false;
|
bCanRelease = false;
|
||||||
bWaitingForPredictedSupport = bHavePredictedSupport;
|
bWaitingForPredictedSupport = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engage() stops velocity, so the blocked tile must be remembered across subsequent ticks;
|
const TCHAR* NonBlockingReason = CurrentSupportUnavailableReason != nullptr
|
||||||
// otherwise a gate caused by a predicted crossing would release immediately on the next tick.
|
? CurrentSupportUnavailableReason : PredictedSupportUnavailableReason;
|
||||||
if (bPawnCollisionGateEngaged && bPawnGateWaitingForPredictedSupport)
|
if (NonBlockingReason != nullptr)
|
||||||
{
|
{
|
||||||
if (!IsCollisionReadyFromSupportTile(PawnGatePredictedSupportTile,
|
if (!bPawnGateUnavailableReported)
|
||||||
&PredictedSupportTile))
|
|
||||||
{
|
{
|
||||||
bCanRelease = false;
|
UE_LOG(LogTemp, Display,
|
||||||
bWaitingForPredictedSupport = true;
|
TEXT("[VoxelForgeCollisionGate] nonblocking pawn=%s reason=%s "
|
||||||
|
"current_reason=%s predicted_reason=%s current_support=(%d,%d,%d) "
|
||||||
|
"predicted_support=(%d,%d,%d); tile will not be produced, no collision to wait for"),
|
||||||
|
*Pawn->GetName(),
|
||||||
|
NonBlockingReason,
|
||||||
|
CurrentSupportUnavailableReason != nullptr
|
||||||
|
? CurrentSupportUnavailableReason : TEXT("none"),
|
||||||
|
PredictedSupportUnavailableReason != nullptr
|
||||||
|
? PredictedSupportUnavailableReason : TEXT("none"),
|
||||||
|
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y,
|
||||||
|
CurrentSupportTile.Coord.Z,
|
||||||
|
PredictedSupportTile.Coord.X, PredictedSupportTile.Coord.Y,
|
||||||
|
PredictedSupportTile.Coord.Z);
|
||||||
}
|
}
|
||||||
|
bPawnGateUnavailableReported = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bPawnGateUnavailableReported = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bCanRelease)
|
if (bCanRelease)
|
||||||
{
|
{
|
||||||
bPawnGateWaitingForPredictedSupport = false;
|
|
||||||
ReleasePawnCollisionGate();
|
ReleasePawnCollisionGate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bWaitingForPredictedSupport)
|
constexpr double MaxCollisionGateWaitSeconds = 2.0;
|
||||||
|
if (bPawnCollisionGateEngaged && PawnGateEngagedAtSeconds > 0.0)
|
||||||
{
|
{
|
||||||
bPawnGateWaitingForPredictedSupport = true;
|
const double WaitSeconds = FPlatformTime::Seconds() - PawnGateEngagedAtSeconds;
|
||||||
PawnGatePredictedSupportTile = PredictedSupportTile;
|
if (WaitSeconds >= MaxCollisionGateWaitSeconds)
|
||||||
}
|
{
|
||||||
else if (!bCurrentSupportReady)
|
UE_LOG(LogTemp, Warning,
|
||||||
{
|
TEXT("[VoxelForgeCollisionGate] wait_timeout pawn=%s waited_s=%.3f "
|
||||||
bPawnGateWaitingForPredictedSupport = false;
|
"current_ready=%d predicted_wait=%d current_support=(%d,%d,%d) "
|
||||||
|
"predicted_support=(%d,%d,%d); releasing safety gate"),
|
||||||
|
*Pawn->GetName(),
|
||||||
|
WaitSeconds,
|
||||||
|
bCurrentSupportReady ? 1 : 0,
|
||||||
|
bWaitingForPredictedSupport ? 1 : 0,
|
||||||
|
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y,
|
||||||
|
CurrentSupportTile.Coord.Z,
|
||||||
|
PredictedSupportTile.Coord.X, PredictedSupportTile.Coord.Y,
|
||||||
|
PredictedSupportTile.Coord.Z);
|
||||||
|
ReleasePawnCollisionGate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bPawnCollisionGateEngaged)
|
if (!bPawnCollisionGateEngaged)
|
||||||
@@ -3167,6 +3699,7 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
|
|||||||
CharacterMovement->StopMovementImmediately();
|
CharacterMovement->StopMovementImmediately();
|
||||||
CharacterMovement->SetMovementMode(MOVE_None);
|
CharacterMovement->SetMovementMode(MOVE_None);
|
||||||
bPawnCollisionGateEngaged = true;
|
bPawnCollisionGateEngaged = true;
|
||||||
|
PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3178,6 +3711,7 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
|
|||||||
PawnMovement->StopMovementImmediately();
|
PawnMovement->StopMovementImmediately();
|
||||||
PawnMovement->Deactivate();
|
PawnMovement->Deactivate();
|
||||||
bPawnCollisionGateEngaged = true;
|
bPawnCollisionGateEngaged = true;
|
||||||
|
PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3200,7 +3734,7 @@ void AVoxelWorld::ReleasePawnCollisionGate()
|
|||||||
CollisionGatedCharacterMovement.Reset();
|
CollisionGatedCharacterMovement.Reset();
|
||||||
bSavedCharacterMovementMode = false;
|
bSavedCharacterMovementMode = false;
|
||||||
bSavedPawnMovementActive = false;
|
bSavedPawnMovementActive = false;
|
||||||
bPawnGateWaitingForPredictedSupport = false;
|
PawnGateEngagedAtSeconds = 0.0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3235,7 +3769,7 @@ void AVoxelWorld::ReleasePawnCollisionGate()
|
|||||||
bSavedCharacterMovementMode = false;
|
bSavedCharacterMovementMode = false;
|
||||||
bSavedPawnMovementActive = false;
|
bSavedPawnMovementActive = false;
|
||||||
bPawnCollisionGateEngaged = false;
|
bPawnCollisionGateEngaged = false;
|
||||||
bPawnGateWaitingForPredictedSupport = false;
|
PawnGateEngagedAtSeconds = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh)
|
void AVoxelWorld::BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh)
|
||||||
|
|||||||
@@ -522,11 +522,14 @@ private:
|
|||||||
void ConfigureHeadlessStreamingTest();
|
void ConfigureHeadlessStreamingTest();
|
||||||
void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition,
|
void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition,
|
||||||
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||||
|
void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition,
|
||||||
|
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||||
void InitializeHeadlessStrateCrossingTest();
|
void InitializeHeadlessStrateCrossingTest();
|
||||||
void AdvanceHeadlessStrateCrossingTest(FVector& InOutPlayerPosition,
|
void AdvanceHeadlessStrateCrossingTest(FVector& InOutPlayerPosition,
|
||||||
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||||
void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn);
|
void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn);
|
||||||
void MaybeFinishHeadlessStrateCrossingTest();
|
void MaybeFinishHeadlessStrateCrossingTest();
|
||||||
|
void MaybeFinishHeadlessSurfaceFallTest();
|
||||||
void MaybeFinishHeadlessStreamingTest();
|
void MaybeFinishHeadlessStreamingTest();
|
||||||
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
@@ -824,10 +827,14 @@ public:
|
|||||||
FVoxelTileKey& OutSupportTile) const;
|
FVoxelTileKey& OutSupportTile) const;
|
||||||
|
|
||||||
bool IsTileCollisionReady(const FVoxelTileKey& Tile) const;
|
bool IsTileCollisionReady(const FVoxelTileKey& Tile) const;
|
||||||
|
bool IsCollisionTileUnavailable(const FVoxelTileKey& Tile,
|
||||||
|
const TCHAR*& OutReason) const;
|
||||||
bool IsCollisionReadyFromSupportTile(const FVoxelTileKey& CandidateTile,
|
bool IsCollisionReadyFromSupportTile(const FVoxelTileKey& CandidateTile,
|
||||||
FVoxelTileKey* OutSupportTile = nullptr) const;
|
FVoxelTileKey* OutSupportTile = nullptr,
|
||||||
|
const TCHAR** OutUnavailableReason = nullptr) const;
|
||||||
bool IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition,
|
bool IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition,
|
||||||
FVoxelTileKey* OutSupportTile = nullptr) const;
|
FVoxelTileKey* OutSupportTile = nullptr,
|
||||||
|
const TCHAR** OutUnavailableReason = nullptr) const;
|
||||||
|
|
||||||
/** Gate pawn movement until its current and, when moving, next support tile have a completed
|
/** Gate pawn movement until its current and, when moving, next support tile have a completed
|
||||||
* level-0 RMC collision body. The world tick is installed as the pawn prerequisite in the
|
* level-0 RMC collision body. The world tick is installed as the pawn prerequisite in the
|
||||||
@@ -997,6 +1004,29 @@ public:
|
|||||||
double HeadlessStreamingTestBeginSeconds = 0.0;
|
double HeadlessStreamingTestBeginSeconds = 0.0;
|
||||||
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||||
|
|
||||||
|
// Explicit owner-layout collision-gate repro. This is a command-line-only harness path: it
|
||||||
|
// places the possessed character above the authored single-strate surface and lets ordinary
|
||||||
|
// CharacterMovement gravity carry it to the generated ground.
|
||||||
|
bool bHeadlessSurfaceFallTest = false;
|
||||||
|
bool bHeadlessSurfaceFallTestStartPlaced = false;
|
||||||
|
bool bHeadlessSurfaceFallTestPassed = false;
|
||||||
|
bool bHeadlessSurfaceFallTestFailed = false;
|
||||||
|
bool bHeadlessSurfaceFallTestExitRequested = false;
|
||||||
|
bool bHeadlessSurfaceFallTestPredictionProbeInjected = false;
|
||||||
|
bool bHeadlessSurfaceFallTestPredictionProbeCleared = false;
|
||||||
|
float HeadlessSurfaceFallTestSpawnHeightVoxels = 20.0f;
|
||||||
|
float HeadlessSurfaceFallTestGroundToleranceVoxels = 4.0f;
|
||||||
|
float HeadlessSurfaceFallTestTimeoutSeconds = 8.0f;
|
||||||
|
float HeadlessSurfaceFallTestPlayerHalfHeightVoxels = 3.52f;
|
||||||
|
float HeadlessSurfaceFallTestTerrainZ = 0.0f;
|
||||||
|
double HeadlessSurfaceFallTestBeginSeconds = 0.0;
|
||||||
|
double HeadlessSurfaceFallTestStartSeconds = 0.0;
|
||||||
|
double HeadlessSurfaceFallTestDistanceCm = 0.0;
|
||||||
|
FVector HeadlessSurfaceFallTestStartPosition = FVector::ZeroVector;
|
||||||
|
FVector HeadlessSurfaceFallTestLastActualPosition = FVector::ZeroVector;
|
||||||
|
FVector HeadlessSurfaceFallTestPredictionProbePosition = FVector::ZeroVector;
|
||||||
|
FString HeadlessSurfaceFallTestFailureReason;
|
||||||
|
|
||||||
// Explicit parity evidence. Workers append under the lock; EndPlay writes the compact JSON
|
// Explicit parity evidence. Workers append under the lock; EndPlay writes the compact JSON
|
||||||
// after ActiveTaskCount has reached zero, so the dump is a complete worker-side snapshot.
|
// after ActiveTaskCount has reached zero, so the dump is a complete worker-side snapshot.
|
||||||
bool bTileHashDumpEnabled = false;
|
bool bTileHashDumpEnabled = false;
|
||||||
@@ -1015,6 +1045,7 @@ public:
|
|||||||
bool bHeadlessStrateCrossingTestFailed = false;
|
bool bHeadlessStrateCrossingTestFailed = false;
|
||||||
bool bHeadlessStrateCrossingTestExitRequested = false;
|
bool bHeadlessStrateCrossingTestExitRequested = false;
|
||||||
bool bHeadlessStrateCrossingTestGateWasEngaged = false;
|
bool bHeadlessStrateCrossingTestGateWasEngaged = false;
|
||||||
|
bool bHeadlessStrateCrossingTestInitialSupportReported = false;
|
||||||
int32 HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
|
int32 HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
|
||||||
int32 HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
|
int32 HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
|
||||||
int32 HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
|
int32 HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
|
||||||
@@ -1050,8 +1081,8 @@ public:
|
|||||||
bool bSavedCharacterMovementMode = false;
|
bool bSavedCharacterMovementMode = false;
|
||||||
bool bSavedPawnMovementActive = false;
|
bool bSavedPawnMovementActive = false;
|
||||||
bool bPawnCollisionGateEngaged = false;
|
bool bPawnCollisionGateEngaged = false;
|
||||||
bool bPawnGateWaitingForPredictedSupport = false;
|
double PawnGateEngagedAtSeconds = 0.0;
|
||||||
FVoxelTileKey PawnGatePredictedSupportTile;
|
bool bPawnGateUnavailableReported = false;
|
||||||
bool bPawnGateUnsupportedReported = false;
|
bool bPawnGateUnsupportedReported = false;
|
||||||
|
|
||||||
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
|
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
|
||||||
|
|||||||
+100
-3
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'parity')]
|
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'surface-fall', 'crossing', 'parity')]
|
||||||
[string]$Scenario = 'canonical',
|
[string]$Scenario = 'canonical',
|
||||||
[switch]$Build,
|
[switch]$Build,
|
||||||
[ValidateSet('owner', 'default')]
|
[ValidateSet('owner', 'default')]
|
||||||
@@ -43,6 +43,8 @@ $BuildRecord = [ordered]@{ requested = [bool]$Build; status = 'not_requested' }
|
|||||||
$AssetEvidence = @()
|
$AssetEvidence = @()
|
||||||
$CommandletMetrics = [ordered]@{}
|
$CommandletMetrics = [ordered]@{}
|
||||||
$GameMetrics = [ordered]@{}
|
$GameMetrics = [ordered]@{}
|
||||||
|
$SurfaceFallMetrics = [ordered]@{ requested = $Scenario -in @('tests', 'surface-fall'); status = 'not_run' }
|
||||||
|
$StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' }
|
||||||
$TestMetrics = @()
|
$TestMetrics = @()
|
||||||
$ParityEvidence = [ordered]@{ requested = $Scenario -eq 'parity'; status = 'not_run' }
|
$ParityEvidence = [ordered]@{ requested = $Scenario -eq 'parity'; status = 'not_run' }
|
||||||
$DllEvidence = [ordered]@{
|
$DllEvidence = [ordered]@{
|
||||||
@@ -290,7 +292,13 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
|
|||||||
arguments = @($Arguments)
|
arguments = @($Arguments)
|
||||||
status = 'not_started'
|
status = 'not_started'
|
||||||
}
|
}
|
||||||
|
$isolatedLocalAppData = Join-Path $RunRoot 'LocalAppData'
|
||||||
|
Ensure-Directory $isolatedLocalAppData
|
||||||
|
$oldLocalAppData = $env:LOCALAPPDATA
|
||||||
try {
|
try {
|
||||||
|
# Unreal resolves several engine-wide stores from LOCALAPPDATA even when -userdir and
|
||||||
|
# -LocalDataCachePath are supplied. Keep this child process inside the run directory.
|
||||||
|
$env:LOCALAPPDATA = $isolatedLocalAppData
|
||||||
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
|
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
|
||||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden -PassThru
|
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden -PassThru
|
||||||
$ledger.pid = [int]$process.Id
|
$ledger.pid = [int]$process.Id
|
||||||
@@ -301,7 +309,10 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
|
|||||||
} catch {
|
} catch {
|
||||||
$ledger.status = 'launch_error'
|
$ledger.status = 'launch_error'
|
||||||
$ledger.error = $_.Exception.Message
|
$ledger.error = $_.Exception.Message
|
||||||
|
} finally {
|
||||||
|
if ($null -eq $oldLocalAppData) { Remove-Item Env:LOCALAPPDATA -ErrorAction SilentlyContinue } else { $env:LOCALAPPDATA = $oldLocalAppData }
|
||||||
}
|
}
|
||||||
|
$ledger.isolated_local_app_data = $isolatedLocalAppData
|
||||||
$ledger.ended_utc = [datetime]::UtcNow.ToString('o')
|
$ledger.ended_utc = [datetime]::UtcNow.ToString('o')
|
||||||
$ledger.crash_reporters_reaped = @(Reap-OwnCrashReporters $reportersBefore $startedUtc $LogPath)
|
$ledger.crash_reporters_reaped = @(Reap-OwnCrashReporters $reportersBefore $startedUtc $LogPath)
|
||||||
$ledger.new_crashes = @(Get-NewCrashes $crashesBefore $startedUtc)
|
$ledger.new_crashes = @(Get-NewCrashes $crashesBefore $startedUtc)
|
||||||
@@ -325,12 +336,16 @@ function Invoke-BuildProcess([string[]]$Arguments, [string]$LogPath, [string]$Er
|
|||||||
$oldDotnetCliHome = $env:DOTNET_CLI_HOME
|
$oldDotnetCliHome = $env:DOTNET_CLI_HOME
|
||||||
$oldDotnetFirstUse = $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE
|
$oldDotnetFirstUse = $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE
|
||||||
$oldDotnetNoLogo = $env:DOTNET_NOLOGO
|
$oldDotnetNoLogo = $env:DOTNET_NOLOGO
|
||||||
|
$isolatedLocalAppData = Join-Path $RunRoot 'LocalAppData'
|
||||||
|
Ensure-Directory $isolatedLocalAppData
|
||||||
|
$oldLocalAppData = $env:LOCALAPPDATA
|
||||||
try {
|
try {
|
||||||
# Keep the build tool's first-use sentinel and package metadata inside this run. This is
|
# Keep the build tool's first-use sentinel and package metadata inside this run. This is
|
||||||
# separate from Unreal's LocalAppData guard and avoids touching the owner's profile.
|
# separate from Unreal's LocalAppData guard and avoids touching the owner's profile.
|
||||||
$env:DOTNET_CLI_HOME = $dotnetHome
|
$env:DOTNET_CLI_HOME = $dotnetHome
|
||||||
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
||||||
$env:DOTNET_NOLOGO = '1'
|
$env:DOTNET_NOLOGO = '1'
|
||||||
|
$env:LOCALAPPDATA = $isolatedLocalAppData
|
||||||
$process = Start-Process -FilePath $dotnet -ArgumentList $Arguments -WorkingDirectory $PluginRoot `
|
$process = Start-Process -FilePath $dotnet -ArgumentList $Arguments -WorkingDirectory $PluginRoot `
|
||||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden -PassThru
|
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden -PassThru
|
||||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds
|
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds
|
||||||
@@ -338,6 +353,7 @@ function Invoke-BuildProcess([string[]]$Arguments, [string]$LogPath, [string]$Er
|
|||||||
if ($null -eq $oldDotnetCliHome) { Remove-Item Env:DOTNET_CLI_HOME -ErrorAction SilentlyContinue } else { $env:DOTNET_CLI_HOME = $oldDotnetCliHome }
|
if ($null -eq $oldDotnetCliHome) { Remove-Item Env:DOTNET_CLI_HOME -ErrorAction SilentlyContinue } else { $env:DOTNET_CLI_HOME = $oldDotnetCliHome }
|
||||||
if ($null -eq $oldDotnetFirstUse) { Remove-Item Env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE -ErrorAction SilentlyContinue } else { $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = $oldDotnetFirstUse }
|
if ($null -eq $oldDotnetFirstUse) { Remove-Item Env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE -ErrorAction SilentlyContinue } else { $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = $oldDotnetFirstUse }
|
||||||
if ($null -eq $oldDotnetNoLogo) { Remove-Item Env:DOTNET_NOLOGO -ErrorAction SilentlyContinue } else { $env:DOTNET_NOLOGO = $oldDotnetNoLogo }
|
if ($null -eq $oldDotnetNoLogo) { Remove-Item Env:DOTNET_NOLOGO -ErrorAction SilentlyContinue } else { $env:DOTNET_NOLOGO = $oldDotnetNoLogo }
|
||||||
|
if ($null -eq $oldLocalAppData) { Remove-Item Env:LOCALAPPDATA -ErrorAction SilentlyContinue } else { $env:LOCALAPPDATA = $oldLocalAppData }
|
||||||
}
|
}
|
||||||
return [ordered]@{
|
return [ordered]@{
|
||||||
tool = $dotnet
|
tool = $dotnet
|
||||||
@@ -350,6 +366,7 @@ function Invoke-BuildProcess([string[]]$Arguments, [string]$LogPath, [string]$Er
|
|||||||
stdout = $StdoutPath
|
stdout = $StdoutPath
|
||||||
error_log = $ErrorPath
|
error_log = $ErrorPath
|
||||||
arguments = @($Arguments)
|
arguments = @($Arguments)
|
||||||
|
isolated_local_app_data = $isolatedLocalAppData
|
||||||
status = if ($wait.timed_out) { 'timeout' } elseif ($wait.exit_code -eq 0) { 'passed' } else { 'failed' }
|
status = if ($wait.timed_out) { 'timeout' } elseif ($wait.exit_code -eq 0) { 'passed' } else { 'failed' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +426,7 @@ function Sync-OwnerAssets {
|
|||||||
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
|
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
|
||||||
[void]$rows.Add([ordered]@{
|
[void]$rows.Add([ordered]@{
|
||||||
name = $assetName
|
name = $assetName
|
||||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity')
|
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing')
|
||||||
source_path = $source
|
source_path = $source
|
||||||
source_sha256 = Get-Sha256 $source
|
source_sha256 = Get-Sha256 $source
|
||||||
staged_path = $target
|
staged_path = $target
|
||||||
@@ -457,7 +474,7 @@ function Get-AssetEvidence {
|
|||||||
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
|
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
|
||||||
[void]$rows.Add([ordered]@{
|
[void]$rows.Add([ordered]@{
|
||||||
name = $assetName
|
name = $assetName
|
||||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity')
|
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing')
|
||||||
source_path = $source
|
source_path = $source
|
||||||
source_sha256 = Get-Sha256 $source
|
source_sha256 = Get-Sha256 $source
|
||||||
staged_path = $target
|
staged_path = $target
|
||||||
@@ -644,6 +661,36 @@ function Get-StreamingMetrics([string]$LogPath) {
|
|||||||
return $metrics
|
return $metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-SurfaceFallMetrics([string]$LogPath) {
|
||||||
|
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeSurfaceFallTest\] result=' -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -Last 1)
|
||||||
|
if ($lineMatch.Count -eq 0) { return [ordered]@{ status = 'missing'; log = $LogPath } }
|
||||||
|
$line = [string]$lineMatch[0].Line
|
||||||
|
$metrics = [ordered]@{ status = 'failed'; log = $LogPath; raw = $line }
|
||||||
|
$result = [regex]::Match($line, 'result=(?<v>PASS|FAIL)')
|
||||||
|
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
|
||||||
|
foreach ($key in @('reason', 'elapsed_s', 'distance_m', 'terrain_vox')) {
|
||||||
|
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
|
||||||
|
if ($match.Success) { $metrics[$key] = $match.Groups['v'].Value }
|
||||||
|
}
|
||||||
|
return $metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-StrateCrossingMetrics([string]$LogPath) {
|
||||||
|
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeStrateCrossingTest\] result=' -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -Last 1)
|
||||||
|
if ($lineMatch.Count -eq 0) { return [ordered]@{ status = 'missing'; log = $LogPath } }
|
||||||
|
$line = [string]$lineMatch[0].Line
|
||||||
|
$metrics = [ordered]@{ status = 'failed'; log = $LogPath; raw = $line }
|
||||||
|
$result = [regex]::Match($line, 'result=(?<v>PASS|FAIL)')
|
||||||
|
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
|
||||||
|
foreach ($key in @('reason', 'elapsed_s', 'distance_m', 'max_gate_s', 'gate_limit_s', 'gate_total_s', 'min_floor_clearance_vox')) {
|
||||||
|
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
|
||||||
|
if ($match.Success) { $metrics[$key] = $match.Groups['v'].Value }
|
||||||
|
}
|
||||||
|
return $metrics
|
||||||
|
}
|
||||||
|
|
||||||
function Get-TestMetrics($Ledger) {
|
function Get-TestMetrics($Ledger) {
|
||||||
if ($null -eq $Ledger) { return @() }
|
if ($null -eq $Ledger) { return @() }
|
||||||
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
|
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
|
||||||
@@ -904,6 +951,35 @@ try {
|
|||||||
(Join-Path $RunRoot 'Logs\perf.log') (Join-Path $RunRoot 'Logs\perf.stdout.log') (Join-Path $RunRoot 'Logs\perf.stderr.log')
|
(Join-Path $RunRoot 'Logs\perf.log') (Join-Path $RunRoot 'Logs\perf.stdout.log') (Join-Path $RunRoot 'Logs\perf.stderr.log')
|
||||||
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\perf.log')
|
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\perf.log')
|
||||||
}
|
}
|
||||||
|
'surface-fall' {
|
||||||
|
$baseCvars['voxel.TestSurfaceFall'] = 1
|
||||||
|
$baseCvars['voxel.TestSurfaceFallSpawnHeightVoxels'] = 20
|
||||||
|
$baseCvars['voxel.TestSurfaceFallGroundToleranceVoxels'] = 4
|
||||||
|
$baseCvars['voxel.TestSurfaceFallTimeoutSeconds'] = 8
|
||||||
|
$args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\surface-fall.log') (Merge-Cvars $baseCvars)
|
||||||
|
$gameLedger = Invoke-UnrealLaunch 'surface-fall-game' $args `
|
||||||
|
(Join-Path $RunRoot 'Logs\surface-fall.log') (Join-Path $RunRoot 'Logs\surface-fall.stdout.log') (Join-Path $RunRoot 'Logs\surface-fall.stderr.log')
|
||||||
|
$SurfaceFallMetrics = Get-SurfaceFallMetrics (Join-Path $RunRoot 'Logs\surface-fall.log')
|
||||||
|
if ($SurfaceFallMetrics.status -ne 'passed') {
|
||||||
|
Add-Failure "surface-fall repro did not pass: $($SurfaceFallMetrics.status) $($SurfaceFallMetrics.reason)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'crossing' {
|
||||||
|
$baseCvars['voxel.TestStrateCrossing'] = 1
|
||||||
|
# The authored four-strate route is a real switchback whose vertical drop is hundreds
|
||||||
|
# of metres. Use a fixed diagnostic speed and enough wall time for initial collision
|
||||||
|
# cooking; the game code still uses swept character movement and the collision gate.
|
||||||
|
$baseCvars['voxel.TestStrateCrossingSpeedCmPerSecond'] = 2000
|
||||||
|
$baseCvars['voxel.TestStrateCrossingTimeoutSeconds'] = 360
|
||||||
|
$baseCvars['voxel.TestStrateCrossingMaxGateSeconds'] = 10
|
||||||
|
$args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\crossing.log') (Merge-Cvars $baseCvars)
|
||||||
|
$gameLedger = Invoke-UnrealLaunch 'strate-crossing-game' $args `
|
||||||
|
(Join-Path $RunRoot 'Logs\crossing.log') (Join-Path $RunRoot 'Logs\crossing.stdout.log') (Join-Path $RunRoot 'Logs\crossing.stderr.log')
|
||||||
|
$StrateCrossingMetrics = Get-StrateCrossingMetrics (Join-Path $RunRoot 'Logs\crossing.log')
|
||||||
|
if ($StrateCrossingMetrics.status -ne 'passed') {
|
||||||
|
Add-Failure "strate-crossing test did not pass: $($StrateCrossingMetrics.status) $($StrateCrossingMetrics.reason)"
|
||||||
|
}
|
||||||
|
}
|
||||||
'tests' {
|
'tests' {
|
||||||
$testPath = if ([string]::IsNullOrWhiteSpace($TestFilter)) { 'VoxelForge' } else { $TestFilter }
|
$testPath = if ([string]::IsNullOrWhiteSpace($TestFilter)) { 'VoxelForge' } else { $TestFilter }
|
||||||
$baseCvars['ExecCmds'] = "Automation RunTests $testPath; Quit"
|
$baseCvars['ExecCmds'] = "Automation RunTests $testPath; Quit"
|
||||||
@@ -925,6 +1001,23 @@ try {
|
|||||||
$failedNames = ($failedTests | ForEach-Object { $_.path }) -join ', '
|
$failedNames = ($failedTests | ForEach-Object { $_.path }) -join ', '
|
||||||
Add-Failure "automation tests failed: $failedNames"
|
Add-Failure "automation tests failed: $failedNames"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The runtime collision-gate repro is part of the tests scenario as an integration
|
||||||
|
# test alongside the 44 pure automation tests. It uses the same isolated host/assets
|
||||||
|
# and gets its own launch ledger entry and log.
|
||||||
|
$surfaceBaseCvars = [ordered]@{
|
||||||
|
'voxel.TestSurfaceFall' = 1
|
||||||
|
'voxel.TestSurfaceFallSpawnHeightVoxels' = 20
|
||||||
|
'voxel.TestSurfaceFallGroundToleranceVoxels' = 4
|
||||||
|
'voxel.TestSurfaceFallTimeoutSeconds' = 8
|
||||||
|
}
|
||||||
|
$surfaceArgs = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\surface-fall.log') (Merge-Cvars $surfaceBaseCvars)
|
||||||
|
$surfaceFallLedger = Invoke-UnrealLaunch 'surface-fall-game' $surfaceArgs `
|
||||||
|
(Join-Path $RunRoot 'Logs\surface-fall.log') (Join-Path $RunRoot 'Logs\surface-fall.stdout.log') (Join-Path $RunRoot 'Logs\surface-fall.stderr.log')
|
||||||
|
$SurfaceFallMetrics = Get-SurfaceFallMetrics (Join-Path $RunRoot 'Logs\surface-fall.log')
|
||||||
|
if ($SurfaceFallMetrics.status -ne 'passed') {
|
||||||
|
Add-Failure "surface-fall repro did not pass: $($SurfaceFallMetrics.status) $($SurfaceFallMetrics.reason)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
'parity' {
|
'parity' {
|
||||||
$parityArgs = New-CommonArguments 'commandlet' (Join-Path $RunRoot 'Logs\parity-commandlet.log') (Merge-Cvars $baseCvars)
|
$parityArgs = New-CommonArguments 'commandlet' (Join-Path $RunRoot 'Logs\parity-commandlet.log') (Merge-Cvars $baseCvars)
|
||||||
@@ -1015,6 +1108,8 @@ $result = [ordered]@{
|
|||||||
probe_amplitudes = if ($CommandletMetrics.Contains('probe_amplitudes')) { $CommandletMetrics.probe_amplitudes } else { [ordered]@{} }
|
probe_amplitudes = if ($CommandletMetrics.Contains('probe_amplitudes')) { $CommandletMetrics.probe_amplitudes } else { [ordered]@{} }
|
||||||
commandlet = $CommandletMetrics
|
commandlet = $CommandletMetrics
|
||||||
game = $GameMetrics
|
game = $GameMetrics
|
||||||
|
surface_fall = $SurfaceFallMetrics
|
||||||
|
strate_crossing = $StrateCrossingMetrics
|
||||||
worker_seconds = [ordered]@{
|
worker_seconds = [ordered]@{
|
||||||
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
|
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
|
||||||
game = if ($GameMetrics.Contains('worker_generation_sum_s')) { $GameMetrics.worker_generation_sum_s } else { $null }
|
game = if ($GameMetrics.Contains('worker_generation_sum_s')) { $GameMetrics.worker_generation_sum_s } else { $null }
|
||||||
@@ -1044,6 +1139,8 @@ try {
|
|||||||
[void]$summaryLines.Add("connectivity=$(ConvertTo-CompactJson $result.connectivity)")
|
[void]$summaryLines.Add("connectivity=$(ConvertTo-CompactJson $result.connectivity)")
|
||||||
[void]$summaryLines.Add("geometry_hash=$($result.geometry_hash)")
|
[void]$summaryLines.Add("geometry_hash=$($result.geometry_hash)")
|
||||||
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
|
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
|
||||||
|
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
|
||||||
|
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
|
||||||
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
|
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
|
||||||
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")
|
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")
|
||||||
[void]$summaryLines.Add("p50_p95=$(ConvertTo-CompactJson $result.p50_p95)")
|
[void]$summaryLines.Add("p50_p95=$(ConvertTo-CompactJson $result.p50_p95)")
|
||||||
|
|||||||
Reference in new issue
Block a user