test(measure): the window was NOT the problem -- my sixth hypothesis dies

I thought the fitted mouth-AABB window was cropping an origin-routed path
and producing a false NotConnected. Codex tested it three ways at step 1 on
the Maze candidate Jahni walked:

  mouth-AABB + margin      842,996 cells    NotConnected
  origin-inclusive       3,251,556 cells    NotConnected
  WHOLE STRATE          31,881,528 cells    NotConnected

115,578 fit cells, 2,903 components, largest 11,418 -- and the mouths sit
in components of 172 and FOUR cells, 274 voxels apart. The window was never
the problem. Sixth measurement hypothesis, first one that was wrong in the
other direction: this time the instrument was fine and I doubted it.

The window rule still improved: bIncludeOriginInCoverWindow now covers the
route an origin-rooted topology actually implies (Maze, VerticalShafts,
TunnelNetwork, Underwater), expansion happens BEFORE MaxCells, and an
over-cap window is REFUSED honestly instead of silently cropping back to a
mouth-only box. VerticalShafts refused at the 4M production cap and said so.

Corrected law across eight: 0/7 measured Connected, 1 unknown
(CoarseLiedBudgetExhausted), 1 refused.

⇒ FIVE CAUSES NOW ELIMINATED: size (Jahni walked it), the fit stencil
(fixed, 309 pockets -> 96), placement (mouths land on legal poses),
corridor connectivity (spanning tree, 0 disconnected cells over 64 seeds),
and the measurement window (whole-strate says the same thing).

⭐ AND THE REMAINING CAUSE IS NAMED BY ITS OWN NUMBER: the departure mouth
sits in a FOUR-CELL player-fit component. A passage tube is a bore through
rock -- sloped or vertical, no floor within the walkable angle -- so the
mouth point is not a standing position and nothing around it is either.
You arrive and there is nowhere to stand.

That is precisely COMPOSER-NOTES §6.5 / Tier 1's unbuilt item: "the passage
mouth becomes a room plus a distinctive landmark". It was never started.
The law cannot pass while a mouth opens into a bore instead of a landing.

Build clean, 32/32 successful, 0 failed, same two known warnings.
WorldRadiusVoxels=0, lateral gating, determinism, equivalence all intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-06 12:58:56 +02:00
1 parent 2eaceb5984
commit e4e159f0cd
7 files changed
+444 -39

No files matched your search

+3 -1
View File
@@ -364,7 +364,9 @@ Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one stra
boundary-seal-derived (or explicitly overridden, including explicit zero) interior margin into a bounded grid. boundary-seal-derived (or explicitly overridden, including explicit zero) interior margin into a bounded grid.
When both `CoverPointA` and `CoverPointB` are set, the XY window is their AABB expanded by When both `CoverPointA` and `CoverPointB` are set, the XY window is their AABB expanded by
`CoverMarginVoxels`; otherwise it is the legacy square centered on `CenterXY` with `CoverMarginVoxels`; otherwise it is the legacy square centered on `CenterXY` with
`RadiusInVoxels`. `RadiusInVoxels`. Origin-rooted callers set `bIncludeOriginInCoverWindow` to expand that fitted
AABB to include `(0,0)` before the `MaxCells` check; an over-cap route window is refused rather
than cropped back to a mouth-only box.
It performs one deterministic 6-connected air flood fill, and derives fractions, components, It performs one deterministic 6-connected air flood fill, and derives fractions, components,
walkability, feature scale, and clearance from that grid. It then performs one bounded 4-neighbour walkability, feature scale, and clearance from that grid. It then performs one bounded 4-neighbour
flood fill over the projected XY columns that contain a walkable surface; this is a floor-continuity flood fill over the projected XY columns that contain a walkable surface; this is a floor-continuity
+72
View File
@@ -1946,3 +1946,75 @@ the world. Fix the measurement before touching generation again.
- **Acceptance must be the measured property, never "the code was written."** Twice a task returned - **Acceptance must be the measured property, never "the code was written."** Twice a task returned
"built, and it does not meet the criterion — here is the number." Both times that honesty pointed "built, and it does not meet the criterion — here is the number." Both times that honesty pointed
straight at the real cause. straight at the real cause.
---
## 16. Player-fit route-window audit — the origin is not the missing flip (2026-09-06)
This pass tested the proposed sixth measurement artifact directly on the fixed Maze identity:
composer seed `0`, candidate index `50`, fourth one-based fixture slot (`SlotMaze == 3`). The
player-fit stencil remained `SampleStep=1`; every row below names its exact XY/Z window and cell
budget. `NotConnectedAtThisResolution` is a result of that stated grid, not a claim that the
infinite Maze graph is disconnected.
### 16.1 Part A — three Maze windows
| window | exact window / grid | MaxCells | fit cells | components | largest | arrival / departure | gap (voxels) | law |
|---|---|---:|---:|---:|---:|---:|---:|---|
| current mouth-AABB + margin | `X[-84.1,168.7) Y[39.5,53.3) Z[-1175,-937)`, `253×14×238`, `842,996` cells, step 1 | 4,000,000 | 1,352 | 50 | 237 | 128 / 1 | 254.492 | `NotConnectedAtThisResolution` |
| origin-inclusive mouth-AABB + margin | `X[-84.1,168.7) Y[0.0,53.3) Z[-1175,-937)`, `253×54×238`, `3,251,556` cells, step 1 | 4,000,000 | 9,598 | 309 | 845 | 120 / 3 | 274.811 | `NotConnectedAtThisResolution` |
| whole-strate centered origin cap window | `X[-183,183) Y[-183,183) Z[-1175,-937)`, `366×366×238`, `31,881,528` cells, step 1 | 32,000,000 | 115,578 | 2,903 | 11,418 | 172 / 4 | 274.811 | `NotConnectedAtThisResolution` |
The origin-inclusive box did **not** flip to `Connected`, and the larger centered window did not
either. The hypothesis is therefore **not confirmed** for this candidate. The whole-strate probe
used an explicit 32M cap so it could remain a real step-1 player-fit measurement; the production
fine-law cap remains 4M. A step-2 player-fit probe is not equivalent here: the player-fit API
intentionally refuses every `SampleStep != 1` because the capsule stencil cannot be resolved at
that resolution.
### 16.2 Part B — route-aware window policy
The measurement now has an explicit `bIncludeOriginInCoverWindow` flag. It expands the fitted mouth
AABB to include `(0,0)` **before** the `MaxCells` check. If that route window is over the cap, the
measurement refuses with `The requested sample grid exceeds MaxCells; measurement refused.` It does
not shrink back to a mouth-only box.
The policy used by the eight-row law is:
- origin-inclusive mouth-AABB: `Maze`, `VerticalShafts`, `TunnelNetwork`, and `Underwater`;
- mouth-AABB plus capsule margin: `CrystalChamber`, `FlatPlain`, `SurfaceWorld`, and
`FloatingIslands`.
The first group is origin-rooted by its actual topology: Maze parent chains, VerticalShafts
drainage-tree chains, and the origin-flowing room graph shared by TunnelNetwork/Underwater. The
global structural spine exists for every stack, but it is not treated as the promised route for
the four local/independent families.
### 16.3 Part C — corrected eight-row law
These are the fixed seed-0 identities in the scale diagnosis. All valid rows are step 1. The
window column is part of the result, not an omitted implementation detail.
| archetype | player-fit window (`X/Y/Z`, grid, cells) | fit cells | components | largest | arrival / departure | gap (voxels) | law |
|---|---|---:|---:|---:|---:|---:|---|
| CrystalChamber | mouth-AABB + margin; `X[-178.9,-152.8) Y[-38.2,29.1) Z[-537,-295)`, `27×68×242`, `444,312` | 992 | 102 | 165 | 40 / 6 | 50.070 | `NotConnectedAtThisResolution` |
| FlatPlain | mouth-AABB + margin; `X[-178.9,-152.8) Y[-38.2,29.1) Z[-534,-298)`, `27×68×236`, `433,296` | 2,346 | 9 | 2,337 | 1 / 2,337 | 138.105 | `NotConnectedAtThisResolution` |
| FloatingIslands | mouth-AABB + margin; `X[-143.5,-135.5) Y[121.8,129.8) Z[-537,-295)`, `8×8×242`, `15,488` | 0 | 0 | 0 | 0 / 0 | -1.000 | `StartCellNotPlayerFit` |
| Maze | origin-inclusive mouth-AABB + margin; `X[-84.1,168.7) Y[0.0,53.3) Z[-1175,-937)`, `253×54×238`, `3,251,556` | 9,598 | 309 | 845 | 120 / 3 | 274.811 | `NotConnectedAtThisResolution` |
| SurfaceWorld | mouth-AABB + margin; `X[-133.7,-76.1) Y[-68.8,47.5) Z[-1495,-1257)`, `58×117×238`, `1,615,068` | 1,720 | 248 | 801 | 1 / 3 | 101.454 | `NotConnectedAtThisResolution` |
| TunnelNetwork | origin-inclusive mouth-AABB + margin; `X[-178.9,0.0) Y[-38.2,29.1) Z[-536,-296)`, `179×68×240`, `2,921,280` | 3,684 | 213 | 1,176 | 108 / 82 | 83.361 | `NotConnectedAtThisResolution` |
| Underwater | origin-inclusive mouth-AABB + margin; `X[-178.9,0.0) Y[-38.2,29.1) Z[-535,-297)`, `179×68×238`, `2,896,936` | 1,669 | 79 | 618 | 109 / 109 | 0.000 | `CoarseLiedBudgetExhausted` (**unknown**, not a pass) |
| VerticalShafts | origin-inclusive mouth-AABB + margin; **refused before allocation** because the required grid exceeds `MaxCells=4,000,000` | — | — | — | — | — | `REFUSED(MaxCells)` |
The valid-row tally is **0/7 Connected**, with **1/8 refused** and one `CoarseLiedBudgetExhausted`
unknown. The old one-cell/one-pocket success is not present in this corrected table.
### 16.4 Plain verdict
The remaining failure is not the cropped measurement window. Maze remains disconnected in player-fit
volume even when the origin is covered and when the full 32M-cell step-1 centered window is measured.
The proven spanning-tree graph and the player-fit volume are different claims: the graph has a route,
but the generated player-fitting corridor/mouth volume does not currently provide a continuous route
from these two passage mouths. The smallest next generation task is therefore at the mouths or their
player-fit join to the corridor network (with the possibility of a fine-floor/stencil break along
the route), not another window, size, density, or topology tuning pass.
@@ -13,6 +13,7 @@
#include "VoxelStrateComposer.h" #include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelStratePreview.h" #include "VoxelStratePreview.h"
#include "VoxelForgePlayerFitWindow.h"
#include "VoxelTypes.h" #include "VoxelTypes.h"
namespace namespace
@@ -113,6 +114,7 @@ namespace
FVoxelStrateMetrics FineMetrics; FVoxelStrateMetrics FineMetrics;
FVoxelConnectivityDiagnostics LegacyLaw; FVoxelConnectivityDiagnostics LegacyLaw;
FVoxelConnectivityDiagnostics PlayerFitLaw; FVoxelConnectivityDiagnostics PlayerFitLaw;
FString FineWindowSummary;
double SelectionScore = -1.0; double SelectionScore = -1.0;
FString LawText; FString LawText;
int32 TargetStrateIndex = INDEX_NONE; int32 TargetStrateIndex = INDEX_NONE;
@@ -247,22 +249,15 @@ namespace
const FVoxelStrateMeasureSettings& BaseSettings, const FVoxelStrateMeasureSettings& BaseSettings,
const FVoxelStrateFinePreviewSettings& FinePreviewSettings, const FVoxelStrateFinePreviewSettings& FinePreviewSettings,
const FVector& ArrivalPoint, const FVector& ArrivalPoint,
const FVector& DeparturePoint) const FVector& DeparturePoint,
ECaveGeneratorType Archetype)
{ {
FVoxelStrateMeasureSettings Settings = FVoxelStrateMeasureSettings Settings =
FinePreviewSettings.MakeMeasureSettings(BaseSettings); FinePreviewSettings.MakeMeasureSettings(BaseSettings);
// The old fine preview was centred on the coarse largest-component representative. That // The fine preview follows the measured route contract. Local topologies use the two
// is useful for a picture, but it can put both mouths outside the ROI and turn a real // mouths plus a capsule margin; origin-rooted topologies also cover the (0,0) spine.
// player-fit route into a meaningless OutOfWindow result. Fit the fine ROI to the two VoxelForgePlayerFitWindow::ConfigureMouthWindow(
// existing mouth XY points instead; the small explicit margin leaves room for the Settings, ArrivalPoint, DeparturePoint, Archetype);
// capsule's horizontal stencil at either endpoint without changing placement.
Settings.CenterXY = FVector2D(
0.5f * (ArrivalPoint.X + DeparturePoint.X),
0.5f * (ArrivalPoint.Y + DeparturePoint.Y));
Settings.CoverPointA = FVector2D(ArrivalPoint.X, ArrivalPoint.Y);
Settings.CoverPointB = FVector2D(DeparturePoint.X, DeparturePoint.Y);
Settings.CoverMarginVoxels = FMath::CeilToFloat(
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
return Settings; return Settings;
} }
@@ -366,7 +361,8 @@ namespace
FinePreviewSettings.MaxCells = FinePreviewMaxCells; FinePreviewSettings.MaxCells = FinePreviewMaxCells;
FVoxelStrateMeasureSettings FineMeasureSettings = FVoxelStrateMeasureSettings FineMeasureSettings =
VF_MakeFinePlayerFitSettings( VF_MakeFinePlayerFitSettings(
MeasureSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint); MeasureSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint,
Candidate.Archetype);
FVoxelStrateMetrics FineMetrics; FVoxelStrateMetrics FineMetrics;
const FVoxelConnectivityDiagnostics PlayerFitLaw = const FVoxelConnectivityDiagnostics PlayerFitLaw =
VF_DiagnosePlayerFitConnectivityWithSampler( VF_DiagnosePlayerFitConnectivityWithSampler(
@@ -374,6 +370,8 @@ namespace
ArrivalPoint, DeparturePoint, FineMeasureSettings, &FineMetrics); ArrivalPoint, DeparturePoint, FineMeasureSettings, &FineMetrics);
OutSample.FineMetrics = FineMetrics; OutSample.FineMetrics = FineMetrics;
OutSample.PlayerFitLaw = PlayerFitLaw; OutSample.PlayerFitLaw = PlayerFitLaw;
OutSample.FineWindowSummary = VoxelForgePlayerFitWindow::DescribeResolvedWindow(
FineMetrics, FineMeasureSettings.SampleStep, Candidate.Archetype);
OutSample.LawText = FString::Printf( OutSample.LawText = FString::Printf(
TEXT("legacy=%s%s; player_fit=%s%s"), TEXT("legacy=%s%s; player_fit=%s%s"),
VF_ShowcaseConnectivityName(Law.Result), VF_ShowcaseConnectivityName(Law.Result),
@@ -948,7 +946,8 @@ namespace
FString Report = FString::Printf( FString Report = FString::Printf(
TEXT("%s (player-fit fields are fitted SampleStep=1; walkable floor area and " TEXT("%s (player-fit fields are fitted SampleStep=1; walkable floor area and "
"median vertical clearance are coarse SampleStep=4 proxies)\n" "median vertical clearance are coarse SampleStep=4 proxies)\n"
"archetype | player_fit_fraction_step1 | traversable_share_step1 | " "archetype | player_fit_window | player_fit_fraction_step1 | "
"traversable_share_step1 | "
"fit_cells_step1 | components_step1 | largest_component_cells_step1 | " "fit_cells_step1 | components_step1 | largest_component_cells_step1 | "
"arrival_component_cells_step1 | departure_component_cells_step1 | " "arrival_component_cells_step1 | departure_component_cells_step1 | "
"mouth_component_gap_voxels_step1 | restricted_law_step1 | " "mouth_component_gap_voxels_step1 | restricted_law_step1 | "
@@ -965,6 +964,7 @@ namespace
{ {
Report += FString::Printf( Report += FString::Printf(
TEXT("%s | no measured sample | no measured sample | no measured sample | " TEXT("%s | no measured sample | no measured sample | no measured sample | "
"no measured sample | "
"no measured sample | no measured sample | no measured sample | " "no measured sample | no measured sample | no measured sample | "
"no measured sample | no measured sample | no measured sample | " "no measured sample | no measured sample | no measured sample | "
"n/a | n/a | n/a\n"), "n/a | n/a | n/a\n"),
@@ -979,9 +979,10 @@ namespace
const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction
* 128.0f * 128.0f; * 128.0f * 128.0f;
Report += FString::Printf( Report += FString::Printf(
TEXT("%s | %.6f | %.6f | %lld | %d | %lld | %lld | %lld | %.3f | %s | " TEXT("%s | %s | %.6f | %.6f | %lld | %d | %lld | %lld | %lld | %.3f | %s | "
"%.1f (%.6f footprint) | %.2f | %.3f\n"), "%.1f (%.6f footprint) | %.2f | %.3f\n"),
VF_GetStrateArchetypeName(Archetype), VF_GetStrateArchetypeName(Archetype),
*Found->FineWindowSummary,
M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f, M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f,
M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f, M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f,
static_cast<long long>(M.NumPlayerFitCells), static_cast<long long>(M.NumPlayerFitCells),
@@ -1042,7 +1043,8 @@ namespace
Stack.PrepareChunk(Context); Stack.PrepareChunk(Context);
FShowcaseStackSampler Sampler(Stack); FShowcaseStackSampler Sampler(Stack);
const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings( const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings(
BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint); BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint,
MazeSample.Candidate.Archetype);
FVoxelStrateMetrics ZeroMetrics; FVoxelStrateMetrics ZeroMetrics;
const FVoxelConnectivityDiagnostics ZeroLaw = const FVoxelConnectivityDiagnostics ZeroLaw =
VF_DiagnosePlayerFitConnectivityWithSampler( VF_DiagnosePlayerFitConnectivityWithSampler(
@@ -1096,7 +1098,8 @@ namespace
} }
const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings( const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings(
BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint); BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint,
MazeSample.Candidate.Archetype);
const float RadiusSweep[] = { 6.0f, 8.0f, 10.0f, 12.0f, 12.5f }; const float RadiusSweep[] = { 6.0f, 8.0f, 10.0f, 12.0f, 12.5f };
OutReport = TEXT( OutReport = TEXT(
"Part A Maze corridor-radius sweep (same seed/index, fitted step-1 ROI; " "Part A Maze corridor-radius sweep (same seed/index, fitted step-1 ROI; "
@@ -1889,12 +1892,13 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
} }
const FString FineSummary = Sample.FineMetrics.bPlayerFitResolved const FString FineSummary = Sample.FineMetrics.bPlayerFitResolved
? FString::Printf( ? FString::Printf(
TEXT("fit_step1=%.6f traversable_step1=%.6f fit_cells_step1=%lld " TEXT("window=%s; fit_step1=%.6f traversable_step1=%.6f fit_cells_step1=%lld "
"components_step1=%d largest_component_cells_step1=%lld " "components_step1=%d largest_component_cells_step1=%lld "
"arrival_component_cells_step1=%lld departure_component_cells_step1=%lld " "arrival_component_cells_step1=%lld departure_component_cells_step1=%lld "
"mouth_component_gap_step1=%.3f min_player_clearance_step1=%.2f " "mouth_component_gap_step1=%.3f min_player_clearance_step1=%.2f "
"restricted_law_step1=%s; coarse_step4_walkable=%.6f " "restricted_law_step1=%s; coarse_step4_walkable=%.6f "
"coarse_step4_clearance=%d voxels"), "coarse_step4_clearance=%d voxels"),
*Sample.FineWindowSummary,
Sample.FineMetrics.PlayerFitFraction, Sample.FineMetrics.PlayerFitFraction,
Sample.FineMetrics.TraversableComponentShare, Sample.FineMetrics.TraversableComponentShare,
static_cast<long long>(Sample.FineMetrics.NumPlayerFitCells), static_cast<long long>(Sample.FineMetrics.NumPlayerFitCells),
@@ -1908,8 +1912,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
Sample.Metrics.WalkableFraction, Sample.Metrics.WalkableFraction,
Sample.Metrics.MedianVerticalClearance) Sample.Metrics.MedianVerticalClearance)
: FString::Printf( : FString::Printf(
TEXT("fit_step1=UNRESOLVED reason=%s; coarse_step4_walkable=%.6f " TEXT("window=%s; fit_step1=UNRESOLVED reason=%s; coarse_step4_walkable=%.6f "
"coarse_step4_clearance=%d voxels"), "coarse_step4_clearance=%d voxels"),
*Sample.FineWindowSummary,
Sample.FineMetrics.PlayerFitRefusalReason.IsEmpty() Sample.FineMetrics.PlayerFitRefusalReason.IsEmpty()
? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason, ? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason,
Sample.Metrics.WalkableFraction, Sample.Metrics.WalkableFraction,
@@ -2114,7 +2119,8 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
World, Selected.TargetStrateIndex, FineArrivalPoint, FineDeparturePoint)); World, Selected.TargetStrateIndex, FineArrivalPoint, FineDeparturePoint));
const FVoxelStrateMeasureSettings FineMeasureSettings = const FVoxelStrateMeasureSettings FineMeasureSettings =
VF_MakeFinePlayerFitSettings( VF_MakeFinePlayerFitSettings(
MeasureSettings, FinePreviewSettings, FineArrivalPoint, FineDeparturePoint); MeasureSettings, FinePreviewSettings, FineArrivalPoint, FineDeparturePoint,
Candidate.Archetype);
FVoxelStrateSampleGrid FineGrid; FVoxelStrateSampleGrid FineGrid;
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler( const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness, Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness,
@@ -2224,8 +2230,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
{ {
const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex]; const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex];
Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window; Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window;
Summaries[ArchetypeIndex].WindowSummary = VF_FormatShowcaseSummaryWindow( Summaries[ArchetypeIndex].WindowSummary = FString::Printf(
Stats[ArchetypeIndex]); TEXT("coarse_window=%s; player_fit_window=%s"),
*VF_FormatShowcaseSummaryWindow(Stats[ArchetypeIndex]),
*Selected.FineWindowSummary);
Summaries[ArchetypeIndex].PlayerFitFractionSummary = FString::Printf( Summaries[ArchetypeIndex].PlayerFitFractionSummary = FString::Printf(
TEXT("%.6f (step=1)"), Selected.FineMetrics.PlayerFitFraction); TEXT("%.6f (step=1)"), Selected.FineMetrics.PlayerFitFraction);
Summaries[ArchetypeIndex].TraversableComponentShareSummary = FString::Printf( Summaries[ArchetypeIndex].TraversableComponentShareSummary = FString::Printf(
@@ -2239,7 +2247,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
"components_step1=%d, largest_component_cells_step1=%lld, " "components_step1=%d, largest_component_cells_step1=%lld, "
"arrival_component_cells_step1=%lld, departure_component_cells_step1=%lld, " "arrival_component_cells_step1=%lld, departure_component_cells_step1=%lld, "
"mouth_component_gap_voxels_step1=%.3f, player_clearance_step1=%.2f voxels, " "mouth_component_gap_voxels_step1=%.3f, player_clearance_step1=%.2f voxels, "
"restricted_law_step1=%s, window=%s"), "restricted_law_step1=%s, coarse_window=%s, player_fit_window=%s"),
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]), VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]),
Stats[ArchetypeIndex].Survivors.Num(), Stats[ArchetypeIndex].Survivors.Num(),
Selected.Candidate.Seed, Selected.Candidate.Index, Selected.Candidate.Seed, Selected.Candidate.Index,
@@ -2258,7 +2266,8 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
Selected.PlayerFitLaw.StartToGoalComponentDistanceCells, Selected.PlayerFitLaw.StartToGoalComponentDistanceCells,
Selected.FineMetrics.MinimumPlayerClearanceVoxels, Selected.FineMetrics.MinimumPlayerClearanceVoxels,
VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result), VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result),
*PreviewCandidates[ArchetypeIndex].Window.Describe())); *PreviewCandidates[ArchetypeIndex].Window.Describe(),
*Selected.FineWindowSummary));
} }
FString IndexPath; FString IndexPath;
@@ -0,0 +1,101 @@
// Shared measurement-window policy for player-fit diagnostics.
//
// This is deliberately test/editor-side policy. The density generators do not know which two
// points a measurement is asking about, so the caller that knows the archetype must declare
// whether its topology can require the origin spine.
#pragma once
#if WITH_DEV_AUTOMATION_TESTS
#include "CoreMinimal.h"
#include "VoxelStrateMeasure.h"
#include "VoxelStrateTypes.h"
namespace VoxelForgePlayerFitWindow
{
/**
* The four families below have an origin-rooted connectivity implication:
*
* - Maze: every lattice parent chain terminates at (0,0,0).
* - VerticalShafts: every drainage-tree parent chain terminates at the origin spine.
* - TunnelNetwork/Underwater: their origin-flowing room graph terminates at the origin hub.
*
* Slabs, SurfaceWorld, and FloatingIslands have local/independent interior topology for
* this law. The global structural spine is still generated for them, but it is not the
* topology that their mouth-to-mouth fit law promises to follow.
*/
inline bool NeedsOriginInWindow(ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::Maze:
case ECaveGeneratorType::VerticalShafts:
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return true;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
case ECaveGeneratorType::SurfaceWorld:
case ECaveGeneratorType::FloatingIslands:
default:
return false;
}
}
inline const TCHAR* RouteWindowName(ECaveGeneratorType Archetype)
{
return NeedsOriginInWindow(Archetype)
? TEXT("origin-inclusive mouth-AABB + margin")
: TEXT("mouth-AABB + margin");
}
inline FString DescribeResolvedWindow(
const FVoxelStrateMetrics& Metrics,
int32 SampleStep,
ECaveGeneratorType Archetype)
{
if (!Metrics.bValid)
{
return FString::Printf(
TEXT("%s step=%d REFUSED: %s"),
RouteWindowName(Archetype),
SampleStep,
Metrics.RefusalReason.IsEmpty() ? TEXT("unknown") : *Metrics.RefusalReason);
}
return FString::Printf(
TEXT("%s step=%d X[%.1f,%.1f) Y[%.1f,%.1f) Z[%d,%d) "
"grid=%dx%dx%d cells=%lld"),
RouteWindowName(Archetype),
SampleStep,
Metrics.SampledMinX,
Metrics.SampledMaxX,
Metrics.SampledMinY,
Metrics.SampledMaxY,
Metrics.SampledMinZ,
Metrics.SampledMaxZ,
Metrics.SampledNumX,
Metrics.SampledNumY,
Metrics.SampledNumZ,
static_cast<long long>(Metrics.NumSampled));
}
/** Configure the exact fitted XY window used by the player-fit law. */
inline void ConfigureMouthWindow(
FVoxelStrateMeasureSettings& InOutSettings,
const FVector& ArrivalPoint,
const FVector& DeparturePoint,
ECaveGeneratorType Archetype)
{
InOutSettings.CenterXY = FVector2D(
0.5f * (ArrivalPoint.X + DeparturePoint.X),
0.5f * (ArrivalPoint.Y + DeparturePoint.Y));
InOutSettings.CoverPointA = FVector2D(ArrivalPoint.X, ArrivalPoint.Y);
InOutSettings.CoverPointB = FVector2D(DeparturePoint.X, DeparturePoint.Y);
InOutSettings.CoverMarginVoxels = FMath::CeilToFloat(
InOutSettings.PlayerCapsuleRadiusVoxels) + 2.0f;
InOutSettings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Archetype);
}
}
#endif // WITH_DEV_AUTOMATION_TESTS
@@ -13,6 +13,7 @@
#include "VoxelForgeTestFixture.h" #include "VoxelForgeTestFixture.h"
#include "VoxelStrateComposer.h" #include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelForgePlayerFitWindow.h"
#include "VoxelTypes.h" #include "VoxelTypes.h"
namespace namespace
@@ -703,17 +704,99 @@ namespace
} }
} }
int32 VF_MaxCenteredRadiusForCap(
int32 TopVoxelZ,
int32 BottomVoxelZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings)
{
const int64 StrateHeight = static_cast<int64>(TopVoxelZ) + 1
- static_cast<int64>(BottomVoxelZ);
const int64 MaxMargin64 = FMath::Max<int64>(1, StrateHeight / 4);
const int32 MaxMargin = static_cast<int32>(FMath::Min<int64>(
MaxMargin64, static_cast<int64>(INT32_MAX)));
int32 RequestedMargin = Settings.InteriorMarginVoxels;
if (RequestedMargin < 0)
{
RequestedMargin = FMath::CeilToInt(2.0f * BoundarySealThickness);
}
const int32 ResolvedMargin = FMath::Clamp(RequestedMargin, 0, MaxMargin);
const int64 InteriorHeight = StrateHeight - 2 * static_cast<int64>(ResolvedMargin);
if (InteriorHeight <= 0 || Settings.SampleStep <= 0 || Settings.MaxCells <= 0)
{
return 0;
}
const int64 NumZ = (InteriorHeight + Settings.SampleStep - 1)
/ static_cast<int64>(Settings.SampleStep);
const int64 MaxAxisCells = FMath::Min<int64>(
static_cast<int64>(INT32_MAX),
static_cast<int64>(FMath::Sqrt(
static_cast<double>(Settings.MaxCells / FMath::Max<int64>(NumZ, 1)))));
int32 Radius = static_cast<int32>(FMath::Max<int64>(
1, (MaxAxisCells * static_cast<int64>(Settings.SampleStep)) / 2));
auto CellCountForRadius = [Settings, NumZ](int32 CandidateRadius) -> int64
{
const int64 NumXY = (2 * static_cast<int64>(CandidateRadius)
+ Settings.SampleStep - 1)
/ static_cast<int64>(Settings.SampleStep);
if (NumXY <= 0 || NumXY > INT64_MAX / NumXY
|| NumXY * NumXY > INT64_MAX / FMath::Max<int64>(NumZ, 1))
{
return INT64_MAX;
}
return NumXY * NumXY * NumZ;
};
while (Radius > 1 && CellCountForRadius(Radius) > Settings.MaxCells)
{
--Radius;
}
while (Radius < INT32_MAX
&& CellCountForRadius(Radius + 1) <= Settings.MaxCells)
{
++Radius;
}
return Radius;
}
void VF_ReportPlayerFitRow( void VF_ReportPlayerFitRow(
FAutomationTestBase& Test, FAutomationTestBase& Test,
const TCHAR* Label, const TCHAR* Label,
const FString& WindowLabel,
int32 SampleStep,
const FVoxelStrateMetrics& Metrics, const FVoxelStrateMetrics& Metrics,
const FVoxelConnectivityDiagnostics& Diagnostics) const FVoxelConnectivityDiagnostics& Diagnostics)
{ {
if (!Metrics.bValid || !Metrics.bPlayerFitResolved)
{
const FString& Refusal = !Metrics.RefusalReason.IsEmpty()
? Metrics.RefusalReason : Metrics.PlayerFitRefusalReason;
Test.AddInfo(FString::Printf(
TEXT("PLAYER_FIT step=%d window=%s %s: REFUSED reason=%s"),
SampleStep,
*WindowLabel,
Label,
Refusal.IsEmpty() ? TEXT("player-fit metrics unresolved") : *Refusal));
return;
}
Test.AddInfo(FString::Printf( Test.AddInfo(FString::Printf(
TEXT("PLAYER_FIT step=1 fitted-window %s: fit_cells=%lld components=%d " TEXT("PLAYER_FIT step=%d window=%s bounds=X[%.1f,%.1f) Y[%.1f,%.1f) "
"Z[%d,%d) grid=%dx%dx%d sampled=%lld %s: fit_cells=%lld components=%d "
"largest_component_cells=%lld arrival_component_cells=%lld " "largest_component_cells=%lld arrival_component_cells=%lld "
"departure_component_cells=%lld mouth_component_gap_voxels=%.3f " "departure_component_cells=%lld mouth_component_gap_voxels=%.3f "
"fit_fraction=%.9f traversable_share=%.9f law=%s"), "fit_fraction=%.9f traversable_share=%.9f law=%s"),
SampleStep,
*WindowLabel,
Metrics.SampledMinX,
Metrics.SampledMaxX,
Metrics.SampledMinY,
Metrics.SampledMaxY,
Metrics.SampledMinZ,
Metrics.SampledMaxZ,
Metrics.SampledNumX,
Metrics.SampledNumY,
Metrics.SampledNumZ,
static_cast<long long>(Metrics.NumSampled),
Label, Label,
static_cast<long long>(Metrics.NumPlayerFitCells), static_cast<long long>(Metrics.NumPlayerFitCells),
Metrics.NumTraversableComponents, Metrics.NumTraversableComponents,
@@ -1016,7 +1099,115 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
ControlSettings.PlayerWalkableFloorAngleDegrees, ControlSettings.PlayerWalkableFloorAngleDegrees,
ControlSettings.HeadroomCells)); ControlSettings.HeadroomCells));
// H3 — the eight fixed showcase identities, measured at step 1 on a mouth-fitted window. // PART A — direct route-window experiment on the requested Maze identity. The first row is
// intentionally the old mouth-only box; the second turns on the topology-aware origin
// coverage; the third uses the largest centered origin window that the same MaxCells cap can
// hold while retaining enough resolution to contain both mouths.
{
const int32 PartAMazeSlot = FTestWorld::SlotMaze;
VF_SetCandidateOnSlot(World, MazeCandidate, PartAMazeSlot);
FVector ArrivalPoint = FVector::ZeroVector;
FVector DeparturePoint = FVector::ZeroVector;
int32 ArrivalCount = 0;
int32 DepartureCount = 0;
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
{
if (Passage.LowerStrateIndex == PartAMazeSlot
&& Passage.UpperStrateIndex + 1 == PartAMazeSlot)
{
ArrivalPoint = Passage.LowerPoint;
++ArrivalCount;
}
if (Passage.UpperStrateIndex == PartAMazeSlot
&& Passage.LowerStrateIndex == PartAMazeSlot + 1)
{
DeparturePoint = Passage.UpperPoint;
++DepartureCount;
}
}
TestEqual(TEXT("PART A Maze has one arrival mouth"), ArrivalCount, 1);
TestEqual(TEXT("PART A Maze has one departure mouth"), DepartureCount, 1);
int32 MazeTopVoxelZ = 0;
int32 MazeBottomVoxelZ = 0;
TestTrue(TEXT("PART A Maze has a vertical measurement range"),
World.GetSlotVoxelZRange(PartAMazeSlot, MazeTopVoxelZ, MazeBottomVoxelZ));
if (ArrivalCount == 1 && DepartureCount == 1
&& World.GetSlotVoxelZRange(PartAMazeSlot, MazeTopVoxelZ, MazeBottomVoxelZ))
{
FVoxelStrateMeasureSettings PartABase = CharacterFitSettings;
PartABase.SampleStep = 1;
PartABase.RadiusInVoxels = 64;
PartABase.MaxCells = FineDiagnosisMaxCells;
PartABase.MaxRouteRetries = 16;
PartABase.HeadroomCells = 2;
PartABase.InteriorMarginVoxels = -1;
FGeneratorSampler GeneratorSampler(*World.Generator);
auto RunPartAWindow =
[&](const FString& WindowLabel, const FVoxelStrateMeasureSettings& Settings)
{
FVoxelStrateMetrics Metrics;
const FVoxelConnectivityDiagnostics Diagnostics =
VF_DiagnosePlayerFitConnectivityWithSampler(
GeneratorSampler, MazeBottomVoxelZ, MazeTopVoxelZ + 1,
VF_BoundarySealForCandidate(MazeCandidate),
ArrivalPoint, DeparturePoint, Settings, &Metrics);
VF_ReportPlayerFitRow(
*this, TEXT("Maze seed=0 index=50 slot=4"), WindowLabel,
Settings.SampleStep, Metrics, Diagnostics);
return TTuple<FVoxelStrateMetrics, FVoxelConnectivityDiagnostics>(
MoveTemp(Metrics), Diagnostics);
};
FVoxelStrateMeasureSettings CurrentSettings = PartABase;
VoxelForgePlayerFitWindow::ConfigureMouthWindow(
CurrentSettings, ArrivalPoint, DeparturePoint, ECaveGeneratorType::Maze);
CurrentSettings.bIncludeOriginInCoverWindow = false;
const auto CurrentResult = RunPartAWindow(
TEXT("current mouth-AABB + margin"), CurrentSettings);
FVoxelStrateMeasureSettings OriginSettings = PartABase;
VoxelForgePlayerFitWindow::ConfigureMouthWindow(
OriginSettings, ArrivalPoint, DeparturePoint, ECaveGeneratorType::Maze);
const auto OriginResult = RunPartAWindow(
TEXT("origin-inclusive mouth-AABB + margin"), OriginSettings);
FVoxelStrateMeasureSettings WholeSettings = PartABase;
WholeSettings.MaxCells = FineDiagnosisMaxCells * 8;
WholeSettings.CenterXY = FVector2D::ZeroVector;
WholeSettings.CoverPointA.Reset();
WholeSettings.CoverPointB.Reset();
WholeSettings.bIncludeOriginInCoverWindow = false;
const float MouthMargin = FMath::CeilToFloat(
WholeSettings.PlayerCapsuleRadiusVoxels) + 2.0f;
const int32 MouthReach = FMath::CeilToInt(FMath::Max(
FMath::Max(FMath::Abs(ArrivalPoint.X), FMath::Abs(DeparturePoint.X)),
FMath::Max(FMath::Abs(ArrivalPoint.Y), FMath::Abs(DeparturePoint.Y)))
+ MouthMargin);
WholeSettings.SampleStep = 1;
WholeSettings.RadiusInVoxels = VF_MaxCenteredRadiusForCap(
MazeTopVoxelZ, MazeBottomVoxelZ,
VF_BoundarySealForCandidate(MazeCandidate), WholeSettings);
const FString WholeWindowLabel = FString::Printf(
TEXT("whole-strate centered origin cap window (radius=%d; mouth_reach=%d; "
"MaxCells=%d)"),
WholeSettings.RadiusInVoxels, MouthReach, WholeSettings.MaxCells);
const auto WholeResult = RunPartAWindow(WholeWindowLabel, WholeSettings);
AddInfo(FString::Printf(
TEXT("PART_A verdict: current=%s; origin-inclusive=%s; whole-strate-cap=%s; "
"origin flip=%s"),
VF_DiagnosisConnectivityName(CurrentResult.Get<1>().Result),
VF_DiagnosisConnectivityName(OriginResult.Get<1>().Result),
VF_DiagnosisConnectivityName(WholeResult.Get<1>().Result),
OriginResult.Get<1>().Result == EVoxelConnectivityResult::Connected
? TEXT("CONFIRMED") : TEXT("NOT_CONFIRMED")));
}
}
// H3 — the eight fixed showcase identities, measured at step 1 on the topology-aware fitted
// window. Origin-rooted families include (0,0) before the MaxCells refusal check.
// The Maze identity is intentionally the requested seed 0 / index 50 / fourth one-based slot. // The Maze identity is intentionally the requested seed 0 / index 50 / fourth one-based slot.
struct FShowcaseSelection struct FShowcaseSelection
{ {
@@ -1065,6 +1256,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
int32 NumH3Connected = 0; int32 NumH3Connected = 0;
int32 NumH3DegenerateConnected = 0; int32 NumH3DegenerateConnected = 0;
int32 NumH3Measured = 0; int32 NumH3Measured = 0;
int32 NumH3Refused = 0;
for (const FShowcaseSelection& Selection : Selections) for (const FShowcaseSelection& Selection : Selections)
{ {
@@ -1109,13 +1301,8 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
FVoxelStrateMeasureSettings Settings = CharacterFitSettings; FVoxelStrateMeasureSettings Settings = CharacterFitSettings;
Settings.SampleStep = 1; Settings.SampleStep = 1;
Settings.RadiusInVoxels = 64; Settings.RadiusInVoxels = 64;
Settings.CenterXY = FVector2D( VoxelForgePlayerFitWindow::ConfigureMouthWindow(
0.5f * (ArrivalPoint.X + DeparturePoint.X), Settings, ArrivalPoint, DeparturePoint, Selection.Archetype);
0.5f * (ArrivalPoint.Y + DeparturePoint.Y));
Settings.CoverPointA = FVector2D(ArrivalPoint.X, ArrivalPoint.Y);
Settings.CoverPointB = FVector2D(DeparturePoint.X, DeparturePoint.Y);
Settings.CoverMarginVoxels = FMath::CeilToFloat(
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
Settings.MaxCells = FineDiagnosisMaxCells; Settings.MaxCells = FineDiagnosisMaxCells;
Settings.MaxRouteRetries = 16; Settings.MaxRouteRetries = 16;
Settings.HeadroomCells = 2; Settings.HeadroomCells = 2;
@@ -1136,7 +1323,22 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
GeneratorSampler, BottomVoxelZ, TopVoxelZ + 1, GeneratorSampler, BottomVoxelZ, TopVoxelZ + 1,
VF_BoundarySealForCandidate(Candidate), ArrivalPoint, DeparturePoint, VF_BoundarySealForCandidate(Candidate), ArrivalPoint, DeparturePoint,
Settings, &Metrics); Settings, &Metrics);
VF_ReportPlayerFitRow(*this, Selection.Label, Metrics, Diagnostics); VF_ReportPlayerFitRow(
*this, Selection.Label,
VoxelForgePlayerFitWindow::RouteWindowName(Selection.Archetype),
Settings.SampleStep, Metrics, Diagnostics);
if (!Metrics.bValid || !Metrics.bPlayerFitResolved)
{
++NumH3Refused;
const FString& Refusal = !Metrics.RefusalReason.IsEmpty()
? Metrics.RefusalReason : Metrics.PlayerFitRefusalReason;
AddInfo(FString::Printf(
TEXT("H3 %s law=REFUSED; window=%s; reason=%s"),
Selection.Label,
VoxelForgePlayerFitWindow::RouteWindowName(Selection.Archetype),
Refusal.IsEmpty() ? TEXT("player-fit metrics unresolved") : *Refusal));
continue;
}
++NumH3Measured; ++NumH3Measured;
if (Diagnostics.Result == EVoxelConnectivityResult::Connected) if (Diagnostics.Result == EVoxelConnectivityResult::Connected)
{ {
@@ -1182,11 +1384,12 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("LAW_TALLY restricted step1: before_connected=1/8 " TEXT("LAW_TALLY restricted step1: before_connected=1/8 "
"before_connected_through_4_cells=1; after_connected=%d/%d " "before_connected_through_4_cells=1; after_connected=%d/%d "
"after_connected_through_4_cells=%d measured=%d/8"), "after_connected_through_4_cells=%d measured=%d/8 refused=%d/8"),
NumH3Connected, NumH3Connected,
UE_ARRAY_COUNT(Selections), NumH3Measured,
NumH3DegenerateConnected, NumH3DegenerateConnected,
NumH3Measured)); NumH3Measured,
NumH3Refused));
return true; return true;
} }
@@ -202,6 +202,17 @@ namespace VoxelStrateMeasurePrivate
MaxX64 = FMath::Max(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) + Margin; MaxX64 = FMath::Max(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) + Margin;
MinY64 = FMath::Min(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) - Margin; MinY64 = FMath::Min(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) - Margin;
MaxY64 = FMath::Max(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) + Margin; MaxY64 = FMath::Max(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) + Margin;
if (Settings.bIncludeOriginInCoverWindow)
{
// The fitted box is a route-coverage contract, not merely an endpoint box. An
// origin-rooted graph may have its only mouth-to-mouth path through this spine.
// Expand the requested bounds before the MaxCells check below; never crop the
// route after discovering that the full box is too large.
MinX64 = FMath::Min(MinX64, 0.0);
MaxX64 = FMath::Max(MaxX64, 0.0);
MinY64 = FMath::Min(MinY64, 0.0);
MaxY64 = FMath::Max(MaxY64, 0.0);
}
WindowCenter = FVector2D( WindowCenter = FVector2D(
static_cast<float>(0.5 * (MinX64 + MaxX64)), static_cast<float>(0.5 * (MinX64 + MaxX64)),
static_cast<float>(0.5 * (MinY64 + MaxY64))); static_cast<float>(0.5 * (MinY64 + MaxY64)));
@@ -2169,6 +2180,7 @@ namespace VoxelStrateMeasurePrivate
} }
if (OutPlayerMetrics != nullptr) if (OutPlayerMetrics != nullptr)
{ {
OutPlayerMetrics->RefusalReason = Reason;
OutPlayerMetrics->PlayerFitRefusalReason = Reason; OutPlayerMetrics->PlayerFitRefusalReason = Reason;
} }
}; };
@@ -2216,6 +2228,7 @@ namespace VoxelStrateMeasurePrivate
} }
if (OutPlayerMetrics != nullptr) if (OutPlayerMetrics != nullptr)
{ {
OutPlayerMetrics->RefusalReason = RefusalReason;
OutPlayerMetrics->PlayerFitRefusalReason = RefusalReason; OutPlayerMetrics->PlayerFitRefusalReason = RefusalReason;
} }
return EVoxelConnectivityResult::OutOfWindow; return EVoxelConnectivityResult::OutOfWindow;
@@ -68,6 +68,11 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
TOptional<FVector2D> CoverPointA; TOptional<FVector2D> CoverPointA;
TOptional<FVector2D> CoverPointB; TOptional<FVector2D> CoverPointB;
float CoverMarginVoxels = 48.0f; float CoverMarginVoxels = 48.0f;
// Origin-rooted topologies can route from one mouth to the other through the (0,0) spine.
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
// this enabled for that topology; an over-cap route window is then refused honestly instead
// of being cropped back to a mouth-only box.
bool bIncludeOriginInCoverWindow = false;
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 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
int32 HeadroomCells = 2; // Legacy walkable-column metric only. int32 HeadroomCells = 2; // Legacy walkable-column metric only.