diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 9f6b7a4..c138f69 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -4362,3 +4362,43 @@ sitting behind a routing mistake in the classifier's archetype lookup. out-of-layout chunks their own category in `ClassifyTile` rather than borrowing `TunnelNetwork`. If open air above the stack is provably uniform, those tiles become an `AllAir`/`AllSolid` verdict and T1.d finally fires on the majority of a surface flight. + +## 2026-08-16 (s) — T1.d BLOCKER FIXED: out-of-layout is constant AIR, not a cave archetype + +The contradictory comments are settled by reading `GetGenerationParams`: + +```cpp +// If outside all strates, return negative density → guaranteed air. +if (SlotIdx < 0) { + Empty.BaseDensity = -1.0f; Empty.WormStrength = 0.0f; Empty.RoomDensity = 0.0f; +``` + +⇒ **`GetGeneratorTypeForChunk`'s comment ("the fallback density path will produce solid rock +anyway") is FALSE.** Out-of-layout is a **constant air field** — no rooms, no worms, nothing to +carve or fill it. `IsGapChunk`'s "open air, NOT a gap" was the correct one. + +### The fix + +`ClassifyTile` gains a fourth Z category (`MemoCat = 3`) for chunks with no layout slot: + +- sets `bAnyNonCave = true` ⇒ **never enters the cave branch**, which is where all 83 % were dying; +- sets `bCanSolid = false` ⇒ the AllSolid hypothesis dies, **AllAir survives**; +- bails to `Mixed` if a disturbance could *add* rock there (`BridgeDensity`/`RidgeDensity` > 0) — + chasms only carve, so they cannot threaten an air verdict. + +A tile entirely above the stack now resolves: no cave block, no column scan (`NumSlots == 0`), +`bCanSolid = false`, `bCanAir = true` ⇒ **`AllAir`, skipped.** That is the majority of a surface +flight, and it is a class of tile T1.d has never once been able to prove. + +### What to read after the build + +- **`Cave Bail Not Op Stack No Layout` should collapse to ~0** — those tiles no longer reach the + cave branch at all. +- **`Tiles Skipped All Air` should rise sharply**, and `Tiles Meshed` should fall below + `Tiles Classified` for the first time in the game. +- ⚠️ **`violations` must stay 0** in every test, and the eight equivalences must stay bit-identical: + this changes only the *classifier*, never a density value. + +Compile-risk spots: `FindSlotIndexForChunkZ` is public (`VoxelStrateManager.h:351`, verified); +`FStrateDisturbanceParams` is already used later in the same function; the new `else if` sits before +the surface `else`, so `Slots[MemoSlotIdx]` is never dereferenced for the new category. diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index ccea273..76e1d8d 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -2777,6 +2777,37 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in MemoCat = 0; bAnyNonCave = true; } + //================================================================= + // ⛔ HORS LAYOUT = AIR CONSTANT. C'ÉTAIT LE BLOCAGE DE T1.d. + //================================================================= + // `GetGeneratorTypeForChunk` rend `TunnelNetwork` pour tout chunk hors de la pile de + // strates (« le chemin de repli produit de la roche de toute façon » — CE COMMENTAIRE + // EST FAUX) et `IsGapChunk` rend false au-dessus du sommet (« open air, NOT a gap »). + // Résultat : chaque tuile touchant l'air libre au-dessus du monde entrait dans la + // BRANCHE DE CAVE, n'y trouvait aucun slot, et abandonnait — mesuré en jeu à 83 % des + // tuiles classées (`Cave Bail Not Op Stack No Layout` = 1.58 / 1.90). + // + // La vérité est dans `GetGenerationParams` : hors layout il rend `BaseDensity = -1`, + // `RoomDensity = 0`, `WormStrength = 0` — un champ CONSTANT, donc de l'air, sans salle + // ni ver pour le percer. Une telle tuile est prouvable sans échantillonner. + // + // Out-of-layout is a CONSTANT AIR field, not a cave archetype. Every tile touching the + // open air above the world was being routed into the cave branch and bailing there. + else if (StrateManager->FindSlotIndexForChunkZ(ChunkZ) < 0) + { + MemoCat = 3; + bAnyNonCave = true; // n'entre JAMAIS dans la branche de cave + + // Les disturbances sont appliquées APRÈS la densité d'archétype et peuvent AJOUTER + // de la roche (ponts, arêtes). Même prudence que les branches gap et cave : si + // l'une peut agir ici, on ne prouve rien. Les chasms ne font que creuser ⇒ ils ne + // menacent pas un verdict d'air. + const FStrateDisturbanceParams DOut = StrateManager->GetDisturbanceParamsForChunk(CC); + if (DOut.BridgeDensity > 0.0f || DOut.RidgeDensity > 0.0f) + { + return EVoxelTileClass::Mixed; + } + } else if (StrateManager->GetGeneratorTypeForChunk(CC) == ECaveGeneratorType::SurfaceWorld) { // Retrouve (ou résout) le slot de strate — l'identité vient des bornes chunk-Z du layout. @@ -2888,6 +2919,14 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in { bCanAir = false; // bedrock du gap = solide (le carve des passages est déjà gardé) } + else if (MemoCat == 3) + { + // Hors layout = air constant (BaseDensity = -1, aucune salle, aucun ver). L'hypothèse + // « tout solide » meurt ; « tout air » survit. Les passages et la spine ne font que + // creuser — ils sont déjà gardés plus haut et ne peuvent pas rendre ce z solide. + // Out of layout = constant air: AllSolid dies, AllAir survives. + bCanSolid = false; + } else { FSurfSlot& S = Slots[MemoSlotIdx];