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:
1 parent
42e1f0ec8c
commit
dd38832bb6
14 files changed
+3040
-48
No files matched your search
+51
-3
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
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));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
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));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
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));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
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));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
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));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
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.
|
||||
Candidate.AppendStructuralPost(Top, Bottom, Seal, Base, SpineRadius, StrateManager);
|
||||
// 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
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);
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user