From cd31dec8692e33947d36cb5e6b265e7f53bee010 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 30 Aug 2026 14:24:24 +0200 Subject: [PATCH] test(measure): the VerticalShafts disconnection is real, not a coarse lie Our guard was one-directional. FullResolutionPathIsAir catches the false POSITIVE -- coarse says joined, reality says walled -- which is the dangerous direction for safety, so we built it first. The opposite error was unguarded: a corridor narrower than SampleStep falls between sample points and vanishes, so connected regions read as separate. That is exactly what a network of narrow vertical shafts would fake at step 4. Refinement sweep, radius held constant for the controls: step 4 / r256 NOT_CONNECTED 57 comp largest .119 458,752 cells step 4 / r192 NOT_CONNECTED 32 comp largest .220 258,048 step 2 / r192 NOT_CONNECTED 27 comp largest .220 2,064,384 step 4 / r128 NOT_CONNECTED 20 comp largest .291 114,688 step 1 / r128 NOT_CONNECTED 11 comp largest .303 7,340,032 Both fixed-radius pairs stayed negative, so neither the step nor the radius explains it. At one-voxel sampling the arrival and departure mouths are still in different air components. VerticalShafts is a live primordial-law violation: a player descends into it and cannot reach its exit. NotConnected is renamed NotConnectedAtThisResolution and documents its own limit at the declaration, so a bare negative can never again be read as proof. CoarseLied names one direction; this names the other. Sweep split into VoxelForge.Generation.StrateConnectivityRefinement so the fast test stays fast. 4.4 s. The MaxCells refusal path was exercised and still refuses. Build clean, 19 succeeded / 0 warnings / 0 failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM --- CODEMAP.md | 8 +- .../VoxelForgeStrateConnectivityTest.cpp | 247 +++++++++++++++++- .../VoxelForge/Private/VoxelStrateMeasure.cpp | 2 +- Source/VoxelForge/Public/VoxelStrateMeasure.h | 6 +- 4 files changed, 252 insertions(+), 11 deletions(-) diff --git a/CODEMAP.md b/CODEMAP.md index f9a94d3..1d32c62 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -312,13 +312,15 @@ representative point, cell count, and count of components holding at least 1% of `VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks only that route at full voxel resolution before returning `Connected`. It returns an explicit `EVoxelConnectivityResult`; a solid endpoint cell is repaired by snapping to the nearest air cell -among its 26 neighbours when possible, and reports the snap through its out flags. No UObject state, -cache, actor, world, or PIE is required. +among its 26 neighbours when possible, and reports the snap through its out flags. A missing coarse +route returns `NotConnectedAtThisResolution`: that is evidence only at the grid's resolution, +because a corridor thinner than `SampleStep` is invisible. No UObject state, cache, actor, world, +or PIE is required. | Symbol | Role | |--------|------| | `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin, and results identify the resolved margin/Z window and deterministic component representatives. | | `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; refuses invalid bounds or a grid over `MaxCells`. | -| `VF_AreConnected` | Coarse 6-connected BFS plus one full-resolution recheck of its recovered route, with explicit endpoint-solid, out-of-window, coarse-lie, and connectivity outcomes. | +| `VF_AreConnected` | Coarse 6-connected BFS plus one full-resolution recheck of its recovered route, with explicit endpoint-solid, out-of-window, `CoarseLied`, `NotConnectedAtThisResolution`, and connected outcomes. | ### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp` Header is rich with inline docs. Two namespaces + a per-chunk cache system. diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index a79d4a6..6ca6f39 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -3,6 +3,7 @@ #if WITH_DEV_AUTOMATION_TESTS +#include "HAL/PlatformTime.h" #include "Misc/AutomationTest.h" #include "VoxelForgeTestFixture.h" @@ -13,6 +14,11 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( "VoxelForge.Generation.StrateConnectivity", EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeStrateConnectivityRefinementTest, + "VoxelForge.Generation.StrateConnectivityRefinement", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + namespace { struct FArchetypeReport @@ -99,7 +105,8 @@ namespace switch (Result) { case EVoxelConnectivityResult::Connected: return TEXT("CONNECTED"); - case EVoxelConnectivityResult::NotConnected: return TEXT("NOT_CONNECTED"); + case EVoxelConnectivityResult::NotConnectedAtThisResolution: + return TEXT("NOT_CONNECTED_AT_THIS_RESOLUTION"); case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID"); case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID"); case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW"); @@ -234,6 +241,40 @@ namespace / static_cast(OutSampleCount)) : 0.0f; } + + struct FRefinementSweepRow + { + int32 SampleStep = 0; + int32 RadiusInVoxels = 0; + FVoxelStrateMetrics Metrics; + FConnectivityProbe Probe; + }; + + bool FindChainMouths( + const TArray& Passages, + int32 StrateIndex, + FVector& OutArrivalPoint, + FVector& OutDeparturePoint) + { + int32 ArrivalCount = 0; + int32 DepartureCount = 0; + for (const FVoxelPassage& Passage : Passages) + { + if (Passage.LowerStrateIndex == StrateIndex + && Passage.UpperStrateIndex + 1 == StrateIndex) + { + OutArrivalPoint = Passage.LowerPoint; + ++ArrivalCount; + } + if (Passage.UpperStrateIndex == StrateIndex + && Passage.LowerStrateIndex == StrateIndex + 1) + { + OutDeparturePoint = Passage.UpperPoint; + ++DepartureCount; + } + } + return ArrivalCount == 1 && DepartureCount == 1; + } } bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) @@ -348,6 +389,25 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) } World.DiffLayer->Clear(); + // The refinement test deliberately approaches MaxCells at step 1. Keep a separate, + // cheap over-cap control here so a future allocation/refusal regression cannot hide behind + // the connectivity API's endpoint result. + FVoxelStrateMeasureSettings TooLargeSettings = Settings; + TooLargeSettings.SampleStep = 1; + TooLargeSettings.RadiusInVoxels = 256; + const FVoxelStrateMetrics TooLargeMetrics = VF_MeasureStrate( + *World.Generator, *World.StrateManager, 0, TooLargeSettings); + if (TooLargeMetrics.bValid + || !TooLargeMetrics.RefusalReason.Contains(TEXT("MaxCells"))) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: the over-cap grid was not refused by MaxCells (valid=%s, " + "reason='%s')."), + TooLargeMetrics.bValid ? TEXT("true") : TEXT("false"), + *TooLargeMetrics.RefusalReason)); + bAllChecksPassed = false; + } + TArray ArchetypeReports; const TArray& Layout = World.StrateManager->GetLayout(); ArchetypeReports.Reserve(Layout.Num()); @@ -448,6 +508,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) GapAirFraction, GapAirSamples, GapSamples); + Summary += FString::Printf( + TEXT("MaxCells refusal control: over-cap measurement refused with reason '%s'.\n"), + *TooLargeMetrics.RefusalReason); Summary += TEXT("Archetypes (old one-chunk vs derived window; derived InteriorMarginVoxels<0 => 2x BoundarySealThickness, clamped):\n"); Summary += TEXT(" name | old margin | old Z span [min,max) | old Air | old Largest | old LargestCells | old >=1% | old Walkable | old FeatureScale vox | old Clearance vox | old Components | old Samples | old Solid | derived margin | derived Z span [min,max) | derived Air | derived Largest | derived LargestCells | derived >=1% | derived Walkable | derived FeatureScale vox | derived Clearance vox | derived Components | derived Samples | derived Solid\n"); for (const FArchetypeReport& Report : ArchetypeReports) @@ -496,7 +559,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) int32 NumAnchorProbes = 0; int32 NumAnchorConnected = 0; - int32 NumAnchorNotConnected = 0; + int32 NumAnchorNotConnectedAtThisResolution = 0; int32 NumAnchorStartCellSolid = 0; int32 NumAnchorGoalCellSolid = 0; int32 NumAnchorOutOfWindow = 0; @@ -552,7 +615,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) switch (Probe.Result) { case EVoxelConnectivityResult::Connected: ++NumAnchorConnected; break; - case EVoxelConnectivityResult::NotConnected: ++NumAnchorNotConnected; break; + case EVoxelConnectivityResult::NotConnectedAtThisResolution: + ++NumAnchorNotConnectedAtThisResolution; + break; case EVoxelConnectivityResult::StartCellSolid: ++NumAnchorStartCellSolid; break; case EVoxelConnectivityResult::GoalCellSolid: ++NumAnchorGoalCellSolid; break; case EVoxelConnectivityResult::OutOfWindow: ++NumAnchorOutOfWindow; break; @@ -836,11 +901,11 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) NumArrivalDepartureRoutesChecked); Summary += FString::Printf( TEXT("Correct largest-component endpoint probes (%d, including the surface-entry auxiliary): " - "connected=%d, not-connected=%d, start-cell-solid=%d, goal-cell-solid=%d, " + "connected=%d, not-connected-at-resolution=%d, start-cell-solid=%d, goal-cell-solid=%d, " "out-of-window=%d, coarse-lied=%d, snapped endpoint events=%d.\n"), NumAnchorProbes, NumAnchorConnected, - NumAnchorNotConnected, + NumAnchorNotConnectedAtThisResolution, NumAnchorStartCellSolid, NumAnchorGoalCellSolid, NumAnchorOutOfWindow, @@ -926,4 +991,176 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) return bAllChecksPassed; } +bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + + FTestWorld World; + World.Build(/*InSeed=*/1337, /*InGapChunks=*/2); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + + const TArray& Layout = World.StrateManager->GetLayout(); + const int32 VerticalShaftsIndex = FTestWorld::SlotVerticalShafts; + if (!Layout.IsValidIndex(VerticalShaftsIndex) + || Layout[VerticalShaftsIndex].Definition == nullptr + || Layout[VerticalShaftsIndex].Definition->GeneratorType + != ECaveGeneratorType::VerticalShafts) + { + AddError(TEXT("HARD FAILURE: the pinned VerticalShafts fixture slot is missing or changed.")); + return false; + } + + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + if (!FindChainMouths( + World.StrateManager->GetPassages(), + VerticalShaftsIndex, + ArrivalPoint, + DeparturePoint)) + { + AddError(TEXT("HARD FAILURE: VerticalShafts does not have exactly one arrival and one " + "departure mouth between consecutive strates.")); + return false; + } + + FVoxelStrateMeasureSettings BaseSettings; + BaseSettings.CenterXY = FVector2D::ZeroVector; + BaseSettings.MaxCells = 8000000; + BaseSettings.HeadroomCells = 2; + BaseSettings.InteriorMarginVoxels = -1; + + struct FRefinementCase + { + int32 SampleStep; + int32 RadiusInVoxels; + }; + static constexpr FRefinementCase Cases[] = { + {4, 256}, + {4, 192}, + {2, 192}, + {4, 128}, + {1, 128}, + }; + + bool bAllChecksPassed = true; + TArray Rows; + Rows.Reserve(UE_ARRAY_COUNT(Cases)); + + const double SweepStartSeconds = FPlatformTime::Seconds(); + for (const FRefinementCase& TestCase : Cases) + { + FRefinementSweepRow& Row = Rows.AddDefaulted_GetRef(); + Row.SampleStep = TestCase.SampleStep; + Row.RadiusInVoxels = TestCase.RadiusInVoxels; + + FVoxelStrateMeasureSettings RowSettings = BaseSettings; + RowSettings.SampleStep = TestCase.SampleStep; + RowSettings.RadiusInVoxels = TestCase.RadiusInVoxels; + + Row.Metrics = VF_MeasureStrate( + *World.Generator, + *World.StrateManager, + VerticalShaftsIndex, + RowSettings); + + Row.Probe.bChecked = true; + Row.Probe.Result = VF_AreConnected( + *World.Generator, + *World.StrateManager, + VerticalShaftsIndex, + ArrivalPoint, + DeparturePoint, + RowSettings, + Row.Probe.bStartSnapped, + Row.Probe.bGoalSnapped); + + if (!Row.Metrics.bValid + || Row.Metrics.NumSampled <= 0 + || Row.Metrics.NumSampled > BaseSettings.MaxCells + || Row.Metrics.NumAir <= 0 + || Row.Metrics.NumSolid <= 0) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: VerticalShafts refinement row step=%d radius=%d did not " + "produce a bounded non-vacuous measurement (valid=%s, reason='%s', " + "cells=%lld, air=%lld, solid=%lld)."), + Row.SampleStep, + Row.RadiusInVoxels, + Row.Metrics.bValid ? TEXT("true") : TEXT("false"), + *Row.Metrics.RefusalReason, + Row.Metrics.NumSampled, + Row.Metrics.NumAir, + Row.Metrics.NumSolid)); + bAllChecksPassed = false; + } + + if (Row.Probe.Result == EVoxelConnectivityResult::OutOfWindow + || Row.Probe.Result == EVoxelConnectivityResult::StartCellSolid + || Row.Probe.Result == EVoxelConnectivityResult::GoalCellSolid) + { + const FString ProbeText = ConnectivityProbeText(Row.Probe); + AddError(FString::Printf( + TEXT("HARD FAILURE: VerticalShafts refinement row step=%d radius=%d could " + "not query both in-window air mouths: %s."), + Row.SampleStep, + Row.RadiusInVoxels, + *ProbeText)); + bAllChecksPassed = false; + } + + if (Row.Probe.bStartSnapped || Row.Probe.bGoalSnapped) + { + AddError(FString::Printf( + TEXT("HARD FAILURE: VerticalShafts refinement row step=%d radius=%d snapped " + "an endpoint; the controlled comparison is not a mouth-to-mouth query."), + Row.SampleStep, + Row.RadiusInVoxels)); + bAllChecksPassed = false; + } + + } + const double SweepSeconds = FPlatformTime::Seconds() - SweepStartSeconds; + + FString Summary = TEXT( + "VerticalShafts arrival->departure refinement (seed 1337; source mouth pair; " + "derived interior window):\n"); + Summary += TEXT( + " SampleStep | RadiusInVoxels | verdict | components | largest share | cells | margin | " + "Z window | endpoint snaps\n"); + for (const FRefinementSweepRow& Row : Rows) + { + const FString VerdictText = ConnectivityProbeText(Row.Probe); + FString SnapText = TEXT("none"); + if (Row.Probe.bStartSnapped || Row.Probe.bGoalSnapped) + { + SnapText = ConnectivityProbeText(Row.Probe); + } + Summary += FString::Printf( + TEXT(" %d | %d | %s | %d | %.9g | %lld | %d | [%d,%d) | %s\n"), + Row.SampleStep, + Row.RadiusInVoxels, + *VerdictText, + Row.Metrics.NumAirComponents, + Row.Metrics.LargestComponentShare, + Row.Metrics.NumSampled, + Row.Metrics.ResolvedMarginVoxels, + Row.Metrics.SampledMinZ, + Row.Metrics.SampledMaxZ, + *SnapText); + } + Summary += TEXT( + "Controlled comparisons hold radius constant: (step 4, radius 192) vs (step 2, " + "radius 192), and (step 4, radius 128) vs (step 1, radius 128).\n"); + Summary += FString::Printf( + TEXT("VerticalShafts refinement sweep wall-clock: %.3f seconds.\n"), + SweepSeconds); + AddInfo(Summary); + + return bAllChecksPassed; +} + #endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index 97b97d5..6af34f5 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -882,7 +882,7 @@ EVoxelConnectivityResult VF_AreConnected( TArray Path; if (!VoxelStrateMeasurePrivate::FindCoarsePath(Grid, Start, Goal, Path)) { - return EVoxelConnectivityResult::NotConnected; + return EVoxelConnectivityResult::NotConnectedAtThisResolution; } // The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel spacing diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index 506a915..8cb65cc 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -11,7 +11,7 @@ class UVoxelStrateManager; enum class EVoxelConnectivityResult : uint8 { Connected, - NotConnected, + NotConnectedAtThisResolution, // Evidence only at this grid's resolution; corridors thinner than SampleStep are invisible. StartCellSolid, GoalCellSolid, OutOfWindow, @@ -74,7 +74,9 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate( * If an endpoint's own coarse cell is solid, the query searches the 26 neighbouring cells and * uses the nearest air cell when one exists. The snap flags report those repairs. Solid-cell * results are returned only when no adjacent air cell exists. A coarse route that fails its - * full-resolution re-check returns CoarseLied. + * full-resolution re-check returns CoarseLied; a missing coarse route returns + * NotConnectedAtThisResolution because a corridor thinner than SampleStep is invisible to the + * grid. A bare "not connected" result is evidence only at the grid's own resolution. */ VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected( const UVoxelGenerator& Generator,