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