fix(opstack): the worm inherits the room source's box verdict -- the actual blocker

First build of the spatial EffectOverBox came back green with 0 tiles proved of
40. The warning written for exactly that case fired, and then was not good
enough: it offered two candidate causes and BOTH were wrong. The real one was a
third operator neither candidate mentioned.

FWormFieldSource answered CarveOnly unconditionally with a provably correct
amplitude bound of WormStrength. But BaseDensity = 8 and WormStrength = 10 are
the DEFAULTS -- the field's own comment requires the worm to exceed BaseDensity
or it could never carve air. So SolidMargin = 8 - 10 < 0 on every tile of every
strate with worms on, before the fold reached anything the room source proved. A
numerically correct bound that is structurally always fatal.

The fix was already written three lines up in the worm's own Eval: NetworkMask
is 0 when CaveSDF >= WormNetworkRange, which FLT_MAX always satisfies. The worm
IS spatially bounded -- by the room source's bound, exactly like the twelve
detail modifiers -- so where the source proves Identity it does not execute at
all. Thirteen inheritors instead of twelve. Verified FVoxelOpSample::Sdf really
does initialise to FLT_MAX rather than assuming it; the inheritance inverts into
a hole otherwise.

Deliberately NOT VF_NoCaveOverBox: that helper answers "identity" for a null
Rooms, correct for the twelve modifiers and wrong for an op a future assembly
could place behind a different SDF writer. No room source means we do not know,
which must cost CPU rather than a hole.

And the instrument, because the guess is the thing that cost a build: a
diagnostic that lists candidate causes without measuring them is still a guess
wearing rigour. FVoxelOpStack::ClassifyBoxAttributed reports the index of the
first op that kills each hypothesis -- same loop, same early-out, verdict
identical to ClassifyBox, because a diagnostic that takes a different path than
the thing it explains sends you hunting in the wrong place.
IVoxelDensityOp::DebugName gives them names and touches no cache key, so it
cannot change the world. Check 4 now always prints "AllSolid killed by: <op>
xN", and the zero-proved warning says to read it instead of re-deriving.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 18:10:51 +02:00
parent 03ddcde334
commit f537648867
6 changed files with 287 additions and 15 deletions
+1 -1
View File
@@ -155,7 +155,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 3 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
| `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**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. **`EffectOverBox` ANSWERS SPATIALLY** since 2026-07-28: it builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then lifts each primitive's per-voxel cull from point to box — rooms/tunnels as spheres vs the warp-dilated box, pits/chimneys vs the **undilated** box (real coords), columns as infinite cylinders. `Identity``Sdf` stays `FLT_MAX``FSdfConvertOp` **and all twelve detail modifiers** go identity with it. Verdict is memoised per box (the twelve all ask the same question). Warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`, not the header's observed `~[-1,1]`. |
| `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. |
| `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). **`EffectOverBox` INHERITS `FRoomGraphSource`'s verdict** since 2026-07-28 — its `Eval` sets `NetworkMask = 0` when `CaveSDF >= WormNetworkRange`, which `FLT_MAX` always satisfies, so where the room source proves `Identity` the worm doesn't execute at all. ⚠️ This was **the** blocker: `BaseDensity = 8` < `WormStrength = 10` **by default** (the field comment requires it), so an unconditional `CarveOnly` drove `SolidMargin` negative on every tile in the world and no room-source proof could survive behind it. Deliberately **not** `VF_NoCaveOverBox` — that helper answers "identity" for a null `Rooms`, which is wrong for an op that could sit behind a different SDF writer. |
| `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. |
+96
View File
@@ -2583,3 +2583,99 @@ at the site rather than in a handoff.
- Everything else should be **unchanged and green**. The §C2 fix changes generated terrain only
inside transition bands on the `switch` path (where it was previously order-dependent, i.e. not
well-defined), so an equivalence test that moves is a real signal, not expected noise.
## 2026-07-28 — GREEN, and `0 proved of 40`. The blocker was the **worm**, not the room source.
The build was green and every number in the previous entry held. One line did not:
```
Box verdicts over 40 TunnelNetwork tiles: 0 proved (0 AllSolid, 0 AllAir), 40 Mixed
-- brute-forced over 0 voxels, 0 violations.
WARNING: No TunnelNetwork tile was proved ... this check verified nothing.
```
**The warning I wrote for exactly this case fired, and then it was not good enough.** It offered two
candidate causes — "the tiles genuinely straddle cave" or "the source isn't reaching its `Identity`
branch" — and **both were wrong**. The real cause was a third operator that neither candidate
mentioned. That is the failure worth recording, more than the bug itself.
### The cause, found by reading two default values
`FWormFieldSource::EffectOverBox` answered `CarveOnly` unconditionally, with a *provably correct*
amplitude bound of `WormStrength`. And in `VoxelStrateTypes.h`:
```
float BaseDensity = 8.0f;
float WormStrength = 10.0f; // "Must exceed BaseDensity to create air." <- the field's own comment
```
So `SolidMargin = 8 10 = 2 < 0`, on **every tile of every strate with worms on**, before the fold
ever reached anything the room source had proved. The worm's bound is not loose by accident — the
defaults *require* it to exceed `BaseDensity`, or worms could never carve. A numerically correct
bound that is structurally always fatal.
### The fix was already written in the worm's own `Eval`, three lines up
```cpp
if (CaveSDF >= P.WormNetworkRange) // vrai aussi quand il n'y a pas de réseau (FLT_MAX)
{ NetworkMask = 0.0f; }
...
if (NetworkMask <= 0.0f) { return; }
```
**The worm IS spatially bounded** — not by a bound of its own, but by the room source's, exactly like
the twelve detail modifiers. Where `FRoomGraphSource` proves `Identity`, `Sdf` stays `FLT_MAX`
(verified: `FVoxelOpSample::Sdf = FLT_MAX` is the initialiser), so `NetworkMask` is 0 at every voxel
and `Eval` returns before touching `Density`. The worm is the *identity* there, not "a bounded
carve". It simply never asked the question.
So it now inherits the verdict — thirteen inheritors instead of twelve. Two details:
- **Deliberately NOT `VF_NoCaveOverBox`.** That helper returns `true` when `Rooms == nullptr`, which
is right for the twelve modifiers (they only ever exist in a stack where the room source is the
sole SDF writer) and **wrong** for the worm, which a future assembly could place behind a different
SDF writer — `FLatticeCorridorSource` writes that channel too. No room source ⇒ we don't know ⇒
`CarveOnly`. Not knowing must cost CPU, never a hole.
- `MaxCarveOverBox` now calls `EffectOverBox` rather than re-testing the condition, so the two cannot
drift. The room source's verdict memo makes the second call free.
### The lesson, and the instrument that came out of it
**A diagnostic that lists candidate causes without measuring them is still a guess** — it just looks
like rigour. My warning named two causes and had a number for neither, so a green run with a real
defect in it produced a message that sent the reader to the wrong two places.
`FVoxelOpStack::ClassifyBoxAttributed` now exists: the same fold, verdict-identical to `ClassifyBox`
(same loop, same early-out — a diagnostic that takes a different path than the thing it explains is
worse than none), reporting the **index of the first operator that kills each hypothesis**.
`IVoxelDensityOp::DebugName()` gives them readable names; it touches no cache key and no generation
decision, so it cannot change the world. Check 4 prints:
```
AllSolid killed by: <op> x<count>, <op> x<count>, ...
```
Always printed, not only on failure — when tiles *are* proved, that line is what says why the rest
are not. And the `0 proved` warning now says **"do not re-derive the cause, read the attribution
line"**, because the next person's guess would be as good as mine was.
### Ready to build. Likely compile-error spots
1. `FWormFieldSource` gained a third ctor arg (defaulted) and a `Rooms` member; its single
construction site in `BuildTunnelNetworkStack` passes `RoomPtr`, which is already in scope there.
2. `IVoxelDensityOp::DebugName()` is a new virtual with a default — six overrides added
(`ConstantFieldSource`, `RoomGraphSource`, `SdfConvertOp`, `OriginSpineOp`, `BoundarySealOp`,
`PassageCarveOp`, `WormFieldSource`). Everything else inherits `"(unnamed op)"` and reports by index.
3. `ClassifyBoxAttributed` / `GetOpDebugName` are new inline methods on `FVoxelOpStack` (header).
4. The test uses `TMap<FString,int32>::ValueSort` and `FindOrAdd` on a `const TCHAR*` key.
### What to read, in order
1. **`AllSolid killed by:`** — the new line. If it is empty ("AllSolid survived every tile") the
prize has landed. If it names `RoomGraphSource`, the tiles genuinely straddle cave and the sampler
is what to look at. If it names anything else, that operator's box answer is more pessimistic than
its `Eval`, and it is now named rather than guessed at.
2. **`Box verdicts over 40`** — proved count is a measurement; the assertion is only that no proved
tile is *wrong*.
3. Everything else should be unchanged. The worm change cannot alter density: `EffectOverBox` and
`MaxCarveOverBox` are box-verdict methods, and `Eval` is untouched.
@@ -1106,6 +1106,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
int32 NumProved = 0, NumMixed = 0, NumSolid = 0, NumAir = 0;
int32 NumBruteSamples = 0, NumViolations = 0;
float WorstViolation = 0.0f;
TMap<FString, int32> SolidKillerCounts;
FRandomStream Rng(97531);
for (int32 t = 0; t < 40; ++t)
@@ -1121,7 +1122,18 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Ctx);
// ATTRIBUTION — le même pliage, mais il dit QUI tue chaque hypothèse. Le premier build
// de l'`EffectOverBox` spatial est revenu vert avec 0 tuile prouvée, et le rapport ne
// savait nommer aucun coupable : les deux causes que la mise en garde proposait étaient
// toutes les deux fausses, la vraie étant un troisième opérateur. On ne redevine pas.
int32 SolidKiller = INDEX_NONE, AirKiller = INDEX_NONE;
const EVoxelTileClass Verdict = Stack.ClassifyBoxAttributed(Box, Ctx, SolidKiller, AirKiller);
if (SolidKiller != INDEX_NONE)
{
SolidKillerCounts.FindOrAdd(Stack.GetOpDebugName(SolidKiller))++;
}
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
++NumProved;
@@ -1158,13 +1170,37 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
TEXT("verdict leaves no geometry and no collision behind it."),
NumProved, NumSolid, NumAir, NumMixed, NumBruteSamples, NumViolations));
// QUI TUE `AllSolid`, ET COMBIEN DE FOIS. Toujours imprimé, pas seulement en cas d'échec :
// c'est aussi la ligne qui dit, quand des tuiles SONT prouvées, ce qui bloque les autres.
{
SolidKillerCounts.ValueSort([](int32 A, int32 B) { return A > B; });
FString Breakdown;
for (const TPair<FString, int32>& Kv : SolidKillerCounts)
{
if (!Breakdown.IsEmpty()) { Breakdown += TEXT(", "); }
Breakdown += FString::Printf(TEXT("%s x%d"), *Kv.Key, Kv.Value);
}
if (Breakdown.IsEmpty()) { Breakdown = TEXT("nothing -- AllSolid survived every tile"); }
AddInfo(FString::Printf(
TEXT("AllSolid killed by: %s. This is the line that replaced a guess. The first ")
TEXT("build of the spatial EffectOverBox reported 0 proved of 40, and the warning ")
TEXT("offered two candidate causes -- BOTH WRONG. The real one was a third operator ")
TEXT("nobody was looking at: FWormFieldSource answered CarveOnly everywhere, and ")
TEXT("since BaseDensity=8 < WormStrength=10 BY DEFAULT, its provable amplitude bound ")
TEXT("alone drove SolidMargin negative on every tile in the world. Attribution is ")
TEXT("cheap; a second wrong guess is not."),
*Breakdown));
}
if (NumProved == 0)
{
AddWarning(TEXT("No TunnelNetwork tile was proved. That is not a failure, but it means ")
TEXT("this check verified nothing: the brute force below has no verdict to ")
TEXT("contradict. Either the sampled tiles all genuinely straddle cave, or ")
TEXT("the spatial EffectOverBox is not reaching its Identity branch -- the ")
TEXT("bake-coverage line of check 5b is the one that tells those apart."));
AddWarning(TEXT("No TunnelNetwork tile was proved, so the brute force below verified ")
TEXT("nothing -- it has no verdict to contradict. Do NOT re-derive the cause: ")
TEXT("read the 'AllSolid killed by' line above, which names the operator and ")
TEXT("counts how often. If it names RoomGraphSource, the tiles genuinely ")
TEXT("straddle cave (check the bake-coverage line of 5b); anything else is an ")
TEXT("operator whose box answer is more pessimistic than its Eval."));
}
TestEqual(FString::Printf(
@@ -144,6 +144,8 @@ namespace
return FMath::Abs(Value);
}
const TCHAR* DebugName() const override { return TEXT("ConstantFieldSource"); }
private:
float Value;
};
@@ -1119,6 +1121,8 @@ namespace
return EVoxelOpEffect::Identity; // la source a répondu pour la paire
}
const TCHAR* DebugName() const override { return TEXT("SdfConvertOp"); }
private:
float Blend, BaseDensity, Sign, MinDivisor;
};
@@ -1159,6 +1163,8 @@ namespace
return EVoxelOpEffect::CarveOnly;
}
const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); }
private:
float TopZ, BotZ, Seal, Base, Radius;
};
@@ -1226,6 +1232,8 @@ namespace
return EVoxelOpEffect::FillOnly;
}
const TCHAR* DebugName() const override { return TEXT("BoundarySealOp"); }
private:
float TopZ, BotZ, Thickness, Base;
};
@@ -1258,6 +1266,8 @@ namespace
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
}
const TCHAR* DebugName() const override { return TEXT("PassageCarveOp"); }
private:
const UVoxelStrateManager* Manager;
float Base, Seal;
@@ -2393,6 +2403,8 @@ namespace
return B.Verdict;
}
const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); }
private:
FStrateGenerationParams P;
int32 Seed;
@@ -3505,8 +3517,12 @@ namespace
class FWormFieldSource final : public IVoxelDensityOp
{
public:
FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed)
: P(InP), SeedU((uint32)Seed) {}
/** @param InRooms ⚠️ UNIQUEMENT pour `EffectOverBox` / `MaxCarveOverBox`. `Eval` lit le
* canal SDF de `InOut`, pas ce pointeur — le ver n'interroge jamais la
* source directement, il consomme ce qu'elle a écrit. Peut être nullptr. */
FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed,
const FRoomGraphSource* InRooms = nullptr)
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3572,10 +3588,57 @@ namespace
* est solide de plus que la somme des carves restants » redevient prouvable — et c'est le
* plus gros poste de perf du plan. Noté ici, au point exact où la borne manque.
*/
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
/**
* ✅ **LE VER HÉRITE DU VERDICT DE LA SOURCE DE SALLES — ET C'EST CE QUI DÉBLOQUE TOUT.**
*
* La note ci-dessus (« aucune borne spatiale, donc il tue `AllSolid` sur CHAQUE tuile »)
* était vraie, et pourtant elle passait à côté de ce que son propre `Eval` fait trois
* lignes plus haut :
*
* ```
* if (CaveSDF >= P.WormNetworkRange) // vrai aussi quand il n'y a pas de réseau (FLT_MAX)
* { NetworkMask = 0.0f; }
* ...
* if (NetworkMask <= 0.0f) { return; }
* ```
*
* **Le ver EST spatialement borné** — pas par une borne à lui, mais par celle de la source
* de salles, exactement comme les douze modificateurs de détail. Là où `FRoomGraphSource`
* prouve `Identity`, `Sdf` reste `FLT_MAX` sur toute la boîte, donc `NetworkMask` vaut 0
* partout, donc ce `return` est pris à chaque voxel. Le ver est l'identité, pas « un carve
* borné » : il ne s'exécute pas.
*
* ⚠️ POURQUOI CE CONTRÔLE COMPTAIT AUTANT. `BaseDensity = 8` et `WormStrength = 10` sont
* les DÉFAUTS, et le commentaire de `WormStrength` dit pourquoi (« must exceed BaseDensity
* to create air »). Donc `SolidMargin = 8 10 < 0` : tant que le ver rendait `CarveOnly`
* partout, il tuait `AllSolid` sur **toutes** les tuiles, et la réponse spatiale de la
* source de salles ne pouvait rien prouver derrière lui. Le premier build l'a montré —
* 0 tuile prouvée sur 40, la source ayant pourtant appris à répondre.
*
* ⚠️ ET POURQUOI ON N'UTILISE **PAS** `VF_NoCaveOverBox` ICI. Cet assistant rend `true`
* quand `Rooms == nullptr` — correct pour les douze modificateurs, qui n'existent que dans
* une pile où la source de salles est le seul écrivain du canal SDF. Le ver, lui, est un
* opérateur dont un futur assemblage pourrait le placer derrière un AUTRE écrivain de SDF
* (`FLatticeCorridorSource` en écrit un). Sans source de salles, on ne sait pas : on rend
* `CarveOnly`. Ne pas savoir doit coûter du CPU, jamais un trou.
*
* The worm IS spatially bounded — by the room source's bound, not one of its own, exactly
* like the twelve detail modifiers. Where the room source proves Identity, Sdf stays
* FLT_MAX, NetworkMask is 0 everywhere and Eval returns immediately. This mattered because
* BaseDensity=8 < WormStrength=10 BY DEFAULT, so an unconditional CarveOnly killed AllSolid
* on every tile. Deliberately not VF_NoCaveOverBox: its null-Rooms case answers "identity",
* which is wrong for an op that could sit behind a different SDF writer.
*/
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? EVoxelOpEffect::CarveOnly
: EVoxelOpEffect::Identity;
if (!(P.WormStrength > 0.0f && P.WormThreshold > 0.0f)) { return EVoxelOpEffect::Identity; }
if (P.WormNetworkRange > 0.0f && Rooms != nullptr
&& Rooms->EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity)
{
return EVoxelOpEffect::Identity;
}
return EVoxelOpEffect::CarveOnly;
}
/**
@@ -3588,8 +3651,12 @@ namespace
* la seule sorte qui ait le droit d'être ici : sur-estimer coûte du CPU, sous-estimer fait
* un trou.
*/
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
// Cohérent avec `EffectOverBox` PAR CONSTRUCTION plutôt que par relecture : deux
// conditions écrites deux fois finiraient par diverger. Le mémo de verdict de
// `FRoomGraphSource` rend ce second appel gratuit.
if (EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity) { return 0.0f; }
return MaxCarveAmplitude();
}
@@ -3599,15 +3666,19 @@ namespace
return 0.0f;
}
/** L'amplitude max de carve, en unités de densité. */
/** L'amplitude max de carve, en unités de densité. Borne BRUTE : elle ignore la portée du
* réseau, c'est `MaxCarveOverBox` qui l'applique. */
float MaxCarveAmplitude() const
{
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? P.WormStrength : 0.0f;
}
const TCHAR* DebugName() const override { return TEXT("WormFieldSource"); }
private:
FStrateGenerationParams P;
uint32 SeedU;
const FRoomGraphSource* Rooms; // NON possédant — peut être nullptr (voir EffectOverBox)
};
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
@@ -3819,7 +3890,11 @@ namespace VoxelDensityOps
OutStack.Add(MakeUnique<FDomeMod>(P, RoomPtr)); // 4g — dômes
OutStack.Add(MakeUnique<FPinchMod>(P, RoomPtr)); // 4h — pincement
OutStack.Add(MakeUnique<FFloorBiasMod>(P, RoomPtr)); // fin 4h — biais de sol
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed));
// ⚠️ `RoomPtr` N'EST PAS DÉCORATIF ICI. Le ver hérite du verdict de boîte de la source de
// salles, faute de quoi il rend `CarveOnly` partout et tue `AllSolid` sur chaque tuile —
// avec les défauts (`BaseDensity = 8`, `WormStrength = 10`) la marge part négative, donc
// aucune tuile n'est prouvable, quoi que la source de salles ait réussi à prouver.
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed, RoomPtr));
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
+21
View File
@@ -415,6 +415,27 @@ public:
* ValidateDeterminism — qui échantillonne le long d'une frontière en X — ne le verrait pas.
*/
virtual bool IsXYPure() const { return false; }
/**
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
*
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE, ET CE QU'ELLE A COÛTÉ DE NE PAS AVOIR. Le premier build de
* l'`EffectOverBox` spatial est revenu **vert avec 0 tuile prouvée sur 40**, et la seule chose
* que le rapport pouvait dire était « ou bien les tuiles traversent toutes une grotte, ou bien
* la source n'atteint pas sa branche `Identity` ». Deux causes, zéro nombre pour les
* départager — exactement le piège que ce projet a déjà payé plusieurs fois. La vraie cause
* était un TROISIÈME opérateur (le ver, qui rendait `CarveOnly` partout). Avec un nom par
* opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au lieu de laisser deviner.
*
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
* sans nom se repère à son index, ce qui suffit à savoir où regarder.
*
* Diagnostic only: the first build of the spatial EffectOverBox came back green with 0 tiles
* proved, and the report could not name which operator was killing the hypothesis. It was a
* third one nobody was looking at. Never part of a cache key or any generation decision.
*/
virtual const TCHAR* DebugName() const { return TEXT("(unnamed op)"); }
};
//=============================================================================
@@ -135,6 +135,50 @@ public:
return H.Resolve();
}
/**
* LE MÊME PLIAGE, MAIS QUI DIT **QUI** A TUÉ CHAQUE HYPOTHÈSE. Diagnostic, réservé aux tests.
*
* ⚠️ IL DOIT RENDRE EXACTEMENT LE MÊME VERDICT QUE `ClassifyBox` — même boucle, même early-out,
* même ordre. Un diagnostic qui emprunte un chemin légèrement différent de celui qu'il explique
* est pire que pas de diagnostic : il envoie chercher le bug ailleurs. Si l'un des deux change,
* l'autre change avec lui.
*
* `OutSolidKiller` / `OutAirKiller` reçoivent l'INDEX du premier opérateur qui fait passer
* l'hypothèse correspondante de vraie à fausse, ou `INDEX_NONE` si elle a survécu. Le nom
* lisible s'obtient par `GetOpDebugName(index)`.
*
* Same fold, but it reports WHICH op killed each hypothesis. Must stay verdict-identical to
* ClassifyBox — a diagnostic that takes a slightly different path sends you hunting in the
* wrong place.
*/
EVoxelTileClass ClassifyBoxAttributed(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32& OutSolidKiller, int32& OutAirKiller) const
{
OutSolidKiller = INDEX_NONE;
OutAirKiller = INDEX_NONE;
FVoxelBoxHypotheses H;
for (int32 i = 0; i < Ops.Num(); ++i)
{
const bool bSolidBefore = H.bCanBeAllSolid;
const bool bAirBefore = H.bCanBeAllAir;
VF_FoldOp(H, *Ops[i], VoxelBox, Ctx);
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
if (H.IsDead()) { return EVoxelTileClass::Mixed; }
}
return H.Resolve();
}
/** Nom lisible d'un opérateur, pour les rapports de test. Voir `IVoxelDensityOp::DebugName`. */
const TCHAR* GetOpDebugName(int32 Index) const
{
return Ops.IsValidIndex(Index) ? Ops[Index]->DebugName() : TEXT("(none)");
}
/**
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
* spine (0,0) → seal de frontière → carve de passage.