diff --git a/CODEMAP.md b/CODEMAP.md index 0556b4b..f55b270 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -121,11 +121,15 @@ stack share ONE copy. `VoxelGenerator.cpp` keeps same-named `static FORCEINLINE` ⚠️ **Feeds the game, behind a per-strate opt-in** (Phase 1 step 3). `GetDensityAt` builds the stack in its per-chunk refetch block and evaluates it *instead of* the `switch` only when `UVoxelStrateManager::UsesOperatorStackForChunk` says so — strate ticked `bUseOperatorStack` **and** -archetype in the ported list (**Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, -FloatingIslands** — 6 of 8). Everything else still takes the `switch`, unchanged. -**`ClassifyTile` is NOT wired** — still hand-written guards, not `ClassifyBox`. That is Phase 2. -⛔ Never run both paths in one world, and never compare them for equality: the ~1 ULP residue is -inherent (AUDIT §C10). The acceptance bar is visual (OPSTACK-PLAN §2.6). +archetype in the ported list, which is now **all 8 of 8**: Maze, FlatPlain, CrystalChamber, +SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater. A strate that has not +ticked the box still takes the `switch`, unchanged — the flag is the only thing that switches paths. +**`ClassifyTile` is NOT wired** — still hand-written guards, not `ClassifyBox`. That is the next +piece, and it is where the measured tile-skipping turns into frames. +⛔ Never run both paths in one world. **Comparing them IS legitimate now** — the ~1 ULP residue of +AUDIT §C10 is gone since `FPSemantics = Precise`, and all eight equivalence tests compare bit for +bit. They are port-correctness oracles, not fidelity checks: the acceptance bar is §2.6.1 (same seed +⇒ same world on every peer), which does not require resembling the pre-refactor world. | Symbol | Role | Notes | |--------|------|-------| @@ -145,7 +149,14 @@ inherent (AUDIT §C10). The acceptance bar is visual (OPSTACK-PLAN §2.6). | `VoxelDensityOps::BuildVerticalShaftStack` | — | 8 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. | | `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion** that the original lacks — see the suspected staleness note in AUDIT §C2. `EffectOverBox` → `Both` for now (a real answer means building the cache for the queried box; only pays once `ClassifyTile` consumes `ClassifyBox`). | | `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). `EffectOverBox` → **`CarveOnly` everywhere** — no spatial bound, so it kills `AllSolid` on every tile of every strate with worms on. `MaxCarveAmplitude()` holds the bound from DECOMPOSITION §0.2 that would recover it, waiting for a fold that carries numbers. | -| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **STAGE A of three — incomplete on purpose.** 6 ops: rock → room graph → carve → worms → structural ×3. The 13 detail modifiers (stage B) and the per-room op override (stage C) are missing, which is why TunnelNetwork is still **off** in `UsesOperatorStackForChunk`. Testable now because every detail modifier is amplitude-gated and defaults to 0. | +| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 19 ops** — the biggest port in the plugin (~1080 lines), done in three stages: SDF spine (A) → the twelve detail modifiers of 4b–4h (B) → the per-room op override (C). Serves **TunnelNetwork and Underwater** from one builder. Operator order is the original's, line for line, and it is load-bearing (`FFloorBiasMod` exists to undo what `FCaveRoughnessMod` did to floors). | +| `FCaveRoughnessMod` (internal) | 3 | STEP 4b, **density space** — a different op from `MakeSdfRoughnessMod`: two octave sets, optional domain warp, four noise types, an anti-fill clamp inside definite air, quadratic fade. ⚠️ **Reads STRATE params, not the per-room copy** — the original's shadow is declared *after* step 4b. Eleven of twelve modifiers read the room copy; this one does not. | +| `FCaveTerraceMod` (internal) | 3 | STEP 4c. The only modifier that **re-queries the SDF** (Z±1, through `FRoomGraphSource::ProbeSdfUnwarped`) for its horizontality gate — which is why the room source's cache is exposed at all. ⚠️ Those probes use unwarped X/Y and raw Z although the field was evaluated warped: transcribed as-is, see OPSTACK-PROGRESS. | +| `FLayerLineMod` / `FRibbingMod` (internal) | 3 | The same sine along Z: cubed and subtracted (grooves) vs quarter-phase-shifted, squared and added (ribs). `CarveOnly` / `FillOnly` — two of the few detail modifiers that keep a usable direction for the fold. | +| `FCaveOverhangMod` / `FCaveCliffMod` / `FScallopMod` (internal) | 3 | STEP 4c. ⚠️ The cliff's own comment promises a sampled vertical gradient; the **code** uses a Z-stretched Perlin as a proxy and samples nothing. Ported as written — fixing it would change the world. | +| `FCaveArchMod` / `FDomeMod` / `FPinchMod` / `FFloorBiasMod` (internal) | 3 | Room-relative: they read `FRoomGraphSource::GetNearestRoomIdx()` and the cached room. Their gate is `SDF < SDFBlendRadius`, **not** the shared `·3` one — they live in the cave's void, not its wall. | +| `FRoomColumnMod` (internal) | 3 | STEP 4d, and **not** `MakeGridColumnMod`: it walks `SDFCache.Columns`, pre-baked per room. ⚠️ It has **no strate parameter at all** — neither the bake nor the loop reads `FStrateGenerationParams::ColumnDensity`. Columns exist only through a `Column` terrain-op asset in the strate's pool, and the only way to prove they fired is to look at `SDFCache.Columns.Num()`. | +| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. ⚠️ `EffectOverBox` still answers from STRATE params, so a box verdict can be **too optimistic** on a strate with a terrain-op pool; harmless until `ClassifyTile` consumes `ClassifyBox`, and it must be fixed before that. | | `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. `EffectOverBox` → `FillOnly` when a blob reaches the box, `Identity` otherwise; its pad must cover warp·**√2** (two independent noise axes), roughness, fill blend and the `SmoothMin` dip. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. | | `VoxelDensityOps::BuildFloatingIslandStack` | — | 7 ops, and **the stack runs backwards**: void source + fill instead of rock source + carve, using the *same* classes with the opposite sign. Only the blob source is new. Reuse by **inversion** — a stronger result than reuse by identity, since it says the abstract axis (the density sign) is the right one. | | `VoxelDensityOps::BuildMazeStack` | — | The 7-op Maze stack. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. | @@ -327,7 +338,7 @@ Maps depth→strate at runtime; owns passages. | `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize`). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so editor rebuilds never serve stale data. | | `GetStrateForChunk` | 466 | Chunk → definition. | | `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. | -| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands — 6 of 8; missing: TunnelNetwork, Underwater). **That list is written down here and nowhere else** — an unported archetype ignores the flag, so ticking the box anywhere is harmless. Add a row here when you port one. | +| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list — **now all 8 of 8** (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater). **That list is written down here and nowhere else.** With every archetype ported the flag is now the *only* thing that decides the path, so ticking the box is no longer a no-op anywhere — it is a real switch onto the operator stack for that strate. | | `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). | | `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. | | `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). | diff --git a/OPSTACK-PLAN.md b/OPSTACK-PLAN.md index 38d035a..6422f03 100644 --- a/OPSTACK-PLAN.md +++ b/OPSTACK-PLAN.md @@ -4,11 +4,18 @@ > *composable density pipeline*, so new world ideas become authoring instead of C++. Written > 2026-07-26 as a handoff for a future context — read this instead of re-deriving it. > -> **Status (2026-07-27):** **Phases 0.5 and 1 DONE and verified. Phase 2 is 5 of 8 archetypes in**, -> all bit-identical to their originals and all wired behind `bUseOperatorStack`: -> **Maze · FlatPlain · CrystalChamber · SurfaceWorld (biomes included) · VerticalShafts.** -> Remaining: `FloatingIslands`, `TunnelNetwork` (**last** — it owns the §8.4 window-invariance -> discipline), `Underwater` (TunnelNetwork + a flag). +> **Status (2026-07-28):** **Phases 0.5, 1 and 2 CODE-COMPLETE — 8 of 8 archetypes ported**, each +> bit-identical to its original in an equivalence test, all wired behind `bUseOperatorStack`: +> **Maze · FlatPlain · CrystalChamber · SurfaceWorld (biomes included) · VerticalShafts · +> FloatingIslands · TunnelNetwork · Underwater.** The archetype `switch` now has a complete +> operator-stack twin, opt-in per strate. +> +> ⚠️ **The last three groups (stage B, C1, C2, C3) are UNVERIFIED — written, committed, never +> compiled.** Run the `VoxelForge` filter; `OPSTACK-PROGRESS.md`'s last entry lists every commit and +> what breaks first if one is wrong. +> +> **Not done, and it is the next real prize:** `ClassifyTile` still uses hand-written guards and does +> not consume `ClassifyBox`. That is where measured tile-skipping becomes frames. > > Two things came out of Phase 2 that were not in the original design: **height space** > (`VoxelHeightOp.h`, a second operator family — some things are not another channel but another @@ -406,12 +413,27 @@ care: the T1.a column cache and the exact-lattice `ClassifyTile` bound both surv ✅ `VerticalShafts` (**done**, 3 ops reused from Maze unchanged) → ✅ `FloatingIslands` (**done**, `BuildFloatingIslandStack` — the stack that runs **backwards**: void source + fill instead of rock source + carve, the *same* classes with the opposite sign; only the -blob source is new) → `Underwater` (TunnelNetwork + a flag) → `TunnelNetwork` -(**last** — it owns `BuildChunkCache`'s two-region window-invariance discipline, §8.4, the most delicate -code in the plugin). +blob source is new) → ✅ `TunnelNetwork` + `Underwater` (**done**, one builder for both — +`BuildTunnelNetworkStack`, 19 ops). -**6 of 8 ported.** The two that remain are really one: `Underwater` *is* TunnelNetwork plus -`WaterLevelRelative` (§8), so the switch loses its last two cases in a single port. +**8 of 8 ported.** The last two were really one: `Underwater` *is* TunnelNetwork plus +`WaterLevelRelative` (§8, re-verified before relying on it), so the switch lost its last two cases in +a single port. + +TunnelNetwork was ~1080 lines and was taken in **three stages, each verifiable on its own** rather +than as ~600 unverified lines on top of ~200 (the `AUDIT §P3` pattern): +* **A** — SDF spine: vertical scale, base rock, cave warp, room graph (+ pits + chimneys), carve, + worms, structural post. Verifiable *while incomplete* because every detail modifier is + amplitude-gated and defaults to zero, so zeroing them sends the ORIGINAL down exactly stage A's path. +* **B** — the twelve detail modifiers of `STEP 4b–4h`, one group per commit, each with a coverage + probe that proves the group actually moved something (`B1` roughness, `B2` terrace/lines/ribs, + `B3` overhang/cliff/scallop/arch, `B4` columns/domes/pinch/floor-bias, `B5` the gate itself). +* **C** — the per-room op override (`§2`'s option (a), and it needed no scoping predicate: one op + owns the state, eleven read it), `Underwater`, and the flag flip. + +⚠️ **`FRoomGraphSource` CALLS `BuildChunkCache`/`EvaluateSDFCached`; it does not transcribe them.** +That is where §8.4's two-region window-invariance discipline lives, and a copy would fork it — with +the fork "validated" by a test that compares it to the original. Along the way, `FStrateGenerationParams`' 74 fields decompose into per-op structs, which retires the `VF_STRATE_PARAM_FIELDS` X-macro drift problem for free. diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 1490995..e1cd16c 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -2139,3 +2139,147 @@ amplitudes back ON one group at a time (a `Terrace` op in the room pool will be break stage A's equivalence, by design). --- + +## 2026-07-28 — **8 OF 8.** TunnelNetwork + Underwater ported. Unattended run, 8 commits, zero builds. + +Jahni started this session and left. Rule #1 held: nothing was compiled, nothing was launched, +nothing was pushed. **Everything below is written and committed but UNVERIFIED.** He reverts by sha, +which is why the one-commit-per-group rule was the one thing not bent. + +``` +6e29cbf B1 surface roughness (4b) + VoxelNoise::Cellular3D shared out of VoxelGenerator.cpp +ab1a996 B2 Terrace · LayerLines · Ribbing (4c) + FRoomGraphSource::FState hoisted out of Eval +6ec6009 B3 cave Overhang · Cliff · Scallop · Arch (4c) +b063d43 B4 Columns (4d) · Domes (4g) · Pinch (4h) · FloorBias + Column op in the test pool +8a303cc B5 the gate itself — test only, proves a voxel outside it is bit-identically untouched +9591088 C1 per-room op override — eleven detail ops read FRoomGraphSource::LocalParams() +e479fcd C2 Underwater wired to the same builder + its own equivalence check +(this) C3 UsesOperatorStackForChunk returns true for both — 8 of 8 — + CODEMAP / PLAN / PROGRESS +``` + +**Test filter: `VoxelForge`. Still 13 tests** — no test file was added; the TunnelNetwork test grew +five new checks. `VoxelForge.OpStack.TunnelNetworkSpineEquivalence` is the one to read. + +### What the archetype switch looks like now + +`ConstantRock → RoomGraph(warp + pits + chimneys) → SdfCarve → Roughness → Terrace → LayerLines → +Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn → Dome → Pinch → FloorBias → Worms → +[spine → seal → passage]` — **19 ops, one builder, two archetypes.** Every one of the eight +archetypes now has an operator-stack twin, per-strate opt-in, each equivalence-tested bit for bit +against its original function. + +⚠️ `UsesOperatorStackForChunk` returning true for TunnelNetwork/Underwater **changes nothing by +itself** — it still requires `bUseOperatorStack` ticked on a strate asset, which is Jahni's call and +was deliberately not done. What *has* changed: the flag is no longer a no-op anywhere. Ticking it on +any strate now really switches that strate onto the operator stack. + +### Five things found by reading the original, ported as-is, and worth knowing + +Each of these looked like a bug or an oversight. None was fixed, because with no test feedback a +"cleanup" is an unfalsifiable guess — and three of them are load-bearing for the look of the world. + +1. **There are TWELVE detail modifiers, not thirteen** — and only **ELEVEN** read the per-room param + copy. The `const FStrateGenerationParams& Params = LocalTerrainParams;` shadow is declared *inside* + the `if (bNearCaveSurface)` block, which begins **after** STEP 4b, so **surface roughness reads + strate params**. Both numbers were carried wrong in the handoff and both matter for C1. +2. **The cliff modifier's comment describes code that does not exist.** It promises "sample density at + Z±1 and compute the vertical gradient"; the code samples nothing and uses a Perlin with 3× Z + frequency as a proxy it calls `VertGrad`. Multiplying by `CaveSDF` still gives it the right sign + either side of the surface, which is why it produces steeper faces at all. +3. **Terrace's two SDF gradient probes query unwarped X/Y and raw Z**, while the field they probe was + evaluated at warped coordinates and effective Z — and they exclude pits and chimneys. The probe + does not sample the field whose slope it measures. +4. **`ColumnDensity` on `FStrateGenerationParams` is read by nothing.** Neither the bake (which reads + a fresh `OpParams` with only the room's op applied) nor the per-voxel loop. Columns exist *only* + through a `Column` terrain-op asset in the strate's pool. Same shape as the `PitDensity` finding, + and the reason B4's coverage had to be an assertion about the bake rather than a params probe. +5. **The per-voxel ~74-field param copy** of the override is transcribed as-is. Real perf item. + +### Two decisions this run made that are not reversible by taste + +**B5 — the gate is a repeated early-out per operator, not a scoping container.** The stack is a flat +list that `ClassifyBox` folds op by op; a container would have to re-implement `VF_FoldOp` and would +hide its children from the fold, and an op that only exists inside a container cannot become a Phase-3 +asset. The cost is stated rather than hidden: the original tests once and skips twelve, the stack +tests twelve times. Predictable branches, but measure before optimising — that is the §C10 lesson. + +**C1 — one op owns the shared state, eleven read it.** `DECOMPOSITION §2` called the per-room override +"the piece with no clean home" and proposed a per-modifier scoping predicate. The difficulty came +entirely from assuming each modifier must *own* its params. `FRoomGraphSource::LocalParams()` publishes +the strate params with the nearest room's op applied, memoised once per voxel (invalidated at the top +of every `Eval`, before any early-out), and the modifiers read it. **No scoping predicate was +invented** — this is the third use of a pattern already in the file (`FOverhangShelfMod` ← +`FSurfaceColumnSource`, `FShaftLedgeMod` ← `FShaftFieldSource`). Same shape as the pit/chimney +resolution: the problem was in the framing, not the code. + +### ⚠️ One thing C1 broke that nothing consumes yet — fix before `ClassifyTile` does + +`EffectOverBox` still answers from **strate** params, because a box spans many rooms and a per-voxel +copy has no meaning there. But `ApplyTo` writes the op's value even where the strate's was `0`, so a +room op can **enable a modifier the strate had switched off**. On a strate with a terrain-op pool, a +box verdict can therefore be too optimistic. Harmless today — nothing consumes `ClassifyBox` in +production — and **it must be fixed before `ClassifyTile` starts to.** Noted at +`FLayerLineMod::EffectOverBox`, which is where a reader would land. + +### The coverage discipline, extended — and why the checks grew faster than the code + +Every group added an operator *and* a way to be told it did nothing. The test now reports: + +- **group coverage** (check 1b): rebuild the stack with one group off, count moved samples. **Zero is + an error, not a warning.** Legitimate as a stack-diff here — unlike for the op pool — because these + are params, and the params CRC *is* in the SDF cache key. +- **the noise switch** (1c): all four roughness noise types × warp on/off. The main equivalence only + ever takes the FBM branch; three of four cases were otherwise untested. +- **the gate** (1d): outside-gate samples must be bit-identical to a modifier-free stack, *and* the + inside-gate move count must be non-zero — otherwise "nothing leaked" only means "nothing happened". +- **the bake** (3b): rooms, pits, chimneys and now **columns**, which is the only sound way to cover an + operator that has no parameter. The stage-A guard that errored on `TotalColumns > 0` is inverted. +- **the override** (3c): how many baked rooms drew a `Terrace` op, and how many samples land inside + one of those rooms. Both non-zero, or check 1's green means only that two paths agree where the + override never applies — which is what stage B already proved. +- **Underwater** (5): 2000 samples in the Underwater *slot*, which is a different strate index, hence + a different bake seed and a different entry in the strate-index memo — something six chunks of slot + 0 cannot exercise. + +That is the pit lesson applied a fourth and fifth time: **enabling a feature is not evidence it +fired, and the check that proves it must be able to fail for exactly one reason.** + +### What breaks first, per group, if a group is wrong + +- **B1** — diffs concentrated in open cave within `|SDF| < SurfaceRoughness·2`. If instead only check + 1c fails, exactly one branch of the noise switch is implicated and nothing else. +- **B2** — diffs near horizontal surfaces (terrace) or in thin Z bands (lines/ribs). If the *whole* + equivalence collapses everywhere, suspect the `FState` hoist, not the three new ops. +- **B3** — a band `|SDF| < range`, or inside open cave for the arch. +- **B4** — dome/pinch near room centres and perimeters; a column error is a ring of diffs at fixed XY + through a whole room height. +- **C1** — diffs confined to the subset of rooms that drew `Terrace`, with terrace-shaped Z banding, + while check 3c still reports non-zero coverage. +- **C2** — if the TunnelNetwork equivalence is green and only this fails, **the operators are fine** + and the finding is a real density difference between Underwater and TunnelNetwork. That would + contradict `DECOMPOSITION §8` and is worth more written down than patched. + +### Likely compile-error spots, in order + +1. `VoxelNoise::Cellular3D` in `VoxelCaveMorphology.h` — it reopens `namespace VoxelNoise` in a header + that does not include `VoxelNoise.h`. Legal, but it is the newest structural change. +2. `#include "VoxelTerrainOpDefinition.h"` newly needed in `VoxelDensityOpStack.cpp` for `ApplyTo` + (added). `FCachedRoom::RoomOp` was only forward-declared before. +3. The five modifier constructors that gained a `const FRoomGraphSource*` parameter — a missed call + site is a clean argument-count error at `BuildTunnelNetworkStack`. +4. `FRoomGraphSource::FState` is declared *after* `PrepareChunk` and used inside `Eval` further down; + in-class member functions see the whole class, so this is fine, but it is the `C4430` shape that + bit the shaft port. +5. The test's `EVoxelTerrainOpType::Terrace` / `Column` and `FTestWorld::SlotUnderwater` — all exist, + all newly referenced from this file. + +### Next single action + +Build, run the `VoxelForge` filter, and read three numbers before anything else: the **group-coverage +percentages** (1b), the **Terrace-room sample count** (3c), and the **inside-gate move count** (1d). +A green run with a zero in any of those is a green run that proved much less than it looks. + +Then, in the order agreed: settle `AUDIT §C2`'s suspected item (answerable by reading +`GetGenerationParams` and `FStrateGenerationParams::Lerp` — no build needed), the worm amplitude cap +(`DECOMPOSITION §0.2`, its own commit, extends `BoxVerdictFold`), and then make `ClassifyTile` consume +`ClassifyBox`. Perf stays parked until Jahni unparks it. diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 59bf322..ecbb7af 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -587,7 +587,23 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord case ECaveGeneratorType::FloatingIslands: // Phase 2 — la pile qui tourne à l'ENVERS : source de VIDE + fill, au lieu de source de ROC - // + carve, avec les MÊMES opérateurs au signe près. 6 des 8 portés. + // + carve, avec les MÊMES opérateurs au signe près. + return true; + + case ECaveGeneratorType::Underwater: + // ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé + // côté rendu. `GetDensityAt` les met dans le même `case`, et `WaterLevelRelative` n'est lu + // que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé). + case ECaveGeneratorType::TunnelNetwork: + // Phase 2, LE DERNIER, et le plus gros : ~1080 lignes portées en trois étapes (squelette + // SDF → douze modificateurs de détail → override d'op par salle), 19 opérateurs, dont + // `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached` au lieu de les + // transcrire — c'est là que vit la discipline d'invariance de fenêtre d'ARCHITECTURE §8.4, + // et en forker une copie aurait été le pire résultat possible de ce refactor. + // + // **8 SUR 8.** Le `switch` d'archétypes a désormais un jumeau en pile d'opérateurs, opt-in + // par strate, chacun vérifié par un test d'équivalence bit à bit contre sa fonction + // d'origine. Ce qui n'est PAS fait : `ClassifyTile` n'utilise toujours pas `ClassifyBox`. return true; default: return false;