From cf5f83e9f0ed537e39b3c1d5e57a2d1040f73a41 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 6 Sep 2026 03:16:55 +0200 Subject: [PATCH] test(diag): H1 dead, H3 measured -- player-sized space is SHATTERED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnosis only, no generation or parameter changes. ⭐ H1 IS DEAD, confirmed by Jahni's playtest and by measurement. He walked the maze candidate: "i CAN go through it, the tunnels are just the right size with very tiny wiggle room." The radial probe agrees: measured bore span 3.62 m against a 3.677 m parameter. The carve produces what it claims; the scale change worked. ⭐ H3 IS WHAT THE NUMBERS SHOW. Player-sized volume EXISTS but is broken into hundreds of disconnected pockets, with the two mouths in different ones, tens to hundreds of metres apart (step 1, fitted window): archetype fit cells components largest arrival/departure gap Maze 2801 309 176 43 / 81 206 vox (51 m) TunnelNetwork 2748 467 1292 2 / 1 164 vox (41 m) VerticalShafts 1156 112 96 3 / 36 94 vox (24 m) SurfaceWorld 713 520 6 1 / 1 1101 vox (275 m) CrystalChamber 655 197 24 1 / 1 48 vox Only FloatingIslands connects, through FOUR cells. ⚠️ BUT I DO NOT ACCEPT "H3, therefore the world is shattered" YET, and the reason is Jahni. He WALKED a maze end to end that this metric says is 309 disconnected pockets whose largest holds 176 cells. Direct observation contradicts the instrument. An arrival mouth sitting in a THREE-CELL pocket is not a description of a cave; it is the signature of a stencil that fragments a continuous floor. H2 is the likely cause of that fragmentation, and Codex found it exactly: the fit test demands a solid cell at EXACTLY (X,Y,Z-1) -- no supporting- surface search, no slope tolerance, no step height. HeadroomCells applies only to the legacy walkable metric, not the capsule mask. VoxelStrateMeasure.cpp:697 (stencil), :729 (the exact-floor test) At step 1, any floor that slopes or is rough loses its solid cell at Z-1 under most of its air, so a walkable corridor dissolves into specks. ⇒ SEQUENCING: fix the stencil first, THEN re-measure H3. Until the instrument stops shredding continuous floors its component counts cannot be trusted. Fifth time this session that the measurement was the suspect. Recommended stencil, Codex's design and I agree: a bounded downward search for a COHERENT SUPPORTING SURFACE within a step-height tolerance, then re-test capsule clearance at that support height -- explicitly NOT "any solid within N cells", which would accept ledges and isolated support pixels. Fixed the mixed-resolution reporting that nearly cost me a false finding: fields are now coarse_step4_* and *_step1, never both unlabelled on one line. Recorded from the playtest, not acted on: - The maze is a GRID, not a maze. Edge selection is independent hash gating per +X/+Y/+Z edge (VoxelDensityOpStack.cpp:582/584/586), so every junction looks alike. A spanning subset would give dead ends and uneven runs. - TunnelNetwork DOES have explicit room-to-room connections (VoxelCaveMorphology.cpp:1406 backbone, :1465 tunnels) -- so Jahni's "lack of real passageway" applies to FlatPlain/CrystalChamber, which are slab voids with no room graph at all. - Two-abreast wants 2.5-3.0 m usable floor; a 4 m circular bore gives ~2 m. Build clean, 31 succeeded / 0 failed, run by me after Jahni closed the editor (the previous attempt could not link -- the DLL was held by his running editor). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV --- .../Tests/VoxelForgeComposerShowcaseTest.cpp | 70 +- .../Tests/VoxelForgeScaleDiagnosisTest.cpp | 895 ++++++++++++++++++ 2 files changed, 949 insertions(+), 16 deletions(-) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp index be7f2a6..bc266a2 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp @@ -946,10 +946,14 @@ namespace const TCHAR* Label) { FString Report = FString::Printf( - TEXT("%s (walkable floor area is the coarse 128 m x 128 m footprint proxy)\n" - "archetype | PlayerFitFraction | TraversableComponentShare | " - "player-fit arrival->departure | walkable floor area m2 | " - "median vertical clearance m | roughness-to-feature ratio\n"), Label); + TEXT("%s (player-fit fields are fitted SampleStep=1; walkable floor area and " + "median vertical clearance are coarse SampleStep=4 proxies)\n" + "archetype | player_fit_fraction_step1 | traversable_share_step1 | " + "fit_cells_step1 | components_step1 | largest_component_cells_step1 | " + "arrival_component_cells_step1 | departure_component_cells_step1 | " + "mouth_component_gap_voxels_step1 | restricted_law_step1 | " + "coarse_walkable_floor_area_m2 | coarse_median_vertical_clearance_m | " + "roughness-to-feature ratio\n"), Label); for (const ECaveGeneratorType Archetype : GShowcaseArchetypes) { const FShowcaseSample* Found = Samples.FindByPredicate( @@ -961,21 +965,31 @@ namespace { Report += FString::Printf( TEXT("%s | no measured sample | no measured sample | no measured sample | " + "no measured sample | no measured sample | no measured sample | " + "no measured sample | no measured sample | no measured sample | " "n/a | n/a | n/a\n"), VF_GetStrateArchetypeName(Archetype)); continue; } const FVoxelStrateMetrics& M = Found->FineMetrics; + const FVoxelConnectivityDiagnostics& D = Found->PlayerFitLaw; const FString Law = M.bPlayerFitResolved ? VF_ShowcaseConnectivityName(Found->PlayerFitLaw.Result) : TEXT("unresolved"); const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction * 128.0f * 128.0f; Report += FString::Printf( - TEXT("%s | %.6f | %.6f | %s | %.1f (%.6f footprint) | %.2f | %.3f\n"), + TEXT("%s | %.6f | %.6f | %lld | %d | %lld | %lld | %lld | %.3f | %s | " + "%.1f (%.6f footprint) | %.2f | %.3f\n"), VF_GetStrateArchetypeName(Archetype), M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f, M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f, + static_cast(M.NumPlayerFitCells), + M.NumTraversableComponents, + static_cast(M.LargestTraversableComponentCells), + static_cast(D.StartComponentCells), + static_cast(D.GoalComponentCells), + D.StartToGoalComponentDistanceCells, *Law, FloorAreaM2, Found->Metrics.WalkableFloorAreaFraction, Found->Metrics.MedianVerticalClearance * 0.25f, @@ -1875,20 +1889,35 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) } const FString FineSummary = Sample.FineMetrics.bPlayerFitResolved ? FString::Printf( - TEXT("fit=%.6f traversable=%.6f fit_cells=%lld min_player_clearance=%.2f " - "restricted_law=%s"), + TEXT("fit_step1=%.6f traversable_step1=%.6f fit_cells_step1=%lld " + "components_step1=%d largest_component_cells_step1=%lld " + "arrival_component_cells_step1=%lld departure_component_cells_step1=%lld " + "mouth_component_gap_step1=%.3f min_player_clearance_step1=%.2f " + "restricted_law_step1=%s; coarse_step4_walkable=%.6f " + "coarse_step4_clearance=%d voxels"), Sample.FineMetrics.PlayerFitFraction, Sample.FineMetrics.TraversableComponentShare, static_cast(Sample.FineMetrics.NumPlayerFitCells), + Sample.FineMetrics.NumTraversableComponents, + static_cast(Sample.FineMetrics.LargestTraversableComponentCells), + static_cast(Sample.PlayerFitLaw.StartComponentCells), + static_cast(Sample.PlayerFitLaw.GoalComponentCells), + Sample.PlayerFitLaw.StartToGoalComponentDistanceCells, Sample.FineMetrics.MinimumPlayerClearanceVoxels, - VF_ShowcaseConnectivityName(Sample.PlayerFitLaw.Result)) + VF_ShowcaseConnectivityName(Sample.PlayerFitLaw.Result), + Sample.Metrics.WalkableFraction, + Sample.Metrics.MedianVerticalClearance) : FString::Printf( - TEXT("fit=UNRESOLVED reason=%s"), + TEXT("fit_step1=UNRESOLVED reason=%s; coarse_step4_walkable=%.6f " + "coarse_step4_clearance=%d voxels"), Sample.FineMetrics.PlayerFitRefusalReason.IsEmpty() - ? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason); + ? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason, + Sample.Metrics.WalkableFraction, + Sample.Metrics.MedianVerticalClearance); AddInfo(FString::Printf( - TEXT("previous showcase %s seed=%d index=%d: %s; old_walkable=%.6f " - "old_floor_area=%.6f old_largest_surface=%.6f old_law=%s; %s%s"), + TEXT("previous showcase %s seed=%d index=%d: %s; coarse_step4_walkable=%.6f " + "coarse_step4_floor_area=%.6f coarse_step4_largest_surface=%.6f " + "coarse_step4_law=%s; %s%s"), VF_GetStrateArchetypeName(Previous.Archetype), Previous.Seed, Previous.CandidateIndex, bPassedNewGate ? TEXT("ACCEPTED by player-fit gate") : TEXT("REJECTED by player-fit gate"), @@ -2203,10 +2232,14 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) TEXT("%.6f (step=1)"), Selected.FineMetrics.TraversableComponentShare); AddInfo(FString::Printf( TEXT("%s: hard-gate survivors=%d, selected seed=%d index=%d, " - "old_walkable=%.6f, old_floor_area=%.6f, old_clearance=%d voxels, " - "old_largest_surface=%.6f, old_law=%s, player_fit_fraction=%.6f, " - "traversable_component_share=%.6f, player_fit_cells=%lld, " - "player_clearance=%.2f voxels, restricted_law=%s, window=%s"), + "coarse_step4_walkable=%.6f, coarse_step4_floor_area=%.6f, " + "coarse_step4_clearance=%d voxels, coarse_step4_largest_surface=%.6f, " + "coarse_step4_law=%s, player_fit_fraction_step1=%.6f, " + "traversable_component_share_step1=%.6f, player_fit_cells_step1=%lld, " + "components_step1=%d, largest_component_cells_step1=%lld, " + "arrival_component_cells_step1=%lld, departure_component_cells_step1=%lld, " + "mouth_component_gap_voxels_step1=%.3f, player_clearance_step1=%.2f voxels, " + "restricted_law_step1=%s, window=%s"), VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]), Stats[ArchetypeIndex].Survivors.Num(), Selected.Candidate.Seed, Selected.Candidate.Index, @@ -2218,6 +2251,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters) Selected.FineMetrics.PlayerFitFraction, Selected.FineMetrics.TraversableComponentShare, static_cast(Selected.FineMetrics.NumPlayerFitCells), + Selected.FineMetrics.NumTraversableComponents, + static_cast(Selected.FineMetrics.LargestTraversableComponentCells), + static_cast(Selected.PlayerFitLaw.StartComponentCells), + static_cast(Selected.PlayerFitLaw.GoalComponentCells), + Selected.PlayerFitLaw.StartToGoalComponentDistanceCells, Selected.FineMetrics.MinimumPlayerClearanceVoxels, VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result), *PreviewCandidates[ArchetypeIndex].Window.Describe())); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp new file mode 100644 index 0000000..8b09201 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp @@ -0,0 +1,895 @@ +// Measurement-only scale diagnosis for the player-fit showcase. + +#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR + +#include "Misc/AutomationTest.h" + +#include "VoxelCaveMorphology.h" +#include "VoxelDensityOpStack.h" +#include "VoxelForgeTestFixture.h" +#include "VoxelStrateComposer.h" +#include "VoxelStrateMeasure.h" +#include "VoxelTypes.h" + +namespace +{ + constexpr int32 DiagnosisSeed = 0; + constexpr int32 MazeCandidateIndex = 50; + constexpr int32 TunnelCandidateIndex = 61; + // These are the current seed-0 showcase identities from the bounded roll set. The previous + // report's VerticalShafts identity was seed 0 / index 34; index 15 belongs to another family + // in the current corpus and would silently leave the shaft probe on fixture defaults. + constexpr int32 ShaftCandidateIndex = 34; + constexpr int32 RadialSampleMaxVoxels = 48; + constexpr int32 FineDiagnosisMaxCells = 4000000; + + const TCHAR* VF_DiagnosisConnectivityName(EVoxelConnectivityResult Result) + { + switch (Result) + { + case EVoxelConnectivityResult::Connected: return TEXT("Connected"); + case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnectedAtThisResolution"); + case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid"); + case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid"); + case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit"); + case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit"); + case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow"); + case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudgetExhausted"); + } + return TEXT("Unknown"); + } + + struct FDensityCrossing + { + bool bFound = false; + float AxisDensity = 0.0f; + float CrossingRadiusVoxels = -1.0f; + int32 FirstSolidStep = -1; + float FirstSolidDensity = 0.0f; + }; + + FDensityCrossing VF_SampleRadialRay( + const FVector& AxisPoint, + const FVector& UnitNormal, + TFunctionRef SampleDensity, + int32 MaxDistanceVoxels = RadialSampleMaxVoxels) + { + FDensityCrossing Result; + float PreviousDensity = SampleDensity(AxisPoint); + Result.AxisDensity = PreviousDensity; + for (int32 Distance = 1; Distance <= MaxDistanceVoxels; ++Distance) + { + const float CurrentDensity = SampleDensity( + AxisPoint + UnitNormal * static_cast(Distance)); + // MC convention in this codebase: density >= 0 is air and density < 0 is solid. + if (PreviousDensity >= 0.0f && CurrentDensity < 0.0f) + { + const float Denominator = PreviousDensity - CurrentDensity; + const float Fraction = FMath::IsNearlyZero(Denominator) + ? 0.0f : FMath::Clamp(PreviousDensity / Denominator, 0.0f, 1.0f); + Result.bFound = true; + Result.CrossingRadiusVoxels = static_cast(Distance - 1) + Fraction; + Result.FirstSolidStep = Distance; + Result.FirstSolidDensity = CurrentDensity; + return Result; + } + PreviousDensity = CurrentDensity; + } + return Result; + } + + struct FRadialSection + { + FVector AxisPoint = FVector::ZeroVector; + FVector UnitNormal = FVector::ZeroVector; + FDensityCrossing Positive; + FDensityCrossing Negative; + bool bValid = false; + }; + + FRadialSection VF_SampleSection( + const FVector& AxisPoint, + const FVector& UnitNormal, + TFunctionRef SampleDensity) + { + FRadialSection Result; + Result.AxisPoint = AxisPoint; + Result.UnitNormal = UnitNormal.GetSafeNormal(); + if (Result.UnitNormal.IsNearlyZero()) + { + return Result; + } + Result.Positive = VF_SampleRadialRay(AxisPoint, Result.UnitNormal, SampleDensity); + Result.Negative = VF_SampleRadialRay(AxisPoint, -Result.UnitNormal, SampleDensity); + Result.bValid = Result.Positive.bFound && Result.Negative.bFound; + return Result; + } + + float VF_SectionScore(const FRadialSection& Section) + { + return Section.bValid + ? FMath::Min(Section.Positive.CrossingRadiusVoxels, + Section.Negative.CrossingRadiusVoxels) + : -1.0f; + } + + FVector VF_PerpendicularTo(const FVector& Tangent) + { + const FVector Basis = FMath::Abs(Tangent.Z) < 0.9f + ? FVector::UpVector : FVector::RightVector; + return FVector::CrossProduct(Tangent, Basis).GetSafeNormal(); + } + + void VF_SetCandidateOnSlot( + VoxelForgeTest::FTestWorld& World, + const FVoxelStrateComposerCandidate& Candidate, + int32 SlotIndex) + { + UVoxelStrateDefinition* Definition = World.Definitions.IsValidIndex(SlotIndex) + ? World.Definitions[SlotIndex].Get() : nullptr; + if (Definition == nullptr) + { + return; + } + Definition->GeneratorType = Candidate.Archetype; + Definition->GenerationParams = Candidate.ArchetypeParams.TunnelNetworkParams; + Definition->SlabParams = Candidate.ArchetypeParams.SlabParams; + Definition->MazeParams = Candidate.ArchetypeParams.MazeParams; + Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams; + Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams; + Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams; + Definition->bUseOperatorStack = true; + Definition->TransitionType = EVoxelStrateTransition::Hard; + World.Reinitialize(); + } + + int32 VF_MidChunkZ(const VoxelForgeTest::FTestWorld& World, int32 SlotIndex) + { + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + if (!World.GetSlotVoxelZRange(SlotIndex, TopVoxelZ, BottomVoxelZ)) + { + return 0; + } + return FMath::FloorToInt( + 0.5f * static_cast(TopVoxelZ + 1 + BottomVoxelZ) + / static_cast(CHUNK_SIZE)); + } + + struct FCoreStackProbe + { + FVoxelOpStack Stack; + + void Prepare(int32 ChunkZ, float TopZ, float BottomZ, + float WorldRadiusVoxels, float EdgeSealThickness, + uint32 LayoutVersion) + { + FVoxelOpContext Context; + Context.ChunkCoord = FIntVector(0, 0, ChunkZ); + Context.Step = 1; + Context.LayoutVersion = LayoutVersion; + Context.WorldRadiusVoxels = WorldRadiusVoxels; + Context.EdgeSealThickness = EdgeSealThickness; + Context.StrateTopWorldZ = TopZ; + Context.StrateBottomWorldZ = BottomZ; + Stack.PrepareChunk(Context); + } + + float Sample(const FVector& Point) const + { + return Stack.EvalMC(Point.X, Point.Y, Point.Z); + } + }; + + bool VF_FindMazeAxis( + const VoxelForgeTest::FTestWorld& World, + const FMazeGenerationParams& Params, + int32 SlotIndex, + FRadialSection& OutFinalSection, + FRadialSection& OutCoreSection, + FString& OutDescription) + { + const float CellSize = FMath::Max(Params.CellSize, 1.0f); + const uint32 Salt = static_cast(World.Generator->Seed) ^ 0x4D617A65u; + const int32 MidChunkZ = VF_MidChunkZ(World, SlotIndex); + const int32 BaseCellZ = FMath::FloorToInt( + (0.5f * (Params.StrateTopWorldZ + Params.StrateBottomWorldZ)) / CellSize); + + FCoreStackProbe Core; + VoxelDensityOps::BuildMazeStack( + Core.Stack, Params, World.Generator->Seed, World.Generator->OriginSpineRadius, + World.StrateManager.Get(), false); + Core.Prepare(MidChunkZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ, + World.Generator->WorldRadiusVoxels, World.Generator->EdgeSealThickness, + World.StrateManager->GetLayoutVersion()); + + auto EdgeOpen = [Salt](int32 X, int32 Y, int32 Z, + uint32 AxisSalt, float Threshold) + { + uint32 H = VoxelHash::Cell(X, Y, Salt ^ AxisSalt); + H ^= VoxelHash::Mix(static_cast(Z * 73856093) ^ AxisSalt); + return VoxelHash::ToFloat01(VoxelHash::Mix(H)) < Threshold; + }; + + auto FinalDensity = [&World](const FVector& Point) + { + return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z); + }; + auto CoreDensity = [&Core](const FVector& Point) + { + return Core.Sample(Point); + }; + + float BestDistanceSq = FLT_MAX; + for (int32 Ring = 1; Ring <= 12; ++Ring) + { + for (int32 NodeY = -Ring; NodeY <= Ring; ++NodeY) + { + for (int32 NodeX = -Ring; NodeX <= Ring; ++NodeX) + { + if (FMath::Max(FMath::Abs(NodeX), FMath::Abs(NodeY)) != Ring) + { + continue; + } + for (int32 NodeZ = BaseCellZ - 1; NodeZ <= BaseCellZ + 1; ++NodeZ) + { + const FVector A( + (NodeX + 0.5f) * CellSize, + (NodeY + 0.5f) * CellSize, + (NodeZ + 0.5f) * CellSize); + const float DistanceSq = A.X * A.X + A.Y * A.Y; + + struct FAxisSpec { uint32 Salt; float Threshold; FVector Delta; const TCHAR* Name; }; + const FAxisSpec Axes[] = { + { 0xA1u, Params.BranchProbability, + FVector(CellSize, 0.0f, 0.0f), TEXT("X") }, + { 0xB2u, Params.BranchProbability, + FVector(0.0f, CellSize, 0.0f), TEXT("Y") }, + }; + for (const FAxisSpec& Axis : Axes) + { + if (!EdgeOpen(NodeX, NodeY, NodeZ, Axis.Salt, Axis.Threshold)) + { + continue; + } + const FVector B = A + Axis.Delta; + const FVector AxisPoint = 0.5f * (A + B); + if (AxisPoint.Z <= Params.StrateBottomWorldZ + Params.BoundarySealThickness + 8.0f + || AxisPoint.Z >= Params.StrateTopWorldZ - Params.BoundarySealThickness - 8.0f + || DistanceSq < FMath::Square(World.Generator->OriginSpineRadius + 24.0f)) + { + continue; + } + + const FVector Normal = VF_PerpendicularTo((B - A).GetSafeNormal()); + const FRadialSection FinalSection = VF_SampleSection( + AxisPoint, Normal, FinalDensity); + const FRadialSection CoreSection = VF_SampleSection( + AxisPoint, Normal, CoreDensity); + if (!FinalSection.bValid || !CoreSection.bValid + || FinalSection.Positive.AxisDensity <= 0.0f) + { + continue; + } + + if (DistanceSq >= BestDistanceSq) + { + continue; + } + BestDistanceSq = DistanceSq; + OutFinalSection = FinalSection; + OutCoreSection = CoreSection; + OutDescription = FString::Printf( + TEXT("Maze edge axis=%s node=(%d,%d,%d) endpointA=(%.2f,%.2f,%.2f) " + "endpointB=(%.2f,%.2f,%.2f)"), + Axis.Name, NodeX, NodeY, NodeZ, + A.X, A.Y, A.Z, B.X, B.Y, B.Z); + } + } + } + } + if (OutFinalSection.bValid) + { + return true; + } + } + return false; + } + + struct FTunnelAxisCandidate + { + bool bValid = false; + float Score = -1.0f; + float NominalRadius = 0.0f; + FString Description; + FRadialSection FinalSection; + FRadialSection CoreSection; + }; + + void VF_ConsiderTunnelSegment( + const VoxelForgeTest::FTestWorld& World, + const FStrateGenerationParams& Params, + const FCoreStackProbe& Core, + const FVector& A, + const FVector& B, + float RadiusA, + float RadiusB, + const FString& SegmentName, + FTunnelAxisCandidate& InOutBest) + { + const FVector Tangent = (B - A).GetSafeNormal(); + const float Length = FVector::Dist(A, B); + if (Tangent.IsNearlyZero() || Length < 24.0f) + { + return; + } + + const FVector Normal = VF_PerpendicularTo(Tangent); + const FVector Binormal = FVector::CrossProduct(Tangent, Normal).GetSafeNormal(); + const float NominalRadius = FMath::Lerp(RadiusA, RadiusB, 0.5f); + const int32 SearchExtent = 16; + const int32 SearchStep = 4; + const int32 MaxDistance = FMath::Clamp( + FMath::CeilToInt(NominalRadius + FMath::Max(Params.SurfaceRoughness, 0.0f) + 20.0f), + 24, RadialSampleMaxVoxels); + + auto FinalDensity = [&World](const FVector& Point) + { + return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z); + }; + auto CoreDensity = [&Core](const FVector& Point) + { + return Core.Sample(Point); + }; + + for (int32 OffsetB = -SearchExtent; OffsetB <= SearchExtent; OffsetB += SearchStep) + { + for (int32 OffsetN = -SearchExtent; OffsetN <= SearchExtent; OffsetN += SearchStep) + { + const FVector AxisPoint = 0.5f * (A + B) + + Binormal * static_cast(OffsetB) + + Normal * static_cast(OffsetN); + if (AxisPoint.Z <= Params.StrateBottomWorldZ + Params.BoundarySealThickness + 8.0f + || AxisPoint.Z >= Params.StrateTopWorldZ - Params.BoundarySealThickness - 8.0f + || FVector2D(AxisPoint.X, AxisPoint.Y).SizeSquared() + < FMath::Square(World.Generator->OriginSpineRadius + 24.0f)) + { + continue; + } + + const FRadialSection FinalSection = VF_SampleSection( + AxisPoint, Normal, FinalDensity); + if (!FinalSection.bValid) + { + continue; + } + const FRadialSection CoreSection = VF_SampleSection( + AxisPoint, Normal, CoreDensity); + if (!CoreSection.bValid) + { + continue; + } + + const float Score = VF_SectionScore(FinalSection); + if (Score <= InOutBest.Score) + { + continue; + } + InOutBest.bValid = true; + InOutBest.Score = Score; + InOutBest.NominalRadius = NominalRadius; + InOutBest.Description = FString::Printf( + TEXT("%s segment A=(%.2f,%.2f,%.2f) B=(%.2f,%.2f,%.2f) " + "axisOffset=(%.2f,%.2f,%.2f)"), + *SegmentName, A.X, A.Y, A.Z, B.X, B.Y, B.Z, + AxisPoint.X - 0.5f * (A.X + B.X), + AxisPoint.Y - 0.5f * (A.Y + B.Y), + AxisPoint.Z - 0.5f * (A.Z + B.Z)); + InOutBest.FinalSection = FinalSection; + InOutBest.CoreSection = CoreSection; + } + } + } + + bool VF_FindTunnelAxis( + const VoxelForgeTest::FTestWorld& World, + const FStrateGenerationParams& Params, + int32 SlotIndex, + FTunnelAxisCandidate& OutBest) + { + const int32 MidChunkZ = VF_MidChunkZ(World, SlotIndex); + FCoreStackProbe Core; + VoxelDensityOps::BuildTunnelNetworkStack( + Core.Stack, Params, World.Generator->Seed, World.Generator->OriginSpineRadius, + World.StrateManager.Get(), false); + Core.Prepare(MidChunkZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ, + World.Generator->WorldRadiusVoxels, World.Generator->EdgeSealThickness, + World.StrateManager->GetLayoutVersion()); + + FChunkSDFCache Cache; + const float SearchExtent = FMath::Max( + 512.0f, FMath::Max(Params.RoomSpacing * 4.0f, Params.MaxTunnelLength * 2.0f)); + VoxelCaveMorphology::BuildChunkCache( + Cache, -SearchExtent, -SearchExtent, SearchExtent, SearchExtent, + Params, static_cast(World.Generator->Seed), SlotIndex, nullptr); + + for (int32 TunnelIndex = 0; TunnelIndex < Cache.Tunnels.Num(); ++TunnelIndex) + { + const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIndex]; + if (Tunnel.bHasMidpoint) + { + VF_ConsiderTunnelSegment( + World, Params, Core, Tunnel.EndpointA, Tunnel.Midpoint, + Tunnel.RadiusA, Tunnel.RadiusMid, + FString::Printf(TEXT("tunnel[%d].A-mid"), TunnelIndex), OutBest); + VF_ConsiderTunnelSegment( + World, Params, Core, Tunnel.Midpoint, Tunnel.EndpointB, + Tunnel.RadiusMid, Tunnel.RadiusB, + FString::Printf(TEXT("tunnel[%d].mid-B"), TunnelIndex), OutBest); + } + else + { + VF_ConsiderTunnelSegment( + World, Params, Core, Tunnel.EndpointA, Tunnel.EndpointB, + Tunnel.RadiusA, Tunnel.RadiusB, + FString::Printf(TEXT("tunnel[%d]"), TunnelIndex), OutBest); + } + } + return OutBest.bValid; + } + + bool VF_FindShaftAxis( + const VoxelForgeTest::FTestWorld& World, + const FVerticalShaftParams& Params, + int32 SlotIndex, + FRadialSection& OutFinalSection, + FRadialSection& OutCoreSection, + float& OutNominalRadius, + FString& OutDescription) + { + const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); + const uint32 Salt = static_cast(World.Generator->Seed) ^ 0x53686674u; + const int32 MidChunkZ = VF_MidChunkZ(World, SlotIndex); + FCoreStackProbe Core; + VoxelDensityOps::BuildVerticalShaftStack( + Core.Stack, Params, World.Generator->Seed, World.Generator->OriginSpineRadius, + World.StrateManager.Get(), false); + Core.Prepare(MidChunkZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ, + World.Generator->WorldRadiusVoxels, World.Generator->EdgeSealThickness, + World.StrateManager->GetLayoutVersion()); + + auto FinalDensity = [&World](const FVector& Point) + { + return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z); + }; + auto CoreDensity = [&Core](const FVector& Point) + { + return Core.Sample(Point); + }; + + const float InnerBottom = Params.StrateBottomWorldZ + Params.BoundarySealThickness + 4.0f; + const float InnerTop = Params.StrateTopWorldZ - Params.BoundarySealThickness - 4.0f; + const float Period = FMath::Max(Params.LedgeSpacing, 32.0f); + for (int32 Ring = 1; Ring <= 32; ++Ring) + { + for (int32 CellY = -Ring; CellY <= Ring; ++CellY) + { + for (int32 CellX = -Ring; CellX <= Ring; ++CellX) + { + if (FMath::Max(FMath::Abs(CellX), FMath::Abs(CellY)) != Ring) + { + continue; + } + const uint32 CellHash = VoxelHash::Cell(CellX, CellY, Salt); + if (VoxelHash::ToFloat01(CellHash) > Params.ShaftDensity) + { + continue; + } + const float JitterX = VoxelHash::ToFloat01( + VoxelHash::Mix(CellHash ^ 0x12345678u)); + const float JitterY = VoxelHash::ToFloat01( + VoxelHash::Mix(CellHash ^ 0x9ABCDEF0u)); + const float ShaftX = (CellX + 0.15f + JitterX * 0.7f) * Spacing; + const float ShaftY = (CellY + 0.15f + JitterY * 0.7f) * Spacing; + const float Radius = FMath::Lerp( + Params.ShaftMinRadius, Params.ShaftMaxRadius, + VoxelHash::ToFloat01(VoxelHash::Mix(CellHash ^ 0xBEEFu))); + if (FMath::Square(ShaftX) + FMath::Square(ShaftY) + < FMath::Square(World.Generator->OriginSpineRadius + 24.0f)) + { + continue; + } + + for (int32 Level = 0; Level < 16; ++Level) + { + const float Z = InnerBottom + (static_cast(Level) + 0.5f) * Period; + if (Z >= InnerTop) + { + break; + } + const FVector AxisPoint(ShaftX, ShaftY, Z); + const FRadialSection FinalSection = VF_SampleSection( + AxisPoint, FVector(1.0f, 0.0f, 0.0f), FinalDensity); + const FRadialSection CoreSection = VF_SampleSection( + AxisPoint, FVector(1.0f, 0.0f, 0.0f), CoreDensity); + if (!FinalSection.bValid || !CoreSection.bValid) + { + continue; + } + OutFinalSection = FinalSection; + OutCoreSection = CoreSection; + OutNominalRadius = Radius; + OutDescription = FString::Printf( + TEXT("shaft cell=(%d,%d) center=(%.2f,%.2f) z=%.2f"), + CellX, CellY, ShaftX, ShaftY, Z); + return true; + } + } + } + } + return false; + } + + class FEmptyRoomSampler final : public IVoxelStrateDensitySampler + { + public: + float SampleDensity(float, float, float WorldZ) const override + { + // A 20 m square room: z=0.5 is the one-cell solid floor and every sampled + // cell above it is empty air. This is deliberately a control for the stencil, + // not a generation change. + return WorldZ < 1.0f ? -1.0f : 1.0f; + } + }; + + class FGeneratorSampler final : public IVoxelStrateDensitySampler + { + public: + explicit FGeneratorSampler(const UVoxelGenerator& InGenerator) + : Generator(InGenerator) {} + + float SampleDensity(float WorldX, float WorldY, float WorldZ) const override + { + return Generator.GetDensityAt(WorldX, WorldY, WorldZ); + } + + private: + const UVoxelGenerator& Generator; + }; + + float VF_BoundarySealForCandidate(const FVoxelStrateComposerCandidate& Candidate) + { + switch (Candidate.Archetype) + { + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + return Candidate.ArchetypeParams.TunnelNetworkParams.BoundarySealThickness; + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + return Candidate.ArchetypeParams.SlabParams.BoundarySealThickness; + case ECaveGeneratorType::Maze: + return Candidate.ArchetypeParams.MazeParams.BoundarySealThickness; + case ECaveGeneratorType::SurfaceWorld: + return Candidate.ArchetypeParams.SurfaceParams.BoundarySealThickness; + case ECaveGeneratorType::VerticalShafts: + return Candidate.ArchetypeParams.VerticalShaftParams.BoundarySealThickness; + case ECaveGeneratorType::FloatingIslands: + return Candidate.ArchetypeParams.FloatingIslandParams.BoundarySealThickness; + default: + return 0.0f; + } + } + + void VF_ReportPlayerFitRow( + FAutomationTestBase& Test, + const TCHAR* Label, + const FVoxelStrateMetrics& Metrics, + const FVoxelConnectivityDiagnostics& Diagnostics) + { + Test.AddInfo(FString::Printf( + TEXT("PLAYER_FIT step=1 fitted-window %s: fit_cells=%lld components=%d " + "largest_component_cells=%lld arrival_component_cells=%lld " + "departure_component_cells=%lld mouth_component_gap_voxels=%.3f " + "fit_fraction=%.9f traversable_share=%.9f law=%s"), + Label, + static_cast(Metrics.NumPlayerFitCells), + Metrics.NumTraversableComponents, + static_cast(Metrics.LargestTraversableComponentCells), + static_cast(Diagnostics.StartComponentCells), + static_cast(Diagnostics.GoalComponentCells), + Diagnostics.StartToGoalComponentDistanceCells, + Metrics.PlayerFitFraction, + Metrics.TraversableComponentShare, + VF_DiagnosisConnectivityName(Diagnostics.Result))); + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeScaleDiagnosisTest, + "VoxelForge.Composer.ScaleDiagnosis", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + (void)Parameters; + + UVoxelSettings* AuthoredSettings = LoadObject( + nullptr, TEXT("/Game/VoxelForge/DA_Settings.DA_Settings")); + if (AuthoredSettings == nullptr) + { + AddError(TEXT("Could not load /Game/VoxelForge/DA_Settings.DA_Settings.")); + return false; + } + + FVoxelStrateCorpus Corpus; + FString CorpusReport; + const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport); + AddInfo(CorpusReport); + TestTrue(TEXT("scale-diagnosis corpus is usable"), bCorpusLoaded && Corpus.IsValid()); + if (!bCorpusLoaded || !Corpus.IsValid()) + { + return false; + } + + FTestWorld World; + World.Build(DiagnosisSeed, 2, true, 8); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + + const FVoxelStrateComposerCandidate MazeCandidate = VF_RollStrateCandidate( + Corpus, DiagnosisSeed, MazeCandidateIndex, false); + const FVoxelStrateComposerCandidate TunnelCandidate = VF_RollStrateCandidate( + Corpus, DiagnosisSeed, TunnelCandidateIndex, false); + const FVoxelStrateComposerCandidate ShaftCandidate = VF_RollStrateCandidate( + Corpus, DiagnosisSeed, ShaftCandidateIndex, false); + TestTrue(TEXT("seed 0 index 50 is a valid Maze candidate"), + MazeCandidate.bValid && MazeCandidate.Archetype == ECaveGeneratorType::Maze); + TestTrue(TEXT("seed 0 index 61 is a valid TunnelNetwork candidate"), + TunnelCandidate.bValid && TunnelCandidate.Archetype == ECaveGeneratorType::TunnelNetwork); + TestTrue(TEXT("seed 0 index 34 is a valid VerticalShafts candidate"), + ShaftCandidate.bValid && ShaftCandidate.Archetype == ECaveGeneratorType::VerticalShafts); + + // H1 — direct GetDensityAt radial crossings. The Maze is the fourth one-based layout slot + // (FTestWorld uses zero-based SlotMaze == 3), matching the requested "slot 4". + const int32 MazeSlot = FTestWorld::SlotMaze; + VF_SetCandidateOnSlot(World, MazeCandidate, MazeSlot); + int32 MazeTop = 0; + int32 MazeBottom = 0; + TestTrue(TEXT("Maze diagnosis slot has a Z range"), + World.GetSlotVoxelZRange(MazeSlot, MazeTop, MazeBottom)); + const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk( + FIntVector(0, 0, VF_MidChunkZ(World, MazeSlot))); + FRadialSection MazeFinal; + FRadialSection MazeCore; + FString MazeAxisDescription; + const bool bHaveMazeAxis = VF_FindMazeAxis( + World, MazeParams, MazeSlot, MazeFinal, MazeCore, MazeAxisDescription); + TestTrue(TEXT("Maze diagnosis found an open corridor axis"), bHaveMazeAxis); + if (bHaveMazeAxis) + { + AddInfo(FString::Printf( + TEXT("BORE maze seed=%d candidate=%d slot=%d(one-based 4) %s: " + "parameter_radius=%.3f vox (%.3f m), parameter_bore=%.3f m; " + "GetDensityAt +ray crossing=%.3f vox (%.3f m), first_solid_step=%d " + "core_without_structural_posts +ray=%.3f vox (%.3f m); " + "-ray final=%.3f vox (%.3f m), -ray core=%.3f vox (%.3f m)"), + DiagnosisSeed, MazeCandidateIndex, MazeSlot, *MazeAxisDescription, + MazeParams.CorridorRadius, MazeParams.CorridorRadius * 0.25f, + MazeParams.CorridorRadius * 2.0f * 0.25f, + MazeFinal.Positive.CrossingRadiusVoxels, + MazeFinal.Positive.CrossingRadiusVoxels * 0.25f, + MazeFinal.Positive.FirstSolidStep, + MazeCore.Positive.CrossingRadiusVoxels, + MazeCore.Positive.CrossingRadiusVoxels * 0.25f, + MazeFinal.Negative.CrossingRadiusVoxels, + MazeFinal.Negative.CrossingRadiusVoxels * 0.25f, + MazeCore.Negative.CrossingRadiusVoxels, + MazeCore.Negative.CrossingRadiusVoxels * 0.25f)); + } + + // TunnelNetwork — use the current fixed showcase roll and recover a real cached tunnel + // segment, then sample the final GetDensityAt field at a locally centred radial section. + const int32 TunnelSlot = FTestWorld::SlotTunnelNetwork; + VF_SetCandidateOnSlot(World, TunnelCandidate, TunnelSlot); + const FStrateGenerationParams TunnelParams = World.StrateManager->GetGenerationParams( + FIntVector(0, 0, VF_MidChunkZ(World, TunnelSlot))); + FTunnelAxisCandidate TunnelAxis; + const bool bHaveTunnelAxis = VF_FindTunnelAxis( + World, TunnelParams, TunnelSlot, TunnelAxis); + TestTrue(TEXT("TunnelNetwork diagnosis found a tunnel axis"), bHaveTunnelAxis); + if (bHaveTunnelAxis) + { + AddInfo(FString::Printf( + TEXT("BORE tunnel seed=%d candidate=%d %s: parameter_radius_range=%.3f..%.3f vox " + "(%.3f..%.3f m), nominal_radius_at_probe=%.3f vox (%.3f m); " + "GetDensityAt +ray crossing=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m); " + "core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"), + DiagnosisSeed, TunnelCandidateIndex, *TunnelAxis.Description, + TunnelParams.TunnelMinRadius, TunnelParams.TunnelMaxRadius, + TunnelParams.TunnelMinRadius * 0.25f, TunnelParams.TunnelMaxRadius * 0.25f, + TunnelAxis.NominalRadius, TunnelAxis.NominalRadius * 0.25f, + TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels, + TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels * 0.25f, + TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels, + TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels * 0.25f, + TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels, + TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels * 0.25f, + TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels, + TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels * 0.25f)); + } + + // VerticalShafts — recover the deterministic shaft centre from the same hash contract as + // production, then sample horizontally at a non-ledge height. + const int32 ShaftSlot = FTestWorld::SlotVerticalShafts; + VF_SetCandidateOnSlot(World, ShaftCandidate, ShaftSlot); + const FVerticalShaftParams ShaftParams = World.StrateManager->GetVerticalShaftParamsForChunk( + FIntVector(0, 0, VF_MidChunkZ(World, ShaftSlot))); + FRadialSection ShaftFinal; + FRadialSection ShaftCore; + float ShaftNominalRadius = 0.0f; + FString ShaftAxisDescription; + const bool bHaveShaftAxis = VF_FindShaftAxis( + World, ShaftParams, ShaftSlot, ShaftFinal, ShaftCore, + ShaftNominalRadius, ShaftAxisDescription); + TestTrue(TEXT("VerticalShafts diagnosis found a shaft axis"), bHaveShaftAxis); + if (bHaveShaftAxis) + { + AddInfo(FString::Printf( + TEXT("BORE shaft seed=%d candidate=%d %s: parameter_radius_range=%.3f..%.3f vox " + "(%.3f..%.3f m), nominal_radius_at_probe=%.3f vox (%.3f m); " + "GetDensityAt +ray crossing=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m); " + "core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"), + DiagnosisSeed, ShaftCandidateIndex, *ShaftAxisDescription, + ShaftParams.ShaftMinRadius, ShaftParams.ShaftMaxRadius, + ShaftParams.ShaftMinRadius * 0.25f, ShaftParams.ShaftMaxRadius * 0.25f, + ShaftNominalRadius, ShaftNominalRadius * 0.25f, + ShaftFinal.Positive.CrossingRadiusVoxels, + ShaftFinal.Positive.CrossingRadiusVoxels * 0.25f, + ShaftFinal.Negative.CrossingRadiusVoxels, + ShaftFinal.Negative.CrossingRadiusVoxels * 0.25f, + ShaftCore.Positive.CrossingRadiusVoxels, + ShaftCore.Positive.CrossingRadiusVoxels * 0.25f, + ShaftCore.Negative.CrossingRadiusVoxels, + ShaftCore.Negative.CrossingRadiusVoxels * 0.25f)); + } + + // H2 — a known-open control with a solid floor. This must produce non-zero fit cells. + FEmptyRoomSampler EmptyRoom; + FVoxelStrateMeasureSettings ControlSettings; + ControlSettings.SampleStep = 1; + ControlSettings.RadiusInVoxels = 40; // 80 voxels = 20 m across. + ControlSettings.CenterXY = FVector2D::ZeroVector; + ControlSettings.MaxCells = 300000; + ControlSettings.HeadroomCells = 2; + ControlSettings.InteriorMarginVoxels = 0; + FVoxelStrateMetrics ControlMetrics = VF_MeasureStrateWithSampler( + EmptyRoom, 0, 32, 0.0f, ControlSettings, nullptr); + TestTrue(TEXT("empty-room player-fit control resolved"), + ControlMetrics.bValid && ControlMetrics.bPlayerFitResolved); + AddInfo(FString::Printf( + TEXT("CONTROL empty_room=20m_square_with_one_cell_floor step=1: " + "sampled=%lld air=%lld fit_cells=%lld fit_fraction=%.9f " + "components=%d largest_component_cells=%lld traversable_share=%.9f"), + static_cast(ControlMetrics.NumSampled), + static_cast(ControlMetrics.NumAir), + static_cast(ControlMetrics.NumPlayerFitCells), + ControlMetrics.PlayerFitFraction, + ControlMetrics.NumTraversableComponents, + static_cast(ControlMetrics.LargestTraversableComponentCells), + ControlMetrics.TraversableComponentShare)); + AddInfo(FString::Printf( + TEXT("PLAYER_STENCIL radius=%.3f voxels (%.2f m), half_height=%.3f voxels " + "(%.2f m), height=%.3f voxels (%.2f m); rows=ceil(height)=%d " + "relative rows 0..%d; floor=air anchor with exactly one solid cell at Z-1; " + "no floor tolerance; horizontal integer offsets are center plus four cardinals " + "in rows 0..6 and center-only forced in row 7; HeadroomCells=%d is legacy-only"), + ControlSettings.PlayerCapsuleRadiusVoxels, + ControlSettings.PlayerCapsuleRadiusVoxels * 0.25f, + ControlSettings.PlayerCapsuleHalfHeightVoxels, + ControlSettings.PlayerCapsuleHalfHeightVoxels * 0.25f, + FVoxelPlayerCapsuleConstants::HeightVoxels, + FVoxelPlayerCapsuleConstants::HeightVoxels * 0.25f, + FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels), + FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels) - 1, + ControlSettings.HeadroomCells)); + + // H3 — the eight fixed showcase identities, measured at step 1 on a mouth-fitted window. + // The Maze identity is intentionally the requested seed 0 / index 50 / fourth one-based slot. + struct FShowcaseSelection + { + ECaveGeneratorType Archetype; + int32 Seed; + int32 CandidateIndex; + int32 TargetSlot; + const TCHAR* Label; + }; + const FShowcaseSelection Selections[] = { + { ECaveGeneratorType::CrystalChamber, 0, 36, FTestWorld::SlotFlatPlain, TEXT("CrystalChamber") }, + { ECaveGeneratorType::FlatPlain, 0, 11, FTestWorld::SlotFlatPlain, TEXT("FlatPlain") }, + { ECaveGeneratorType::FloatingIslands, 0, 15, FTestWorld::SlotFlatPlain, TEXT("FloatingIslands") }, + { ECaveGeneratorType::Maze, 0, 50, FTestWorld::SlotMaze, TEXT("Maze") }, + { ECaveGeneratorType::SurfaceWorld, 0, 10, FTestWorld::SlotSurfaceWorld, TEXT("SurfaceWorld") }, + { ECaveGeneratorType::TunnelNetwork, 0, 61, FTestWorld::SlotFlatPlain, TEXT("TunnelNetwork") }, + { ECaveGeneratorType::Underwater, 0, 63, FTestWorld::SlotFlatPlain, TEXT("Underwater") }, + { ECaveGeneratorType::VerticalShafts, 0, 34, FTestWorld::SlotFlatPlain, TEXT("VerticalShafts") }, + }; + + for (const FShowcaseSelection& Selection : Selections) + { + const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate( + Corpus, Selection.Seed, Selection.CandidateIndex, false); + TestTrue(FString::Printf(TEXT("H3 %s candidate identity is valid"), Selection.Label), + Candidate.bValid && Candidate.Archetype == Selection.Archetype); + if (!Candidate.bValid || Candidate.Archetype != Selection.Archetype) + { + continue; + } + VF_SetCandidateOnSlot(World, Candidate, Selection.TargetSlot); + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + int32 ArrivalCount = 0; + int32 DepartureCount = 0; + for (const FVoxelPassage& Passage : World.StrateManager->GetPassages()) + { + if (Passage.LowerStrateIndex == Selection.TargetSlot + && Passage.UpperStrateIndex + 1 == Selection.TargetSlot) + { + ArrivalPoint = Passage.LowerPoint; + ++ArrivalCount; + } + if (Passage.UpperStrateIndex == Selection.TargetSlot + && Passage.LowerStrateIndex == Selection.TargetSlot + 1) + { + DeparturePoint = Passage.UpperPoint; + ++DepartureCount; + } + } + if (ArrivalCount != 1 || DepartureCount != 1) + { + AddInfo(FString::Printf( + TEXT("PLAYER_FIT step=1 fitted-window %s: mouths unavailable arrival=%d departure=%d"), + Selection.Label, ArrivalCount, DepartureCount)); + continue; + } + + FVoxelStrateMeasureSettings Settings; + Settings.SampleStep = 1; + Settings.RadiusInVoxels = 64; + Settings.CenterXY = FVector2D( + 0.5f * (ArrivalPoint.X + DeparturePoint.X), + 0.5f * (ArrivalPoint.Y + DeparturePoint.Y)); + Settings.CoverPointA = FVector2D(ArrivalPoint.X, ArrivalPoint.Y); + Settings.CoverPointB = FVector2D(DeparturePoint.X, DeparturePoint.Y); + Settings.CoverMarginVoxels = FMath::CeilToFloat( + Settings.PlayerCapsuleRadiusVoxels) + 2.0f; + Settings.MaxCells = FineDiagnosisMaxCells; + Settings.MaxRouteRetries = 16; + Settings.HeadroomCells = 2; + Settings.InteriorMarginVoxels = -1; + + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + if (!World.GetSlotVoxelZRange(Selection.TargetSlot, TopVoxelZ, BottomVoxelZ)) + { + AddError(FString::Printf(TEXT("PLAYER_FIT %s: target slot has no Z range."), + Selection.Label)); + continue; + } + FGeneratorSampler GeneratorSampler(*World.Generator); + FVoxelStrateMetrics Metrics; + const FVoxelConnectivityDiagnostics Diagnostics = + VF_DiagnosePlayerFitConnectivityWithSampler( + GeneratorSampler, BottomVoxelZ, TopVoxelZ + 1, + VF_BoundarySealForCandidate(Candidate), ArrivalPoint, DeparturePoint, + Settings, &Metrics); + VF_ReportPlayerFitRow(*this, Selection.Label, Metrics, Diagnostics); + } + + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR