wip(assembler): M3 round 4 -- plans built inside Unreal from the owner's strates; no import needed

SurfaceWorld and FloatingIslands plans generate at startup from the resolved seed,
strate types and heights (hashed, cached). Imported JSON is an opt-in override (off by
default); an invalid override shows an on-screen notice and falls back to legacy.
BuildHost on staged owner assets: SurfaceWorld cross-system checks, islands at owner
height (route, collision, anchors, BP edits), seed 137501, W1/W8/W16, Core-off legacy.
XY-column cache verified (hashes kept); island LOD0 p95 91 ms vs 133 ms sprint deadline.
Not exercised: season-seed path (no Season in staged settings), rendered notice pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-10 22:48:46 +02:00
1 parent 48fa028086
commit 111cf3d262
21 files changed
+1934 -391

No files matched your search

@@ -283,8 +283,14 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
if (Settings) if (Settings)
{ {
FString ManifestJson, ManifestHash, ManifestError; FString ManifestJson, ManifestHash, ManifestError;
if (Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError) const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
&& !ManifestJson.IsEmpty()) const bool bHasRuntimeAssembly = VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
if (!bHasRuntimeAssembly)
{
Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError);
}
if (!ManifestJson.IsEmpty())
{ {
FTCHARToUTF8 Utf8Json(*ManifestJson); FTCHARToUTF8 Utf8Json(*ManifestJson);
std::string CoreError; std::string CoreError;
@@ -9,16 +9,477 @@
#include "UObject/TextProperty.h" #include "UObject/TextProperty.h"
#include <VoxelForgeCore/VoxelForgeCore.h> #include <VoxelForgeCore/VoxelForgeCore.h>
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#include "VoxelBiomeDefinition.h"
#include "VoxelBiomeTypes.h"
#include "VoxelSettings.h" #include "VoxelSettings.h"
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
#include "VoxelStrateManager.h" #include "VoxelStrateManager.h"
#include "VoxelTerrainOpDefinition.h" #include "VoxelTerrainOpDefinition.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <string>
#include <utility>
#include <vector>
namespace namespace
{ {
using FCoreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>; using FCoreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
constexpr double CoreVoxelSizeCm = 25.0; constexpr double CoreVoxelSizeCm = 25.0;
constexpr double OwnerVoxelSizeCm = 12.5; constexpr double OwnerVoxelSizeCm = 12.5;
constexpr double OwnerToCoreScale = OwnerVoxelSizeCm / CoreVoxelSizeCm;
std::int64_t WorldAssemblyFixed(double Voxels)
{
return static_cast<std::int64_t>(std::llround(
Voxels * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
}
std::int64_t WorldAssemblyQ16(double Value)
{
return static_cast<std::int64_t>(std::llround(Value * 65536.0));
}
std::int64_t WorldAssemblyWavelength(double Frequency, double FallbackVoxels)
{
const double Wavelength = FMath::IsFinite(Frequency) && Frequency > 0.0
? 1.0 / Frequency : FallbackVoxels;
return std::max<std::int64_t>(1, WorldAssemblyFixed(Wavelength));
}
std::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
std::int64_t Y, std::uint32_t Kind)
{
std::uint64_t Hash = 14695981039346656037ull;
const auto Add = [&Hash](std::uint64_t Value, int Bytes)
{
for (int Index = 0; Index < Bytes; ++Index)
{
Hash ^= static_cast<std::uint8_t>(Value >> (Index * 8));
Hash *= 1099511628211ull;
}
};
Add(Seed, 4);
Add(static_cast<std::uint64_t>(X), 8);
Add(static_cast<std::uint64_t>(Y), 8);
Add(Kind, 4);
return Hash == 0 ? 1 : Hash;
}
std::int64_t WorldAssemblyRoundRatio(std::int64_t Numerator, std::int64_t Denominator)
{
if (Numerator >= 0) return (Numerator + Denominator / 2) / Denominator;
return -((-Numerator + Denominator / 2) / Denominator);
}
std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
{
std::uint64_t Root = 0;
std::uint64_t Bit = 1ull << 62;
while (Bit > Value) Bit >>= 2;
while (Bit != 0)
{
if (Value >= Root + Bit)
{
Value -= Root + Bit;
Root = (Root >> 1) + Bit;
}
else
{
Root >>= 1;
}
Bit >>= 2;
}
const std::uint64_t OriginalValue = Value + Root * Root;
if (4 * OriginalValue >= (2 * Root + 1) * (2 * Root + 1)) ++Root;
return Root;
}
bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, std::string& Error)
{
using namespace VoxelForgeCore;
constexpr std::int64_t PlayerRadiusQ = 348;
constexpr std::int64_t PlayerHeightQ = 1802;
constexpr std::int64_t HalfHeightQ = PlayerHeightQ / 2;
constexpr std::int64_t StepQ = 128;
constexpr std::int64_t ProbeRadiusQ = 174;
constexpr std::int64_t SupportDepthQ = 256;
constexpr std::int64_t GradeQ16 = 17560;
constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel;
struct FDirection { std::int32_t X; std::int32_t Y; };
constexpr std::array<FDirection, 8> Directions{{
{65536, 0}, {46341, 46341}, {0, 65536}, {-46341, 46341},
{-65536, 0}, {-46341, -46341}, {0, -65536}, {46341, -46341}}};
struct FCandidate
{
std::int64_t Score = 0;
std::int32_t CenterX = 0;
std::int32_t CenterY = 0;
std::int32_t Direction = 0;
std::vector<WorldAssemblyRoutePointV1> Points;
};
auto& Constraints = Stratum.routeConstraints;
Constraints.version = 1;
Constraints.capsuleRadiusSubvoxels = PlayerRadiusQ;
Constraints.capsuleHeightSubvoxels = PlayerHeightQ;
Constraints.maximumGradeQ16 = GradeQ16;
Constraints.sweepSpacingSubvoxels = StepQ;
Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ;
Constraints.supportSearchDepthSubvoxels = SupportDepthQ;
Constraints.minimumClearanceSubvoxels = 8;
Constraints.minimumSupportProbes = 3;
const Heightfield2D BaseField(Recipe, Stratum);
if (!BaseField.isValid())
{
Error = "SurfaceWorld route planner received an invalid heightfield.";
return false;
}
std::vector<FCandidate> Candidates;
constexpr std::int32_t WaypointSpacing = 8;
for (std::int32_t CenterY = -256; CenterY <= 256; CenterY += 64)
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 64)
for (std::int32_t DirectionIndex = 0;
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
{
FCandidate Candidate;
Candidate.CenterX = CenterX;
Candidate.CenterY = CenterY;
Candidate.Direction = DirectionIndex;
Candidate.Points.reserve(33);
std::int64_t PreviousGround = 0;
std::int64_t PreviousX = 0;
std::int64_t PreviousY = 0;
bool bGradePass = true;
for (std::int32_t Index = 0; Index <= 32; ++Index)
{
const std::int64_t Along = (Index - 16) * WaypointSpacing;
const std::int64_t X = CenterX + WorldAssemblyRoundRatio(
static_cast<std::int64_t>(Along) * Directions[DirectionIndex].X, 65536);
const std::int64_t Y = CenterY + WorldAssemblyRoundRatio(
static_cast<std::int64_t>(Along) * Directions[DirectionIndex].Y, 65536);
const auto Column = BaseField.point(WorldAssemblyFixed(static_cast<double>(X)),
WorldAssemblyFixed(static_cast<double>(Y)), 0);
const std::int64_t DeltaX = X - PreviousX;
const std::int64_t DeltaY = Y - PreviousY;
const std::uint64_t HorizontalVoxels = WorldAssemblyRoundedSqrt(
static_cast<std::uint64_t>(DeltaX * DeltaX + DeltaY * DeltaY));
const std::int64_t HorizontalQ = static_cast<std::int64_t>(HorizontalVoxels)
* WorldAssemblySubvoxelsPerVoxel;
if (Index > 0)
{
const std::int64_t RiseQ = std::abs(Column.groundHeightSubvoxels - PreviousGround);
if (HorizontalQ == 0 || RiseQ * 65536 > HorizontalQ * GradeQ16)
{
bGradePass = false;
break;
}
Candidate.Score += RiseQ * 65536 / std::max<std::int64_t>(1, HorizontalQ);
}
WorldAssemblyRoutePointV1 Point;
Point.positionSubvoxels = {WorldAssemblyFixed(static_cast<double>(X)),
WorldAssemblyFixed(static_cast<double>(Y)),
Column.groundHeightSubvoxels + HalfHeightQ};
Point.radiusSubvoxels = RouteRadiusQ;
Point.stableFeatureId = WorldAssemblyFeatureId(Recipe.seed, X, Y,
0x52540000u + static_cast<std::uint32_t>(Index));
Candidate.Points.push_back(Point);
PreviousGround = Column.groundHeightSubvoxels;
PreviousX = X;
PreviousY = Y;
}
if (bGradePass) Candidates.push_back(std::move(Candidate));
}
std::stable_sort(Candidates.begin(), Candidates.end(), [](const FCandidate& A, const FCandidate& B)
{
if (A.Score != B.Score) return A.Score < B.Score;
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
return A.Direction < B.Direction;
});
for (const FCandidate& Candidate : Candidates)
{
Stratum.route = Candidate.Points;
std::int64_t LiftQ = 0;
constexpr std::int64_t RepairStepQ = 64;
constexpr std::int64_t MaximumRepairLiftQ = SupportDepthQ - RepairStepQ;
for (;;)
{
if (LiftQ > 0)
{
for (std::size_t PointIndex = 0; PointIndex < Stratum.route.size(); ++PointIndex)
{
Stratum.route[PointIndex].positionSubvoxels[2] =
Candidate.Points[PointIndex].positionSubvoxels[2] + LiftQ;
}
}
const Heightfield2D CandidateField(Recipe, Stratum);
if (AuditHeightfieldRoute(CandidateField).passed) return true;
if (LiftQ >= MaximumRepairLiftQ) break;
LiftQ += RepairStepQ;
}
}
Error = "SurfaceWorld route candidates failed the capsule and support sweep.";
return false;
}
VoxelForgeCore::WorldAssemblyBiomeProfileV1 BuildSurfaceProfile(
std::uint32_t StableId, const FSurfaceGenerationParams& Params,
const FBiomeResolved* Climate, double BottomVoxel, double HeightVoxels)
{
using namespace VoxelForgeCore;
WorldAssemblyBiomeProfileV1 Profile;
Profile.stableId = StableId;
Profile.baseHeightSubvoxels = WorldAssemblyFixed(BottomVoxel
+ HeightVoxels * Params.BaseGroundRelative);
Profile.continentAmplitudeSubvoxels = WorldAssemblyFixed(Params.ElevationRange);
Profile.continentWavelengthSubvoxels = WorldAssemblyWavelength(Params.ContinentFrequency, 1000.0);
Profile.mountainAmplitudeSubvoxels = WorldAssemblyFixed(Params.ElevationRange * Params.MountainStrength);
Profile.mountainWavelengthSubvoxels = WorldAssemblyWavelength(Params.MountainFrequency, 500.0);
Profile.detailAmplitudeSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness);
Profile.detailWavelengthSubvoxels = WorldAssemblyWavelength(Params.DetailFrequency, 25.0);
Profile.reliefMinimumQ16 = Climate ? WorldAssemblyQ16(Climate->ReliefMin) : 0;
Profile.reliefMaximumQ16 = Climate ? WorldAssemblyQ16(Climate->ReliefMax) : 65536;
Profile.moistureMinimumQ16 = Climate ? WorldAssemblyQ16(Climate->MoistureMin) : 0;
Profile.moistureMaximumQ16 = Climate ? WorldAssemblyQ16(Climate->MoistureMax) : 65536;
Profile.reliefStrengthQ16 = WorldAssemblyQ16(Params.ReliefStrength);
Profile.reliefContrastQ16 = WorldAssemblyQ16(Params.ReliefContrast);
Profile.reliefWavelengthSubvoxels = WorldAssemblyWavelength(Params.ReliefFrequency, 667.0);
Profile.heightWarpStrengthSubvoxels = WorldAssemblyFixed(Params.HeightWarpStrength);
Profile.heightWarpWavelengthSubvoxels = WorldAssemblyWavelength(Params.HeightWarpFrequency, 125.0);
Profile.beachWidthSubvoxels = WorldAssemblyFixed(Params.BeachWidth);
Profile.cliffStrengthQ16 = WorldAssemblyQ16(Params.CliffStrength);
Profile.cliffSlopeThresholdQ16 = std::max<std::int64_t>(1, WorldAssemblyQ16(Params.CliffSlopeThreshold));
Profile.cliffSharpnessQ16 = WorldAssemblyQ16(Params.CliffSharpness);
Profile.cliffSampleDistanceSubvoxels = WorldAssemblyFixed(std::max(0.5f, Params.CliffSampleDist));
const double StructuralAbsBound = static_cast<double>(Params.ElevationRange)
+ static_cast<double>(Params.ElevationRange) * Params.MountainStrength
+ 2.0 * Params.SurfaceRoughness;
const double CliffAbsBound = 2.0 * StructuralAbsBound * Params.CliffStrength * Params.CliffSharpness;
Profile.cliffMaximumDisplacementSubvoxels = WorldAssemblyFixed(Params.CliffStrength > 0.0f
? std::max(1.0, CliffAbsBound) : 0.0);
Profile.hasCapProfile = true;
Profile.capHeightSubvoxels = WorldAssemblyFixed(BottomVoxel + HeightVoxels * Params.CeilingRelative);
Profile.capWarpStrengthSubvoxels = WorldAssemblyFixed(Params.CeilingWarpStrength);
Profile.capWarpWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingWarpFrequency, 100.0);
Profile.capUndulationSubvoxels = WorldAssemblyFixed(Params.CeilingUndulation);
Profile.capWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingUndulationFrequency, 500.0);
Profile.capDetailAmplitudeSubvoxels = WorldAssemblyFixed(Params.CeilingRoughness);
Profile.capDetailWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingRoughnessFrequency, 25.0);
Profile.capRidgeAmplitudeSubvoxels = WorldAssemblyFixed(Params.CeilingRidgeStrength);
Profile.capRidgeWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingRidgeFrequency, 50.0);
return Profile;
}
bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
const FStrateSlot& Slot,
VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, FString& OutError)
{
using namespace VoxelForgeCore;
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
const FSurfaceGenerationParams Surface = StrateManager->GetSurfaceParamsForChunk(Chunk);
const FBiomeContext BiomeContext = StrateManager->GetBiomeContextForChunk(Chunk);
const UVoxelStrateDefinition* Definition = Slot.Definition;
const double BottomVoxel = static_cast<double>(Slot.BottomChunkZ) * CHUNK_SIZE;
const double HeightVoxels = static_cast<double>(Slot.TopChunkZ - Slot.BottomChunkZ + 1) * CHUNK_SIZE;
const FBiomeMapParams& Map = BiomeContext.Map;
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
Recipe.biomeCellSizeSubvoxels = WorldAssemblyFixed(Map.CellSize);
Recipe.biomeWarpSubvoxels = WorldAssemblyFixed(Map.WarpStrength);
Recipe.biomeWarpWavelengthSubvoxels = WorldAssemblyWavelength(Map.WarpFrequency, 556.0);
Recipe.biomeBorderBlendSubvoxels = WorldAssemblyFixed(Map.BorderBlend);
Recipe.biomeClimateReliefWavelengthSubvoxels = WorldAssemblyWavelength(Map.ReliefFrequency, 3333.0);
Recipe.biomeClimateMoistureWavelengthSubvoxels = WorldAssemblyWavelength(Map.MoistureFrequency, 2500.0);
Recipe.biomeClimateReliefContrastQ16 = WorldAssemblyQ16(Map.ReliefContrast);
Recipe.biomes.clear();
if (BiomeContext.IsValid() && Definition != nullptr)
{
for (const FBiomeResolved& Resolved : BiomeContext.Biomes)
{
const UVoxelBiomeDefinition* Biome = Definition->Biomes.IsValidIndex(Resolved.Index)
? Definition->Biomes[Resolved.Index] : nullptr;
FSurfaceGenerationParams Params = Surface;
if (Biome != nullptr && Biome->bOverrideTerrain
&& Biome->GeneratorType == ECaveGeneratorType::SurfaceWorld)
{
Params = Biome->SurfaceParams;
Params.StrateTopWorldZ = Surface.StrateTopWorldZ;
Params.StrateBottomWorldZ = Surface.StrateBottomWorldZ;
Params.BoundarySealThickness = Surface.BoundarySealThickness;
Params.BaseDensity = Surface.BaseDensity;
Params.WaterLevelRelative = Surface.WaterLevelRelative;
}
Recipe.biomes.push_back(BuildSurfaceProfile(
0x1001u + static_cast<std::uint32_t>(Resolved.Index), Params, &Resolved,
BottomVoxel, HeightVoxels));
}
}
if (Recipe.biomes.empty())
{
Recipe.biomes.push_back(BuildSurfaceProfile(0x1001u, Surface, nullptr,
BottomVoxel, HeightVoxels));
}
Stratum.strateIndex = Slot.StrateIndex;
Stratum.topChunkZ = Slot.TopChunkZ;
Stratum.bottomChunkZ = Slot.BottomChunkZ;
Stratum.capHeightSubvoxels = WorldAssemblyFixed(BottomVoxel + HeightVoxels * Surface.CeilingRelative);
Stratum.capUndulationSubvoxels = WorldAssemblyFixed(Surface.CeilingUndulation);
Stratum.capWavelengthSubvoxels = WorldAssemblyWavelength(Surface.CeilingUndulationFrequency, 250.0);
Stratum.capDetailAmplitudeSubvoxels = WorldAssemblyFixed(Surface.CeilingRoughness);
Stratum.capDetailWavelengthSubvoxels = WorldAssemblyWavelength(Surface.CeilingRoughnessFrequency, 25.0);
Stratum.capRidgeAmplitudeSubvoxels = WorldAssemblyFixed(Surface.CeilingRidgeStrength);
Stratum.capRidgeWavelengthSubvoxels = WorldAssemblyWavelength(Surface.CeilingRidgeFrequency, 50.0);
Stratum.waterSurfaceSubvoxels = WorldAssemblyFixed(BottomVoxel
+ HeightVoxels * Surface.WaterLevelRelative);
Stratum.waterEnabled = Definition != nullptr && Definition->bHasWater
&& Surface.WaterLevelRelative > 0.0f;
{
// The hashed plan must not depend on platform-specific legacy float sampling.
Heightfield2D BaseField(Recipe, Stratum);
if (!BaseField.isValid())
{
OutError = TEXT("SurfaceWorld automatic plan could not initialize its fixed-point field.");
return false;
}
constexpr int ProbeSide = 49;
constexpr int ProbeStep = 32;
std::array<std::int64_t, ProbeSide * ProbeSide> Heights{};
for (int Y = 0; Y < ProbeSide; ++Y)
for (int X = 0; X < ProbeSide; ++X)
{
const std::int64_t PX = static_cast<std::int64_t>(X - ProbeSide / 2) * ProbeStep;
const std::int64_t PY = static_cast<std::int64_t>(Y - ProbeSide / 2) * ProbeStep;
const std::size_t Index = static_cast<std::size_t>(Y) * ProbeSide + X;
Heights[Index] = BaseField.point(WorldAssemblyFixed(static_cast<double>(PX)),
WorldAssemblyFixed(static_cast<double>(PY)), 0).groundHeightSubvoxels;
}
std::int64_t SteepestNumerator = 0;
int CliffX = 0;
int CliffY = 0;
bool bXAxis = true;
int Downhill = 1;
for (int Y = 1; Y < ProbeSide - 1; ++Y)
for (int X = 1; X < ProbeSide - 1; ++X)
{
const std::size_t Center = static_cast<std::size_t>(Y) * ProbeSide + X;
const std::int64_t DX = Heights[Center + 1] - Heights[Center - 1];
const std::int64_t DY = Heights[Center + ProbeSide] - Heights[Center - ProbeSide];
const std::int64_t SlopeNumerator = std::abs(DX) + std::abs(DY);
if (SlopeNumerator > SteepestNumerator)
{
SteepestNumerator = SlopeNumerator;
CliffX = (X - ProbeSide / 2) * ProbeStep;
CliffY = (Y - ProbeSide / 2) * ProbeStep;
bXAxis = std::abs(DX) >= std::abs(DY);
Downhill = (bXAxis ? DX : DY) >= 0 ? -1 : 1;
}
}
if (SteepestNumerator * 20
> 4 * ProbeStep * WorldAssemblySubvoxelsPerVoxel)
{
WorldAssemblyFeatureV1 Shelf;
Shelf.stableId = WorldAssemblyFeatureId(Recipe.seed, CliffX, CliffY, 0x4f564552u);
Shelf.type = 4;
constexpr int Reach = 72;
constexpr int Width = 36;
constexpr int ShelfHeight = 64;
constexpr int Feather = 8;
const int DirX = bXAxis ? Downhill : 0;
const int DirY = bXAxis ? 0 : Downhill;
const int MinX = bXAxis ? (DirX > 0 ? CliffX - Feather : CliffX - Reach - Feather)
: CliffX - Width - Feather;
const int MaxX = bXAxis ? (DirX > 0 ? CliffX + Reach + Feather : CliffX + Feather)
: CliffX + Width + Feather;
const int MinY = bXAxis ? CliffY - Width - Feather
: (DirY > 0 ? CliffY - Feather : CliffY - Reach - Feather);
const int MaxY = bXAxis ? CliffY + Width + Feather
: (DirY > 0 ? CliffY + Reach + Feather : CliffY + Feather);
std::int64_t LowestGround = std::numeric_limits<std::int64_t>::max();
std::int64_t HighestGround = std::numeric_limits<std::int64_t>::min();
for (int SampleY = MinY; SampleY <= MaxY; SampleY += 4)
for (int SampleX = MinX; SampleX <= MaxX; SampleX += 4)
{
const auto Sample = BaseField.point(WorldAssemblyFixed(SampleX),
WorldAssemblyFixed(SampleY), 0);
LowestGround = std::min(LowestGround, Sample.groundHeightSubvoxels);
HighestGround = std::max(HighestGround, Sample.groundHeightSubvoxels);
}
if (LowestGround == std::numeric_limits<std::int64_t>::max()
|| HighestGround == std::numeric_limits<std::int64_t>::min())
{
OutError = TEXT("SurfaceWorld automatic plan could not bound its terrain shelf.");
return false;
}
Shelf.minimumSubvoxels = {WorldAssemblyFixed(MinX), WorldAssemblyFixed(MinY),
std::max(WorldAssemblyFixed(BottomVoxel), LowestGround - WorldAssemblyFixed(4.0))};
Shelf.maximumSubvoxels = {WorldAssemblyFixed(MaxX), WorldAssemblyFixed(MaxY),
std::min(WorldAssemblyFixed((Slot.TopChunkZ + 1) * CHUNK_SIZE),
HighestGround + WorldAssemblyFixed(ShelfHeight + 4.0))};
Shelf.parametersSubvoxels = {WorldAssemblyFixed(DirX), WorldAssemblyFixed(DirY),
WorldAssemblyFixed(ShelfHeight), WorldAssemblyFixed(Reach)};
Stratum.features.push_back(Shelf);
}
}
std::string RouteError;
if (!BuildFiniteSurfaceRoute(Recipe, Stratum, RouteError))
{
OutError = UTF8_TO_TCHAR(RouteError.c_str());
return false;
}
return true;
}
bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest,
FString& OutError)
{
using namespace VoxelForgeCore;
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
const FFloatingIslandParams Params = StrateManager->GetFloatingIslandParamsForChunk(Chunk);
WorldAssemblyFloatingIslandPlanV1 IslandPlan;
IslandPlan.strateIndex = Slot.StrateIndex;
IslandPlan.topChunkZ = Slot.TopChunkZ;
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
auto& Recipe = IslandPlan.manifest.recipe;
Recipe.geometryVersion = 2;
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
const std::int64_t BottomCoreVoxels = static_cast<std::int64_t>(Slot.BottomChunkZ) * 16;
const std::int64_t TopCoreVoxels = static_cast<std::int64_t>(Slot.TopChunkZ + 1) * 16;
Recipe.layoutMinimumZSubvoxels = BottomCoreVoxels * WorldAssemblySubvoxelsPerVoxel;
Recipe.layoutMaximumZSubvoxels = TopCoreVoxels * WorldAssemblySubvoxelsPerVoxel;
Recipe.layoutCenterTopZSubvoxels = (BottomCoreVoxels + TopCoreVoxels)
* WorldAssemblySubvoxelsPerVoxel / 2;
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale);
Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale);
Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale);
Recipe.thicknessRatioQ16 = WorldAssemblyQ16(Params.ThicknessRatio);
Recipe.verticalJitterQ16 = WorldAssemblyQ16(Params.VerticalJitter);
Recipe.topFlattenQ16 = WorldAssemblyQ16(Params.TopFlatten);
Recipe.roughnessSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness * OwnerToCoreScale);
Recipe.blendRadiusSubvoxels = WorldAssemblyFixed(Params.SDFBlendRadius * OwnerToCoreScale);
Recipe.boundarySealSubvoxels = WorldAssemblyFixed(Params.BoundarySealThickness * OwnerToCoreScale);
std::string PlanError;
const std::optional<FloatingIslandManifestV1> Planned = PlanFloatingIslandCluster(Recipe, PlanError);
if (!Planned)
{
OutError = FString::Printf(TEXT("FloatingIslands automatic plan for strate %d failed: %s"),
Slot.StrateIndex, UTF8_TO_TCHAR(PlanError.c_str()));
return false;
}
IslandPlan.manifest = *Planned;
Manifest.floatingIslands.push_back(std::move(IslandPlan));
return true;
}
TSharedPtr<FJsonValue> SerializeCoreStruct( TSharedPtr<FJsonValue> SerializeCoreStruct(
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects, const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
@@ -287,21 +748,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
{ {
return false; return false;
} }
if (ManifestJson.IsEmpty()) std::string CoreError;
std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest;
const bool bImportedOverride = !ManifestJson.IsEmpty();
if (bImportedOverride)
{ {
return true; FTCHARToUTF8 Utf8Json(*ManifestJson);
Manifest = VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!Manifest)
{
OutError = FString::Printf(TEXT("Core world assembly validation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
}
else
{
VoxelForgeCore::WorldAssemblyManifest Generated;
Generated.recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
int32 SurfaceSlotCount = 0;
int32 IslandSlotCount = 0;
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition == nullptr) continue;
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
FIntVector(0, 0, Slot.TopChunkZ));
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
}
if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
{
return true;
}
if (SurfaceSlotCount == 0)
{
VoxelForgeCore::WorldAssemblyBiomeProfileV1 CarrierProfile;
CarrierProfile.stableId = 0x1001u;
Generated.recipe.biomes.push_back(CarrierProfile);
}
int32 BuiltSurfaceCount = 0;
std::vector<VoxelForgeCore::WorldAssemblyRecipeV1> SurfaceRecipes;
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition == nullptr) continue;
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
FIntVector(0, 0, Slot.TopChunkZ));
if (Type == ECaveGeneratorType::SurfaceWorld)
{
VoxelForgeCore::WorldAssemblyRecipeV1 SlotRecipe;
SlotRecipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
VoxelForgeCore::WorldAssemblyStratumPlanV1 Stratum;
if (!BuildSurfaceStratumPlan(StrateManager, Slot,
SlotRecipe, Stratum, OutError))
{
return false;
}
if (BuiltSurfaceCount == 0) Generated.recipe = SlotRecipe;
SurfaceRecipes.push_back(SlotRecipe);
Generated.plan.strates.push_back(std::move(Stratum));
++BuiltSurfaceCount;
}
else if (Type == ECaveGeneratorType::FloatingIslands)
{
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated, OutError))
{
return false;
}
}
}
if (BuiltSurfaceCount > 1)
{
for (std::size_t Index = 0; Index < Generated.plan.strates.size(); ++Index)
{
Generated.plan.strates[Index].hasRecipeOverride = true;
Generated.plan.strates[Index].recipeOverride = SurfaceRecipes[Index];
}
}
if (!Generated.finalize(CoreError))
{
OutError = FString::Printf(TEXT("Core automatic world assembly planning failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
ManifestHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Generated.contentHash));
Manifest = MoveTemp(Generated);
} }
FTCHARToUTF8 Utf8Json(*ManifestJson); if (!Manifest.has_value())
std::string CoreError;
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!Manifest)
{ {
OutError = FString::Printf(TEXT("Core world assembly validation failed: %s"), return true;
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
} }
if (Manifest->recipe.seed != static_cast<uint32>(StrateManager->GetWorldSeed())) if (Manifest->recipe.seed != static_cast<uint32>(StrateManager->GetWorldSeed()))
{ {
@@ -394,6 +932,12 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
} }
Root->SetObjectField(TEXT("world_assembly"), ManifestObject.ToSharedRef()); Root->SetObjectField(TEXT("world_assembly"), ManifestObject.ToSharedRef());
Root->SetStringField(TEXT("world_assembly_content_hash"), ManifestHash); Root->SetStringField(TEXT("world_assembly_content_hash"), ManifestHash);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreRuntime] world_assembly_plan=%s source=%s surface_strates=%d island_strates=%d hash=%s"),
bImportedOverride ? TEXT("imported") : TEXT("generated_at_startup"),
bImportedOverride ? TEXT("settings_override") : TEXT("resolved_settings_and_layout"),
static_cast<int32>(Manifest->plan.strates.size()),
static_cast<int32>(Manifest->floatingIslands.size()), *ManifestHash);
return true; return true;
} }
} }
@@ -95,6 +95,7 @@ bool UVoxelSettings::SetWorldAssemblyManifestJson(const FString& InJson, FString
WorldAssemblyManifestJson = UTF8_TO_TCHAR(CanonicalJson.c_str()); WorldAssemblyManifestJson = UTF8_TO_TCHAR(CanonicalJson.c_str());
WorldAssemblyManifestHash = FString::Printf(TEXT("%016llx"), WorldAssemblyManifestHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Manifest->contentHash)); static_cast<unsigned long long>(Manifest->contentHash));
bUseWorldAssemblyManifestOverride = true;
MarkPackageDirty(); MarkPackageDirty();
OutReport = FString::Printf( OutReport = FString::Printf(
TEXT("imported native world assembly %s (%d SurfaceWorld plans, %d floating-island plans)"), TEXT("imported native world assembly %s (%d SurfaceWorld plans, %d floating-island plans)"),
@@ -109,8 +110,17 @@ bool UVoxelSettings::GetWorldAssemblyManifestJson(
OutCanonicalJson.Reset(); OutCanonicalJson.Reset();
OutContentHash.Reset(); OutContentHash.Reset();
OutError.Reset(); OutError.Reset();
if (!bUseWorldAssemblyManifestOverride)
{
return true;
}
if (WorldAssemblyManifestJson.IsEmpty()) if (WorldAssemblyManifestJson.IsEmpty())
{ {
if (bUseWorldAssemblyManifestOverride)
{
OutError = TEXT("world assembly override is enabled but its manifest JSON is empty");
return false;
}
if (!WorldAssemblyManifestHash.IsEmpty()) if (!WorldAssemblyManifestHash.IsEmpty())
{ {
OutError = TEXT("world assembly hash is set but its manifest JSON is empty"); OutError = TEXT("world assembly hash is set but its manifest JSON is empty");
+122 -17
View File
@@ -152,6 +152,7 @@ public:
std::atomic<bool> bShuttingDown{false}; std::atomic<bool> bShuttingDown{false};
std::atomic<uint64> NextEditSequence{1}; std::atomic<uint64> NextEditSequence{1};
int32 FullResolutionLodCount = 0; int32 FullResolutionLodCount = 0;
FString InitialFailureReason;
}; };
#if WITH_EDITOR #if WITH_EDITOR
@@ -2456,12 +2457,13 @@ void AVoxelWorld::BeginPlay()
DiffLayer->SetBudget(Settings->MaxModifications, Settings->MaxBrushRadius, Settings->MaxTotalVolume); DiffLayer->SetBudget(Settings->MaxModifications, Settings->MaxBrushRadius, Settings->MaxTotalVolume);
Generator->SetDiffLayer(DiffLayer); Generator->SetDiffLayer(DiffLayer);
if (bUseVoxelForgeCore && !InitializeVoxelForgeCoreWorld()) if (bUseVoxelForgeCore)
{ {
// A failed opt-in must stay visibly failed. Falling back to the legacy route here would FString CoreFailureReason;
// make the switch appear to work while silently changing the generated world. if (!InitializeVoxelForgeCoreWorld(CoreFailureReason))
UE_LOG(LogTemp, Error, {
TEXT("[VoxelForgeCoreRuntime] Core mode is enabled but initialization did not complete.")); FallBackToLegacyAfterCoreInitializationFailure(CoreFailureReason);
}
} }
if (bTestCeilingViewEnabled && IsParameterizedResearchFieldActive()) if (bTestCeilingViewEnabled && IsParameterizedResearchFieldActive())
@@ -6596,6 +6598,12 @@ void AVoxelWorld::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown
if (bUseVoxelForgeCore && VoxelForgeCoreState.IsValid()
&& VoxelForgeCoreState->bInitialFailed.load(std::memory_order_acquire))
{
FallBackToLegacyAfterCoreInitializationFailure(
VoxelForgeCoreState->InitialFailureReason);
}
#if WITH_EDITOR #if WITH_EDITOR
if (bTestResearchProfileSwitchConfigured if (bTestResearchProfileSwitchConfigured
&& !bTestResearchProfileSwitchApplied && !bTestResearchProfileSwitchApplied
@@ -7217,6 +7225,14 @@ FVector AVoxelWorld::GetVoxelForgeCoreDefaultCenterWorld() const
return GetActorTransform().TransformPosition(CoreFallbackCenterLocalCm); return GetActorTransform().TransformPosition(CoreFallbackCenterLocalCm);
} }
bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
FString& OutCanonicalJson, FString& OutContentHash) const
{
OutCanonicalJson = VoxelForgeCoreWorldAssemblyJson;
OutContentHash = VoxelForgeCoreWorldAssemblyHash;
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty();
}
bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
{ {
OutDensity = 0.0f; OutDensity = 0.0f;
@@ -7230,13 +7246,15 @@ bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& Out
return FMath::IsFinite(OutDensity); return FMath::IsFinite(OutDensity);
} }
bool AVoxelWorld::InitializeVoxelForgeCoreWorld() bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
{ {
OutFailureReason.Reset();
VoxelForgeCoreWorldAssemblyJson.Reset();
VoxelForgeCoreWorldAssemblyHash.Reset();
if (!bUseVoxelForgeCore) return true; if (!bUseVoxelForgeCore) return true;
if (!Settings || !StrateManager) if (!Settings || !StrateManager)
{ {
UE_LOG(LogTemp, Error, OutFailureReason = TEXT("Settings or the resolved strate layout is missing.");
TEXT("[VoxelForgeCoreRuntime] initialization failed: Settings or resolved StrateManager is missing."));
return false; return false;
} }
@@ -7247,7 +7265,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld( if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
Settings, StrateManager, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, Json, ExportError)) Settings, StrateManager, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, Json, ExportError))
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] snapshot export failed: %s"), *ExportError); OutFailureReason = FString::Printf(TEXT("snapshot export failed: %s"), *ExportError);
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
return false; return false;
} }
FTCHARToUTF8 Utf8Json(*Json); FTCHARToUTF8 Utf8Json(*Json);
@@ -7257,12 +7276,22 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError); std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!Snapshot) if (!Snapshot)
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] schema-v1 resolve failed: %s"), OutFailureReason = FString::Printf(TEXT("schema-v1 snapshot resolve failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
return false; return false;
} }
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly()) if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
{ {
const std::string CanonicalAssembly = Assembly->ToJson();
if (CanonicalAssembly.empty())
{
OutFailureReason = TEXT("the generated world assembly could not be serialized canonically.");
return false;
}
VoxelForgeCoreWorldAssemblyJson = UTF8_TO_TCHAR(CanonicalAssembly.c_str());
VoxelForgeCoreWorldAssemblyHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Assembly->contentHash));
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx surface_strates=%d island_strates=%d"), TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx surface_strates=%d island_strates=%d"),
static_cast<unsigned long long>(Assembly->contentHash), static_cast<unsigned long long>(Assembly->contentHash),
@@ -7294,7 +7323,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z}); {CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
if (InitialDesired.empty()) if (InitialDesired.empty())
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] Core returned an empty initial clipmap.")); OutFailureReason = TEXT("Core produced an empty initial clipmap.");
return false; return false;
} }
@@ -7327,7 +7356,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
} }
if (FineMin.x == IntMax || ClipMin.x == IntMax) if (FineMin.x == IntMax || ClipMin.x == IntMax)
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] Core initial clipmap bounds are invalid.")); OutFailureReason = TEXT("Core produced invalid initial clipmap bounds.");
return false; return false;
} }
@@ -7373,6 +7402,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
} }
if (!bCompiled) if (!bCompiled)
{ {
State->InitialFailureReason = FString::Printf(
TEXT("Core shape compilation failed: %s"), UTF8_TO_TCHAR(Error.c_str()));
State->bInitialFailed.store(true, std::memory_order_release); State->bInitialFailed.store(true, std::memory_order_release);
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] shape compile failed: %s"), UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] shape compile failed: %s"),
UTF8_TO_TCHAR(Error.c_str())); UTF8_TO_TCHAR(Error.c_str()));
@@ -7394,6 +7425,49 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
return true; return true;
} }
void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString& FailureReason)
{
if (!bUseVoxelForgeCore) return;
const FString Reason = FailureReason.IsEmpty()
? TEXT("Core initialization failed without a diagnostic reason.") : FailureReason;
if (VoxelForgeCoreState.IsValid())
{
VoxelForgeCoreState->bShuttingDown.store(true, std::memory_order_release);
VoxelForgeCoreState.Reset();
}
VoxelForgeCoreWorldAssemblyJson.Reset();
VoxelForgeCoreWorldAssemblyHash.Reset();
bUseVoxelForgeCore = false;
CoreFallbackCenterLocalCm = FVector::ZeroVector;
if (Mesher != nullptr)
{
Mesher->SetGeometryVoxelSizeCm(VOXEL_SIZE);
}
if (ContentManager != nullptr && Settings != nullptr)
{
ContentManager->Initialize(this, StrateManager, Generator, Settings,
Settings->GetEffectiveWorldSeed());
ContentManager->ClearAll();
}
if (AtmosphereManager != nullptr)
{
AtmosphereManager->Reset();
}
RegenerateAllChunks();
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeCoreRuntime] Core initialization failed; falling back to the legacy generator. reason=%s"),
*Reason);
if (GEngine != nullptr)
{
const FString ScreenMessage = FString::Printf(
TEXT("VoxelForge Core could not start: %s Legacy voxel generation is active."), *Reason);
GEngine->AddOnScreenDebugMessage(INDEX_NONE, 20.0f, FColor::Red, ScreenMessage);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreRuntime] fallback_notice=on_screen seconds=20 reason=%s"), *Reason);
}
}
bool AVoxelWorld::TryGetPlayerPosition(FVector& OutPosition, APawn** OutPawn) const bool AVoxelWorld::TryGetPlayerPosition(FVector& OutPosition, APawn** OutPawn) const
{ {
OutPosition = FVector::ZeroVector; OutPosition = FVector::ZeroVector;
@@ -13293,12 +13367,43 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
return SeasonHash; return SeasonHash;
} }
FString AssemblyJson; FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
FString AssemblyHash; if (AssemblyHash.IsEmpty())
FString AssemblyError;
if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError))
{ {
return FString(); FString AssemblyJson;
FString AssemblyError;
if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError))
{
return FString();
}
if (AssemblyHash.IsEmpty() && StrateManager != nullptr)
{
// Multiplayer/content-hash callers can ask before BeginPlay has cached the startup
// artifact. Resolve the same deterministic plan from the already-resolved settings
// and layout instead of temporarily returning an identity without that plan.
FString SnapshotJson;
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(Settings, StrateManager,
FMath::Clamp(CoreClipRadius, 1, 64),
FMath::Clamp(CoreMaxClipLevel, 0, 8), SnapshotJson, AssemblyError))
{
return FString();
}
FTCHARToUTF8 Utf8Snapshot(*SnapshotJson);
std::string CoreError;
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> Snapshot =
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(Utf8Snapshot.Get(),
static_cast<std::size_t>(Utf8Snapshot.Length())), CoreError);
if (!Snapshot)
{
return FString();
}
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
{
AssemblyHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Assembly->contentHash));
}
}
} }
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1; const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
if (AssemblyHash.IsEmpty() && !bHybridMesher) if (AssemblyHash.IsEmpty() && !bHybridMesher)
+5
View File
@@ -383,6 +383,11 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Voxel|Strates") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
FString WorldAssemblyManifestHash; FString WorldAssemblyManifestHash;
/** Use imported JSON above instead of planning from the resolved settings and layout. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates|World Assembly",
meta = (DisplayName = "Use Imported World Assembly Override"))
bool bUseWorldAssemblyManifestOverride = false;
#if WITH_EDITORONLY_DATA #if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, Category = "Voxel|Strates|World Assembly", meta = (FilePathFilter = "json")) UPROPERTY(EditAnywhere, Category = "Voxel|Strates|World Assembly", meta = (FilePathFilter = "json"))
FFilePath SourceWorldAssemblyManifestJson; FFilePath SourceWorldAssemblyManifestJson;
+5 -1
View File
@@ -588,6 +588,7 @@ public:
bool IsVoxelForgeCoreReady() const; bool IsVoxelForgeCoreReady() const;
FVector GetVoxelForgeCoreDefaultCenterWorld() const; FVector GetVoxelForgeCoreDefaultCenterWorld() const;
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const; bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate") UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const; int32 GetStrateAtPosition(FVector WorldPosition) const;
@@ -675,7 +676,8 @@ private:
void SetDensityVolumeMaterialParamsDisabled(); void SetDensityVolumeMaterialParamsDisabled();
/** Runtime Core snapshot, world lifetime, and worker-side streaming bridge. */ /** Runtime Core snapshot, world lifetime, and worker-side streaming bridge. */
bool InitializeVoxelForgeCoreWorld(); bool InitializeVoxelForgeCoreWorld(FString& OutFailureReason);
void FallBackToLegacyAfterCoreInitializationFailure(const FString& FailureReason);
void BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, void BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
APawn* PlayerPawn, const FVector& PlayerHeading, APawn* PlayerPawn, const FVector& PlayerHeading,
TArray<FVoxelTileKey>& OutLeavers); TArray<FVoxelTileKey>& OutLeavers);
@@ -740,6 +742,8 @@ private:
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState; TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
FString VoxelForgeCoreWorldAssemblyJson;
FString VoxelForgeCoreWorldAssemblyHash;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector; FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
bool bCoreRegionCompilePending = false; bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles; TArray<FVoxelTileKey> PendingCoreRegionTiles;
File diff suppressed because it is too large. Load diff
@@ -254,6 +254,10 @@ namespace VoxelForgeCore
std::int32_t strateIndex = -1; std::int32_t strateIndex = -1;
std::int32_t topChunkZ = 0; std::int32_t topChunkZ = 0;
std::int32_t bottomChunkZ = 0; std::int32_t bottomChunkZ = 0;
// Optional per-stratum recipe for seasons containing differently authored SurfaceWorld
// slots. Empty preserves the original shared-recipe schema and its hashes.
bool hasRecipeOverride = false;
WorldAssemblyRecipeV1 recipeOverride;
std::int64_t capHeightSubvoxels = 0; std::int64_t capHeightSubvoxels = 0;
std::int64_t capUndulationSubvoxels = 0; std::int64_t capUndulationSubvoxels = 0;
std::int64_t capWavelengthSubvoxels = 256 * 512; std::int64_t capWavelengthSubvoxels = 256 * 512;
+94 -59
View File
@@ -342,6 +342,11 @@ namespace world_assembly_detail
HashWord(hash, static_cast<std::uint32_t>(stratum.strateIndex), 4); HashWord(hash, static_cast<std::uint32_t>(stratum.strateIndex), 4);
HashWord(hash, static_cast<std::uint32_t>(stratum.topChunkZ), 4); HashWord(hash, static_cast<std::uint32_t>(stratum.topChunkZ), 4);
HashWord(hash, static_cast<std::uint32_t>(stratum.bottomChunkZ), 4); HashWord(hash, static_cast<std::uint32_t>(stratum.bottomChunkZ), 4);
if (stratum.hasRecipeOverride)
{
HashWord(hash, 0x5354524154455250ull, 8);
HashWord(hash, HashRecipe(stratum.recipeOverride), 8);
}
HashWord(hash, static_cast<std::uint64_t>(stratum.capHeightSubvoxels), 8); HashWord(hash, static_cast<std::uint64_t>(stratum.capHeightSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(stratum.capUndulationSubvoxels), 8); HashWord(hash, static_cast<std::uint64_t>(stratum.capUndulationSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(stratum.capWavelengthSubvoxels), 8); HashWord(hash, static_cast<std::uint64_t>(stratum.capWavelengthSubvoxels), 8);
@@ -554,6 +559,65 @@ namespace world_assembly_detail
return true; return true;
} }
inline std::string RecipeJson(const WorldAssemblyRecipeV1& recipe)
{
std::ostringstream out;
out << "{\"schema_version\":" << recipe.schemaVersion
<< ",\"evaluator_version\":" << recipe.evaluatorVersion
<< ",\"units_per_voxel\":" << recipe.unitsPerVoxel
<< ",\"seed\":" << recipe.seed
<< ",\"biome_cell_size_q\":" << recipe.biomeCellSizeSubvoxels
<< ",\"biome_warp_q\":" << recipe.biomeWarpSubvoxels
<< ",\"biome_warp_wavelength_q\":" << recipe.biomeWarpWavelengthSubvoxels
<< ",\"biome_border_blend_q\":" << recipe.biomeBorderBlendSubvoxels;
if (recipe.evaluatorVersion >= 2)
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
{
const auto& biome = recipe.biomes[i];
if (i) out << ',';
out << "{\"id\":" << biome.stableId << ",\"base_height_q\":" << biome.baseHeightSubvoxels
<< ",\"continent_amplitude_q\":" << biome.continentAmplitudeSubvoxels
<< ",\"continent_wavelength_q\":" << biome.continentWavelengthSubvoxels
<< ",\"mountain_amplitude_q\":" << biome.mountainAmplitudeSubvoxels
<< ",\"mountain_wavelength_q\":" << biome.mountainWavelengthSubvoxels
<< ",\"detail_amplitude_q\":" << biome.detailAmplitudeSubvoxels
<< ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels;
if (recipe.evaluatorVersion >= 2)
out << ",\"relief_min_q16\":" << biome.reliefMinimumQ16
<< ",\"relief_max_q16\":" << biome.reliefMaximumQ16
<< ",\"moisture_min_q16\":" << biome.moistureMinimumQ16
<< ",\"moisture_max_q16\":" << biome.moistureMaximumQ16
<< ",\"relief_strength_q16\":" << biome.reliefStrengthQ16
<< ",\"relief_contrast_q16\":" << biome.reliefContrastQ16
<< ",\"relief_wavelength_q\":" << biome.reliefWavelengthSubvoxels
<< ",\"height_warp_strength_q\":" << biome.heightWarpStrengthSubvoxels
<< ",\"height_warp_wavelength_q\":" << biome.heightWarpWavelengthSubvoxels
<< ",\"beach_width_q\":" << biome.beachWidthSubvoxels
<< ",\"cliff_strength_q16\":" << biome.cliffStrengthQ16
<< ",\"cliff_slope_threshold_q16\":" << biome.cliffSlopeThresholdQ16
<< ",\"cliff_sharpness_q16\":" << biome.cliffSharpnessQ16
<< ",\"cliff_sample_distance_q\":" << biome.cliffSampleDistanceSubvoxels
<< ",\"cliff_max_displacement_q\":" << biome.cliffMaximumDisplacementSubvoxels
<< ",\"cap_enabled\":" << (biome.hasCapProfile ? "true" : "false")
<< ",\"cap_height_q\":" << biome.capHeightSubvoxels
<< ",\"cap_warp_strength_q\":" << biome.capWarpStrengthSubvoxels
<< ",\"cap_warp_wavelength_q\":" << biome.capWarpWavelengthSubvoxels
<< ",\"cap_undulation_q\":" << biome.capUndulationSubvoxels
<< ",\"cap_wavelength_q\":" << biome.capWavelengthSubvoxels
<< ",\"cap_detail_amplitude_q\":" << biome.capDetailAmplitudeSubvoxels
<< ",\"cap_detail_wavelength_q\":" << biome.capDetailWavelengthSubvoxels
<< ",\"cap_ridge_amplitude_q\":" << biome.capRidgeAmplitudeSubvoxels
<< ",\"cap_ridge_wavelength_q\":" << biome.capRidgeWavelengthSubvoxels;
out << '}';
}
out << "]}";
return out.str();
}
inline bool ParsePlan(const VFJson::Value& value, WorldAssemblyPlanV1& plan, inline bool ParsePlan(const VFJson::Value& value, WorldAssemblyPlanV1& plan,
std::string& error) std::string& error)
{ {
@@ -573,6 +637,12 @@ namespace world_assembly_detail
|| !ReadHash(item.Get("content_hash"), stratum.contentHash) || !ReadHash(item.Get("content_hash"), stratum.contentHash)
|| !item.Get("features").IsArray() || !item.Get("route").IsArray()) || !item.Get("features").IsArray() || !item.Get("route").IsArray())
{ error = "world assembly stratum is missing a required fixed-scale field"; return false; } { error = "world assembly stratum is missing a required fixed-scale field"; return false; }
const VFJson::Value& recipeOverrideValue = item.Get("recipe_override");
if (!std::holds_alternative<std::nullptr_t>(recipeOverrideValue.Data))
{
if (!ParseRecipe(recipeOverrideValue, stratum.recipeOverride, error)) return false;
stratum.hasRecipeOverride = true;
}
const auto readOptional = [&item](const char* name, std::int64_t& output) const auto readOptional = [&item](const char* name, std::int64_t& output)
{ {
const VFJson::Value& field = item.Get(name); const VFJson::Value& field = item.Get(name);
@@ -1402,6 +1472,13 @@ bool WorldAssemblyManifest::finalize(std::string& error)
std::vector<std::int32_t> strateIds; std::vector<std::int32_t> strateIds;
for (auto& stratum : plan.strates) for (auto& stratum : plan.strates)
{ {
if (stratum.hasRecipeOverride)
{
if (!world_assembly_detail::ValidateRecipe(stratum.recipeOverride, error)) return false;
if (stratum.recipeOverride.seed != recipe.seed)
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
}
if (!world_assembly_detail::ValidateStratum(stratum, error)) return false; if (!world_assembly_detail::ValidateStratum(stratum, error)) return false;
if (recipe.evaluatorVersion >= 4) if (recipe.evaluatorVersion >= 4)
{ {
@@ -1460,16 +1537,25 @@ bool WorldAssemblyManifest::validate(std::string& error) const
const std::uint64_t expectedPlan = plan.contentHash; const std::uint64_t expectedPlan = plan.contentHash;
const std::uint64_t expectedManifest = contentHash; const std::uint64_t expectedManifest = contentHash;
std::vector<std::uint64_t> expectedStrates; std::vector<std::uint64_t> expectedStrates;
for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.contentHash); std::vector<std::uint64_t> expectedStratumRecipes;
for (const auto& stratum : plan.strates)
{
expectedStrates.push_back(stratum.contentHash);
expectedStratumRecipes.push_back(stratum.hasRecipeOverride
? stratum.recipeOverride.contentHash : 0);
}
std::vector<std::uint64_t> expectedIslandManifests; std::vector<std::uint64_t> expectedIslandManifests;
for (const auto& islands : floatingIslands) expectedIslandManifests.push_back(islands.manifest.contentHash); for (const auto& islands : floatingIslands) expectedIslandManifests.push_back(islands.manifest.contentHash);
if (!copy.finalize(error)) return false; if (!copy.finalize(error)) return false;
if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash
|| expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size() || expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size()
|| expectedStratumRecipes.size() != copy.plan.strates.size()
|| expectedIslandManifests.size() != copy.floatingIslands.size()) || expectedIslandManifests.size() != copy.floatingIslands.size())
{ error = "world assembly recipe, plan or manifest content hash does not match its fields"; return false; } { error = "world assembly recipe, plan or manifest content hash does not match its fields"; return false; }
for (std::size_t i = 0; i < expectedStrates.size(); ++i) for (std::size_t i = 0; i < expectedStrates.size(); ++i)
if (expectedStrates[i] != copy.plan.strates[i].contentHash) if (expectedStrates[i] != copy.plan.strates[i].contentHash
|| expectedStratumRecipes[i] != (copy.plan.strates[i].hasRecipeOverride
? copy.plan.strates[i].recipeOverride.contentHash : 0))
{ error = "world assembly stratum content hash does not match its fields"; return false; } { error = "world assembly stratum content hash does not match its fields"; return false; }
for (std::size_t i = 0; i < expectedIslandManifests.size(); ++i) for (std::size_t i = 0; i < expectedIslandManifests.size(); ++i)
if (expectedIslandManifests[i] != copy.floatingIslands[i].manifest.contentHash) if (expectedIslandManifests[i] != copy.floatingIslands[i].manifest.contentHash)
@@ -1485,62 +1571,8 @@ std::string WorldAssemblyManifest::ToJson() const
std::ostringstream out; std::ostringstream out;
out << "{\"schema_version\":" << value.schemaVersion out << "{\"schema_version\":" << value.schemaVersion
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.contentHash) << ",\"content_hash\":\"" << world_assembly_detail::HashText(value.contentHash)
<< "\",\"recipe\":{\"schema_version\":" << value.recipe.schemaVersion << "\",\"recipe\":" << world_assembly_detail::RecipeJson(value.recipe)
<< ",\"evaluator_version\":" << value.recipe.evaluatorVersion << ",\"plan\":{\"schema_version\":" << value.plan.schemaVersion
<< ",\"units_per_voxel\":" << value.recipe.unitsPerVoxel
<< ",\"seed\":" << value.recipe.seed
<< ",\"biome_cell_size_q\":" << value.recipe.biomeCellSizeSubvoxels
<< ",\"biome_warp_q\":" << value.recipe.biomeWarpSubvoxels
<< ",\"biome_warp_wavelength_q\":" << value.recipe.biomeWarpWavelengthSubvoxels
<< ",\"biome_border_blend_q\":" << value.recipe.biomeBorderBlendSubvoxels
;
if (value.recipe.evaluatorVersion >= 2)
out << ",\"climate_relief_wavelength_q\":" << value.recipe.biomeClimateReliefWavelengthSubvoxels
<< ",\"climate_moisture_wavelength_q\":" << value.recipe.biomeClimateMoistureWavelengthSubvoxels
<< ",\"climate_relief_contrast_q16\":" << value.recipe.biomeClimateReliefContrastQ16;
out
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.recipe.contentHash)
<< "\",\"biomes\":[";
for (std::size_t i = 0; i < value.recipe.biomes.size(); ++i)
{
const auto& biome = value.recipe.biomes[i];
if (i) out << ',';
out << "{\"id\":" << biome.stableId << ",\"base_height_q\":" << biome.baseHeightSubvoxels
<< ",\"continent_amplitude_q\":" << biome.continentAmplitudeSubvoxels
<< ",\"continent_wavelength_q\":" << biome.continentWavelengthSubvoxels
<< ",\"mountain_amplitude_q\":" << biome.mountainAmplitudeSubvoxels
<< ",\"mountain_wavelength_q\":" << biome.mountainWavelengthSubvoxels
<< ",\"detail_amplitude_q\":" << biome.detailAmplitudeSubvoxels
<< ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels;
if (value.recipe.evaluatorVersion >= 2)
out << ",\"relief_min_q16\":" << biome.reliefMinimumQ16
<< ",\"relief_max_q16\":" << biome.reliefMaximumQ16
<< ",\"moisture_min_q16\":" << biome.moistureMinimumQ16
<< ",\"moisture_max_q16\":" << biome.moistureMaximumQ16
<< ",\"relief_strength_q16\":" << biome.reliefStrengthQ16
<< ",\"relief_contrast_q16\":" << biome.reliefContrastQ16
<< ",\"relief_wavelength_q\":" << biome.reliefWavelengthSubvoxels
<< ",\"height_warp_strength_q\":" << biome.heightWarpStrengthSubvoxels
<< ",\"height_warp_wavelength_q\":" << biome.heightWarpWavelengthSubvoxels
<< ",\"beach_width_q\":" << biome.beachWidthSubvoxels
<< ",\"cliff_strength_q16\":" << biome.cliffStrengthQ16
<< ",\"cliff_slope_threshold_q16\":" << biome.cliffSlopeThresholdQ16
<< ",\"cliff_sharpness_q16\":" << biome.cliffSharpnessQ16
<< ",\"cliff_sample_distance_q\":" << biome.cliffSampleDistanceSubvoxels
<< ",\"cliff_max_displacement_q\":" << biome.cliffMaximumDisplacementSubvoxels
<< ",\"cap_enabled\":" << (biome.hasCapProfile ? "true" : "false")
<< ",\"cap_height_q\":" << biome.capHeightSubvoxels
<< ",\"cap_warp_strength_q\":" << biome.capWarpStrengthSubvoxels
<< ",\"cap_warp_wavelength_q\":" << biome.capWarpWavelengthSubvoxels
<< ",\"cap_undulation_q\":" << biome.capUndulationSubvoxels
<< ",\"cap_wavelength_q\":" << biome.capWavelengthSubvoxels
<< ",\"cap_detail_amplitude_q\":" << biome.capDetailAmplitudeSubvoxels
<< ",\"cap_detail_wavelength_q\":" << biome.capDetailWavelengthSubvoxels
<< ",\"cap_ridge_amplitude_q\":" << biome.capRidgeAmplitudeSubvoxels
<< ",\"cap_ridge_wavelength_q\":" << biome.capRidgeWavelengthSubvoxels;
out << '}';
}
out << "]},\"plan\":{\"schema_version\":" << value.plan.schemaVersion
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash) << ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash)
<< "\",\"strates\":["; << "\",\"strates\":[";
for (std::size_t i = 0; i < value.plan.strates.size(); ++i) for (std::size_t i = 0; i < value.plan.strates.size(); ++i)
@@ -1563,6 +1595,9 @@ std::string WorldAssemblyManifest::ToJson() const
<< ",\"cap_ridge_wavelength_q\":" << stratum.capRidgeWavelengthSubvoxels << ",\"cap_ridge_wavelength_q\":" << stratum.capRidgeWavelengthSubvoxels
<< ",\"water_surface_q\":" << stratum.waterSurfaceSubvoxels << ",\"water_surface_q\":" << stratum.waterSurfaceSubvoxels
<< ",\"water_enabled\":" << (stratum.waterEnabled ? "true" : "false"); << ",\"water_enabled\":" << (stratum.waterEnabled ? "true" : "false");
if (stratum.hasRecipeOverride)
out << ",\"recipe_override\":"
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
out out
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash) << ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
<< "\",\"features\":["; << "\",\"features\":[";
@@ -1626,7 +1661,7 @@ std::string WorldAssemblyManifest::ToJson() const
Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe, Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe,
const WorldAssemblyStratumPlanV1& plan) noexcept const WorldAssemblyStratumPlanV1& plan) noexcept
: recipe_(&recipe), plan_(&plan) : recipe_(plan.hasRecipeOverride ? &plan.recipeOverride : &recipe), plan_(&plan)
{} {}
bool Heightfield2D::isValid() const noexcept bool Heightfield2D::isValid() const noexcept
@@ -1067,4 +1067,13 @@ New source file this round: `RenderFloatingIslandRound2.py`.
The owner's scale is settled at 12.5 cm per legacy voxel and 25 cm per Core voxel (owner-to-Core length ratio 0.5). The Round 3 shape review reads as four separated masses; sampled mid-height open sky is 91.89% of the XY bounding box. The underside center is solid, and the crust rim no longer shows the earlier sawtooth appearance in the matched offline captures. The owner's scale is settled at 12.5 cm per legacy voxel and 25 cm per Core voxel (owner-to-Core length ratio 0.5). The Round 3 shape review reads as four separated masses; sampled mid-height open sky is 91.89% of the XY bounding box. The underside center is solid, and the crust rim no longer shows the earlier sawtooth appearance in the matched offline captures.
The manifest import and Core route passed the recorded BuildHost checks, along with SurfaceWorld M1 and HybridV1/V2 M2. Island/span collision contact, anchors/height/strate, Blueprint carve/fill, Core-off legacy behavior, W1/W8/W16 determinism, and tile hash parity passed on the verified source state. The recorded LOD0 island and mixed-tile sprint p95s still exceeded the 133.333 ms deadline by about 24 ms and 20 ms. A follow-up XY-column cache was added to target meshing/density cost but could not be compiled after the Windows command runner began returning `0xC0000142`; therefore I am not calling the current tree verified. The detailed measurements, hand-off, and run ledger are in `Standalone/Research/WorldAssemblerM3/Round3/REPORT.md`.
## World assembler M3 round 4 — 2026-10-10
Round 4 moves SurfaceWorld and FloatingIslands planning into the Unreal startup adapter. It uses the resolved seed, actual strate type/height/layout, and authored parameters, then caches the canonical JSON and hash for that world. Imported JSON is now opt-in through **Use Imported World Assembly Override**. This makes the owner’s already-imported but mismatched round-3 payload harmless by default while preserving a deliberate debug override and its clear error/fallback behavior.
The staged owner settings did contain the old manifest, and the plan test retained those bytes with override off: seed 137501 produced an identical canonical serialization and startup hash `15c54ba04b430e42`. SurfaceWorld passed height/water/atmosphere, biome, worker-hash, and Blueprint carve/fill checks. The owner-height FloatingIslands copy passed the route and W1/W8/W16 checks, island/span anchor agreement, cooked capsule contacts, Blueprint edits, and legacy switch-off. The exact Round 3 standalone probe also passed with its old recipe/plan/manifest and worker hashes unchanged. Its native 32K-sample timing measured 38.91 ms after the XY-column cache versus the recorded 46.30 ms; the owner-height LOD0 island p95 was 91.429 ms versus the 133.333 ms sprint deadline.
One remaining fixture caveat is recorded in [`WorldAssemblerM3/Round4/REPORT.md`](../../WorldAssemblerM3/Round4/REPORT.md): the staged saved `DA_Settings` points to `DA_Strate4`, while the owner’s read-only Play log resolves the active single SurfaceWorld from `DA_Strate1`. The BuildHost commandlet therefore reconstructed that logged one-slot layout as a transient copy at the real `[0,-63]` bounds. No Season asset was assigned in the staged settings, so the season-seed path was not tested. The report includes this distinction, the full pass/fail ledger, crash check, and editor hand-off. No owner project/editor was touched, no `.uasset` was edited, and no commit, push, or stash was made.
No new source file was added. The post-change UHT/BuildHost compile and Core link audit passed at four actions; the Core-only Standalone rebuild/audit passed. No `0xC0000142` retry or new UECC folder occurred.
@@ -0,0 +1,79 @@
# World Assembler M3 Round 4 — Unreal plans from the owner’s settings
**Date:** 2026-10-10
**Result:** automatic startup plans and the owner-height SurfaceWorld/FloatingIslands BuildHost paths pass. The old imported JSON no longer blocks the default path.
## Planning and multiplayer choice
`AVoxelWorld` now asks the adapter to build the assembly at startup from the already-resolved `UVoxelStrateManager` layout, each slot’s generator type and height bounds, its authored generation parameters, and the effective world seed. The effective seed is the assigned season’s seed when `Season` is set; otherwise it is `UVoxelSettings::Seed`. SurfaceWorld and FloatingIslands slots receive native plans. The adapter serializes the canonical manifest, hashes it, and the world caches that JSON and digest for the lifetime of the initialized world. It is also included in Core world/season content identity. It is not regenerated per tile.
This is a deterministic-startup artifact: clients with the same season or settings seed, resolved layout, definitions, and parameters produce the same canonical hash. W1/W8/W16 checks pass. The hash is exposed through the world content-hash API for a join-time comparison. This round did not add or verify a network handshake that distributes the server’s artifact or rejects a peer with a different hash, so multiplayer mismatch enforcement remains unproven.
Imported JSON is now an explicit override. `Use Imported World Assembly Override` defaults off; the previous imported bytes can remain on an old settings asset without suppressing automatic planning. The settings import action turns the checkbox on. With the checkbox on, a stale/mismatched manifest remains an error and follows the visible-notice/legacy-fallback path. The default owner workflow needs no import.
Strata types without a native assembly plan retain the existing Core behavior. Core still uses its current generic archetype interpretation (for example, TunnelNetwork’s approximate primitive/passage representation); unsupported or approximate strata are not silently rerouted to the legacy generator. The SurfaceWorld/FloatingIslands plan addition does not claim exact native plans for those types.
If Core cannot serialize, validate, or initialize, the runtime logs the reason, displays the reason in red for 20 seconds through `AddOnScreenDebugMessage`, disables Core for that world, clears the failed Core state, and initializes the legacy generator. The mismatch BuildHost test ran with NullRHI: it proves the message call/log marker and legacy fallback, not visible pixels in a PIE viewport.
## Owner assets staged in BuildHost
All source assets were copied into the isolated BuildHost project and hash-checked by the build helper; no owner asset was edited. Recorded SHA-256 values:
| Asset | SHA-256 |
|---|---|
| `DA_Settings` | `8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454` |
| `DA_Strate1` | `76063D691D5CE26D2014B587E1972A6724787BE1204E4652CA863BB8D5001BD0` |
| `DA_Strate2` | `02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2` |
| `DA_Strate3` | `F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C` |
| `DA_Strate4` | `7C0DE7D06876035AC7BC4F117B9166AFD77402021843BA4900B470F4B0D0185B` |
The owner’s read-only Play log resolves one SurfaceWorld slot. The staged saved `DA_Settings` copy, however, references `DA_Strate4` and has no season asset assigned; the staged `DA_Strate1` is a 64-chunk SurfaceWorld. To reproduce the layout reported by the active editor log without changing assets, the commandlet used a transient copy of `DA_Settings` with `DA_Strate1` in that one slot, seed 0, gap 0, and the original bounds `[0,-63]` (64 chunks). Runtime logs number that slot as zero-based strate 0. Thus the tests use the owner’s real definition and real height, but the staged saved settings bytes alone do not describe the active editor layout. No `UVoxelSeasonAsset` was assigned in those staged settings, so the season-seed path was not exercised; the code selects it when one is assigned.
The staged settings contain the previous manifest JSON. The post-change plan test deliberately restored those bytes while leaving the new override checkbox off; automatic planning succeeded (`stored_manifest_ignored=1`). The recurring missing `/Game/VoxelForge/Blocks/NewDataAsset` reference is logged by BuildHost but did not prevent the world or checks from resolving.
## BuildHost acceptance matrix
| Case | Result and evidence | Would fail if… |
|---|---|---|
| (a) Core on, SurfaceWorld unchanged | PASS. Startup generated 1 SurfaceWorld / 0 island plans, hash `2bb89236290414d5`. Biome field: ID 4099, three biomes sampled. W1/W8/W16 density/mesh/payload/topology `0cd2a1a4152ee4aa / 17775c835711e5a3 / a27224958fc27d7b / 3821fea90b134ce4`. Height agreement delta 0 cm; water `-36575.977 cm`; atmosphere bounds `[-50400,800] cm`; BP carve and fill pass. | No startup-generated SurfaceWorld plan/hash, any worker hash differs, height/content/water/atmosphere or BP edit marker fails, or the actor test does not complete. |
| (b) SurfaceWorld definition changed to FloatingIslands | PASS on a transient copy at owner bounds `[0,-63]`. Plan hash `9c372e7bfb5b8310`, recipe `5e0370d9f95f1c15`, plan `dbbda4932378047a`; four islands, three connectors, 28 route segments, minimum route clearance 119 subvoxels, fixed water. Top and connector-span content anchors agree with terrain within 0.4 cm; both capsule contacts hit cooked collision. BP carve/fill and Core-to-legacy switch-off pass. W1/W8/W16 aggregates: density/mesh/payload/topology `4dcfb37585d7d986 / 725a18374c5623de / fb345d8cb319d18b / 76f2846be8efb5f4`. | No plan at `[0,-63]`, route audit fails, any W hash differs, either content/terrain anchor or capsule contact fails, Blueprint edits fail, or Core-off changes legacy scale/density. |
| (c) Nonzero seed | PASS. Seed 137501 produced hash `15c54ba04b430e42`; two canonical serializations matched. The stored prior JSON was present and ignored with override off. | The generated JSON/hash differs between identical resolutions, seed is not reflected in the plan, or an inactive stored manifest prevents planning. |
| (d) Worker stability | PASS. SurfaceWorld and FloatingIslands actor tests both report W1/W8/W16 stable; the exact Round 3 standalone fixture also reports matching W1/W8/W16. | Any deterministic hash differs across worker counts. |
| (e) Core off | PASS. Legacy generator remained enabled; voxel scale `12.5 cm`; three density samples unchanged. | Core-off toggling changes the sampled legacy densities, disables the legacy generator, or changes the legacy scale. |
| (f) Mismatched old manifest override | PASS when the override is explicitly enabled. It reports the exact `0 SurfaceWorld/1 island` versus `1 SurfaceWorld/0 island` mismatch, prints the 20-second on-screen notice, disables Core, initializes the legacy generator, and returns finite density `70.0000`. The commandlet logs the expected Core errors and exits process status 1 although its test result is 0; the wrapper accepts only this combination with the fallback marker, complete marker, and no new crash. | A mismatch silently yields no world, no visible-notice call, Core stays enabled, legacy is not initialized, density is non-finite, or the commandlet lacks the explicit pass/result markers. |
## XY-column cache verification and timing
Rebuilt the canonical Core library, compiled the Round 3 audit against its same probe, and wrote new results under `Round4/results/xy_cache_audit` so the Round 3 record stayed unchanged. The Round 3 recipe/plan/manifest hashes remain `ef1340d32b072ba3 / 61fd58f65c5fe157 / 54160b4d25d0279f`; W1/W8/W16 remain `5764b702482740e8`. Stable island/connector IDs, open-sky fraction (91.8919%), bounds (515 boxes / 13,851 samples / zero missed), thickness (minimum 0.75), and route results (28 segments, clearance 99) match the Round 3 audit. The XY-column cache is compiled and verified; no revert was needed.
On the same 32,768-query audit, native elapsed time was 38.91 ms versus the recorded 46.30 ms before the cache; legacy was 65.64 ms versus 64.33 ms. These are one-run elapsed measurements, not a controlled benchmark series. The owner-height actor’s LOD0 island p95 was 91.429 ms and mixed p95 90.652 ms against the 133.333 ms sprint deadline; legacy was 25.635/25.803 ms. Round 3’s actor measurement was 153–158 ms Core and 42–44 ms legacy, so the current owner-height measurement is under the target. A deeper speed change is not needed for this round.
## Failed attempts and corrections
- The first owner SurfaceWorld commandlet completed its world assertions, but the PowerShell wrapper rejected the actual 2,850-tile layout because it expected exactly 2,553 tiles. The assertion now checks for a positive tile count; the owner SurfaceWorld run then passed.
- The first generated-island actor attempt used a hard-coded slot 1 although the owner-height layout resolves to zero-based slot 0. The harness now uses the resolved slot index.
- The next island attempt passed plan, route, height anchors, and collision, but timed out after 90 seconds waiting for an injected transient tree decoration. That random placement was not needed for the requested anchor proof; the test now checks native top/span content anchors against terrain and their collision contacts directly. No production decoration behavior was changed by this correction.
- The first mismatch-fallback actor attempt had no GameMode/BeginPlay in its transient world. The fixture now starts play explicitly. A subsequent expected `LogTemp Error` made Unreal return process status 1 despite commandlet result 0; the wrapper was narrowed to accept only the exact tested fallback/pass/no-crash markers. The final mismatch run passed with that condition.
- Initial compile iterations caught and corrected an `IsSet`/`has_value` mismatch, a missing `CoreError` local, and a link to an unavailable `GetEffectiveWorldSeed` symbol. Final UBT passed after the override checkbox and test changes.
- The first standalone-audit wrapper invocation referenced a memory-probe type that existed only in a previous PowerShell session. It printed that preflight error but proceeded to launch; an immediate independent system-memory query showed 19.18 GiB available. The audit passed in 1.66 seconds with a 154 MiB peak. No audit retry was made.
No run returned `0xC0000142`; no retry for that status was needed. All commandlets stayed below 8 GiB private memory and under 15 minutes (SurfaceWorld about 12m, final islands 3m28s, plan/fallback under 30s). No new `UECC-*` folder appeared; the sole BuildHost crash folder is dated 2026-09-17. Commandlets ran sequentially at BelowNormal priority with the launcher's 4 GiB free-memory gate. The owner’s editor/project was not opened, closed, or built. No `.uasset`, junction, symlink, commit, push, or stash was made.
## Files changed and compile impact
No new C++ source file was added. Modified sources:
- Runtime: `Source/VoxelForge/Private/VoxelContentManager.cpp`, `VoxelForgeCoreAdapter.cpp`, `VoxelSettings.cpp`, `VoxelWorld.cpp`; headers `Source/VoxelForge/Public/VoxelSettings.h`, `VoxelWorld.h`.
- Core API/implementation: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/WorldAssemblerInternal.h`.
- BuildHost harness: `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`, `Tools/BuildVoxelForgeCoreHost.ps1`, `Tools/TestVoxelForgeCoreWorldHost.ps1`.
Likely compile spots are UHT/reflected `VoxelSettings.h` and its generated code, runtime settings/adapter/world/content translation units, the Core public API and assembly internals, and the editor commandlet. The BuildHost UBT/UHT compile passed at max 4 actions and BelowNormal priority; Core link audit passed (18 shims; 94,368 symbol lines checked against 415 Unreal-style type names). The Standalone Core rebuild and its symbol audit also passed. Existing compiler output included the known `VoxelStrateManager.cpp` double-to-float warnings and a missing `vswhere.exe` notice; neither failed the build.
## Owner hand-off
1. Select the `AVoxelWorld` actor in the level. In Details, under **Voxel World | Core**, check **Use VoxelForge Core**.
2. In the current settings asset, open **Voxel | Strates | Fixed Strates** and duplicate the strate definition assigned to the desired slot (for the tested slot, the asset is `DA_Strate1`). In that duplicate, set **Strate | Generation | Generator Type** to **FloatingIslands**, then assign the duplicate to the same Fixed Strates entry. Keep **Use Imported World Assembly Override** unchecked; no world-plan file is needed. Keep the existing seed and water settings.
3. The staged settings currently have no Season assigned, so Fixed Strates and `Seed` are active. If a Season is assigned later, its reviewed manifest owns the layout and seed instead; edit that season definition for a different seasonal layout.
4. Start at actor-local `(-980, 431.8, -12438.2) cm` (transform that point by the world actor’s transform) and follow the generated route.
For SurfaceWorld, leave that slot’s Generator Type as **SurfaceWorld**. Its plan is generated at startup the same way.
@@ -0,0 +1,30 @@
{
"recipe_hash":"ef1340d32b072ba3",
"plan_hash":"61fd58f65c5fe157",
"manifest_hash":"54160b4d25d0279f",
"unchanged_existing_hashes":{"m1_round1_manifest":"c87ae279b0e26f53","surfaceworld_round4_manifest":"8933693d9560ba9f"},
"geometry_version":2,
"owner_voxel_size_cm":12.5,
"core_voxel_size_cm":25,
"owner_to_core_length_scale":0.5,
"plan_islands":4,
"mid_height_open_sky":{"sampling_step_m":1.0,"z_core_voxels":-964.0390625,"bounds_xy_core_voxels":[-45.58203125,-107.33984375,99.73828125,62.82421875],"open_samples":1462,"total_samples":1591,"open_fraction":0.918918918919},
"plan_connectors":3,
"route_points":29,
"legacy_anchor_owner_voxels":[48,16,-1904.03515625],
"comparison_anchor_core_voxels":[24,8,-952.015625],
"legacy_anchor_plateau_samples":25,
"recipe_parameters":{"seed":0,"primary_island_count":3,"density_q16":32768,"spacing_voxels":56,"radius_voxels":[12,24],"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"contour_warp_voxels":6.30078125,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"roughness_voxels":1,"blend_voxels":3,"bridge_half_width_voxels":2.5,"bridge_thickness_voxels":4},
"stable_island_ids":["e6e461e18b3a9c87","2bc1820baba047f6","adc259cbe2a395e9","7855fb022cc561c9"],
"stable_connector_ids":["eaa5dd310e846520","034023599b466c51","4962fa885545fb26"],
"island_profiles":[{"id":"e6e461e18b3a9c87","ordinal":0,"center_voxels":[-15.19921875,25.2734375,-960.5078125],"radius_voxels":14.08203125,"top_voxels":-957.69140625,"bottom_voxels":-978.81640625,"depth_to_diameter":0.750069348128},{"id":"2bc1820baba047f6","ordinal":1,"center_voxels":[24,-12.2265625,-954.8984375],"radius_voxels":14.42578125,"top_voxels":-952.015625,"bottom_voxels":-973.65625,"depth_to_diameter":0.75006769564},{"id":"adc259cbe2a395e9","ordinal":2,"center_voxels":[63.19921875,26.28515625,-955.57421875],"radius_voxels":20.23828125,"top_voxels":-951.52734375,"bottom_voxels":-981.88671875,"depth_to_diameter":0.750048253233},{"id":"7855fb022cc561c9","ordinal":3,"center_voxels":[-1.19921875,-68.2265625,-950.75390625],"radius_voxels":22.8125,"top_voxels":-946.19140625,"bottom_voxels":-980.41015625,"depth_to_diameter":0.75}],
"bounds":{"passed":true,"negative_control_detected":true,"boxes":515,"samples":13851,"unknown":72,"missed":0,"island_boxes":4,"connector_boxes":3,"void_boxes_proved_air":1},
"thickness":{"passed":true,"islands_tested":4,"islands_below_minimum":0,"minimum_depth_to_diameter_ratio":0.75,"required_depth_to_diameter_ratio":0.7,"minimum_island_id":"7855fb022cc561c9"},
"thin_pad_negative_control":{"detected":true,"failed_islands":4,"minimum_depth_to_diameter_ratio":0.299891686975},
"route":{"passed":true,"segments":28,"centers":253,"capsule_samples":68816,"blocked":0,"support_samples":1344,"unsupported":0,"grade_failures":0,"minimum_clearance_subvoxels":99},
"raised_route_negative_control":{"detected":true,"passed":false,"unsupported":253,"blocked":0},
"determinism":{"W1":"5764b702482740e8","W8":"5764b702482740e8","W16":"5764b702482740e8","plan_repeat":true,"hash_samples":32768},
"timing":{"same_queries":32768,"legacy_ms":65.6427,"native_ms":38.9083,"native_over_legacy":0.592728513605,"sign_mismatches":1473,"sign_mismatch_percent":4.49523925781,"legacy_solid_samples":100,"native_solid_samples":1399,"legacy_only_solid_samples":87,"native_only_solid_samples":1386},
"mesh":[{"name":"cluster_legacy","grid_samples":34686,"triangles":1512,"bytes":72584},{"name":"cluster_native","grid_samples":34686,"triangles":8312,"bytes":398984},{"name":"detail_legacy","grid_samples":207025,"triangles":4124,"bytes":197960},{"name":"detail_native","grid_samples":207025,"triangles":26122,"bytes":1253864}],
"route_points_core_voxels":[[-15.19921875,25.2734375,-953.48046875],[-12.3984375,22.59375,-953.57421875],[-9.59765625,19.91796875,-952.78515625],[-6.80078125,17.23828125,-952.28515625],[-4,14.55859375,-951.765625],[-1.19921875,11.87890625,-951.25],[1.6015625,9.203125,-950.66015625],[4.40234375,6.5234375,-950.12890625],[7.19921875,3.84375,-949.6015625],[10,1.16796875,-948.97265625],[12.80078125,-1.51171875,-948.3203125],[15.6015625,-4.19140625,-947.796875],[18.3984375,-6.87109375,-947.4375],[21.19921875,-9.546875,-947.9296875],[24,-12.2265625,-947.890625],[26.80078125,-9.4765625,-948.04296875],[29.6015625,-6.7265625,-947.046875],[32.3984375,-3.97265625,-947.21875],[35.19921875,-1.22265625,-947.36328125],[38,1.52734375,-947.37890625],[40.80078125,4.27734375,-947.3125],[43.6015625,7.03125,-947.15234375],[46.3984375,9.78125,-947.1953125],[49.19921875,12.53125,-947.16015625],[52,15.28125,-947.03125],[54.80078125,18.03125,-946.94921875],[57.59765625,20.78515625,-947.55078125],[60.3984375,23.53515625,-947.46875],[63.19921875,26.28515625,-947.34765625]]
}
@@ -0,0 +1 @@
{"schema_version":1,"content_hash":"54160b4d25d0279f","recipe":{"schema_version":1,"units_per_voxel":256,"owner_voxel_size_microns":125000,"core_voxel_size_microns":250000,"owner_to_core_scale_numerator":1,"owner_to_core_scale_denominator":2,"seed":0,"origin_x_q":6144,"origin_y_q":2048,"center_top_z_q":-243716,"layout_min_z_q":-264196,"layout_max_z_q":-231428,"primary_island_count":3,"island_density_q16":32768,"spacing_q":14336,"minimum_radius_q":3072,"maximum_radius_q":6144,"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"taper_end_min_q16":29491,"taper_end_max_q16":45875,"geometry_version":2,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"blend_radius_q":768,"contour_warp_q":1613,"roughness_q":256,"boundary_seal_q":512,"bridge_half_width_q":640,"bridge_thickness_q":1024,"route_constraints":{"version":1,"capsule_radius_q":348,"capsule_height_q":1802,"maximum_grade_q16":17560,"sweep_spacing_q":128,"support_probe_radius_q":230,"support_search_depth_q":512,"minimum_clearance_q":128,"minimum_support_probes":3},"content_hash":"ef1340d32b072ba3"},"plan":{"content_hash":"61fd58f65c5fe157","islands":[{"stable_id":"e6e461e18b3a9c87","ordinal":0,"center_q":[-3891,6470,-245890],"radius_q":3605,"top_half_q":721,"top_z_q":-245169,"bottom_z_q":-250577,"taper_end_q16":41663,"bounds_min_q":[-11669,-1308,-253137],"bounds_max_q":[3887,14248,-242609]},{"stable_id":"2bc1820baba047f6","ordinal":1,"center_q":[6144,-3130,-244454],"radius_q":3693,"top_half_q":738,"top_z_q":-243716,"bottom_z_q":-249256,"taper_end_q16":33506,"bounds_min_q":[-1722,-10996,-251816],"bounds_max_q":[14010,4736,-241156]},{"stable_id":"adc259cbe2a395e9","ordinal":2,"center_q":[16179,6729,-244627],"radius_q":5181,"top_half_q":1036,"top_z_q":-243591,"bottom_z_q":-251363,"taper_end_q16":39164,"bounds_min_q":[6825,-2625,-253923],"bounds_max_q":[25533,16083,-241031]},{"stable_id":"7855fb022cc561c9","ordinal":3,"center_q":[-307,-17466,-243393],"radius_q":5840,"top_half_q":1168,"top_z_q":-242225,"bottom_z_q":-250985,"taper_end_q16":33440,"bounds_min_q":[-10320,-27479,-253545],"bounds_max_q":[9706,-7453,-239665]}],"cluster_edges":[{"stable_id":"eaa5dd310e846520","from_island_id":"e6e461e18b3a9c87","to_island_id":"2bc1820baba047f6","start_q":[-2536,5174,-245089],"end_q":[4756,-1803,-243568],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[-4200,-3467,-249185],"bounds_max_q":[6420,6838,-242544],"sharp_tag_ready":true},{"stable_id":"034023599b466c51","from_island_id":"2bc1820baba047f6","to_island_id":"adc259cbe2a395e9","start_q":[7514,-1784,-243592],"end_q":[14257,4841,-243505],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[5850,-3448,-247688],"bounds_max_q":[15921,6505,-242481],"sharp_tag_ready":true},{"stable_id":"4962fa885545fb26","from_island_id":"2bc1820baba047f6","to_island_id":"7855fb022cc561c9","start_q":[5356,-4881,-243607],"end_q":[939,-14697,-242382],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[-725,-16361,-247703],"bounds_max_q":[7020,-3217,-241358],"sharp_tag_ready":true}],"route":[{"feature_id":"2e805a1f1f778cbb","position_q":[-3891,6470,-244091],"radius_q":1024},{"feature_id":"5be3b2b1e01722ea","position_q":[-3174,5784,-244115],"radius_q":1024},{"feature_id":"ab4eda612c1f03b5","position_q":[-2457,5099,-243913],"radius_q":1024},{"feature_id":"ba2fd2b050d0dd8c","position_q":[-1741,4413,-243785],"radius_q":1024},{"feature_id":"fc5829ee129fdc4f","position_q":[-1024,3727,-243652],"radius_q":1024},{"feature_id":"1393bb00329a9d5e","position_q":[-307,3041,-243520],"radius_q":1024},{"feature_id":"1dabc9715c93e2c9","position_q":[410,2356,-243369],"radius_q":1024},{"feature_id":"0ea2008a85e73050","position_q":[1127,1670,-243233],"radius_q":1024},{"feature_id":"754f55a5f1465cf3","position_q":[1843,984,-243098],"radius_q":1024},{"feature_id":"1b6752776b4a7d22","position_q":[2560,299,-242937],"radius_q":1024},{"feature_id":"0ab3b704db76084d","position_q":[3277,-387,-242770],"radius_q":1024},{"feature_id":"bd6980911e0879c4","position_q":[3994,-1073,-242636],"radius_q":1024},{"feature_id":"ad164d7d0c049d87","position_q":[4710,-1759,-242544],"radius_q":1024},{"feature_id":"9661463acbada2d6","position_q":[5427,-2444,-242670],"radius_q":1024},{"feature_id":"280a96547fadb0a1","position_q":[6144,-3130,-242660],"radius_q":1024},{"feature_id":"531455b42fc13108","position_q":[6861,-2426,-242699],"radius_q":1024},{"feature_id":"55d04c20849d7fcb","position_q":[7578,-1722,-242444],"radius_q":1024},{"feature_id":"ac4ebe80860dbdfa","position_q":[8294,-1017,-242488],"radius_q":1024},{"feature_id":"ff942855bf8e3fc5","position_q":[9011,-313,-242525],"radius_q":1024},{"feature_id":"e6b8bba6c6acaf9c","position_q":[9728,391,-242529],"radius_q":1024},{"feature_id":"ae47379bbd4f7fdf","position_q":[10445,1095,-242512],"radius_q":1024},{"feature_id":"98779442384a30ee","position_q":[11162,1800,-242471],"radius_q":1024},{"feature_id":"f4c7757b2ee9fe19","position_q":[11878,2504,-242482],"radius_q":1024},{"feature_id":"3ea33fae0fLine truncated
@@ -0,0 +1 @@
attempt=1 status=passed elapsed_s=1.66 peak_private_MiB=154.4 exit=0
@@ -0,0 +1 @@
{"schema_version":1,"content_hash":"94b563f1ce622def","recipe":{"schema_version":1,"evaluator_version":4,"units_per_voxel":256,"seed":0,"biome_cell_size_q":204800,"biome_warp_q":64000,"biome_warp_wavelength_q":142222,"biome_border_blend_q":30720,"climate_relief_wavelength_q":853333,"climate_moisture_wavelength_q":640000,"climate_relief_contrast_q16":131072,"content_hash":"c37de78b759c0a4c","biomes":[{"id":4097,"base_height_q":-516096,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":0,"relief_max_q16":19661,"moisture_min_q16":0,"moisture_max_q16":19661,"relief_strength_q16":6554,"relief_contrast_q16":104858,"relief_wavelength_q":2560000,"height_warp_strength_q":12288,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4098,"base_height_q":-484639,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":128000,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":52429,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":65536,"cliff_slope_threshold_q16":19661,"cliff_sharpness_q16":327680,"cliff_sample_distance_q":5120,"cliff_max_displacement_q":2570240,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4099,"base_height_q":-516096,"continent_amplitude_q":20480,"continent_wavelength_q":85333,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":6554,"relief_max_q16":26214,"moisture_min_q16":32768,"moisture_max_q16":52429,"relief_strength_q16":0,"relief_contrast_q16":16384,"relief_wavelength_q":170752,"height_warp_strength_q":16640,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4100,"base_height_q":-385024,"continent_amplitude_q":20480,"continent_wavelength_q":256000,"mountain_amplitude_q":20480,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":64881,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800}]},"plan":{"schema_version":1,"content_hash":"89cd31291d2aefa4","strates":[]},"floating_islands":[{"strate_index":1,"top_chunk_z":-16,"bottom_chunk_z":-79,"manifest":{"schema_version":1,"content_hash":"54160b4d25d0279f","recipe":{"schema_version":1,"units_per_voxel":256,"owner_voxel_size_microns":125000,"core_voxel_size_microns":250000,"owner_to_core_scale_numerator":1,"owner_to_core_scale_denominator":2,"seed":0,"origin_x_q":6144,"origin_y_q":2048,"center_top_z_q":-243716,"layout_min_z_q":-264196,"layout_max_z_q":-231428,"primary_island_count":3,"island_density_q16":32768,"spacing_q":14336,"minimum_radius_q":3072,"maximum_radius_q":6144,"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"taper_end_min_q16":29491,"taper_end_max_q16":45875,"geometry_version":2,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"blend_radius_q":768,"contour_warp_q":1613,"roughness_q":256,"boundary_seal_q":512,"bridge_half_width_q":640,"bridge_thickness_q":1024,"route_constraints":{"version":1,"capsule_radius_q":Line truncated
+48 -5
View File
@@ -1,6 +1,6 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateRange(1, 16)][int]$MaxParallelActions = 16, [ValidateRange(1, 4)][int]$MaxParallelActions = 4,
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900 [ValidateRange(60, 900)][int]$TimeoutSeconds = 900
) )
@@ -33,6 +33,42 @@ function Assert-UnderPath([string]$Candidate, [string]$Root) {
} }
} }
if (-not ('VoxelForgeRound4BuildMemoryProbe' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class VoxelForgeRound4BuildMemoryProbe {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MEMORYSTATUSEX {
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
public static ulong AvailablePhysicalBytes() {
var status = new MEMORYSTATUSEX();
status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
if (!GlobalMemoryStatusEx(ref status)) throw new InvalidOperationException("GlobalMemoryStatusEx failed.");
return status.ullAvailPhys;
}
}
'@
}
function Wait-ForLaunchMemory {
while ([VoxelForgeRound4BuildMemoryProbe]::AvailablePhysicalBytes() -lt 4GB) {
Write-Warning 'BuildHost UBT launch is waiting for at least 4 GiB of free physical memory.'
Start-Sleep -Seconds 20
}
}
function Copy-DirectoryContents([string]$Source, [string]$Destination) { function Copy-DirectoryContents([string]$Source, [string]$Destination) {
if (-not (Test-Path -LiteralPath $Source -PathType Container)) { if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
throw "Canonical Core source directory is missing: $Source" throw "Canonical Core source directory is missing: $Source"
@@ -92,11 +128,11 @@ Copy-Item -LiteralPath (Join-Path $PluginRoot 'VoxelForge.uplugin') `
# never resolve, open, copy, or compare against the owner's project Content directory. # never resolve, open, copy, or compare against the owner's project Content directory.
$hostAssetRoot = Join-Path $HostRoot 'Content\VoxelForge' $hostAssetRoot = Join-Path $HostRoot 'Content\VoxelForge'
$expectedAssetHashes = @{ $expectedAssetHashes = @{
'DA_Settings' = '5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7' 'DA_Settings' = '8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454'
'DA_Strate1' = 'CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9' 'DA_Strate1' = '76063D691D5CE26D2014B587E1972A6724787BE1204E4652CA863BB8D5001BD0'
'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2' 'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2'
'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C' 'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C'
'DA_Strate4' = '279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656' 'DA_Strate4' = '7C0DE7D06876035AC7BC4F117B9166AFD77402021843BA4900B470F4B0D0185B'
} }
foreach ($assetName in $expectedAssetHashes.Keys) { foreach ($assetName in $expectedAssetHashes.Keys) {
$stagedAsset = Join-Path $hostAssetRoot "$assetName.uasset" $stagedAsset = Join-Path $hostAssetRoot "$assetName.uasset"
@@ -105,7 +141,7 @@ foreach ($assetName in $expectedAssetHashes.Keys) {
} }
$stagedHash = (Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash $stagedHash = (Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash
if ($stagedHash -ne $expectedAssetHashes[$assetName]) { if ($stagedHash -ne $expectedAssetHashes[$assetName]) {
throw "BuildHost asset copy differs from the recorded round-2 hash: $assetName" throw "BuildHost owner asset copy differs from the recorded round-4 hash: $assetName"
} }
} }
@@ -131,6 +167,13 @@ for ($attempt = 1; $attempt -le 2 -and -not $buildSucceeded; ++$attempt) {
$attemptStdout = if ($attempt -eq 1) { $BuildStdout } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stdout.log' } $attemptStdout = if ($attempt -eq 1) { $BuildStdout } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stdout.log' }
$attemptStderr = if ($attempt -eq 1) { $BuildStderr } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stderr.log' } $attemptStderr = if ($attempt -eq 1) { $BuildStderr } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stderr.log' }
Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; BelowNormal priority." Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; BelowNormal priority."
Wait-ForLaunchMemory
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' })
if ($foreignEditors.Count -gt 0) {
$ids = ($foreignEditors | ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ', '
throw "UnrealEditor is already running; leaving it untouched: $ids"
}
$process = Start-Process -FilePath $dotnet -ArgumentList $argumentList -WorkingDirectory $PluginRoot ` $process = Start-Process -FilePath $dotnet -ArgumentList $argumentList -WorkingDirectory $PluginRoot `
-RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr ` -RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr `
-WindowStyle Hidden -PassThru -WindowStyle Hidden -PassThru
+190 -48
View File
@@ -6,7 +6,12 @@ param(
[switch]$EnableHybridFixturePlacement, [switch]$EnableHybridFixturePlacement,
[switch]$HybridCliffOnly, [switch]$HybridCliffOnly,
[switch]$HybridFixturesOnly, [switch]$HybridFixturesOnly,
[switch]$FloatingIslandsOnly [switch]$FloatingIslandsOnly,
[switch]$CoreFallbackTest,
[switch]$MismatchedManifestFallback,
[switch]$CoreOffTest,
[switch]$CorePlanOnly,
[int]$CoreTestSeed = [int]::MinValue
) )
# Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only. # Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only.
@@ -29,6 +34,25 @@ $ZenData = Join-Path $OutputRoot 'ZenData'
$EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' $EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
$MemoryLimitBytes = 8GB $MemoryLimitBytes = 8GB
$CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes')) $CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'))
$RunName = if ($FloatingIslandsOnly) { 'floating_islands' }
elseif ($CoreFallbackTest -or $MismatchedManifestFallback) { 'mismatched_fallback' }
elseif ($CoreOffTest) { 'core_off' }
elseif ($CorePlanOnly) { if ($CoreTestSeed -ne [int]::MinValue) { "plan_seed_$CoreTestSeed" } else { 'plan_only' } }
else { 'owner_surface' }
if ($MismatchedManifestFallback) { $CoreFallbackTest = $true }
$SelectedModeFlags = @(
[bool]$FloatingIslandsOnly, [bool]$CoreFallbackTest,
[bool]$CoreOffTest, [bool]$CorePlanOnly
)
$SelectedModeCount = @($SelectedModeFlags | Where-Object { $_ }).Count
if ($SelectedModeCount -gt 1) { throw 'Select one Core test mode per BuildHost launch.' }
if ($CorePlanOnly -and $CoreTestSeed -eq [int]::MinValue) {
throw '-CorePlanOnly requires a specific CoreTestSeed.'
}
if ($CoreTestSeed -ne [int]::MinValue -and -not $CorePlanOnly) {
throw '-CoreTestSeed is valid only with -CorePlanOnly.'
}
function Assert-UnderPath([string]$Candidate, [string]$Root) { function Assert-UnderPath([string]$Candidate, [string]$Root) {
$candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/') $candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/')
@@ -39,26 +63,65 @@ function Assert-UnderPath([string]$Candidate, [string]$Root) {
} }
} }
Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost') if (-not ('VoxelForgeRound4MemoryProbe' -as [type])) {
Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved') Add-Type -TypeDefinition @'
if ([string]::IsNullOrWhiteSpace($WorldAssemblyManifestPath)) { using System;
$WorldAssemblyManifestPath = if ($FloatingIslandsOnly) { using System.Runtime.InteropServices;
Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM3\Round3\results\world_assembly_floating_islands.json' public static class VoxelForgeRound4MemoryProbe {
} else { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM1\round4\world_assembly.json' private struct MEMORYSTATUSEX {
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
public static ulong AvailablePhysicalBytes() {
var status = new MEMORYSTATUSEX();
status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
if (!GlobalMemoryStatusEx(ref status)) throw new InvalidOperationException("GlobalMemoryStatusEx failed.");
return status.ullAvailPhys;
} }
} }
$WorldAssemblyManifestPath = [IO.Path]::GetFullPath($WorldAssemblyManifestPath) '@
Assert-UnderPath $WorldAssemblyManifestPath $PluginRoot }
function Wait-ForLaunchMemory {
while ([VoxelForgeRound4MemoryProbe]::AvailablePhysicalBytes() -lt 4GB) {
Write-Warning 'BuildHost launch is waiting for at least 4 GiB of free physical memory.'
Start-Sleep -Seconds 20
}
}
Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost')
Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved')
if ([string]::IsNullOrWhiteSpace($WorldAssemblyManifestPath) -and
($CoreFallbackTest -or $MismatchedManifestFallback)) {
$WorldAssemblyManifestPath = Join-Path $PluginRoot `
'Standalone\Research\WorldAssemblerM3\Round3\results\world_assembly_floating_islands.json'
}
$HasManifestOverride = -not [string]::IsNullOrWhiteSpace($WorldAssemblyManifestPath)
if ($HasManifestOverride) {
$WorldAssemblyManifestPath = [IO.Path]::GetFullPath($WorldAssemblyManifestPath)
Assert-UnderPath $WorldAssemblyManifestPath $PluginRoot
}
foreach ($required in @($HostProject, $EditorCmd)) { foreach ($required in @($HostProject, $EditorCmd)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "Required BuildHost commandlet input is missing: $required" throw "Required BuildHost commandlet input is missing: $required"
} }
} }
if (-not (Test-Path -LiteralPath $WorldAssemblyManifestPath -PathType Leaf)) { if ($HasManifestOverride -and -not (Test-Path -LiteralPath $WorldAssemblyManifestPath -PathType Leaf)) {
throw "Reviewed world assembly manifest is missing: $WorldAssemblyManifestPath" throw "Reviewed world assembly manifest is missing: $WorldAssemblyManifestPath"
} }
if ($MismatchedManifestFallback) { $RunName = 'mismatched_fallback' }
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue | $foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' }) Where-Object { $_.ProcessName -like 'UnrealEditor*' })
if ($foreignEditors.Count -gt 0) { if ($foreignEditors.Count -gt 0) {
@@ -67,11 +130,11 @@ if ($foreignEditors.Count -gt 0) {
} }
$ExpectedAssetHashes = @{ $ExpectedAssetHashes = @{
'DA_Settings' = '5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7' 'DA_Settings' = '8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454'
'DA_Strate1' = 'CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9' 'DA_Strate1' = '76063D691D5CE26D2014B587E1972A6724787BE1204E4652CA863BB8D5001BD0'
'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2' 'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2'
'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C' 'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C'
'DA_Strate4' = '279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656' 'DA_Strate4' = '7C0DE7D06876035AC7BC4F117B9166AFD77402021843BA4900B470F4B0D0185B'
} }
foreach ($assetName in $ExpectedAssetHashes.Keys) { foreach ($assetName in $ExpectedAssetHashes.Keys) {
$stagedAsset = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset" $stagedAsset = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
@@ -79,7 +142,7 @@ foreach ($assetName in $ExpectedAssetHashes.Keys) {
throw "Verified BuildHost asset copy is missing: $assetName" throw "Verified BuildHost asset copy is missing: $assetName"
} }
if ((Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash -ne $ExpectedAssetHashes[$assetName]) { if ((Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash -ne $ExpectedAssetHashes[$assetName]) {
throw "BuildHost asset copy differs from the recorded round-2 hash: $assetName" throw "BuildHost owner asset copy differs from the recorded round-4 hash: $assetName"
} }
} }
@@ -87,16 +150,14 @@ foreach ($directory in @($OutputDirectory, $InputDirectory, $LogDirectory, $Loca
$UserDirectory, $DerivedDataCache, $ZenData)) { $UserDirectory, $DerivedDataCache, $ZenData)) {
[IO.Directory]::CreateDirectory($directory) | Out-Null [IO.Directory]::CreateDirectory($directory) | Out-Null
} }
Copy-Item -LiteralPath $WorldAssemblyManifestPath -Destination $StagedWorldAssembly -Force if ($HasManifestOverride) {
$ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom-Json Copy-Item -LiteralPath $WorldAssemblyManifestPath -Destination $StagedWorldAssembly -Force
if ($FloatingIslandsOnly) { $ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom-Json
if ($ManifestData.content_hash -ne '94b563f1ce622def' -or } else {
@($ManifestData.floating_islands).Count -ne 1 -or $ManifestData = $null
@($ManifestData.plan.strates).Count -ne 0) { }
throw "BuildHost world assembly input is not the reviewed round-3 island manifest: $($ManifestData.content_hash)" if ($MismatchedManifestFallback -and ($null -eq $ManifestData -or $ManifestData.content_hash -ne '94b563f1ce622def' -or @($ManifestData.floating_islands).Count -ne 1 -or @($ManifestData.plan.strates).Count -ne 0)) {
} throw "Fallback regression input is not the old round-3 island manifest: $($ManifestData.content_hash)"
} elseif ($ManifestData.content_hash -ne '8933693d9560ba9f') {
throw "BuildHost world assembly input is not the reviewed round-4 manifest: $($ManifestData.content_hash)"
} }
if ($EnableHybridFixturePlacement -and $CoreMesherRecipe -eq 'MarchingCubesV1') { if ($EnableHybridFixturePlacement -and $CoreMesherRecipe -eq 'MarchingCubesV1') {
throw 'Hybrid fixture placement requires a WorldAssemblerHybrid recipe.' throw 'Hybrid fixture placement requires a WorldAssemblerHybrid recipe.'
@@ -113,9 +174,8 @@ if ($HybridFixturesOnly -and $CoreMesherRecipe -eq 'MarchingCubesV1') {
if ($HybridFixturesOnly -and ($HybridCliffOnly -or $EnableHybridFixturePlacement)) { if ($HybridFixturesOnly -and ($HybridCliffOnly -or $EnableHybridFixturePlacement)) {
throw '-HybridFixturesOnly cannot be combined with the other focused hybrid modes.' throw '-HybridFixturesOnly cannot be combined with the other focused hybrid modes.'
} }
if ($FloatingIslandsOnly -and ($CoreMesherRecipe -ne 'MarchingCubesV1' -or if (($FloatingIslandsOnly -or $CoreFallbackTest -or $CoreOffTest -or $CorePlanOnly) -and ($CoreMesherRecipe -ne 'MarchingCubesV1' -or $EnableHybridFixturePlacement -or $HybridCliffOnly -or $HybridFixturesOnly)) {
$EnableHybridFixturePlacement -or $HybridCliffOnly -or $HybridFixturesOnly)) { throw 'Core world integration modes use Marching Cubes and cannot be combined with hybrid modes.'
throw '-FloatingIslandsOnly uses the reviewed MC island manifest and cannot be combined with hybrid modes.'
} }
$CrashBefore = @{} $CrashBefore = @{}
@@ -150,6 +210,9 @@ $HybridFixturePlacementFlag = if ($EnableHybridFixturePlacement) { 1 } else { 0
$HybridCliffOnlyFlag = if ($HybridCliffOnly) { 1 } else { 0 } $HybridCliffOnlyFlag = if ($HybridCliffOnly) { 1 } else { 0 }
$HybridFixturesOnlyFlag = if ($HybridFixturesOnly) { 1 } else { 0 } $HybridFixturesOnlyFlag = if ($HybridFixturesOnly) { 1 } else { 0 }
$FloatingIslandsOnlyFlag = if ($FloatingIslandsOnly) { 1 } else { 0 } $FloatingIslandsOnlyFlag = if ($FloatingIslandsOnly) { 1 } else { 0 }
$CoreFallbackTestFlag = if ($CoreFallbackTest) { 1 } else { 0 }
$CoreOffTestFlag = if ($CoreOffTest) { 1 } else { 0 }
$CorePlanOnlyFlag = if ($CorePlanOnly) { 1 } else { 0 }
$BaseArguments = @( $BaseArguments = @(
('-project="{0}"' -f $HostProject), ('-project="{0}"' -f $HostProject),
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound', '-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound',
@@ -161,17 +224,27 @@ $BaseArguments = @(
'-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0', '-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0',
'-modes=export', '-gameconfig=1', '-coreworldtest=1', '-modes=export', '-gameconfig=1', '-coreworldtest=1',
("-coreislandtest=$FloatingIslandsOnlyFlag"), ("-coreislandtest=$FloatingIslandsOnlyFlag"),
("-corefallbacktest=$CoreFallbackTestFlag"),
("-coreofftest=$CoreOffTestFlag"),
("-coreplantest=$CorePlanOnlyFlag"),
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings', '-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
('-coreworldassembly="{0}"' -f $StagedWorldAssembly),
("-corehybridmesher=$HybridMesherFlag"), ("-corehybridmesher=$HybridMesherFlag"),
("-corehybridfixtureplacement=$HybridFixturePlacementFlag"), ("-corehybridfixtureplacement=$HybridFixturePlacementFlag"),
("-corehybridcliffonly=$HybridCliffOnlyFlag"), ("-corehybridcliffonly=$HybridCliffOnlyFlag"),
("-corehybridfixturesonly=$HybridFixturesOnlyFlag"), ("-corehybridfixturesonly=$HybridFixturesOnlyFlag"),
('-out="{0}"' -f $OutputDirectory) ('-out="{0}"' -f $OutputDirectory)
) )
if ($FloatingIslandsOnly) { if ($HasManifestOverride) {
# Keep this stationary route fixture out of the movement gate; island/span cooked contacts are probed directly. $BaseArguments += ('-coreworldassembly="{0}"' -f $StagedWorldAssembly)
# Garder ce fixture immobile hors du gate de mouvement ; les contacts cuisines ile/pont sont verifies directement. }
if ($CoreTestSeed -ne [int]::MinValue) {
$BaseArguments += "-coretestseed=$CoreTestSeed"
}
if ($FloatingIslandsOnly -or (-not $CoreFallbackTest -and -not $CoreOffTest -and -not $CorePlanOnly)) {
# This commandlet keeps the travel pawn stationary; surface's initial collision cooks are still audited,
# and FloatingIslandsOnly separately probes cooked island/span capsule contacts.
# Le pawn de voyage du commandlet reste immobile ; les collisions de surface initiales sont auditees,
# et FloatingIslandsOnly sonde separement les contacts capsules ile/pont cuisines.
$BaseArguments += '-voxel.CollisionGate=0' $BaseArguments += '-voxel.CollisionGate=0'
} }
@@ -181,19 +254,26 @@ try {
$Succeeded = $false $Succeeded = $false
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) { for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
if ($Attempt -eq 1) { if ($Attempt -eq 1) {
$LogFileName = 'CoreWorldValidation.log' $LogFileName = "CoreWorldValidation-$RunName.log"
$StdoutFileName = 'CoreWorldValidation.stdout.log' $StdoutFileName = "CoreWorldValidation-$RunName.stdout.log"
$StderrFileName = 'CoreWorldValidation.stderr.log' $StderrFileName = "CoreWorldValidation-$RunName.stderr.log"
} else { } else {
$LogFileName = 'CoreWorldValidation.retry.log' $LogFileName = "CoreWorldValidation-$RunName.retry.log"
$StdoutFileName = 'CoreWorldValidation.retry.stdout.log' $StdoutFileName = "CoreWorldValidation-$RunName.retry.stdout.log"
$StderrFileName = 'CoreWorldValidation.retry.stderr.log' $StderrFileName = "CoreWorldValidation-$RunName.retry.stderr.log"
} }
$LogPath = Join-Path $LogDirectory $LogFileName $LogPath = Join-Path $LogDirectory $LogFileName
$StdoutPath = Join-Path $LogDirectory $StdoutFileName $StdoutPath = Join-Path $LogDirectory $StdoutFileName
$StderrPath = Join-Path $LogDirectory $StderrFileName $StderrPath = Join-Path $LogDirectory $StderrFileName
$Arguments = @($BaseArguments) + ('-abslog="{0}"' -f $LogPath) $Arguments = @($BaseArguments) + ('-abslog="{0}"' -f $LogPath)
Write-Host "BuildHost Core actor commandlet attempt $Attempt of 2; BelowNormal priority." Wait-ForLaunchMemory
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' })
if ($foreignEditors.Count -gt 0) {
$ids = ($foreignEditors | ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ', '
throw "UnrealEditor is already running; leaving it untouched: $ids"
}
Write-Host "BuildHost Core actor commandlet attempt $Attempt of 2; BelowNormal priority; run=$RunName."
$Process = Start-Process -FilePath $EditorCmd -ArgumentList $Arguments -WorkingDirectory $HostRoot ` $Process = Start-Process -FilePath $EditorCmd -ArgumentList $Arguments -WorkingDirectory $HostRoot `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath ` -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath `
-WindowStyle Hidden -PassThru -WindowStyle Hidden -PassThru
@@ -228,6 +308,17 @@ try {
$ExitCode = [int]$Matches[1] $ExitCode = [int]$Matches[1]
} }
} }
if ($CoreFallbackTest -and $ExitCode -eq 1 -and (Test-Path -LiteralPath $LogPath -PathType Leaf)) {
$CompletedLog = Get-Content -LiteralPath $LogPath -Raw
$ExpectedFallbackErrors = $CompletedLog -match '\[VoxelForgeCoreFallbackTest\] fallback=PASS core_disabled=1 legacy_generator=1' -and
$CompletedLog -match '\[VoxelForgeCoreRuntime\] fallback_notice=on_screen seconds=20' -and
$CompletedLog -match '\[VoxelForgeCoreWorldTest\] pass complete=1' -and
$CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 0\)'
if ($ExpectedFallbackErrors -and @(Get-NewCrashFolders).Count -eq 0) {
Write-Warning 'Accepting commandlet status 1 because the expected Core Error logs accompanied explicit fallback-pass/result-0 markers and no crash.'
$ExitCode = 0
}
}
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) {
Write-Warning 'BuildHost Core actor commandlet exited with 0xC0000142; retrying once.' Write-Warning 'BuildHost Core actor commandlet exited with 0xC0000142; retrying once.'
continue continue
@@ -248,14 +339,61 @@ try {
} }
} }
$FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.log' $FinalLog = Join-Path $LogDirectory "CoreWorldValidation-$RunName.log"
if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) { if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) {
$FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log' $FinalLog = Join-Path $LogDirectory "CoreWorldValidation-$RunName.retry.log"
} }
$Text = Get-Content -LiteralPath $FinalLog -Raw $Text = Get-Content -LiteralPath $FinalLog -Raw
if ($CoreFallbackTest) {
foreach ($Marker in @(
'[VoxelForgeCoreFallbackTest] fallback=PASS core_disabled=1 legacy_generator=1',
'[VoxelForgeCoreRuntime] fallback_notice=on_screen seconds=20',
'Core initialization failed; falling back to the legacy generator.'
)) {
if ($Text.IndexOf($Marker, [StringComparison]::Ordinal) -lt 0) {
throw "BuildHost fallback regression log is missing '$Marker'. See $FinalLog."
}
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during the fallback regression: $($NewCrashes -join '; ')"
}
Write-Host 'VOXELFORGE_CORE_MISMATCHED_MANIFEST_FALLBACK_PASS on_screen=1 legacy=1'
Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
return
}
if ($CoreOffTest) {
if ($Text -notmatch '\[VoxelForgeCoreOffTest\] legacy_unchanged=PASS core_disabled=1 voxel_cm=12\.5 density_samples=3' -or
$Text -notmatch '\[VoxelForgeCoreWorldTest\] pass complete=1') {
throw "BuildHost Core-off run did not prove unchanged owner legacy density and scale. See $FinalLog."
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during the Core-off test: $($NewCrashes -join '; ')"
}
Write-Host 'VOXELFORGE_CORE_OFF_LEGACY_PASS voxel_cm=12.5 density_samples=3'
Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
return
}
if ($CorePlanOnly) {
if ($Text -notmatch ('\[VoxelForgeCorePlanTest\] pass source=locked_owner_layout_seed_override seed=' +
[regex]::Escape([string]$CoreTestSeed) + ' .* deterministic_json=1')) {
throw "BuildHost did not generate a deterministic owner-layout plan for seed $CoreTestSeed. See $FinalLog."
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during the nonzero-seed plan test: $($NewCrashes -join '; ')"
}
Write-Host "VOXELFORGE_CORE_NONZERO_SEED_PLAN_PASS seed=$CoreTestSeed"
Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
return
}
if ($FloatingIslandsOnly) { if ($FloatingIslandsOnly) {
foreach ($Marker in @( foreach ($Marker in @(
'[VoxelForgeCoreIslandTest] manifest_import=PASS', '[VoxelForgeCoreIslandTest] plan_generated=PASS',
'[VoxelForgeCoreIslandTest] standalone_reference=PASS', '[VoxelForgeCoreIslandTest] standalone_reference=PASS',
'[VoxelForgeCoreIslandTest] worker_hash_matrix=1/8/16 stable=1', '[VoxelForgeCoreIslandTest] worker_hash_matrix=1/8/16 stable=1',
'[VoxelForgeCoreIslandTest] pass phase=initial', '[VoxelForgeCoreIslandTest] pass phase=initial',
@@ -286,7 +424,7 @@ if ($FloatingIslandsOnly) {
if ($NewCrashes.Count -gt 0) { if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during the island actor test: $($NewCrashes -join '; ')" throw "New UECC crash folders appeared during the island actor test: $($NewCrashes -join '; ')"
} }
Write-Host "VOXELFORGE_CORE_FLOATING_ISLANDS_PASS manifest=$($ManifestData.content_hash) hashes=standalone water_fixed=1 lod=0,1 workers=1,8,16" Write-Host "VOXELFORGE_CORE_FLOATING_ISLANDS_PASS hashes=standalone water_fixed=1 lod=0,1 workers=1,8,16"
Write-Host "Core actor log: $FinalLog" Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes" Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
return return
@@ -345,7 +483,7 @@ foreach ($Phase in @('initial', 'anchor', 'edit', 'surface_travel', 'surface_str
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') { if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') {
throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog." throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog."
} }
$ExpectedRuntimeAssemblyHash = $ManifestData.content_hash $ExpectedRuntimeAssemblyHash = ''
if ($EnableHybridFixturePlacement) { if ($EnableHybridFixturePlacement) {
$FixtureHashMatch = [regex]::Match($Text, $FixtureHashMatch = [regex]::Match($Text,
'\[VoxelForgeCoreWorldTest\] hybrid_fixture_placement=1 boxes=9 contacts=5 manifest_hash=([0-9a-f]{16})') '\[VoxelForgeCoreWorldTest\] hybrid_fixture_placement=1 boxes=9 contacts=5 manifest_hash=([0-9a-f]{16})')
@@ -354,10 +492,14 @@ if ($EnableHybridFixturePlacement) {
} }
$ExpectedRuntimeAssemblyHash = $FixtureHashMatch.Groups[1].Value $ExpectedRuntimeAssemblyHash = $FixtureHashMatch.Groups[1].Value
} }
$AssemblyMarker = '\[VoxelForgeCoreWorldTest\] world_assembly_hash=' + if ($EnableHybridFixturePlacement) {
[regex]::Escape($ExpectedRuntimeAssemblyHash) + ' plans=1 native=1' $AssemblyMarker = '\[VoxelForgeCoreWorldTest\] world_assembly_source=settings_override world_assembly_hash=' +
[regex]::Escape($ExpectedRuntimeAssemblyHash) + ' surface_strates=1 island_strates=0 native=1'
} else {
$AssemblyMarker = '\[VoxelForgeCoreWorldTest\] world_assembly_source=generated_at_startup world_assembly_hash=[0-9a-f]{16} surface_strates=1 island_strates=0 native=1'
}
if ($Text -notmatch $AssemblyMarker) { if ($Text -notmatch $AssemblyMarker) {
throw "BuildHost actor log does not prove the expected native SurfaceWorld plan was attached. See $FinalLog." throw "BuildHost actor log does not prove a plan for the unchanged owner SurfaceWorld settings. See $FinalLog."
} }
$RecipeMarker = '\[VoxelForgeCoreWorldTest\] world_initialized=1 recipe=' + $RecipeMarker = '\[VoxelForgeCoreWorldTest\] world_initialized=1 recipe=' +
[regex]::Escape($CoreMesherRecipe) [regex]::Escape($CoreMesherRecipe)
@@ -396,7 +538,7 @@ if ($CoreMesherRecipe -eq 'WorldAssemblerHybridV2' -and $EnableHybridFixturePlac
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] native_biome_field=1 .*sampled_biomes=[2-9]') { if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] native_biome_field=1 .*sampled_biomes=[2-9]') {
throw "BuildHost Core actor log does not prove native SurfaceWorld biome assignment. See $FinalLog." throw "BuildHost Core actor log does not prove native SurfaceWorld biome assignment. See $FinalLog."
} }
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] matched_surface_tile_timing=1 .*same_keys=1 tiles=2553') { if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] matched_surface_tile_timing=1 .*same_keys=1 tiles=[1-9]\d* hashes=on') {
throw "BuildHost Core actor log is missing the matched native/legacy-bridge surface timing. See $FinalLog." throw "BuildHost Core actor log is missing the matched native/legacy-bridge surface timing. See $FinalLog."
} }
foreach ($Marker in @( foreach ($Marker in @(