feat(opstack C3): TunnelNetwork + Underwater in the ported list -- 8 of 8

UsesOperatorStackForChunk now returns true for both, so the archetype switch has a complete
operator-stack twin: per-strate opt-in, every archetype equivalence-tested bit for bit against its
original function.

This changes nothing by itself. The flag still requires bUseOperatorStack ticked on a strate asset,
which is Jahni's call and was deliberately NOT done. What HAS changed is that the flag is no longer
a no-op anywhere: ticking it on any strate now really switches that strate onto the stack.

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.

DOCS
- CODEMAP 3.2d: ported list 6 of 8 -> 8 of 8; the BuildTunnelNetworkStack row rewritten (19 ops, one
  builder for two archetypes); six new rows for the detail modifiers, each carrying the thing a
  reader would otherwise have to rediscover -- roughness reads STRATE params, terrace re-queries the
  SDF, the cliff's comment disagrees with its code, columns have no strate parameter at all, and
  LocalParams() is the override whose EffectOverBox is too optimistic on a strate with an op pool.
  Also corrected the stale "never compare the two paths" line: C10 is closed and all eight
  equivalence tests compare bit for bit.
- CODEMAP 3.3 UsesOperatorStackForChunk row: same list, plus the warning that the flag is now a real
  switch rather than a harmless tick.
- OPSTACK-PLAN: status header and the Phase 2 order both updated; the three-stage TunnelNetwork
  breakdown and the calls-not-transcribes rule recorded there rather than only in the code.
- OPSTACK-PROGRESS: the closing entry for this unattended run -- every commit in order, the five
  original-code findings ported as-is, the two decisions that are not reversible by taste, the
  ClassifyBox optimism C1 introduced and that must be fixed before ClassifyTile consumes it, what
  breaks first per group, and the likely compile-error spots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 03:46:17 +02:00
parent e479fcdf7d
commit 10dbe64b06
4 changed files with 211 additions and 18 deletions
+18 -7
View File
@@ -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 4b4h (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** (topbottom ≤ 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). |
+32 -10
View File
@@ -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 4b4h`, 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.
+144
View File
@@ -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.
@@ -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;