diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index d4b3c8b..93bc7a5 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -4455,3 +4455,90 @@ four-way split of `Cave Bail Not Op Stack` showed `SoleSlot` and `BoundaryTile` proved the assets were correctly ticked (Jahni said so twice; I doubted it twice) and pointed at `NoLayout`, a cause nobody had considered. *A counter that can fire for two reasons is not a measurement.* + +## 2026-08-17 (v) — mixed-content split + guarded category fold LANDED, awaiting Jahni's build + +**Nothing in this entry was built, compiled, run in the editor, automation-tested, or measured.** +The only validation was static source/diff inspection and `git diff --check`. All rates below are +instructions for the next flight, not claimed results. + +### The old 0.77 bucket is split before changing its guard + +`Cave Bail Mixed Content` had three call sites and four meanings. It is replaced by: + +- `Cave Mix Out Of Layout` — a tile contains one cave slot plus constant-air outside the layout; +- `Cave Mix Gap` — one cave slot plus solid inter-strate bedrock; +- `Cave Mix Surface World` — one cave slot plus a SurfaceWorld slot; +- `Cave Bail Two Cave Slots` — a second cave slot was found; this remains an immediate `Mixed` bail. + +The first three are deliberately **presence counters**, incremented once per present category per +tile before the later stack guards. They are not exclusive for an unusually tall tile, so they are +not expected to sum to the old 0.77; each answers one unambiguous yes/no question. This placement is +why the diagnostic survives the fold: a tile can increment `Cave Mix Out Of Layout` and later be +proved `AllAir`. `Two Cave Slots` is still a bail counter because that condition was not relaxed. + +The split was applied first with the original `return Mixed` intact. Only after that +behavior-preserving split was inspected was the return deliberately replaced by the fold below. + +### Decision: relax one-category, NOT one-cave-slot + +Folding is safe, but only in the narrow form suggested by the code: + +- Gap already contributes a proved `solid` hypothesis, out-of-layout a proved `air` hypothesis, + and SurfaceWorld classifies its exact mesher lattice through `ComputeSurfaceColumn`. +- The one cave slot records the minimum/maximum **sampled cave Z**. `ClassifyBox` receives that cave + Z sub-box with the tile's full XY extent; asking it about the previous full tile box would model + gap/surface/out-of-layout with cave params and is not valid. +- Opt-in, generator type, and bit-identical params are still checked for **every chunk intersecting + that cave sub-box in XYZ**. They were narrowed in Z only to exclude categories represented by + their own hypotheses; none was removed or weakened. +- The stack verdict folds into the existing `bCanSolid` / `bCanAir`, then SurfaceWorld's exact + column scan still runs. `Tiles Operator Stack Solid/Air` now increments only for the final + non-`Mixed` tile verdict, after all categories have agreed. + +The independent **one-cave-slot requirement cannot be relaxed by this fold**. A second cave slot can +carry a different archetype, opt-in, params, and stack; proving it would require building and +guarding a second stack, not stretching the first. `Cave Bail Two Cave Slots` therefore still +returns `Mixed`. Category folding is compatible with the generator-type and bit-identical-param +guards precisely because those guards remain exhaustive over the sole cave sub-box. + +`Cave Bail Fold Conflict` was added for a proved cave verdict that contradicts an already proved +category (for example cave=solid plus out-of-layout=air). That is distinct from `Cave Bail Stack +Verdict` (the stack itself could not prove its box) and `Cave Bail Disturbance` (a post-stack feature +killed the remaining hypothesis). + +### Exact next-build / next-flight readout + +Read these counters together on the same underground route: + +1. `Cave Mix Out Of Layout`, `Cave Mix Gap`, `Cave Mix Surface World`, and + `Cave Bail Two Cave Slots` — whichever is largest names the dominant old mixed-content cause. + The first three are presence counts and may overlap; `Cave Bail Mixed Content` should be absent. +2. `Tiles Operator Stack Air` and `Tiles Operator Stack Solid` — either becoming non-zero proves a + cave stack participated in a **final** skip verdict. For the measured cave+open-air population, + the expected success signal is primarily `Tiles Operator Stack Air`. +3. `Tiles Skipped All Air`, `Tiles Skipped All Solid`, `Tiles Meshed`, and `Tiles Classified` — the + accounting must still close: `Meshed + SkippedAllAir + SkippedAllSolid = Classified`. A fold win + raises a skipped counter and lowers `Tiles Meshed`; the pre-fold baseline was 1.15 meshed, + 0.73 skipped-air, 1.88 classified. +4. `Cave Bail Fold Conflict`, `Cave Bail Stack Verdict`, `Cave Bail Params`, + `Cave Bail Disturbance`, and `Cave Bail Two Cave Slots` — these explain mixed tiles that remain. + In particular, a high `Fold Conflict` means the dominant mix is genuinely heterogeneous rather + than blocked merely by the former one-category policy. + +After Jahni runs the suite, every brute-force `violations` count must remain zero and all eight +operator-stack equivalences must remain bit-identical. Those are correctness requirements, not +results from this unbuilt change. + +### Likely compile-error / review watch spots + +- declaration/definition/use spelling for the five new UE stats, especially + `STAT_VoxelForgeCaveBailFoldConflict` and `STAT_VoxelForgeCaveBailTwoCaveSlots`; +- the `MAX_int32` / `MIN_int32` cave-extrema initializers and `FMath::Min/Max(int32)` assignments to + `CaveMinZ` / `CaveMaxZ`; +- construction of `FBox CaveBox` from the cave-only integer Z extrema; +- control flow after removing the cave branch's final return: SurfaceWorld columns must run before + the shared final verdict, and `bAnyCave` must imply initialized cave extrema (established in the + same `MemoCat == 2` iteration); +- no protected layout accessor was introduced: the code still uses public + `GetStrateChunkZBounds`, never `FindSlotIndexForChunkZ`. diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 37227b1..1b8a9db 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -2725,8 +2725,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (CX * CX + CY * CY <= Reach * Reach) bCanSolid = false; } - // ── Catégorisation par Z de treillis. v1 : gap bedrock = solide ; SurfaceWorld = test - // colonne ; tout le reste (intérieurs de caves, hors layout) = Mixed immédiat. ── + // ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ; + // SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ── struct FSurfSlot { int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks) @@ -2753,13 +2753,19 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in int32 NumSlots = 0; // ── T1.d GÉNÉRIQUE : la pile d'opérateurs classe les archétypes de CAVE ── - // Ces trois-là suivent le slot de cave que la tuile touche. Le verdict de la pile porte sur la - // BOÎTE ENTIÈRE, pas sur un z, donc il ne peut être calculé qu'après la boucle — et il n'est - // valable que si la tuile ne touche QUE ce slot-là (voir la garde `bAnyNonCave`). + // Ces valeurs suivent l'UNIQUE slot de cave que la tuile touche. La pile classera seulement la + // sous-boîte Z de ses échantillons ; les catégories gap/surface/hors-layout plient séparément + // leurs hypothèses dans `bCanSolid` / `bCanAir`. + // These values track the ONE cave slot touched by the tile. The stack classifies only its + // sampled Z sub-box; gap/surface/out-of-layout fold their hypotheses separately. int32 CaveBotChunkZ = INT32_MAX; // identité du slot de cave (borne basse, en chunks) int32 CaveRepChunkZ = 0; - bool bAnyCave = false; - bool bAnyNonCave = false; // gap ou SurfaceWorld dans la même tuile ⇒ on abandonne + int32 CaveMinZ = MAX_int32; + int32 CaveMaxZ = MIN_int32; + bool bAnyCave = false; + bool bAnyGap = false; + bool bAnySurface = false; + bool bAnyOutOfLayout = false; int32 MemoChunkZ = INT32_MAX; int32 MemoCat = -1; // 0 = gap, 1 = surface, 2 = cave (pile), 3 = hors layout @@ -2783,7 +2789,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (StrateManager->IsGapChunk(CC)) { MemoCat = 0; - bAnyNonCave = true; + bAnyGap = true; } //================================================================= // ⛔ HORS LAYOUT = AIR CONSTANT. C'ÉTAIT LE BLOCAGE DE T1.d. @@ -2809,7 +2815,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in else if (!VF_ChunkZHasSlot(ChunkZ)) { MemoCat = 3; - bAnyNonCave = true; // n'entre JAMAIS dans la branche de cave + bAnyOutOfLayout = true; // 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 @@ -2861,7 +2867,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) bCanAir = false; } MemoCat = 1; - bAnyNonCave = true; + bAnySurface = true; } else { @@ -2909,12 +2915,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in int32 CaveTopCZ = 0, CaveBotCZ = 0; if (!StrateManager->GetStrateChunkZBounds(ChunkZ, CaveTopCZ, CaveBotCZ)) { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); + INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout); return EVoxelTileClass::Mixed; // hors layout } if (CaveBotChunkZ != INT32_MAX && CaveBotChunkZ != CaveBotCZ) { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); + INC_DWORD_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots); return EVoxelTileClass::Mixed; } CaveBotChunkZ = CaveBotCZ; @@ -2926,7 +2932,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (MemoCat == 2) { - // Rien par z : la pile répond pour la boîte entière, après la boucle. + // La pile répond après la boucle, sur la sous-boîte Z contenant exactement les + // échantillons cave (XY reste la boîte complète du treillis). + CaveMinZ = FMath::Min(CaveMinZ, Zi); + CaveMaxZ = FMath::Max(CaveMaxZ, Zi); } else if (MemoCat == 0) { @@ -2970,23 +2979,27 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in // DENSITÉ de cette tuile — même fabrique, mêmes params, même drapeau. if (bAnyCave) { - // Une tuile mi-cave mi-surface (ou mi-gap) n'est pas classable ainsi : la pile de cave ne - // répond que pour SA strate, et sa boîte couvrirait des z appartenant à une autre. - if (bAnyNonCave) + // Diagnostic de PRÉSENCE avant les gardes : le signal reste visible même si le pliage rend + // finalement AllSolid/AllAir. Ces compteurs ne sont pas exclusifs entre eux sur une tuile + // très haute ; chacun répond exactement à « cette catégorie était-elle aussi présente ? ». + // Presence diagnostics run before the guards, so a successful fold cannot hide the mix. + // They are not mutually exclusive for a very tall tile; each answers one exact question. + if (bAnyOutOfLayout || bAnyGap || bAnySurface) { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); - return EVoxelTileClass::Mixed; + if (bAnyOutOfLayout) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout); } + if (bAnyGap) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixGap); } + if (bAnySurface) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixSurfaceWorld); } } const FIntVector RepCC(0, 0, CaveRepChunkZ); const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC); //--------------------------------------------------------------------- - // ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA TUILE + // ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA SOUS-BOÎTE CAVE //--------------------------------------------------------------------- // `GetGenerationParams` et ses homologues BLENDENT les params dans les bandes de transition : // `Alpha` dépend du chunk Z pour `Gradient`, et du chunk XY EN PLUS pour `Interleaved`. Deux - // chunks d'une même tuile peuvent donc porter des params différents — c'est le constat de + // chunks d'une même sous-boîte peuvent donc porter des params différents — c'est le constat de // `AUDIT §C2`, confirmé par lecture le 2026-07-28 — et UNE pile ne peut pas représenter DEUX // champs. On construit donc les params pour CHAQUE coordonnée de chunk que la boîte touche et // on exige qu'ils soient identiques bit à bit. @@ -2995,7 +3008,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in // `Mixed` de trop. On se trompe du côté du CPU, jamais du côté du trou. const int32 CX0 = FloorDivC(MinX, CHUNK_SIZE), CX1 = FloorDivC(MaxX, CHUNK_SIZE); const int32 CY0 = FloorDivC(MinY, CHUNK_SIZE), CY1 = FloorDivC(MaxY, CHUNK_SIZE); - const int32 CZ0 = FloorDivC(MinZ, CHUNK_SIZE), CZ1 = FloorDivC(MaxZ, CHUNK_SIZE); + // IMPORTANT : les gardes restent complètes, mais seulement sur les chunks où le mesher + // échantillonne réellement CETTE strate cave. Inclure gap/surface/hors-layout ici ferait + // échouer la garde d'archétype avant de pouvoir plier leurs hypothèses indépendantes. + // The guards stay exhaustive over the cave samples. Non-cave chunks are intentionally not + // represented by this stack; their hypotheses were folded separately in the Z pass. + const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE); // Une tuile très étalée (Step élevé) toucherait trop de chunks pour que cette vérification // reste bon marché. Au-delà, `Mixed` — on renonce au gain, jamais à la sûreté. @@ -3098,7 +3116,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in } default: INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); - return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas arriver ici (bAnyNonCave) + return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas être le type du slot cave } bFirst = false; @@ -3134,9 +3152,9 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in } TileStack.PrepareChunk(OpCtx); - const FBox TileBox(FVector((float)MinX, (float)MinY, (float)MinZ), - FVector((float)MaxX, (float)MaxY, (float)MaxZ)); - const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(TileBox, OpCtx); + const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ), + FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ)); + const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(CaveBox, OpCtx); if (StackVerdict == EVoxelTileClass::Mixed) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict); @@ -3146,6 +3164,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; } else { bCanSolid = false; } + // Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les + // deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec + // de borne de la pile ni une disturbance. + // Fold the cave verdict with gap=solid, out-of-layout=air, and solid surface seals. If both + // hypotheses die here, the categories conflict; this is not a stack-bound/disturbance bail. + if (!bCanSolid && !bCanAir) + { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailFoldConflict); + return EVoxelTileClass::Mixed; + } + //--------------------------------------------------------------------- // ⚠️ LES DISTURBANCES NE SONT PAS DANS LA PILE (`OPSTACK-DECOMPOSITION §10.2`) : // `GetDensityAt` les applique APRÈS, sur la densité déjà négatée. Un verdict qui les @@ -3156,20 +3185,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (D.ChasmDensity > 0.0f) { bCanSolid = false; } if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) { bCanAir = false; } - if (bCanSolid == bCanAir) + if (!bCanSolid && !bCanAir) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailDisturbance); return EVoxelTileClass::Mixed; } - if (bCanSolid) - { - INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); - } - else - { - INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); - } - return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; } // ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne @@ -3228,6 +3248,15 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in // Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles // sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed. if (bCanSolid == bCanAir) return EVoxelTileClass::Mixed; + if (bAnyCave) + { + // Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir + // prouvé sa sous-boîte cave puis perdre l'hypothèse sur une colonne SurfaceWorld adjacente. + // Count only final verdicts that actually skip a tile; a later surface column may still + // invalidate the hypothesis proved for the cave sub-box. + if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); } + else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); } + } return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; } diff --git a/Source/VoxelForge/Private/VoxelStats.cpp b/Source/VoxelForge/Private/VoxelStats.cpp index 1a37a17..d70d2de 100644 --- a/Source/VoxelForge/Private/VoxelStats.cpp +++ b/Source/VoxelForge/Private/VoxelStats.cpp @@ -14,9 +14,13 @@ DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot); DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile); DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout); DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck); -DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent); +DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout); +DEFINE_STAT(STAT_VoxelForgeCaveMixGap); +DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld); +DEFINE_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots); DEFINE_STAT(STAT_VoxelForgeCaveBailParams); DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict); +DEFINE_STAT(STAT_VoxelForgeCaveBailFoldConflict); DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance); DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack); DEFINE_STAT(STAT_VoxelForgeColumnMemoHit); diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 248ac54..93f25fb 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -313,12 +313,12 @@ public: * (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict * porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse). * - * v1 : ne prouve que les chunks GAP (bedrock) et les strates SurfaceWorld — colonnes - * terrain/plafond évaluées par le MÊME ComputeSurfaceColumn que le chemin densité (donc - * bit-identiques), bandes de seal solides, gardes spine/passages/disturbances/diff. - * Tout autre archétype (intérieur de caves) ⇒ Mixed. Worker-safe (lecture seule + - * caches thread_local partagés avec GetDensityAt — un verdict Mixed laisse les colonnes - * chaudes pour la génération qui suit). + * GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ; + * SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le + * chemin densité. Un unique slot cave opt-in peut ajouter le verdict conservatif de sa pile, + * sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent + * des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec + * GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit). */ EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const; diff --git a/Source/VoxelForge/Public/VoxelStats.h b/Source/VoxelForge/Public/VoxelStats.h index 171656e..170aaa6 100644 --- a/Source/VoxelForge/Public/VoxelStats.h +++ b/Source/VoxelForge/Public/VoxelStats.h @@ -18,9 +18,13 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Sole Slot"), STAT DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Boundary Tile"), STAT_VoxelForgeCaveBailNotOpStackBoundaryTile, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack No Layout"), STAT_VoxelForgeCaveBailNotOpStackNoLayout, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Recheck"), STAT_VoxelForgeCaveBailNotOpStackRecheck, STATGROUP_VoxelForge, VOXELFORGE_API); -DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Mixed Content"), STAT_VoxelForgeCaveBailMixedContent, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Two Cave Slots"), STAT_VoxelForgeCaveBailTwoCaveSlots, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Params"), STAT_VoxelForgeCaveBailParams, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Stack Verdict"), STAT_VoxelForgeCaveBailStackVerdict, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Fold Conflict"), STAT_VoxelForgeCaveBailFoldConflict, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Disturbance"), STAT_VoxelForgeCaveBailDisturbance, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail No Stack"), STAT_VoxelForgeCaveBailNoStack, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Hits"), STAT_VoxelForgeColumnMemoHit, STATGROUP_VoxelForge, VOXELFORGE_API);