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

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

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

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

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

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

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

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

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

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

No files matched your search

File diff suppressed because it is too large. Load diff
+486 -21
View File
@@ -4886,6 +4886,129 @@ namespace
uint32 SeedU;
};
//=============================================================================
// RÔLE 2 — COMBINER : RÉGIONS LATÉRALES
//=============================================================================
// The creative stacks stay independent. This op owns only their density-level junction;
// the parent FVoxelOpStack appends the global structural posts after it. In particular, no
// recipe can make the spine, vertical seal, passage carve, or XY edge seal region-local.
class FLateralRegionBlendOp final : public IVoxelDensityOp
{
public:
FLateralRegionBlendOp(const FVoxelStrateRegionManifest& InManifest,
TArray<FVoxelOpStack>&& InStacks)
: RegionStacks(MoveTemp(InStacks))
{
// Keep only partition metadata here. The native vectors/recipes have already been
// consumed by the stacks and must not be duplicated in the voxel operator.
PartitionManifest.bValid = true;
PartitionManifest.Seed = InManifest.Seed;
PartitionManifest.StrateIndex = InManifest.StrateIndex;
PartitionManifest.RegionCount = InManifest.RegionCount;
PartitionManifest.PartitionSeed = InManifest.PartitionSeed;
PartitionManifest.LatticeCellSize = InManifest.LatticeCellSize;
PartitionManifest.BlendWidth = InManifest.BlendWidth;
PartitionManifest.Regions.SetNum(InManifest.RegionCount);
}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::Combiner; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext& Ctx) override
{
PartitionCache.PrepareForChunk(PartitionManifest, Ctx.ChunkCoord);
bPrepared = true;
for (FVoxelOpStack& Stack : RegionStacks)
{
Stack.PrepareChunk(Ctx);
}
}
void Eval(float WorldX, float WorldY, float WorldZ,
FVoxelOpSample& InOut) const override
{
if (RegionStacks.Num() == 0) { return; }
const FVoxelStrateRegionQuery Query = bPrepared
? PartitionCache.Query(WorldX, WorldY)
: VF_QueryStrateRegion(PartitionManifest, WorldX, WorldY);
const int32 Primary = FMath::Clamp(Query.PrimaryRegion, 0, RegionStacks.Num() - 1);
const float PrimaryDensity = RegionStacks[Primary].EvalInternal(
WorldX, WorldY, WorldZ);
float Density = PrimaryDensity;
if (Query.NeighborWeight > 0.0f && Query.NeighborRegion != INDEX_NONE
&& RegionStacks.IsValidIndex(Query.NeighborRegion)
&& Query.NeighborRegion != Primary)
{
const float NeighborDensity = RegionStacks[Query.NeighborRegion].EvalInternal(
WorldX, WorldY, WorldZ);
Density = FMath::Lerp(PrimaryDensity, NeighborDensity, Query.NeighborWeight);
}
InOut.Density = Density;
}
/**
* A box wholly outside the band can use its one creative stack. A box that may touch a
* bisector or the band asks every region stack. If any is Mixed, the parent is Mixed. If
* all possible stacks agree, their convex density blend has the same sign everywhere, so
* the blend band is proved uniform too. This is deliberately stricter than sampling a
* center point and is the protection against a cross-region false uniform tile.
*/
EVoxelTileClass ClassifyBox(const FBox& VoxelBox,
const FVoxelOpContext& Ctx) const override
{
if (RegionStacks.Num() == 0) { return EVoxelTileClass::Mixed; }
const FVoxelStrateRegionBoxProof Proof =
VF_AnalyzeStrateRegionBox(PartitionManifest, VoxelBox);
if (Proof.bProvablySingleRegion)
{
const FVoxelStrateRegionQuery Query = VF_QueryStrateRegion(
PartitionManifest,
((float)VoxelBox.Min.X + (float)VoxelBox.Max.X) * 0.5f,
((float)VoxelBox.Min.Y + (float)VoxelBox.Max.Y) * 0.5f);
const int32 Primary = FMath::Clamp(
Query.PrimaryRegion, 0, RegionStacks.Num() - 1);
return RegionStacks[Primary].ClassifyBox(VoxelBox, Ctx);
}
EVoxelTileClass CommonVerdict = EVoxelTileClass::Mixed;
for (const FVoxelOpStack& Stack : RegionStacks)
{
const EVoxelTileClass Verdict = Stack.ClassifyBox(VoxelBox, Ctx);
if (Verdict == EVoxelTileClass::Mixed)
{
return EVoxelTileClass::Mixed;
}
if (CommonVerdict == EVoxelTileClass::Mixed)
{
CommonVerdict = Verdict;
}
else if (CommonVerdict != Verdict)
{
return EVoxelTileClass::Mixed;
}
}
return CommonVerdict;
}
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
{
return EVoxelOpEffect::Both;
}
const TCHAR* DebugName() const override { return TEXT("LateralRegionBlendOp"); }
private:
FVoxelStrateRegionManifest PartitionManifest;
TArray<FVoxelOpStack> RegionStacks;
mutable FVoxelStrateRegionPartitionCache PartitionCache;
mutable bool bPrepared = false;
};
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
// Même piège que dans VoxelHeightOpStack.cpp : s'ancrer sur une bannière située plus bas
// (« FVoxelOpStack », « FABRIQUES ») insère la classe HORS du namespace anonyme, et l'accolade
@@ -5249,7 +5372,8 @@ namespace VoxelDensityOps
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams,
TUniquePtr<IVoxelBiomeField> BiomeField)
TUniquePtr<IVoxelBiomeField> BiomeField,
bool bAppendStructuralPosts)
{
// ARCHÉTYPE COMPLET depuis l'étape 2c : vide + overhang + mélange de biomes.
// `PerBiomeParams` vide ⇒ chemin sans biomes, strictement inchangé.
@@ -5262,12 +5386,17 @@ namespace VoxelDensityOps
// motif que cliff → structural : un modificateur qui a besoin de ce que la source a produit.
OutStack.Add(MakeUnique<FOverhangShelfMod>(P, Seed, ColumnPtr));
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;
}
+129 -2
View File
@@ -741,9 +741,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// cache on the same versioned refetch as the native params; no worker reads mutable editor
// state while the world is applying a new candidate.
thread_local bool CP_UseComposerRecipe = false;
thread_local bool CP_UseComposerRegions = false;
thread_local int32 CP_ComposerSeed = 0;
thread_local FVoxelStrateArchetypeParams CP_ComposerParams;
thread_local FVoxelOpStackRecipe CP_ComposerRecipe;
thread_local FVoxelStrateRegionManifest CP_ComposerRegions;
#endif
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
@@ -763,9 +765,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
#if WITH_EDITOR
CP_UseComposerRecipe = false;
CP_UseComposerRegions = false;
ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork;
bool bComposerUseRecipe = false;
if (StrateManager->GetComposerOverrideForChunk(
if (StrateManager->GetComposerRegionOverrideForChunk(
ChunkCoord, CP_ComposerRegions))
{
CP_UseComposerRegions = true;
CP_ComposerSeed = CP_ComposerRegions.Seed;
if (CP_ComposerRegions.Regions.Num() > 0)
{
CP_GenType = CP_ComposerRegions.Regions[0].Archetype;
}
}
else if (StrateManager->GetComposerOverrideForChunk(
ChunkCoord, CP_ComposerSeed, ComposerArchetype, CP_ComposerParams,
bComposerUseRecipe, CP_ComposerRecipe))
{
@@ -804,11 +817,37 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est
// par chunk (comme le refetch de params juste au-dessus), jamais par voxel.
CP_UseOpStack = StrateManager->UsesOperatorStackForChunk(ChunkCoord);
#if WITH_EDITOR
// A composer override is already an explicit, validated stack description. It must
// be evaluated even when the authored definition had the normal operator-stack opt-in
// disabled; otherwise a multi-region candidate would be installed in the manager but
// silently fall back to the old one-archetype switch.
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions || CP_UseComposerRecipe;
#endif
if (CP_UseOpStack)
{
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
#if WITH_EDITOR
if (CP_UseComposerRecipe)
if (CP_UseComposerRegions)
{
FVoxelOpContext RegionContext;
if (VF_BuildStrateRegionStack(
CP_ComposerRegions, OriginSpineRadius, StrateManager,
CP_OpStack, RegionContext, nullptr))
{
RegionContext.ChunkCoord = ChunkCoord;
RegionContext.Step = 1;
RegionContext.LayoutVersion = LayoutVersion;
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RegionContext);
}
else
{
CP_UseOpStack = false;
}
}
else if (CP_UseComposerRecipe)
{
FVoxelOpContext ComposerContext;
if (VF_BuildStackFromRecipe(
@@ -2897,6 +2936,94 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (!StrateManager) return EVoxelTileClass::Mixed;
#if WITH_EDITOR
// A multi-region composer override is already a complete parent stack: it owns the lateral
// blend and the four global structural posts. Classify it as one field only when the complete
// mesher box stays inside that one strate; otherwise the Z-category pass below deliberately
// returns Mixed. Disturbances and the diff layer remain outside the stack, so either one also
// vetoes a skip.
FVoxelStrateRegionManifest ComposerRegions;
const FIntVector RegionProbeChunk(
FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), FloorDivC(MinZ, CHUNK_SIZE));
if (StrateManager->GetComposerRegionOverrideForChunk(RegionProbeChunk, ComposerRegions))
{
int32 RegionTopChunkZ = 0;
int32 RegionBottomChunkZ = 0;
const int32 RegionMinChunkZ = FloorDivC(MinZ, CHUNK_SIZE);
const int32 RegionMaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE);
if (!StrateManager->GetStrateChunkZBounds(RegionProbeChunk.Z,
RegionTopChunkZ, RegionBottomChunkZ)
|| RegionMinChunkZ < RegionBottomChunkZ
|| RegionMaxChunkZ > RegionTopChunkZ)
{
return EVoxelTileClass::Mixed;
}
// A representative chunk is not enough for a proof: the mesher box can cross an XY
// chunk edge, and disturbances are chunk-local. Require every touched chunk to remain
// in the same slot and to have no active disturbance before delegating the proof to the
// complete lateral stack. Any uncertainty is Mixed; a false uniform answer here can
// skip a real seam or carve.
const int32 RegionMinChunkX = FloorDivC(MinX, CHUNK_SIZE);
const int32 RegionMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE);
const int32 RegionMinChunkY = FloorDivC(MinY, CHUNK_SIZE);
const int32 RegionMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE);
for (int32 CZ = RegionMinChunkZ; CZ <= RegionMaxChunkZ; ++CZ)
{
int32 TouchedTopChunkZ = 0;
int32 TouchedBottomChunkZ = 0;
if (!StrateManager->GetStrateChunkZBounds(
CZ, TouchedTopChunkZ, TouchedBottomChunkZ)
|| TouchedTopChunkZ != RegionTopChunkZ
|| TouchedBottomChunkZ != RegionBottomChunkZ)
{
return EVoxelTileClass::Mixed;
}
for (int32 CY = RegionMinChunkY; CY <= RegionMaxChunkY; ++CY)
{
for (int32 CX = RegionMinChunkX; CX <= RegionMaxChunkX; ++CX)
{
const FIntVector TouchedChunk(CX, CY, CZ);
FVoxelStrateRegionManifest TouchedRegions;
if (!StrateManager->GetComposerRegionOverrideForChunk(
TouchedChunk, TouchedRegions)
|| TouchedRegions.StrateIndex != ComposerRegions.StrateIndex
|| TouchedRegions.Seed != ComposerRegions.Seed
|| TouchedRegions.RegionCount != ComposerRegions.RegionCount
|| TouchedRegions.PartitionSeed != ComposerRegions.PartitionSeed
|| TouchedRegions.LatticeCellSize != ComposerRegions.LatticeCellSize
|| TouchedRegions.BlendWidth != ComposerRegions.BlendWidth)
{
return EVoxelTileClass::Mixed;
}
const FStrateDisturbanceParams Disturbances =
StrateManager->GetDisturbanceParamsForChunk(TouchedChunk);
if (Disturbances.ChasmDensity > 0.0f
|| Disturbances.BridgeDensity > 0.0f
|| Disturbances.RidgeDensity > 0.0f)
{
return EVoxelTileClass::Mixed;
}
}
}
}
FVoxelOpStack RegionStack;
FVoxelOpContext RegionContext;
if (!VF_BuildStrateRegionStack(ComposerRegions, OriginSpineRadius,
StrateManager, RegionStack, RegionContext, nullptr))
{
return EVoxelTileClass::Mixed;
}
RegionContext.ChunkCoord = RegionProbeChunk;
RegionContext.Step = Step;
RegionContext.LayoutVersion = StrateManager->GetLayoutVersion();
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness;
RegionStack.PrepareChunk(RegionContext);
return RegionStack.ClassifyBox(TileVoxelBox, RegionContext);
}
// A custom recipe is deliberately not fed to the native ClassifyBox proof yet. Returning
// Mixed keeps the live walk-through hole-safe: GenerateMesh samples the exact recipe stack,
// while no optimistic AllAir/AllSolid result can skip a candidate tile.
@@ -1092,6 +1092,152 @@ namespace VoxelSeasonManifestPrivate
Root->SetStringField(TEXT("policy_note"), Manifest.PolicyNote);
}
TSharedPtr<FJsonObject> SerializeRegionManifest(
const FVoxelStrateRegionManifest& Regions)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetBoolField(TEXT("valid"), Regions.bValid);
Object->SetStringField(TEXT("failure_reason"), Regions.FailureReason);
Object->SetNumberField(TEXT("seed"), Regions.Seed);
Object->SetNumberField(TEXT("strate_index"), Regions.StrateIndex);
Object->SetNumberField(TEXT("region_count"), Regions.RegionCount);
Object->SetNumberField(TEXT("partition_seed"),
static_cast<double>(Regions.PartitionSeed));
SetMetricFloat(Object, TEXT("lattice_cell_size"), Regions.LatticeCellSize);
SetMetricFloat(Object, TEXT("blend_width"), Regions.BlendWidth);
Object->SetBoolField(TEXT("has_global_structural_params"),
Regions.bHasGlobalStructuralParams);
Object->SetNumberField(TEXT("structural_param_block"),
static_cast<int32>(Regions.StructuralParamBlock));
SetMetricFloat(Object, TEXT("strate_top_world_z"), Regions.StrateTopWorldZ);
SetMetricFloat(Object, TEXT("strate_bottom_world_z"), Regions.StrateBottomWorldZ);
SetMetricFloat(Object, TEXT("boundary_seal_thickness"),
Regions.BoundarySealThickness);
SetMetricFloat(Object, TEXT("base_density"), Regions.BaseDensity);
TArray<TSharedPtr<FJsonValue>> RegionValues;
for (const FVoxelStrateRegion& Region : Regions.Regions)
{
TSharedPtr<FJsonObject> RegionObject = MakeShared<FJsonObject>();
RegionObject->SetNumberField(TEXT("region_index"), Region.RegionIndex);
RegionObject->SetNumberField(TEXT("seed"), Region.Seed);
RegionObject->SetStringField(TEXT("archetype"), ArchetypeName(Region.Archetype));
RegionObject->SetNumberField(TEXT("archetype_id"),
static_cast<int32>(Region.Archetype));
RegionObject->SetBoolField(TEXT("uses_recipe"), Region.bUsesRecipe);
RegionObject->SetObjectField(TEXT("recipe"), SerializeRecipe(Region.Recipe));
RegionObject->SetObjectField(TEXT("parameters"),
SerializeAllParams(Region.ArchetypeParams));
RegionValues.Add(MakeShared<FJsonValueObject>(MoveTemp(RegionObject)));
}
Object->SetArrayField(TEXT("regions"), MoveTemp(RegionValues));
return Object;
}
bool ReadJsonUInt32(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, uint32& OutValue)
{
double Number = 0.0;
if (!ReadJsonNumber(Object, Name, Number)
|| !FMath::IsFinite(Number)
|| Number < 0.0 || Number > 4294967295.0
|| FMath::FloorToDouble(Number) != Number)
{
return false;
}
OutValue = static_cast<uint32>(Number);
return true;
}
bool DeserializeRegionManifest(const TSharedPtr<FJsonObject>& Object,
FVoxelStrateRegionManifest& OutRegions)
{
if (!Object.IsValid()) return false;
OutRegions = FVoxelStrateRegionManifest();
bool JsonValid = false;
int32 StructuralBlock = 0;
bool bFieldsValid = ReadJsonBool(Object, TEXT("valid"), JsonValid)
&& ReadJsonString(Object, TEXT("failure_reason"), OutRegions.FailureReason)
&& ReadJsonInt32(Object, TEXT("seed"), OutRegions.Seed)
&& ReadJsonInt32(Object, TEXT("strate_index"), OutRegions.StrateIndex)
&& ReadJsonInt32(Object, TEXT("region_count"), OutRegions.RegionCount)
&& ReadJsonUInt32(Object, TEXT("partition_seed"), OutRegions.PartitionSeed)
&& ReadMetricFloat(Object, TEXT("lattice_cell_size"), OutRegions.LatticeCellSize)
&& ReadMetricFloat(Object, TEXT("blend_width"), OutRegions.BlendWidth)
&& ReadJsonBool(Object, TEXT("has_global_structural_params"),
OutRegions.bHasGlobalStructuralParams)
&& ReadJsonInt32(Object, TEXT("structural_param_block"), StructuralBlock)
&& ReadMetricFloat(Object, TEXT("strate_top_world_z"), OutRegions.StrateTopWorldZ)
&& ReadMetricFloat(Object, TEXT("strate_bottom_world_z"),
OutRegions.StrateBottomWorldZ)
&& ReadMetricFloat(Object, TEXT("boundary_seal_thickness"),
OutRegions.BoundarySealThickness)
&& ReadMetricFloat(Object, TEXT("base_density"), OutRegions.BaseDensity);
if (StructuralBlock < static_cast<int32>(EVoxelStrateParamBlock::None)
|| StructuralBlock > static_cast<int32>(EVoxelStrateParamBlock::FloatingIsland))
{
bFieldsValid = false;
}
OutRegions.StructuralParamBlock = static_cast<EVoxelStrateParamBlock>(StructuralBlock);
const TArray<TSharedPtr<FJsonValue>>* RegionValues = nullptr;
bFieldsValid = ReadJsonArray(Object, TEXT("regions"), RegionValues) && bFieldsValid;
if (RegionValues == nullptr
|| OutRegions.RegionCount < 1 || OutRegions.RegionCount > 3
|| RegionValues->Num() != OutRegions.RegionCount)
{
bFieldsValid = false;
}
OutRegions.Regions.Reset();
if (RegionValues != nullptr)
{
OutRegions.Regions.Reserve(RegionValues->Num());
for (int32 Index = 0; Index < RegionValues->Num(); ++Index)
{
const TSharedPtr<FJsonValue>& Value = (*RegionValues)[Index];
if (!Value.IsValid() || Value->Type != EJson::Object)
{
bFieldsValid = false;
continue;
}
const TSharedPtr<FJsonObject> RegionObject = Value->AsObject();
FVoxelStrateRegion& Region = OutRegions.Regions.AddDefaulted_GetRef();
FString ArchetypeText;
int32 ArchetypeId = -1;
TSharedPtr<FJsonObject> Recipe, Params;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("region_index"),
Region.RegionIndex) && bFieldsValid;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("seed"), Region.Seed)
&& bFieldsValid;
bFieldsValid = ReadJsonString(RegionObject, TEXT("archetype"), ArchetypeText)
&& bFieldsValid;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("archetype_id"), ArchetypeId)
&& bFieldsValid;
bFieldsValid = ReadJsonBool(RegionObject, TEXT("uses_recipe"),
Region.bUsesRecipe) && bFieldsValid;
bFieldsValid = ReadJsonObject(RegionObject, TEXT("recipe"), Recipe)
&& bFieldsValid;
bFieldsValid = ReadJsonObject(RegionObject, TEXT("parameters"), Params)
&& bFieldsValid;
bFieldsValid = ParseArchetype(ArchetypeText, Region.Archetype)
&& bFieldsValid;
bFieldsValid = Region.RegionIndex == Index && bFieldsValid;
bFieldsValid = ArchetypeId == static_cast<int32>(Region.Archetype)
&& bFieldsValid;
if (Region.bUsesRecipe)
{
bFieldsValid = DeserializeRecipe(Recipe, Region.Recipe) && bFieldsValid;
}
bFieldsValid = DeserializeAllParams(Params, Region.ArchetypeParams)
&& bFieldsValid;
}
}
OutRegions.bValid = JsonValid;
return bFieldsValid && OutRegions.IsValid();
}
TSharedPtr<FJsonObject> SerializeStrate(const FVoxelSeasonStrate& Strate)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
@@ -1104,8 +1250,16 @@ namespace VoxelSeasonManifestPrivate
Object->SetStringField(TEXT("archetype"), ArchetypeName(Strate.Archetype));
Object->SetNumberField(TEXT("archetype_id"), static_cast<int32>(Strate.Archetype));
Object->SetBoolField(TEXT("uses_recipe"), Strate.bUsesRecipe);
Object->SetBoolField(TEXT("uses_regions"), Strate.bUsesRegions);
Object->SetObjectField(TEXT("recipe"), SerializeRecipe(Strate.Recipe));
Object->SetObjectField(TEXT("parameters"), SerializeAllParams(Strate.Params));
// Keep old/single-region artifacts compact and on the exact legacy rebuild path. A
// multi-region strate carries the complete parent manifest because that is its runtime
// identity; an invalid default Regions object must not make fixed legacy entries unloadable.
if (Strate.bUsesRegions)
{
Object->SetObjectField(TEXT("regions"), SerializeRegionManifest(Strate.Regions));
}
Object->SetObjectField(TEXT("measured_metrics"), SerializeMetrics(Strate.Metrics));
Object->SetNumberField(TEXT("distance_from_corpus_centroid"), Strate.DistanceFromCorpusCentroid);
Object->SetStringField(TEXT("selection_reason"), Strate.SelectionReason);
@@ -1151,10 +1305,34 @@ namespace VoxelSeasonManifestPrivate
OutStrate.DistanceFromCorpusCentroid = Distance;
OutStrate.SelectionReason = Reason;
OutStrate.SourceDefinitionPath = SourcePath;
TSharedPtr<FJsonObject> Recipe, Params, Metrics;
TSharedPtr<FJsonObject> Recipe, Params, Metrics, RegionsObject;
bValid = ReadJsonObject(Object, TEXT("recipe"), Recipe) && bValid;
bValid = ReadJsonObject(Object, TEXT("parameters"), Params) && bValid;
bValid = ReadJsonObject(Object, TEXT("measured_metrics"), Metrics) && bValid;
const TSharedPtr<FJsonValue>* RegionsValue = Object->Values.Find(TEXT("regions"));
if (RegionsValue != nullptr)
{
bValid = ReadJsonObject(Object, TEXT("regions"), RegionsObject) && bValid;
if (RegionsObject.IsValid())
{
bValid = DeserializeRegionManifest(RegionsObject, OutStrate.Regions)
&& bValid;
}
}
const TSharedPtr<FJsonValue>* UsesRegionsValue =
Object->Values.Find(TEXT("uses_regions"));
if (UsesRegionsValue != nullptr)
{
bValid = ReadJsonBool(Object, TEXT("uses_regions"), OutStrate.bUsesRegions)
&& bValid;
}
else
{
// Schema-1 manifests predating lateral regions simply take the old path. A region
// object is optional for backward compatibility, and never implicitly changes that
// path without the explicit opt-in bit.
OutStrate.bUsesRegions = false;
}
bValid = ParseArchetype(ArchetypeText, OutStrate.Archetype) && bValid;
bValid = ArchetypeId == static_cast<int32>(OutStrate.Archetype) && bValid;
bValid = ParseConnectivityName(LawText, OutStrate.PrimordialLawResult) && bValid;
@@ -1164,6 +1342,11 @@ namespace VoxelSeasonManifestPrivate
}
bValid = DeserializeAllParams(Params, OutStrate.Params) && bValid;
bValid = DeserializeMetrics(Metrics, OutStrate.Metrics) && bValid;
if (OutStrate.bUsesRegions
&& (!OutStrate.Regions.IsValid() || OutStrate.Regions.RegionCount <= 1))
{
bValid = false;
}
return bValid;
}
@@ -1461,12 +1644,31 @@ bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
for (int32 Index = 0; Index < OutManifest.Strates.Num(); ++Index)
{
const FVoxelSeasonStrate& Strate = OutManifest.Strates[Index];
const float StoredTop = static_cast<float>(Strate.TopWorldZ);
const float StoredBottom = static_cast<float>(Strate.BottomWorldZ);
if (Strate.DepthIndex != Index || !Strate.bPassedPrimordialLaw
|| Strate.PrimordialLawResult != EVoxelConnectivityResult::Connected)
{
bFieldsValid = false;
break;
}
if (Strate.bUsesRegions
&& (!Strate.Regions.IsValid()
|| Strate.Regions.RegionCount <= 1
|| Strate.Regions.Seed != Strate.Seed
|| Strate.Regions.StrateIndex != Strate.DepthIndex
|| Strate.Regions.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Strate.Seed, Strate.DepthIndex)
|| !Strate.Regions.bHasGlobalStructuralParams
|| FMemory::Memcmp(&Strate.Regions.StrateTopWorldZ,
&StoredTop, sizeof(float)) != 0
|| FMemory::Memcmp(&Strate.Regions.StrateBottomWorldZ,
&StoredBottom, sizeof(float)) != 0)
)
{
bFieldsValid = false;
break;
}
}
if (!bFieldsValid)
{
@@ -1487,6 +1689,9 @@ namespace VoxelSeasonCompositionPrivate
float ActiveBottom(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
float ActiveSeal(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
void SetRuntimeBounds(FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
void SetRegionRuntimeBounds(FVoxelStrateRegionManifest& Regions,
int32 Seed, int32 StrateIndex,
float TopWorldZ, float BottomWorldZ);
const TCHAR* RejectionReasonForLaw(EVoxelConnectivityResult Result);
// These caps are policy/allocator guards, not gameplay limits. They keep a malformed build-box
@@ -1690,9 +1895,17 @@ namespace VoxelSeasonCompositionPrivate
OutStrate.Archetype = Candidate.Archetype;
OutStrate.Recipe = Candidate.Recipe;
OutStrate.Params = Candidate.ArchetypeParams;
OutStrate.Regions = Candidate.Regions;
OutStrate.bUsesRecipe = true;
OutStrate.bUsesRegions = Candidate.Regions.RegionCount > 1;
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
if (OutStrate.Regions.IsValid())
{
SetRegionRuntimeBounds(OutStrate.Regions, Candidate.Seed, CandidateIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
}
#endif
@@ -1712,11 +1925,19 @@ namespace VoxelSeasonCompositionPrivate
OutStrate.Archetype = Fixed.Archetype;
OutStrate.Recipe = Fixed.Recipe;
OutStrate.Params = Fixed.Params;
OutStrate.Regions = Fixed.Regions;
OutStrate.SourceDefinitionPath = Fixed.SourceDefinitionPath;
OutStrate.bFixed = true;
OutStrate.bUsesRecipe = Fixed.bUsesRecipe;
OutStrate.bUsesRegions = Fixed.bUsesRegions && Fixed.Regions.IsValid();
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
if (OutStrate.bUsesRegions)
{
SetRegionRuntimeBounds(OutStrate.Regions, Fixed.Seed, DepthIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
}
bool EvaluateStrate(const FVoxelSeasonStrate& Strate,
@@ -1739,7 +1960,13 @@ namespace VoxelSeasonCompositionPrivate
FVoxelOpContext Context;
FString BuildError;
bool bBuilt = false;
if (Strate.bUsesRecipe)
if (Strate.bUsesRegions)
{
bBuilt = VF_BuildStrateRegionStack(
Strate.Regions, Settings.OriginSpineRadius, nullptr,
Stack, Context, &BuildError);
}
else if (Strate.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Strate.Recipe, Strate.Params, Strate.Seed,
Settings.OriginSpineRadius, nullptr, Stack,
@@ -1960,6 +2187,21 @@ namespace VoxelSeasonCompositionPrivate
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
}
void SetRegionRuntimeBounds(FVoxelStrateRegionManifest& Regions,
int32 Seed, int32 StrateIndex,
float TopWorldZ, float BottomWorldZ)
{
VF_RekeyStrateRegionManifest(Regions, Seed, StrateIndex);
Regions.StrateTopWorldZ = TopWorldZ;
Regions.StrateBottomWorldZ = BottomWorldZ;
Regions.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : Regions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TopWorldZ, BottomWorldZ);
}
}
bool SameFloatBits(float A, float B)
{
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
@@ -2353,6 +2595,15 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
Settings.StrateHeightInChunks, TopWorldZ,
BottomWorldZ, Strate);
Strate.DepthIndex = DepthIndex;
if (Strate.bUsesRegions)
{
// Candidate probes use the attempt index only to make their provisional
// measurements independent. The persisted world is keyed by its absolute
// strate slot, so rekey before the final placement gate and measurement.
SetRegionRuntimeBounds(Strate.Regions, Strate.Seed, DepthIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
FSeasonEvaluation Evaluation;
if (!EvaluateStrate(Strate, Settings, Evaluation))
{
@@ -2592,6 +2843,21 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
const float StoredTop = static_cast<float>(Strate.TopWorldZ);
const float StoredBottom = static_cast<float>(Strate.BottomWorldZ);
if (Strate.bUsesRegions)
{
if (!Strate.Regions.IsValid()
|| Strate.Regions.RegionCount <= 1
|| Strate.Regions.Seed != Strate.Seed
|| Strate.Regions.StrateIndex != Strate.DepthIndex
|| Strate.Regions.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Strate.Seed, Strate.DepthIndex)
|| !Strate.Regions.bHasGlobalStructuralParams
|| !SameFloatBits(Strate.Regions.StrateTopWorldZ, StoredTop)
|| !SameFloatBits(Strate.Regions.StrateBottomWorldZ, StoredBottom))
{
return Fail(TEXT("Manifest lateral region description is invalid or not keyed to its slot."));
}
}
if (!SameFloatBits(ActiveTop(Strate.Params, Strate.Archetype), StoredTop)
|| !SameFloatBits(ActiveBottom(Strate.Params, Strate.Archetype), StoredBottom))
{
@@ -2599,7 +2865,12 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
}
bool bBuilt = false;
if (Strate.bUsesRecipe)
if (Strate.bUsesRegions)
{
bBuilt = VF_BuildStrateRegionStack(Strate.Regions, SpineRadius, StrateManager,
OutStack, OutContext, OutError);
}
else if (Strate.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Strate.Recipe, Strate.Params, Strate.Seed,
SpineRadius, StrateManager, OutStack, OutContext,
@@ -22,6 +22,7 @@
#include "VoxelStrateDefinition.h"
#include "VoxelTerrainOpDefinition.h"
#include "VoxelDensityOpStack.h"
#include "VoxelCaveMorphology.h"
#include "VoxelTypes.h"
#include <type_traits>
@@ -4361,6 +4362,358 @@ bool FVoxelStrateCorpus::SetMeasuredMetrics(
return false;
}
void VF_SetStrateArchetypeRuntimeBounds(
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ)
{
Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ;
Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ;
Params.SlabParams.StrateTopWorldZ = TopWorldZ;
Params.SlabParams.StrateBottomWorldZ = BottomWorldZ;
Params.MazeParams.StrateTopWorldZ = TopWorldZ;
Params.MazeParams.StrateBottomWorldZ = BottomWorldZ;
Params.SurfaceParams.StrateTopWorldZ = TopWorldZ;
Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ;
Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ;
Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ;
Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ;
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
}
namespace VoxelStrateRegionPrivate
{
constexpr uint32 PartitionSalt = 0x5245474Eu; // "REGN"
constexpr uint32 PositionSaltX = 0x584F4646u; // "XOFF"
constexpr uint32 PositionSaltY = 0x594F4646u; // "YOFF"
constexpr uint32 RegionSalt = 0x52494458u; // "RIDX"
static int32 CellAt(float World, float CellSize)
{
return FMath::FloorToInt(World / CellSize);
}
static FVoxelStrateRegionSite MakeSite(int32 CellX, int32 CellY,
int32 InRegionCount, uint32 InPartitionSeed,
float CellSize)
{
FVoxelStrateRegionSite Site;
Site.CellX = CellX;
Site.CellY = CellY;
const uint32 H = VoxelHash::Cell(CellX, CellY, InPartitionSeed ^ PartitionSalt);
const float JitterX = VoxelHash::ToFloatSigned(
VoxelHash::Mix(H ^ PositionSaltX)) * 0.35f;
const float JitterY = VoxelHash::ToFloatSigned(
VoxelHash::Mix(H ^ PositionSaltY)) * 0.35f;
Site.WorldX = (static_cast<float>(CellX) + 0.5f + JitterX) * CellSize;
Site.WorldY = (static_cast<float>(CellY) + 0.5f + JitterY) * CellSize;
const uint32 RegionHash = VoxelHash::Cell(
CellX, CellY, InPartitionSeed ^ RegionSalt);
Site.Region = InRegionCount > 1
? static_cast<int32>(RegionHash % static_cast<uint32>(InRegionCount)) : 0;
return Site;
}
static void GatherSites(float WorldX, float WorldY, int32 InRegionCount,
uint32 InPartitionSeed, float CellSize,
TArray<FVoxelStrateRegionSite>& OutSites)
{
OutSites.Reset();
if (!(CellSize > 0.0f) || !FMath::IsFinite(CellSize))
{
return;
}
const int32 CenterX = CellAt(WorldX, CellSize);
const int32 CenterY = CellAt(WorldY, CellSize);
OutSites.Reserve(25);
for (int32 DY = -2; DY <= 2; ++DY)
{
for (int32 DX = -2; DX <= 2; ++DX)
{
OutSites.Add(MakeSite(CenterX + DX, CenterY + DY,
InRegionCount, InPartitionSeed, CellSize));
}
}
}
static bool IsEarlierSite(const FVoxelStrateRegionSite& A,
const FVoxelStrateRegionSite& B)
{
if (A.CellX != B.CellX) { return A.CellX < B.CellX; }
return A.CellY < B.CellY;
}
static FVoxelStrateRegionQuery QuerySites(
const TArray<FVoxelStrateRegionSite>& Sites, int32 InRegionCount,
float BlendWidth, float CellSize, float WorldX, float WorldY)
{
FVoxelStrateRegionQuery Result;
if (InRegionCount <= 1 || Sites.Num() == 0)
{
return Result;
}
Result.NearestDifferentRegionGap = 0.0f;
const int32 CenterCellX = CellAt(WorldX, CellSize);
const int32 CenterCellY = CellAt(WorldY, CellSize);
int32 PrimaryIndex = INDEX_NONE;
int32 NeighborIndex = INDEX_NONE;
float PrimaryDistanceSq = FLT_MAX;
float NeighborDistanceSq = FLT_MAX;
for (int32 Index = 0; Index < Sites.Num(); ++Index)
{
const FVoxelStrateRegionSite& Site = Sites[Index];
if (FMath::Abs(Site.CellX - CenterCellX) > 2
|| FMath::Abs(Site.CellY - CenterCellY) > 2)
{
continue;
}
const float DX = WorldX - Site.WorldX;
const float DY = WorldY - Site.WorldY;
const float DistanceSq = DX * DX + DY * DY;
if (PrimaryIndex == INDEX_NONE
|| DistanceSq < PrimaryDistanceSq
|| (DistanceSq == PrimaryDistanceSq
&& IsEarlierSite(Site, Sites[PrimaryIndex])))
{
PrimaryIndex = Index;
PrimaryDistanceSq = DistanceSq;
}
}
const int32 PrimaryRegion = Sites[PrimaryIndex].Region;
for (int32 Index = 0; Index < Sites.Num(); ++Index)
{
if (FMath::Abs(Sites[Index].CellX - CenterCellX) > 2
|| FMath::Abs(Sites[Index].CellY - CenterCellY) > 2)
{
continue;
}
if (Sites[Index].Region == PrimaryRegion) { continue; }
const float DX = WorldX - Sites[Index].WorldX;
const float DY = WorldY - Sites[Index].WorldY;
const float DistanceSq = DX * DX + DY * DY;
if (NeighborIndex == INDEX_NONE
|| DistanceSq < NeighborDistanceSq
|| (DistanceSq == NeighborDistanceSq
&& IsEarlierSite(Sites[Index], Sites[NeighborIndex])))
{
NeighborIndex = Index;
NeighborDistanceSq = DistanceSq;
}
}
Result.PrimaryRegion = PrimaryRegion;
if (NeighborIndex == INDEX_NONE)
{
return Result;
}
const float NeighborDistance = FMath::Sqrt(FMath::Max(NeighborDistanceSq, 0.0f));
const float Gap = FMath::Max(
NeighborDistance
- FMath::Sqrt(FMath::Max(PrimaryDistanceSq, 0.0f)), 0.0f);
Result.NeighborRegion = Sites[NeighborIndex].Region;
Result.NearestDifferentRegionGap = Gap;
// A site outside the ±2-cell window is at least 2.15 cell lengths from a point in the
// center cell (site jitter is bounded to ±0.35 around the cell midpoint). A candidate
// within 2.0 cell lengths is therefore known to beat every omitted site. Otherwise the
// density path can conservatively use the candidate/zero weight, but box proof must not
// treat the local window as an infinite search.
Result.bNearestDifferentRegionKnown = FMath::IsFinite(CellSize)
&& CellSize > 0.0f
&& NeighborDistance <= 2.0f * CellSize;
if (!(BlendWidth > 0.0f))
{
return Result;
}
// The vertical transition is linear from 0 at the outer edge to 1 at the boundary. A
// Voronoi boundary is shared by two regions, so the same curve is split symmetrically:
// each side contributes 0.5 at the bisector and 0 outside the band.
const float Curve = FMath::Clamp(1.0f - Gap / BlendWidth, 0.0f, 1.0f);
Result.NeighborWeight = 0.5f * Curve;
Result.bInBlendBand = Result.NeighborWeight > 0.0f;
return Result;
}
static FVoxelStrateRegionQuery QueryWithGather(
float WorldX, float WorldY, int32 InRegionCount, uint32 InPartitionSeed,
float CellSize, float BlendWidth)
{
TArray<FVoxelStrateRegionSite> Sites;
GatherSites(WorldX, WorldY, InRegionCount, InPartitionSeed, CellSize, Sites);
return QuerySites(Sites, InRegionCount, BlendWidth, CellSize, WorldX, WorldY);
}
}
int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex)
{
return 1 + static_cast<int32>(VoxelHash::Cell(
StrateIndex, StrateIndex ^ 0x6D, static_cast<uint32>(Seed) ^ 0x524F4C4Cu) % 3u);
}
uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex)
{
return VoxelHash::Cell(StrateIndex, StrateIndex ^ 0x6D,
static_cast<uint32>(Seed) ^ 0x52454750u);
}
void VF_RekeyStrateRegionManifest(
FVoxelStrateRegionManifest& Manifest, int32 InSeed, int32 InStrateIndex)
{
Manifest.Seed = InSeed;
Manifest.StrateIndex = InStrateIndex;
Manifest.PartitionSeed = VF_GetStrateRegionPartitionSeed(InSeed, InStrateIndex);
}
FVoxelStrateRegionQuery VF_QueryStrateRegion(
const FVoxelStrateRegionManifest& Manifest, float WorldX, float WorldY)
{
// The retained seed is an offline artifact, not an alternate identity. Reject a stale or
// hand-edited partition key instead of silently evaluating a different world for the same
// (seed, strate-index) pair.
if (!Manifest.IsValid()
|| Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
return FVoxelStrateRegionQuery();
}
return VoxelStrateRegionPrivate::QueryWithGather(
WorldX, WorldY, Manifest.RegionCount, Manifest.PartitionSeed,
Manifest.LatticeCellSize, Manifest.BlendWidth);
}
void FVoxelStrateRegionPartitionCache::PrepareForChunk(
const FVoxelStrateRegionManifest& Manifest, const FIntVector& ChunkCoord)
{
RegionCount = Manifest.RegionCount;
PartitionSeed = Manifest.PartitionSeed;
LatticeCellSize = Manifest.LatticeCellSize;
BlendWidth = Manifest.BlendWidth;
BaseX = ChunkCoord.X * CHUNK_SIZE - 1;
BaseY = ChunkCoord.Y * CHUNK_SIZE - 1;
Dim = CHUNK_SIZE + 3;
Sites.Reset();
IntegerSamples.Reset();
if (!Manifest.IsValid()
|| Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
RegionCount = 1;
Dim = 0;
return;
}
const int32 MinCellX = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseX), LatticeCellSize) - 2;
const int32 MaxCellX = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseX + Dim - 1), LatticeCellSize) + 2;
const int32 MinCellY = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseY), LatticeCellSize) - 2;
const int32 MaxCellY = VoxelStrateRegionPrivate::CellAt(
static_cast<float>(BaseY + Dim - 1), LatticeCellSize) + 2;
Sites.Reserve((MaxCellX - MinCellX + 1) * (MaxCellY - MinCellY + 1));
for (int32 CY = MinCellY; CY <= MaxCellY; ++CY)
{
for (int32 CX = MinCellX; CX <= MaxCellX; ++CX)
{
Sites.Add(VoxelStrateRegionPrivate::MakeSite(
CX, CY, RegionCount, PartitionSeed, LatticeCellSize));
}
}
IntegerSamples.SetNum(Dim * Dim);
for (int32 Y = 0; Y < Dim; ++Y)
{
for (int32 X = 0; X < Dim; ++X)
{
IntegerSamples[Y * Dim + X] = VoxelStrateRegionPrivate::QuerySites(
Sites, RegionCount, BlendWidth, LatticeCellSize,
static_cast<float>(BaseX + X), static_cast<float>(BaseY + Y));
}
}
}
FVoxelStrateRegionQuery FVoxelStrateRegionPartitionCache::Query(
float WorldX, float WorldY) const
{
if (Dim > 0
&& WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY))
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
const int32 LocalX = IX - BaseX;
const int32 LocalY = IY - BaseY;
if (LocalX >= 0 && LocalX < Dim && LocalY >= 0 && LocalY < Dim)
{
return IntegerSamples[LocalY * Dim + LocalX];
}
}
// The production stack is prepared for the current chunk before its first sample. Reuse the
// prepared local site set for the rare fractional probe as well; rebuilding a lattice window
// here would turn a cache miss into a per-voxel allocation/hash walk. The prepared set has a
// one-voxel halo, which covers the mesher's gradient probes. The pure public query remains the
// fallback for an intentionally out-of-window diagnostic call.
const float PreparedMaxX = static_cast<float>(BaseX + Dim - 1);
const float PreparedMaxY = static_cast<float>(BaseY + Dim - 1);
if (Sites.Num() > 0 && Dim > 0
&& WorldX >= static_cast<float>(BaseX) && WorldX <= PreparedMaxX
&& WorldY >= static_cast<float>(BaseY) && WorldY <= PreparedMaxY)
{
return VoxelStrateRegionPrivate::QuerySites(
Sites, RegionCount, BlendWidth, LatticeCellSize, WorldX, WorldY);
}
return VoxelStrateRegionPrivate::QueryWithGather(
WorldX, WorldY, RegionCount, PartitionSeed,
LatticeCellSize, BlendWidth);
}
FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
const FVoxelStrateRegionManifest& Manifest, const FBox& VoxelBox)
{
FVoxelStrateRegionBoxProof Proof;
if (!Manifest.IsValid()
|| Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Manifest.Seed, Manifest.StrateIndex))
{
return Proof;
}
if (Manifest.RegionCount <= 1)
{
Proof.bProvablySingleRegion = true;
Proof.bTouchesRegionBoundary = false;
Proof.bTouchesBlendBand = false;
return Proof;
}
const float HalfX = FMath::Max((float)(VoxelBox.Max.X - VoxelBox.Min.X) * 0.5f, 0.0f);
const float HalfY = FMath::Max((float)(VoxelBox.Max.Y - VoxelBox.Min.Y) * 0.5f, 0.0f);
const float Radius = FMath::Sqrt(HalfX * HalfX + HalfY * HalfY);
const float CenterX = ((float)VoxelBox.Min.X + (float)VoxelBox.Max.X) * 0.5f;
const float CenterY = ((float)VoxelBox.Min.Y + (float)VoxelBox.Max.Y) * 0.5f;
const FVoxelStrateRegionQuery Center = VF_QueryStrateRegion(Manifest, CenterX, CenterY);
if (!Center.bNearestDifferentRegionKnown)
{
return Proof;
}
const float GapLowerBound = Center.NearestDifferentRegionGap - 2.0f * Radius;
// The difference of two Euclidean distances is 2-Lipschitz. Strict inequalities are
// intentional: an equality may put a sample exactly on a bisector or at the blend cutoff,
// and that is not a proof a mesher is allowed to skip.
Proof.bTouchesRegionBoundary = !(GapLowerBound > 0.0f);
Proof.bTouchesBlendBand = !(GapLowerBound > Manifest.BlendWidth);
Proof.bProvablySingleRegion = GapLowerBound > Manifest.BlendWidth;
return Proof;
}
FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index)
{
@@ -4831,6 +5184,246 @@ FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index)
return Recipe;
}
namespace VoxelStrateRegionPrivate
{
static ECaveGeneratorType ArchetypeForRecipe(const FVoxelOpStackRecipe& Recipe)
{
switch (Recipe.StructuralParamBlock)
{
case EVoxelStrateParamBlock::TunnelNetwork: return ECaveGeneratorType::TunnelNetwork;
case EVoxelStrateParamBlock::Slab: return ECaveGeneratorType::FlatPlain;
case EVoxelStrateParamBlock::Maze: return ECaveGeneratorType::Maze;
case EVoxelStrateParamBlock::Surface: return ECaveGeneratorType::SurfaceWorld;
case EVoxelStrateParamBlock::VerticalShaft: return ECaveGeneratorType::VerticalShafts;
case EVoxelStrateParamBlock::FloatingIsland: return ECaveGeneratorType::FloatingIslands;
default: return ECaveGeneratorType::TunnelNetwork;
}
}
static EVoxelStrateParamBlock ParamBlockForArchetype(ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber: return EVoxelStrateParamBlock::Slab;
case ECaveGeneratorType::Maze: return EVoxelStrateParamBlock::Maze;
case ECaveGeneratorType::SurfaceWorld: return EVoxelStrateParamBlock::Surface;
case ECaveGeneratorType::VerticalShafts: return EVoxelStrateParamBlock::VerticalShaft;
case ECaveGeneratorType::FloatingIslands:return EVoxelStrateParamBlock::FloatingIsland;
case ECaveGeneratorType::Underwater:
case ECaveGeneratorType::TunnelNetwork:
default: return EVoxelStrateParamBlock::TunnelNetwork;
}
}
static void CopyParamBlock(FVoxelStrateArchetypeParams& OutParams,
const FVoxelStrateRollInfo& Roll,
ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams;
break;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
OutParams.SlabParams = Roll.ArchetypeParams.SlabParams;
break;
case ECaveGeneratorType::Maze:
OutParams.MazeParams = Roll.ArchetypeParams.MazeParams;
break;
case ECaveGeneratorType::SurfaceWorld:
OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
break;
case ECaveGeneratorType::VerticalShafts:
OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
break;
case ECaveGeneratorType::FloatingIslands:
OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
break;
default:
break;
}
}
static float BoundarySeal(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BoundarySealThickness;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BoundarySealThickness;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BoundarySealThickness;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BoundarySealThickness;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BoundarySealThickness;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.BoundarySealThickness;
}
}
static float BaseDensity(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BaseDensity;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BaseDensity;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BaseDensity;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BaseDensity;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BaseDensity;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.BaseDensity;
}
}
static float Top(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateTopWorldZ;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.StrateTopWorldZ;
}
}
static float Bottom(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateBottomWorldZ;
case EVoxelStrateParamBlock::TunnelNetwork:
default: return Params.TunnelNetworkParams.StrateBottomWorldZ;
}
}
}
bool VF_LateralRegionsAreShippable()
{
// ⛔ FALSE ON PURPOSE. See the declaration in VoxelStrateComposer.h for the full reasoning.
//
// FR : volontairement désactivé — la loi primordiale ne tient pas encore au travers d'une
// couture entre régions (9/16 graines mesurées le 2026-09-05).
//
// Measured: arrival -> departure across a region seam passes 9/16 seeds. A strate that mixes
// archetypes but strands the player at the boundary is worse than one that does not mix.
// Flipping this without an explicit cross-seam corridor contract turns the suite red by design.
return false;
}
FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 StrateIndex, bool bRollStructure)
{
FVoxelStrateRegionManifest Manifest;
Manifest.RegionCount = VF_RollStrateRegionCount(Seed, StrateIndex);
VF_RekeyStrateRegionManifest(Manifest, Seed, StrateIndex);
Manifest.Regions.Reserve(Manifest.RegionCount);
Manifest.bValid = true;
static const ECaveGeneratorType StructureBlocks[] =
{
ECaveGeneratorType::TunnelNetwork,
ECaveGeneratorType::FlatPlain,
ECaveGeneratorType::Maze,
ECaveGeneratorType::SurfaceWorld,
ECaveGeneratorType::VerticalShafts,
ECaveGeneratorType::FloatingIslands,
};
static const EVoxelStrateParamBlock BlockIds[] =
{
EVoxelStrateParamBlock::TunnelNetwork,
EVoxelStrateParamBlock::Slab,
EVoxelStrateParamBlock::Maze,
EVoxelStrateParamBlock::Surface,
EVoxelStrateParamBlock::VerticalShaft,
EVoxelStrateParamBlock::FloatingIsland,
};
static const uint32 BlockSalts[] = { 0x1001u, 0x1003u, 0x1005u,
0x1007u, 0x1009u, 0x100Bu };
for (int32 RegionIndex = 0; RegionIndex < Manifest.RegionCount; ++RegionIndex)
{
FVoxelStrateRegion& Region = Manifest.Regions.AddDefaulted_GetRef();
Region.RegionIndex = RegionIndex;
Region.Seed = RegionIndex == 0
? Seed
: static_cast<int32>(VoxelHash::Cell(
StrateIndex, RegionIndex, static_cast<uint32>(Seed) ^ 0x52454753u));
Region.bUsesRecipe = bRollStructure;
if (!bRollStructure)
{
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailed(
Corpus, Region.Seed, StrateIndex);
if (!Roll.bValid)
{
Manifest.bValid = false;
if (Manifest.FailureReason.IsEmpty())
{
Manifest.FailureReason = Roll.FailureReason;
}
continue;
}
Region.Archetype = Roll.Archetype;
Region.ArchetypeParams = Roll.ArchetypeParams;
}
else
{
Region.Recipe = VF_RollStrateStructure(Region.Seed, StrateIndex);
Region.Archetype = VoxelStrateRegionPrivate::ArchetypeForRecipe(Region.Recipe);
for (int32 BlockIndex = 0; BlockIndex < UE_ARRAY_COUNT(StructureBlocks); ++BlockIndex)
{
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailedForArchetype(
Corpus, StructureBlocks[BlockIndex],
Region.Seed ^ static_cast<int32>(BlockSalts[BlockIndex]), StrateIndex);
if (!Roll.bValid)
{
Manifest.bValid = false;
if (Manifest.FailureReason.IsEmpty())
{
Manifest.FailureReason = Roll.FailureReason;
}
continue;
}
VoxelStrateRegionPrivate::CopyParamBlock(
Region.ArchetypeParams, Roll, StructureBlocks[BlockIndex]);
}
}
}
if (Manifest.Regions.Num() == Manifest.RegionCount && Manifest.RegionCount > 0)
{
const FVoxelStrateRegion& First = Manifest.Regions[0];
Manifest.StructuralParamBlock = bRollStructure
? First.Recipe.StructuralParamBlock
: VoxelStrateRegionPrivate::ParamBlockForArchetype(First.Archetype);
Manifest.StrateTopWorldZ = VoxelStrateRegionPrivate::Top(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.StrateBottomWorldZ = VoxelStrateRegionPrivate::Bottom(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.BoundarySealThickness = VoxelStrateRegionPrivate::BoundarySeal(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.BaseDensity = VoxelStrateRegionPrivate::BaseDensity(
First.ArchetypeParams, Manifest.StructuralParamBlock);
Manifest.bHasGlobalStructuralParams = FMath::IsFinite(Manifest.StrateTopWorldZ)
&& FMath::IsFinite(Manifest.StrateBottomWorldZ)
&& Manifest.StrateTopWorldZ > Manifest.StrateBottomWorldZ
&& FMath::IsFinite(Manifest.BoundarySealThickness)
&& FMath::IsFinite(Manifest.BaseDensity);
}
return Manifest;
}
#if WITH_EDITOR
namespace
{
@@ -4919,6 +5512,13 @@ FVoxelStrateComposerCandidate VF_RollStrateCandidate(
Candidate.Archetype = Candidate.ParameterRoll.Archetype;
Candidate.ArchetypeParams = Candidate.ParameterRoll.ArchetypeParams;
Candidate.Regions = VF_RollStrateRegionManifest(Corpus, Seed, Index, false);
if (!Candidate.Regions.IsValid())
{
Candidate.FailureReason = Candidate.Regions.FailureReason.IsEmpty()
? TEXT("lateral region roll failed") : Candidate.Regions.FailureReason;
return Candidate;
}
Candidate.bValid = true;
return Candidate;
}
@@ -4951,7 +5551,13 @@ FVoxelStrateComposerCandidate VF_RollStrateCandidate(
VF_CopyComposerBlock(Candidate.ArchetypeParams, Roll, Spec.Archetype);
}
Candidate.bValid = bAllBlocksValid;
Candidate.Regions = VF_RollStrateRegionManifest(Corpus, Seed, Index, true);
if (!Candidate.Regions.IsValid() && Candidate.FailureReason.IsEmpty())
{
Candidate.FailureReason = Candidate.Regions.FailureReason.IsEmpty()
? TEXT("lateral region roll failed") : Candidate.Regions.FailureReason;
}
Candidate.bValid = bAllBlocksValid && Candidate.Regions.IsValid();
return Candidate;
}
@@ -19,6 +19,8 @@ struct FVoxelStrateComposerSlotOverride
FVoxelStrateArchetypeParams Params;
bool bUseRecipe = false;
FVoxelOpStackRecipe Recipe;
bool bUseRegions = false;
FVoxelStrateRegionManifest Regions;
};
static void VF_SetComposerRuntimeBounds(
@@ -286,7 +288,8 @@ const FVoxelStrateComposerSlotOverride* UVoxelStrateManager::FindComposerOverrid
bool UVoxelStrateManager::SetComposerOverrideForStrate(
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
const FVoxelOpStackRecipe* Recipe, FString& OutError)
const FVoxelOpStackRecipe* Recipe, FString& OutError,
const FVoxelStrateRegionManifest* InRegions)
{
OutError.Reset();
@@ -342,6 +345,32 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
Override->Recipe = *Recipe;
}
if (InRegions != nullptr && InRegions->RegionCount > 1)
{
if (!InRegions->IsValid())
{
OutError = InRegions->FailureReason.IsEmpty()
? TEXT("The composer returned an invalid lateral region manifest.")
: InRegions->FailureReason;
return false;
}
Override->bUseRegions = true;
Override->Regions = *InRegions;
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE;
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * CHUNK_SIZE;
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
Override->Regions.StrateTopWorldZ,
Override->Regions.StrateBottomWorldZ);
for (FVoxelStrateRegion& Region : Override->Regions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
Override->Regions.StrateTopWorldZ,
Override->Regions.StrateBottomWorldZ);
}
Override->Regions.bHasGlobalStructuralParams = true;
}
// Only one slot is overridden at a time. Keeping this map small also makes the worker-side
// copy-on-chunk-refetch cheap. Passage geometry is deliberately not regenerated: the layout
// and its passages are unchanged, and this is the same manager state used by the offline
@@ -381,6 +410,25 @@ bool UVoxelStrateManager::GetComposerOverrideForChunk(
return true;
}
bool UVoxelStrateManager::GetComposerRegionOverrideForChunk(
const FIntVector& ChunkCoord, FVoxelStrateRegionManifest& OutRegions) const
{
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
if (SlotIdx < 0)
{
return false;
}
const FVoxelStrateComposerSlotOverride* Override =
FindComposerOverride(StrateLayout[SlotIdx].StrateIndex);
if (Override == nullptr || !Override->bUseRegions)
{
return false;
}
OutRegions = Override->Regions;
return true;
}
bool UVoxelStrateManager::HasComposerRecipeOverride() const
{
for (const TPair<int32, TSharedPtr<FVoxelStrateComposerSlotOverride>>& Pair : ComposerOverrides)
+42 -4
View File
@@ -194,6 +194,7 @@ namespace
const FVoxelStrateComposerCandidate& Candidate,
const UVoxelGenerator& Generator,
const UVoxelStrateManager& StrateManager,
int32 TargetStrateIndex,
int32 TargetTopChunkZ,
int32 TargetBottomChunkZ,
FVoxelOpStack& OutStack,
@@ -206,7 +207,24 @@ namespace
VF_SetComposerRuntimeBounds(Params, TopWorldZ, BottomWorldZ);
bool bBuilt = false;
if (Candidate.bStructureRoll)
if (Candidate.Regions.RegionCount > 1)
{
FVoxelStrateRegionManifest RegionManifest = Candidate.Regions;
VF_RekeyStrateRegionManifest(RegionManifest, Candidate.Seed,
TargetStrateIndex);
RegionManifest.StrateTopWorldZ = TopWorldZ;
RegionManifest.StrateBottomWorldZ = BottomWorldZ;
RegionManifest.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : RegionManifest.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TopWorldZ, BottomWorldZ);
}
bBuilt = VF_BuildStrateRegionStack(
RegionManifest, Generator.OriginSpineRadius, &StrateManager,
OutStack, OutContext, &OutError);
}
else if (Candidate.bStructureRoll)
{
bBuilt = VF_BuildStackFromRecipe(
Candidate.Recipe, Params, Generator.Seed, Generator.OriginSpineRadius,
@@ -573,7 +591,25 @@ void AVoxelWorld::ApplyComposerCandidate()
FVoxelOpContext PreflightContext;
FString PreflightError;
bool bPreflightBuilt = false;
if (Candidate.bStructureRoll)
FVoxelStrateRegionManifest PreflightRegions;
if (Candidate.Regions.RegionCount > 1)
{
PreflightRegions = Candidate.Regions;
VF_RekeyStrateRegionManifest(PreflightRegions, Candidate.Seed,
ComposerTargetStrateIndex);
PreflightRegions.StrateTopWorldZ = TargetTopWorldZ;
PreflightRegions.StrateBottomWorldZ = TargetBottomWorldZ;
PreflightRegions.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : PreflightRegions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TargetTopWorldZ, TargetBottomWorldZ);
}
bPreflightBuilt = VF_BuildStrateRegionStack(
PreflightRegions, Generator->OriginSpineRadius, StrateManager,
PreflightStack, PreflightContext, &PreflightError);
}
else if (Candidate.bStructureRoll)
{
bPreflightBuilt = VF_BuildStackFromRecipe(
Candidate.Recipe, PreflightParams, Generator->Seed, Generator->OriginSpineRadius,
@@ -615,7 +651,8 @@ void AVoxelWorld::ApplyComposerCandidate()
if (!StrateManager->SetComposerOverrideForStrate(
ComposerTargetStrateIndex, Candidate.Seed, Candidate.Archetype,
Candidate.ArchetypeParams, Candidate.bStructureRoll,
Candidate.bStructureRoll ? &Candidate.Recipe : nullptr, ApplyError))
Candidate.bStructureRoll ? &Candidate.Recipe : nullptr, ApplyError,
Candidate.Regions.RegionCount > 1 ? &Candidate.Regions : nullptr))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d was not applied: %s"),
@@ -638,7 +675,8 @@ void AVoxelWorld::ApplyComposerCandidate()
FVoxelOpContext ReferenceContext;
FString ReferenceError;
const bool bReferenceBuilt = VF_BuildComposerReferenceStack(
Candidate, *Generator, *StrateManager, TargetTopChunkZ, TargetBottomChunkZ,
Candidate, *Generator, *StrateManager, ComposerTargetStrateIndex,
TargetTopChunkZ, TargetBottomChunkZ,
ReferenceStack, ReferenceContext, ReferenceError);
FString ComparableReason;
+26 -8
View File
@@ -41,6 +41,7 @@
class UVoxelStrateManager;
struct FVoxelOpStackRecipe;
struct FVoxelStrateArchetypeParams;
struct FVoxelStrateRegionManifest;
enum class EVoxelStrateOpClass : uint8;
/** The declarations the offline structure roller reads before it ever materialises an op. */
@@ -353,7 +354,8 @@ namespace VoxelDensityOps
const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams =
TArray<FSurfaceGenerationParams>(),
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr);
TUniquePtr<IVoxelBiomeField> BiomeField = nullptr,
bool bAppendStructuralPosts = true);
/**
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
@@ -365,7 +367,8 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* VerticalShafts — 9 ops, et **TROIS viennent de Maze sans une ligne de changement** :
@@ -378,7 +381,8 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* TunnelNetwork — **COMPLET, 20 ops** :
@@ -399,7 +403,8 @@ namespace VoxelDensityOps
VOXELFORGE_API void BuildTunnelNetworkStack(FVoxelOpStack& OutStack,
const FStrateGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
@@ -448,7 +453,8 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildFloatingIslandStack(FVoxelOpStack& OutStack, const FFloatingIslandParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
/**
* La pile Maze complète, décomposée — PAS un `FMazeOp` monolithique :
@@ -459,14 +465,26 @@ namespace VoxelDensityOps
*/
VOXELFORGE_API void BuildMazeStack(FVoxelOpStack& OutStack, const FMazeGenerationParams& P,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true);
}
/** Materialise an offline recipe. Structural posts are appended internally and cannot be omitted. */
/** Materialise an offline recipe. Structural posts are appended by default; lateral region cores
* deliberately pass false so the parent stack can append the global posts exactly once. */
VOXELFORGE_API bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const FVoxelStrateArchetypeParams& Params,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
FString* OutError = nullptr,
bool bAppendStructuralPosts = true);
/** Build the region combiner stack. Creative cores are independent; structural posts are global. */
VOXELFORGE_API bool VF_BuildStrateRegionStack(
const FVoxelStrateRegionManifest& Manifest,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
+20 -3
View File
@@ -65,7 +65,9 @@ struct VOXELFORGE_API FVoxelSeasonFixedStrate
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
bool bUsesRecipe = true;
bool bUsesRegions = false;
FString SourceDefinitionPath;
};
@@ -162,6 +164,9 @@ struct VOXELFORGE_API FVoxelSeasonStrate
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelOpStackRecipe Recipe;
FVoxelStrateArchetypeParams Params;
// When true this is the complete lateral parent description. Params/Archetype remain the
// compatibility identity of region zero; runtime rebuilds use Regions and never blend params.
FVoxelStrateRegionManifest Regions;
FVoxelStrateMeasuredMetrics Metrics;
double DistanceFromCorpusCentroid = 0.0;
@@ -173,6 +178,7 @@ struct VOXELFORGE_API FVoxelSeasonStrate
bool bBossSlot = false;
bool bFixed = false;
bool bUsesRecipe = true;
bool bUsesRegions = false;
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
@@ -224,8 +230,20 @@ struct VOXELFORGE_API FVoxelSeasonManifest
bool IsUsable() const
{
return bValid && Error.IsEmpty() && WorldRadiusVoxels == 0.0f
&& Strates.Num() == SelectedCount && SelectedCount > 0;
if (!bValid || !Error.IsEmpty() || WorldRadiusVoxels != 0.0f
|| Strates.Num() != SelectedCount || SelectedCount <= 0)
{
return false;
}
for (const FVoxelSeasonStrate& Strate : Strates)
{
if (Strate.bUsesRegions
&& (!Strate.Regions.IsValid() || Strate.Regions.RegionCount <= 1))
{
return false;
}
}
return true;
}
};
@@ -266,4 +284,3 @@ VOXELFORGE_API bool VF_RebuildVoxelSeasonStrate(
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
@@ -228,6 +228,142 @@ struct VOXELFORGE_API FVoxelStrateArchetypeParams
FFloatingIslandParams FloatingIslandParams;
};
/**
* One lateral region in a strate.
*
* This is deliberately a plain manifest record rather than a label on the strate. The recipe
* and the native vector belong to the region, and the runtime combines the resulting density
* fields. Structural posts are not region-owned.
*/
struct VOXELFORGE_API FVoxelStrateRegion
{
int32 RegionIndex = 0;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
bool bUsesRecipe = false;
FVoxelOpStackRecipe Recipe;
};
/**
* Offline-composed lateral region manifest for one strate.
*
* `StrateIndex` is part of the partition identity. A candidate can be moved to another live
* slot, so the editor hand-off must call VF_RekeyStrateRegionManifest before installing it.
* `PartitionSeed` is retained in the manifest so the runtime never has to recreate an RNG stream.
*/
struct VOXELFORGE_API FVoxelStrateRegionManifest
{
bool bValid = false;
FString FailureReason;
int32 Seed = 0;
int32 StrateIndex = 0;
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
// A 32-voxel chunk sees a tiny portion of this lattice. The values are manifest data, not
// hidden runtime tuning knobs; changing them changes the authored world and its hash.
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
// One global structural post configuration. Region recipes may use different native blocks,
// but no region is allowed to choose the spine/seal/passage/edge-seal values independently.
bool bHasGlobalStructuralParams = false;
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
float BoundarySealThickness = 0.0f;
float BaseDensity = 8.0f;
TArray<FVoxelStrateRegion> Regions;
bool IsSingleRegion() const { return RegionCount == 1 && Regions.Num() == 1; }
bool IsValid() const
{
return bValid && RegionCount >= 1 && RegionCount <= 3
&& Regions.Num() == RegionCount
&& FMath::IsFinite(LatticeCellSize) && LatticeCellSize > 0.0f
&& FMath::IsFinite(BlendWidth) && BlendWidth >= 0.0f;
}
};
/** Result of the positional Voronoi query used by the lateral density combiner. */
struct VOXELFORGE_API FVoxelStrateRegionQuery
{
int32 PrimaryRegion = 0;
int32 NeighborRegion = INDEX_NONE;
float NeighborWeight = 0.0f;
float NearestDifferentRegionGap = FLT_MAX;
// False means the bounded lattice neighborhood could not prove that its different-region
// candidate is the nearest one. Density evaluation may still use a zero blend weight, but a
// ClassifyBox proof must refuse to call that an infinite boundary distance.
bool bNearestDifferentRegionKnown = false;
bool bInBlendBand = false;
};
/** Conservative XY proof used before a tile may be skipped. */
struct VOXELFORGE_API FVoxelStrateRegionBoxProof
{
bool bProvablySingleRegion = false;
bool bTouchesRegionBoundary = true;
bool bTouchesBlendBand = true;
};
/** One hash-lattice site retained by the per-chunk partition cache. */
struct VOXELFORGE_API FVoxelStrateRegionSite
{
int32 CellX = 0;
int32 CellY = 0;
int32 Region = 0;
float WorldX = 0.0f;
float WorldY = 0.0f;
};
/**
* Worker-local cache for the lateral partition.
*
* Hash/site discovery and the integer XY grid are prepared once for a chunk. The voxel path
* then does an O(1) lookup for the ordinary integer density samples; only fractional gradient
* probes use the small prepared site list. No seed lattice is searched or allocated per voxel.
*/
struct VOXELFORGE_API FVoxelStrateRegionPartitionCache
{
void PrepareForChunk(const FVoxelStrateRegionManifest& Manifest,
const FIntVector& ChunkCoord);
FVoxelStrateRegionQuery Query(float WorldX, float WorldY) const;
private:
int32 RegionCount = 1;
uint32 PartitionSeed = 0;
float LatticeCellSize = 256.0f;
float BlendWidth = 24.0f;
int32 BaseX = 0;
int32 BaseY = 0;
int32 Dim = 0;
TArray<FVoxelStrateRegionSite> Sites;
TArray<FVoxelStrateRegionQuery> IntegerSamples;
};
/** Set every native family’s runtime Z bounds to the same strate interval. */
VOXELFORGE_API void VF_SetStrateArchetypeRuntimeBounds(
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
/** Stable identity helpers for the deterministic lateral partition. */
VOXELFORGE_API int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex);
VOXELFORGE_API uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex);
VOXELFORGE_API void VF_RekeyStrateRegionManifest(
FVoxelStrateRegionManifest& Manifest, int32 Seed, int32 StrateIndex);
/** Pure positional query; no retained state and no ordering-dependent RNG. */
VOXELFORGE_API FVoxelStrateRegionQuery VF_QueryStrateRegion(
const FVoxelStrateRegionManifest& Manifest, float WorldX, float WorldY);
/** Conservative proof for an XY projection of a voxel box. */
VOXELFORGE_API FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
const FVoxelStrateRegionManifest& Manifest, const FBox& VoxelBox);
/** One known-good authored vector and the archetype that gives it meaning. */
struct VOXELFORGE_API FVoxelStrateCorpusEntry
{
@@ -413,6 +549,7 @@ struct VOXELFORGE_API FVoxelStrateComposerCandidate
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
FVoxelOpStackRecipe Recipe;
FVoxelStrateRegionManifest Regions;
// Kept so automation/editor callers can audit the exact six independent structure blocks
// without re-rolling them through a second implementation.
@@ -532,6 +669,30 @@ VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
/** Roll one-to-three independent region recipes/vectors plus their deterministic partition. */
VOXELFORGE_API FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 StrateIndex, bool bRollStructure);
/**
* ⛔ THE LATERAL-REGION GATE. Returns false, and must keep returning false until the seam law holds.
*
* Lateral regions (archetypes mixing inside one strate — "tunnel network leading to a big chamber")
* are BUILT and MEASURED but NOT SHIPPABLE. Measured 2026-09-05: `arrival -> departure` across a
* region seam passes **9 of 16 seeds**, against 11/16 for same-region controls in the same harness.
*
* Density blending alone does not guarantee a player can cross a seam. A strate that mixes archetypes
* and strands you at the boundary is strictly worse than one that does not mix — so this stays off.
*
* ⚠️ DO NOT flip this to true to "enable the feature". The fix is an explicit corridor/landing
* contract across seams (a design decision, deliberately not made unilaterally), after which the
* measured rate must be 16/16. `VoxelForge.Composer.LateralRegions` asserts that this gate is shut
* whenever the rate is imperfect, so flipping it without fixing the law turns the suite red.
*
* Safe today regardless: single-region output is bit-identical to legacy, and cross-boundary box
* verdicts returned 0 violations over 199,800 brute-forced samples (all correctly `Mixed`).
*/
VOXELFORGE_API bool VF_LateralRegionsAreShippable();
/**
* Provenance for a parameter block that does not read the authored corpus.
*
@@ -27,6 +27,7 @@ class UVoxelSettings;
#if WITH_EDITOR
struct FVoxelStrateArchetypeParams;
struct FVoxelOpStackRecipe;
struct FVoxelStrateRegionManifest;
struct FVoxelStrateComposerSlotOverride;
#endif
@@ -158,7 +159,8 @@ public:
bool SetComposerOverrideForStrate(
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
const FVoxelOpStackRecipe* Recipe, FString& OutError);
const FVoxelOpStackRecipe* Recipe, FString& OutError,
const FVoxelStrateRegionManifest* Regions = nullptr);
/** Read the immutable editor override for a chunk into worker-local storage. */
bool GetComposerOverrideForChunk(
@@ -166,6 +168,10 @@ public:
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
bool& bOutUseRecipe, FVoxelOpStackRecipe& OutRecipe) const;
/** Read the immutable editor region override into worker-local storage. */
bool GetComposerRegionOverrideForChunk(
const FIntVector& ChunkCoord, FVoxelStrateRegionManifest& OutRegions) const;
/** Custom recipes cannot be safely classified by the native ClassifyBox proof yet. */
bool HasComposerRecipeOverride() const;
#endif