fix(passages): land mouths where a BODY fits -- and the cause is now cornered

VF_SuggestLandingPoint aimed at open AIR. Air is not where a body fits: a
mouth could sit in a perfectly open pocket no capsule can stand in. It now
requires a legal capsule pose -- clearance (r 1.36, half-height 3.52),
support within a 1.8-voxel / 45 cm step height, a walkable support PATCH
(4 of 5 columns, centre required, slope <= 44.8 deg), positive density
throughout the capsule, strict 2x seal interior, bounded lateral snap.

It stays a PURE free function, which was the hard constraint --
GeneratePassages runs inside Initialize while the layout is half-built. So
it cannot flood-fill, and Codex correctly did not pretend otherwise: it
aims at deterministic guaranteed features instead (room centre/floor, slab
floor, horizontal maze edge, shaft feature-core ledge pose, island top).
Both mouths query their own strate independently; no passage chaining, so
LayoutOrderIndependence stays green.

Placement measurably improved:
  Maze            comps 309 -> 82,  largest 176 -> 1217, gap 206 -> 122
  VerticalShafts  comps 112 -> 36,  largest  96 ->  619
  CrystalChamber  gap 47.5 -> 19.1, departure mouth now in a 717-cell comp
  Underwater      comps  86 -> 13

⚠️ AND THE LAW IS STILL 1/8. Reported honestly rather than tuned. Mouths
now land on legal body-sized poses; the poses just are not connected to
each other.

⇒ BY ELIMINATION, THE CAUSE IS GENERATION. Four candidates, three dead:
  size        DEAD -- Jahni walked it; bore measures 3.62 m vs 3.677 m spec
  the stencil FIXED -- fragmentation was 3x overstated by an exact-Z-1 floor
  placement   FIXED -- mouths now sit on legal poses, several in large comps
  GENERATION  STANDS -- body-sized space exists in big components that do
              not reach each other

⭐ AND THE MAZE TIES TWO OF JAHNI'S COMPLAINTS TOGETHER. Its arrival mouth
lands in a 30-cell pocket while the main network holds 1217. Edge selection
is INDEPENDENT HASH GATING per +X/+Y/+Z edge
(VoxelDensityOpStack.cpp:582/584/586) -- so the corridor graph is a random
subgraph with no connectivity guarantee, exactly the percolation failure
VerticalShafts had before its drainage tree. That is the same root cause as
"mostly a cube full of straight pathway along the edges": a spanning tree
would make it BOTH feel like a maze (dead ends, uneven runs) AND be
connected by construction. One fix, two complaints.

Codex found a real error in my spec: an infinite shaft cylinder has no
support surface AT its axis, so the old exact-axis assertion was
unsatisfiable. It now requires a deterministic feature core and audits tree
connectivity from the resolved axis.

Build clean, 29 succeeded / 0 failed / 2 known deliberate warnings.
Box audits 0 violations. Shaft refinement 16/16. Order-independence green
across 10 slots, 31 passages, 3 pool orders.

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 08:08:38 +02:00
1 parent 110ae9d0ed
commit 4378385b37
7 files changed
+1360 -160

No files matched your search

+11 -8
View File
@@ -406,11 +406,14 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
`MakeStrateSeed` (h:~477) is the shared pure world-seed/strate-index salt used by both `MakeStrateSeed` (h:~477) is the shared pure world-seed/strate-index salt used by both
`BuildChunkCache` and the TunnelNetwork/Underwater destination landing queries. `BuildChunkCache` and the TunnelNetwork/Underwater destination landing queries.
`VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure landing-site `VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure player-fit
query: nearest hash-room height at the requested XY for TunnelNetwork/Underwater, slab void midpoint landing-site query: it evaluates the local archetype source through the same capsule/support
for FlatPlain/CrystalChamber, and bounded lateral searches for a horizontal lattice corridor (one contract as the measurement pass — support within step height, walkable slope, and full capsule
Maze cell), the exact axis of a lower-seal/ledge-safe VerticalShafts tree shaft (up to two local clearance. Room queries use a room-floor core, slabs keep their requested XY, and the sparse
shaft spacings at the 0.6 site density), or a validated blob top (one IslandSpacing). The sources use bounded feature cores: a horizontal lattice corridor (one Maze cell), a selected
VerticalShafts tree shaft plus deterministic ledge-side offsets (up to two local shaft spacings at the 0.6
site density), or a validated blob top (one IslandSpacing). It is intentionally not a flood fill:
the pure query can prove a local pose, not a large component. The
VerticalShafts result is therefore on the drainage tree, not merely in an open roughness disk. VerticalShafts result is therefore on the drainage tree, not merely in an open roughness disk.
It returns `false` when those feature-bearing conditions cannot be proven inside the supplied budget; It returns `false` when those feature-bearing conditions cannot be proven inside the supplied budget;
SurfaceWorld remains refused because its production height can depend on manager-owned biome/per-column SurfaceWorld remains refused because its production height can depend on manager-owned biome/per-column
@@ -452,7 +455,7 @@ Maps depth→strate at runtime; owns passages.
| Method | .cpp line | Role | | Method | .cpp line | Role |
|--------|-----------|------| |--------|-----------|------|
| `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. | | `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
| `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. After XY placement, the destination's pure `VF_SuggestLandingPoint` may move the lower mouth within an archetype-specific lattice budget, then the result is clamped outside the destination seal bands and interpolated through the control points; unsupported/no-footing answers warn and preserve the old random reach. | | `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then is clamped outside the destination seal bands and interpolated through the control points; unsupported/no-fit answers preserve the old random reach. |
| `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. | | `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. |
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. | | `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. | | `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
@@ -666,8 +669,8 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). | | `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. | | `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. | | `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. | | `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries. Answered mouths must match the pure player-fit point within the float envelope; the lower mouth also keeps the 16-point ring and source-footing assertions outside the carve/blend band. This is a local-fit/open-space proxy, not a flood-fill proof. Reports answered/refused mouths and archetypes; fails if either side is vacuous. |
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. | | `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
| `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. | | `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. |
## 4. The density pipeline (most-edited hot path) ## 4. The density pipeline (most-edited hot path)
+76 -14
View File
@@ -1549,19 +1549,27 @@ law has a design-level fix.
--- ---
## 11. Tier 1 status — passages aim at real open space (bounded full-point query; build not run) ## 11. Tier 1 status — passages aim at player-fit source space (pure local stencil)
**Updated 2026-08-29.** `VF_SuggestLandingPoint` (in `VoxelCaveMorphology`) is a **pure free function** **Updated 2026-09-06.** `VF_SuggestLandingPoint` (in `VoxelCaveMorphology`) is a **pure free function**
of (archetype, params, seed, desired XY, lateral budget) — it touches no operator stack, no of (archetype, params, seed, desired XY, lateral budget) — it touches no operator stack, no
`UVoxelStrateManager`, no cache. That matters: `GeneratePassages` runs inside `Initialize`, when the `UVoxelStrateManager`, no cache. That matters: `GeneratePassages` runs inside `Initialize`, when the
layout is half-built and `LayoutVersion` is mid-flight, so constructing anything holding a manager layout is half-built and `LayoutVersion` is mid-flight, so constructing anything holding a manager
pointer there is a re-entrancy trap. pointer there is a re-entrancy trap.
`GeneratePassages` now asks the **destination** strate for a full open/footing point near the chosen XY. `GeneratePassages` now asks **each mouth's own strate**, independently, for a player-fit pose near
Maze, VerticalShafts, and FloatingIslands may snap to a nearby lattice site, shaft, or island top; the chosen XY. The pure query reconstructs the exact default capsule stencil used by
the lower mouth uses the returned X/Y/Z, clamped strictly inside the interior (never into a seal band). `VoxelStrateMeasure`: support coverage within max step height, a walkable support patch, and a clear
Where the archetype cannot prove footing inside its bound, **today's random reach and original XY are capsule volume, with positive density meaning air and non-positive density meaning solid. It samples
preserved unchanged**. only a stateless source evaluator — no manager pointer, live layout, operator stack, cache, or
measurement flood fill enters the query.
The candidate is deliberately a feature core rather than arbitrary air: a room floor core,
requested XY in a slab, a horizontal Maze edge, a VerticalShafts shaft core with a deterministic
ledge-side offset above its lower seal, or a FloatingIslands top surface. The local stencil proves a legal pose, not that the pose belongs to a
large connected component; that question remains in the measurement pass. When no legal local pose
exists inside the bounded source neighbourhood, **the random reach and original XY are preserved
unchanged**.
### ⚠️ Coverage is source-dependent and the number is the point ### ⚠️ Coverage is source-dependent and the number is the point
The failing pre-fix measurement was: The failing pre-fix measurement was:
@@ -1569,11 +1577,15 @@ The failing pre-fix measurement was:
> *7 inter-strate passages, 3 checked, 0 footing checks, 38/48 room/slab ring samples air, 4 > *7 inter-strate passages, 3 checked, 0 footing checks, 38/48 room/slab ring samples air, 4
> query-false (4 unique archetypes; 1 unsupported, 3 supported-but-no-point).* > query-false (4 unique archetypes; 1 unsupported, 3 supported-but-no-point).*
The Tier 1 query now searches laterally for `Maze`, `VerticalShafts`, and `FloatingIslands` landing The Tier 1 query now searches laterally for `Maze`, `VerticalShafts`, and `FloatingIslands` feature
sites within one configured placement period. `SurfaceWorld` remains intentionally refused. cores within one configured placement period, and for cave rooms within one `RoomSpacing`.
`SurfaceWorld` remains intentionally refused.
The extended test uses vertical source footing brackets for those three instead of applying the The extended test uses vertical source footing brackets for those three instead of applying the
room/slab circumference ring to thin or void-dominated geometry. The post-fix measurement is deliberately room/slab circumference ring to thin or void-dominated geometry. The post-fix measurement is not
not claimed here because this change does not build or run the Unreal automation test. claimed as a connectivity proof: the focused fixture now reports 7 passages, 3 upper and 3 lower
player-fit queries answered, 8/14 mouths falling back, 0/0 room/slab ring samples air, and all 3
answered thin-source footing checks valid. The eight-card step-1 measurement is recorded in §12.1;
it is the source of truth for the remaining component gaps.
### Seed contracts, do not mix them ### Seed contracts, do not mix them
- **TunnelNetwork / Underwater** → `VoxelCaveMorphology::MakeStrateSeed` (the strate's own room seed). - **TunnelNetwork / Underwater** → `VoxelCaveMorphology::MakeStrateSeed` (the strate's own room seed).
@@ -1593,7 +1605,9 @@ construction*. It passed whether the aiming worked or not. Replaced with a **16-
destinations. A mouth in a real room has open space around it; a mouth in bedrock has only the tube it destinations. A mouth in a real room has open space around it; a mouth in bedrock has only the tube it
dug itself. Maze, VerticalShafts, and FloatingIslands use a manager-free source density bracket: dug itself. Maze, VerticalShafts, and FloatingIslands use a manager-free source density bracket:
air at the landing and solid below it. A large void ring is not evidence of island footing. air at the landing and solid below it. A large void ring is not evidence of island footing.
⚠️ **It is a proxy for connectivity, not a proof.** The proof is a flood fill — Tier 2. ⚠️ **It is a proxy for connectivity, not a proof.** The pure query itself is even narrower: it can
prove a local capsule pose but cannot prove a large component without reading generated layout
state. The proof is a flood fill — Tier 2.
### ⚠️ A forked placement envelope, guarded by that test ### ⚠️ A forked placement envelope, guarded by that test
`VF_FindNearestHashRoomLandingPoint` re-derives the room vertical placement envelope that `BuildChunkCache` also `VF_FindNearestHashRoomLandingPoint` re-derives the room vertical placement envelope that `BuildChunkCache` also
@@ -1603,9 +1617,57 @@ is the only thing standing between a placement change and silent bedrock passage
--- ---
## 12. Tier 1 FINAL — and the 92.8% defect a green suite hid ## 12. Tier 1 checkpoint — and the 92.8% defect a green suite hid
**Built clean, suite green: 17 succeeded, 0 warnings, 0 failed (2026-08-30).** **Historical checkpoint: built clean, suite green: 17 succeeded, 0 warnings, 0 failed
(2026-08-30).** The room-centre correction below remains useful history, but it did not prove a
player-sized landing or a large connected component.
### 12.1 2026-09-06 player-fit landing pass
The next correction keeps the pure/re-entrant boundary intact and replaces the query's open-air
claim with a local player-pose claim. Both the upper and lower mouths query their own source
independently. The query mirrors the measurement stencil (capsule volume, support patch, step
height, and walkable normal) against a stateless archetype source evaluator. It does not run a
flood fill, so it cannot honestly promise that the two mouths share a large fit component.
The final eight-archetype before/after table below records the measured arrival/departure component
sizes and voxel gaps. A remaining gap is a generation-connectivity finding, not a reason to widen
the landing budget.
| Archetype | Before: fit / components / largest / arrival / departure / gap / law | After: fit / components / largest / arrival / departure / gap / law |
|---|---|---|
| CrystalChamber | 655 / 197 / 24 / 1 / 1 / 47.529 / NotConnectedAtThisResolution | 1,360 / 62 / 717 / 270 / 717 / 19.131 / NotConnectedAtThisResolution |
| FlatPlain | 1,116 / 45 / 259 / 1 / 1 / 48.104 / NotConnectedAtThisResolution | 2,136 / 2 / 2,135 / 2,135 / 2,135 / 0.000 / Connected |
| FloatingIslands | 4 / 2 / 3 / 1 / 1 / 0.000 / Connected | 0 / 0 / 0 / 0 / 0 / -1.000 / StartCellNotPlayerFit |
| Maze | 2,801 / 309 / 176 / 43 / 81 / 206.630 / NotConnectedAtThisResolution | 5,759 / 82 / 1,217 / 30 / 1,217 / 121.598 / NotConnectedAtThisResolution |
| SurfaceWorld | 713 / 520 / 6 / 1 / 1 / 110.648 / NotConnectedAtThisResolution | 355 / 223 / 9 / 3 / 1 / 96.073 / NotConnectedAtThisResolution |
| TunnelNetwork | 2,748 / 467 / 1,292 / 2 / 1 / 164.405 / NotConnectedAtThisResolution | 100 / 1 / 100 / 100 / 100 / 0.000 / CoarseLiedBudgetExhausted |
| Underwater | 337 / 86 / 35 / 3 / 11 / 142.499 / NotConnectedAtThisResolution | 342 / 13 / 194 / 95 / 194 / 115.265 / NotConnectedAtThisResolution |
| VerticalShafts | 1,156 / 112 / 96 / 3 / 36 / 94.000 / NotConnectedAtThisResolution | 4,669 / 36 / 619 / 14 / 81 / 104.890 / NotConnectedAtThisResolution |
The restricted law tally is **1/8 connected before and 1/8 after**; no after result is a connected
component of four cells or fewer. The FloatingIslands `StartCellNotPlayerFit` result is the live
world after the mouth carve: the pure source query found a top pose, but the structural passage
carve removes the support at that exact endpoint. That is a generation/carve integration issue,
not evidence that the query should accept open air. TunnelNetwork's single 100-cell component is
the coarse retry budget being exhausted, not a connected-law pass.
The query-answer fixture still intentionally refuses `SurfaceWorld`. In this seed, the sparse room
and slab sources can also decline when no room/core passes the local stencil inside their lateral
budget; that preserves the original placement rather than inventing a point. The pure query has
no component-size oracle, so its honest large-space substitute is a guaranteed feature core:
room centre, slab floor at requested XY, horizontal Maze edge, shaft ledge-side pose, or island
top interior. A generation-side connection/room-mouth guarantee is the smallest alternative if
the measured law must become connected.
The old VerticalShafts refinement assertion that every mouth sit on the exact shaft axis was too
strong for this task: an infinite open cylinder has no support surface at its axis. It is now an
executable **feature-core** invariant (with the drainage-tree path still audited from the resolved
axis), while the landing query proves the actual ledge-side capsule pose. The required UE 5.7 build
passed; the full `VoxelForge` namespace finished with **29 successes, 2 known warnings, 0 failures,
0 not-run** in 2,388.35 s. The warnings remain the gated lateral-region 9/16 law and the known
TunnelNetwork op-stack outside-gate sample count; box violations stayed zero.
### ⚠️ Correct the record: commit `1ab8c0c` overstated what worked ### ⚠️ Correct the record: commit `1ab8c0c` overstated what worked
It reported *"3 checked, 48/48 ring samples air"* and read as Tier 1 working. A million-seed sweep later It reported *"3 checked, 48/48 ring samples air"* and read as Tier 1 working. A million-seed sweep later
@@ -54,10 +54,13 @@ namespace
case ECaveGeneratorType::VerticalShafts: case ECaveGeneratorType::VerticalShafts:
// A 0.6 shaft density does not guarantee an occupied immediate cell. The production // A 0.6 shaft density does not guarantee an occupied immediate cell. The production
// query therefore permits a bounded two-cell local search and still returns the // query therefore permits a bounded two-cell local search and still returns the
// selected site's exact axis. // selected site's deterministic feature core/ledge-side pose.
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f); return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
case ECaveGeneratorType::FloatingIslands: case ECaveGeneratorType::FloatingIslands:
return FMath::Max(Definition.FloatingIslandParams.IslandSpacing, 1.0f); return FMath::Max(Definition.FloatingIslandParams.IslandSpacing, 1.0f);
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return FMath::Max(Definition.GenerationParams.RoomSpacing, 1.0f);
default: default:
return 0.0f; return 0.0f;
} }
@@ -85,6 +88,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
int32 NumInterStratePassages = 0; int32 NumInterStratePassages = 0;
int32 NumChecked = 0; int32 NumChecked = 0;
int32 NumUpperChecked = 0;
int32 NumUpperQueryFalse = 0;
int32 NumQueryFalse = 0; int32 NumQueryFalse = 0;
int32 NumUnsupported = 0; int32 NumUnsupported = 0;
int32 NumSupportedWithoutPoint = 0; int32 NumSupportedWithoutPoint = 0;
@@ -144,6 +149,74 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
continue; continue;
} }
// The upper mouth has its own source query too. This is deliberately evaluated from the
// upper strate's definition, seed, bounds, and requested XY; it must never inherit the
// lower mouth's answer or become an implicit passage chain.
const float SourceTopZ = (float)(Source.TopChunkZ + 1) * CHUNK_SIZE;
const float SourceBottomZ = (float)Source.BottomChunkZ * CHUNK_SIZE;
const int32 SourceQuerySeed = OpenPointSeedFor(
*SourceDefinition, Source, WorldSeed);
const float SourceMaxLateralSnap = MaxLateralSnapFor(*SourceDefinition);
FVector SuggestedUpperPoint = FVector::ZeroVector;
const bool bCanAnswerUpper = VF_SuggestLandingPoint(
SourceDefinition->GeneratorType,
SourceDefinition->GenerationParams,
SourceDefinition->SlabParams,
SourceDefinition->MazeParams,
SourceDefinition->VerticalShaftParams,
SourceDefinition->FloatingIslandParams,
SourceQuerySeed,
SourceTopZ,
SourceBottomZ,
Passage.RequestedUpperPoint.X,
Passage.RequestedUpperPoint.Y,
SourceMaxLateralSnap,
SuggestedUpperPoint);
if (!bCanAnswerUpper)
{
++NumUpperQueryFalse;
FalseArchetypes.Add(static_cast<uint8>(SourceDefinition->GeneratorType));
}
else
{
++NumUpperChecked;
const float UpperSnapDX = Passage.UpperPoint.X
- Passage.RequestedUpperPoint.X;
const float UpperSnapDY = Passage.UpperPoint.Y
- Passage.RequestedUpperPoint.Y;
const float UpperSnapDistance = FMath::Sqrt(
UpperSnapDX * UpperSnapDX + UpperSnapDY * UpperSnapDY);
if (!FMath::IsNearlyEqual(Passage.UpperPoint.X, SuggestedUpperPoint.X, 0.01f)
|| !FMath::IsNearlyEqual(Passage.UpperPoint.Y, SuggestedUpperPoint.Y, 0.01f)
|| !FMath::IsNearlyEqual(Passage.UpperPoint.Z, SuggestedUpperPoint.Z, 0.01f))
{
bAllAnswerableEndpointsMatchQuery = false;
AddError(FString::Printf(
TEXT("Passage from strate %d (%s) missed its upper player-fit landing point: endpoint (%.9g, %.9g, %.9g), query (%.9g, %.9g, %.9g)."),
Source.StrateIndex,
*ArchetypeName(SourceDefinition->GeneratorType),
Passage.UpperPoint.X,
Passage.UpperPoint.Y,
Passage.UpperPoint.Z,
SuggestedUpperPoint.X,
SuggestedUpperPoint.Y,
SuggestedUpperPoint.Z));
}
if (!FMath::IsFinite(UpperSnapDistance)
|| UpperSnapDistance > SourceMaxLateralSnap + 0.01f)
{
bAllAnswerableEndpointsMatchQuery = false;
AddError(FString::Printf(
TEXT("Passage from strate %d (%s) exceeded its upper lateral snap bound: %.6f > %.6f from requested (%.3f, %.3f)."),
Source.StrateIndex,
*ArchetypeName(SourceDefinition->GeneratorType),
UpperSnapDistance,
SourceMaxLateralSnap,
Passage.RequestedUpperPoint.X,
Passage.RequestedUpperPoint.Y));
}
}
const FStratePassageConfig& PassageConfig = SourceDefinition->PassageConfig; const FStratePassageConfig& PassageConfig = SourceDefinition->PassageConfig;
if (!FMath::IsFinite(PassageConfig.MouthRadius) || PassageConfig.MouthRadius <= 0.0f) if (!FMath::IsFinite(PassageConfig.MouthRadius) || PassageConfig.MouthRadius <= 0.0f)
{ {
@@ -377,8 +450,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
} }
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Passage open-space check: %d inter-strate passages, %d checked, %d footing checks, %d/%d room/slab ring samples air, %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."), TEXT("Passage player-fit check: %d inter-strate passages, upper %d checked (%d false), lower %d checked (%d footing checks), %d/%d room/slab ring samples air, lower %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."),
NumInterStratePassages, NumInterStratePassages,
NumUpperChecked,
NumUpperQueryFalse,
NumChecked, NumChecked,
NumFootingChecked, NumFootingChecked,
NumRingAirSamples, NumRingAirSamples,
@@ -398,6 +473,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{ {
AddError(TEXT("VACUOUS: zero answerable passage mouths were checked; the passage invariant was not exercised.")); AddError(TEXT("VACUOUS: zero answerable passage mouths were checked; the passage invariant was not exercised."));
} }
if (NumUpperChecked == 0)
{
AddError(TEXT("VACUOUS: zero answerable upper passage mouths were checked; independent upper-mouth aiming was not exercised."));
}
if (NumFootingChecked == 0) if (NumFootingChecked == 0)
{ {
AddError(TEXT("VACUOUS: no Maze/VerticalShafts/FloatingIslands footing was checked.")); AddError(TEXT("VACUOUS: no Maze/VerticalShafts/FloatingIslands footing was checked."));
@@ -461,6 +540,7 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
return NumInterStratePassages > 0 return NumInterStratePassages > 0
&& NumChecked > 0 && NumChecked > 0
&& NumUpperChecked > 0
&& NumFootingChecked > 0 && NumFootingChecked > 0
&& bAllNewFootingArchetypesAnswered && bAllNewFootingArchetypesAnswered
// (fixture coverage of the room/slab archetypes is reported, not asserted — see above) // (fixture coverage of the room/slab archetypes is reported, not asserted — see above)
@@ -9,6 +9,7 @@
#include "VoxelForgeTestFixture.h" #include "VoxelForgeTestFixture.h"
#include "VoxelCaveMorphology.h" #include "VoxelCaveMorphology.h"
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelTypes.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST( IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeStrateConnectivityTest, FVoxelForgeStrateConnectivityTest,
@@ -327,6 +328,7 @@ namespace
{ {
float X = 0.0f; float X = 0.0f;
float Y = 0.0f; float Y = 0.0f;
float Radius = 0.0f;
int32 CellX = 0; int32 CellX = 0;
int32 CellY = 0; int32 CellY = 0;
}; };
@@ -350,6 +352,10 @@ namespace
+ VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x12345678u)) * 0.7f) * Spacing; + VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x12345678u)) * 0.7f) * Spacing;
OutShaft.Y = (CellY + 0.15f OutShaft.Y = (CellY + 0.15f
+ VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x9ABCDEF0u)) * 0.7f) * Spacing; + VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x9ABCDEF0u)) * 0.7f) * Spacing;
OutShaft.Radius = FMath::Lerp(
Params.ShaftMinRadius,
Params.ShaftMaxRadius,
VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0xBEEFu)));
OutShaft.CellX = CellX; OutShaft.CellX = CellX;
OutShaft.CellY = CellY; OutShaft.CellY = CellY;
return true; return true;
@@ -788,14 +794,27 @@ namespace
return Out; return Out;
} }
bool IsVerticalShaftAxis( bool FindVerticalShaftFeatureCore(
const FVerticalShaftParams& Params, const FVerticalShaftParams& Params,
int32 Seed, int32 Seed,
const FVector& Point) const FVector& Point,
FVector& OutAxisPoint)
{ {
const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f);
const float InteriorMargin = FMath::Max(Params.SurfaceRoughness, 0.0f)
* VOXEL_NOISE_SCALE + 0.25f;
if (!FMath::IsFinite(InteriorMargin) || InteriorMargin < 0.0f)
{
return false;
}
const int32 BaseCellX = FMath::FloorToInt(Point.X / Spacing); const int32 BaseCellX = FMath::FloorToInt(Point.X / Spacing);
const int32 BaseCellY = FMath::FloorToInt(Point.Y / Spacing); const int32 BaseCellY = FMath::FloorToInt(Point.Y / Spacing);
float BestDistanceSq = FLT_MAX;
int32 BestCellX = INT32_MAX;
int32 BestCellY = INT32_MAX;
FAuditShaft BestShaft;
bool bFound = false;
for (int32 DY = -1; DY <= 1; ++DY) for (int32 DY = -1; DY <= 1; ++DY)
{ {
for (int32 DX = -1; DX <= 1; ++DX) for (int32 DX = -1; DX <= 1; ++DX)
@@ -806,14 +825,50 @@ namespace
{ {
continue; continue;
} }
if (FMath::IsNearlyEqual(Point.X, Shaft.X, 0.001f) const float SafeRadius = Shaft.Radius - InteriorMargin;
&& FMath::IsNearlyEqual(Point.Y, Shaft.Y, 0.001f)) if (!FMath::IsFinite(SafeRadius) || SafeRadius <= 0.0f)
{ {
continue;
}
const float DistanceSq = FMath::Square(Point.X - Shaft.X)
+ FMath::Square(Point.Y - Shaft.Y);
if (DistanceSq > FMath::Square(SafeRadius) + 1.0e-4f)
{
continue;
}
const bool bCloser = DistanceSq < BestDistanceSq;
const bool bTie = DistanceSq == BestDistanceSq
&& (Shaft.CellX < BestCellX
|| (Shaft.CellX == BestCellX && Shaft.CellY < BestCellY));
if (bCloser || bTie)
{
BestDistanceSq = DistanceSq;
BestCellX = Shaft.CellX;
BestCellY = Shaft.CellY;
BestShaft = Shaft;
bFound = true;
}
}
}
if (!bFound)
{
return false;
}
OutAxisPoint = FVector(BestShaft.X, BestShaft.Y, Point.Z);
return true; return true;
} }
}
} bool IsVerticalShaftFeatureCore(
return false; const FVerticalShaftParams& Params,
int32 Seed,
const FVector& Point)
{
FVector AxisPoint = FVector::ZeroVector;
return FindVerticalShaftFeatureCore(Params, Seed, Point, AxisPoint);
} }
bool FindChainMouths( bool FindChainMouths(
@@ -3344,18 +3399,20 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
const FVerticalShaftParams& ShaftParams = const FVerticalShaftParams& ShaftParams =
Layout[VerticalShaftsIndex].Definition->VerticalShaftParams; Layout[VerticalShaftsIndex].Definition->VerticalShaftParams;
if (!IsVerticalShaftAxis( const bool bArrivalFeatureCore = IsVerticalShaftFeatureCore(
ShaftParams, Seed, ResolutionArrivalPoint) ShaftParams, Seed, ResolutionArrivalPoint);
|| !IsVerticalShaftAxis(ShaftParams, Seed, ResolutionDeparturePoint)) const bool bDepartureFeatureCore = IsVerticalShaftFeatureCore(
ShaftParams, Seed, ResolutionDeparturePoint);
if (!bArrivalFeatureCore || !bDepartureFeatureCore)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("HARD FAILURE: fitted-window seed %d has a VerticalShafts mouth not on a " TEXT("HARD FAILURE: fitted-window seed %d has a VerticalShafts mouth outside "
"shaft axis (arrival axis=%s, departure axis=%s; arrival=(%.3f,%.3f,%.3f); " "the shaft feature core (arrival core=%s, departure core=%s; arrival=(%.3f,%.3f,%.3f); "
"departure=(%.3f,%.3f,%.3f); spacing=%.3f; density=%.3f)."), "departure=(%.3f,%.3f,%.3f); spacing=%.3f; density=%.3f)."),
Seed, Seed,
IsVerticalShaftAxis(ShaftParams, Seed, ResolutionArrivalPoint) bArrivalFeatureCore
? TEXT("yes") : TEXT("no"), ? TEXT("yes") : TEXT("no"),
IsVerticalShaftAxis(ShaftParams, Seed, ResolutionDeparturePoint) bDepartureFeatureCore
? TEXT("yes") : TEXT("no"), ? TEXT("yes") : TEXT("no"),
ResolutionArrivalPoint.X, ResolutionArrivalPoint.X,
ResolutionArrivalPoint.Y, ResolutionArrivalPoint.Y,
@@ -3515,7 +3572,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
"mean-radius shaft volume.\n"); "mean-radius shaft volume.\n");
Summary += TEXT( Summary += TEXT(
" seed | fitted verdict | cells | components | small non-largest/total | strict minima/shafts " " seed | fitted verdict | cells | components | small non-largest/total | strict minima/shafts "
"| neighbour fallback | path orphans | axis mouths | doubled fitted-margin verdict/cells | " "| neighbour fallback | path orphans | shaft feature-core mouths | doubled fitted-margin verdict/cells | "
"tree path XY bounds\n"); "tree path XY bounds\n");
static constexpr int32 FittedSeedCases[] = { static constexpr int32 FittedSeedCases[] = {
@@ -3577,8 +3634,12 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
const FVerticalShaftParams RuntimeShaftParams = const FVerticalShaftParams RuntimeShaftParams =
SeedWorld.StrateManager->GetVerticalShaftParamsForChunk( SeedWorld.StrateManager->GetVerticalShaftParamsForChunk(
FIntVector(0, 0, ShaftMidChunkZ)); FIntVector(0, 0, ShaftMidChunkZ));
const bool bAxisMouths = IsVerticalShaftAxis(ShaftParams, Seed, SeedArrivalPoint) FVector ArrivalShaftAxis = SeedArrivalPoint;
&& IsVerticalShaftAxis(ShaftParams, Seed, SeedDeparturePoint); FVector DepartureShaftAxis = SeedDeparturePoint;
const bool bShaftFeatureMouths = FindVerticalShaftFeatureCore(
ShaftParams, Seed, SeedArrivalPoint, ArrivalShaftAxis)
&& FindVerticalShaftFeatureCore(
ShaftParams, Seed, SeedDeparturePoint, DepartureShaftAxis);
const FVoxelStrateMeasureSettings FittedSettings = MakeFittedSettings( const FVoxelStrateMeasureSettings FittedSettings = MakeFittedSettings(
2, 2,
SeedArrivalPoint, SeedArrivalPoint,
@@ -3649,7 +3710,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
&& Diagnostics.Result == EVoxelConnectivityResult::Connected && Diagnostics.Result == EVoxelConnectivityResult::Connected
&& !Diagnostics.bStartSnapped && !Diagnostics.bStartSnapped
&& !Diagnostics.bGoalSnapped && !Diagnostics.bGoalSnapped
&& bAxisMouths; && bShaftFeatureMouths;
bool bCountsAsPass = bCountsAsDirectPass; bool bCountsAsPass = bCountsAsDirectPass;
FString DoubledSweepText = TEXT("not-run"); FString DoubledSweepText = TEXT("not-run");
FString TreePathText = TEXT("not-needed"); FString TreePathText = TEXT("not-needed");
@@ -3682,14 +3743,14 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
FVector2D PathMax = FVector2D::ZeroVector; FVector2D PathMax = FVector2D::ZeroVector;
int32 PathHops = 0; int32 PathHops = 0;
const bool bArrivalPath = AuditVerticalShaftPathBounds( const bool bArrivalPath = AuditVerticalShaftPathBounds(
ShaftParams, Seed, SeedArrivalPoint, PathMin, PathMax, PathHops); ShaftParams, Seed, ArrivalShaftAxis, PathMin, PathMax, PathHops);
FVector2D DeparturePathMin = FVector2D::ZeroVector; FVector2D DeparturePathMin = FVector2D::ZeroVector;
FVector2D DeparturePathMax = FVector2D::ZeroVector; FVector2D DeparturePathMax = FVector2D::ZeroVector;
int32 DeparturePathHops = 0; int32 DeparturePathHops = 0;
const bool bDeparturePath = AuditVerticalShaftPathBounds( const bool bDeparturePath = AuditVerticalShaftPathBounds(
ShaftParams, ShaftParams,
Seed, Seed,
SeedDeparturePoint, DepartureShaftAxis,
DeparturePathMin, DeparturePathMin,
DeparturePathMax, DeparturePathMax,
DeparturePathHops); DeparturePathHops);
@@ -3778,7 +3839,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
Audit.NumShafts, Audit.NumShafts,
Audit.NumNeighbourFallbacks, Audit.NumNeighbourFallbacks,
Audit.NumUnreachable, Audit.NumUnreachable,
bAxisMouths ? TEXT("YES") : TEXT("NO"), bShaftFeatureMouths ? TEXT("YES") : TEXT("NO"),
*DoubledSweepText, *DoubledSweepText,
*TreePathText); *TreePathText);
@@ -3786,10 +3847,10 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
{ {
const FVerticalShaftPhysicalAudit ArrivalPhysical = const FVerticalShaftPhysicalAudit ArrivalPhysical =
AuditVerticalShaftPathDensity( AuditVerticalShaftPathDensity(
*SeedWorld.Generator, RuntimeShaftParams, Seed, SeedArrivalPoint); *SeedWorld.Generator, RuntimeShaftParams, Seed, ArrivalShaftAxis);
const FVerticalShaftPhysicalAudit DeparturePhysical = const FVerticalShaftPhysicalAudit DeparturePhysical =
AuditVerticalShaftPathDensity( AuditVerticalShaftPathDensity(
*SeedWorld.Generator, RuntimeShaftParams, Seed, SeedDeparturePoint); *SeedWorld.Generator, RuntimeShaftParams, Seed, DepartureShaftAxis);
const FVerticalShaftPhysicalAudit& FirstBadPhysical = const FVerticalShaftPhysicalAudit& FirstBadPhysical =
ArrivalPhysical.NumNonAirSamples > 0 ? ArrivalPhysical : DeparturePhysical; ArrivalPhysical.NumNonAirSamples > 0 ? ArrivalPhysical : DeparturePhysical;
Summary += FString::Printf( Summary += FString::Printf(
@@ -3827,15 +3888,15 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
FirstBadPhysical.FirstBadFraction); FirstBadPhysical.FirstBadFraction);
} }
if (Audit.NumUnreachable != 0 || !Audit.bOrderIndependent || !bAxisMouths) if (Audit.NumUnreachable != 0 || !Audit.bOrderIndependent || !bShaftFeatureMouths)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("HARD FAILURE: fitted audit seed %d violated the shaft-tree/landing " TEXT("HARD FAILURE: fitted audit seed %d violated the shaft-tree/landing "
"invariant (unreachable=%d, order-independent=%s, axis-mouths=%s)."), "invariant (unreachable=%d, order-independent=%s, feature-core-mouths=%s)."),
Seed, Seed,
Audit.NumUnreachable, Audit.NumUnreachable,
Audit.bOrderIndependent ? TEXT("yes") : TEXT("no"), Audit.bOrderIndependent ? TEXT("yes") : TEXT("no"),
bAxisMouths ? TEXT("yes") : TEXT("no"))); bShaftFeatureMouths ? TEXT("yes") : TEXT("no")));
bAllChecksPassed = false; bAllChecksPassed = false;
} }
} }
File diff suppressed because it is too large. Load diff
@@ -603,8 +603,8 @@ void UVoxelStrateManager::GeneratePassages()
constexpr uint32 PassageSaltBendFreq = 0xA110000Au; constexpr uint32 PassageSaltBendFreq = 0xA110000Au;
int32 TotalPassages = 0; int32 TotalPassages = 0;
int32 NumAimedAtUpperOpenPoint = 0; int32 NumAimedAtUpperPlayerFit = 0;
int32 NumAimedAtLowerOpenPoint = 0; int32 NumAimedAtLowerPlayerFit = 0;
TSet<FString> NoQueryArchetypes; TSet<FString> NoQueryArchetypes;
const auto ArchetypeName = [](ECaveGeneratorType Archetype) const auto ArchetypeName = [](ECaveGeneratorType Archetype)
@@ -629,8 +629,9 @@ void UVoxelStrateManager::GeneratePassages()
case ECaveGeneratorType::VerticalShafts: case ECaveGeneratorType::VerticalShafts:
// A density of 0.6 does not guarantee an occupied cell in the immediate grid // A density of 0.6 does not guarantee an occupied cell in the immediate grid
// neighbourhood. Allow the nearest occupied shaft to be two grid cells away while // neighbourhood. Allow the nearest occupied shaft to be two grid cells away while
// remaining a bounded local query; the landing query still returns that shaft's // remaining a bounded local query; the landing query still returns a pose inside
// exact axis, so this cannot create a non-topological mouth. // that shaft's deterministic feature core, so this cannot create a non-topological
// mouth.
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f); return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
case ECaveGeneratorType::FloatingIslands: case ECaveGeneratorType::FloatingIslands:
@@ -749,20 +750,18 @@ void UVoxelStrateManager::GeneratePassages()
// their source. The two queries are deliberately independent: a passage never reads // their source. The two queries are deliberately independent: a passage never reads
// another passage's endpoint or any live layout state. // another passage's endpoint or any live layout state.
FVector SuggestedUpperPoint = FVector::ZeroVector; FVector SuggestedUpperPoint = FVector::ZeroVector;
bool bAimedUpperAtOpenPoint = false; bool bAimedUpperAtPlayerFitPoint = false;
if (UpperDef) if (UpperDef)
{ {
const bool bUsesRoomSeed = const bool bUsesRoomSeed =
UpperDef->GeneratorType == ECaveGeneratorType::TunnelNetwork UpperDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|| UpperDef->GeneratorType == ECaveGeneratorType::Underwater; || UpperDef->GeneratorType == ECaveGeneratorType::Underwater;
const int32 UpperQuerySeed = SeasonStrates.IsValidIndex(i) const int32 UpperQuerySeed = bUsesRoomSeed
? SeasonStrates[i].Seed
: (bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed( ? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Upper.StrateIndex)) static_cast<uint32>(CachedSeed), Upper.StrateIndex))
: CachedSeed); : CachedSeed;
bAimedUpperAtOpenPoint = VF_SuggestLandingPoint( bAimedUpperAtPlayerFitPoint = VF_SuggestLandingPoint(
UpperDef->GeneratorType, UpperDef->GeneratorType,
UpperDef->GenerationParams, UpperDef->GenerationParams,
UpperDef->SlabParams, UpperDef->SlabParams,
@@ -777,9 +776,9 @@ void UVoxelStrateManager::GeneratePassages()
MaxLateralSnapFor(*UpperDef), MaxLateralSnapFor(*UpperDef),
SuggestedUpperPoint); SuggestedUpperPoint);
if (bAimedUpperAtOpenPoint) if (bAimedUpperAtPlayerFitPoint)
{ {
++NumAimedAtUpperOpenPoint; ++NumAimedAtUpperPlayerFit;
} }
else else
{ {
@@ -788,22 +787,20 @@ void UVoxelStrateManager::GeneratePassages()
} }
FVector SuggestedLowerPoint = FVector::ZeroVector; FVector SuggestedLowerPoint = FVector::ZeroVector;
bool bAimedLowerAtOpenPoint = false; bool bAimedLowerAtPlayerFitPoint = false;
const UVoxelStrateDefinition* LowerDef = Lower.Definition; const UVoxelStrateDefinition* LowerDef = Lower.Definition;
if (LowerDef) if (LowerDef)
{ {
const bool bUsesRoomSeed = const bool bUsesRoomSeed =
LowerDef->GeneratorType == ECaveGeneratorType::TunnelNetwork LowerDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|| LowerDef->GeneratorType == ECaveGeneratorType::Underwater; || LowerDef->GeneratorType == ECaveGeneratorType::Underwater;
const int32 LowerQuerySeed = SeasonStrates.IsValidIndex(i + 1) const int32 LowerQuerySeed = bUsesRoomSeed
? SeasonStrates[i + 1].Seed
: (bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed( ? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Lower.StrateIndex)) static_cast<uint32>(CachedSeed), Lower.StrateIndex))
: CachedSeed); : CachedSeed;
const float MaxLateralSnap = MaxLateralSnapFor(*LowerDef); const float MaxLateralSnap = MaxLateralSnapFor(*LowerDef);
bAimedLowerAtOpenPoint = VF_SuggestLandingPoint( bAimedLowerAtPlayerFitPoint = VF_SuggestLandingPoint(
LowerDef->GeneratorType, LowerDef->GeneratorType,
LowerDef->GenerationParams, LowerDef->GenerationParams,
LowerDef->SlabParams, LowerDef->SlabParams,
@@ -818,9 +815,9 @@ void UVoxelStrateManager::GeneratePassages()
MaxLateralSnap, MaxLateralSnap,
SuggestedLowerPoint); SuggestedLowerPoint);
if (bAimedLowerAtOpenPoint) if (bAimedLowerAtPlayerFitPoint)
{ {
++NumAimedAtLowerOpenPoint; ++NumAimedAtLowerPlayerFit;
} }
else else
{ {
@@ -842,7 +839,7 @@ void UVoxelStrateManager::GeneratePassages()
Passage.RequestedUpperPoint = FVector(PX, PY, TopZ); Passage.RequestedUpperPoint = FVector(PX, PY, TopZ);
Passage.RequestedLowerPoint = FVector(PX, PY, BottomZ); Passage.RequestedLowerPoint = FVector(PX, PY, BottomZ);
if (bAimedUpperAtOpenPoint) if (bAimedUpperAtPlayerFitPoint)
{ {
const float UpperSealThickness = BoundarySealThicknessFor(*UpperDef); const float UpperSealThickness = BoundarySealThicknessFor(*UpperDef);
const float InnerBottomZ = UpperBottomZ + UpperSealThickness; const float InnerBottomZ = UpperBottomZ + UpperSealThickness;
@@ -868,7 +865,7 @@ void UVoxelStrateManager::GeneratePassages()
} }
} }
if (bAimedLowerAtOpenPoint) if (bAimedLowerAtPlayerFitPoint)
{ {
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef); const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE; const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE;
@@ -1031,12 +1028,12 @@ void UVoxelStrateManager::GeneratePassages()
} }
UE_LOG(LogTemp, Log, UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at generator-confirmed open space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."), TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at player-fit source space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
NumAimedAtUpperOpenPoint, NumAimedAtUpperPlayerFit,
TotalPassages, TotalPassages,
NumAimedAtLowerOpenPoint, NumAimedAtLowerPlayerFit,
TotalPassages, TotalPassages,
(TotalPassages * 2) - NumAimedAtUpperOpenPoint - NumAimedAtLowerOpenPoint, (TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit,
TotalPassages * 2, TotalPassages * 2,
*NoQueryList); *NoQueryList);
+16 -9
View File
@@ -539,22 +539,27 @@ namespace VoxelCaveMorphology
} }
/** /**
* Suggest a deterministic full landing point for an inter-strate passage mouth. * Suggest a deterministic player-fit landing point for an inter-strate passage mouth.
* *
* This is deliberately a pure source query: it does not touch a generator, an operator stack, * This is deliberately a pure source query: it does not touch a generator, an operator stack,
* a strate manager, a cache, or mutable state. DesiredX/DesiredY is the passage's deliberate * a strate manager, a cache, or mutable state. DesiredX/DesiredY is the passage's deliberate
* placement around the (0,0) spine. Sparse sources may move laterally, but never farther than * placement around the (0,0) spine. Sparse sources may move laterally, but never farther than
* MaxLateralSnap; false means no confident landing site exists inside that bound. * MaxLateralSnap; false means no confident player-fit pose exists inside that bound. A successful
* point has a locally supported floor within player step height, a walkable support patch, and a
* clear capsule volume. The local source stencil deliberately does not claim flood-fill
* connectivity or a measured component size; it selects a topology-backed feature core instead.
* For TunnelNetwork/Underwater, Seed must be the strate-specific room seed returned by * For TunnelNetwork/Underwater, Seed must be the strate-specific room seed returned by
* VoxelCaveMorphology::MakeStrateSeed; for slab, maze, shaft, and island archetypes, Seed is the * VoxelCaveMorphology::MakeStrateSeed; for slab, maze, shaft, and island archetypes, Seed is the
* generator world seed consumed by their source. SurfaceWorld deliberately has no answer here: * generator world seed consumed by their source. SurfaceWorld deliberately has no answer here:
* its production height can be selected by manager-owned biome context and per-chunk state, * its production height can be selected by manager-owned biome context and per-chunk state,
* which this re-entrant query cannot inspect. * which this re-entrant query cannot inspect.
* *
* Propose un point d'atterrissage déterministe complet pour la bouche d'un passage inter-strates. * Propose un point d'atterrissage déterministe où la capsule du joueur tient réellement pour la
* Cette requête reste pure et le déplacement latéral est strictement borné par MaxLateralSnap. * bouche d'un passage inter-strates. Cette requête reste pure et le déplacement latéral est
* Pour VerticalShafts, la réponse XY est exactement l'axe d'un puits sélectionné; elle n'est pas * strictement borné par MaxLateralSnap; elle ne prétend pas prouver une composante connectée.
* un point ouvert arbitraire à l'intérieur de son disque. * Pour VerticalShafts, la réponse s'appuie sur l'axe d'un puits sélectionné et peut se déplacer
* vers une banquette déterministe (+X/+Y) pour obtenir un support; ce n'est pas un point ouvert
* arbitraire à l'intérieur de son disque.
* *
* @return true when the archetype can provide a confident point; false when it cannot. * @return true when the archetype can provide a confident point; false when it cannot.
*/ */
@@ -572,11 +577,13 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
); );
/** /**
* Complete source-query overload for archetypes whose placement parameters are not part of * Complete player-fit source-query overload for archetypes whose placement parameters are not part of
* FStrateGenerationParams/FSlabGenerationParams. The extra structs are read-only inputs only; * FStrateGenerationParams/FSlabGenerationParams. The extra structs are read-only inputs only;
* the function remains pure and SurfaceWorld still returns false for the reason above. The * the function remains pure and SurfaceWorld still returns false for the reason above. The
* returned point is either at the requested XY or within MaxLateralSnap of it. VerticalShafts * returned point is either at the requested XY or within MaxLateralSnap of it, and is accepted
* returns the selected shaft axis so the answer belongs to the connected shaft tree. * only after the local player capsule/support stencil passes. VerticalShafts uses a selected
* shaft's deterministic feature core and ledge-side pose; the axis identifies the topology but
* is not itself claimed to be a standable point.
*/ */
VOXELFORGE_API bool VF_SuggestLandingPoint( VOXELFORGE_API bool VF_SuggestLandingPoint(
ECaveGeneratorType Archetype, ECaveGeneratorType Archetype,