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:
Fr0zkaandClaude Opus 5 committed 2026-09-05 08:37:01 +02:00
1 parent 088b40216d
commit faa62d419e
21 files changed
+1268 -287

No files matched your search

@@ -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