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