wip(assembler): round L21 -- resolved strate layout snapshot + validator (no world change)
Core ResolvedLayoutSnapshotV1 (canonical JSON, stable identity): per strate stable ID, generator, recipe, interior bounds in metres, ceiling/bedrock bands as Core really applies them, transition mode/width, dispatch provenance. Validator: compat mode reports today's deviations; strict mode enforces ceiling -> content -> bedrock -> next ceiling (owner rule), two separate bands, nominal contiguity, interpenetration only within the declared EVoxelStrateTransition width; 7 negative controls rejected. Findings: SW built at -120..-632 m although the params say -60 m; a 60 m unowned gap filled by analytic FI fallback; no active bedrock bands; transitions ignored by Core. All L19 hashes unchanged (28 + 14 crossing runs). The Unreal adapter exports resolved_layout (uncompiled here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
5d5447dc32
commit
8753f6dbe4
12 files changed
+1347
-3
No files matched your search
@@ -944,6 +944,199 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
MatchedStrateIndices.Add(Plan.strateIndex);
|
||||
}
|
||||
|
||||
// Keep the inspectable physical-layout export separate from the manifest identity: it is
|
||||
// assembled from the same resolved slots/plans, but adding it must not salt evaluation hashes.
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV1 Layout;
|
||||
Layout.worldIdentity = std::string("owner-world/seed/")
|
||||
+ std::to_string(StrateManager->GetWorldSeed());
|
||||
Layout.ownerVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||
Layout.coreVoxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
const auto generatorName = [](ECaveGeneratorType Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return "TunnelNetwork";
|
||||
case ECaveGeneratorType::FlatPlain: return "FlatPlain";
|
||||
case ECaveGeneratorType::CrystalChamber: return "CrystalChamber";
|
||||
case ECaveGeneratorType::Maze: return "Maze";
|
||||
case ECaveGeneratorType::SurfaceWorld: return "SurfaceWorld";
|
||||
case ECaveGeneratorType::VerticalShafts: return "VerticalShafts";
|
||||
case ECaveGeneratorType::FloatingIslands: return "FloatingIslands";
|
||||
case ECaveGeneratorType::Underwater: return "Underwater";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
const auto transitionName = [](EVoxelStrateTransition Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EVoxelStrateTransition::Gradient: return "Gradient";
|
||||
case EVoxelStrateTransition::Hard: return "Hard";
|
||||
case EVoxelStrateTransition::Interleaved: return "Interleaved";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
const std::int64_t OwnerVoxelMicrons = Layout.ownerVoxelSizeMicrometres;
|
||||
const auto FixedToMicrons = [ExpectedCoreVoxelMicrons](std::int64_t Fixed)
|
||||
{
|
||||
return static_cast<std::int64_t>(std::llround(static_cast<double>(Fixed)
|
||||
* ExpectedCoreVoxelMicrons / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||||
};
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
VoxelForgeCore::ResolvedLayoutStratumV1 Item;
|
||||
Item.strateIndex = Slot.StrateIndex;
|
||||
Item.generatorKind = generatorName(Type);
|
||||
Item.stableId = Layout.worldIdentity + "/strate/" + std::to_string(Slot.StrateIndex)
|
||||
+ "/generator/" + generatorName(Type);
|
||||
Item.recipeId = Type == ECaveGeneratorType::SurfaceWorld ? "SurfaceWorld.Heightfield2D"
|
||||
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
|
||||
: std::string("legacy.") + generatorName(Type);
|
||||
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
||||
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
Item.interiorBottomMicrometres = static_cast<std::int64_t>(Slot.BottomChunkZ)
|
||||
* CHUNK_SIZE * ExpectedCoreVoxelMicrons;
|
||||
Item.interiorTopMicrometres = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
|
||||
* CHUNK_SIZE * ExpectedCoreVoxelMicrons;
|
||||
Item.managerChunkWidthMicrometres = CHUNK_SIZE * OwnerVoxelMicrons;
|
||||
Item.dispatchChunkWidthMicrometres = Type == ECaveGeneratorType::FloatingIslands
|
||||
? CHUNK_SIZE * OwnerVoxelMicrons : CHUNK_SIZE * ExpectedCoreVoxelMicrons;
|
||||
Item.transitionMode = transitionName(Slot.Definition->TransitionType);
|
||||
Item.transitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
|
||||
* Item.managerChunkWidthMicrometres;
|
||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||
? 2 * Item.managerChunkWidthMicrometres : 0;
|
||||
Item.transitionInterpenetrationMicrometres = 0;
|
||||
Item.transitionHonouredInCore = false;
|
||||
Item.transitionDispatchPath = "upper strate owns lower interface; manager GetGenerationParams can blend legacy vectors, but Core's compiled field selects per-strate/native recipe fields without transition blending";
|
||||
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate";
|
||||
if (Type == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
Item.boundDispatchPath = "BuildSurfaceStratumPlan uses slot chunkZ * 32 Core voxels";
|
||||
Item.queryDispatchPath = "NativeSurfaceDensityAt uses floor(CoreZ / 32), then GetGeneratorTypeForChunk/GetStrateIndex";
|
||||
const auto Plan = std::find_if(Manifest->plan.strates.begin(), Manifest->plan.strates.end(),
|
||||
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
||||
if (Plan != Manifest->plan.strates.end())
|
||||
{
|
||||
for (const auto& Profile : Manifest->recipe.biomes)
|
||||
if (Profile.hasCapProfile)
|
||||
{
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "cap_profile";
|
||||
Anchor.stableId = "biome/" + std::to_string(Profile.stableId);
|
||||
Anchor.units = "1/256 Core voxel";
|
||||
Anchor.values = { Profile.capHeightSubvoxels, Profile.capUndulationSubvoxels,
|
||||
Profile.capWavelengthSubvoxels, Profile.capDetailAmplitudeSubvoxels,
|
||||
Profile.capRidgeAmplitudeSubvoxels };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
for (std::size_t RouteIndex = 0; RouteIndex < Plan->route.size(); ++RouteIndex)
|
||||
{
|
||||
const auto& Point = Plan->route[RouteIndex];
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "route_point"; Anchor.stableId = "route/" + std::to_string(RouteIndex);
|
||||
Anchor.units = "1/256 Core voxel";
|
||||
Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1],
|
||||
Point.positionSubvoxels[2], Point.radiusSubvoxels };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
}
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres, Item.interiorTopMicrometres,
|
||||
false, true, false, "no separate volumetric boundary band; cap profile remains content",
|
||||
"none", "Heightfield2D cap + AppendStratumBoundaryCeilings fallback mesh" };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
|
||||
Item.interiorBottomMicrometres, false, true, false,
|
||||
"no lower bedrock band", "none", "none" };
|
||||
}
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
Item.boundDispatchPath = "BuildFloatingIslandStratumPlan uses slot chunkZ * 32 * owner/Core voxel ratio";
|
||||
Item.queryDispatchPath = "NativeFloatingIslandDensityAt uses floor(CoreZ / (32 * owner/Core ratio)), then manager lookup";
|
||||
const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(),
|
||||
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
||||
std::int64_t SealMicrometres = 0;
|
||||
if (Plan != Manifest->floatingIslands.end())
|
||||
{
|
||||
Item.interiorBottomMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMinimumZSubvoxels);
|
||||
Item.interiorTopMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMaximumZSubvoxels);
|
||||
SealMicrometres = FixedToMicrons(Plan->manifest.recipe.boundarySealSubvoxels);
|
||||
Item.recipeVersion = Plan->manifest.recipe.geometryVersion;
|
||||
for (std::size_t IslandIndex = 0; IslandIndex < Plan->manifest.plan.islands.size(); ++IslandIndex)
|
||||
{
|
||||
const auto& Island = Plan->manifest.plan.islands[IslandIndex];
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "island_anchor"; Anchor.stableId = "island/" + std::to_string(IslandIndex);
|
||||
Anchor.units = "1/256 Core voxel";
|
||||
Anchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1],
|
||||
Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
}
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres - SealMicrometres,
|
||||
Item.interiorTopMicrometres, SealMicrometres > 0, true, false,
|
||||
"v3 shared upper boundary seal; one-sided and inset from upper edge",
|
||||
"upper/interior side", "VF_ApplyUpperBoundarySeal" };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
|
||||
Item.interiorBottomMicrometres, false, true, false,
|
||||
"no lower seal in FloatingIsland geometryVersion 3", "none", "none" };
|
||||
}
|
||||
else
|
||||
{
|
||||
Item.boundDispatchPath = "resolved manager slot scaled by Core chunk width";
|
||||
Item.queryDispatchPath = "legacy Core route; manager chunk selected by floor(CoreZ / 32)";
|
||||
const std::int64_t SealMicrometres = static_cast<std::int64_t>(std::llround(
|
||||
std::max(0.0f, Slot.Definition->GenerationParams.BoundarySealThickness)
|
||||
* OwnerVoxelMicrons));
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres,
|
||||
Item.interiorTopMicrometres + SealMicrometres, false, true, false,
|
||||
"manager configures a two-sided legacy seal, but Core's compiled analytic field does not apply this density band",
|
||||
"configured upper side", "legacy VF_ApplyBoundarySeal; absent from current Core density path" };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres - SealMicrometres,
|
||||
Item.interiorBottomMicrometres, false, true, false,
|
||||
"manager configures a two-sided legacy seal, but Core's compiled analytic field does not apply this density band; breakable by default",
|
||||
"configured lower side", "legacy VF_ApplyBoundarySeal; absent from current Core density path" };
|
||||
}
|
||||
Item.content = { "content", Item.interiorBottomMicrometres,
|
||||
Item.interiorTopMicrometres, true, true, false, "resolved generator interior",
|
||||
"none", "none" };
|
||||
for (std::size_t PassageIndex = 0; PassageIndex < StrateManager->GetPassages().Num(); ++PassageIndex)
|
||||
{
|
||||
const FVoxelPassage& Passage = StrateManager->GetPassages()[static_cast<int32>(PassageIndex)];
|
||||
if (Passage.UpperStrateIndex != Slot.StrateIndex && Passage.LowerStrateIndex != Slot.StrateIndex) continue;
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "passage"; Anchor.stableId = "passage/" + std::to_string(PassageIndex);
|
||||
Anchor.units = "milli owner voxels";
|
||||
Anchor.values = { static_cast<std::int64_t>(std::llround(Passage.UpperPoint.X * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.UpperPoint.Y * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.UpperPoint.Z * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.LowerPoint.X * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.LowerPoint.Y * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.LowerPoint.Z * 1000.0)) };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
Layout.strates.push_back(std::move(Item));
|
||||
}
|
||||
std::string LayoutError;
|
||||
if (!Layout.finalize(LayoutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core resolved-layout export failed: %s"), UTF8_TO_TCHAR(LayoutError.c_str()));
|
||||
return false;
|
||||
}
|
||||
const std::string LayoutJson = Layout.ToJson();
|
||||
const FString LayoutText = UTF8_TO_TCHAR(LayoutJson.c_str());
|
||||
TSharedPtr<FJsonObject> LayoutObject;
|
||||
const TSharedRef<TJsonReader<>> LayoutReader = TJsonReaderFactory<>::Create(LayoutText);
|
||||
if (!FJsonSerializer::Deserialize(LayoutReader, LayoutObject) || !LayoutObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Core resolved-layout canonical JSON could not be embedded in the resolved snapshot.");
|
||||
return false;
|
||||
}
|
||||
Root->SetObjectField(TEXT("resolved_layout"), LayoutObject.ToSharedRef());
|
||||
|
||||
const std::string CanonicalJson = Manifest->ToJson();
|
||||
if (CanonicalJson.empty())
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user