fix(measure): a blocked route is not a disconnected pair

VF_AreConnected took the shortest coarse BFS route, re-walked it at full
resolution, and on hitting a wall returned false and gave up -- never
looking for another way round. That is a false NEGATIVE, and it was
inflating the failure count of a bug we were actively trying to fix.

On refutation it now excludes that specific coarse step and re-searches,
up to a bounded budget (16). Outcomes are distinguished honestly:
  Connected                   a route survived full-resolution verification
  NotConnectedAtThisResolution no coarse route remains after exclusions
  CoarseLiedBudgetExhausted    UNKNOWN -- routes kept being refuted
The third is deliberately NOT folded into either other verdict. Collapsing
"unknown" into "disconnected" is precisely what misled us.

  arrival->departure   7/16 -> 14/16
  All 7 former COARSE_LIED became CONNECTED.
  arrival->spine   15/16 connected      departure->spine 15/16 connected
  per-archetype law: 6/6 applicable routes pass

Retries needed are small -- max 3 per query, 0 budget hits -- so 16 is
generous rather than load-bearing. Runtime slightly IMPROVED (45.3 -> 44.9 s
on the 16-seed sweep) because a satisfied query stops early.

The false-POSITIVE guard is untouched and is now stronger in meaning:
every Connected still requires a fully re-walked, air-verified route, and
it now says "this specific route is open" rather than "the first route I
tried was open". The controlled wall still returns BudgetExhausted after
exactly 16 retries, and a repeat query is bit-identical in verdict, snap
flags and retry count.

⚠️ REMAINING: seeds 6 and 14 are genuinely NOT_CONNECTED. Real or a
step-2 resolution limit is not yet settled -- refine before touching
generation again, per COMPOSER-NOTES §13.

No generation, placement or parameter changed. Verifier only.
Build clean, 20 succeeded / 0 warnings / 0 failed, confirmed in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-03 23:56:25 +02:00
1 parent 271bbdd034
commit 44938f3f98
4 files changed
+340 -65

No files matched your search

+12 -7
View File
@@ -311,17 +311,22 @@ walkability, feature scale, and clearance from that grid. Metrics report the res
the inclusive/exclusive voxel Z window used, plus the largest component's deterministic lowest-cell the inclusive/exclusive voxel Z window used, plus the largest component's deterministic lowest-cell
representative point, cell count, and count of components holding at least 1% of the air. representative point, cell count, and count of components holding at least 1% of the air.
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks `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 candidate routes at full voxel resolution before returning `Connected`. When a candidate is
`EVoxelConnectivityResult`; a solid endpoint cell is repaired by snapping to the nearest air cell refuted, its specific coarse cell edge is added to a deterministic array blocklist and BFS is
among its 26 neighbours when possible, and reports the snap through its out flags. A missing coarse retried up to `MaxRouteRetries` (default 16). It returns an explicit `EVoxelConnectivityResult`;
route returns `NotConnectedAtThisResolution`: that is evidence only at the grid's resolution, a solid endpoint cell is repaired by snapping to the nearest air cell among its 26 neighbours
because a corridor thinner than `SampleStep` is invisible. No UObject state, cache, actor, world, when possible, and reports the snap through its out flags. A missing coarse route after edge
or PIE is required. exclusions returns `NotConnectedAtThisResolution`: that is evidence only at the grid's
resolution, because a corridor thinner than `SampleStep` is invisible. Exhausting the retry
budget (or finding a refutation in an endpoint segment with no coarse edge to exclude) returns
`CoarseLiedBudgetExhausted`, which is unknown rather than disconnected. Every `Connected` result
has a full-resolution air-verified route, and the query reports the alternate route retry count.
No UObject state, cache, actor, world, or PIE is required.
| Symbol | Role | | Symbol | Role |
|--------|------| |--------|------|
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal), and results identify the resolved margin/Z window and deterministic component representatives. | | `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal), 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_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, `CoarseLied`, `NotConnectedAtThisResolution`, and connected outcomes; the diagnostics overload returns endpoint component facts from that same grid. | | `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. | | `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp` ### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
@@ -42,6 +42,7 @@ namespace
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow; EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bStartSnapped = false; bool bStartSnapped = false;
bool bGoalSnapped = false; bool bGoalSnapped = false;
int32 NumRouteRetries = 0;
}; };
struct FStrateConnectivityReport struct FStrateConnectivityReport
@@ -115,11 +116,40 @@ namespace
case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID"); case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID");
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID"); case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW"); case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW");
case EVoxelConnectivityResult::CoarseLied: return TEXT("COARSE_LIED"); case EVoxelConnectivityResult::CoarseLiedBudgetExhausted:
return TEXT("COARSE_LIED_BUDGET_EXHAUSTED");
default: return TEXT("UNKNOWN"); default: return TEXT("UNKNOWN");
} }
} }
constexpr int32 ConnectivityResultCount =
static_cast<int32>(EVoxelConnectivityResult::CoarseLiedBudgetExhausted) + 1;
int32 NumRefutedRoutes(const FConnectivityProbe& Probe)
{
switch (Probe.Result)
{
case EVoxelConnectivityResult::Connected:
case EVoxelConnectivityResult::NotConnectedAtThisResolution:
return Probe.NumRouteRetries;
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted:
return Probe.NumRouteRetries + 1;
default:
return 0;
}
}
bool ConnectivityProbesAreBitIdentical(
const FConnectivityProbe& A,
const FConnectivityProbe& B)
{
return A.bChecked == B.bChecked
&& A.Result == B.Result
&& A.bStartSnapped == B.bStartSnapped
&& A.bGoalSnapped == B.bGoalSnapped
&& A.NumRouteRetries == B.NumRouteRetries;
}
FString ConnectivityProbeText(const FConnectivityProbe& Probe) FString ConnectivityProbeText(const FConnectivityProbe& Probe)
{ {
if (!Probe.bChecked) if (!Probe.bChecked)
@@ -127,7 +157,10 @@ namespace
return TEXT("NOT_CHECKED"); return TEXT("NOT_CHECKED");
} }
FString Text = ConnectivityResultName(Probe.Result); FString Text = FString::Printf(
TEXT("%s [retries=%d]"),
ConnectivityResultName(Probe.Result),
Probe.NumRouteRetries);
if (Probe.bStartSnapped || Probe.bGoalSnapped) if (Probe.bStartSnapped || Probe.bGoalSnapped)
{ {
Text += TEXT(" ["); Text += TEXT(" [");
@@ -557,6 +590,8 @@ namespace
OutReport.SpineComponentProbe.bStartSnapped, OutReport.SpineComponentProbe.bStartSnapped,
OutReport.SpineComponentProbe.bGoalSnapped, OutReport.SpineComponentProbe.bGoalSnapped,
OutReport.SpineComponent); OutReport.SpineComponent);
OutReport.SpineComponentProbe.NumRouteRetries =
OutReport.SpineComponent.NumRouteRetries;
} }
if (!FindDiagnosticMouths(Manager.GetPassages(), StrateIndex, OutReport)) if (!FindDiagnosticMouths(Manager.GetPassages(), StrateIndex, OutReport))
@@ -584,6 +619,7 @@ namespace
OutProbe.bStartSnapped, OutProbe.bStartSnapped,
OutProbe.bGoalSnapped, OutProbe.bGoalSnapped,
OutFacts); OutFacts);
OutProbe.NumRouteRetries = OutFacts.NumRouteRetries;
}; };
if (OutReport.bHasArrival) if (OutReport.bHasArrival)
@@ -613,7 +649,8 @@ namespace
OutReport.DeparturePoint, OutReport.DeparturePoint,
Settings, Settings,
OutReport.ArrivalToDeparture.bStartSnapped, OutReport.ArrivalToDeparture.bStartSnapped,
OutReport.ArrivalToDeparture.bGoalSnapped); OutReport.ArrivalToDeparture.bGoalSnapped,
OutReport.ArrivalToDeparture.NumRouteRetries);
} }
return true; return true;
} }
@@ -791,7 +828,8 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
* 3. Range sanity: fractions/components must describe an actual air field. * 3. Range sanity: fractions/components must describe an actual air field.
* 4. Determinism: identical inputs must produce bit-identical metrics. * 4. Determinism: identical inputs must produce bit-identical metrics.
* 5. Coarse-lie guard: a coarse route must never be reported CONNECTED after a * 5. Coarse-lie guard: a coarse route must never be reported CONNECTED after a
* full-resolution solid sample; the controlled 3-voxel wall below must return COARSE_LIED. * full-resolution solid sample; the controlled 3-voxel wall below must never return
* CONNECTED, and the bounded retry outcome must remain explicitly unknown.
*/ */
int32 GapTopChunkZ = 0; int32 GapTopChunkZ = 0;
@@ -1029,6 +1067,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
int32 NumRoutesChecked = 0; int32 NumRoutesChecked = 0;
int32 NumRoutesRefuted = 0; int32 NumRoutesRefuted = 0;
int32 NumRouteRetriesUsed = 0;
int32 MaxRouteRetriesUsed = 0;
int32 NumBudgetExhausted = 0;
int32 NumGuardRoutesChecked = 0; int32 NumGuardRoutesChecked = 0;
int32 NumGuardRoutesRefuted = 0; int32 NumGuardRoutesRefuted = 0;
int32 NumSnappedEndpointEvents = 0; int32 NumSnappedEndpointEvents = 0;
@@ -1041,7 +1082,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
int32 NumAnchorStartCellSolid = 0; int32 NumAnchorStartCellSolid = 0;
int32 NumAnchorGoalCellSolid = 0; int32 NumAnchorGoalCellSolid = 0;
int32 NumAnchorOutOfWindow = 0; int32 NumAnchorOutOfWindow = 0;
int32 NumAnchorCoarseLied = 0; int32 NumAnchorBudgetExhausted = 0;
int32 NumAnchorSnappedEndpointEvents = 0; int32 NumAnchorSnappedEndpointEvents = 0;
auto ProbeRoute = [&](int32 StrateIndex, auto ProbeRoute = [&](int32 StrateIndex,
@@ -1061,20 +1102,19 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
Goal, Goal,
DerivedWindowSettings, DerivedWindowSettings,
Probe.bStartSnapped, Probe.bStartSnapped,
Probe.bGoalSnapped); Probe.bGoalSnapped,
Probe.NumRouteRetries);
++NumRoutesChecked; ++NumRoutesChecked;
if (Probe.Result == EVoxelConnectivityResult::CoarseLied) NumRoutesRefuted += NumRefutedRoutes(Probe);
{ NumRouteRetriesUsed += Probe.NumRouteRetries;
++NumRoutesRefuted; MaxRouteRetriesUsed = FMath::Max(MaxRouteRetriesUsed, Probe.NumRouteRetries);
} NumBudgetExhausted +=
Probe.Result == EVoxelConnectivityResult::CoarseLiedBudgetExhausted ? 1 : 0;
if (bCountInGuard) if (bCountInGuard)
{ {
++NumGuardRoutesChecked; ++NumGuardRoutesChecked;
if (Probe.Result == EVoxelConnectivityResult::CoarseLied) NumGuardRoutesRefuted += NumRefutedRoutes(Probe);
{
++NumGuardRoutesRefuted;
}
} }
const int32 SnappedHere = (Probe.bStartSnapped ? 1 : 0) const int32 SnappedHere = (Probe.bStartSnapped ? 1 : 0)
@@ -1099,7 +1139,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
case EVoxelConnectivityResult::StartCellSolid: ++NumAnchorStartCellSolid; break; case EVoxelConnectivityResult::StartCellSolid: ++NumAnchorStartCellSolid; break;
case EVoxelConnectivityResult::GoalCellSolid: ++NumAnchorGoalCellSolid; break; case EVoxelConnectivityResult::GoalCellSolid: ++NumAnchorGoalCellSolid; break;
case EVoxelConnectivityResult::OutOfWindow: ++NumAnchorOutOfWindow; break; case EVoxelConnectivityResult::OutOfWindow: ++NumAnchorOutOfWindow; break;
case EVoxelConnectivityResult::CoarseLied: ++NumAnchorCoarseLied; break; case EVoxelConnectivityResult::CoarseLiedBudgetExhausted:
++NumAnchorBudgetExhausted;
break;
default: break; default: break;
} }
} }
@@ -1157,12 +1199,35 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
const FConnectivityProbe WallProbe = ProbeRoute( const FConnectivityProbe WallProbe = ProbeRoute(
0, WallA, WallB, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false, 0, WallA, WallB, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false,
/*bCountAsArrivalDeparture=*/false); /*bCountAsArrivalDeparture=*/false);
if (WallProbe.Result != EVoxelConnectivityResult::CoarseLied) if (WallProbe.Result != EVoxelConnectivityResult::CoarseLiedBudgetExhausted
|| WallProbe.NumRouteRetries != DerivedWindowSettings.MaxRouteRetries)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("HARD FAILURE: the controlled coarse-connectivity lie returned %s instead of " TEXT("HARD FAILURE: the controlled solid wall returned %s after %d retries; "
"COARSE_LIED."), "expected COARSE_LIED_BUDGET_EXHAUSTED after the MaxRouteRetries=%d cap."),
ConnectivityResultName(WallProbe.Result))); ConnectivityResultName(WallProbe.Result),
WallProbe.NumRouteRetries,
DerivedWindowSettings.MaxRouteRetries));
bAllChecksPassed = false;
}
FConnectivityProbe WallProbeRepeat;
WallProbeRepeat.bChecked = true;
WallProbeRepeat.Result = VF_AreConnected(
*World.Generator,
*World.StrateManager,
0,
WallA,
WallB,
DerivedWindowSettings,
WallProbeRepeat.bStartSnapped,
WallProbeRepeat.bGoalSnapped,
WallProbeRepeat.NumRouteRetries);
if (!ConnectivityProbesAreBitIdentical(WallProbe, WallProbeRepeat))
{
AddError(TEXT(
"HARD FAILURE: the blocked-edge retry order was not deterministic; repeating the "
"same wall query changed its verdict, snaps, or retry count."));
bAllChecksPassed = false; bAllChecksPassed = false;
} }
World.DiffLayer->Clear(); World.DiffLayer->Clear();
@@ -1371,6 +1436,14 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
"checked, %d coarse routes refuted at full resolution.\n"), "checked, %d coarse routes refuted at full resolution.\n"),
NumRoutesChecked, NumRoutesChecked,
NumRoutesRefuted); NumRoutesRefuted);
Summary += FString::Printf(
TEXT("Retry budget: MaxRouteRetries=%d; %d alternate routes checked across %d queries "
"(maximum %d retries in one query); budget exhausted on %d queries.\n"),
DerivedWindowSettings.MaxRouteRetries,
NumRouteRetriesUsed,
NumRoutesChecked,
MaxRouteRetriesUsed,
NumBudgetExhausted);
Summary += FString::Printf( Summary += FString::Printf(
TEXT("Endpoint snap repair: %d snapped endpoint events across all probes; %d in " TEXT("Endpoint snap repair: %d snapped endpoint events across all probes; %d in "
"%d arrival->departure probes.\n"), "%d arrival->departure probes.\n"),
@@ -1380,14 +1453,14 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
Summary += FString::Printf( Summary += FString::Printf(
TEXT("Correct largest-component endpoint probes (%d, including the surface-entry auxiliary): " TEXT("Correct largest-component endpoint probes (%d, including the surface-entry auxiliary): "
"connected=%d, not-connected-at-resolution=%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"), "out-of-window=%d, coarse-lied-budget-exhausted=%d, snapped endpoint events=%d.\n"),
NumAnchorProbes, NumAnchorProbes,
NumAnchorConnected, NumAnchorConnected,
NumAnchorNotConnectedAtThisResolution, NumAnchorNotConnectedAtThisResolution,
NumAnchorStartCellSolid, NumAnchorStartCellSolid,
NumAnchorGoalCellSolid, NumAnchorGoalCellSolid,
NumAnchorOutOfWindow, NumAnchorOutOfWindow,
NumAnchorCoarseLied, NumAnchorBudgetExhausted,
NumAnchorSnappedEndpointEvents); NumAnchorSnappedEndpointEvents);
Summary += TEXT("Previous \"5 of 15 disconnected\" verdict: ARTIFACT as a claim about sealed " Summary += TEXT("Previous \"5 of 15 disconnected\" verdict: ARTIFACT as a claim about sealed "
"pockets; it used a guessed anchor and asked the wrong (largest-component) law.\n"); "pockets; it used a guessed anchor and asked the wrong (largest-component) law.\n");
@@ -1557,7 +1630,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
DeparturePoint, DeparturePoint,
RowSettings, RowSettings,
Row.Probe.bStartSnapped, Row.Probe.bStartSnapped,
Row.Probe.bGoalSnapped); Row.Probe.bGoalSnapped,
Row.Probe.NumRouteRetries);
if (!Row.Metrics.bValid if (!Row.Metrics.bValid
|| Row.Metrics.NumSampled <= 0 || Row.Metrics.NumSampled <= 0
@@ -1638,7 +1712,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
DeparturePoint, DeparturePoint,
RowSettings, RowSettings,
Row.Probe.bStartSnapped, Row.Probe.bStartSnapped,
Row.Probe.bGoalSnapped); Row.Probe.bGoalSnapped,
Row.Probe.NumRouteRetries);
if (!Row.Metrics.bValid if (!Row.Metrics.bValid
|| Row.Metrics.NumSampled <= 0 || Row.Metrics.NumSampled <= 0
@@ -1789,7 +1864,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
int32 NumSpineLargestComponents = 0; int32 NumSpineLargestComponents = 0;
int32 NumMouthToSpineQueries = 0; int32 NumMouthToSpineQueries = 0;
int32 NumMouthToSpineConnected = 0; int32 NumMouthToSpineConnected = 0;
int32 NumMouthToSpineCoarseLied = 0; int32 NumMouthToSpineBudgetExhausted = 0;
int32 NumMouthToSpineRetriesUsed = 0;
int32 MaxMouthToSpineRetries = 0;
for (const FSpineDiagnosisReport& Report : SpineReports) for (const FSpineDiagnosisReport& Report : SpineReports)
{ {
if (Report.Spine.bFullWindowRun) if (Report.Spine.bFullWindowRun)
@@ -1817,7 +1894,11 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
} }
++NumMouthToSpineQueries; ++NumMouthToSpineQueries;
NumMouthToSpineConnected += Probe->Result == EVoxelConnectivityResult::Connected; NumMouthToSpineConnected += Probe->Result == EVoxelConnectivityResult::Connected;
NumMouthToSpineCoarseLied += Probe->Result == EVoxelConnectivityResult::CoarseLied; NumMouthToSpineBudgetExhausted +=
Probe->Result == EVoxelConnectivityResult::CoarseLiedBudgetExhausted;
NumMouthToSpineRetriesUsed += Probe->NumRouteRetries;
MaxMouthToSpineRetries = FMath::Max(
MaxMouthToSpineRetries, Probe->NumRouteRetries);
} }
} }
@@ -1959,7 +2040,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
bSpineDiagnosisUsable ? TEXT("yes") : TEXT("no")); bSpineDiagnosisUsable ? TEXT("yes") : TEXT("no"));
Summary += FString::Printf( Summary += FString::Printf(
TEXT("Interpretation counts: spine full-window run=%d/%d, one uninterrupted open run=%d/%d, " TEXT("Interpretation counts: spine full-window run=%d/%d, one uninterrupted open run=%d/%d, "
"spine component largest=%d/%d; mouth->spine CONNECTED=%d/%d, COARSE_LIED=%d.\n"), "spine component largest=%d/%d; mouth->spine CONNECTED=%d/%d, "
"COARSE_LIED_BUDGET_EXHAUSTED=%d, retries=%d (max %d).\n"),
NumSpineFullWindowRuns, NumSpineFullWindowRuns,
SpineReports.Num(), SpineReports.Num(),
NumSpineOneOpenRuns, NumSpineOneOpenRuns,
@@ -1968,7 +2050,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
SpineReports.Num(), SpineReports.Num(),
NumMouthToSpineConnected, NumMouthToSpineConnected,
NumMouthToSpineQueries, NumMouthToSpineQueries,
NumMouthToSpineCoarseLied); NumMouthToSpineBudgetExhausted,
NumMouthToSpineRetriesUsed,
MaxMouthToSpineRetries);
Summary += TEXT( Summary += TEXT(
"Interpretation: the structural spine remains a continuous column. After the " "Interpretation: the structural spine remains a continuous column. After the "
"VerticalShafts connector change, the seed-1337 spine is the largest component, but the " "VerticalShafts connector change, the seed-1337 spine is the largest component, but the "
@@ -2041,8 +2125,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
Diagnostics.GoalToStartComponentDistanceCells, Diagnostics.GoalToStartComponentDistanceCells,
Diagnostics.GoalToStartComponentDistanceCells * static_cast<float>(DiagnosticSettings.SampleStep)); Diagnostics.GoalToStartComponentDistanceCells * static_cast<float>(DiagnosticSettings.SampleStep));
Summary += FString::Printf( Summary += FString::Printf(
TEXT(" diagnostic route verdict: %s; endpoint snaps: start=%s, goal=%s.\n"), TEXT(" diagnostic route verdict: %s; retries=%d; endpoint snaps: start=%s, goal=%s.\n"),
ConnectivityResultName(Diagnostics.Result), ConnectivityResultName(Diagnostics.Result),
Diagnostics.NumRouteRetries,
Diagnostics.bStartSnapped ? TEXT("yes") : TEXT("no"), Diagnostics.bStartSnapped ? TEXT("yes") : TEXT("no"),
Diagnostics.bGoalSnapped ? TEXT("yes") : TEXT("no")); Diagnostics.bGoalSnapped ? TEXT("yes") : TEXT("no"));
@@ -2069,9 +2154,18 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
int32 SeedArrivalToSpineSnapEvents = 0; int32 SeedArrivalToSpineSnapEvents = 0;
int32 SeedDepartureToSpineSnapEvents = 0; int32 SeedDepartureToSpineSnapEvents = 0;
int32 SeedArrivalToDepartureSnapEvents = 0; int32 SeedArrivalToDepartureSnapEvents = 0;
int32 SeedArrivalToSpineResultCounts[6] = {}; int32 SeedArrivalToSpineResultCounts[ConnectivityResultCount] = {};
int32 SeedDepartureToSpineResultCounts[6] = {}; int32 SeedDepartureToSpineResultCounts[ConnectivityResultCount] = {};
int32 SeedArrivalToDepartureResultCounts[6] = {}; int32 SeedArrivalToDepartureResultCounts[ConnectivityResultCount] = {};
int32 SeedArrivalToSpineRetries = 0;
int32 SeedDepartureToSpineRetries = 0;
int32 SeedArrivalToDepartureRetries = 0;
int32 SeedArrivalToSpineMaxRetries = 0;
int32 SeedDepartureToSpineMaxRetries = 0;
int32 SeedArrivalToDepartureMaxRetries = 0;
int32 SeedArrivalToSpineBudgetExhausted = 0;
int32 SeedDepartureToSpineBudgetExhausted = 0;
int32 SeedArrivalToDepartureBudgetExhausted = 0;
int32 SeedFullWindowRuns = 0; int32 SeedFullWindowRuns = 0;
int32 SeedOneOpenRuns = 0; int32 SeedOneOpenRuns = 0;
int32 SeedSpineLargest = 0; int32 SeedSpineLargest = 0;
@@ -2176,7 +2270,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
continue; continue;
} }
const auto CountSeedResult = [](const FConnectivityProbe& Probe, int32 (&Counts)[6]) const auto CountSeedResult = [](
const FConnectivityProbe& Probe,
int32 (&Counts)[ConnectivityResultCount])
{ {
if (Probe.bChecked) if (Probe.bChecked)
{ {
@@ -2187,6 +2283,29 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
CountSeedResult(SeedReport.DepartureToSpine, SeedDepartureToSpineResultCounts); CountSeedResult(SeedReport.DepartureToSpine, SeedDepartureToSpineResultCounts);
CountSeedResult(SeedReport.ArrivalToDeparture, SeedArrivalToDepartureResultCounts); CountSeedResult(SeedReport.ArrivalToDeparture, SeedArrivalToDepartureResultCounts);
SeedArrivalToSpineRetries += SeedReport.ArrivalToSpine.NumRouteRetries;
SeedDepartureToSpineRetries += SeedReport.DepartureToSpine.NumRouteRetries;
SeedArrivalToDepartureRetries +=
SeedReport.ArrivalToDeparture.NumRouteRetries;
SeedArrivalToSpineMaxRetries = FMath::Max(
SeedArrivalToSpineMaxRetries,
SeedReport.ArrivalToSpine.NumRouteRetries);
SeedDepartureToSpineMaxRetries = FMath::Max(
SeedDepartureToSpineMaxRetries,
SeedReport.DepartureToSpine.NumRouteRetries);
SeedArrivalToDepartureMaxRetries = FMath::Max(
SeedArrivalToDepartureMaxRetries,
SeedReport.ArrivalToDeparture.NumRouteRetries);
SeedArrivalToSpineBudgetExhausted +=
SeedReport.ArrivalToSpine.Result
== EVoxelConnectivityResult::CoarseLiedBudgetExhausted;
SeedDepartureToSpineBudgetExhausted +=
SeedReport.DepartureToSpine.Result
== EVoxelConnectivityResult::CoarseLiedBudgetExhausted;
SeedArrivalToDepartureBudgetExhausted +=
SeedReport.ArrivalToDeparture.Result
== EVoxelConnectivityResult::CoarseLiedBudgetExhausted;
SeedArrivalToSpineSnapEvents += SeedArrivalToSpineSnapEvents +=
(SeedReport.ArrivalToSpine.bStartSnapped ? 1 : 0) (SeedReport.ArrivalToSpine.bStartSnapped ? 1 : 0)
+ (SeedReport.ArrivalToSpine.bGoalSnapped ? 1 : 0); + (SeedReport.ArrivalToSpine.bGoalSnapped ? 1 : 0);
@@ -2245,6 +2364,20 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
SeedFailures, SeedFailures,
SeedArrivalToDepartureSnapEvents, SeedArrivalToDepartureSnapEvents,
SeedSweepSeconds); SeedSweepSeconds);
Summary += FString::Printf(
TEXT(" seed retries used (total/max/budget-exhausted): arrival->spine=%d/%d/%d; "
"departure->spine=%d/%d/%d; arrival->departure=%d/%d/%d; "
"MaxRouteRetries=%d.\n"),
SeedArrivalToSpineRetries,
SeedArrivalToSpineMaxRetries,
SeedArrivalToSpineBudgetExhausted,
SeedDepartureToSpineRetries,
SeedDepartureToSpineMaxRetries,
SeedDepartureToSpineBudgetExhausted,
SeedArrivalToDepartureRetries,
SeedArrivalToDepartureMaxRetries,
SeedArrivalToDepartureBudgetExhausted,
DiagnosticSettings.MaxRouteRetries);
Summary += FString::Printf( Summary += FString::Printf(
TEXT(" ACCEPTANCE arrival->departure: %s (%d/%d seeds pass).\n"), TEXT(" ACCEPTANCE arrival->departure: %s (%d/%d seeds pass).\n"),
SeedPasses == UE_ARRAY_COUNT(SeedCases) && SeedFailures == 0 SeedPasses == UE_ARRAY_COUNT(SeedCases) && SeedFailures == 0
@@ -2276,29 +2409,29 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
Summary += FString::Printf( Summary += FString::Printf(
TEXT(" seed result breakdown — arrival->spine: CONNECTED=%d, " TEXT(" seed result breakdown — arrival->spine: CONNECTED=%d, "
"NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, " "NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, "
"OUT_OF_WINDOW=%d, COARSE_LIED=%d; departure->spine: CONNECTED=%d, " "OUT_OF_WINDOW=%d, COARSE_LIED_BUDGET_EXHAUSTED=%d; departure->spine: CONNECTED=%d, "
"NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, " "NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, "
"OUT_OF_WINDOW=%d, COARSE_LIED=%d; arrival->departure: CONNECTED=%d, " "OUT_OF_WINDOW=%d, COARSE_LIED_BUDGET_EXHAUSTED=%d; arrival->departure: CONNECTED=%d, "
"NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, " "NOT_CONNECTED_AT_THIS_RESOLUTION=%d, START_CELL_SOLID=%d, GOAL_CELL_SOLID=%d, "
"OUT_OF_WINDOW=%d, COARSE_LIED=%d.\n"), "OUT_OF_WINDOW=%d, COARSE_LIED_BUDGET_EXHAUSTED=%d.\n"),
SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::Connected)], SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::Connected)],
SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::NotConnectedAtThisResolution)], SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::NotConnectedAtThisResolution)],
SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::StartCellSolid)], SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::StartCellSolid)],
SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::GoalCellSolid)], SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::GoalCellSolid)],
SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::OutOfWindow)], SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::OutOfWindow)],
SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::CoarseLied)], SeedArrivalToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::CoarseLiedBudgetExhausted)],
SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::Connected)], SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::Connected)],
SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::NotConnectedAtThisResolution)], SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::NotConnectedAtThisResolution)],
SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::StartCellSolid)], SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::StartCellSolid)],
SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::GoalCellSolid)], SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::GoalCellSolid)],
SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::OutOfWindow)], SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::OutOfWindow)],
SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::CoarseLied)], SeedDepartureToSpineResultCounts[static_cast<int32>(EVoxelConnectivityResult::CoarseLiedBudgetExhausted)],
SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::Connected)], SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::Connected)],
SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::NotConnectedAtThisResolution)], SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::NotConnectedAtThisResolution)],
SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::StartCellSolid)], SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::StartCellSolid)],
SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::GoalCellSolid)], SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::GoalCellSolid)],
SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::OutOfWindow)], SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::OutOfWindow)],
SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::CoarseLied)]); SeedArrivalToDepartureResultCounts[static_cast<int32>(EVoxelConnectivityResult::CoarseLiedBudgetExhausted)]);
Summary += FString::Printf( Summary += FString::Printf(
TEXT(" seed endpoint snaps — arrival->spine=%d, departure->spine=%d, " TEXT(" seed endpoint snaps — arrival->spine=%d, departure->spine=%d, "
"arrival->departure=%d.\n"), "arrival->departure=%d.\n"),
+131 -12
View File
@@ -662,6 +662,41 @@ namespace VoxelStrateMeasurePrivate
return EEndpointCellResult::Air; return EEndpointCellResult::Air;
} }
struct FCoarseEdge
{
int32 First = INDEX_NONE;
int32 Second = INDEX_NONE;
bool IsValid() const
{
return First >= 0 && Second >= 0 && First != Second;
}
};
FCoarseEdge MakeCoarseEdge(int32 A, int32 B)
{
FCoarseEdge Edge;
Edge.First = FMath::Min(A, B);
Edge.Second = FMath::Max(A, B);
return Edge;
}
bool IsCoarseEdgeBlocked(
const TArray<FCoarseEdge>& BlockedEdges,
int32 A,
int32 B)
{
const FCoarseEdge Candidate = MakeCoarseEdge(A, B);
for (const FCoarseEdge& Blocked : BlockedEdges)
{
if (Blocked.First == Candidate.First && Blocked.Second == Candidate.Second)
{
return true;
}
}
return false;
}
bool FullResolutionAirOnSegment( bool FullResolutionAirOnSegment(
const UVoxelGenerator& Generator, const UVoxelGenerator& Generator,
const FVector& Start, const FVector& Start,
@@ -689,8 +724,11 @@ namespace VoxelStrateMeasurePrivate
const FSampleGrid& Grid, const FSampleGrid& Grid,
const FVector& A, const FVector& A,
const FVector& B, const FVector& B,
const TArray<int32>& Path) const TArray<int32>& Path,
FCoarseEdge& OutFailedEdge)
{ {
OutFailedEdge = FCoarseEdge();
int32 StartX = 0; int32 StartX = 0;
int32 StartY = 0; int32 StartY = 0;
int32 StartZ = 0; int32 StartZ = 0;
@@ -702,6 +740,9 @@ namespace VoxelStrateMeasurePrivate
if (!FullResolutionAirOnSegment(Generator, A, Grid.CellCenter(StartX, StartY, StartZ))) if (!FullResolutionAirOnSegment(Generator, A, Grid.CellCenter(StartX, StartY, StartZ)))
{ {
// The endpoint-to-cell-center segment has no coarse cell pair to exclude. It is
// common to every route from this endpoint, so report unknown rather than claiming
// that the pair is disconnected at this resolution.
return false; return false;
} }
@@ -720,17 +761,25 @@ namespace VoxelStrateMeasurePrivate
Grid.CellCenter(PreviousX, PreviousY, PreviousZ), Grid.CellCenter(PreviousX, PreviousY, PreviousZ),
Grid.CellCenter(CurrentX, CurrentY, CurrentZ))) Grid.CellCenter(CurrentX, CurrentY, CurrentZ)))
{ {
OutFailedEdge = MakeCoarseEdge(Path[PathIndex - 1], Path[PathIndex]);
return false; return false;
} }
} }
return FullResolutionAirOnSegment(Generator, Grid.CellCenter(EndX, EndY, EndZ), B); if (!FullResolutionAirOnSegment(Generator, Grid.CellCenter(EndX, EndY, EndZ), B))
{
// As above, the final endpoint segment is shared by every route that reaches the
// goal cell and therefore has no precise coarse edge to exclude.
return false;
}
return true;
} }
bool FindCoarsePath( bool FindCoarsePath(
const FSampleGrid& Grid, const FSampleGrid& Grid,
int32 Start, int32 Start,
int32 Goal, int32 Goal,
const TArray<FCoarseEdge>& BlockedEdges,
TArray<int32>& OutPath) TArray<int32>& OutPath)
{ {
if (Grid.Air[Start] == 0u || Grid.Air[Goal] == 0u) if (Grid.Air[Start] == 0u || Grid.Air[Goal] == 0u)
@@ -762,7 +811,9 @@ namespace VoxelStrateMeasurePrivate
auto Visit = [&](int32 Neighbor) auto Visit = [&](int32 Neighbor)
{ {
if (Grid.Air[Neighbor] != 0u && Parent[Neighbor] == -2) if (Grid.Air[Neighbor] != 0u
&& Parent[Neighbor] == -2
&& !IsCoarseEdgeBlocked(BlockedEdges, Current, Neighbor))
{ {
Parent[Neighbor] = Current; Parent[Neighbor] = Current;
Queue[Tail++] = Neighbor; Queue[Tail++] = Neighbor;
@@ -812,12 +863,15 @@ namespace VoxelStrateMeasurePrivate
int32& OutStart, int32& OutStart,
int32& OutGoal, int32& OutGoal,
bool& bOutStartSnapped, bool& bOutStartSnapped,
bool& bOutGoalSnapped) bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
int32 MaxRouteRetries)
{ {
OutStart = -1; OutStart = -1;
OutGoal = -1; OutGoal = -1;
bOutStartSnapped = false; bOutStartSnapped = false;
bOutGoalSnapped = false; bOutGoalSnapped = false;
OutNumRouteRetries = 0;
const EEndpointCellResult StartCell = ResolveEndpointCell( const EEndpointCellResult StartCell = ResolveEndpointCell(
Grid, AVoxel, OutStart, bOutStartSnapped); Grid, AVoxel, OutStart, bOutStartSnapped);
@@ -837,22 +891,42 @@ namespace VoxelStrateMeasurePrivate
return EVoxelConnectivityResult::GoalCellSolid; return EVoxelConnectivityResult::GoalCellSolid;
} }
TArray<FCoarseEdge> BlockedEdges;
TArray<int32> Path; TArray<int32> Path;
if (!FindCoarsePath(Grid, OutStart, OutGoal, Path)) for (;;)
{ {
if (!FindCoarsePath(Grid, OutStart, OutGoal, BlockedEdges, Path))
{
// This is a negative verdict only after all edges excluded by earlier
// full-resolution failures leave no coarse route at this resolution.
return EVoxelConnectivityResult::NotConnectedAtThisResolution; return EVoxelConnectivityResult::NotConnectedAtThisResolution;
} }
// The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel // The coarse route is evidence, not a pass. Re-check every candidate at one-voxel
// spacing in the original voxel field. A single solid sample is the dangerous coarse // spacing in the original voxel field. A single solid sample refutes only this
// false positive. // route, so exclude its precise coarse edge and search again.
if (!FullResolutionPathIsAir(Generator, Grid, AVoxel, BVoxel, Path)) FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(Generator, Grid, AVoxel, BVoxel, Path, FailedEdge))
{ {
return EVoxelConnectivityResult::CoarseLied;
}
return EVoxelConnectivityResult::Connected; return EVoxelConnectivityResult::Connected;
} }
if (OutNumRouteRetries >= MaxRouteRetries || !FailedEdge.IsValid())
{
// A route can be refuted without a retryable pair when the failure is in an
// endpoint-to-cell segment (or when the explicit retry budget is spent). Both
// cases are unknown, never evidence of disconnection.
return EVoxelConnectivityResult::CoarseLiedBudgetExhausted;
}
check(FailedEdge.First != FailedEdge.Second);
check(!IsCoarseEdgeBlocked(
BlockedEdges, FailedEdge.First, FailedEdge.Second));
BlockedEdges.Add(FailedEdge);
++OutNumRouteRetries;
}
}
float DistanceToComponent( float DistanceToComponent(
const FSampleGrid& Grid, const FSampleGrid& Grid,
int32 StartCell, int32 StartCell,
@@ -899,12 +973,14 @@ namespace VoxelStrateMeasurePrivate
EVoxelConnectivityResult ConnectivityResult, EVoxelConnectivityResult ConnectivityResult,
bool bStartSnapped, bool bStartSnapped,
bool bGoalSnapped, bool bGoalSnapped,
int32 NumRouteRetries,
FVoxelConnectivityDiagnostics& OutDiagnostics) FVoxelConnectivityDiagnostics& OutDiagnostics)
{ {
OutDiagnostics = FVoxelConnectivityDiagnostics(); OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics.Result = ConnectivityResult; OutDiagnostics.Result = ConnectivityResult;
OutDiagnostics.bStartSnapped = bStartSnapped; OutDiagnostics.bStartSnapped = bStartSnapped;
OutDiagnostics.bGoalSnapped = bGoalSnapped; OutDiagnostics.bGoalSnapped = bGoalSnapped;
OutDiagnostics.NumRouteRetries = NumRouteRetries;
OutDiagnostics.bValid = ConnectivityResult != EVoxelConnectivityResult::OutOfWindow OutDiagnostics.bValid = ConnectivityResult != EVoxelConnectivityResult::OutOfWindow
|| (Start >= 0 && Goal >= 0); || (Start >= 0 && Goal >= 0);
if (Start < 0 || Goal < 0) if (Start < 0 || Goal < 0)
@@ -983,10 +1059,12 @@ namespace VoxelStrateMeasurePrivate
const FVoxelStrateMeasureSettings& Settings, const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped, bool& bOutStartSnapped,
bool& bOutGoalSnapped, bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
FVoxelConnectivityDiagnostics* OutDiagnostics) FVoxelConnectivityDiagnostics* OutDiagnostics)
{ {
bOutStartSnapped = false; bOutStartSnapped = false;
bOutGoalSnapped = false; bOutGoalSnapped = false;
OutNumRouteRetries = 0;
if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y) if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y)
|| !FMath::IsFinite(AVoxel.Z) || !FMath::IsFinite(BVoxel.X) || !FMath::IsFinite(AVoxel.Z) || !FMath::IsFinite(BVoxel.X)
|| !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z)) || !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z))
@@ -997,6 +1075,14 @@ namespace VoxelStrateMeasurePrivate
} }
return EVoxelConnectivityResult::OutOfWindow; return EVoxelConnectivityResult::OutOfWindow;
} }
if (Settings.MaxRouteRetries < 0)
{
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
}
return EVoxelConnectivityResult::OutOfWindow;
}
FSampleGrid Grid; FSampleGrid Grid;
FString RefusalReason; FString RefusalReason;
@@ -1019,7 +1105,9 @@ namespace VoxelStrateMeasurePrivate
Start, Start,
Goal, Goal,
bOutStartSnapped, bOutStartSnapped,
bOutGoalSnapped); bOutGoalSnapped,
OutNumRouteRetries,
Settings.MaxRouteRetries);
if (OutDiagnostics != nullptr) if (OutDiagnostics != nullptr)
{ {
PopulateConnectivityDiagnostics( PopulateConnectivityDiagnostics(
@@ -1029,6 +1117,7 @@ namespace VoxelStrateMeasurePrivate
Result, Result,
bOutStartSnapped, bOutStartSnapped,
bOutGoalSnapped, bOutGoalSnapped,
OutNumRouteRetries,
*OutDiagnostics); *OutDiagnostics);
} }
return Result; return Result;
@@ -1071,6 +1160,7 @@ EVoxelConnectivityResult VF_AreConnected(
bool& bOutStartSnapped, bool& bOutStartSnapped,
bool& bOutGoalSnapped) bool& bOutGoalSnapped)
{ {
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity( return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, Generator,
Manager, Manager,
@@ -1080,6 +1170,31 @@ EVoxelConnectivityResult VF_AreConnected(
Settings, Settings,
bOutStartSnapped, bOutStartSnapped,
bOutGoalSnapped, bOutGoalSnapped,
NumRouteRetries,
nullptr);
}
EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries)
{
return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator,
Manager,
StrateIndex,
AVoxel,
BVoxel,
Settings,
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
nullptr); nullptr);
} }
@@ -1094,6 +1209,7 @@ EVoxelConnectivityResult VF_AreConnected(
bool& bOutGoalSnapped, bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics& OutDiagnostics) FVoxelConnectivityDiagnostics& OutDiagnostics)
{ {
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity( return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, Generator,
Manager, Manager,
@@ -1103,6 +1219,7 @@ EVoxelConnectivityResult VF_AreConnected(
Settings, Settings,
bOutStartSnapped, bOutStartSnapped,
bOutGoalSnapped, bOutGoalSnapped,
NumRouteRetries,
&OutDiagnostics); &OutDiagnostics);
} }
@@ -1115,6 +1232,7 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
const FVoxelStrateMeasureSettings& Settings) const FVoxelStrateMeasureSettings& Settings)
{ {
FVoxelConnectivityDiagnostics Result; FVoxelConnectivityDiagnostics Result;
int32 NumRouteRetries = 0;
VoxelStrateMeasurePrivate::QueryConnectivity( VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, Generator,
Manager, Manager,
@@ -1124,6 +1242,7 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
Settings, Settings,
Result.bStartSnapped, Result.bStartSnapped,
Result.bGoalSnapped, Result.bGoalSnapped,
NumRouteRetries,
&Result); &Result);
return Result; return Result;
} }
+26 -8
View File
@@ -15,7 +15,7 @@ enum class EVoxelConnectivityResult : uint8
StartCellSolid, StartCellSolid,
GoalCellSolid, GoalCellSolid,
OutOfWindow, OutOfWindow,
CoarseLied CoarseLiedBudgetExhausted // Unknown: retries are exhausted, or the refutation has no coarse edge to exclude.
}; };
/** Settings for the deterministic coarse grid used by the strate measurement pass. */ /** Settings for the deterministic coarse grid used by the strate measurement pass. */
@@ -25,6 +25,7 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY. int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates. FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells. int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable. int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal. int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal.
}; };
@@ -71,6 +72,7 @@ struct VOXELFORGE_API FVoxelConnectivityDiagnostics
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow; EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bStartSnapped = false; bool bStartSnapped = false;
bool bGoalSnapped = false; bool bGoalSnapped = false;
int32 NumRouteRetries = 0; // Alternate routes full-resolution-checked after the first route.
int64 NumAirCells = 0; int64 NumAirCells = 0;
int64 StartComponentCells = 0; int64 StartComponentCells = 0;
@@ -100,14 +102,17 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
const FVoxelStrateMeasureSettings& Settings); const FVoxelStrateMeasureSettings& Settings);
/** /**
* Test coarse connectivity and then re-check the one recovered coarse route at full resolution. * Test coarse connectivity and re-check recovered coarse routes at full resolution.
* *
* If an endpoint's own coarse cell is solid, the query searches the 26 neighbouring cells and * 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 * 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 * results are returned only when no adjacent air cell exists. When a route fails its
* full-resolution re-check returns CoarseLied; a missing coarse route returns * full-resolution re-check, the failed coarse edge is excluded from a deterministic local
* NotConnectedAtThisResolution because a corridor thinner than SampleStep is invisible to the * blocklist and BFS is retried up to MaxRouteRetries. A missing route after exclusions returns
* grid. A bare "not connected" result is evidence only at the grid's own resolution. * NotConnectedAtThisResolution. If the retry budget is exhausted while routes are still being
* refuted, the result is CoarseLiedBudgetExhausted: unknown, not disconnected. The same unknown
* result is used when a refutation is in an endpoint-to-cell segment with no coarse edge to
* exclude. Every Connected result has a fully re-walked air route.
*/ */
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected( VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator, const UVoxelGenerator& Generator,
@@ -119,6 +124,18 @@ VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
bool& bOutStartSnapped, bool& bOutStartSnapped,
bool& bOutGoalSnapped); bool& bOutGoalSnapped);
/** The same query as the simple overload, with the number of alternate routes actually tried. */
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries);
/** /**
* The same connectivity query with component facts returned from the same sampled grid. This * The same connectivity query with component facts returned from the same sampled grid. This
* overload exists for diagnostics that need both the required route verdict and the component * overload exists for diagnostics that need both the required route verdict and the component
@@ -137,8 +154,9 @@ VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected(
/** /**
* Return the component sizes and spatial separation facts for the same query performed by * Return the component sizes and spatial separation facts for the same query performed by
* VF_AreConnected. The returned Result uses the same full-resolution route re-check. bValid means * VF_AreConnected. The returned Result uses the same retrying full-resolution route re-check,
* the sample grid was built and both endpoint coordinates were in its bounds; component fields * and NumRouteRetries reports the number of alternate routes actually checked. bValid means the
* sample grid was built and both endpoint coordinates were in its bounds; component fields
* remain zero/negative when an endpoint has no air cell to analyze. Largest-component membership * remain zero/negative when an endpoint has no air cell to analyze. Largest-component membership
* treats equal-sized components as tied for largest. * treats equal-sized components as tied for largest.
*/ */