feat(composer): lateral REGIONS -- built, measured, and gated OFF

Jahni's own idea: "archetype could even mix within the same strate at some
point, tunnel network leading to a big chamber". An archetype becomes a
spatial REGION inside a strate, not a label on it.

Built by extending the mechanism that already exists rather than inventing
one: the stack blends VERTICALLY between strates, so regions blend the same
way LATERALLY. Deterministic jittered Voronoi sites on a 256-voxel lattice,
1-3 regions per strate from (seed, strateIndex), 24-voxel symmetric blend
band, per-chunk lookup cache. Parameters are never blended -- a shaft's
ShaftSpacing means nothing to a slab -- only the resulting DENSITY is.
Structural posts stay global and unconditional, so an invented region
cannot switch off the primordial law.

  single-region path   BIT-IDENTICAL to legacy, 256 probes, native + recipe
  cross-boundary boxes 20 of 40, 199,800 voxels brute-forced, 0 VIOLATIONS
                       (every seam-spanning box correctly returned Mixed)
  blend-band cost      663 ns vs 338 ns per voxel, over 16.0% of voxels
                       ~1.15x field-weighted
  survival             16/16 valid, non-vacuous, largest share >= .50

⛔ AND IT IS GATED OFF, because the seam breaks the one law that matters:
arrival -> departure across a region boundary passes 9/16 seeds, against
11/16 same-region controls in the same harness. Density blending alone does
not guarantee a player can cross a seam. A strate that mixes archetypes and
strands you at the boundary is strictly worse than one that does not mix.

VF_LateralRegionsAreShippable() returns false and the test ASSERTS THE GATE
IS SHUT while the rate is imperfect -- so flipping it without fixing
connectivity turns the suite red by design. The rate is a warning, not an
assertion: it is the measured state of an off-by-default feature, not a
regression in a live world, and a permanently red suite stops being read,
which is how real failures get missed.

Everything that ships is still hard-guarded: single-region bit-identity,
zero cross-boundary box violations, and every shipping-path law test
untouched.

The fix is an explicit CROSS-SEAM CORRIDOR CONTRACT -- a design decision,
recorded rather than made unilaterally. Codex diagnosed this correctly and
declined to tune the threshold, which was the right call.

Build clean, 29 succeeded / 0 failed, run and verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-05 05:06:32 +02:00
1 parent 42e1f0ec8c
commit dd38832bb6
14 files changed
+3032 -40

No files matched your search

+51 -3
View File
@@ -32,12 +32,53 @@ negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated aft
the global rim cannot be reopened by a post-process; the diff layer remains the explicit player
override.
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
(macro `VF_ARCHETYPE_PARAMS_GETTER`) — no cross-boundary blend (Hard transitions between archetypes).
(macro `VF_ARCHETYPE_PARAMS_GETTER`) — the ordinary authored path has no cross-boundary blend
(Hard transitions between strates). The offline composer’s lateral manifest is the explicit
within-strate exception described in §8.1a; it blends evaluated density fields, not these params.
On top of the archetype, an optional **biome** layer (§8.14) modulates terrain & content WITHIN a
strate via a window-invariant XY field — currently wired into SurfaceWorld.
⚠️ **The `switch` above is no longer the only density path.** 6 of the 8 archetypes (all but
TunnelNetwork and Underwater) also exist as **operator stacks**, selected per strate by
### 8.1a Lateral regions (Tier 4d)
An archetype is now allowed to be a **spatial region inside one strate**, rather than a label that
describes the whole strate. The offline composer rolls one to three region records. Each record
owns its own native parameter vector and, for structure candidates, its own recipe; region zero is
also retained as the compatibility identity. A one-region record takes the existing stack path.
The partition is a pure function of `(world XY, world seed, absolute strate index)`. A 256-voxel
hash lattice places jittered sites (`VoxelHash::Cell`), each site receives a deterministic region
id, and the nearest-site assignment is Voronoi-like. Near a bisector, the nearest different-region
site supplies the neighbour; its weight is the same linear boundary curve used by the vertical
transition, split symmetrically so both sides meet at 0.5. The parent lerps the two **evaluated
internal densities**, never their parameter vectors: a shaft spacing and a slab parameter are not
commensurable. Internal density is positive solid; the parent is negated once at the MC boundary.
Region cores are built without structural posts. `FLateralRegionBlendOp` resolves the creative
field, then one parent appends the global spine, vertical seal, passage carve, and XY edge seal in
the existing fixed order. No region can disable or duplicate those posts. The manifest is an
offline artifact and the editor hand-off copies it into a worker-local per-chunk cache: the chunk
prepares a small lattice-site window plus an integer XY query grid, so ordinary voxel evaluation is
an O(1) lookup rather than a seed search or allocation. Fractional gradient probes reuse the same
prepared site window.
The lateral `ClassifyBox` proof is deliberately one-sided. It uses a certified nearest-different
site from the bounded local lattice window plus a 2-Lipschitz bound over the box's XY radius; only
a strict lower bound greater than the blend width proves that one region and no blend band can touch
the box. If the bounded query cannot certify that neighbor, proof is refused. Every other box asks
every region stack; any `Mixed`, disagreement, or unknown result remains `Mixed`. Convex density blending means
that equal all-air/all-solid signs are safe, but no cross-region uniformity is inferred from a
single sample. The same key is checked when a season manifest is loaded, and `WorldRadiusVoxels`
remains zero during offline materialisation.
This mechanism is implemented and instrumented, but not ready to promote: the seam-specific
16-seed passage-mouth audit currently reports **9/16** connected opposite-region pairs, while the
exact region-zero controls report **11/16**. The same cases are **16/16 valid, non-vacuous, and
above the 0.50 largest-component survival threshold** before the law gate. No corridor or
threshold tuning was added. The failed primordial-law gate is therefore retained as a design
blocker; see `COMPOSER-NOTES.md §3.2b` and the validation log in §10.
⚠️ **The `switch` above is no longer the only density path.** All 8 archetypes also exist as
**operator stacks**, selected per strate by
`bUseOperatorStack` and evaluated instead of the `switch`; each is bit-identical to the function in
its row. The design lives in `OPSTACK-PLAN.md` / `OPSTACK-DECOMPOSITION.md`, the symbol index in
`CODEMAP §3.2d` — not repeated here. What matters for *this* document: the archetype table describes
@@ -392,6 +433,13 @@ driven by `EditorBrush*` props.
the stack entry. `ValidateChannelOrder` is assembly/diagnostic-only; no declaration virtual may
enter `Eval`'s per-voxel loop. If `FVoxelOpSample` gains a field, add its channel bit and update
every operator declaration and the validator together.
- **Lateral region lookup/blend** (§8.1a): `FVoxelStrateRegionPartitionCache::PrepareForChunk`
builds the bounded jittered-site window and a `(CHUNK_SIZE+3)^2` integer query grid once per
chunk. Integer voxel samples read that grid; fractional gradient probes use the prepared site
window. `FLateralRegionBlendOp` evaluates one creative stack in a region interior and a second
only when the cached gap enters the blend band. The parent appends the four global structural
posts once. Do not move `VoxelHash` site gathering, `TArray` construction, or region-stack
discovery into `Eval`; the measured band overhead is an explicit Tier 4d budget item.
- **Isolated box proofs** (`FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval`): the box fold carries
the SDF interval produced so far. An SDF-only source publishes only its own interval; a later
converter or detail op applies its own state-aware response. Unknown or invalid bounds force the
+16
View File
@@ -173,6 +173,22 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
| `VoxelDensityOps::BuildFloatingIslandStack` | — | 8 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 8-op Maze stack (4 structural posts included). 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`. |
#### Lateral region parent (Tier 4d)
`VF_RollStrateRegionManifest` rolls a deterministic count of 1–3 regions and gives each region its
own native parameter vector and, for structure candidates, its own recipe. `VF_QueryStrateRegion`
assigns XY through jittered `VoxelHash::Cell` sites on a 256-voxel lattice; the pure query is keyed
by `(WorldXY, Seed, StrateIndex)` and the worker-side `FVoxelStrateRegionPartitionCache` prepares
the site window and integer query grid once per chunk. `FLateralRegionBlendOp` evaluates the primary
region core and, inside a 24-voxel band, the nearest different-region core, then lerps **density**
(not native parameters). The four structural posts are appended once to the parent after the
blend. Count==1 uses the ordinary one-stack path and is covered by native and recipe bit-identity
oracles. `ClassifyBox` proves a single-region/non-band box only from a conservative Lipschitz gap
bound; otherwise it folds every region and returns `Mixed` on any disagreement or unknown. The
runtime/editor override reads the immutable manifest per chunk; it never searches lattice seeds per
voxel. The dedicated `VoxelForge.Composer.LateralRegions` test reports cross-boundary brute-force
box samples, interior-vs-band timing, 16 opposite-region passage-mouth trials, and a saved preview.
### 3.2e Height-space operators — `Public/VoxelHeightOp.h` + `Private/VoxelHeightOpStack.cpp`
⚠️ **Feeds nothing yet** — built and exercised only by `VoxelForge.OpStack.SurfaceHeightEquivalence`.
**A SECOND op family, and it exists for a reason worth knowing:** SurfaceWorld's terrain ops (cliff /
+74 -1
View File
@@ -169,7 +169,8 @@ which it writes**.
sort of that DAG is a legal stack** — no authored order, and it produces orderings nobody wrote down.
**Built status — 2026-09-04:** `EVoxelOpChannel` now names the two fields actually carried by
`FVoxelOpSample` (`Density` and `Sdf`); all 29 concrete density operators declare reads, writes, and
`FVoxelOpSample` (`Density` and `Sdf`); all 30 concrete density operators (including the lateral
parent combiner) declare reads, writes, and
additive/transformative behavior. `FVoxelOpStack::Add` snapshots that metadata, and
`ValidateChannelOrder` checks the resulting channel DAG without entering the voxel loop.
`VoxelForge.OpStack.ChannelDAG` builds all eight shipping stacks and validates them: **22 tests
@@ -255,6 +256,63 @@ through the offline Tier 2 sampler; no runtime generation path calls this API. T
test took **76.466 s**. The complete `VoxelForge` namespace then passed **24/24 tests (0 failed,
0 not run)** in **339.797 s**.
### 3.2b Lateral regions — implemented, primordial-law gate failed (2026-09-05)
The owner's “tunnel network leading to a big chamber and drops into shafts” question changes the
unit of composition: an archetype is a **region inside a strate**, not the strate's label. Tier 4d
therefore adds `FVoxelStrateRegionManifest`. The composer rolls 1–3 region records by a salted
`VoxelHash::Cell` identity. Every region carries its own native parameter vector and, for structure
rolls, its own recipe. The partition is a jittered 256-voxel XY lattice with nearest-site,
nearest-different-site Voronoi assignment. Its identity is pure `(XY, seed, absolute strate index)`;
rekeying is explicit when a candidate moves from an attempt index into a live slot.
The lateral junction mirrors the vertical transition shape but blends the result at the right
level. In a region core, one stack evaluates. Within the 24-voxel boundary band, the nearest
different region's stack evaluates too and the two **internal densities** are crossfaded with the
same linear curve used by the vertical transition (split symmetrically at the bisector). Native
parameters are never blended: `ShaftSpacing` and a slab's floor/ceiling controls do not share a
meaning. Region cores omit structural posts; the parent appends the global spine, vertical seal,
passage carve, and XY edge seal once, in the existing order, after the lateral result. `Density`
keeps the stack convention positive=solid and is negated only at the MC-facing boundary.
The runtime hand-off remains an explicit manifest copy, not a new runtime composer. A worker
prepares a small lattice-site halo and `(CHUNK_SIZE+3)^2` integer query grid per chunk; normal
integer voxel samples use O(1) cached region lookup, and fractional gradient probes reuse the
prepared site set. `ValidateChannelOrder` still runs for every region core and the parent, and
`WorldRadiusVoxels` remains zero during offline materialisation.
The box proof is intentionally conservative. A box is allowed to use one child only when the
centre's bounded-neighborhood nearest-site gap is known and, minus twice the XY half-diagonal, is
**strictly greater** than the blend width. If the local lattice window cannot certify that nearest
different-region site, the proof is refused. Every other box evaluates every region's `ClassifyBox`;
any `Mixed`, disagreement, boundary, or unproved band remains `Mixed`. The dedicated audit exercised 40 boxes, including 20 boxes that
actually touched more than one region, brute-forced **199,800** integer samples, and found **0
false-uniform violations**. All 20 cross-boundary verdicts were `Mixed`; no cross-boundary uniform
claim was accepted.
The performance audit at width 24 measured **10,517/65,536 = 16.0477%** band voxels over the
Z-independent XY field. With chunk preparation excluded and the partition cache hot, the latest
focused run was **338.11 ns/voxel** in an interior and **663.12 ns/voxel** in a band (**1.961×**).
That is the expected cost of evaluating a second stack, and it is an explicit §8.10 budget item;
if this
feature proceeds, a narrower band or a cheaper density junction needs an owner-approved design
decision rather than an unreported performance regression.
The primordial law is the blocker. The test deliberately collected **16 actual opposite-region
arrival/departure mouth pairs**, without adding a corridor, changing a threshold, or filtering for
success. The lateral parent connected **9/16 (56.25%)**. The same exact region-zero recipe/vector
and structural settings in a one-region control connected **11/16**, so the underlying structure
roll is itself not universally connected; that control does not excuse the lateral failure. The
same 16 seam cases were **valid 16/16, non-vacuous 16/16, and largest-component share ≥ 0.50
16/16**; including the unsnapped seam law, full hard-gate survival is therefore **9/16**. The
result is not ready for promotion. A future fix must be a design decision such as a guaranteed
cross-seam corridor/landing contract, not a hidden pass threshold or seed selection.
The rendered review artifact is
`Saved/ComposerPreview/LateralRegions_seed_12001/index.html` (vertical/plan images and contour
variants). It is intentionally retained even though the law test is red, so a measured seam can be
looked at rather than trusted.
### 3.2a Editor walk-through hand-off (not a runtime composer)
AVoxelWorld::ApplyComposerCandidate is the one editor-only bridge from this offline API to a live
@@ -1229,6 +1287,21 @@ same seed twice emitted byte-identical JSON. The selected-only descent review wa
manifest. The run also kept all existing equivalence, determinism, box-verdict, and connectivity
tests green.
**2026-09-05 — Tier 4d lateral-region audit.** The specified build succeeded after adding the
per-chunk partition cache, density combiner, conservative box fold, season round-trip fields, and
the editor preview hand-off. The dedicated test intentionally remains red on the primordial-law
gate: it collected **16 opposite-region mouth pairs** and connected **9/16 (56.25%)**; the same
region-zero controls connected **11/16**. This is evidence, not a tuned threshold. The box audit
covered **40** boxes (**20 cross-boundary**), brute-forced **199,800** integer samples, and found
**0** false-uniform violations; all cross-boundary verdicts were `Mixed`. At blend width **24**,
**10,517/65,536 = 16.0477%** of sampled XY voxels were in a band; cached interior timing was
**338.11 ns/voxel**, band timing **663.12 ns/voxel**, or **1.961×**. The multi-region preview was
written to `Saved/ComposerPreview/LateralRegions_seed_12001/index.html`. On those same 16 seam
cases, survival before the law gate was **valid 16/16, non-vacuous 16/16, largest-component share
≥ 0.50 16/16**, and full hard-gate survival was **9/16**. No corridor, threshold change, or
successful-seed filter was introduced; lateral regions are not ready for promotion until the seam
law has a design-level fix.
---
## 11. Tier 1 status — passages aim at real open space (bounded full-point query; build not run)
File diff suppressed because it is too large. Load diff
+479 -14
View File
@@ -4886,6 +4886,129 @@ namespace
uint32 SeedU;
};
//=============================================================================
// RÔLE 2 — COMBINER : RÉGIONS LATÉRALES
//=============================================================================
// The creative stacks stay independent. This op owns only their density-level junction;
// the parent FVoxelOpStack appends the global structural posts after it. In particular, no
// recipe can make the spine, vertical seal, passage carve, or XY edge seal region-local.
class FLateralRegionBlendOp final : public IVoxelDensityOp
{
public:
FLateralRegionBlendOp(const FVoxelStrateRegionManifest& InManifest,
TArray<FVoxelOpStack>&& InStacks)
: RegionStacks(MoveTemp(InStacks))
{
// Keep only partition metadata here. The native vectors/recipes have already been
// consumed by the stacks and must not be duplicated in the voxel operator.
PartitionManifest.bValid = true;
PartitionManifest.Seed = InManifest.Seed;
PartitionManifest.StrateIndex = InManifest.StrateIndex;
PartitionManifest.RegionCount = InManifest.RegionCount;
PartitionManifest.PartitionSeed = InManifest.PartitionSeed;
PartitionManifest.LatticeCellSize = InManifest.LatticeCellSize;
PartitionManifest.BlendWidth = InManifest.BlendWidth;
PartitionManifest.Regions.SetNum(InManifest.RegionCount);
}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::Combiner; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext& Ctx) override
{
PartitionCache.PrepareForChunk(PartitionManifest, Ctx.ChunkCoord);
bPrepared = true;
for (FVoxelOpStack& Stack : RegionStacks)
{
Stack.PrepareChunk(Ctx);
}
}
void Eval(float WorldX, float WorldY, float WorldZ,
FVoxelOpSample& InOut) const override
{
if (RegionStacks.Num() == 0) { return; }
const FVoxelStrateRegionQuery Query = bPrepared
? PartitionCache.Query(WorldX, WorldY)
: VF_QueryStrateRegion(PartitionManifest, WorldX, WorldY);
const int32 Primary = FMath::Clamp(Query.PrimaryRegion, 0, RegionStacks.Num() - 1);
const float PrimaryDensity = RegionStacks[Primary].EvalInternal(
WorldX, WorldY, WorldZ);
float Density = PrimaryDensity;
if (Query.NeighborWeight > 0.0f && Query.NeighborRegion != INDEX_NONE
&& RegionStacks.IsValidIndex(Query.NeighborRegion)
&& Query.NeighborRegion != Primary)
{
const float NeighborDensity = RegionStacks[Query.NeighborRegion].EvalInternal(
WorldX, WorldY, WorldZ);
Density = FMath::Lerp(PrimaryDensity, NeighborDensity, Query.NeighborWeight);
}
InOut.Density = Density;
}
/**
* A box wholly outside the band can use its one creative stack. A box that may touch a
* bisector or the band asks every region stack. If any is Mixed, the parent is Mixed. If
* all possible stacks agree, their convex density blend has the same sign everywhere, so
* the blend band is proved uniform too. This is deliberately stricter than sampling a
* center point and is the protection against a cross-region false uniform tile.
*/
EVoxelTileClass ClassifyBox(const FBox& VoxelBox,
const FVoxelOpContext& Ctx) const override
{
if (RegionStacks.Num() == 0) { return EVoxelTileClass::Mixed; }
const FVoxelStrateRegionBoxProof Proof =
VF_AnalyzeStrateRegionBox(PartitionManifest, VoxelBox);
if (Proof.bProvablySingleRegion)
{
const FVoxelStrateRegionQuery Query = VF_QueryStrateRegion(
PartitionManifest,
((float)VoxelBox.Min.X + (float)VoxelBox.Max.X) * 0.5f,
((float)VoxelBox.Min.Y + (float)VoxelBox.Max.Y) * 0.5f);
const int32 Primary = FMath::Clamp(
Query.PrimaryRegion, 0, RegionStacks.Num() - 1);
return RegionStacks[Primary].ClassifyBox(VoxelBox, Ctx);
}
EVoxelTileClass CommonVerdict = EVoxelTileClass::Mixed;
for (const FVoxelOpStack& Stack : RegionStacks)
{
const EVoxelTileClass Verdict = Stack.ClassifyBox(VoxelBox, Ctx);
if (Verdict == EVoxelTileClass::Mixed)
{
return EVoxelTileClass::Mixed;
}
if (CommonVerdict == EVoxelTileClass::Mixed)
{
CommonVerdict = Verdict;
}
else if (CommonVerdict != Verdict)
{
return EVoxelTileClass::Mixed;
}
}
return CommonVerdict;
}
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
{
return EVoxelOpEffect::Both;
}
const TCHAR* DebugName() const override { return TEXT("LateralRegionBlendOp"); }
private:
FVoxelStrateRegionManifest PartitionManifest;
TArray<FVoxelOpStack> RegionStacks;
mutable FVoxelStrateRegionPartitionCache PartitionCache;
mutable bool bPrepared = false;
};
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
// Même piège que dans VoxelHeightOpStack.cpp : s'ancrer sur une bannière située plus bas
// (« FVoxelOpStack », « FABRIQUES ») insère la classe HORS du namespace anonyme, et l'accolade
@@ -5249,7 +5372,8 @@ namespace VoxelDensityOps
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams,
TUniquePtr<IVoxelBiomeField> BiomeField)
TUniquePtr<IVoxelBiomeField> BiomeField,
bool bAppendStructuralPosts)
{
// ARCHÉTYPE COMPLET depuis l'étape 2c : vide + overhang + mélange de biomes.
// `PerBiomeParams` vide ⇒ chemin sans biomes, strictement inchangé.
@@ -5262,12 +5386,17 @@ namespace VoxelDensityOps
// motif que cliff → structural : un modificateur qui a besoin de ce que la source a produit.
OutStack.Add(MakeUnique<FOverhangShelfMod>(P, Seed, ColumnPtr));
if (bAppendStructuralPosts)
{
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager);
}
}
void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que
// `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que
@@ -5276,12 +5405,17 @@ namespace VoxelDensityOps
OutStack.Add(MakeSlabVoidSource(P, Seed));
OutStack.Add(MakeGridColumnMod(P, Seed));
if (bAppendStructuralPosts)
{
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager);
}
}
void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq
// opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** —
@@ -5308,12 +5442,17 @@ namespace VoxelDensityOps
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
if (bAppendStructuralPosts)
{
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager);
}
}
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE.
// Portés : échelle verticale, roc de base, warp, graphe de salles (+ pits + cheminées),
@@ -5384,12 +5523,17 @@ namespace VoxelDensityOps
// different SDF writer assembled by the composer.
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed));
if (bAppendStructuralPosts)
{
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager);
}
}
void BuildFloatingIslandStack(FVoxelOpStack& OutStack, const FFloatingIslandParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// ⚠️ LA PILE QUI S'INVERSE, et c'est la mesure que ce portage-ci ajoute : les quatre autres
// archétypes partent de ROC et CREUSENT ; celui-ci part du VIDE et REMPLIT. Aucune des deux
@@ -5411,12 +5555,17 @@ namespace VoxelDensityOps
P.SurfaceRoughness + BlendK + 2.0f));
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
if (bAppendStructuralPosts)
{
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager);
}
}
void BuildMazeStack(FVoxelOpStack& OutStack, const FMazeGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// Les constantes viennent telles quelles de GetMazeDensity — elles y étaient codées en dur.
constexpr float CarveBlend = 2.0f;
@@ -5434,8 +5583,12 @@ namespace VoxelDensityOps
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, RoughFrequency, RoughOctaves, RoughApplyWithin));
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
if (bAppendStructuralPosts)
{
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager);
}
}
}
@@ -5876,7 +6029,8 @@ bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError)
FString* OutError,
bool bAppendStructuralPosts)
{
auto Fail = [&](const FString& Reason) -> bool
{
@@ -5971,8 +6125,13 @@ bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
OutContext.StrateBottomWorldZ = Bottom;
// This is the only place a recipe can acquire world-law operators. There is no recipe field
// for posts, so a serialised manifest cannot forget them or reorder them.
// for posts, so a serialised manifest cannot forget them or reorder them. Lateral callers
// materialise this same creative recipe with posts disabled, then append the global post set
// to the parent stack below.
if (bAppendStructuralPosts)
{
Candidate.AppendStructuralPost(Top, Bottom, Seal, Base, SpineRadius, StrateManager);
}
FString ValidationError;
if (!Candidate.ValidateChannelOrder(&ValidationError))
@@ -5984,3 +6143,309 @@ bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
OutStack = MoveTemp(Candidate);
return true;
}
namespace
{
static EVoxelStrateParamBlock VF_RegionParamBlock(ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber: return EVoxelStrateParamBlock::Slab;
case ECaveGeneratorType::Maze: return EVoxelStrateParamBlock::Maze;
case ECaveGeneratorType::SurfaceWorld: return EVoxelStrateParamBlock::Surface;
case ECaveGeneratorType::VerticalShafts: return EVoxelStrateParamBlock::VerticalShaft;
case ECaveGeneratorType::FloatingIslands:return EVoxelStrateParamBlock::FloatingIsland;
case ECaveGeneratorType::Underwater:
case ECaveGeneratorType::TunnelNetwork:
default: return EVoxelStrateParamBlock::TunnelNetwork;
}
}
static float VF_RegionTop(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.StrateTopWorldZ;
}
}
static float VF_RegionBottom(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.StrateBottomWorldZ;
}
}
static float VF_RegionSeal(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BoundarySealThickness;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BoundarySealThickness;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BoundarySealThickness;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BoundarySealThickness;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BoundarySealThickness;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.BoundarySealThickness;
}
}
static float VF_RegionBase(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BaseDensity;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BaseDensity;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BaseDensity;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BaseDensity;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BaseDensity;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.BaseDensity;
}
}
static bool VF_BuildNativeRegionCore(
const FVoxelStrateRegion& Region,
const FVoxelStrateArchetypeParams& Params,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext)
{
switch (Region.Archetype)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
if (Params.SlabParams.StrateTopWorldZ - Params.SlabParams.StrateBottomWorldZ <= 0.0f)
{
return false;
}
OutContext.StrateTopWorldZ = Params.SlabParams.StrateTopWorldZ;
OutContext.StrateBottomWorldZ = Params.SlabParams.StrateBottomWorldZ;
VoxelDensityOps::BuildSlabStack(OutStack, Params.SlabParams, Region.Seed,
SpineRadius, StrateManager, bAppendStructuralPosts);
return true;
case ECaveGeneratorType::Maze:
if (Params.MazeParams.StrateTopWorldZ - Params.MazeParams.StrateBottomWorldZ <= 0.0f)
{
return false;
}
OutContext.StrateTopWorldZ = Params.MazeParams.StrateTopWorldZ;
OutContext.StrateBottomWorldZ = Params.MazeParams.StrateBottomWorldZ;
VoxelDensityOps::BuildMazeStack(OutStack, Params.MazeParams, Region.Seed,
SpineRadius, StrateManager, bAppendStructuralPosts);
return true;
case ECaveGeneratorType::SurfaceWorld:
if (Params.SurfaceParams.StrateTopWorldZ - Params.SurfaceParams.StrateBottomWorldZ <= 0.0f)
{
return false;
}
OutContext.StrateTopWorldZ = Params.SurfaceParams.StrateTopWorldZ;
OutContext.StrateBottomWorldZ = Params.SurfaceParams.StrateBottomWorldZ;
VoxelDensityOps::BuildSurfaceStack(
OutStack, Params.SurfaceParams, Region.Seed, SpineRadius, StrateManager,
TArray<FSurfaceGenerationParams>(), nullptr, bAppendStructuralPosts);
return true;
case ECaveGeneratorType::VerticalShafts:
if (Params.VerticalShaftParams.StrateTopWorldZ
- Params.VerticalShaftParams.StrateBottomWorldZ <= 0.0f)
{
return false;
}
OutContext.StrateTopWorldZ = Params.VerticalShaftParams.StrateTopWorldZ;
OutContext.StrateBottomWorldZ = Params.VerticalShaftParams.StrateBottomWorldZ;
VoxelDensityOps::BuildVerticalShaftStack(
OutStack, Params.VerticalShaftParams, Region.Seed,
SpineRadius, StrateManager, bAppendStructuralPosts);
return true;
case ECaveGeneratorType::FloatingIslands:
if (Params.FloatingIslandParams.StrateTopWorldZ
- Params.FloatingIslandParams.StrateBottomWorldZ <= 0.0f)
{
return false;
}
OutContext.StrateTopWorldZ = Params.FloatingIslandParams.StrateTopWorldZ;
OutContext.StrateBottomWorldZ = Params.FloatingIslandParams.StrateBottomWorldZ;
VoxelDensityOps::BuildFloatingIslandStack(
OutStack, Params.FloatingIslandParams, Region.Seed,
SpineRadius, StrateManager, bAppendStructuralPosts);
return true;
case ECaveGeneratorType::Underwater:
case ECaveGeneratorType::TunnelNetwork:
OutContext.StrateTopWorldZ = Params.TunnelNetworkParams.StrateTopWorldZ;
OutContext.StrateBottomWorldZ = Params.TunnelNetworkParams.StrateBottomWorldZ;
VoxelDensityOps::BuildTunnelNetworkStack(
OutStack, Params.TunnelNetworkParams, Region.Seed,
SpineRadius, StrateManager, bAppendStructuralPosts);
return true;
default:
return false;
}
}
}
bool VF_BuildStrateRegionStack(
const FVoxelStrateRegionManifest& Manifest,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError)
{
auto Fail = [&](const FString& Reason) -> bool
{
if (OutError != nullptr) { *OutError = Reason; }
return false;
};
if (OutError != nullptr) { OutError->Reset(); }
if (!Manifest.IsValid())
{
return Fail(Manifest.FailureReason.IsEmpty()
? TEXT("lateral region manifest is invalid") : Manifest.FailureReason);
}
if (Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
return Fail(TEXT("lateral region partition seed is stale for its seed/strate index"));
}
EVoxelStrateParamBlock GlobalBlock = Manifest.StructuralParamBlock;
if (GlobalBlock == EVoxelStrateParamBlock::None)
{
GlobalBlock = Manifest.Regions[0].bUsesRecipe
? Manifest.Regions[0].Recipe.StructuralParamBlock
: VF_RegionParamBlock(Manifest.Regions[0].Archetype);
}
FVoxelStrateArchetypeParams FirstParams = Manifest.Regions[0].ArchetypeParams;
float Top = Manifest.bHasGlobalStructuralParams
? Manifest.StrateTopWorldZ : VF_RegionTop(FirstParams, GlobalBlock);
float Bottom = Manifest.bHasGlobalStructuralParams
? Manifest.StrateBottomWorldZ : VF_RegionBottom(FirstParams, GlobalBlock);
float Seal = Manifest.bHasGlobalStructuralParams
? Manifest.BoundarySealThickness : VF_RegionSeal(FirstParams, GlobalBlock);
float Base = Manifest.bHasGlobalStructuralParams
? Manifest.BaseDensity : VF_RegionBase(FirstParams, GlobalBlock);
if (!FMath::IsFinite(Top) || !FMath::IsFinite(Bottom) || !(Top > Bottom)
|| !FMath::IsFinite(Seal) || Seal < 0.0f
|| !FMath::IsFinite(Base) || !(Base > 0.0f))
{
return Fail(TEXT("lateral region structural parameters are not a finite positive strate"));
}
auto MakeRegionParams = [&](const FVoxelStrateRegion& Region)
{
FVoxelStrateArchetypeParams Params = Region.ArchetypeParams;
VF_SetStrateArchetypeRuntimeBounds(Params, Top, Bottom);
return Params;
};
// The one-region form remains a normal stack. It is useful to callers that consume a region
// manifest, while production keeps its old branch entirely untouched for the hard identity
// gate.
if (Manifest.RegionCount == 1)
{
const FVoxelStrateRegion& Region = Manifest.Regions[0];
const FVoxelStrateArchetypeParams Params = MakeRegionParams(Region);
bool bBuilt = false;
if (Region.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Region.Recipe, Params, Region.Seed,
SpineRadius, StrateManager, OutStack,
OutContext, OutError, true);
}
else
{
OutContext = FVoxelOpContext();
OutContext.Seed = static_cast<uint32>(Region.Seed);
OutContext.LayoutVersion = StrateManager != nullptr
? StrateManager->GetLayoutVersion() : 0;
bBuilt = VF_BuildNativeRegionCore(Region, Params, SpineRadius, StrateManager,
true, OutStack, OutContext);
if (!bBuilt && OutError != nullptr)
{
*OutError = TEXT("single lateral region native stack could not be materialised");
}
}
if (!bBuilt) { return false; }
OutContext.WorldRadiusVoxels = 0.0f;
OutContext.EdgeSealThickness = Seal;
return true;
}
TArray<FVoxelOpStack> RegionStacks;
RegionStacks.Reserve(Manifest.RegionCount);
for (const FVoxelStrateRegion& Region : Manifest.Regions)
{
const FVoxelStrateArchetypeParams Params = MakeRegionParams(Region);
FVoxelOpStack Core;
FVoxelOpContext CoreContext;
bool bBuilt = false;
if (Region.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Region.Recipe, Params, Region.Seed,
SpineRadius, StrateManager, Core,
CoreContext, OutError, false);
}
else
{
CoreContext = FVoxelOpContext();
CoreContext.Seed = static_cast<uint32>(Region.Seed);
CoreContext.LayoutVersion = StrateManager != nullptr
? StrateManager->GetLayoutVersion() : 0;
bBuilt = VF_BuildNativeRegionCore(Region, Params, SpineRadius, StrateManager,
false, Core, CoreContext);
if (!bBuilt && OutError != nullptr)
{
*OutError = FString::Printf(TEXT("region %d native stack could not be materialised"),
Region.RegionIndex);
}
}
if (!bBuilt)
{
return false;
}
RegionStacks.Add(MoveTemp(Core));
}
FVoxelOpStack Parent;
Parent.Add(MakeUnique<FLateralRegionBlendOp>(Manifest, MoveTemp(RegionStacks)));
Parent.AppendStructuralPost(Top, Bottom, Seal, Base, SpineRadius, StrateManager);
FString ValidationError;
if (!Parent.ValidateChannelOrder(&ValidationError))
{
return Fail(FString::Printf(TEXT("lateral region parent failed ValidateChannelOrder: %s"),
*ValidationError));
}
OutContext = FVoxelOpContext();
OutContext.Seed = static_cast<uint32>(Manifest.Seed);
OutContext.LayoutVersion = StrateManager != nullptr
? StrateManager->GetLayoutVersion() : 0;
OutContext.WorldRadiusVoxels = 0.0f;
OutContext.EdgeSealThickness = Seal;
OutContext.StrateTopWorldZ = Top;
OutContext.StrateBottomWorldZ = Bottom;
OutStack = MoveTemp(Parent);
return true;
}
+129 -2
View File
@@ -741,9 +741,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// cache on the same versioned refetch as the native params; no worker reads mutable editor
// state while the world is applying a new candidate.
thread_local bool CP_UseComposerRecipe = false;
thread_local bool CP_UseComposerRegions = false;
thread_local int32 CP_ComposerSeed = 0;
thread_local FVoxelStrateArchetypeParams CP_ComposerParams;
thread_local FVoxelOpStackRecipe CP_ComposerRecipe;
thread_local FVoxelStrateRegionManifest CP_ComposerRegions;
#endif
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
@@ -763,9 +765,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
#if WITH_EDITOR
CP_UseComposerRecipe = false;
CP_UseComposerRegions = false;
ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork;
bool bComposerUseRecipe = false;
if (StrateManager->GetComposerOverrideForChunk(
if (StrateManager->GetComposerRegionOverrideForChunk(
ChunkCoord, CP_ComposerRegions))
{
CP_UseComposerRegions = true;
CP_ComposerSeed = CP_ComposerRegions.Seed;
if (CP_ComposerRegions.Regions.Num() > 0)
{
CP_GenType = CP_ComposerRegions.Regions[0].Archetype;
}
}
else if (StrateManager->GetComposerOverrideForChunk(
ChunkCoord, CP_ComposerSeed, ComposerArchetype, CP_ComposerParams,
bComposerUseRecipe, CP_ComposerRecipe))
{
@@ -804,11 +817,37 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est
// par chunk (comme le refetch de params juste au-dessus), jamais par voxel.
CP_UseOpStack = StrateManager->UsesOperatorStackForChunk(ChunkCoord);
#if WITH_EDITOR
// A composer override is already an explicit, validated stack description. It must
// be evaluated even when the authored definition had the normal operator-stack opt-in
// disabled; otherwise a multi-region candidate would be installed in the manager but
// silently fall back to the old one-archetype switch.
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions || CP_UseComposerRecipe;
#endif
if (CP_UseOpStack)
{
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
#if WITH_EDITOR
if (CP_UseComposerRecipe)
if (CP_UseComposerRegions)
{
FVoxelOpContext RegionContext;
if (VF_BuildStrateRegionStack(
CP_ComposerRegions, OriginSpineRadius, StrateManager,
CP_OpStack, RegionContext, nullptr))
{
RegionContext.ChunkCoord = ChunkCoord;
RegionContext.Step = 1;
RegionContext.LayoutVersion = LayoutVersion;
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RegionContext);
}
else
{
CP_UseOpStack = false;
}
}
else if (CP_UseComposerRecipe)
{
FVoxelOpContext ComposerContext;
if (VF_BuildStackFromRecipe(
@@ -2897,6 +2936,94 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (!StrateManager) return EVoxelTileClass::Mixed;
#if WITH_EDITOR
// A multi-region composer override is already a complete parent stack: it owns the lateral
// blend and the four global structural posts. Classify it as one field only when the complete
// mesher box stays inside that one strate; otherwise the Z-category pass below deliberately
// returns Mixed. Disturbances and the diff layer remain outside the stack, so either one also
// vetoes a skip.
FVoxelStrateRegionManifest ComposerRegions;
const FIntVector RegionProbeChunk(
FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), FloorDivC(MinZ, CHUNK_SIZE));
if (StrateManager->GetComposerRegionOverrideForChunk(RegionProbeChunk, ComposerRegions))
{
int32 RegionTopChunkZ = 0;
int32 RegionBottomChunkZ = 0;
const int32 RegionMinChunkZ = FloorDivC(MinZ, CHUNK_SIZE);
const int32 RegionMaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE);
if (!StrateManager->GetStrateChunkZBounds(RegionProbeChunk.Z,
RegionTopChunkZ, RegionBottomChunkZ)
|| RegionMinChunkZ < RegionBottomChunkZ
|| RegionMaxChunkZ > RegionTopChunkZ)
{
return EVoxelTileClass::Mixed;
}
// A representative chunk is not enough for a proof: the mesher box can cross an XY
// chunk edge, and disturbances are chunk-local. Require every touched chunk to remain
// in the same slot and to have no active disturbance before delegating the proof to the
// complete lateral stack. Any uncertainty is Mixed; a false uniform answer here can
// skip a real seam or carve.
const int32 RegionMinChunkX = FloorDivC(MinX, CHUNK_SIZE);
const int32 RegionMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE);
const int32 RegionMinChunkY = FloorDivC(MinY, CHUNK_SIZE);
const int32 RegionMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE);
for (int32 CZ = RegionMinChunkZ; CZ <= RegionMaxChunkZ; ++CZ)
{
int32 TouchedTopChunkZ = 0;
int32 TouchedBottomChunkZ = 0;
if (!StrateManager->GetStrateChunkZBounds(
CZ, TouchedTopChunkZ, TouchedBottomChunkZ)
|| TouchedTopChunkZ != RegionTopChunkZ
|| TouchedBottomChunkZ != RegionBottomChunkZ)
{
return EVoxelTileClass::Mixed;
}
for (int32 CY = RegionMinChunkY; CY <= RegionMaxChunkY; ++CY)
{
for (int32 CX = RegionMinChunkX; CX <= RegionMaxChunkX; ++CX)
{
const FIntVector TouchedChunk(CX, CY, CZ);
FVoxelStrateRegionManifest TouchedRegions;
if (!StrateManager->GetComposerRegionOverrideForChunk(
TouchedChunk, TouchedRegions)
|| TouchedRegions.StrateIndex != ComposerRegions.StrateIndex
|| TouchedRegions.Seed != ComposerRegions.Seed
|| TouchedRegions.RegionCount != ComposerRegions.RegionCount
|| TouchedRegions.PartitionSeed != ComposerRegions.PartitionSeed
|| TouchedRegions.LatticeCellSize != ComposerRegions.LatticeCellSize
|| TouchedRegions.BlendWidth != ComposerRegions.BlendWidth)
{
return EVoxelTileClass::Mixed;
}
const FStrateDisturbanceParams Disturbances =
StrateManager->GetDisturbanceParamsForChunk(TouchedChunk);
if (Disturbances.ChasmDensity > 0.0f
|| Disturbances.BridgeDensity > 0.0f
|| Disturbances.RidgeDensity > 0.0f)
{
return EVoxelTileClass::Mixed;
}
}
}
}
FVoxelOpStack RegionStack;
FVoxelOpContext RegionContext;
if (!VF_BuildStrateRegionStack(ComposerRegions, OriginSpineRadius,
StrateManager, RegionStack, RegionContext, nullptr))
{
return EVoxelTileClass::Mixed;
}
RegionContext.ChunkCoord = RegionProbeChunk;
RegionContext.Step = Step;
RegionContext.LayoutVersion = StrateManager->GetLayoutVersion();
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness;
RegionStack.PrepareChunk(RegionContext);
return RegionStack.ClassifyBox(TileVoxelBox, RegionContext);
}
// A custom recipe is deliberately not fed to the native ClassifyBox proof yet. Returning
// Mixed keeps the live walk-through hole-safe: GenerateMesh samples the exact recipe stack,
// while no optimistic AllAir/AllSolid result can skip a candidate tile.
@@ -1092,6 +1092,152 @@ namespace VoxelSeasonManifestPrivate
Root->SetStringField(TEXT("policy_note"), Manifest.PolicyNote);
}
TSharedPtr<FJsonObject> SerializeRegionManifest(
const FVoxelStrateRegionManifest& Regions)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetBoolField(TEXT("valid"), Regions.bValid);
Object->SetStringField(TEXT("failure_reason"), Regions.FailureReason);
Object->SetNumberField(TEXT("seed"), Regions.Seed);
Object->SetNumberField(TEXT("strate_index"), Regions.StrateIndex);
Object->SetNumberField(TEXT("region_count"), Regions.RegionCount);
Object->SetNumberField(TEXT("partition_seed"),
static_cast<double>(Regions.PartitionSeed));
SetMetricFloat(Object, TEXT("lattice_cell_size"), Regions.LatticeCellSize);
SetMetricFloat(Object, TEXT("blend_width"), Regions.BlendWidth);
Object->SetBoolField(TEXT("has_global_structural_params"),
Regions.bHasGlobalStructuralParams);
Object->SetNumberField(TEXT("structural_param_block"),
static_cast<int32>(Regions.StructuralParamBlock));
SetMetricFloat(Object, TEXT("strate_top_world_z"), Regions.StrateTopWorldZ);
SetMetricFloat(Object, TEXT("strate_bottom_world_z"), Regions.StrateBottomWorldZ);
SetMetricFloat(Object, TEXT("boundary_seal_thickness"),
Regions.BoundarySealThickness);
SetMetricFloat(Object, TEXT("base_density"), Regions.BaseDensity);
TArray<TSharedPtr<FJsonValue>> RegionValues;
for (const FVoxelStrateRegion& Region : Regions.Regions)
{
TSharedPtr<FJsonObject> RegionObject = MakeShared<FJsonObject>();
RegionObject->SetNumberField(TEXT("region_index"), Region.RegionIndex);
RegionObject->SetNumberField(TEXT("seed"), Region.Seed);
RegionObject->SetStringField(TEXT("archetype"), ArchetypeName(Region.Archetype));
RegionObject->SetNumberField(TEXT("archetype_id"),
static_cast<int32>(Region.Archetype));
RegionObject->SetBoolField(TEXT("uses_recipe"), Region.bUsesRecipe);
RegionObject->SetObjectField(TEXT("recipe"), SerializeRecipe(Region.Recipe));
RegionObject->SetObjectField(TEXT("parameters"),
SerializeAllParams(Region.ArchetypeParams));
RegionValues.Add(MakeShared<FJsonValueObject>(MoveTemp(RegionObject)));
}
Object->SetArrayField(TEXT("regions"), MoveTemp(RegionValues));
return Object;
}
bool ReadJsonUInt32(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, uint32& OutValue)
{
double Number = 0.0;
if (!ReadJsonNumber(Object, Name, Number)
|| !FMath::IsFinite(Number)
|| Number < 0.0 || Number > 4294967295.0
|| FMath::FloorToDouble(Number) != Number)
{
return false;
}
OutValue = static_cast<uint32>(Number);
return true;
}
bool DeserializeRegionManifest(const TSharedPtr<FJsonObject>& Object,
FVoxelStrateRegionManifest& OutRegions)
{
if (!Object.IsValid()) return false;
OutRegions = FVoxelStrateRegionManifest();
bool JsonValid = false;
int32 StructuralBlock = 0;
bool bFieldsValid = ReadJsonBool(Object, TEXT("valid"), JsonValid)
&& ReadJsonString(Object, TEXT("failure_reason"), OutRegions.FailureReason)
&& ReadJsonInt32(Object, TEXT("seed"), OutRegions.Seed)
&& ReadJsonInt32(Object, TEXT("strate_index"), OutRegions.StrateIndex)
&& ReadJsonInt32(Object, TEXT("region_count"), OutRegions.RegionCount)
&& ReadJsonUInt32(Object, TEXT("partition_seed"), OutRegions.PartitionSeed)
&& ReadMetricFloat(Object, TEXT("lattice_cell_size"), OutRegions.LatticeCellSize)
&& ReadMetricFloat(Object, TEXT("blend_width"), OutRegions.BlendWidth)
&& ReadJsonBool(Object, TEXT("has_global_structural_params"),
OutRegions.bHasGlobalStructuralParams)
&& ReadJsonInt32(Object, TEXT("structural_param_block"), StructuralBlock)
&& ReadMetricFloat(Object, TEXT("strate_top_world_z"), OutRegions.StrateTopWorldZ)
&& ReadMetricFloat(Object, TEXT("strate_bottom_world_z"),
OutRegions.StrateBottomWorldZ)
&& ReadMetricFloat(Object, TEXT("boundary_seal_thickness"),
OutRegions.BoundarySealThickness)
&& ReadMetricFloat(Object, TEXT("base_density"), OutRegions.BaseDensity);
if (StructuralBlock < static_cast<int32>(EVoxelStrateParamBlock::None)
|| StructuralBlock > static_cast<int32>(EVoxelStrateParamBlock::FloatingIsland))
{
bFieldsValid = false;
}
OutRegions.StructuralParamBlock = static_cast<EVoxelStrateParamBlock>(StructuralBlock);
const TArray<TSharedPtr<FJsonValue>>* RegionValues = nullptr;
bFieldsValid = ReadJsonArray(Object, TEXT("regions"), RegionValues) && bFieldsValid;
if (RegionValues == nullptr
|| OutRegions.RegionCount < 1 || OutRegions.RegionCount > 3
|| RegionValues->Num() != OutRegions.RegionCount)
{
bFieldsValid = false;
}
OutRegions.Regions.Reset();
if (RegionValues != nullptr)
{
OutRegions.Regions.Reserve(RegionValues->Num());
for (int32 Index = 0; Index < RegionValues->Num(); ++Index)
{
const TSharedPtr<FJsonValue>& Value = (*RegionValues)[Index];
if (!Value.IsValid() || Value->Type != EJson::Object)
{
bFieldsValid = false;
continue;
}
const TSharedPtr<FJsonObject> RegionObject = Value->AsObject();
FVoxelStrateRegion& Region = OutRegions.Regions.AddDefaulted_GetRef();
FString ArchetypeText;
int32 ArchetypeId = -1;
TSharedPtr<FJsonObject> Recipe, Params;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("region_index"),
Region.RegionIndex) && bFieldsValid;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("seed"), Region.Seed)
&& bFieldsValid;
bFieldsValid = ReadJsonString(RegionObject, TEXT("archetype"), ArchetypeText)
&& bFieldsValid;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("archetype_id"), ArchetypeId)
&& bFieldsValid;
bFieldsValid = ReadJsonBool(RegionObject, TEXT("uses_recipe"),
Region.bUsesRecipe) && bFieldsValid;
bFieldsValid = ReadJsonObject(RegionObject, TEXT("recipe"), Recipe)
&& bFieldsValid;
bFieldsValid = ReadJsonObject(RegionObject, TEXT("parameters"), Params)
&& bFieldsValid;
bFieldsValid = ParseArchetype(ArchetypeText, Region.Archetype)
&& bFieldsValid;
bFieldsValid = Region.RegionIndex == Index && bFieldsValid;
bFieldsValid = ArchetypeId == static_cast<int32>(Region.Archetype)
&& bFieldsValid;
if (Region.bUsesRecipe)
{
bFieldsValid = DeserializeRecipe(Recipe, Region.Recipe) && bFieldsValid;
}
bFieldsValid = DeserializeAllParams(Params, Region.ArchetypeParams)
&& bFieldsValid;
}
}
OutRegions.bValid = JsonValid;
return bFieldsValid && OutRegions.IsValid();
}
TSharedPtr<FJsonObject> SerializeStrate(const FVoxelSeasonStrate& Strate)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
@@ -1104,8 +1250,16 @@ namespace VoxelSeasonManifestPrivate
Object->SetStringField(TEXT("archetype"), ArchetypeName(Strate.Archetype));
Object->SetNumberField(TEXT("archetype_id"), static_cast<int32>(Strate.Archetype));
Object->SetBoolField(TEXT("uses_recipe"), Strate.bUsesRecipe);
Object->SetBoolField(TEXT("uses_regions"), Strate.bUsesRegions);
Object->SetObjectField(TEXT("recipe"), SerializeRecipe(Strate.Recipe));
Object->SetObjectField(TEXT("parameters"), SerializeAllParams(Strate.Params));
// Keep old/single-region artifacts compact and on the exact legacy rebuild path. A
// multi-region strate carries the complete parent manifest because that is its runtime
// identity; an invalid default Regions object must not make fixed legacy entries unloadable.
if (Strate.bUsesRegions)
{
Object->SetObjectField(TEXT("regions"), SerializeRegionManifest(Strate.Regions));
}
Object->SetObjectField(TEXT("measured_metrics"), SerializeMetrics(Strate.Metrics));
Object->SetNumberField(TEXT("distance_from_corpus_centroid"), Strate.DistanceFromCorpusCentroid);
Object->SetStringField(TEXT("selection_reason"), Strate.SelectionReason);
@@ -1151,10 +1305,34 @@ namespace VoxelSeasonManifestPrivate
OutStrate.DistanceFromCorpusCentroid = Distance;
OutStrate.SelectionReason = Reason;
OutStrate.SourceDefinitionPath = SourcePath;
TSharedPtr<FJsonObject> Recipe, Params, Metrics;
TSharedPtr<FJsonObject> Recipe, Params, Metrics, RegionsObject;
bValid = ReadJsonObject(Object, TEXT("recipe"), Recipe) && bValid;
bValid = ReadJsonObject(Object, TEXT("parameters"), Params) && bValid;
bValid = ReadJsonObject(Object, TEXT("measured_metrics"), Metrics) && bValid;
const TSharedPtr<FJsonValue>* RegionsValue = Object->Values.Find(TEXT("regions"));
if (RegionsValue != nullptr)
{
bValid = ReadJsonObject(Object, TEXT("regions"), RegionsObject) && bValid;
if (RegionsObject.IsValid())
{
bValid = DeserializeRegionManifest(RegionsObject, OutStrate.Regions)
&& bValid;
}
}
const TSharedPtr<FJsonValue>* UsesRegionsValue =
Object->Values.Find(TEXT("uses_regions"));
if (UsesRegionsValue != nullptr)
{
bValid = ReadJsonBool(Object, TEXT("uses_regions"), OutStrate.bUsesRegions)
&& bValid;
}
else
{
// Schema-1 manifests predating lateral regions simply take the old path. A region
// object is optional for backward compatibility, and never implicitly changes that
// path without the explicit opt-in bit.
OutStrate.bUsesRegions = false;
}
bValid = ParseArchetype(ArchetypeText, OutStrate.Archetype) && bValid;
bValid = ArchetypeId == static_cast<int32>(OutStrate.Archetype) && bValid;
bValid = ParseConnectivityName(LawText, OutStrate.PrimordialLawResult) && bValid;
@@ -1164,6 +1342,11 @@ namespace VoxelSeasonManifestPrivate
}
bValid = DeserializeAllParams(Params, OutStrate.Params) && bValid;
bValid = DeserializeMetrics(Metrics, OutStrate.Metrics) && bValid;
if (OutStrate.bUsesRegions
&& (!OutStrate.Regions.IsValid() || OutStrate.Regions.RegionCount <= 1))
{
bValid = false;
}
return bValid;
}
@@ -1461,12 +1644,31 @@ bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
for (int32 Index = 0; Index < OutManifest.Strates.Num(); ++Index)
{
const FVoxelSeasonStrate& Strate = OutManifest.Strates[Index];
const float StoredTop = static_cast<float>(Strate.TopWorldZ);
const float StoredBottom = static_cast<float>(Strate.BottomWorldZ);
if (Strate.DepthIndex != Index || !Strate.bPassedPrimordialLaw
|| Strate.PrimordialLawResult != EVoxelConnectivityResult::Connected)
{
bFieldsValid = false;
break;
}
if (Strate.bUsesRegions
&& (!Strate.Regions.IsValid()
|| Strate.Regions.RegionCount <= 1
|| Strate.Regions.Seed != Strate.Seed
|| Strate.Regions.StrateIndex != Strate.DepthIndex
|| Strate.Regions.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Strate.Seed, Strate.DepthIndex)
|| !Strate.Regions.bHasGlobalStructuralParams
|| FMemory::Memcmp(&Strate.Regions.StrateTopWorldZ,
&StoredTop, sizeof(float)) != 0
|| FMemory::Memcmp(&Strate.Regions.StrateBottomWorldZ,
&StoredBottom, sizeof(float)) != 0)
)
{
bFieldsValid = false;
break;
}
}
if (!bFieldsValid)
{
@@ -1487,6 +1689,9 @@ namespace VoxelSeasonCompositionPrivate
float ActiveBottom(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
float ActiveSeal(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
void SetRuntimeBounds(FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
void SetRegionRuntimeBounds(FVoxelStrateRegionManifest& Regions,
int32 Seed, int32 StrateIndex,
float TopWorldZ, float BottomWorldZ);
const TCHAR* RejectionReasonForLaw(EVoxelConnectivityResult Result);
// These caps are policy/allocator guards, not gameplay limits. They keep a malformed build-box
@@ -1690,9 +1895,17 @@ namespace VoxelSeasonCompositionPrivate
OutStrate.Archetype = Candidate.Archetype;
OutStrate.Recipe = Candidate.Recipe;
OutStrate.Params = Candidate.ArchetypeParams;
OutStrate.Regions = Candidate.Regions;
OutStrate.bUsesRecipe = true;
OutStrate.bUsesRegions = Candidate.Regions.RegionCount > 1;
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
if (OutStrate.Regions.IsValid())
{
SetRegionRuntimeBounds(OutStrate.Regions, Candidate.Seed, CandidateIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
}
#endif
@@ -1712,11 +1925,19 @@ namespace VoxelSeasonCompositionPrivate
OutStrate.Archetype = Fixed.Archetype;
OutStrate.Recipe = Fixed.Recipe;
OutStrate.Params = Fixed.Params;
OutStrate.Regions = Fixed.Regions;
OutStrate.SourceDefinitionPath = Fixed.SourceDefinitionPath;
OutStrate.bFixed = true;
OutStrate.bUsesRecipe = Fixed.bUsesRecipe;
OutStrate.bUsesRegions = Fixed.bUsesRegions && Fixed.Regions.IsValid();
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
if (OutStrate.bUsesRegions)
{
SetRegionRuntimeBounds(OutStrate.Regions, Fixed.Seed, DepthIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
}
bool EvaluateStrate(const FVoxelSeasonStrate& Strate,
@@ -1739,7 +1960,13 @@ namespace VoxelSeasonCompositionPrivate
FVoxelOpContext Context;
FString BuildError;
bool bBuilt = false;
if (Strate.bUsesRecipe)
if (Strate.bUsesRegions)
{
bBuilt = VF_BuildStrateRegionStack(
Strate.Regions, Settings.OriginSpineRadius, nullptr,
Stack, Context, &BuildError);
}
else if (Strate.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Strate.Recipe, Strate.Params, Strate.Seed,
Settings.OriginSpineRadius, nullptr, Stack,
@@ -1960,6 +2187,21 @@ namespace VoxelSeasonCompositionPrivate
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
}
void SetRegionRuntimeBounds(FVoxelStrateRegionManifest& Regions,
int32 Seed, int32 StrateIndex,
float TopWorldZ, float BottomWorldZ)
{
VF_RekeyStrateRegionManifest(Regions, Seed, StrateIndex);
Regions.StrateTopWorldZ = TopWorldZ;
Regions.StrateBottomWorldZ = BottomWorldZ;
Regions.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : Regions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TopWorldZ, BottomWorldZ);
}
}
bool SameFloatBits(float A, float B)
{
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
@@ -2353,6 +2595,15 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
Settings.StrateHeightInChunks, TopWorldZ,
BottomWorldZ, Strate);
Strate.DepthIndex = DepthIndex;
if (Strate.bUsesRegions)
{
// Candidate probes use the attempt index only to make their provisional
// measurements independent. The persisted world is keyed by its absolute
// strate slot, so rekey before the final placement gate and measurement.
SetRegionRuntimeBounds(Strate.Regions, Strate.Seed, DepthIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
FSeasonEvaluation Evaluation;
if (!EvaluateStrate(Strate, Settings, Evaluation))
{
@@ -2592,6 +2843,21 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
const float StoredTop = static_cast<float>(Strate.TopWorldZ);
const float StoredBottom = static_cast<float>(Strate.BottomWorldZ);
if (Strate.bUsesRegions)
{
if (!Strate.Regions.IsValid()
|| Strate.Regions.RegionCount <= 1
|| Strate.Regions.Seed != Strate.Seed
|| Strate.Regions.StrateIndex != Strate.DepthIndex
|| Strate.Regions.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Strate.Seed, Strate.DepthIndex)
|| !Strate.Regions.bHasGlobalStructuralParams
|| !SameFloatBits(Strate.Regions.StrateTopWorldZ, StoredTop)
|| !SameFloatBits(Strate.Regions.StrateBottomWorldZ, StoredBottom))
{
return Fail(TEXT("Manifest lateral region description is invalid or not keyed to its slot."));
}
}
if (!SameFloatBits(ActiveTop(Strate.Params, Strate.Archetype), StoredTop)
|| !SameFloatBits(ActiveBottom(Strate.Params, Strate.Archetype), StoredBottom))
{
@@ -2599,7 +2865,12 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
}
bool bBuilt = false;
if (Strate.bUsesRecipe)
if (Strate.bUsesRegions)
{
bBuilt = VF_BuildStrateRegionStack(Strate.Regions, SpineRadius, StrateManager,
OutStack, OutContext, OutError);
}
else if (Strate.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Strate.Recipe, Strate.Params, Strate.Seed,
SpineRadius, StrateManager, OutStack, OutContext,
@@ -22,6 +22,7 @@
#include "VoxelStrateDefinition.h"
#include "VoxelTerrainOpDefinition.h"
#include "VoxelDensityOpStack.h"
#include "VoxelCaveMorphology.h"
#include "VoxelTypes.h"
#include <type_traits>
@@ -4361,6 +4362,358 @@ bool FVoxelStrateCorpus::SetMeasuredMetrics(
return false;
}
void VF_SetStrateArchetypeRuntimeBounds(
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ)
{
Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ;
Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ;
Params.SlabParams.StrateTopWorldZ = TopWorldZ;
Params.SlabParams.StrateBottomWorldZ = BottomWorldZ;
Params.MazeParams.StrateTopWorldZ = TopWorldZ;
Params.MazeParams.StrateBottomWorldZ = BottomWorldZ;
Params.SurfaceParams.StrateTopWorldZ = TopWorldZ;
Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ;
Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ;
Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ;
Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ;
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
}
namespace VoxelStrateRegionPrivate
{
constexpr uint32 PartitionSalt = 0x5245474Eu; // "REGN"
constexpr uint32 PositionSaltX = 0x584F4646u; // "XOFF"
constexpr uint32 PositionSaltY = 0x594F4646u; // "YOFF"
constexpr uint32 RegionSalt = 0x52494458u; // "RIDX"
static int32 CellAt(float World, float CellSize)
{
return FMath::FloorToInt(World / CellSize);
}
static FVoxelStrateRegionSite MakeSite(int32 CellX, int32 CellY,
int32 InRegionCount, uint32 InPartitionSeed,
float CellSize)
{
FVoxelStrateRegionSite Site;
Site.CellX = CellX;
Site.CellY = CellY;
const uint32 H = VoxelHash::Cell(CellX, CellY, InPartitionSeed ^ PartitionSalt);
const float JitterX = VoxelHash::ToFloatSigned(
VoxelHash::Mix(H ^ PositionSaltX)) * 0.35f;
const float JitterY = VoxelHash::ToFloatSigned(
VoxelHash::Mix(H ^ PositionSaltY)) * 0.35f;
Site.WorldX = (static_cast<float>(CellX) + 0.5f + JitterX) * CellSize;
Site.WorldY = (static_cast<float>(CellY) + 0.5f + JitterY) * CellSize;
const uint32 RegionHash = VoxelHash::Cell(
CellX, CellY, InPartitionSeed ^ RegionSalt);
Site.Region = InRegionCount > 1
? static_cast<int32>(RegionHash % static_cast<uint32>(InRegionCount)) : 0;
return Site;
}
static void GatherSites(float WorldX, float WorldY, int32 InRegionCount,
uint32 InPartitionSeed, float CellSize,
TArray<FVoxelStrateRegionSite>& OutSites)
{
OutSites.Reset();
if (!(CellSize > 0.0f) || !FMath::IsFinite(CellSize))
{
return;
}
const int32 CenterX = CellAt(WorldX, CellSize);
const int32 CenterY = CellAt(WorldY, CellSize);
OutSites.Reserve(25);
for (int32 DY = -2; DY <= 2; ++DY)
{
for (int32 DX = -2; DX <= 2; ++DX)
{
OutSites.Add(MakeSite(CenterX + DX, CenterY + DY,
InRegionCount, InPartitionSeed, CellSize));
}
}
}
static bool IsEarlierSite(const FVoxelStrateRegionSite& A,
const FVoxelStrateRegionSite& B)
{
if (A.CellX != B.CellX) { return A.CellX < B.CellX; }
return A.CellY < B.CellY;
}
static FVoxelStrateRegionQuery QuerySites(
const TArray<FVoxelStrateRegionSite>& Sites, int32 InRegionCount,
float BlendWidth, float CellSize, float WorldX, float WorldY)
{
FVoxelStrateRegionQuery Result;
if (InRegionCount <= 1 || Sites.Num() == 0)
{
return Result;
}
Result.NearestDifferentRegionGap = 0.0f;
const int32 CenterCellX = CellAt(WorldX, CellSize);
const int32 CenterCellY = CellAt(WorldY, CellSize);
int32 PrimaryIndex = INDEX_NONE;
int32 NeighborIndex = INDEX_NONE;
float PrimaryDistanceSq = FLT_MAX;
float NeighborDistanceSq = FLT_MAX;
for (int32 Index = 0; Index < Sites.Num(); ++Index)
{
const FVoxelStrateRegionSite& Site = Sites[Index];
if (FMath::Abs(Site.CellX - CenterCellX) > 2
|| FMath::Abs(Site.CellY - CenterCellY) > 2)
{
continue;
}
const float DX = WorldX - Site.WorldX;
const float DY = WorldY - Site.WorldY;
const float DistanceSq = DX * DX + DY * DY;
if (PrimaryIndex == INDEX_NONE
|| DistanceSq < PrimaryDistanceSq
|| (DistanceSq == PrimaryDistanceSq
&& IsEarlierSite(Site, Sites[PrimaryIndex])))
{
PrimaryIndex = Index;
PrimaryDistanceSq = DistanceSq;
}
}
const int32 PrimaryRegion = Sites[PrimaryIndex].Region;
for (int32 Index = 0; Index < Sites.Num(); ++Index)
{
if (FMath::Abs(Sites[Index].CellX - CenterCellX) > 2
|| FMath::Abs(Sites[Index].CellY - CenterCellY) > 2)
{
continue;
}
if (Sites[Index].Region == PrimaryRegion) { continue; }
const float DX = WorldX - Sites[Index].WorldX;
const float DY = WorldY - Sites[Index].WorldY;
const float DistanceSq = DX * DX + DY * DY;
if (NeighborIndex == INDEX_NONE
|| DistanceSq < NeighborDistanceSq
|| (DistanceSq == NeighborDistanceSq
&& IsEarlierSite(Sites[Index], Sites[NeighborIndex])))
{
NeighborIndex = Index;
NeighborDistanceSq = DistanceSq;
}
}
Result.PrimaryRegion = PrimaryRegion;
if (NeighborIndex == INDEX_NONE)
{
return Result;
}
const float NeighborDistance = FMath::Sqrt(FMath::Max(NeighborDistanceSq, 0.0f));
const float Gap = FMath::Max(
NeighborDistance
- FMath::Sqrt(FMath::Max(PrimaryDistanceSq, 0.0f)), 0.0f);
Result.NeighborRegion = Sites[NeighborIndex].Region;
Result.NearestDifferentRegionGap = Gap;
// A site outside the ±2-cell window is at least 2.15 cell lengths from a point in the
// center cell (site jitter is bounded to ±0.35 around the cell midpoint). A candidate
// within 2.0 cell lengths is therefore known to beat every omitted site. Otherwise the
// density path can conservatively use the candidate/zero weight, but box proof must not
// treat the local window as an infinite search.
Result.bNearestDifferentRegionKnown = FMath::IsFinite(CellSize)
&& CellSize > 0.0f
&& NeighborDistance <= 2.0f * CellSize;
if (!(BlendWidth > 0.0f))
{
return Result;
}
// The vertical transition is linear from 0 at the outer edge to 1 at the boundary. A
// Voronoi boundary is shared by two regions, so the same curve is split symmetrically:
// each side contributes 0.5 at the bisector and 0 outside the band.
const float Curve = FMath::Clamp(1.0f - Gap / BlendWidth, 0.0f, 1.0f);
Result.NeighborWeight = 0.5f * Curve;
Result.bInBlendBand = Result.NeighborWeight > 0.0f;
return Result;
}
static FVoxelStrateRegionQuery QueryWithGather(
float WorldX, float WorldY, int32 InRegionCount, uint32 InPartitionSeed,
float CellSize, float BlendWidth)
{
TArray<FVoxelStrateRegionSite> Sites;
GatherSites(WorldX, WorldY, InRegionCount, InPartitionSeed, CellSize, Sites);
return QuerySites(Sites, InRegionCount, BlendWidth, CellSize, WorldX, WorldY);
}
}
int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex)
{
return 1 + static_cast<int32>(VoxelHash::Cell(
StrateIndex, StrateIndex ^ 0x6D, static_cast<uint32>(Seed) ^ 0x524F4C4Cu) % 3u);
}
uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex)
{
return VoxelHash::Cell(StrateIndex, StrateIndex ^ 0x6D,
static_cast<uint32>(Seed) ^ 0x52454750u);
}
void VF_RekeyStrateRegionManifest(
FVoxelStrateRegionManifest& Manifest, int32 InSeed, int32 InStrateIndex)
{
Manifest.Seed = InSeed;
Manifest.StrateIndex = InStrateIndex;
Manifest.PartitionSeed = VF_GetStrateRegionPartitionSeed(InSeed, InStrateIndex);
}
FVoxelStrateRegionQuery VF_QueryStrateRegion(
const FVoxelStrateRegionManifest& Manifest, float WorldX, float WorldY)
{
// The retained seed is an offline artifact, not an alternate identity. Reject a stale or
// hand-edited partition key instead of silently evaluating a different world for the same
// (seed, strate-index) pair.
if (!Manifest.IsValid()
|| Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
return FVoxelStrateRegionQuery();
}
return VoxelStrateRegionPrivate::QueryWithGather(
WorldX, WorldY, Manifest.RegionCount, Manifest.PartitionSeed,
Manifest.LatticeCellSize, Manifest.BlendWidth);
}
void FVoxelStrateRegionPartitionCache::PrepareForChunk(
const FVoxelStrateRegionManifest& Manifest, const FIntVector& ChunkCoord)
{
RegionCount = Manifest.RegionCount;
PartitionSeed = Manifest.PartitionSeed;
LatticeCellSize = Manifest.LatticeCellSize;
BlendWidth = Manifest.BlendWidth;
BaseX = ChunkCoord.X * CHUNK_SIZE - 1;
BaseY = ChunkCoord.Y * CHUNK_SIZE - 1;
Dim = CHUNK_SIZE + 3;
Sites.Reset();
IntegerSamples.Reset();
if (!Manifest.IsValid()
|| Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
RegionCount = 1;
Dim = 0;
return;
}
const int32 MinCellX = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseX), LatticeCellSize) - 2;
const int32 MaxCellX = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseX + Dim - 1), LatticeCellSize) + 2;
const int32 MinCellY = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseY), LatticeCellSize) - 2;
const int32 MaxCellY = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseY + Dim - 1), LatticeCellSize) + 2;
Sites.Reserve((MaxCellX - MinCellX + 1) * (MaxCellY - MinCellY + 1));
for (int32 CY = MinCellY; CY <= MaxCellY; ++CY)
{
for (int32 CX = MinCellX; CX <= MaxCellX; ++CX)
{
Sites.Add(VoxelStrateRegionPrivate::MakeSite(
CX, CY, RegionCount, PartitionSeed, LatticeCellSize));
}
}
IntegerSamples.SetNum(Dim * Dim);
for (int32 Y = 0; Y < Dim; ++Y)
{
for (int32 X = 0; X < Dim; ++X)
{
IntegerSamples[Y * Dim + X] = VoxelStrateRegionPrivate::QuerySites(
Sites, RegionCount, BlendWidth, LatticeCellSize,
static_cast<float>(BaseX + X), static_cast<float>(BaseY + Y));
}
}
}
FVoxelStrateRegionQuery FVoxelStrateRegionPartitionCache::Query(
float WorldX, float WorldY) const
{
if (Dim > 0
&& WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY))
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
const int32 LocalX = IX - BaseX;
const int32 LocalY = IY - BaseY;
if (LocalX >= 0 && LocalX < Dim && LocalY >= 0 && LocalY < Dim)
{
return IntegerSamples[LocalY * Dim + LocalX];
}
}
// The production stack is prepared for the current chunk before its first sample. Reuse the
// prepared local site set for the rare fractional probe as well; rebuilding a lattice window
// here would turn a cache miss into a per-voxel allocation/hash walk. The prepared set has a
// one-voxel halo, which covers the mesher's gradient probes. The pure public query remains the
// fallback for an intentionally out-of-window diagnostic call.
const float PreparedMaxX = static_cast<float>(BaseX + Dim - 1);
const float PreparedMaxY = static_cast<float>(BaseY + Dim - 1);
if (Sites.Num() > 0 && Dim > 0
&& WorldX >= static_cast<float>(BaseX) && WorldX <= PreparedMaxX
&& WorldY >= static_cast<float>(BaseY) && WorldY <= PreparedMaxY)
{
return VoxelStrateRegionPrivate::QuerySites(
Sites, RegionCount, BlendWidth, LatticeCellSize, WorldX, WorldY);
}
return VoxelStrateRegionPrivate::QueryWithGather(
WorldX, WorldY, RegionCount, PartitionSeed,
LatticeCellSize, BlendWidth);
}
FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
const FVoxelStrateRegionManifest& Manifest, const FBox& VoxelBox)
{
FVoxelStrateRegionBoxProof Proof;
if (!Manifest.IsValid()
|| Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
return Proof;
}
if (Manifest.RegionCount <= 1)
{
Proof.bProvablySingleRegion = true;
Proof.bTouchesRegionBoundary = false;
Proof.bTouchesBlendBand = false;
return Proof;
}
const float HalfX = FMath::Max((float)(VoxelBox.Max.X - VoxelBox.Min.X) * 0.5f, 0.0f);
const float HalfY = FMath::Max((float)(VoxelBox.Max.Y - VoxelBox.Min.Y) * 0.5f, 0.0f);
const float Radius = FMath::Sqrt(HalfX * HalfX + HalfY * HalfY);
const float CenterX = ((float)VoxelBox.Min.X + (float)VoxelBox.Max.X) * 0.5f;
const float CenterY = ((float)VoxelBox.Min.Y + (float)VoxelBox.Max.Y) * 0.5f;
const FVoxelStrateRegionQuery Center = VF_QueryStrateRegion(Manifest, CenterX, CenterY);
if (!Center.bNearestDifferentRegionKnown)
{
return Proof;
}
const float GapLowerBound = Center.NearestDifferentRegionGap - 2.0f * Radius;
// The difference of two Euclidean distances is 2-Lipschitz. Strict inequalities are
// intentional: an equality may put a sample exactly on a bisector or at the blend cutoff,
// and that is not a proof a mesher is allowed to skip.
Proof.bTouchesRegionBoundary = !(GapLowerBound > 0.0f);
Proof.bTouchesBlendBand = !(GapLowerBound > Manifest.BlendWidth);
Proof.bProvablySingleRegion = GapLowerBound > Manifest.BlendWidth;
return Proof;
}
FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index)
{
@@ -4831,6 +5184,246 @@ FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index)
return Recipe;
}
namespace VoxelStrateRegionPrivate
{
static ECaveGeneratorType ArchetypeForRecipe(const FVoxelOpStackRecipe& Recipe)
{
switch (Recipe.StructuralParamBlock)
{
case EVoxelStrateParamBlock::TunnelNetwork: return ECaveGeneratorType::TunnelNetwork;
case EVoxelStrateParamBlock::Slab: return ECaveGeneratorType::FlatPlain;
case EVoxelStrateParamBlock::Maze: return ECaveGeneratorType::Maze;
case EVoxelStrateParamBlock::Surface: return ECaveGeneratorType::SurfaceWorld;
case EVoxelStrateParamBlock::VerticalShaft: return ECaveGeneratorType::VerticalShafts;
case EVoxelStrateParamBlock::FloatingIsland: return ECaveGeneratorType::FloatingIslands;
default: return ECaveGeneratorType::TunnelNetwork;
}
}
static EVoxelStrateParamBlock ParamBlockForArchetype(ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber: return EVoxelStrateParamBlock::Slab;
case ECaveGeneratorType::Maze: return EVoxelStrateParamBlock::Maze;
case ECaveGeneratorType::SurfaceWorld: return EVoxelStrateParamBlock::Surface;
case ECaveGeneratorType::VerticalShafts: return EVoxelStrateParamBlock::VerticalShaft;
case ECaveGeneratorType::FloatingIslands:return EVoxelStrateParamBlock::FloatingIsland;
case ECaveGeneratorType::Underwater:
case ECaveGeneratorType::TunnelNetwork:
default: return EVoxelStrateParamBlock::TunnelNetwork;
}
}
static void CopyParamBlock(FVoxelStrateArchetypeParams& OutParams,
const FVoxelStrateRollInfo& Roll,
ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams;
break;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
OutParams.SlabParams = Roll.ArchetypeParams.SlabParams;
break;
case ECaveGeneratorType::Maze:
OutParams.MazeParams = Roll.ArchetypeParams.MazeParams;
break;
case ECaveGeneratorType::SurfaceWorld:
OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
break;
case ECaveGeneratorType::VerticalShafts:
OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
break;
case ECaveGeneratorType::FloatingIslands:
OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
break;
default:
break;
}
}
static float BoundarySeal(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BoundarySealThickness;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BoundarySealThickness;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BoundarySealThickness;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BoundarySealThickness;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BoundarySealThickness;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.BoundarySealThickness;
}
}
static float BaseDensity(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BaseDensity;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BaseDensity;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BaseDensity;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BaseDensity;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BaseDensity;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.BaseDensity;
}
}
static float Top(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.StrateTopWorldZ;
}
}
static float Bottom(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.StrateBottomWorldZ;
}
}
}
bool VF_LateralRegionsAreShippable()
{
// ⛔ FALSE ON PURPOSE. See the declaration in VoxelStrateComposer.h for the full reasoning.
//
// FR : volontairement désactivé — la loi primordiale ne tient pas encore au travers d'une
// couture entre régions (9/16 graines mesurées le 2026-09-05).
//
// Measured: arrival -> departure across a region seam passes 9/16 seeds. A strate that mixes
// archetypes but strands the player at the boundary is worse than one that does not mix.
// Flipping this without an explicit cross-seam corridor contract turns the suite red by design.
return false;
}
FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 StrateIndex, bool bRollStructure)
{
FVoxelStrateRegionManifest Manifest;
Manifest.RegionCount = VF_RollStrateRegionCount(Seed, StrateIndex);
VF_RekeyStrateRegionManifest(Manifest, Seed, StrateIndex);
Manifest.Regions.Reserve(Manifest.RegionCount);
Manifest.bValid = true;
static const ECaveGeneratorType StructureBlocks[] =
{
ECaveGeneratorType::TunnelNetwork,
ECaveGeneratorType::FlatPlain,
ECaveGeneratorType::Maze,
ECaveGeneratorType::SurfaceWorld,
ECaveGeneratorType::VerticalShafts,
ECaveGeneratorType::FloatingIslands,
};
static const EVoxelStrateParamBlock BlockIds[] =
{
EVoxelStrateParamBlock::TunnelNetwork,
EVoxelStrateParamBlock::Slab,
EVoxelStrateParamBlock::Maze,
EVoxelStrateParamBlock::Surface,
EVoxelStrateParamBlock::VerticalShaft,
EVoxelStrateParamBlock::FloatingIsland,
};
static const uint32 BlockSalts[] = { 0x1001u, 0x1003u, 0x1005u,
0x1007u, 0x1009u, 0x100Bu };
for (int32 RegionIndex = 0; RegionIndex < Manifest.RegionCount; ++RegionIndex)
{
FVoxelStrateRegion& Region = Manifest.Regions.AddDefaulted_GetRef();
Region.RegionIndex = RegionIndex;
Region.Seed = RegionIndex == 0
? Seed
: static_cast<int32>(VoxelHash::Cell(
StrateIndex, RegionIndex, static_cast<uint32>(Seed) ^ 0x52454753u));
Region.bUsesRecipe = bRollStructure;
if (!bRollStructure)
{
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailed(
Corpus, Region.Seed, StrateIndex);
if (!Roll.bValid)
{
Manifest.bValid = false;
if (Manifest.FailureReason.IsEmpty())
{
Manifest.FailureReason = Roll.FailureReason;
}
continue;
}
Region.Archetype = Roll.Archetype;
Region.ArchetypeParams = Roll.ArchetypeParams;
}
else
{
Region.Recipe = VF_RollStrateStructure(Region.Seed, StrateIndex);
Region.Archetype = VoxelStrateRegionPrivate::ArchetypeForRecipe(Region.Recipe);
for (int32 BlockIndex = 0; BlockIndex < UE_ARRAY_COUNT(StructureBlocks); ++BlockIndex)
{
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailedForArchetype(
Corpus, StructureBlocks[BlockIndex],
Region.Seed ^ static_cast<int32>(BlockSalts[BlockIndex]), StrateIndex);
if (!Roll.bValid)
{
Manifest.bValid = false;
if (Manifest.FailureReason.IsEmpty())
{
Manifest.FailureReason = Roll.FailureReason;
}
continue;
}
VoxelStrateRegionPrivate::CopyParamBlock(
Region.ArchetypeParams, Roll, StructureBlocks[BlockIndex]);
}
}
}
if (Manifest.Regions.Num() == Manifest.RegionCount && Manifest.RegionCount > 0)
{
const FVoxelStrateRegion& First = Manifest.Regions[0];
Manifest.StructuralParamBlock = bRollStructure
? First.Recipe.StructuralParamBlock
: VoxelStrateRegionPrivate::ParamBlockForArchetype(First.Archetype);
Manifest.StrateTopWorldZ = VoxelStrateRegionPrivate::Top(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.StrateBottomWorldZ = VoxelStrateRegionPrivate::Bottom(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.BoundarySealThickness = VoxelStrateRegionPrivate::BoundarySeal(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.BaseDensity = VoxelStrateRegionPrivate::BaseDensity(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.bHasGlobalStructuralParams = FMath::IsFinite(Manifest.StrateTopWorldZ)
&& FMath::IsFinite(Manifest.StrateBottomWorldZ)
&& Manifest.StrateTopWorldZ > Manifest.StrateBottomWorldZ
&& FMath::IsFinite(Manifest.BoundarySealThickness)
&& FMath::IsFinite(Manifest.BaseDensity);
}
return Manifest;
}
#if WITH_EDITOR
namespace
{
@@ -4919,6 +5512,13 @@ FVoxelStrateComposerCandidate VF_RollStrateCandidate(
Candidate.Archetype = Candidate.ParameterRoll.Archetype;
Candidate.ArchetypeParams = Candidate.ParameterRoll.ArchetypeParams;
Candidate.Regions = VF_RollStrateRegionManifest(Corpus, Seed, Index, false);
if (!Candidate.Regions.IsValid())
{
Candidate.FailureReason = Candidate.Regions.FailureReason.IsEmpty()
? TEXT("lateral region roll failed") : Candidate.Regions.FailureReason;
return Candidate;
}
Candidate.bValid = true;
return Candidate;
}
@@ -4951,7 +5551,13 @@ FVoxelStrateComposerCandidate VF_RollStrateCandidate(
VF_CopyComposerBlock(Candidate.ArchetypeParams, Roll, Spec.Archetype);
}
Candidate.bValid = bAllBlocksValid;
Candidate.Regions = VF_RollStrateRegionManifest(Corpus, Seed, Index, true);
if (!Candidate.Regions.IsValid() && Candidate.FailureReason.IsEmpty())
{
Candidate.FailureReason = Candidate.Regions.FailureReason.IsEmpty()
? TEXT("lateral region roll failed") : Candidate.Regions.FailureReason;
}
Candidate.bValid = bAllBlocksValid && Candidate.Regions.IsValid();
return Candidate;
}
@@ -19,6 +19,8 @@ struct FVoxelStrateComposerSlotOverride
FVoxelStrateArchetypeParams Params;
bool bUseRecipe = false;
FVoxelOpStackRecipe Recipe;
bool bUseRegions = false;
FVoxelStrateRegionManifest Regions;
};
static void VF_SetComposerRuntimeBounds(
@@ -286,7 +288,8 @@ const FVoxelStrateComposerSlotOverride* UVoxelStrateManager::FindComposerOverrid
bool UVoxelStrateManager::SetComposerOverrideForStrate(
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
const FVoxelOpStackRecipe* Recipe, FString& OutError)
const FVoxelOpStackRecipe* Recipe, FString& OutError,
const FVoxelStrateRegionManifest* InRegions)
{
OutError.Reset();
@@ -342,6 +345,32 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
Override->Recipe = *Recipe;
}
if (InRegions != nullptr && InRegions->RegionCount > 1)
{
if (!InRegions->IsValid())
{
OutError = InRegions->FailureReason.IsEmpty()
? TEXT("The composer returned an invalid lateral region manifest.")
: InRegions->FailureReason;
return false;
}
Override->bUseRegions = true;
Override->Regions = *InRegions;
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE;
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * CHUNK_SIZE;
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
Override->Regions.StrateTopWorldZ,
Override->Regions.StrateBottomWorldZ);
for (FVoxelStrateRegion& Region : Override->Regions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
Override->Regions.StrateTopWorldZ,
Override->Regions.StrateBottomWorldZ);
}
Override->Regions.bHasGlobalStructuralParams = true;
}
// Only one slot is overridden at a time. Keeping this map small also makes the worker-side
// copy-on-chunk-refetch cheap. Passage geometry is deliberately not regenerated: the layout
// and its passages are unchanged, and this is the same manager state used by the offline
@@ -381,6 +410,25 @@ bool UVoxelStrateManager::GetComposerOverrideForChunk(
return true;
}
bool UVoxelStrateManager::GetComposerRegionOverrideForChunk(
const FIntVector& ChunkCoord, FVoxelStrateRegionManifest& OutRegions) const
{
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
if (SlotIdx < 0)
{
return false;
}
const FVoxelStrateComposerSlotOverride* Override =
FindComposerOverride(StrateLayout[SlotIdx].StrateIndex);
if (Override == nullptr || !Override->bUseRegions)
{
return false;
}
OutRegions = Override->Regions;
return true;
}
bool UVoxelStrateManager::HasComposerRecipeOverride() const
{
for (const TPair<int32, TSharedPtr<FVoxelStrateComposerSlotOverride>>& Pair : ComposerOverrides)
+42 -4
View File
@@ -194,6 +194,7 @@ namespace
const FVoxelStrateComposerCandidate& Candidate,
const UVoxelGenerator& Generator,
const UVoxelStrateManager& StrateManager,
int32 TargetStrateIndex,
int32 TargetTopChunkZ,
int32 TargetBottomChunkZ,
FVoxelOpStack& OutStack,
@@ -206,7 +207,24 @@ namespace
VF_SetComposerRuntimeBounds(Params, TopWorldZ, BottomWorldZ);
bool bBuilt = false;
if (Candidate.bStructureRoll)
if (Candidate.Regions.RegionCount > 1)
{
FVoxelStrateRegionManifest RegionManifest = Candidate.Regions;
VF_RekeyStrateRegionManifest(RegionManifest, Candidate.Seed,
TargetStrateIndex);
RegionManifest.StrateTopWorldZ = TopWorldZ;
RegionManifest.StrateBottomWorldZ = BottomWorldZ;
RegionManifest.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : RegionManifest.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TopWorldZ, BottomWorldZ);
}
bBuilt = VF_BuildStrateRegionStack(
RegionManifest, Generator.OriginSpineRadius, &StrateManager,
OutStack, OutContext, &OutError);
}
else if (Candidate.bStructureRoll)
{
bBuilt = VF_BuildStackFromRecipe(
Candidate.Recipe, Params, Generator.Seed, Generator.OriginSpineRadius,
@@ -573,7 +591,25 @@ void AVoxelWorld::ApplyComposerCandidate()
FVoxelOpContext PreflightContext;
FString PreflightError;
bool bPreflightBuilt = false;
if (Candidate.bStructureRoll)
FVoxelStrateRegionManifest PreflightRegions;
if (Candidate.Regions.RegionCount > 1)
{
PreflightRegions = Candidate.Regions;
VF_RekeyStrateRegionManifest(PreflightRegions, Candidate.Seed,
ComposerTargetStrateIndex);
PreflightRegions.StrateTopWorldZ = TargetTopWorldZ;
PreflightRegions.StrateBottomWorldZ = TargetBottomWorldZ;
PreflightRegions.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : PreflightRegions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TargetTopWorldZ, TargetBottomWorldZ);
}
bPreflightBuilt = VF_BuildStrateRegionStack(
PreflightRegions, Generator->OriginSpineRadius, StrateManager,
PreflightStack, PreflightContext, &PreflightError);
}
else if (Candidate.bStructureRoll)
{
bPreflightBuilt = VF_BuildStackFromRecipe(
Candidate.Recipe, PreflightParams, Generator->Seed, Generator->OriginSpineRadius,
@@ -615,7 +651,8 @@ void AVoxelWorld::ApplyComposerCandidate()
if (!StrateManager->SetComposerOverrideForStrate(
ComposerTargetStrateIndex, Candidate.Seed, Candidate.Archetype,
Candidate.ArchetypeParams, Candidate.bStructureRoll,
Candidate.bStructureRoll ? &Candidate.Recipe : nullptr, ApplyError))
Candidate.bStructureRoll ? &Candidate.Recipe : nullptr, ApplyError,
Candidate.Regions.RegionCount > 1 ? &Candidate.Regions : nullptr))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d was not applied: %s"),
@@ -638,7 +675,8 @@ void AVoxelWorld::ApplyComposerCandidate()
FVoxelOpContext ReferenceContext;
FString ReferenceError;
const bool bReferenceBuilt = VF_BuildComposerReferenceStack(
Candidate, *Generator, *StrateManager, TargetTopChunkZ, TargetBottomChunkZ,
Candidate, *Generator, *StrateManager, ComposerTargetStrateIndex,
TargetTopChunkZ, TargetBottomChunkZ,
ReferenceStack, ReferenceContext, ReferenceError);
FString ComparableReason;
+25 -7
View File
@@ -41,6 +41,7 @@
class UVoxelStrateManager;
struct FVoxelOpStackRecipe;
struct FVoxelStrateArchetypeParams;
struct FVoxelStrateRegionManifest;
enum class EVoxelStrateOpClass : uint8;
/** The declarations the offline structure roller reads before it ever materialises an op. */
@@ -353,7 +354,8 @@ namespace VoxelDensityOps
const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams =
TArray<FSurfaceGenerationParams>(),
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr);
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr,
bool bAppendStructuralPosts = true);
/**
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
@@ -365,7 +367,8 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* VerticalShafts — 9 ops, et **TROIS viennent de Maze sans une ligne de changement** :
@@ -378,7 +381,8 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* TunnelNetwork — **COMPLET, 20 ops** :
@@ -399,7 +403,8 @@ namespace VoxelDensityOps
VOXELFORGE_API void BuildTunnelNetworkStack(FVoxelOpStack& OutStack,
const FStrateGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
@@ -448,7 +453,8 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildFloatingIslandStack(FVoxelOpStack& OutStack, const FFloatingIslandParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* La pile Maze complète, décomposée — PAS un `FMazeOp` monolithique :
@@ -459,14 +465,26 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildMazeStack(FVoxelOpStack& OutStack, const FMazeGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
}
/** Materialise an offline recipe. Structural posts are appended internally and cannot be omitted. */
/** Materialise an offline recipe. Structural posts are appended by default; lateral region cores
* deliberately pass false so the parent stack can append the global posts exactly once. */
VOXELFORGE_API bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const FVoxelStrateArchetypeParams& Params,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr,
bool bAppendStructuralPosts = true);
/** Build the region combiner stack. Creative cores are independent; structural posts are global. */
VOXELFORGE_API bool VF_BuildStrateRegionStack(
const FVoxelStrateRegionManifest& Manifest,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
+20 -3
View File
@@ -65,7 +65,9 @@ struct VOXELFORGE_API FVoxelSeasonFixedStrate
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
bool bUsesRecipe = true;
bool bUsesRegions = false;
FString SourceDefinitionPath;
};
@@ -162,6 +164,9 @@ struct VOXELFORGE_API FVoxelSeasonStrate
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelOpStackRecipe Recipe;
FVoxelStrateArchetypeParams Params;
// When true this is the complete lateral parent description. Params/Archetype remain the
// compatibility identity of region zero; runtime rebuilds use Regions and never blend params.
FVoxelStrateRegionManifest Regions;
FVoxelStrateMeasuredMetrics Metrics;
double DistanceFromCorpusCentroid = 0.0;
@@ -173,6 +178,7 @@ struct VOXELFORGE_API FVoxelSeasonStrate
bool bBossSlot = false;
bool bFixed = false;
bool bUsesRecipe = true;
bool bUsesRegions = false;
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
@@ -224,8 +230,20 @@ struct VOXELFORGE_API FVoxelSeasonManifest
bool IsUsable() const
{
return bValid && Error.IsEmpty() && WorldRadiusVoxels == 0.0f
&& Strates.Num() == SelectedCount && SelectedCount > 0;
if (!bValid || !Error.IsEmpty() || WorldRadiusVoxels != 0.0f
|| Strates.Num() != SelectedCount || SelectedCount <= 0)
{
return false;
}
for (const FVoxelSeasonStrate& Strate : Strates)
{
if (Strate.bUsesRegions
&& (!Strate.Regions.IsValid() || Strate.Regions.RegionCount <= 1))
{
return false;
}
}
return true;
}
};
@@ -266,4 +284,3 @@ VOXELFORGE_API bool VF_RebuildVoxelSeasonStrate(
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
@@ -228,6 +228,142 @@ struct VOXELFORGE_API FVoxelStrateArchetypeParams
FFloatingIslandParams FloatingIslandParams;
};
/**
* One lateral region in a strate.
*
* This is deliberately a plain manifest record rather than a label on the strate. The recipe
* and the native vector belong to the region, and the runtime combines the resulting density
* fields. Structural posts are not region-owned.
*/
struct VOXELFORGE_API FVoxelStrateRegion
{
int32 RegionIndex = 0;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
bool bUsesRecipe = false;
FVoxelOpStackRecipe Recipe;
};
/**
* Offline-composed lateral region manifest for one strate.
*
* `StrateIndex` is part of the partition identity. A candidate can be moved to another live
* slot, so the editor hand-off must call VF_RekeyStrateRegionManifest before installing it.
* `PartitionSeed` is retained in the manifest so the runtime never has to recreate an RNG stream.
*/
struct VOXELFORGE_API FVoxelStrateRegionManifest
{
bool bValid = false;
FString FailureReason;
int32 Seed = 0;
int32 StrateIndex = 0;
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
// A 32-voxel chunk sees a tiny portion of this lattice. The values are manifest data, not
// hidden runtime tuning knobs; changing them changes the authored world and its hash.
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
// One global structural post configuration. Region recipes may use different native blocks,
// but no region is allowed to choose the spine/seal/passage/edge-seal values independently.
bool bHasGlobalStructuralParams = false;
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
float BoundarySealThickness = 0.0f;
float BaseDensity = 8.0f;
TArray<FVoxelStrateRegion> Regions;
bool IsSingleRegion() const { return RegionCount == 1 && Regions.Num() == 1; }
bool IsValid() const
{
return bValid && RegionCount >= 1 && RegionCount <= 3
&& Regions.Num() == RegionCount
&& FMath::IsFinite(LatticeCellSize) && LatticeCellSize > 0.0f
&& FMath::IsFinite(BlendWidth) && BlendWidth >= 0.0f;
}
};
/** Result of the positional Voronoi query used by the lateral density combiner. */
struct VOXELFORGE_API FVoxelStrateRegionQuery
{
int32 PrimaryRegion = 0;
int32 NeighborRegion = INDEX_NONE;
float NeighborWeight = 0.0f;
float NearestDifferentRegionGap = FLT_MAX;
// False means the bounded lattice neighborhood could not prove that its different-region
// candidate is the nearest one. Density evaluation may still use a zero blend weight, but a
// ClassifyBox proof must refuse to call that an infinite boundary distance.
bool bNearestDifferentRegionKnown = false;
bool bInBlendBand = false;
};
/** Conservative XY proof used before a tile may be skipped. */
struct VOXELFORGE_API FVoxelStrateRegionBoxProof
{
bool bProvablySingleRegion = false;
bool bTouchesRegionBoundary = true;
bool bTouchesBlendBand = true;
};
/** One hash-lattice site retained by the per-chunk partition cache. */
struct VOXELFORGE_API FVoxelStrateRegionSite
{
int32 CellX = 0;
int32 CellY = 0;
int32 Region = 0;
float WorldX = 0.0f;
float WorldY = 0.0f;
};
/**
* Worker-local cache for the lateral partition.
*
* Hash/site discovery and the integer XY grid are prepared once for a chunk. The voxel path
* then does an O(1) lookup for the ordinary integer density samples; only fractional gradient
* probes use the small prepared site list. No seed lattice is searched or allocated per voxel.
*/
struct VOXELFORGE_API FVoxelStrateRegionPartitionCache
{
void PrepareForChunk(const FVoxelStrateRegionManifest& Manifest,
const FIntVector& ChunkCoord);
FVoxelStrateRegionQuery Query(float WorldX, float WorldY) const;
private:
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
int32 BaseX = 0;
int32 BaseY = 0;
int32 Dim = 0;
TArray<FVoxelStrateRegionSite> Sites;
TArray<FVoxelStrateRegionQuery> IntegerSamples;
};
/** Set every native family’s runtime Z bounds to the same strate interval. */
VOXELFORGE_API void VF_SetStrateArchetypeRuntimeBounds(
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
/** Stable identity helpers for the deterministic lateral partition. */
VOXELFORGE_API int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex);
VOXELFORGE_API uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex);
VOXELFORGE_API void VF_RekeyStrateRegionManifest(
FVoxelStrateRegionManifest& Manifest, int32 Seed, int32 StrateIndex);
/** Pure positional query; no retained state and no ordering-dependent RNG. */
VOXELFORGE_API FVoxelStrateRegionQuery VF_QueryStrateRegion(
const FVoxelStrateRegionManifest& Manifest, float WorldX, float WorldY);
/** Conservative proof for an XY projection of a voxel box. */
VOXELFORGE_API FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
const FVoxelStrateRegionManifest& Manifest, const FBox& VoxelBox);
/** One known-good authored vector and the archetype that gives it meaning. */
struct VOXELFORGE_API FVoxelStrateCorpusEntry
{
@@ -413,6 +549,7 @@ struct VOXELFORGE_API FVoxelStrateComposerCandidate
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
// Kept so automation/editor callers can audit the exact six independent structure blocks
// without re-rolling them through a second implementation.
@@ -532,6 +669,30 @@ VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
/** Roll one-to-three independent region recipes/vectors plus their deterministic partition. */
VOXELFORGE_API FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 StrateIndex, bool bRollStructure);
/**
* ⛔ THE LATERAL-REGION GATE. Returns false, and must keep returning false until the seam law holds.
*
* Lateral regions (archetypes mixing inside one strate — "tunnel network leading to a big chamber")
* are BUILT and MEASURED but NOT SHIPPABLE. Measured 2026-09-05: `arrival -> departure` across a
* region seam passes **9 of 16 seeds**, against 11/16 for same-region controls in the same harness.
*
* Density blending alone does not guarantee a player can cross a seam. A strate that mixes archetypes
* and strands you at the boundary is strictly worse than one that does not mix — so this stays off.
*
* ⚠️ DO NOT flip this to true to "enable the feature". The fix is an explicit corridor/landing
* contract across seams (a design decision, deliberately not made unilaterally), after which the
* measured rate must be 16/16. `VoxelForge.Composer.LateralRegions` asserts that this gate is shut
* whenever the rate is imperfect, so flipping it without fixing the law turns the suite red.
*
* Safe today regardless: single-region output is bit-identical to legacy, and cross-boundary box
* verdicts returned 0 violations over 199,800 brute-forced samples (all correctly `Mixed`).
*/
VOXELFORGE_API bool VF_LateralRegionsAreShippable();
/**
* Provenance for a parameter block that does not read the authored corpus.
*
@@ -27,6 +27,7 @@ class UVoxelSettings;
#if WITH_EDITOR
struct FVoxelStrateArchetypeParams;
struct FVoxelOpStackRecipe;
struct FVoxelStrateRegionManifest;
struct FVoxelStrateComposerSlotOverride;
#endif
@@ -158,7 +159,8 @@ public:
bool SetComposerOverrideForStrate(
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
const FVoxelOpStackRecipe* Recipe, FString& OutError);
const FVoxelOpStackRecipe* Recipe, FString& OutError,
const FVoxelStrateRegionManifest* Regions = nullptr);
/** Read the immutable editor override for a chunk into worker-local storage. */
bool GetComposerOverrideForChunk(
@@ -166,6 +168,10 @@ public:
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
bool& bOutUseRecipe, FVoxelOpStackRecipe& OutRecipe) const;
/** Read the immutable editor region override into worker-local storage. */
bool GetComposerRegionOverrideForChunk(
const FIntVector& ChunkCoord, FVoxelStrateRegionManifest& OutRegions) const;
/** Custom recipes cannot be safely classified by the native ClassifyBox proof yet. */
bool HasComposerRecipeOverride() const;
#endif