diff --git a/CODEMAP.md b/CODEMAP.md index ac04cde..87aa7ac 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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 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 -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. 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. +candidate routes at full voxel resolution before returning `Connected`. When a candidate is +refuted, its specific coarse cell edge is added to a deterministic array blocklist and BFS is +retried up to `MaxRouteRetries` (default 16). 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. A missing coarse route after edge +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 | |--------|------| | `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_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. | ### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp` diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index 4c08c10..d70b9a9 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -42,6 +42,7 @@ namespace EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow; bool bStartSnapped = false; bool bGoalSnapped = false; + int32 NumRouteRetries = 0; }; struct FStrateConnectivityReport @@ -115,11 +116,40 @@ namespace case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID"); case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID"); 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"); } } + constexpr int32 ConnectivityResultCount = + static_cast(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) { if (!Probe.bChecked) @@ -127,7 +157,10 @@ namespace 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) { Text += TEXT(" ["); @@ -557,6 +590,8 @@ namespace OutReport.SpineComponentProbe.bStartSnapped, OutReport.SpineComponentProbe.bGoalSnapped, OutReport.SpineComponent); + OutReport.SpineComponentProbe.NumRouteRetries = + OutReport.SpineComponent.NumRouteRetries; } if (!FindDiagnosticMouths(Manager.GetPassages(), StrateIndex, OutReport)) @@ -584,6 +619,7 @@ namespace OutProbe.bStartSnapped, OutProbe.bGoalSnapped, OutFacts); + OutProbe.NumRouteRetries = OutFacts.NumRouteRetries; }; if (OutReport.bHasArrival) @@ -613,7 +649,8 @@ namespace OutReport.DeparturePoint, Settings, OutReport.ArrivalToDeparture.bStartSnapped, - OutReport.ArrivalToDeparture.bGoalSnapped); + OutReport.ArrivalToDeparture.bGoalSnapped, + OutReport.ArrivalToDeparture.NumRouteRetries); } return true; } @@ -791,7 +828,8 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) * 3. Range sanity: fractions/components must describe an actual air field. * 4. Determinism: identical inputs must produce bit-identical metrics. * 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; @@ -1029,6 +1067,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) int32 NumRoutesChecked = 0; int32 NumRoutesRefuted = 0; + int32 NumRouteRetriesUsed = 0; + int32 MaxRouteRetriesUsed = 0; + int32 NumBudgetExhausted = 0; int32 NumGuardRoutesChecked = 0; int32 NumGuardRoutesRefuted = 0; int32 NumSnappedEndpointEvents = 0; @@ -1041,7 +1082,7 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) int32 NumAnchorStartCellSolid = 0; int32 NumAnchorGoalCellSolid = 0; int32 NumAnchorOutOfWindow = 0; - int32 NumAnchorCoarseLied = 0; + int32 NumAnchorBudgetExhausted = 0; int32 NumAnchorSnappedEndpointEvents = 0; auto ProbeRoute = [&](int32 StrateIndex, @@ -1061,20 +1102,19 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) Goal, DerivedWindowSettings, Probe.bStartSnapped, - Probe.bGoalSnapped); + Probe.bGoalSnapped, + Probe.NumRouteRetries); ++NumRoutesChecked; - if (Probe.Result == EVoxelConnectivityResult::CoarseLied) - { - ++NumRoutesRefuted; - } + NumRoutesRefuted += NumRefutedRoutes(Probe); + NumRouteRetriesUsed += Probe.NumRouteRetries; + MaxRouteRetriesUsed = FMath::Max(MaxRouteRetriesUsed, Probe.NumRouteRetries); + NumBudgetExhausted += + Probe.Result == EVoxelConnectivityResult::CoarseLiedBudgetExhausted ? 1 : 0; if (bCountInGuard) { ++NumGuardRoutesChecked; - if (Probe.Result == EVoxelConnectivityResult::CoarseLied) - { - ++NumGuardRoutesRefuted; - } + NumGuardRoutesRefuted += NumRefutedRoutes(Probe); } const int32 SnappedHere = (Probe.bStartSnapped ? 1 : 0) @@ -1099,7 +1139,9 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) case EVoxelConnectivityResult::StartCellSolid: ++NumAnchorStartCellSolid; break; case EVoxelConnectivityResult::GoalCellSolid: ++NumAnchorGoalCellSolid; break; case EVoxelConnectivityResult::OutOfWindow: ++NumAnchorOutOfWindow; break; - case EVoxelConnectivityResult::CoarseLied: ++NumAnchorCoarseLied; break; + case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: + ++NumAnchorBudgetExhausted; + break; default: break; } } @@ -1157,12 +1199,35 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) const FConnectivityProbe WallProbe = ProbeRoute( 0, WallA, WallB, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false, /*bCountAsArrivalDeparture=*/false); - if (WallProbe.Result != EVoxelConnectivityResult::CoarseLied) + if (WallProbe.Result != EVoxelConnectivityResult::CoarseLiedBudgetExhausted + || WallProbe.NumRouteRetries != DerivedWindowSettings.MaxRouteRetries) { AddError(FString::Printf( - TEXT("HARD FAILURE: the controlled coarse-connectivity lie returned %s instead of " - "COARSE_LIED."), - ConnectivityResultName(WallProbe.Result))); + TEXT("HARD FAILURE: the controlled solid wall returned %s after %d retries; " + "expected COARSE_LIED_BUDGET_EXHAUSTED after the MaxRouteRetries=%d cap."), + 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; } World.DiffLayer->Clear(); @@ -1371,6 +1436,14 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) "checked, %d coarse routes refuted at full resolution.\n"), NumRoutesChecked, 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( TEXT("Endpoint snap repair: %d snapped endpoint events across all probes; %d in " "%d arrival->departure probes.\n"), @@ -1380,14 +1453,14 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) Summary += FString::Printf( 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, " - "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, NumAnchorConnected, NumAnchorNotConnectedAtThisResolution, NumAnchorStartCellSolid, NumAnchorGoalCellSolid, NumAnchorOutOfWindow, - NumAnchorCoarseLied, + NumAnchorBudgetExhausted, NumAnchorSnappedEndpointEvents); 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"); @@ -1557,7 +1630,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet DeparturePoint, RowSettings, Row.Probe.bStartSnapped, - Row.Probe.bGoalSnapped); + Row.Probe.bGoalSnapped, + Row.Probe.NumRouteRetries); if (!Row.Metrics.bValid || Row.Metrics.NumSampled <= 0 @@ -1638,7 +1712,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet DeparturePoint, RowSettings, Row.Probe.bStartSnapped, - Row.Probe.bGoalSnapped); + Row.Probe.bGoalSnapped, + Row.Probe.NumRouteRetries); if (!Row.Metrics.bValid || Row.Metrics.NumSampled <= 0 @@ -1789,7 +1864,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet int32 NumSpineLargestComponents = 0; int32 NumMouthToSpineQueries = 0; int32 NumMouthToSpineConnected = 0; - int32 NumMouthToSpineCoarseLied = 0; + int32 NumMouthToSpineBudgetExhausted = 0; + int32 NumMouthToSpineRetriesUsed = 0; + int32 MaxMouthToSpineRetries = 0; for (const FSpineDiagnosisReport& Report : SpineReports) { if (Report.Spine.bFullWindowRun) @@ -1817,7 +1894,11 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet } ++NumMouthToSpineQueries; 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")); Summary += FString::Printf( 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, SpineReports.Num(), NumSpineOneOpenRuns, @@ -1968,7 +2050,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet SpineReports.Num(), NumMouthToSpineConnected, NumMouthToSpineQueries, - NumMouthToSpineCoarseLied); + NumMouthToSpineBudgetExhausted, + NumMouthToSpineRetriesUsed, + MaxMouthToSpineRetries); Summary += TEXT( "Interpretation: the structural spine remains a continuous column. After 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 * static_cast(DiagnosticSettings.SampleStep)); 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), + Diagnostics.NumRouteRetries, Diagnostics.bStartSnapped ? TEXT("yes") : TEXT("no"), Diagnostics.bGoalSnapped ? TEXT("yes") : TEXT("no")); @@ -2069,9 +2154,18 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet int32 SeedArrivalToSpineSnapEvents = 0; int32 SeedDepartureToSpineSnapEvents = 0; int32 SeedArrivalToDepartureSnapEvents = 0; - int32 SeedArrivalToSpineResultCounts[6] = {}; - int32 SeedDepartureToSpineResultCounts[6] = {}; - int32 SeedArrivalToDepartureResultCounts[6] = {}; + int32 SeedArrivalToSpineResultCounts[ConnectivityResultCount] = {}; + int32 SeedDepartureToSpineResultCounts[ConnectivityResultCount] = {}; + 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 SeedOneOpenRuns = 0; int32 SeedSpineLargest = 0; @@ -2176,7 +2270,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet continue; } - const auto CountSeedResult = [](const FConnectivityProbe& Probe, int32 (&Counts)[6]) + const auto CountSeedResult = []( + const FConnectivityProbe& Probe, + int32 (&Counts)[ConnectivityResultCount]) { if (Probe.bChecked) { @@ -2187,6 +2283,29 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet CountSeedResult(SeedReport.DepartureToSpine, SeedDepartureToSpineResultCounts); 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 += (SeedReport.ArrivalToSpine.bStartSnapped ? 1 : 0) + (SeedReport.ArrivalToSpine.bGoalSnapped ? 1 : 0); @@ -2245,6 +2364,20 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet SeedFailures, SeedArrivalToDepartureSnapEvents, 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( TEXT(" ACCEPTANCE arrival->departure: %s (%d/%d seeds pass).\n"), SeedPasses == UE_ARRAY_COUNT(SeedCases) && SeedFailures == 0 @@ -2276,29 +2409,29 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet Summary += FString::Printf( TEXT(" seed result breakdown — arrival->spine: CONNECTED=%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, " - "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, " - "OUT_OF_WINDOW=%d, COARSE_LIED=%d.\n"), + "OUT_OF_WINDOW=%d, COARSE_LIED_BUDGET_EXHAUSTED=%d.\n"), SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::Connected)], SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], - SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)], + SeedArrivalToSpineResultCounts[static_cast(EVoxelConnectivityResult::CoarseLiedBudgetExhausted)], SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::Connected)], SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], - SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)], + SeedDepartureToSpineResultCounts[static_cast(EVoxelConnectivityResult::CoarseLiedBudgetExhausted)], SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::Connected)], SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::NotConnectedAtThisResolution)], SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::StartCellSolid)], SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::GoalCellSolid)], SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::OutOfWindow)], - SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::CoarseLied)]); + SeedArrivalToDepartureResultCounts[static_cast(EVoxelConnectivityResult::CoarseLiedBudgetExhausted)]); Summary += FString::Printf( TEXT(" seed endpoint snaps — arrival->spine=%d, departure->spine=%d, " "arrival->departure=%d.\n"), diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index 6beaa4c..f2fb946 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -662,6 +662,41 @@ namespace VoxelStrateMeasurePrivate 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& 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( const UVoxelGenerator& Generator, const FVector& Start, @@ -689,8 +724,11 @@ namespace VoxelStrateMeasurePrivate const FSampleGrid& Grid, const FVector& A, const FVector& B, - const TArray& Path) + const TArray& Path, + FCoarseEdge& OutFailedEdge) { + OutFailedEdge = FCoarseEdge(); + int32 StartX = 0; int32 StartY = 0; int32 StartZ = 0; @@ -702,6 +740,9 @@ namespace VoxelStrateMeasurePrivate 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; } @@ -720,17 +761,25 @@ namespace VoxelStrateMeasurePrivate Grid.CellCenter(PreviousX, PreviousY, PreviousZ), Grid.CellCenter(CurrentX, CurrentY, CurrentZ))) { + OutFailedEdge = MakeCoarseEdge(Path[PathIndex - 1], Path[PathIndex]); 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( const FSampleGrid& Grid, int32 Start, int32 Goal, + const TArray& BlockedEdges, TArray& OutPath) { if (Grid.Air[Start] == 0u || Grid.Air[Goal] == 0u) @@ -762,7 +811,9 @@ namespace VoxelStrateMeasurePrivate 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; Queue[Tail++] = Neighbor; @@ -812,12 +863,15 @@ namespace VoxelStrateMeasurePrivate int32& OutStart, int32& OutGoal, bool& bOutStartSnapped, - bool& bOutGoalSnapped) + bool& bOutGoalSnapped, + int32& OutNumRouteRetries, + int32 MaxRouteRetries) { OutStart = -1; OutGoal = -1; bOutStartSnapped = false; bOutGoalSnapped = false; + OutNumRouteRetries = 0; const EEndpointCellResult StartCell = ResolveEndpointCell( Grid, AVoxel, OutStart, bOutStartSnapped); @@ -837,20 +891,40 @@ namespace VoxelStrateMeasurePrivate return EVoxelConnectivityResult::GoalCellSolid; } + TArray BlockedEdges; TArray Path; - if (!FindCoarsePath(Grid, OutStart, OutGoal, Path)) + for (;;) { - return EVoxelConnectivityResult::NotConnectedAtThisResolution; - } + 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; + } - // The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel - // spacing in the original voxel field. A single solid sample is the dangerous coarse - // false positive. - if (!FullResolutionPathIsAir(Generator, Grid, AVoxel, BVoxel, Path)) - { - return EVoxelConnectivityResult::CoarseLied; + // 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 refutes only this + // route, so exclude its precise coarse edge and search again. + FCoarseEdge FailedEdge; + if (FullResolutionPathIsAir(Generator, Grid, AVoxel, BVoxel, Path, FailedEdge)) + { + 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; } - return EVoxelConnectivityResult::Connected; } float DistanceToComponent( @@ -899,12 +973,14 @@ namespace VoxelStrateMeasurePrivate EVoxelConnectivityResult ConnectivityResult, bool bStartSnapped, bool bGoalSnapped, + int32 NumRouteRetries, FVoxelConnectivityDiagnostics& OutDiagnostics) { OutDiagnostics = FVoxelConnectivityDiagnostics(); OutDiagnostics.Result = ConnectivityResult; OutDiagnostics.bStartSnapped = bStartSnapped; OutDiagnostics.bGoalSnapped = bGoalSnapped; + OutDiagnostics.NumRouteRetries = NumRouteRetries; OutDiagnostics.bValid = ConnectivityResult != EVoxelConnectivityResult::OutOfWindow || (Start >= 0 && Goal >= 0); if (Start < 0 || Goal < 0) @@ -983,10 +1059,12 @@ namespace VoxelStrateMeasurePrivate const FVoxelStrateMeasureSettings& Settings, bool& bOutStartSnapped, bool& bOutGoalSnapped, + int32& OutNumRouteRetries, FVoxelConnectivityDiagnostics* OutDiagnostics) { bOutStartSnapped = false; bOutGoalSnapped = false; + OutNumRouteRetries = 0; if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y) || !FMath::IsFinite(AVoxel.Z) || !FMath::IsFinite(BVoxel.X) || !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z)) @@ -997,6 +1075,14 @@ namespace VoxelStrateMeasurePrivate } return EVoxelConnectivityResult::OutOfWindow; } + if (Settings.MaxRouteRetries < 0) + { + if (OutDiagnostics != nullptr) + { + *OutDiagnostics = FVoxelConnectivityDiagnostics(); + } + return EVoxelConnectivityResult::OutOfWindow; + } FSampleGrid Grid; FString RefusalReason; @@ -1019,7 +1105,9 @@ namespace VoxelStrateMeasurePrivate Start, Goal, bOutStartSnapped, - bOutGoalSnapped); + bOutGoalSnapped, + OutNumRouteRetries, + Settings.MaxRouteRetries); if (OutDiagnostics != nullptr) { PopulateConnectivityDiagnostics( @@ -1029,6 +1117,7 @@ namespace VoxelStrateMeasurePrivate Result, bOutStartSnapped, bOutGoalSnapped, + OutNumRouteRetries, *OutDiagnostics); } return Result; @@ -1071,6 +1160,7 @@ EVoxelConnectivityResult VF_AreConnected( bool& bOutStartSnapped, bool& bOutGoalSnapped) { + int32 NumRouteRetries = 0; return VoxelStrateMeasurePrivate::QueryConnectivity( Generator, Manager, @@ -1080,6 +1170,31 @@ EVoxelConnectivityResult VF_AreConnected( Settings, bOutStartSnapped, 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); } @@ -1094,6 +1209,7 @@ EVoxelConnectivityResult VF_AreConnected( bool& bOutGoalSnapped, FVoxelConnectivityDiagnostics& OutDiagnostics) { + int32 NumRouteRetries = 0; return VoxelStrateMeasurePrivate::QueryConnectivity( Generator, Manager, @@ -1103,6 +1219,7 @@ EVoxelConnectivityResult VF_AreConnected( Settings, bOutStartSnapped, bOutGoalSnapped, + NumRouteRetries, &OutDiagnostics); } @@ -1115,6 +1232,7 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity( const FVoxelStrateMeasureSettings& Settings) { FVoxelConnectivityDiagnostics Result; + int32 NumRouteRetries = 0; VoxelStrateMeasurePrivate::QueryConnectivity( Generator, Manager, @@ -1124,6 +1242,7 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity( Settings, Result.bStartSnapped, Result.bGoalSnapped, + NumRouteRetries, &Result); return Result; } diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index b0b1bb2..0bdc92a 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -15,7 +15,7 @@ enum class EVoxelConnectivityResult : uint8 StartCellSolid, GoalCellSolid, 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. */ @@ -25,6 +25,7 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY. FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates. 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 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; bool bStartSnapped = false; bool bGoalSnapped = false; + int32 NumRouteRetries = 0; // Alternate routes full-resolution-checked after the first route. int64 NumAirCells = 0; int64 StartComponentCells = 0; @@ -100,14 +102,17 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate( 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 * 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; 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. + * results are returned only when no adjacent air cell exists. When a route fails its + * full-resolution re-check, the failed coarse edge is excluded from a deterministic local + * blocklist and BFS is retried up to MaxRouteRetries. A missing route after exclusions returns + * 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( const UVoxelGenerator& Generator, @@ -119,6 +124,18 @@ VOXELFORGE_API EVoxelConnectivityResult VF_AreConnected( bool& bOutStartSnapped, 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 * 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 - * VF_AreConnected. The returned Result uses the same full-resolution route re-check. bValid means - * the sample grid was built and both endpoint coordinates were in its bounds; component fields + * VF_AreConnected. The returned Result uses the same retrying full-resolution route re-check, + * 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 * treats equal-sized components as tied for largest. */