feat(runtime): the composed world can BE the game world
Jahni: "can you not implement those settings? as in, have them be live in
game?" This is that. The composer stops being a lab tool.
THE ARCHITECTURAL LINE, which is the whole design (COMPOSER-NOTES §3.1):
COMPOSITION stays editor-only, EVALUATION becomes runtime. Rolling,
measuring, preview, selection, promotion, the corpus -- none of it reaches
a player's machine. What crosses into the runtime is exactly two things:
reading a season manifest, and materialising a recipe into an op stack.
UVoxelSeasonAsset a cookable UPrimaryDataAsset holding the manifest,
so a season ships in a packaged game. The JSON
stays the reviewable, git-diffable artefact.
UVoxelSettings::Season optional. Unset => behaviour is exactly as today.
GetEffective*() season seed / spine radius / world radius / season
number override the authored values when assigned.
The test deliberately sets DIFFERENT authored
values, so "the season is authoritative" is proved
rather than coincidental.
⭐ MULTIPLAYER DETERMINISM, PROVED: two independent runtime managers built
the same season and compared 393,216 density samples BIT-IDENTICALLY. This
is the property the whole multiplayer model rests on -- replicate seed +
layout + diff, never geometry.
⭐ AND A STALE SEASON IS DETECTABLE, NOT SILENT. Initialize() now returns
bool and an assigned-but-invalid season NEVER falls back to the authored
pool. Falling back would hand a client a world that looks fine and is not
the season -- silent divergence, the worst failure mode in a shared world.
GetCurrentSeasonContentHash() is exposed BlueprintPure to compare at join.
HasComposerRecipeOverride() is gone. It force-classified every custom recipe
as Mixed, which was correct while box verdicts were only proved for
hand-written stacks -- but 8e346a3 made EffectOverBox correct in ISOLATION,
proved on a stack no builder produces. Keeping the force would have cost
tile skipping on every composed world forever.
VF_RollStrateRegionCount moved behind WITH_EDITOR while the partition-seed
helpers stayed runtime: rolling out, evaluation in. Lateral regions remain
gated off.
⚠️ Sol hit its usage limit mid-task, so the edits arrived unvalidated. I
built and ran them myself rather than assume -- and they did not compile:
VoxelForgeComposerSeasonTest.cpp was missing UObject/Package.h, which every
other test picks up transitively through the shared fixture it does not use.
One include; every NewObject/GetTransientPackage in the file was failing.
Editor build clean. SHIPPING BUILD SUCCEEDS -- the invariant that proves no
composer roller is linked into a packaged game. 30 succeeded / 0 failed.
⚠️ UNVERIFIED: whether it looks right on screen. Nothing headless can tell
us that; it needs Jahni to assign a season asset and press play.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
088b40216d
commit
faa62d419e
21 files changed
+1268
-287
No files matched your search
@@ -4,10 +4,12 @@
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "CoreGlobals.h" // GIsAutomationTesting — the opt-in diagnostic stays quiet under tests
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "VoxelStrateComposer.h"
|
||||
@@ -58,22 +60,46 @@ static float PassageFBM(float X, float Seed)
|
||||
return (MaxV > 0.0f) ? (Total / MaxV) : 0.0f;
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
{
|
||||
if (!Settings)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[StrateManager] No settings provided!"));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelSeasonManifest SeasonManifest;
|
||||
bool bUseSeason = false;
|
||||
if (!Settings->Season.IsNull())
|
||||
{
|
||||
UVoxelSeasonAsset* SeasonAsset = Settings->Season.LoadSynchronous();
|
||||
FString SeasonReport;
|
||||
if (SeasonAsset == nullptr
|
||||
|| !SeasonAsset->LoadManifest(SeasonManifest, SeasonReport))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[StrateManager] Season asset is assigned but unusable: %s"),
|
||||
SeasonAsset ? *SeasonReport : *Settings->Season.ToString());
|
||||
return false; // Fail closed: falling back to the authored pool would diverge peers.
|
||||
}
|
||||
bUseSeason = true;
|
||||
WorldSeed = SeasonManifest.Seed;
|
||||
}
|
||||
|
||||
StrateLayout.Empty();
|
||||
SeasonStrates.Empty();
|
||||
ActiveSeasonContentHash.Reset();
|
||||
#if WITH_EDITOR
|
||||
// A full layout rebuild discards any temporary walk-through candidate. The world calls this
|
||||
// under FScopedGenerationPause, so no worker can observe the map while it is being cleared.
|
||||
ComposerOverrides.Reset();
|
||||
#endif
|
||||
|
||||
const int32 TotalStrates = Settings->TotalStrates;
|
||||
const int32 TotalStrates = bUseSeason
|
||||
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
||||
const int32 LayoutGapChunks = bUseSeason
|
||||
? SeasonManifest.InterStrateGapChunks
|
||||
: FMath::Max(0, Settings->InterStrateGapChunks);
|
||||
|
||||
//=========================================================================
|
||||
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||
@@ -84,13 +110,16 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
|
||||
using FLoadedPoolEntry = TPair<FString, UVoxelStrateDefinition*>;
|
||||
TArray<FLoadedPoolEntry> ShuffledPool;
|
||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& SoftPtr : Settings->StratePool)
|
||||
if (!bUseSeason)
|
||||
{
|
||||
// Load the asset (synchronous for now — could be async later)
|
||||
UVoxelStrateDefinition* Def = SoftPtr.LoadSynchronous();
|
||||
if (Def)
|
||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& SoftPtr : Settings->StratePool)
|
||||
{
|
||||
ShuffledPool.Emplace(SoftPtr.ToString(), Def);
|
||||
// Load the asset (synchronous for now — could be async later)
|
||||
UVoxelStrateDefinition* Def = SoftPtr.LoadSynchronous();
|
||||
if (Def)
|
||||
{
|
||||
ShuffledPool.Emplace(SoftPtr.ToString(), Def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,12 +154,15 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
|
||||
// Pre-load fixed strate definitions
|
||||
TMap<int32, UVoxelStrateDefinition*> LoadedFixed;
|
||||
for (auto& Pair : Settings->FixedStrates)
|
||||
if (!bUseSeason)
|
||||
{
|
||||
UVoxelStrateDefinition* Def = Pair.Value.LoadSynchronous();
|
||||
if (Def)
|
||||
for (auto& Pair : Settings->FixedStrates)
|
||||
{
|
||||
LoadedFixed.Add(Pair.Key, Def);
|
||||
UVoxelStrateDefinition* Def = Pair.Value.LoadSynchronous();
|
||||
if (Def)
|
||||
{
|
||||
LoadedFixed.Add(Pair.Key, Def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,32 +174,97 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
FStrateSlot Slot;
|
||||
Slot.StrateIndex = i;
|
||||
|
||||
// Pick definition: fixed or from pool
|
||||
UVoxelStrateDefinition** FixedDef = LoadedFixed.Find(i);
|
||||
if (FixedDef && *FixedDef)
|
||||
if (bUseSeason)
|
||||
{
|
||||
Slot.Definition = *FixedDef;
|
||||
}
|
||||
else if (ShuffledPool.Num() > 0)
|
||||
{
|
||||
// Cycle through the pool (wraps around if more strates than pool entries)
|
||||
Slot.Definition = ShuffledPool[PoolCursor % ShuffledPool.Num()].Value;
|
||||
PoolCursor++;
|
||||
const FVoxelSeasonStrate& SeasonStrate = SeasonManifest.Strates[i];
|
||||
if (!SeasonStrate.SourceDefinitionPath.IsEmpty())
|
||||
{
|
||||
TSoftObjectPtr<UVoxelStrateDefinition> SourceDefinition(
|
||||
FSoftObjectPath(SeasonStrate.SourceDefinitionPath));
|
||||
UVoxelStrateDefinition* LoadedDefinition = SourceDefinition.LoadSynchronous();
|
||||
if (LoadedDefinition == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[StrateManager] Season slot %d could not load authored definition '%s'."),
|
||||
i, *SeasonStrate.SourceDefinitionPath);
|
||||
StrateLayout.Empty();
|
||||
SeasonStrates.Empty();
|
||||
return false;
|
||||
}
|
||||
// Keep the authored content/passage/visual bag without mutating the cooked asset,
|
||||
// then make the manifest's density identity authoritative below.
|
||||
Slot.Definition = DuplicateObject<UVoxelStrateDefinition>(LoadedDefinition, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The current density manifest has no wider content record. Keep all consumers
|
||||
// supplied with a stable definition object, but do not guess an authored theme.
|
||||
Slot.Definition = NewObject<UVoxelStrateDefinition>(this, NAME_None, RF_Transient);
|
||||
Slot.Definition->StrateName = FText::FromString(
|
||||
FString::Printf(TEXT("Season %d Strate %d"), SeasonManifest.Season, i));
|
||||
Slot.Definition->TransitionType = EVoxelStrateTransition::Hard;
|
||||
// Passage configuration is not part of schema v2. Inventing the UObject default
|
||||
// here would add an unreviewed tunnel to every generated boundary and make the
|
||||
// runtime field differ from the field that passed composition. The origin spine
|
||||
// remains the guaranteed connection until a later schema explicitly stores these.
|
||||
Slot.Definition->PassageConfig.Connections = 0;
|
||||
}
|
||||
|
||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
||||
Slot.Definition->bUseOperatorStack = SeasonStrate.bUsesRecipe;
|
||||
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
||||
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
||||
Slot.Definition->SurfaceParams = SeasonStrate.Params.SurfaceParams;
|
||||
Slot.Definition->VerticalShaftParams = SeasonStrate.Params.VerticalShaftParams;
|
||||
Slot.Definition->FloatingIslandParams = SeasonStrate.Params.FloatingIslandParams;
|
||||
|
||||
Slot.HeightInChunks = SeasonStrate.HeightInChunks;
|
||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / CHUNK_SIZE - 1;
|
||||
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / CHUNK_SIZE;
|
||||
if (SeasonStrate.TopWorldZ % CHUNK_SIZE != 0
|
||||
|| SeasonStrate.BottomWorldZ % CHUNK_SIZE != 0
|
||||
|| Slot.TopChunkZ != CurrentTopZ
|
||||
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
||||
StrateLayout.Empty();
|
||||
SeasonStrates.Empty();
|
||||
return false;
|
||||
}
|
||||
SeasonStrates.Add(SeasonStrate);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("[StrateManager] No strate definitions available for slot %d!"), i);
|
||||
continue;
|
||||
}
|
||||
// Pick definition: fixed or from pool
|
||||
UVoxelStrateDefinition** FixedDef = LoadedFixed.Find(i);
|
||||
if (FixedDef && *FixedDef)
|
||||
{
|
||||
Slot.Definition = *FixedDef;
|
||||
}
|
||||
else if (ShuffledPool.Num() > 0)
|
||||
{
|
||||
// Cycle through the pool (wraps around if more strates than pool entries)
|
||||
Slot.Definition = ShuffledPool[PoolCursor % ShuffledPool.Num()].Value;
|
||||
PoolCursor++;
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("[StrateManager] No strate definitions available for slot %d!"), i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute Z range from definition's height
|
||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||
Slot.TopChunkZ = CurrentTopZ;
|
||||
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
||||
// Compute Z range from definition's height
|
||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||
Slot.TopChunkZ = CurrentTopZ;
|
||||
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
||||
}
|
||||
|
||||
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
|
||||
// InterStrateGapChunks chunks between this strate and the next.
|
||||
CurrentTopZ = Slot.BottomChunkZ - 1 - FMath::Max(0, Settings->InterStrateGapChunks);
|
||||
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
|
||||
|
||||
StrateLayout.Add(Slot);
|
||||
|
||||
@@ -187,13 +284,17 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
int32 NumOperatorStackDisabledCaves = 0;
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (!Slot.Definition || Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld)
|
||||
const ECaveGeneratorType SlotArchetype = bUseSeason
|
||||
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
|
||||
if (!Slot.Definition || SlotArchetype == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
++NumCaveSlots;
|
||||
if (!Slot.Definition->bUseOperatorStack)
|
||||
const bool bUsesStack = bUseSeason
|
||||
? SeasonStrates[Slot.StrateIndex].bUsesRecipe : Slot.Definition->bUseOperatorStack;
|
||||
if (!bUsesStack)
|
||||
{
|
||||
++NumOperatorStackDisabledCaves;
|
||||
}
|
||||
@@ -226,9 +327,13 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
const bool bSeasonStack = bUseSeason
|
||||
&& SeasonStrates[Slot.StrateIndex].bUsesRecipe;
|
||||
const ECaveGeneratorType SlotArchetype = bUseSeason
|
||||
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
|
||||
if (!Slot.Definition
|
||||
|| Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld
|
||||
|| Slot.Definition->bUseOperatorStack)
|
||||
|| SlotArchetype == ECaveGeneratorType::SurfaceWorld
|
||||
|| (bUseSeason ? bSeasonStack : Slot.Definition->bUseOperatorStack))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -246,8 +351,13 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
|
||||
CachedSeed = WorldSeed;
|
||||
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
|
||||
OriginSpineRadius = Settings->OriginSpineRadius;
|
||||
InterStrateGapChunks = FMath::Max(0, Settings->InterStrateGapChunks);
|
||||
OriginSpineRadius = bUseSeason
|
||||
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
||||
InterStrateGapChunks = LayoutGapChunks;
|
||||
if (bUseSeason)
|
||||
{
|
||||
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
||||
}
|
||||
// Passage shape/count is per-strate now (UVoxelStrateDefinition::PassageConfig).
|
||||
|
||||
//=========================================================================
|
||||
@@ -260,6 +370,7 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (!Slot.Definition) continue;
|
||||
if (bUseSeason && SeasonStrates[Slot.StrateIndex].bUsesRecipe) continue;
|
||||
|
||||
for (const FStrateTerrainOpEntry& Entry : Slot.Definition->TerrainOperations)
|
||||
{
|
||||
@@ -275,6 +386,7 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
|
||||
// Generate passages between consecutive strates
|
||||
GeneratePassages();
|
||||
return bUseSeason ? IsUsingSeason() : true;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -429,18 +541,40 @@ bool UVoxelStrateManager::GetComposerRegionOverrideForChunk(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UVoxelStrateManager::HasComposerRecipeOverride() const
|
||||
#endif
|
||||
|
||||
bool UVoxelStrateManager::GetRecipeForChunk(
|
||||
const FIntVector& ChunkCoord, int32& OutRecipeSeed,
|
||||
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
|
||||
FVoxelOpStackRecipe& OutRecipe) const
|
||||
{
|
||||
for (const TPair<int32, TSharedPtr<FVoxelStrateComposerSlotOverride>>& Pair : ComposerOverrides)
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||
if (SlotIdx < 0) return false;
|
||||
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override =
|
||||
FindComposerOverride(StrateLayout[SlotIdx].StrateIndex))
|
||||
{
|
||||
if (Pair.Value.IsValid() && Pair.Value->bUseRecipe)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Override->bUseRecipe || Override->bUseRegions) return false;
|
||||
OutRecipeSeed = Override->CandidateSeed;
|
||||
OutArchetype = Override->Archetype;
|
||||
OutParams = Override->Params;
|
||||
OutRecipe = Override->Recipe;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
const FVoxelSeasonStrate& Strate = SeasonStrates[SlotIdx];
|
||||
if (!Strate.bUsesRecipe || Strate.bUsesRegions) return false;
|
||||
OutRecipeSeed = Strate.Seed;
|
||||
OutArchetype = Strate.Archetype;
|
||||
OutParams = Strate.Params;
|
||||
OutRecipe = Strate.Recipe;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
//=============================================================================
|
||||
// PASSAGE GENERATION
|
||||
@@ -619,10 +753,12 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
const bool bUsesRoomSeed =
|
||||
UpperDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| UpperDef->GeneratorType == ECaveGeneratorType::Underwater;
|
||||
const int32 UpperQuerySeed = bUsesRoomSeed
|
||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
|
||||
: CachedSeed;
|
||||
const int32 UpperQuerySeed = SeasonStrates.IsValidIndex(i)
|
||||
? SeasonStrates[i].Seed
|
||||
: (bUsesRoomSeed
|
||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
|
||||
: CachedSeed);
|
||||
|
||||
bAimedUpperAtOpenPoint = VF_SuggestLandingPoint(
|
||||
UpperDef->GeneratorType,
|
||||
@@ -657,10 +793,12 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
const bool bUsesRoomSeed =
|
||||
LowerDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| LowerDef->GeneratorType == ECaveGeneratorType::Underwater;
|
||||
const int32 LowerQuerySeed = bUsesRoomSeed
|
||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
|
||||
: CachedSeed;
|
||||
const int32 LowerQuerySeed = SeasonStrates.IsValidIndex(i + 1)
|
||||
? SeasonStrates[i + 1].Seed
|
||||
: (bUsesRoomSeed
|
||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
|
||||
: CachedSeed);
|
||||
|
||||
const float MaxLateralSnap = MaxLateralSnapFor(*LowerDef);
|
||||
bAimedLowerAtOpenPoint = VF_SuggestLandingPoint(
|
||||
@@ -1145,6 +1283,11 @@ ECaveGeneratorType UVoxelStrateManager::GetGeneratorTypeForChunk(const FIntVecto
|
||||
}
|
||||
#endif
|
||||
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
return SeasonStrates[SlotIdx].Archetype;
|
||||
}
|
||||
|
||||
return StrateLayout[SlotIdx].Definition->GeneratorType;
|
||||
}
|
||||
|
||||
@@ -1153,6 +1296,13 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
|
||||
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
// A recipe is an explicit stack opt-in. Native fixed entries retain the authored/switch
|
||||
// route while still reading their season parameter vector.
|
||||
return SeasonStrates[SlotIdx].bUsesRecipe;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
if (FindComposerOverride(StrateLayout[SlotIdx].StrateIndex) != nullptr)
|
||||
{
|
||||
@@ -1240,8 +1390,12 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
||||
// Copy the designer-authored slab params from the strate definition, unless an editor
|
||||
// composer candidate owns this slot.
|
||||
FSlabGenerationParams Result;
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
Result = SeasonStrates[SlotIdx].Params.SlabParams;
|
||||
}
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override =
|
||||
else if (const FVoxelStrateComposerSlotOverride* Override =
|
||||
FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
Result = (Override->Archetype == ECaveGeneratorType::FlatPlain
|
||||
@@ -1281,7 +1435,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
|
||||
}
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FMazeGenerationParams Result = Slot.Definition->MazeParams;
|
||||
FMazeGenerationParams Result = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Params.MazeParams : Slot.Definition->MazeParams;
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
@@ -1307,7 +1462,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
|
||||
}
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FSurfaceGenerationParams Result = Slot.Definition->SurfaceParams;
|
||||
FSurfaceGenerationParams Result = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Params.SurfaceParams : Slot.Definition->SurfaceParams;
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
@@ -1333,7 +1489,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
|
||||
}
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FVerticalShaftParams Result = Slot.Definition->VerticalShaftParams;
|
||||
FVerticalShaftParams Result = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Params.VerticalShaftParams : Slot.Definition->VerticalShaftParams;
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
@@ -1359,7 +1516,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
|
||||
}
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FFloatingIslandParams Result = Slot.Definition->FloatingIslandParams;
|
||||
FFloatingIslandParams Result = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Params.FloatingIslandParams : Slot.Definition->FloatingIslandParams;
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
@@ -1440,11 +1598,20 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
|
||||
|
||||
// Pull the relative level from whichever archetype owns water.
|
||||
float Rel = 0.0f;
|
||||
switch (Def->GeneratorType)
|
||||
const ECaveGeneratorType Archetype = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Archetype : Def->GeneratorType;
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::SurfaceWorld: Rel = Def->SurfaceParams.WaterLevelRelative; break;
|
||||
case ECaveGeneratorType::Underwater: Rel = Def->GenerationParams.WaterLevelRelative; break;
|
||||
default: Rel = Def->GenerationParams.WaterLevelRelative; break;
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
Rel = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Params.SurfaceParams.WaterLevelRelative
|
||||
: Def->SurfaceParams.WaterLevelRelative;
|
||||
break;
|
||||
default:
|
||||
Rel = SeasonStrates.IsValidIndex(SlotIdx)
|
||||
? SeasonStrates[SlotIdx].Params.TunnelNetworkParams.WaterLevelRelative
|
||||
: Def->GenerationParams.WaterLevelRelative;
|
||||
break;
|
||||
}
|
||||
if (Rel <= 0.0f) return -FLT_MAX;
|
||||
|
||||
@@ -1483,6 +1650,16 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
}
|
||||
#endif
|
||||
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||
// their boundary; blending them with another selected recipe would describe neither one.
|
||||
return Result;
|
||||
}
|
||||
|
||||
FStrateGenerationParams BaseParams = BuildParamsFromDefinition(Slot.Definition);
|
||||
|
||||
//=========================================================================
|
||||
|
||||
Reference in new issue
Block a user