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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user