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
@@ -5,10 +5,18 @@
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "UObject/Package.h" // GetTransientPackage() — this file does not use the shared
|
||||
// fixture, which is where the other tests pick it up implicitly.
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelSeasonManifest.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -121,6 +129,11 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
TestEqual(TEXT("loaded manifest has the same number of selected strates"),
|
||||
LoadedManifest.Strates.Num(), Manifest.Strates.Num());
|
||||
TestEqual(TEXT("loaded manifest content hash matches the composed payload"),
|
||||
LoadedManifest.ContentHash, Manifest.ContentHash);
|
||||
TestEqual(TEXT("manifest recomputes the hash it was composed from"),
|
||||
VF_ComputeVoxelSeasonManifestContentHash(LoadedManifest),
|
||||
LoadedManifest.ContentHash);
|
||||
|
||||
int64 ComparedDensitySamples = 0;
|
||||
bool bAllDensityBitsIdentical = true;
|
||||
@@ -202,6 +215,190 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
||||
ComparedDensitySamples));
|
||||
TestTrue(TEXT("manifest round-trip density field is bit-identical"),
|
||||
bAllDensityBitsIdentical);
|
||||
|
||||
// Runtime seam: embed the reviewed JSON in a cookable primary asset, then let two independent
|
||||
// managers/generators materialise the complete selected layout. The authored settings seed and
|
||||
// radius are deliberately different so the test proves the season is authoritative.
|
||||
TStrongObjectPtr<UVoxelSeasonAsset> SeasonAsset(
|
||||
NewObject<UVoxelSeasonAsset>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
FString AssetReport;
|
||||
TestTrue(TEXT("cookable season asset accepts the reviewed manifest"),
|
||||
SeasonAsset->SetManifestJson(Manifest.SerializedJson, AssetReport));
|
||||
|
||||
TStrongObjectPtr<UVoxelSettings> RuntimeSettings(
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
RuntimeSettings->Seed = Manifest.Seed + 17;
|
||||
RuntimeSettings->OriginSpineRadius = Manifest.OriginSpineRadius + 31.0f;
|
||||
RuntimeSettings->WorldRadiusVoxels = 4096.0f;
|
||||
RuntimeSettings->Season = TSoftObjectPtr<UVoxelSeasonAsset>(SeasonAsset.Get());
|
||||
|
||||
TStrongObjectPtr<UVoxelStrateManager> ManagerA(
|
||||
NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
TStrongObjectPtr<UVoxelStrateManager> ManagerB(
|
||||
NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
TestTrue(TEXT("first independent season manager initializes"),
|
||||
ManagerA->Initialize(RuntimeSettings.Get(), RuntimeSettings->GetEffectiveWorldSeed()));
|
||||
TestTrue(TEXT("second independent season manager initializes"),
|
||||
ManagerB->Initialize(RuntimeSettings.Get(), RuntimeSettings->GetEffectiveWorldSeed()));
|
||||
TestEqual(TEXT("season overrides the authored settings seed"),
|
||||
RuntimeSettings->GetEffectiveWorldSeed(), Manifest.Seed);
|
||||
TestEqual(TEXT("season keeps WorldRadiusVoxels zero"),
|
||||
RuntimeSettings->GetEffectiveWorldRadiusVoxels(), 0.0f);
|
||||
TestEqual(TEXT("season owns the origin spine radius used by recipe stacks"),
|
||||
RuntimeSettings->GetEffectiveOriginSpineRadius(), Manifest.OriginSpineRadius);
|
||||
TestEqual(TEXT("first manager loads every season slot"),
|
||||
ManagerA->GetNumStrates(), Manifest.Strates.Num());
|
||||
TestEqual(TEXT("second manager loads every season slot"),
|
||||
ManagerB->GetNumStrates(), Manifest.Strates.Num());
|
||||
TestEqual(TEXT("first manager exposes the reviewed content hash"),
|
||||
ManagerA->GetSeasonContentHash(), Manifest.ContentHash);
|
||||
TestEqual(TEXT("second manager exposes the reviewed content hash"),
|
||||
ManagerB->GetSeasonContentHash(), Manifest.ContentHash);
|
||||
|
||||
TStrongObjectPtr<UVoxelGenerator> GeneratorA(
|
||||
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
TStrongObjectPtr<UVoxelGenerator> GeneratorB(
|
||||
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
GeneratorA->InitializeSettings(RuntimeSettings.Get());
|
||||
GeneratorB->InitializeSettings(RuntimeSettings.Get());
|
||||
GeneratorA->SetStrateManager(ManagerA.Get());
|
||||
GeneratorB->SetStrateManager(ManagerB.Get());
|
||||
|
||||
int64 ManagerDensitySamples = 0;
|
||||
bool bManagersBitIdentical = true;
|
||||
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
|
||||
{
|
||||
for (int32 Z = Strate.BottomWorldZ;
|
||||
Z < Strate.TopWorldZ && bManagersBitIdentical; Z += 2)
|
||||
{
|
||||
for (int32 Y = -32; Y < 32 && bManagersBitIdentical; Y += 2)
|
||||
{
|
||||
for (int32 X = -32; X < 32; X += 2)
|
||||
{
|
||||
const float A = GeneratorA->GetDensityAt((float)X, (float)Y, (float)Z);
|
||||
const float B = GeneratorB->GetDensityAt((float)X, (float)Y, (float)Z);
|
||||
++ManagerDensitySamples;
|
||||
if (!SameDensityBits(A, B))
|
||||
{
|
||||
bManagersBitIdentical = false;
|
||||
AddError(FString::Printf(
|
||||
TEXT("independent season managers diverged at (%d,%d,%d): 0x%08x vs 0x%08x"),
|
||||
X, Y, Z, *reinterpret_cast<const uint32*>(&A),
|
||||
*reinterpret_cast<const uint32*>(&B)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("two independent runtime season managers compared %lld bit-identical density samples"),
|
||||
ManagerDensitySamples));
|
||||
TestTrue(TEXT("two independent runtime season managers produce a bit-identical field"),
|
||||
bManagersBitIdentical);
|
||||
|
||||
// The former HasComposerRecipeOverride force made every one of these tiles Mixed (zero proved
|
||||
// tiles). The new path asks the exact recipe stack and brute-forces every verdict it accepts.
|
||||
int32 NumClassified = 0;
|
||||
int32 NumMixed = 0;
|
||||
int32 NumAllSolid = 0;
|
||||
int32 NumAllAir = 0;
|
||||
int32 NumVerdictsBruteForced = 0;
|
||||
int32 NumFalseVerdicts = 0;
|
||||
constexpr int32 TileCells = 16;
|
||||
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
|
||||
{
|
||||
for (int32 ZBand = 1; ZBand <= 3; ++ZBand)
|
||||
{
|
||||
const int32 Z = Strate.BottomWorldZ
|
||||
+ (Strate.TopWorldZ - Strate.BottomWorldZ) * ZBand / 4;
|
||||
for (int32 Y = -128; Y <= 128; Y += 32)
|
||||
{
|
||||
for (int32 X = -128; X <= 128; X += 32)
|
||||
{
|
||||
const FIntVector Origin(X, Y, Z);
|
||||
const EVoxelTileClass Verdict = GeneratorA->ClassifyTile(
|
||||
Origin, 1, TileCells);
|
||||
++NumClassified;
|
||||
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
||||
if (Verdict == EVoxelTileClass::AllSolid) ++NumAllSolid;
|
||||
else ++NumAllAir;
|
||||
|
||||
if (NumVerdictsBruteForced >= 64) continue;
|
||||
++NumVerdictsBruteForced;
|
||||
const bool bSolid = Verdict == EVoxelTileClass::AllSolid;
|
||||
bool bBad = false;
|
||||
for (int32 GZ = -1; GZ <= TileCells + 1 && !bBad; ++GZ)
|
||||
for (int32 GY = -1; GY <= TileCells + 1 && !bBad; ++GY)
|
||||
for (int32 GX = -1; GX <= TileCells + 1; ++GX)
|
||||
{
|
||||
const float D = GeneratorA->GetDensityAt(
|
||||
(float)(X + GX), (float)(Y + GY), (float)(Z + GZ));
|
||||
if (bSolid ? D >= 0.0f : D < 0.0f)
|
||||
{
|
||||
bBad = true;
|
||||
++NumFalseVerdicts;
|
||||
AddError(FString::Printf(
|
||||
TEXT("composed recipe false %s tile at (%d,%d,%d), sample (%d,%d,%d)=%.9g"),
|
||||
bSolid ? TEXT("AllSolid") : TEXT("AllAir"), X, Y, Z,
|
||||
X + GX, Y + GY, Z + GZ, D));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("composed recipe tile classification: before force=%d Mixed/0 proved; after=%d Mixed, %d AllSolid, %d AllAir; %d accepted verdicts brute-forced, %d violations"),
|
||||
NumClassified, NumMixed, NumAllSolid, NumAllAir,
|
||||
NumVerdictsBruteForced, NumFalseVerdicts));
|
||||
TestTrue(TEXT("composed recipe classification proves at least one uniform tile"),
|
||||
NumAllSolid + NumAllAir > 0);
|
||||
TestEqual(TEXT("composed recipe classification has no false uniform verdict"),
|
||||
NumFalseVerdicts, 0);
|
||||
|
||||
FString TamperedJson = Manifest.SerializedJson;
|
||||
const FString OriginalSeedField = FString::Printf(TEXT("\"seed\": %d"), Manifest.Seed);
|
||||
const FString TamperedSeedField = FString::Printf(TEXT("\"seed\": %d"), Manifest.Seed + 1);
|
||||
TamperedJson = TamperedJson.Replace(*OriginalSeedField, *TamperedSeedField,
|
||||
ESearchCase::CaseSensitive);
|
||||
FVoxelSeasonManifest TamperedManifest;
|
||||
FString TamperReport;
|
||||
TestFalse(TEXT("stale/edited season JSON is rejected by its content hash"),
|
||||
VF_DeserializeVoxelSeasonManifest(
|
||||
TamperedJson, TamperedManifest, TamperReport));
|
||||
TestTrue(TEXT("stale season failure names the content hash"),
|
||||
TamperReport.Contains(TEXT("content hash")));
|
||||
|
||||
// Explicit unset regression gate: no season means the authored pointer, seed and radius still
|
||||
// flow through the original pool/fixed path.
|
||||
TStrongObjectPtr<UVoxelSettings> LegacySettings(
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
TStrongObjectPtr<UVoxelStrateDefinition> LegacyDefinition(
|
||||
NewObject<UVoxelStrateDefinition>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
LegacySettings->Seed = 77123;
|
||||
LegacySettings->OriginSpineRadius = 23.0f;
|
||||
LegacySettings->WorldRadiusVoxels = 987.0f;
|
||||
LegacySettings->TotalStrates = 1;
|
||||
LegacyDefinition->StrateHeightInChunks = 7;
|
||||
LegacySettings->StratePool.Add(
|
||||
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
||||
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
||||
NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
TestTrue(TEXT("legacy manager still initializes with Season unset"),
|
||||
LegacyManager->Initialize(LegacySettings.Get(), LegacySettings->Seed));
|
||||
TestTrue(TEXT("season unset remains the authored-pool path"),
|
||||
LegacySettings->Season.IsNull()
|
||||
&& LegacyManager->GetNumStrates() == 1
|
||||
&& LegacyManager->GetLayout()[0].Definition == LegacyDefinition.Get()
|
||||
&& LegacyManager->GetLayout()[0].HeightInChunks == 7
|
||||
&& LegacyManager->GetWorldSeed() == LegacySettings->Seed
|
||||
&& LegacySettings->GetEffectiveWorldSeed() == LegacySettings->Seed
|
||||
&& LegacySettings->GetEffectiveOriginSpineRadius()
|
||||
== LegacySettings->OriginSpineRadius
|
||||
&& LegacySettings->GetEffectiveWorldRadiusVoxels()
|
||||
== LegacySettings->WorldRadiusVoxels);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4886,6 +4886,7 @@ namespace
|
||||
uint32 SeedU;
|
||||
};
|
||||
|
||||
#if WITH_EDITOR
|
||||
//=============================================================================
|
||||
// RÔLE 2 — COMBINER : RÉGIONS LATÉRALES
|
||||
//=============================================================================
|
||||
@@ -5008,6 +5009,7 @@ namespace
|
||||
mutable FVoxelStrateRegionPartitionCache PartitionCache;
|
||||
mutable bool bPrepared = false;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // ⚠️ 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
|
||||
@@ -6144,6 +6146,7 @@ bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
|
||||
return true;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
namespace
|
||||
{
|
||||
static EVoxelStrateParamBlock VF_RegionParamBlock(ECaveGeneratorType Archetype)
|
||||
@@ -6449,3 +6452,4 @@ bool VF_BuildStrateRegionStack(
|
||||
OutStack = MoveTemp(Parent);
|
||||
return true;
|
||||
}
|
||||
#endif // WITH_EDITOR — lateral regions remain behind the failed shippability gate
|
||||
@@ -659,9 +659,9 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||
{
|
||||
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
|
||||
// déréférencer l'asset de settings.
|
||||
Seed = Settings ? Settings->Seed : 0;
|
||||
OriginSpineRadius = Settings ? Settings->OriginSpineRadius : 14.0f;
|
||||
WorldRadiusVoxels = Settings ? Settings->WorldRadiusVoxels : 0.0f;
|
||||
Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0;
|
||||
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f;
|
||||
WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f;
|
||||
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f;
|
||||
}
|
||||
|
||||
@@ -736,15 +736,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
||||
thread_local FVoxelOpStack CP_OpStack;
|
||||
thread_local bool CP_UseOpStack = false;
|
||||
// Cooked-season recipes and editor candidate recipes share this immutable worker-local
|
||||
// hand-off. Recipe materialisation is runtime; rolling/selection remains editor-only.
|
||||
thread_local bool CP_UseCustomRecipe = false;
|
||||
thread_local int32 CP_CustomRecipeSeed = 0;
|
||||
thread_local FVoxelStrateArchetypeParams CP_CustomParams;
|
||||
thread_local FVoxelOpStackRecipe CP_CustomRecipe;
|
||||
#if WITH_EDITOR
|
||||
// Editor-only composer override. The candidate recipe is copied into this worker-local
|
||||
// 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
|
||||
|
||||
@@ -763,29 +766,29 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CP_Version = LayoutVersion;
|
||||
CP_Chunk = ChunkCoord;
|
||||
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
||||
CP_UseCustomRecipe = false;
|
||||
bool bAllowCustomRecipe = true;
|
||||
#if WITH_EDITOR
|
||||
CP_UseComposerRecipe = false;
|
||||
CP_UseComposerRegions = false;
|
||||
ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
bool bComposerUseRecipe = false;
|
||||
if (StrateManager->GetComposerRegionOverrideForChunk(
|
||||
ChunkCoord, CP_ComposerRegions))
|
||||
{
|
||||
CP_UseComposerRegions = true;
|
||||
bAllowCustomRecipe = false;
|
||||
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))
|
||||
{
|
||||
CP_GenType = ComposerArchetype;
|
||||
CP_UseComposerRecipe = bComposerUseRecipe;
|
||||
}
|
||||
#endif
|
||||
if (bAllowCustomRecipe
|
||||
&& StrateManager->GetRecipeForChunk(
|
||||
ChunkCoord, CP_CustomRecipeSeed, CP_GenType,
|
||||
CP_CustomParams, CP_CustomRecipe))
|
||||
{
|
||||
CP_UseCustomRecipe = true;
|
||||
}
|
||||
switch (CP_GenType)
|
||||
{
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
@@ -817,19 +820,22 @@ 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);
|
||||
CP_UseOpStack = CP_UseOpStack || CP_UseCustomRecipe;
|
||||
#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;
|
||||
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
|
||||
#endif
|
||||
if (CP_UseOpStack)
|
||||
{
|
||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||
bool bBuiltSpecialStack = false;
|
||||
#if WITH_EDITOR
|
||||
if (CP_UseComposerRegions)
|
||||
{
|
||||
bBuiltSpecialStack = true;
|
||||
FVoxelOpContext RegionContext;
|
||||
if (VF_BuildStrateRegionStack(
|
||||
CP_ComposerRegions, OriginSpineRadius, StrateManager,
|
||||
@@ -847,70 +853,71 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CP_UseOpStack = false;
|
||||
}
|
||||
}
|
||||
else if (CP_UseComposerRecipe)
|
||||
#endif
|
||||
if (!bBuiltSpecialStack)
|
||||
{
|
||||
FVoxelOpContext ComposerContext;
|
||||
if (VF_BuildStackFromRecipe(
|
||||
CP_ComposerRecipe, CP_ComposerParams, Seed,
|
||||
OriginSpineRadius, StrateManager, CP_OpStack, ComposerContext, nullptr))
|
||||
if (CP_UseCustomRecipe)
|
||||
{
|
||||
ComposerContext.ChunkCoord = ChunkCoord;
|
||||
ComposerContext.Step = 1;
|
||||
ComposerContext.LayoutVersion = LayoutVersion;
|
||||
// The recipe builder supplies the candidate's vertical seal to the
|
||||
// structural post. The XY edge seal is global and uses live settings.
|
||||
ComposerContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
ComposerContext.EdgeSealThickness = EdgeSealThickness;
|
||||
CP_OpStack.PrepareChunk(ComposerContext);
|
||||
FVoxelOpContext RecipeContext;
|
||||
if (VF_BuildStackFromRecipe(
|
||||
CP_CustomRecipe, CP_CustomParams, CP_CustomRecipeSeed,
|
||||
OriginSpineRadius, StrateManager, CP_OpStack, RecipeContext, nullptr))
|
||||
{
|
||||
RecipeContext.ChunkCoord = ChunkCoord;
|
||||
RecipeContext.Step = 1;
|
||||
RecipeContext.LayoutVersion = LayoutVersion;
|
||||
// The recipe builder supplies the candidate's vertical seal to the
|
||||
// structural post. The XY edge seal is global and uses live settings.
|
||||
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
RecipeContext.EdgeSealThickness = EdgeSealThickness;
|
||||
CP_OpStack.PrepareChunk(RecipeContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Import/load validates recipes before a season becomes active. Keep
|
||||
// the fallback hole-safe if a future schema changes underneath it.
|
||||
CP_UseOpStack = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The editor action validates before installing the override. Keep this
|
||||
// fallback safe if a future recipe changes underneath PIE: the native
|
||||
// candidate params remain available instead of yielding an uninitialised
|
||||
// stack or a hole.
|
||||
CP_UseOpStack = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
FVoxelOpContext OpCtx;
|
||||
OpCtx.ChunkCoord = ChunkCoord;
|
||||
OpCtx.Seed = (uint32)Seed;
|
||||
OpCtx.LayoutVersion = LayoutVersion;
|
||||
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
OpCtx.EdgeSealThickness = EdgeSealThickness;
|
||||
FVoxelOpContext OpCtx;
|
||||
OpCtx.ChunkCoord = ChunkCoord;
|
||||
OpCtx.Seed = (uint32)Seed;
|
||||
OpCtx.LayoutVersion = LayoutVersion;
|
||||
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
OpCtx.EdgeSealThickness = EdgeSealThickness;
|
||||
|
||||
// ⚠️ LE MAPPING VIT DANS `VF_BuildOpStackForChunk` (haut de ce fichier) ET NULLE
|
||||
// PART AILLEURS — `ClassifyTile` appelle la MÊME fabrique. Un verdict de tuile issu
|
||||
// d'une pile construite autrement serait un trou. Ici on ne fait que fournir les
|
||||
// params déjà cherchés juste au-dessus.
|
||||
FVoxelStackParamRefs Refs;
|
||||
Refs.Slab = &CP_Slab;
|
||||
Refs.Maze = &CP_Maze;
|
||||
Refs.Vert = &CP_Vert;
|
||||
Refs.Float = &CP_Float;
|
||||
Refs.Tunnel = &CP_Tunnel;
|
||||
FVoxelStackParamRefs Refs;
|
||||
Refs.Slab = &CP_Slab;
|
||||
Refs.Maze = &CP_Maze;
|
||||
Refs.Vert = &CP_Vert;
|
||||
Refs.Float = &CP_Float;
|
||||
Refs.Tunnel = &CP_Tunnel;
|
||||
|
||||
// SurfaceWorld : le champ de biomes est fabriqué ICI, du côté qui connaît le
|
||||
// générateur, et TRANSFÉRÉ à la pile. L'opérateur ne voit qu'une `IVoxelBiomeField`,
|
||||
// ce qui lui permet de devenir un asset en Phase 3 sans traîner le générateur.
|
||||
TArray<FSurfaceGenerationParams> PerBiome;
|
||||
if (CP_GenType == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
Refs.Surface = &CP_Surface;
|
||||
if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0)
|
||||
{
|
||||
PerBiome = CP_SurfaceBiomeParams;
|
||||
Refs.SurfaceBiomeParams = &PerBiome;
|
||||
Refs.BiomeField = MakeUnique<FGeneratorBiomeField>(
|
||||
this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z);
|
||||
}
|
||||
}
|
||||
TArray<FSurfaceGenerationParams> PerBiome;
|
||||
if (CP_GenType == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
Refs.Surface = &CP_Surface;
|
||||
if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0)
|
||||
{
|
||||
PerBiome = CP_SurfaceBiomeParams;
|
||||
Refs.SurfaceBiomeParams = &PerBiome;
|
||||
Refs.BiomeField = MakeUnique<FGeneratorBiomeField>(
|
||||
this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z);
|
||||
}
|
||||
}
|
||||
|
||||
CP_UseOpStack = VF_BuildOpStackForChunk(CP_GenType, Refs, Seed, OriginSpineRadius,
|
||||
StrateManager, CP_OpStack, OpCtx);
|
||||
CP_UseOpStack = VF_BuildOpStackForChunk(
|
||||
CP_GenType, Refs, Seed, OriginSpineRadius,
|
||||
StrateManager, CP_OpStack, OpCtx);
|
||||
|
||||
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
|
||||
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
|
||||
@@ -922,7 +929,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// makes an op green in test and wrong in game. All seven bodies are empty today,
|
||||
// which is the point: the first op that hoists real per-chunk work must find the
|
||||
// call already here.
|
||||
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
|
||||
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2935,6 +2943,99 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
|
||||
if (!StrateManager) return EVoxelTileClass::Mixed;
|
||||
|
||||
// A selected recipe must be classified by the exact same materialised stack that supplies
|
||||
// density. Search every touched chunk-Z first: the minimum corner may be in a gap or adjacent
|
||||
// slot, and letting that fall through to the native factory would prove the wrong field.
|
||||
int32 RecipeSeed = 0;
|
||||
ECaveGeneratorType RecipeArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams RecipeParams;
|
||||
FVoxelOpStackRecipe Recipe;
|
||||
bool bTouchesRecipe = false;
|
||||
const int32 RecipeMinChunkZ = FloorDivC(MinZ, CHUNK_SIZE);
|
||||
const int32 RecipeMaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE);
|
||||
for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ)
|
||||
{
|
||||
if (StrateManager->GetRecipeForChunk(
|
||||
FIntVector(0, 0, CZ), RecipeSeed, RecipeArchetype,
|
||||
RecipeParams, Recipe))
|
||||
{
|
||||
bTouchesRecipe = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bTouchesRecipe)
|
||||
{
|
||||
int32 RecipeTopChunkZ = 0;
|
||||
int32 RecipeBottomChunkZ = 0;
|
||||
if (!StrateManager->GetStrateChunkZBounds(
|
||||
RecipeMinChunkZ, RecipeTopChunkZ, RecipeBottomChunkZ)
|
||||
|| RecipeMinChunkZ < RecipeBottomChunkZ
|
||||
|| RecipeMaxChunkZ > RecipeTopChunkZ)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
const int32 RecipeMinChunkX = FloorDivC(MinX, CHUNK_SIZE);
|
||||
const int32 RecipeMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE);
|
||||
const int32 RecipeMinChunkY = FloorDivC(MinY, CHUNK_SIZE);
|
||||
const int32 RecipeMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE);
|
||||
for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ)
|
||||
{
|
||||
int32 TouchedTop = 0;
|
||||
int32 TouchedBottom = 0;
|
||||
if (!StrateManager->GetStrateChunkZBounds(CZ, TouchedTop, TouchedBottom)
|
||||
|| TouchedTop != RecipeTopChunkZ || TouchedBottom != RecipeBottomChunkZ)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
for (int32 CY = RecipeMinChunkY; CY <= RecipeMaxChunkY; ++CY)
|
||||
{
|
||||
for (int32 CX = RecipeMinChunkX; CX <= RecipeMaxChunkX; ++CX)
|
||||
{
|
||||
int32 TouchedSeed = 0;
|
||||
ECaveGeneratorType TouchedArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams TouchedParams;
|
||||
FVoxelOpStackRecipe TouchedRecipe;
|
||||
const FIntVector TouchedChunk(CX, CY, CZ);
|
||||
if (!StrateManager->GetRecipeForChunk(
|
||||
TouchedChunk, TouchedSeed, TouchedArchetype,
|
||||
TouchedParams, TouchedRecipe)
|
||||
|| TouchedSeed != RecipeSeed
|
||||
|| TouchedArchetype != RecipeArchetype)
|
||||
{
|
||||
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 RecipeStack;
|
||||
FVoxelOpContext RecipeContext;
|
||||
if (!VF_BuildStackFromRecipe(
|
||||
Recipe, RecipeParams, RecipeSeed, OriginSpineRadius,
|
||||
StrateManager, RecipeStack, RecipeContext, nullptr))
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
RecipeContext.ChunkCoord = FIntVector(
|
||||
RecipeMinChunkX, RecipeMinChunkY, RecipeMinChunkZ);
|
||||
RecipeContext.Step = Step;
|
||||
RecipeContext.LayoutVersion = StrateManager->GetLayoutVersion();
|
||||
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
RecipeContext.EdgeSealThickness = EdgeSealThickness;
|
||||
RecipeStack.PrepareChunk(RecipeContext);
|
||||
return RecipeStack.ClassifyBox(TileVoxelBox, RecipeContext);
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -3024,13 +3125,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
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.
|
||||
if (StrateManager->HasComposerRecipeOverride())
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
#include "VoxelSeasonManifest.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Misc/FileHelper.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
bool ValidateRuntimeRecipes(const FVoxelSeasonManifest& Manifest, FString& OutReport)
|
||||
{
|
||||
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
|
||||
{
|
||||
if (!Strate.bUsesRecipe) continue;
|
||||
FVoxelOpStack ValidationStack;
|
||||
FVoxelOpContext ValidationContext;
|
||||
FString BuildError;
|
||||
if (!VF_BuildSeasonStrateStack(
|
||||
Strate, Manifest.OriginSpineRadius, nullptr,
|
||||
ValidationStack, ValidationContext, &BuildError))
|
||||
{
|
||||
OutReport = FString::Printf(
|
||||
TEXT("season slot %d recipe is not materialisable: %s"),
|
||||
Strate.DepthIndex, *BuildError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FPrimaryAssetId UVoxelSeasonAsset::GetPrimaryAssetId() const
|
||||
{
|
||||
return FPrimaryAssetId(TEXT("VoxelSeason"), GetFName());
|
||||
}
|
||||
|
||||
bool UVoxelSeasonAsset::LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const
|
||||
{
|
||||
if (ManifestJson.IsEmpty())
|
||||
{
|
||||
OutReport = TEXT("season asset contains no imported manifest JSON");
|
||||
OutManifest = FVoxelSeasonManifest();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VF_DeserializeVoxelSeasonManifest(ManifestJson, OutManifest, OutReport))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!OutManifest.ContentHash.Equals(ManifestContentHash, ESearchCase::IgnoreCase)
|
||||
|| OutManifest.SchemaVersion != SchemaVersion
|
||||
|| OutManifest.Season != SeasonNumber
|
||||
|| OutManifest.Seed != SeasonSeed
|
||||
|| FMemory::Memcmp(&OutManifest.OriginSpineRadius,
|
||||
&OriginSpineRadius, sizeof(float)) != 0
|
||||
|| FMemory::Memcmp(&OutManifest.WorldRadiusVoxels,
|
||||
&WorldRadiusVoxels, sizeof(float)) != 0)
|
||||
{
|
||||
OutReport = TEXT("season asset metadata does not match its embedded manifest; re-import the JSON");
|
||||
OutManifest.bValid = false;
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
if (!ValidateRuntimeRecipes(OutManifest, OutReport))
|
||||
{
|
||||
OutManifest.bValid = false;
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UVoxelSeasonAsset::SetManifestJson(const FString& InManifestJson, FString& OutReport)
|
||||
{
|
||||
FVoxelSeasonManifest Parsed;
|
||||
if (!VF_DeserializeVoxelSeasonManifest(InManifestJson, Parsed, OutReport))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!ValidateRuntimeRecipes(Parsed, OutReport))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Modify();
|
||||
ManifestJson = InManifestJson;
|
||||
SchemaVersion = Parsed.SchemaVersion;
|
||||
SeasonNumber = Parsed.Season;
|
||||
SeasonSeed = Parsed.Seed;
|
||||
OriginSpineRadius = Parsed.OriginSpineRadius;
|
||||
WorldRadiusVoxels = Parsed.WorldRadiusVoxels;
|
||||
ManifestContentHash = Parsed.ContentHash;
|
||||
ReferencedDefinitions.Reset();
|
||||
TSet<FString> SeenDefinitionPaths;
|
||||
for (const FVoxelSeasonStrate& Strate : Parsed.Strates)
|
||||
{
|
||||
if (!Strate.SourceDefinitionPath.IsEmpty()
|
||||
&& !SeenDefinitionPaths.Contains(Strate.SourceDefinitionPath))
|
||||
{
|
||||
SeenDefinitionPaths.Add(Strate.SourceDefinitionPath);
|
||||
ReferencedDefinitions.Add(TSoftObjectPtr<UVoxelStrateDefinition>(
|
||||
FSoftObjectPath(Strate.SourceDefinitionPath)));
|
||||
}
|
||||
}
|
||||
MarkPackageDirty();
|
||||
OutReport = FString::Printf(
|
||||
TEXT("imported season %d seed %d (%d strates, hash %s)"),
|
||||
SeasonNumber, SeasonSeed, Parsed.Strates.Num(), *ManifestContentHash);
|
||||
return true;
|
||||
}
|
||||
|
||||
void UVoxelSeasonAsset::ImportSeasonManifestJson()
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
FString JsonText;
|
||||
FString Report;
|
||||
if (SourceManifestJson.FilePath.IsEmpty()
|
||||
|| !FFileHelper::LoadFileToString(JsonText, *SourceManifestJson.FilePath))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] Could not read manifest JSON '%s'."),
|
||||
*SourceManifestJson.FilePath);
|
||||
return;
|
||||
}
|
||||
if (!SetManifestJson(JsonText, Report))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] Import failed: %s"), *Report);
|
||||
return;
|
||||
}
|
||||
UE_LOG(LogTemp, Log, TEXT("[VoxelSeasonAsset] %s"), *Report);
|
||||
#else
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelSeasonAsset] JSON import is editor-only."));
|
||||
#endif
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Misc/SecureHash.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
@@ -1428,56 +1429,85 @@ const TCHAR* VF_GetVoxelSeasonSelectionReasonName(EVoxelSeasonSelectionReason Re
|
||||
return TEXT("unknown");
|
||||
}
|
||||
|
||||
FString VF_SerializeVoxelSeasonManifest(const FVoxelSeasonManifest& Manifest)
|
||||
namespace
|
||||
{
|
||||
using namespace VoxelSeasonManifestPrivate;
|
||||
TSharedPtr<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
SetCommonManifestFields(Root, Manifest);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Strates;
|
||||
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
|
||||
FString SerializeVoxelSeasonManifestInternal(
|
||||
const FVoxelSeasonManifest& Manifest, bool bIncludeContentHash)
|
||||
{
|
||||
Strates.Add(MakeShared<FJsonValueObject>(SerializeStrate(Strate)));
|
||||
}
|
||||
Root->SetArrayField(TEXT("strates"), MoveTemp(Strates));
|
||||
using namespace VoxelSeasonManifestPrivate;
|
||||
TSharedPtr<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
SetCommonManifestFields(Root, Manifest);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Rejections;
|
||||
for (const FVoxelSeasonRejectionCount& Rejection : Manifest.RejectionCounts)
|
||||
{
|
||||
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
|
||||
Object->SetStringField(TEXT("reason"), Rejection.Reason);
|
||||
Object->SetNumberField(TEXT("count"), Rejection.Count);
|
||||
Rejections.Add(MakeShared<FJsonValueObject>(Object));
|
||||
}
|
||||
Root->SetArrayField(TEXT("rejection_counts"), MoveTemp(Rejections));
|
||||
TArray<TSharedPtr<FJsonValue>> Strates;
|
||||
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
|
||||
{
|
||||
Strates.Add(MakeShared<FJsonValueObject>(SerializeStrate(Strate)));
|
||||
}
|
||||
Root->SetArrayField(TEXT("strates"), MoveTemp(Strates));
|
||||
|
||||
if (!Manifest.CandidateAudit.IsEmpty())
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Audit;
|
||||
for (const FVoxelSeasonCandidateAudit& Row : Manifest.CandidateAudit)
|
||||
TArray<TSharedPtr<FJsonValue>> Rejections;
|
||||
for (const FVoxelSeasonRejectionCount& Rejection : Manifest.RejectionCounts)
|
||||
{
|
||||
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
|
||||
Object->SetNumberField(TEXT("candidate_index"), Row.CandidateIndex);
|
||||
Object->SetNumberField(TEXT("seed"), Row.Seed);
|
||||
Object->SetStringField(TEXT("archetype"), ArchetypeName(Row.Archetype));
|
||||
Object->SetNumberField(TEXT("recipe_hash"), static_cast<double>(Row.RecipeHash));
|
||||
Object->SetNumberField(TEXT("distance_from_corpus_centroid"), Row.DistanceFromCorpusCentroid);
|
||||
Object->SetBoolField(TEXT("passed_hard_gates"), Row.bPassedHardGates);
|
||||
Object->SetBoolField(TEXT("selected"), Row.bSelected);
|
||||
Object->SetStringField(TEXT("rejection_reason"), Row.RejectionReason);
|
||||
Audit.Add(MakeShared<FJsonValueObject>(Object));
|
||||
Object->SetStringField(TEXT("reason"), Rejection.Reason);
|
||||
Object->SetNumberField(TEXT("count"), Rejection.Count);
|
||||
Rejections.Add(MakeShared<FJsonValueObject>(Object));
|
||||
}
|
||||
Root->SetArrayField(TEXT("candidate_audit"), MoveTemp(Audit));
|
||||
}
|
||||
Root->SetArrayField(TEXT("rejection_counts"), MoveTemp(Rejections));
|
||||
|
||||
FString JsonText;
|
||||
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
|
||||
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonText);
|
||||
if (!FJsonSerializer::Serialize(Root.ToSharedRef(), Writer) || !Writer->Close())
|
||||
{
|
||||
return FString();
|
||||
if (!Manifest.CandidateAudit.IsEmpty())
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Audit;
|
||||
for (const FVoxelSeasonCandidateAudit& Row : Manifest.CandidateAudit)
|
||||
{
|
||||
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
|
||||
Object->SetNumberField(TEXT("candidate_index"), Row.CandidateIndex);
|
||||
Object->SetNumberField(TEXT("seed"), Row.Seed);
|
||||
Object->SetStringField(TEXT("archetype"), ArchetypeName(Row.Archetype));
|
||||
Object->SetNumberField(TEXT("recipe_hash"), static_cast<double>(Row.RecipeHash));
|
||||
Object->SetNumberField(TEXT("distance_from_corpus_centroid"), Row.DistanceFromCorpusCentroid);
|
||||
Object->SetBoolField(TEXT("passed_hard_gates"), Row.bPassedHardGates);
|
||||
Object->SetBoolField(TEXT("selected"), Row.bSelected);
|
||||
Object->SetStringField(TEXT("rejection_reason"), Row.RejectionReason);
|
||||
Audit.Add(MakeShared<FJsonValueObject>(Object));
|
||||
}
|
||||
Root->SetArrayField(TEXT("candidate_audit"), MoveTemp(Audit));
|
||||
}
|
||||
|
||||
if (bIncludeContentHash)
|
||||
{
|
||||
const FString Payload = SerializeVoxelSeasonManifestInternal(Manifest, false);
|
||||
if (Payload.IsEmpty()) return FString();
|
||||
FTCHARToUTF8 Utf8(*Payload);
|
||||
uint8 Digest[FSHA1::DigestSize];
|
||||
FSHA1::HashBuffer(Utf8.Get(), Utf8.Length(), Digest);
|
||||
Root->SetStringField(TEXT("content_hash"), BytesToHex(Digest, FSHA1::DigestSize));
|
||||
}
|
||||
|
||||
FString JsonText;
|
||||
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
|
||||
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonText);
|
||||
if (!FJsonSerializer::Serialize(Root.ToSharedRef(), Writer) || !Writer->Close())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
return JsonText;
|
||||
}
|
||||
return JsonText;
|
||||
}
|
||||
|
||||
FString VF_ComputeVoxelSeasonManifestContentHash(const FVoxelSeasonManifest& Manifest)
|
||||
{
|
||||
const FString Payload = SerializeVoxelSeasonManifestInternal(Manifest, false);
|
||||
if (Payload.IsEmpty()) return FString();
|
||||
FTCHARToUTF8 Utf8(*Payload);
|
||||
uint8 Digest[FSHA1::DigestSize];
|
||||
FSHA1::HashBuffer(Utf8.Get(), Utf8.Length(), Digest);
|
||||
return BytesToHex(Digest, FSHA1::DigestSize);
|
||||
}
|
||||
|
||||
FString VF_SerializeVoxelSeasonManifest(const FVoxelSeasonManifest& Manifest)
|
||||
{
|
||||
return SerializeVoxelSeasonManifestInternal(Manifest, true);
|
||||
}
|
||||
|
||||
bool VF_SaveVoxelSeasonManifest(const FString& ManifestPath,
|
||||
@@ -1511,24 +1541,25 @@ bool VF_SaveVoxelSeasonManifest(const FString& ManifestPath,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
|
||||
FVoxelSeasonManifest& OutManifest,
|
||||
FString& OutReport)
|
||||
bool VF_DeserializeVoxelSeasonManifest(const FString& ManifestJson,
|
||||
FVoxelSeasonManifest& OutManifest,
|
||||
FString& OutReport)
|
||||
{
|
||||
using namespace VoxelSeasonManifestPrivate;
|
||||
OutManifest = FVoxelSeasonManifest();
|
||||
OutReport.Reset();
|
||||
if (ManifestPath.IsEmpty())
|
||||
if (ManifestJson.IsEmpty())
|
||||
{
|
||||
OutReport = TEXT("season manifest path is empty");
|
||||
OutReport = TEXT("season manifest JSON is empty");
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> Root;
|
||||
FString OriginalJson;
|
||||
if (!LoadJson(ManifestPath, Root, OutReport, &OriginalJson))
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ManifestJson);
|
||||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
||||
{
|
||||
OutReport = TEXT("season manifest JSON is malformed");
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
@@ -1556,6 +1587,8 @@ bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
|
||||
bool bFieldsValid = true;
|
||||
OutManifest.bValid = bValid;
|
||||
OutManifest.SchemaVersion = SchemaVersion;
|
||||
bFieldsValid = ReadJsonString(Root, TEXT("content_hash"), OutManifest.ContentHash)
|
||||
&& bFieldsValid;
|
||||
bFieldsValid = ReadJsonInt32(Root, TEXT("season"), OutManifest.Season) && bFieldsValid;
|
||||
bFieldsValid = ReadJsonInt32(Root, TEXT("seed"), OutManifest.Seed) && bFieldsValid;
|
||||
double CorpusHash = 0.0;
|
||||
@@ -1669,7 +1702,7 @@ bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
|
||||
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
|
||||
if (Strate.DepthIndex != Index || Strate.bUsesRegions || !Strate.bPassedPrimordialLaw
|
||||
|| Strate.PrimordialLawResult != EVoxelConnectivityResult::Connected)
|
||||
{
|
||||
bFieldsValid = false;
|
||||
@@ -1695,17 +1728,99 @@ bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
|
||||
}
|
||||
if (!bFieldsValid)
|
||||
{
|
||||
OutReport = FString::Printf(TEXT("season manifest %s contains invalid fields"), *ManifestPath);
|
||||
OutReport = TEXT("season manifest contains invalid fields");
|
||||
OutManifest.Error = OutReport;
|
||||
OutManifest.bValid = false;
|
||||
return false;
|
||||
}
|
||||
OutManifest.ManifestPath = ManifestPath;
|
||||
OutManifest.SerializedJson = MoveTemp(OriginalJson);
|
||||
OutReport = FString::Printf(TEXT("loaded season manifest from %s"), *ManifestPath);
|
||||
const FString ComputedHash = VF_ComputeVoxelSeasonManifestContentHash(OutManifest);
|
||||
if (ComputedHash.IsEmpty()
|
||||
|| !ComputedHash.Equals(OutManifest.ContentHash, ESearchCase::IgnoreCase))
|
||||
{
|
||||
OutReport = FString::Printf(
|
||||
TEXT("season manifest content hash mismatch (stored %s, computed %s)"),
|
||||
*OutManifest.ContentHash, *ComputedHash);
|
||||
OutManifest.Error = OutReport;
|
||||
OutManifest.bValid = false;
|
||||
return false;
|
||||
}
|
||||
OutManifest.SerializedJson = ManifestJson;
|
||||
OutReport = FString::Printf(TEXT("loaded season manifest hash %s"), *OutManifest.ContentHash);
|
||||
return OutManifest.IsUsable();
|
||||
}
|
||||
|
||||
bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
|
||||
FVoxelSeasonManifest& OutManifest,
|
||||
FString& OutReport)
|
||||
{
|
||||
FString JsonText;
|
||||
if (ManifestPath.IsEmpty())
|
||||
{
|
||||
OutReport = TEXT("season manifest path is empty");
|
||||
OutManifest = FVoxelSeasonManifest();
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
if (!FFileHelper::LoadFileToString(JsonText, *ManifestPath))
|
||||
{
|
||||
OutReport = FString::Printf(TEXT("could not read season manifest %s"), *ManifestPath);
|
||||
OutManifest = FVoxelSeasonManifest();
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
if (!VF_DeserializeVoxelSeasonManifest(JsonText, OutManifest, OutReport))
|
||||
{
|
||||
OutReport = FString::Printf(TEXT("season manifest %s: %s"), *ManifestPath, *OutReport);
|
||||
OutManifest.Error = OutReport;
|
||||
return false;
|
||||
}
|
||||
OutManifest.ManifestPath = ManifestPath;
|
||||
OutReport = FString::Printf(TEXT("loaded season manifest from %s (hash %s)"),
|
||||
*ManifestPath, *OutManifest.ContentHash);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace VoxelSeasonRuntimePrivate
|
||||
{
|
||||
float ActiveTop(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater: return Params.TunnelNetworkParams.StrateTopWorldZ;
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber: return Params.SlabParams.StrateTopWorldZ;
|
||||
case ECaveGeneratorType::Maze: return Params.MazeParams.StrateTopWorldZ;
|
||||
case ECaveGeneratorType::SurfaceWorld: return Params.SurfaceParams.StrateTopWorldZ;
|
||||
case ECaveGeneratorType::VerticalShafts: return Params.VerticalShaftParams.StrateTopWorldZ;
|
||||
case ECaveGeneratorType::FloatingIslands: return Params.FloatingIslandParams.StrateTopWorldZ;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float ActiveBottom(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater: return Params.TunnelNetworkParams.StrateBottomWorldZ;
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber: return Params.SlabParams.StrateBottomWorldZ;
|
||||
case ECaveGeneratorType::Maze: return Params.MazeParams.StrateBottomWorldZ;
|
||||
case ECaveGeneratorType::SurfaceWorld: return Params.SurfaceParams.StrateBottomWorldZ;
|
||||
case ECaveGeneratorType::VerticalShafts: return Params.VerticalShaftParams.StrateBottomWorldZ;
|
||||
case ECaveGeneratorType::FloatingIslands: return Params.FloatingIslandParams.StrateBottomWorldZ;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
bool SameFloatBits(float A, float B)
|
||||
{
|
||||
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
namespace VoxelSeasonCompositionPrivate
|
||||
{
|
||||
float ActiveTop(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
|
||||
@@ -1920,7 +2035,10 @@ namespace VoxelSeasonCompositionPrivate
|
||||
OutStrate.Params = Candidate.ArchetypeParams;
|
||||
OutStrate.Regions = Candidate.Regions;
|
||||
OutStrate.bUsesRecipe = true;
|
||||
OutStrate.bUsesRegions = Candidate.Regions.RegionCount > 1;
|
||||
// Lateral composition remains a laboratory feature until its cross-seam law is perfect.
|
||||
// The season artifact must therefore stay on the proved single-stack runtime contract.
|
||||
OutStrate.bUsesRegions = VF_LateralRegionsAreShippable()
|
||||
&& Candidate.Regions.RegionCount > 1;
|
||||
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
|
||||
static_cast<float>(BottomWorldZ));
|
||||
if (OutStrate.Regions.IsValid())
|
||||
@@ -2235,6 +2353,7 @@ namespace VoxelSeasonCompositionPrivate
|
||||
return VoxelSeasonManifestPrivate::ConnectivityName(Result);
|
||||
}
|
||||
}
|
||||
#endif // WITH_EDITOR — composition, measurement and review helpers never ship
|
||||
|
||||
namespace VoxelSeasonCompositionPrivate
|
||||
{
|
||||
@@ -2791,6 +2910,11 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
|
||||
|
||||
Manifest.bValid = true;
|
||||
Manifest.Error.Reset();
|
||||
Manifest.ContentHash = VF_ComputeVoxelSeasonManifestContentHash(Manifest);
|
||||
if (Manifest.ContentHash.IsEmpty())
|
||||
{
|
||||
return Fail(TEXT("could not hash composed season manifest"));
|
||||
}
|
||||
Manifest.SerializedJson = VF_SerializeVoxelSeasonManifest(Manifest);
|
||||
if (Manifest.SerializedJson.IsEmpty())
|
||||
{
|
||||
@@ -2807,20 +2931,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
|
||||
return Manifest;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
|
||||
const FVoxelSeasonCompositionSettings& Settings)
|
||||
{
|
||||
(void)SeasonSeed;
|
||||
(void)Settings;
|
||||
FVoxelSeasonManifest Manifest;
|
||||
Manifest.Error = TEXT("season composition is editor/build-box only");
|
||||
return Manifest;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if WITH_EDITOR
|
||||
FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* WorldSettings)
|
||||
{
|
||||
FVoxelSeasonCompositionSettings Settings;
|
||||
@@ -2835,6 +2948,7 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
|
||||
}
|
||||
return VF_ComposeSeason(SeasonSeed, Settings);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
|
||||
float SpineRadius,
|
||||
@@ -2843,7 +2957,7 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
|
||||
FVoxelOpContext& OutContext,
|
||||
FString* OutError)
|
||||
{
|
||||
using namespace VoxelSeasonCompositionPrivate;
|
||||
using namespace VoxelSeasonRuntimePrivate;
|
||||
auto Fail = [&](const FString& Reason) -> bool
|
||||
{
|
||||
if (OutError != nullptr)
|
||||
@@ -2890,8 +3004,12 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
|
||||
bool bBuilt = false;
|
||||
if (Strate.bUsesRegions)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
bBuilt = VF_BuildStrateRegionStack(Strate.Regions, SpineRadius, StrateManager,
|
||||
OutStack, OutContext, OutError);
|
||||
#else
|
||||
return Fail(TEXT("Lateral region manifests are not shippable."));
|
||||
#endif
|
||||
}
|
||||
else if (Strate.bUsesRecipe)
|
||||
{
|
||||
@@ -2902,7 +3020,8 @@ bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
|
||||
else
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
const float Seal = ActiveSeal(Strate.Params, Strate.Archetype);
|
||||
const float Seal = VoxelSeasonCompositionPrivate::ActiveSeal(
|
||||
Strate.Params, Strate.Archetype);
|
||||
bBuilt = VF_BuildNativeStrateStackForCandidate(
|
||||
Strate.Archetype, Strate.Params, Strate.Seed, SpineRadius,
|
||||
0.0f, Seal, StrateManager, OutStack, OutContext);
|
||||
@@ -2940,6 +3059,7 @@ bool VF_RebuildVoxelSeasonStrate(const FVoxelSeasonStrate& Strate,
|
||||
OutStack, OutContext, OutError);
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
namespace VoxelSeasonCompositionPrivate
|
||||
{
|
||||
bool WriteReviewPage(const FVoxelSeasonManifest& Manifest,
|
||||
@@ -3021,3 +3141,4 @@ namespace VoxelSeasonCompositionPrivate
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif // WITH_EDITOR — review rendering never ships
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "VoxelSettings.h"
|
||||
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
{
|
||||
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
||||
{
|
||||
return Asset->GetSeasonSeed();
|
||||
}
|
||||
}
|
||||
return Seed;
|
||||
}
|
||||
|
||||
float UVoxelSettings::GetEffectiveOriginSpineRadius() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
{
|
||||
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
||||
{
|
||||
return Asset->GetOriginSpineRadius();
|
||||
}
|
||||
}
|
||||
return OriginSpineRadius;
|
||||
}
|
||||
|
||||
float UVoxelSettings::GetEffectiveWorldRadiusVoxels() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
{
|
||||
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
||||
{
|
||||
return Asset->GetWorldRadiusVoxels();
|
||||
}
|
||||
}
|
||||
return WorldRadiusVoxels;
|
||||
}
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveSeasonNumber() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
{
|
||||
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
||||
{
|
||||
return Asset->GetSeasonNumber();
|
||||
}
|
||||
}
|
||||
return CurrentSeason;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#if WITH_EDITOR
|
||||
namespace
|
||||
{
|
||||
constexpr float GJitterFraction = 0.15f;
|
||||
@@ -2262,27 +2263,6 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
bool FVoxelStrateMeasuredMetrics::IsUsable() const
|
||||
{
|
||||
return bValid
|
||||
&& NumSampled > 0
|
||||
&& NumAir > 0
|
||||
&& NumSolid > 0
|
||||
&& NumSampled == NumAir + NumSolid
|
||||
&& NumAirComponents > 0
|
||||
&& LargestComponentCells > 0
|
||||
&& AirComponentCells.Num() == NumAirComponents
|
||||
&& FMath::IsFinite(AirFraction)
|
||||
&& FMath::IsFinite(LargestComponentShare)
|
||||
&& FMath::IsFinite(WalkableFraction)
|
||||
&& FMath::IsFinite(WalkableFloorAreaFraction)
|
||||
&& FMath::IsFinite(LargestWalkableSurfaceShare)
|
||||
&& FMath::IsFinite(MedianFeatureScale)
|
||||
&& FMath::IsFinite(LargestComponentPoint.X)
|
||||
&& FMath::IsFinite(LargestComponentPoint.Y)
|
||||
&& FMath::IsFinite(LargestComponentPoint.Z);
|
||||
}
|
||||
|
||||
FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics(
|
||||
const FVoxelStrateMetrics& Metrics)
|
||||
{
|
||||
@@ -3901,22 +3881,6 @@ uint32 FVoxelStrateCorpus::GetContentsHash() const
|
||||
return Hash;
|
||||
}
|
||||
|
||||
const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return TEXT("TunnelNetwork");
|
||||
case ECaveGeneratorType::FlatPlain: return TEXT("FlatPlain");
|
||||
case ECaveGeneratorType::CrystalChamber: return TEXT("CrystalChamber");
|
||||
case ECaveGeneratorType::Maze: return TEXT("Maze");
|
||||
case ECaveGeneratorType::SurfaceWorld: return TEXT("SurfaceWorld");
|
||||
case ECaveGeneratorType::VerticalShafts: return TEXT("VerticalShafts");
|
||||
case ECaveGeneratorType::FloatingIslands: return TEXT("FloatingIslands");
|
||||
case ECaveGeneratorType::Underwater: return TEXT("Underwater");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
const TCHAR* VF_GetStrateCorpusProvenanceName(EVoxelStrateCorpusProvenance Provenance)
|
||||
{
|
||||
switch (Provenance)
|
||||
@@ -4400,6 +4364,7 @@ bool FVoxelStrateCorpus::SetMeasuredMetrics(
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif // WITH_EDITOR — corpus loading, promotion and parameter/structure roll implementation
|
||||
|
||||
void VF_SetStrateArchetypeRuntimeBounds(
|
||||
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ)
|
||||
@@ -4589,11 +4554,13 @@ namespace VoxelStrateRegionPrivate
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex)
|
||||
{
|
||||
return 1 + static_cast<int32>(VoxelHash::Cell(
|
||||
StrateIndex, StrateIndex ^ 0x6D, static_cast<uint32>(Seed) ^ 0x524F4C4Cu) % 3u);
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex)
|
||||
{
|
||||
@@ -4753,6 +4720,7 @@ FVoxelStrateRegionBoxProof VF_AnalyzeStrateRegionBox(
|
||||
return Proof;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpus,
|
||||
int32 Seed, int32 Index)
|
||||
{
|
||||
@@ -5694,3 +5662,4 @@ bool VF_AreStrateStructureRecipesIdentical(const FVoxelOpStackRecipe& A,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif // WITH_EDITOR — no roller/corpus entry point is compiled into Shipping
|
||||
@@ -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);
|
||||
|
||||
//=========================================================================
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "VoxelStrateMeasure.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelTypes.h"
|
||||
@@ -1579,3 +1581,5 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
|
||||
Result.bStartSnapped, Result.bGoalSnapped, &Result);
|
||||
return Result;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR — measurement never ships
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "VoxelStrateComposer.h"
|
||||
|
||||
bool FVoxelStrateMeasuredMetrics::IsUsable() const
|
||||
{
|
||||
return bValid
|
||||
&& NumSampled > 0
|
||||
&& NumAir > 0
|
||||
&& NumSolid > 0
|
||||
&& NumSampled == NumAir + NumSolid
|
||||
&& NumAirComponents > 0
|
||||
&& LargestComponentCells > 0
|
||||
&& AirComponentCells.Num() == NumAirComponents
|
||||
&& FMath::IsFinite(AirFraction)
|
||||
&& FMath::IsFinite(LargestComponentShare)
|
||||
&& FMath::IsFinite(WalkableFraction)
|
||||
&& FMath::IsFinite(WalkableFloorAreaFraction)
|
||||
&& FMath::IsFinite(LargestWalkableSurfaceShare)
|
||||
&& FMath::IsFinite(MedianFeatureScale)
|
||||
&& FMath::IsFinite(LargestComponentPoint.X)
|
||||
&& FMath::IsFinite(LargestComponentPoint.Y)
|
||||
&& FMath::IsFinite(LargestComponentPoint.Z);
|
||||
}
|
||||
|
||||
const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return TEXT("TunnelNetwork");
|
||||
case ECaveGeneratorType::FlatPlain: return TEXT("FlatPlain");
|
||||
case ECaveGeneratorType::CrystalChamber: return TEXT("CrystalChamber");
|
||||
case ECaveGeneratorType::Maze: return TEXT("Maze");
|
||||
case ECaveGeneratorType::SurfaceWorld: return TEXT("SurfaceWorld");
|
||||
case ECaveGeneratorType::VerticalShafts: return TEXT("VerticalShafts");
|
||||
case ECaveGeneratorType::FloatingIslands: return TEXT("FloatingIslands");
|
||||
case ECaveGeneratorType::Underwater: return TEXT("Underwater");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
@@ -423,6 +423,7 @@ void AVoxelWorld::RegenerateAllChunks()
|
||||
|
||||
void AVoxelWorld::RebuildStrates()
|
||||
{
|
||||
bool bLayoutRebuilt = true;
|
||||
{
|
||||
FScopedGenerationPause Guard(this);
|
||||
if (!Guard.Acquired())
|
||||
@@ -434,12 +435,20 @@ void AVoxelWorld::RebuildStrates()
|
||||
if (StrateManager && Settings)
|
||||
{
|
||||
// Re-applies layout + inter-strate gap + passage/spine settings from VoxelSettings.
|
||||
StrateManager->Initialize(Settings, Settings->Seed);
|
||||
bLayoutRebuilt = StrateManager->Initialize(
|
||||
Settings, Settings->GetEffectiveWorldSeed());
|
||||
}
|
||||
if (AtmosphereManager) AtmosphereManager->Reset();
|
||||
if (ContentManager) ContentManager->ClearAll();
|
||||
}
|
||||
|
||||
if (!bLayoutRebuilt)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelWorld] RebuildStrates refused an invalid season; loaded tiles were not regenerated."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload all chunks against the rebuilt strate data.
|
||||
RegenerateAllChunks();
|
||||
|
||||
@@ -831,7 +840,7 @@ void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject)
|
||||
|
||||
if (StrateManager)
|
||||
{
|
||||
StrateManager->Initialize(Settings, Settings->Seed);
|
||||
StrateManager->Initialize(Settings, Settings->GetEffectiveWorldSeed());
|
||||
}
|
||||
if (Generator)
|
||||
{
|
||||
@@ -959,11 +968,19 @@ void AVoxelWorld::BeginPlay()
|
||||
Mesher->SkirtCells = Settings->SkirtCells;
|
||||
Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off
|
||||
|
||||
// Système de strates — piloté par le pool et les fixed entries dans Settings.
|
||||
if (Settings->StratePool.Num() > 0)
|
||||
// Système de strates — a cooked season takes precedence; otherwise the authored pool path is
|
||||
// exactly the legacy one.
|
||||
if (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0)
|
||||
{
|
||||
StrateManager = NewObject<UVoxelStrateManager>(this);
|
||||
StrateManager->Initialize(Settings, Settings->Seed);
|
||||
if (!StrateManager->Initialize(Settings, Settings->GetEffectiveWorldSeed()))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelWorld] World generation disabled because its assigned season is invalid or incomplete."));
|
||||
bShuttingDown.store(true, std::memory_order_release);
|
||||
SetActorTickEnabled(false);
|
||||
return;
|
||||
}
|
||||
Generator->SetStrateManager(StrateManager);
|
||||
UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] Strate system initialized with %d strates"),
|
||||
StrateManager->GetNumStrates());
|
||||
@@ -977,7 +994,8 @@ void AVoxelWorld::BeginPlay()
|
||||
|
||||
// Content manager — distance-based decoration grid (no LOD pop) + level-0 water planes.
|
||||
ContentManager = NewObject<UVoxelContentManager>(this);
|
||||
ContentManager->Initialize(this, StrateManager, Generator, Settings, Settings->Seed);
|
||||
ContentManager->Initialize(
|
||||
this, StrateManager, Generator, Settings, Settings->GetEffectiveWorldSeed());
|
||||
|
||||
// Atmosphere manager — per-strate fog + ambient + persistent ceiling/floor layers.
|
||||
if (bManageAtmosphere && StrateManager)
|
||||
@@ -2641,6 +2659,12 @@ void AVoxelWorld::ChangeSeed(int32 NewSeed)
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ChangeSeed failed — no Settings assigned"));
|
||||
return;
|
||||
}
|
||||
if (!Settings->Season.IsNull())
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelWorld] ChangeSeed rejected: a cooked Season owns the seed. Assign another season asset instead."));
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 OldSeed = Settings->Seed;
|
||||
const int32 OldSeason = Settings->CurrentSeason;
|
||||
@@ -2702,12 +2726,17 @@ void AVoxelWorld::ChangeSeed(int32 NewSeed)
|
||||
|
||||
int32 AVoxelWorld::GetCurrentSeed() const
|
||||
{
|
||||
return Settings ? Settings->Seed : 0;
|
||||
return Settings ? Settings->GetEffectiveWorldSeed() : 0;
|
||||
}
|
||||
|
||||
int32 AVoxelWorld::GetCurrentSeason() const
|
||||
{
|
||||
return Settings ? Settings->CurrentSeason : 0;
|
||||
return Settings ? Settings->GetEffectiveSeasonNumber() : 0;
|
||||
}
|
||||
|
||||
FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
{
|
||||
return StrateManager ? StrateManager->GetSeasonContentHash() : FString();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
||||
@@ -481,6 +481,7 @@ VOXELFORGE_API bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
|
||||
bool bAppendStructuralPosts = true);
|
||||
|
||||
/** Build the region combiner stack. Creative cores are independent; structural posts are global. */
|
||||
#if WITH_EDITOR
|
||||
VOXELFORGE_API bool VF_BuildStrateRegionStack(
|
||||
const FVoxelStrateRegionManifest& Manifest,
|
||||
float SpineRadius,
|
||||
@@ -488,3 +489,4 @@ VOXELFORGE_API bool VF_BuildStrateRegionStack(
|
||||
FVoxelOpStack& OutStack,
|
||||
FVoxelOpContext& OutContext,
|
||||
FString* OutError = nullptr);
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
// Cookable runtime carrier for one reviewed season manifest.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "VoxelSeasonAsset.generated.h"
|
||||
|
||||
struct FVoxelSeasonManifest;
|
||||
class UVoxelStrateDefinition;
|
||||
|
||||
/**
|
||||
* The JSON remains the reviewed/diffable source artifact. This asset embeds that exact text so
|
||||
* Unreal's cooker can carry it into a packaged build, together with an independently stored digest
|
||||
* used to detect an accidentally stale or edited asset.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class VOXELFORGE_API UVoxelSeasonAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
|
||||
|
||||
/** Parse and validate the embedded manifest, including both content-hash copies. */
|
||||
bool LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const;
|
||||
|
||||
/** Runtime-safe import core used by the editor button and automation. */
|
||||
bool SetManifestJson(const FString& InManifestJson, FString& OutReport);
|
||||
|
||||
int32 GetSeasonNumber() const { return SeasonNumber; }
|
||||
int32 GetSeasonSeed() const { return SeasonSeed; }
|
||||
float GetOriginSpineRadius() const { return OriginSpineRadius; }
|
||||
float GetWorldRadiusVoxels() const { return WorldRadiusVoxels; }
|
||||
const FString& GetManifestContentHash() const { return ManifestContentHash; }
|
||||
|
||||
/** Create the asset in Content Browser, choose a JSON file, then press this button. */
|
||||
UFUNCTION(CallInEditor, Category = "Season")
|
||||
void ImportSeasonManifestJson();
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
UPROPERTY(EditAnywhere, Category = "Season|Import", meta = (FilePathFilter = "json"))
|
||||
FFilePath SourceManifestJson;
|
||||
#endif
|
||||
|
||||
private:
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
int32 SchemaVersion = 0;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
int32 SeasonNumber = 0;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
int32 SeasonSeed = 0;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
float OriginSpineRadius = 0.0f;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
float WorldRadiusVoxels = 0.0f;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
FString ManifestContentHash;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season", meta = (MultiLine = true))
|
||||
FString ManifestJson;
|
||||
|
||||
// Import-derived cook references for fixed/authored content bags named by the JSON. Generated
|
||||
// strates legitimately have no source definition and use deterministic C++ defaults at runtime.
|
||||
UPROPERTY(VisibleAnywhere, Category = "Season")
|
||||
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> ReferencedDefinitions;
|
||||
};
|
||||
@@ -1,8 +1,4 @@
|
||||
// Offline Tier 4c season composition and the diffable Tier A manifest.
|
||||
//
|
||||
// This header deliberately describes an editor/build-box artifact. The normal runtime path still
|
||||
// receives its existing settings and builds density exactly as before; no runtime code consumes a
|
||||
// season manifest as a hidden generation fallback.
|
||||
// Offline Tier 4c composition plus the runtime-readable, diffable Tier A manifest.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -195,7 +191,7 @@ struct VOXELFORGE_API FVoxelSeasonStrate
|
||||
*/
|
||||
struct VOXELFORGE_API FVoxelSeasonManifest
|
||||
{
|
||||
static constexpr int32 CurrentSchemaVersion = 1;
|
||||
static constexpr int32 CurrentSchemaVersion = 2;
|
||||
|
||||
bool bValid = false;
|
||||
int32 SchemaVersion = CurrentSchemaVersion;
|
||||
@@ -204,6 +200,9 @@ struct VOXELFORGE_API FVoxelSeasonManifest
|
||||
int32 Season = 0;
|
||||
int32 Seed = 0;
|
||||
uint32 InputCorpusHash = 0;
|
||||
// SHA-1 of the canonical manifest JSON with this field omitted. This detects stale cooked
|
||||
// season assets without requiring the editor-only input corpus on a player's machine.
|
||||
FString ContentHash;
|
||||
int32 CandidateCount = 0;
|
||||
int32 SurvivorCount = 0;
|
||||
int32 SelectedCount = 0;
|
||||
@@ -230,15 +229,17 @@ struct VOXELFORGE_API FVoxelSeasonManifest
|
||||
|
||||
bool IsUsable() const
|
||||
{
|
||||
if (!bValid || !Error.IsEmpty() || WorldRadiusVoxels != 0.0f
|
||||
if (!bValid || !Error.IsEmpty() || ContentHash.Len() != 40 || 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))
|
||||
// Region composition is intentionally not a shippable season format yet. Keeping
|
||||
// this rejection in the portable manifest contract prevents an editor experiment
|
||||
// from becoming packaged terrain merely because its density evaluator exists.
|
||||
if (Strate.bUsesRegions)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -252,16 +253,24 @@ VOXELFORGE_API const TCHAR* VF_GetVoxelSeasonSelectionReasonName(
|
||||
EVoxelSeasonSelectionReason Reason);
|
||||
|
||||
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
|
||||
#if WITH_EDITOR
|
||||
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
|
||||
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
|
||||
|
||||
/** Convenience overload that adapts the existing runtime settings asset. */
|
||||
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
|
||||
int32 SeasonSeed, const UVoxelSettings* WorldSettings);
|
||||
#endif
|
||||
|
||||
/** Serialize/read the complete Tier A manifest as stable pretty JSON. */
|
||||
VOXELFORGE_API FString VF_SerializeVoxelSeasonManifest(
|
||||
const FVoxelSeasonManifest& Manifest);
|
||||
/** Parse an in-memory/cooked JSON manifest and verify its embedded content hash. */
|
||||
VOXELFORGE_API bool VF_DeserializeVoxelSeasonManifest(
|
||||
const FString& ManifestJson, FVoxelSeasonManifest& OutManifest, FString& OutReport);
|
||||
/** Stable SHA-1 over the canonical manifest payload (the content_hash field itself is omitted). */
|
||||
VOXELFORGE_API FString VF_ComputeVoxelSeasonManifestContentHash(
|
||||
const FVoxelSeasonManifest& Manifest);
|
||||
VOXELFORGE_API bool VF_SaveVoxelSeasonManifest(
|
||||
const FString& ManifestPath, const FVoxelSeasonManifest& Manifest, FString& OutReport);
|
||||
VOXELFORGE_API bool VF_LoadVoxelSeasonManifest(
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
// IWYU : pointeur seulement (VoxelMaterial). / Pointer-only use.
|
||||
class UMaterialInterface;
|
||||
class UVoxelSeasonAsset;
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UVoxelSettings : public UPrimaryDataAsset
|
||||
@@ -21,6 +22,12 @@ class UVoxelSettings : public UPrimaryDataAsset
|
||||
|
||||
public:
|
||||
|
||||
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
||||
int32 GetEffectiveWorldSeed() const;
|
||||
float GetEffectiveOriginSpineRadius() const;
|
||||
float GetEffectiveWorldRadiusVoxels() const;
|
||||
int32 GetEffectiveSeasonNumber() const;
|
||||
|
||||
//=========================================================================
|
||||
// STREAMING (distance de vue)
|
||||
//=========================================================================
|
||||
@@ -350,6 +357,11 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||
int32 Seed = 0;
|
||||
|
||||
// Optional reviewed/cooked season. When set, its manifest owns layout, seed, bounds and
|
||||
// density recipes; the authored StratePool/FixedStrates path remains untouched when unset.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
|
||||
TSoftObjectPtr<UVoxelSeasonAsset> Season;
|
||||
|
||||
// Numéro de saison (incrementé par ChangeSeed). Purement informatif côté gameplay.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||
int32 CurrentSeason = 1;
|
||||
|
||||
@@ -356,7 +356,9 @@ VOXELFORGE_API void VF_SetStrateArchetypeRuntimeBounds(
|
||||
FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
|
||||
|
||||
/** Stable identity helpers for the deterministic lateral partition. */
|
||||
#if WITH_EDITOR
|
||||
VOXELFORGE_API int32 VF_RollStrateRegionCount(int32 Seed, int32 StrateIndex);
|
||||
#endif
|
||||
VOXELFORGE_API uint32 VF_GetStrateRegionPartitionSeed(int32 Seed, int32 StrateIndex);
|
||||
VOXELFORGE_API void VF_RekeyStrateRegionManifest(
|
||||
FVoxelStrateRegionManifest& Manifest, int32 Seed, int32 StrateIndex);
|
||||
|
||||
@@ -20,13 +20,12 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelStrateTypes.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelSeasonManifest.h"
|
||||
#include "VoxelStrateManager.generated.h"
|
||||
|
||||
class UVoxelSettings;
|
||||
|
||||
#if WITH_EDITOR
|
||||
struct FVoxelStrateArchetypeParams;
|
||||
struct FVoxelOpStackRecipe;
|
||||
struct FVoxelStrateRegionManifest;
|
||||
struct FVoxelStrateComposerSlotOverride;
|
||||
#endif
|
||||
@@ -148,7 +147,21 @@ public:
|
||||
* @param Settings - VoxelSettings with pool/fixed strate config
|
||||
* @param WorldSeed - Seed for randomizing non-fixed strates
|
||||
*/
|
||||
void Initialize(UVoxelSettings* Settings, int32 WorldSeed);
|
||||
/** Returns false without a usable layout; an assigned invalid/stale season never falls back. */
|
||||
bool Initialize(UVoxelSettings* Settings, int32 WorldSeed);
|
||||
|
||||
/**
|
||||
* Copy the immutable recipe payload for this chunk. In packaged builds this comes only from
|
||||
* the cooked season; editor candidate overrides use the same read-only worker hand-off.
|
||||
*/
|
||||
bool GetRecipeForChunk(
|
||||
const FIntVector& ChunkCoord, int32& OutRecipeSeed,
|
||||
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
|
||||
FVoxelOpStackRecipe& OutRecipe) const;
|
||||
|
||||
bool IsUsingSeason() const { return !ActiveSeasonContentHash.IsEmpty(); }
|
||||
const FString& GetSeasonContentHash() const { return ActiveSeasonContentHash; }
|
||||
int32 GetWorldSeed() const { return CachedSeed; }
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
@@ -172,8 +185,6 @@ public:
|
||||
bool GetComposerRegionOverrideForChunk(
|
||||
const FIntVector& ChunkCoord, FVoxelStrateRegionManifest& OutRegions) const;
|
||||
|
||||
/** Custom recipes cannot be safely classified by the native ClassifyBox proof yet. */
|
||||
bool HasComposerRecipeOverride() const;
|
||||
#endif
|
||||
|
||||
//=========================================================================
|
||||
@@ -356,6 +367,11 @@ protected:
|
||||
UPROPERTY()
|
||||
TArray<FStrateSlot> StrateLayout;
|
||||
|
||||
// Parallel to StrateLayout only while a cooked season is active. Plain immutable data is
|
||||
// copied into worker-local caches on the existing layout-versioned refetch path.
|
||||
TArray<FVoxelSeasonStrate> SeasonStrates;
|
||||
FString ActiveSeasonContentHash;
|
||||
|
||||
// Passages connecting consecutive strates
|
||||
TArray<FVoxelPassage> Passages;
|
||||
|
||||
|
||||
@@ -308,6 +308,10 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Season")
|
||||
int32 GetCurrentSeason() const;
|
||||
|
||||
/** Reviewed manifest digest to replicate/compare at join; empty on the legacy authored path. */
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Season")
|
||||
FString GetCurrentSeasonContentHash() const;
|
||||
|
||||
//=========================================================================
|
||||
// STRATE QUERIES (gameplay integration)
|
||||
//=========================================================================
|
||||
|
||||
Reference in new issue
Block a user