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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-08-30 14:24:24 +02:00
1 parent efcc8fcb65
commit cd31dec869
4 files changed
+252 -11

No files matched your search

+5 -3
View File
@@ -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.
@@ -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<double>(OutSampleCount))
: 0.0f;
}
struct FRefinementSweepRow
{
int32 SampleStep = 0;
int32 RadiusInVoxels = 0;
FVoxelStrateMetrics Metrics;
FConnectivityProbe Probe;
};
bool FindChainMouths(
const TArray<FVoxelPassage>& 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<FArchetypeReport> ArchetypeReports;
const TArray<FStrateSlot>& 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<FStrateSlot>& 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<FRefinementSweepRow> 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
@@ -882,7 +882,7 @@ EVoxelConnectivityResult VF_AreConnected(
TArray<int32> 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
@@ -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,