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:
1 parent
110ae9d0ed
commit
4378385b37
7 files changed
+1365
-165
No files matched your search
+11
-8
@@ -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
@@ -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)
|
||||||
{
|
{
|
||||||
return true;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
if (!bFound)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutAxisPoint = FVector(BestShaft.X, BestShaft.Y, Point.Z);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsVerticalShaftFeatureCore(
|
||||||
|
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
|
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||||
: (bUsesRoomSeed
|
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
|
||||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
: CachedSeed;
|
||||||
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
|
|
||||||
: 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
|
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||||
: (bUsesRoomSeed
|
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
|
||||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
: CachedSeed;
|
||||||
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
|
|
||||||
: 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user