diff --git a/Source/VoxelForge/Private/VoxelContentManager.cpp b/Source/VoxelForge/Private/VoxelContentManager.cpp index 8d2600f..5fb70f1 100644 --- a/Source/VoxelForge/Private/VoxelContentManager.cpp +++ b/Source/VoxelForge/Private/VoxelContentManager.cpp @@ -283,8 +283,14 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt if (Settings) { FString ManifestJson, ManifestHash, ManifestError; - if (Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError) - && !ManifestJson.IsEmpty()) + const AVoxelWorld* VoxelWorld = Cast(Owner.Get()); + const bool bHasRuntimeAssembly = VoxelWorld != nullptr + && VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash); + if (!bHasRuntimeAssembly) + { + Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError); + } + if (!ManifestJson.IsEmpty()) { FTCHARToUTF8 Utf8Json(*ManifestJson); std::string CoreError; diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 08e1cf7..52593d5 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -9,16 +9,477 @@ #include "UObject/TextProperty.h" #include #include "VoxelSeasonAsset.h" +#include "VoxelBiomeDefinition.h" +#include "VoxelBiomeTypes.h" #include "VoxelSettings.h" #include "VoxelStrateDefinition.h" #include "VoxelStrateManager.h" #include "VoxelTerrainOpDefinition.h" +#include +#include +#include +#include +#include +#include +#include +#include + namespace { using FCoreJsonWriter = TJsonWriter>; constexpr double CoreVoxelSizeCm = 25.0; constexpr double OwnerVoxelSizeCm = 12.5; +constexpr double OwnerToCoreScale = OwnerVoxelSizeCm / CoreVoxelSizeCm; + +std::int64_t WorldAssemblyFixed(double Voxels) +{ + return static_cast(std::llround( + Voxels * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)); +} + +std::int64_t WorldAssemblyQ16(double Value) +{ + return static_cast(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(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(Value >> (Index * 8)); + Hash *= 1099511628211ull; + } + }; + Add(Seed, 4); + Add(static_cast(X), 8); + Add(static_cast(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 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 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 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(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(Along) * Directions[DirectionIndex].X, 65536); + const std::int64_t Y = CenterY + WorldAssemblyRoundRatio( + static_cast(Along) * Directions[DirectionIndex].Y, 65536); + const auto Column = BaseField.point(WorldAssemblyFixed(static_cast(X)), + WorldAssemblyFixed(static_cast(Y)), 0); + const std::int64_t DeltaX = X - PreviousX; + const std::int64_t DeltaY = Y - PreviousY; + const std::uint64_t HorizontalVoxels = WorldAssemblyRoundedSqrt( + static_cast(DeltaX * DeltaX + DeltaY * DeltaY)); + const std::int64_t HorizontalQ = static_cast(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(1, HorizontalQ); + } + WorldAssemblyRoutePointV1 Point; + Point.positionSubvoxels = {WorldAssemblyFixed(static_cast(X)), + WorldAssemblyFixed(static_cast(Y)), + Column.groundHeightSubvoxels + HalfHeightQ}; + Point.radiusSubvoxels = RouteRadiusQ; + Point.stableFeatureId = WorldAssemblyFeatureId(Recipe.seed, X, Y, + 0x52540000u + static_cast(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(1, WorldAssemblyQ16(Params.CliffSlopeThreshold)); + Profile.cliffSharpnessQ16 = WorldAssemblyQ16(Params.CliffSharpness); + Profile.cliffSampleDistanceSubvoxels = WorldAssemblyFixed(std::max(0.5f, Params.CliffSampleDist)); + const double StructuralAbsBound = static_cast(Params.ElevationRange) + + static_cast(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(Slot.BottomChunkZ) * CHUNK_SIZE; + const double HeightVoxels = static_cast(Slot.TopChunkZ - Slot.BottomChunkZ + 1) * CHUNK_SIZE; + const FBiomeMapParams& Map = BiomeContext.Map; + Recipe.seed = static_cast(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(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 Heights{}; + for (int Y = 0; Y < ProbeSide; ++Y) + for (int X = 0; X < ProbeSide; ++X) + { + const std::int64_t PX = static_cast(X - ProbeSide / 2) * ProbeStep; + const std::int64_t PY = static_cast(Y - ProbeSide / 2) * ProbeStep; + const std::size_t Index = static_cast(Y) * ProbeSide + X; + Heights[Index] = BaseField.point(WorldAssemblyFixed(static_cast(PX)), + WorldAssemblyFixed(static_cast(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(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::max(); + std::int64_t HighestGround = std::numeric_limits::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::max() + || HighestGround == std::numeric_limits::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(StrateManager->GetWorldSeed()); + const std::int64_t BottomCoreVoxels = static_cast(Slot.BottomChunkZ) * 16; + const std::int64_t TopCoreVoxels = static_cast(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 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 SerializeCoreStruct( const UStruct* Type, const void* Data, TSet& ActiveObjects, @@ -287,21 +748,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, { return false; } - if (ManifestJson.IsEmpty()) + std::string CoreError; + std::optional Manifest; + const bool bImportedOverride = !ManifestJson.IsEmpty(); + if (bImportedOverride) { - return true; + FTCHARToUTF8 Utf8Json(*ManifestJson); + Manifest = VoxelForgeCore::WorldAssemblyManifest::FromJson( + std::string_view(Utf8Json.Get(), static_cast(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(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 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(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(Generated.contentHash)); + Manifest = MoveTemp(Generated); } - FTCHARToUTF8 Utf8Json(*ManifestJson); - std::string CoreError; - const std::optional Manifest = - VoxelForgeCore::WorldAssemblyManifest::FromJson( - std::string_view(Utf8Json.Get(), static_cast(Utf8Json.Length())), CoreError); - if (!Manifest) + if (!Manifest.has_value()) { - OutError = FString::Printf(TEXT("Core world assembly validation failed: %s"), - UTF8_TO_TCHAR(CoreError.c_str())); - return false; + return true; } if (Manifest->recipe.seed != static_cast(StrateManager->GetWorldSeed())) { @@ -394,6 +932,12 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, } Root->SetObjectField(TEXT("world_assembly"), ManifestObject.ToSharedRef()); 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(Manifest->plan.strates.size()), + static_cast(Manifest->floatingIslands.size()), *ManifestHash); return true; } } diff --git a/Source/VoxelForge/Private/VoxelSettings.cpp b/Source/VoxelForge/Private/VoxelSettings.cpp index b6d8689..89f5488 100644 --- a/Source/VoxelForge/Private/VoxelSettings.cpp +++ b/Source/VoxelForge/Private/VoxelSettings.cpp @@ -95,6 +95,7 @@ bool UVoxelSettings::SetWorldAssemblyManifestJson(const FString& InJson, FString WorldAssemblyManifestJson = UTF8_TO_TCHAR(CanonicalJson.c_str()); WorldAssemblyManifestHash = FString::Printf(TEXT("%016llx"), static_cast(Manifest->contentHash)); + bUseWorldAssemblyManifestOverride = true; MarkPackageDirty(); OutReport = FString::Printf( TEXT("imported native world assembly %s (%d SurfaceWorld plans, %d floating-island plans)"), @@ -109,8 +110,17 @@ bool UVoxelSettings::GetWorldAssemblyManifestJson( OutCanonicalJson.Reset(); OutContentHash.Reset(); OutError.Reset(); + if (!bUseWorldAssemblyManifestOverride) + { + return true; + } if (WorldAssemblyManifestJson.IsEmpty()) { + if (bUseWorldAssemblyManifestOverride) + { + OutError = TEXT("world assembly override is enabled but its manifest JSON is empty"); + return false; + } if (!WorldAssemblyManifestHash.IsEmpty()) { OutError = TEXT("world assembly hash is set but its manifest JSON is empty"); diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 5e9cbd0..07c3841 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -152,6 +152,7 @@ public: std::atomic bShuttingDown{false}; std::atomic NextEditSequence{1}; int32 FullResolutionLodCount = 0; + FString InitialFailureReason; }; #if WITH_EDITOR @@ -2456,12 +2457,13 @@ void AVoxelWorld::BeginPlay() DiffLayer->SetBudget(Settings->MaxModifications, Settings->MaxBrushRadius, Settings->MaxTotalVolume); Generator->SetDiffLayer(DiffLayer); - if (bUseVoxelForgeCore && !InitializeVoxelForgeCoreWorld()) + if (bUseVoxelForgeCore) { - // A failed opt-in must stay visibly failed. Falling back to the legacy route here would - // make the switch appear to work while silently changing the generated world. - UE_LOG(LogTemp, Error, - TEXT("[VoxelForgeCoreRuntime] Core mode is enabled but initialization did not complete.")); + FString CoreFailureReason; + if (!InitializeVoxelForgeCoreWorld(CoreFailureReason)) + { + FallBackToLegacyAfterCoreInitializationFailure(CoreFailureReason); + } } if (bTestCeilingViewEnabled && IsParameterizedResearchFieldActive()) @@ -6596,6 +6598,12 @@ void AVoxelWorld::Tick(float DeltaTime) { Super::Tick(DeltaTime); 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 (bTestResearchProfileSwitchConfigured && !bTestResearchProfileSwitchApplied @@ -7217,6 +7225,14 @@ FVector AVoxelWorld::GetVoxelForgeCoreDefaultCenterWorld() const 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 { OutDensity = 0.0f; @@ -7230,13 +7246,15 @@ bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& Out return FMath::IsFinite(OutDensity); } -bool AVoxelWorld::InitializeVoxelForgeCoreWorld() +bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason) { + OutFailureReason.Reset(); + VoxelForgeCoreWorldAssemblyJson.Reset(); + VoxelForgeCoreWorldAssemblyHash.Reset(); if (!bUseVoxelForgeCore) return true; if (!Settings || !StrateManager) { - UE_LOG(LogTemp, Error, - TEXT("[VoxelForgeCoreRuntime] initialization failed: Settings or resolved StrateManager is missing.")); + OutFailureReason = TEXT("Settings or the resolved strate layout is missing."); return false; } @@ -7247,7 +7265,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() if (!VoxelForgeCoreAdapter::SerializeResolvedWorld( 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; } FTCHARToUTF8 Utf8Json(*Json); @@ -7257,12 +7276,22 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() std::string_view(Utf8Json.Get(), static_cast(Utf8Json.Length())), CoreError); 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())); + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason); return false; } 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(Assembly->contentHash)); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx surface_strates=%d island_strates=%d"), static_cast(Assembly->contentHash), @@ -7294,7 +7323,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() {CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z}); 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; } @@ -7327,7 +7356,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() } 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; } @@ -7373,6 +7402,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() } 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); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] shape compile failed: %s"), UTF8_TO_TCHAR(Error.c_str())); @@ -7394,6 +7425,49 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() 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 { OutPosition = FVector::ZeroVector; @@ -13293,12 +13367,43 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const return SeasonHash; } - FString AssemblyJson; - FString AssemblyHash; - FString AssemblyError; - if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError)) + FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash; + if (AssemblyHash.IsEmpty()) { - 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 Snapshot = + VoxelForgeCore::ResolvedWorldSnapshot::FromJson( + std::string_view(Utf8Snapshot.Get(), + static_cast(Utf8Snapshot.Length())), CoreError); + if (!Snapshot) + { + return FString(); + } + if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly()) + { + AssemblyHash = FString::Printf(TEXT("%016llx"), + static_cast(Assembly->contentHash)); + } + } } const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1; if (AssemblyHash.IsEmpty() && !bHybridMesher) diff --git a/Source/VoxelForge/Public/VoxelSettings.h b/Source/VoxelForge/Public/VoxelSettings.h index 3fd7650..efd1c5a 100644 --- a/Source/VoxelForge/Public/VoxelSettings.h +++ b/Source/VoxelForge/Public/VoxelSettings.h @@ -383,6 +383,11 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Voxel|Strates") 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 UPROPERTY(EditAnywhere, Category = "Voxel|Strates|World Assembly", meta = (FilePathFilter = "json")) FFilePath SourceWorldAssemblyManifestJson; diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index af19c6d..a8fa87d 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -588,6 +588,7 @@ public: bool IsVoxelForgeCoreReady() const; FVector GetVoxelForgeCoreDefaultCenterWorld() const; bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const; + bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const; UFUNCTION(BlueprintPure, Category = "Voxel World|Strate") int32 GetStrateAtPosition(FVector WorldPosition) const; @@ -675,7 +676,8 @@ private: void SetDensityVolumeMaterialParamsDisabled(); /** 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, APawn* PlayerPawn, const FVector& PlayerHeading, TArray& OutLeavers); @@ -740,6 +742,8 @@ private: TWeakObjectPtr LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated TSharedPtr VoxelForgeCoreState; + FString VoxelForgeCoreWorldAssemblyJson; + FString VoxelForgeCoreWorldAssemblyHash; FVector CoreFallbackCenterLocalCm = FVector::ZeroVector; bool bCoreRegionCompilePending = false; TArray PendingCoreRegionTiles; diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index de70838..816072f 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -804,6 +804,11 @@ struct FExploreArguments bool bCoreHybridCliffOnly = false; bool bCoreHybridFixturesOnly = false; bool bCoreFloatingIslandTest = false; + bool bCoreFallbackTest = false; + bool bCoreOffTest = false; + bool bCorePlanOnlyTest = false; + bool bCoreTestSeedOverride = false; + int32 CoreTestSeed = 0; bool bRender = true; bool bWalk = true; bool bExport = true; @@ -919,6 +924,17 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr int32 CoreFloatingIslandTest = 0; FParse::Value(*Params, TEXT("coreislandtest="), CoreFloatingIslandTest); OutArguments.bCoreFloatingIslandTest = CoreFloatingIslandTest != 0; + int32 CoreFallbackTest = 0; + FParse::Value(*Params, TEXT("corefallbacktest="), CoreFallbackTest); + OutArguments.bCoreFallbackTest = CoreFallbackTest != 0; + int32 CoreOffTest = 0; + FParse::Value(*Params, TEXT("coreofftest="), CoreOffTest); + OutArguments.bCoreOffTest = CoreOffTest != 0; + int32 CorePlanOnlyTest = 0; + FParse::Value(*Params, TEXT("coreplantest="), CorePlanOnlyTest); + OutArguments.bCorePlanOnlyTest = CorePlanOnlyTest != 0; + OutArguments.bCoreTestSeedOverride = FParse::Value( + *Params, TEXT("coretestseed="), OutArguments.CoreTestSeed); int32 CoreHybridMesher = 0; FParse::Value(*Params, TEXT("corehybridmesher="), CoreHybridMesher); if (CoreHybridMesher < 0 || CoreHybridMesher > 2) @@ -1475,7 +1491,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr } if ((OutArguments.bCoreHybridMesher || OutArguments.bCoreHybridFixturePlacement || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly - || OutArguments.bCoreFloatingIslandTest) + || OutArguments.bCoreFloatingIslandTest || OutArguments.bCoreFallbackTest + || OutArguments.bCoreOffTest || OutArguments.bCorePlanOnlyTest) && !OutArguments.bCoreWorldTest) { OutError = TEXT("Core hybrid tests require coreworldtest."); @@ -1502,12 +1519,37 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr OutError = TEXT("corehybridfixturesonly cannot be combined with the other focused hybrid modes."); return false; } + const int32 CoreTestModeCount = (OutArguments.bCoreFloatingIslandTest ? 1 : 0) + + (OutArguments.bCoreFallbackTest ? 1 : 0) + + (OutArguments.bCoreOffTest ? 1 : 0) + + (OutArguments.bCorePlanOnlyTest ? 1 : 0); + if (CoreTestModeCount > 1) + { + OutError = TEXT("coreislandtest, corefallbacktest, coreofftest, and coreplantest are exclusive modes."); + return false; + } + if (OutArguments.bCoreFallbackTest && OutArguments.CoreWorldAssemblyManifestPath.IsEmpty()) + { + OutError = TEXT("corefallbacktest requires a staged coreworldassembly override."); + return false; + } + if (CoreTestModeCount > 0 + && (OutArguments.bCoreHybridMesher || OutArguments.bCoreHybridFixturePlacement + || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly)) + { + OutError = TEXT("The selected Core world mode cannot be combined with hybrid mesher fixtures."); + return false; + } + if (OutArguments.bCoreTestSeedOverride && !OutArguments.bCorePlanOnlyTest) + { + OutError = TEXT("coretestseed is accepted only with coreplantest."); + return false; + } if (OutArguments.bCoreFloatingIslandTest && (OutArguments.bCoreHybridMesher || OutArguments.bCoreHybridFixturePlacement - || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly - || OutArguments.CoreWorldAssemblyManifestPath.IsEmpty())) + || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly)) { - OutError = TEXT("coreislandtest requires coreworldassembly and cannot be combined with hybrid test modes."); + OutError = TEXT("coreislandtest cannot be combined with hybrid test modes."); return false; } if (!OutArguments.CoreWorldAssemblyManifestPath.IsEmpty() && !OutArguments.bCoreWorldTest) @@ -14520,6 +14562,581 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, bool bIncludeV2, return true; } +bool CreateLockedLayoutSettingsCopy( + UVoxelSettings* SourceSettings, + UVoxelStrateManager* SourceManager, + int32 Seed, + bool bConvertSurfaceToFloatingIslands, + TStrongObjectPtr& OutSettings, + TArray>& OutDefinitions, + int32& OutTargetStrateIndex, + FString& OutError) +{ + if (SourceSettings == nullptr || SourceManager == nullptr || SourceManager->GetLayout().IsEmpty()) + { + OutError = TEXT("Could not copy an empty resolved owner layout for the Core test."); + return false; + } + UVoxelSettings* SettingsCopy = DuplicateObject( + SourceSettings, GetTransientPackage(), NAME_None); + if (SettingsCopy == nullptr) + { + OutError = TEXT("Could not duplicate owner settings for the transient Core test."); + return false; + } + OutSettings = TStrongObjectPtr(SettingsCopy); + SettingsCopy->Season.Reset(); + SettingsCopy->Seed = Seed; + SettingsCopy->TotalStrates = SourceManager->GetLayout().Num(); + SettingsCopy->StratePool.Reset(); + SettingsCopy->FixedStrates.Reset(); + SettingsCopy->WorldAssemblyManifestJson.Reset(); + SettingsCopy->WorldAssemblyManifestHash.Reset(); + SettingsCopy->bUseWorldAssemblyManifestOverride = false; + OutDefinitions.Reset(); + OutTargetStrateIndex = INDEX_NONE; + + int32 ResolvedGap = SourceSettings->InterStrateGapChunks; + if (SourceManager->GetLayout().Num() > 1 && !SourceSettings->Season.IsNull()) + { + ResolvedGap = SourceManager->GetLayout()[0].BottomChunkZ + - SourceManager->GetLayout()[1].TopChunkZ - 1; + for (int32 Index = 1; Index + 1 < SourceManager->GetLayout().Num(); ++Index) + { + const int32 Gap = SourceManager->GetLayout()[Index].BottomChunkZ + - SourceManager->GetLayout()[Index + 1].TopChunkZ - 1; + if (Gap != ResolvedGap) + { + OutError = TEXT("The resolved season uses non-uniform strate gaps; the transient copy cannot preserve it."); + return false; + } + } + } + SettingsCopy->InterStrateGapChunks = FMath::Max(0, ResolvedGap); + + TArray> DefinitionReferences; + DefinitionReferences.Reserve(SourceManager->GetLayout().Num()); + TMap DefinitionCopies; + for (const FStrateSlot& Slot : SourceManager->GetLayout()) + { + if (Slot.Definition == nullptr) + { + OutError = FString::Printf(TEXT("Owner layout slot %d has no definition to copy."), Slot.StrateIndex); + return false; + } + UVoxelStrateDefinition* DefinitionCopy = DuplicateObject( + Slot.Definition, SettingsCopy, FName(*FString::Printf(TEXT("CoreTest_Strate_%d"), Slot.StrateIndex))); + if (DefinitionCopy == nullptr) + { + OutError = FString::Printf(TEXT("Could not duplicate owner strate definition %d."), Slot.StrateIndex); + return false; + } + if (bConvertSurfaceToFloatingIslands + && OutTargetStrateIndex == INDEX_NONE + && Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld) + { + OutTargetStrateIndex = Slot.StrateIndex; + DefinitionCopy->GeneratorType = ECaveGeneratorType::FloatingIslands; + } + DefinitionCopies.Add(Slot.StrateIndex, DefinitionCopy); + SettingsCopy->FixedStrates.Add(Slot.StrateIndex, + TSoftObjectPtr(DefinitionCopy)); + DefinitionReferences.Add(TStrongObjectPtr(DefinitionCopy)); + } + if (bConvertSurfaceToFloatingIslands && OutTargetStrateIndex == INDEX_NONE) + { + OutError = TEXT("The owner layout has no SurfaceWorld strate to convert for the island proof."); + return false; + } + if (!bConvertSurfaceToFloatingIslands) OutTargetStrateIndex = INDEX_NONE; + + const FStrateSlot* TargetSlot = SourceManager->GetLayout().FindByPredicate( + [OutTargetStrateIndex](const FStrateSlot& Slot) + { return Slot.StrateIndex == OutTargetStrateIndex; }); + if (TargetSlot != nullptr) + { + UVoxelStrateDefinition* TargetDefinition = DefinitionCopies.FindRef(OutTargetStrateIndex); + if (TargetDefinition == nullptr) + { + OutError = TEXT("The transient SurfaceWorld definition could not be resolved."); + return false; + } + bool bFixedWaterEnabled = false; + float FixedWaterRelative = 0.0f; + const int32 MidChunkZ = TargetSlot->BottomChunkZ + + (TargetSlot->TopChunkZ - TargetSlot->BottomChunkZ) / 2; + const float WaterVoxelZ = SourceManager->GetWaterLevelWorldZForChunk( + FIntVector(0, 0, MidChunkZ)); + if (WaterVoxelZ != -FLT_MAX) + { + const float TopVoxelZ = (static_cast(TargetSlot->TopChunkZ) + 1.0f) * CHUNK_SIZE; + const float BottomVoxelZ = static_cast(TargetSlot->BottomChunkZ) * CHUNK_SIZE; + FixedWaterRelative = FMath::Clamp( + (WaterVoxelZ - BottomVoxelZ) / FMath::Max(TopVoxelZ - BottomVoxelZ, 1.0f), + 0.0f, 1.0f); + bFixedWaterEnabled = true; + } + TargetDefinition->bHasWater = bFixedWaterEnabled; + TargetDefinition->GenerationParams.WaterLevelRelative = + bFixedWaterEnabled ? FixedWaterRelative : 0.0f; + } + + OutDefinitions = MoveTemp(DefinitionReferences); + TStrongObjectPtr VerificationManager(NewObject( + GetTransientPackage(), NAME_None, RF_Transient)); + if (!VerificationManager.IsValid() || !VerificationManager->Initialize(SettingsCopy, Seed) + || VerificationManager->GetLayout().Num() != SourceManager->GetLayout().Num()) + { + OutError = TEXT("The locked transient settings copy did not resolve the owner's full strate layout."); + return false; + } + for (int32 Index = 0; Index < SourceManager->GetLayout().Num(); ++Index) + { + const FStrateSlot& SourceSlot = SourceManager->GetLayout()[Index]; + const FStrateSlot& CopySlot = VerificationManager->GetLayout()[Index]; + if (SourceSlot.StrateIndex != CopySlot.StrateIndex + || SourceSlot.TopChunkZ != CopySlot.TopChunkZ + || SourceSlot.BottomChunkZ != CopySlot.BottomChunkZ + || SourceSlot.HeightInChunks != CopySlot.HeightInChunks) + { + OutError = FString::Printf(TEXT("Transient layout copy changed owner heights at slot %d."), Index); + return false; + } + const ECaveGeneratorType ExpectedType = bConvertSurfaceToFloatingIslands + && CopySlot.StrateIndex == OutTargetStrateIndex + ? ECaveGeneratorType::FloatingIslands : SourceSlot.Definition->GeneratorType; + if (CopySlot.Definition == nullptr || CopySlot.Definition->GeneratorType != ExpectedType) + { + OutError = FString::Printf(TEXT("Transient layout copy changed an unintended archetype at slot %d."), Index); + return false; + } + } + return true; +} + +struct FCoreTestWorldFixture +{ + UWorld* World = nullptr; + uint64 PreviousFrameCounter = GFrameCounter; + + bool Initialize(const FName& WorldName, FString& OutError) + { + const UWorld::InitializationValues Values = UWorld::InitializationValues() + .AllowAudioPlayback(false).RequiresHitProxies(false).CreatePhysicsScene(true) + .CreateNavigation(false).CreateAISystem(false).ShouldSimulatePhysics(true) + .SetDefaultGameMode(AGameModeBase::StaticClass()).SetTransactional(false); + World = UWorld::CreateWorld(EWorldType::Game, false, WorldName, + GetTransientPackage(), false, ERHIFeatureLevel::Num, &Values); + if (World == nullptr) + { + OutError = TEXT("Could not create a transient BuildHost Core test world."); + return false; + } + if (GEngine == nullptr) + { + OutError = TEXT("Unreal Engine is not available for the BuildHost Core test world."); + return false; + } + UGameInstance* GameInstance = NewObject(GEngine); + if (GameInstance == nullptr) + { + OutError = TEXT("Could not create the BuildHost Core test game instance."); + return false; + } + FWorldContext& Context = GEngine->CreateNewWorldContext(EWorldType::Game); + Context.OwningGameInstance = GameInstance; + Context.SetCurrentWorld(World); + World->SetGameInstance(GameInstance); + World->SetShouldTick(false); + GameInstance->Init(); + World->AddToRoot(); + return true; + } + + ~FCoreTestWorldFixture() + { + if (World != nullptr) + { + if (World->HasBegunPlay()) + { + World->BeginTearingDown(); + World->EndPlay(EEndPlayReason::Quit); + } + if (UGameInstance* GameInstance = World->GetGameInstance()) GameInstance->Shutdown(); + if (GEngine != nullptr) GEngine->DestroyWorldContext(World); + World->DestroyWorld(false); + if (World->IsRooted()) World->RemoveFromRoot(); + } + GFrameCounter = PreviousFrameCounter; + } +}; + +int32 ResolveCoreTestWorldSeed(const UVoxelSettings* Settings) +{ + if (Settings == nullptr) return 0; + if (!Settings->Season.IsNull()) + { + if (const UVoxelSeasonAsset* Season = Settings->Season.LoadSynchronous()) + { + return Season->GetSeasonSeed(); + } + } + return Settings->Seed; +} + +bool CreateLoggedOwnerSurfaceSettingsCopy( + UVoxelSettings* Source, int32 Seed, + TStrongObjectPtr& OutSettings, + TStrongObjectPtr& OutSurfaceDefinition, + FString& OutError) +{ + if (Source == nullptr) + { + OutError = TEXT("Cannot reconstruct the logged owner SurfaceWorld layout without its settings asset."); + return false; + } + if (!Source->Season.IsNull()) + { + OutError = TEXT("The staged owner settings assign a season asset; the logged one-slot SurfaceWorld layout cannot be reconstructed while preserving that season."); + return false; + } + UVoxelStrateDefinition* SurfaceDefinition = LoadObject( + nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1")); + if (SurfaceDefinition == nullptr + || SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld + || SurfaceDefinition->StrateHeightInChunks <= 0 + || Source->TotalStrates != 1) + { + OutError = FString::Printf( + TEXT("The staged owner assets do not describe the logged single SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."), + SurfaceDefinition != nullptr ? 1 : 0, + SurfaceDefinition != nullptr ? static_cast(SurfaceDefinition->GeneratorType) : INDEX_NONE, + SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE, + Source->TotalStrates); + return false; + } + UVoxelSettings* SettingsCopy = DuplicateObject( + Source, GetTransientPackage(), NAME_None); + if (SettingsCopy == nullptr) + { + OutError = TEXT("Could not duplicate owner settings for the logged SurfaceWorld layout."); + return false; + } + OutSettings = TStrongObjectPtr(SettingsCopy); + OutSurfaceDefinition = TStrongObjectPtr(SurfaceDefinition); + SettingsCopy->Seed = Seed; + SettingsCopy->TotalStrates = Source->TotalStrates; + SettingsCopy->StratePool.Reset(); + SettingsCopy->FixedStrates.Reset(); + SettingsCopy->FixedStrates.Add(0, + TSoftObjectPtr(SurfaceDefinition)); + SettingsCopy->WorldAssemblyManifestJson.Reset(); + SettingsCopy->WorldAssemblyManifestHash.Reset(); + SettingsCopy->bUseWorldAssemblyManifestOverride = false; + const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 seed=%d strata=1 bounds=[0,%d] height_chunks=%d gap=%d imported_override=cleared"), + Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks); + return true; +} + +bool RunCorePlanOnlyTest(const FExploreArguments& Arguments, FString& OutError) +{ + UVoxelSettings* AuthoredSettings = LoadObject( + nullptr, *Arguments.SettingsReference); + if (AuthoredSettings == nullptr) + { + OutError = FString::Printf(TEXT("Could not load Core plan test settings '%s'."), + *Arguments.SettingsReference); + return false; + } + const int32 AuthoredSeed = ResolveCoreTestWorldSeed(AuthoredSettings); + TStrongObjectPtr OwnerSurfaceSettings; + TStrongObjectPtr OwnerSurfaceDefinition; + if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, AuthoredSeed, + OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError)) + { + return false; + } + TStrongObjectPtr SourceManager(NewObject( + GetTransientPackage(), NAME_None, RF_Transient)); + if (!SourceManager.IsValid() || !SourceManager->Initialize(OwnerSurfaceSettings.Get(), AuthoredSeed)) + { + OutError = TEXT("Core plan test could not resolve the staged owner settings."); + return false; + } + + const int32 ResolvedSeed = Arguments.bCoreTestSeedOverride + ? Arguments.CoreTestSeed : SourceManager->GetWorldSeed(); + TStrongObjectPtr SettingsCopy; + TArray> DefinitionReferences; + int32 UnusedTargetStrate = INDEX_NONE; + UVoxelSettings* PlanSettings = nullptr; + if (Arguments.bCoreTestSeedOverride) + { + if (!CreateLockedLayoutSettingsCopy(OwnerSurfaceSettings.Get(), SourceManager.Get(), ResolvedSeed, + false, SettingsCopy, DefinitionReferences, UnusedTargetStrate, OutError)) + { + return false; + } + PlanSettings = SettingsCopy.Get(); + } + else + { + SettingsCopy = TStrongObjectPtr( + DuplicateObject(OwnerSurfaceSettings.Get(), GetTransientPackage(), NAME_None)); + if (!SettingsCopy.IsValid()) + { + OutError = TEXT("Could not duplicate the logged owner SurfaceWorld settings for plan generation."); + return false; + } + PlanSettings = SettingsCopy.Get(); + } + // Keep any imported manifest bytes from the staged owner asset, but leave the opt-in off. + // This proves old round-3 payloads do not block automatic planning by default. + PlanSettings->WorldAssemblyManifestJson = AuthoredSettings->WorldAssemblyManifestJson; + PlanSettings->WorldAssemblyManifestHash = AuthoredSettings->WorldAssemblyManifestHash; + PlanSettings->bUseWorldAssemblyManifestOverride = false; + TStrongObjectPtr PlanManager(NewObject( + GetTransientPackage(), NAME_None, RF_Transient)); + if (!PlanManager.IsValid() + || !PlanManager->Initialize(PlanSettings, ResolvedSeed)) + { + OutError = TEXT("Core plan test could not initialize the final resolved layout."); + return false; + } + + FString RuntimeJson; + FString RepeatJson; + if (!VoxelForgeCoreAdapter::SerializeResolvedWorld( + PlanSettings, PlanManager.Get(), 3, 8, RuntimeJson, OutError) + || !VoxelForgeCoreAdapter::SerializeResolvedWorld( + PlanSettings, PlanManager.Get(), 3, 8, RepeatJson, OutError)) + { + return false; + } + if (RuntimeJson != RepeatJson) + { + OutError = TEXT("Core automatic plan serialization changed between identical startup resolutions."); + return false; + } + FTCHARToUTF8 Utf8(*RuntimeJson); + std::string CoreError; + std::optional Snapshot = + VoxelForgeCore::ResolvedWorldSnapshot::FromJson( + std::string_view(Utf8.Get(), static_cast(Utf8.Length())), CoreError); + if (!Snapshot) + { + OutError = FString::Printf(TEXT("Core rejected the generated plan test snapshot: %s"), + UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + int32 ExpectedSurfaceCount = 0; + int32 ExpectedIslandCount = 0; + for (const FStrateSlot& Slot : PlanManager->GetLayout()) + { + if (Slot.Definition == nullptr) continue; + const ECaveGeneratorType Type = PlanManager->GetGeneratorTypeForChunk( + FIntVector(0, 0, Slot.TopChunkZ)); + ExpectedSurfaceCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0; + ExpectedIslandCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0; + } + const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly(); + if (ExpectedSurfaceCount + ExpectedIslandCount > 0 + && (Assembly == nullptr + || Assembly->plan.strates.size() != static_cast(ExpectedSurfaceCount) + || Assembly->floatingIslands.size() != static_cast(ExpectedIslandCount))) + { + OutError = TEXT("Generated plan counts do not match the final resolved owner layout."); + return false; + } + if (ExpectedSurfaceCount + ExpectedIslandCount == 0 && Assembly != nullptr) + { + OutError = TEXT("Core attached a world assembly to a layout with no planned strata."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCorePlanTest] pass source=%s seed=%d layout_strates=%d surface_strates=%d island_strates=%d hash=%s deterministic_json=1 stored_manifest_ignored=%d override_enabled=0"), + Arguments.bCoreTestSeedOverride ? TEXT("locked_owner_layout_seed_override") : TEXT("owner_settings"), + PlanManager->GetWorldSeed(), PlanManager->GetLayout().Num(), ExpectedSurfaceCount, + ExpectedIslandCount, Assembly ? *FString::Printf(TEXT("%016llx"), + static_cast(Assembly->contentHash)) : TEXT("none"), + AuthoredSettings->WorldAssemblyManifestJson.IsEmpty() ? 0 : 1); + for (const FStrateSlot& Slot : PlanManager->GetLayout()) + { + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCorePlanTest] slot=%d bounds=[%d,%d] type=%d height_chunks=%d"), + Slot.StrateIndex, Slot.TopChunkZ, Slot.BottomChunkZ, + Slot.Definition ? static_cast(Slot.Definition->GeneratorType) : INDEX_NONE, + Slot.HeightInChunks); + } + return true; +} + +bool RunCoreFallbackActorTest(const FExploreArguments& Arguments, FString& OutError) +{ + UVoxelSettings* AuthoredSettings = LoadObject( + nullptr, *Arguments.SettingsReference); + const FString ManifestPath = FPaths::ConvertRelativePathToFull( + Arguments.CoreWorldAssemblyManifestPath); + if (AuthoredSettings == nullptr || !IsUnderVoxelForgeSaved(ManifestPath)) + { + OutError = TEXT("Core fallback test needs staged owner settings and a manifest inside plugin Saved."); + return false; + } + FString ManifestJson; + if (!FFileHelper::LoadFileToString(ManifestJson, *ManifestPath)) + { + OutError = FString::Printf(TEXT("Could not read staged fallback manifest '%s'."), *ManifestPath); + return false; + } + const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings); + TStrongObjectPtr TestSettings; + TStrongObjectPtr OwnerSurfaceDefinition; + if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed, + TestSettings, OwnerSurfaceDefinition, OutError)) + { + return false; + } + FString ImportReport; + if (!TestSettings->SetWorldAssemblyManifestJson(ManifestJson, ImportReport)) + { + OutError = FString::Printf(TEXT("Could not stage the mismatched manifest in transient settings: %s."), + *ImportReport); + return false; + } + FCoreTestWorldFixture WorldFixture; + if (!WorldFixture.Initialize(FName(TEXT("VoxelForgeCoreFallbackTestWorld")), OutError)) return false; + AVoxelWorld* Actor = WorldFixture.World->SpawnActorDeferred( + AVoxelWorld::StaticClass(), FTransform::Identity, nullptr, nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + if (Actor == nullptr) + { + OutError = TEXT("Could not spawn the transient Core fallback AVoxelWorld."); + return false; + } + Actor->Settings = TestSettings.Get(); + Actor->bUseVoxelForgeCore = true; + Actor->bManageAtmosphere = false; + Actor->CoreClipRadius = 1; + Actor->CoreMaxClipLevel = 0; + Actor->FinishSpawning(FTransform::Identity); + if (!WorldFixture.World->SetGameMode(FURL())) + { + OutError = TEXT("Could not create the transient GameMode for the Core fallback test."); + return false; + } + WorldFixture.World->InitializeActorsForPlay(FURL()); + WorldFixture.World->BeginPlay(); + if (Actor->bUseVoxelForgeCore || Actor->IsVoxelForgeCoreReady() + || Actor->Generator == nullptr || !Actor->HasActorBegunPlay()) + { + OutError = TEXT("Mismatched world assembly did not disable Core and leave the legacy world running."); + return false; + } + FString RuntimeAssemblyJson, RuntimeAssemblyHash; + float LegacyDensity = Actor->Generator->GetDensityAt(0.0f, 0.0f, -32.0f); + if (!FMath::IsFinite(LegacyDensity) + || Actor->GetVoxelForgeCoreWorldAssembly(RuntimeAssemblyJson, RuntimeAssemblyHash)) + { + OutError = TEXT("Core fallback did not expose a finite legacy density path or cleared runtime plan state."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreFallbackTest] fallback=PASS core_disabled=1 legacy_generator=1 legacy_density=%.4f imported_override=1"), + LegacyDensity); + return true; +} + +bool RunCoreOffActorTest(const FExploreArguments& Arguments, FString& OutError) +{ + UVoxelSettings* Settings = LoadObject(nullptr, *Arguments.SettingsReference); + if (Settings == nullptr) + { + OutError = FString::Printf(TEXT("Could not load Core-off owner settings '%s'."), + *Arguments.SettingsReference); + return false; + } + const int32 Seed = ResolveCoreTestWorldSeed(Settings); + TStrongObjectPtr OwnerSurfaceSettings; + TStrongObjectPtr OwnerSurfaceDefinition; + if (!CreateLoggedOwnerSurfaceSettingsCopy(Settings, Seed, + OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError)) + { + return false; + } + TStrongObjectPtr ReferenceManager(NewObject( + GetTransientPackage(), NAME_None, RF_Transient)); + if (!ReferenceManager.IsValid() || !ReferenceManager->Initialize(OwnerSurfaceSettings.Get(), Seed)) + { + OutError = TEXT("Core-off test could not resolve the staged owner layout."); + return false; + } + UVoxelGenerator* ReferenceGenerator = NewObject( + GetTransientPackage(), NAME_None, RF_Transient); + if (ReferenceGenerator == nullptr) + { + OutError = TEXT("Core-off test could not create its legacy density reference."); + return false; + } + ReferenceGenerator->InitializeSettings(OwnerSurfaceSettings.Get()); + ReferenceGenerator->SetStrateManager(ReferenceManager.Get()); + + FCoreTestWorldFixture WorldFixture; + if (!WorldFixture.Initialize(FName(TEXT("VoxelForgeCoreOffTestWorld")), OutError)) return false; + AVoxelWorld* Actor = WorldFixture.World->SpawnActorDeferred( + AVoxelWorld::StaticClass(), FTransform::Identity, nullptr, nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + if (Actor == nullptr) + { + OutError = TEXT("Could not spawn the transient Core-off AVoxelWorld."); + return false; + } + Actor->Settings = OwnerSurfaceSettings.Get(); + Actor->bUseVoxelForgeCore = false; + Actor->bManageAtmosphere = false; + Actor->FinishSpawning(FTransform::Identity); + if (!WorldFixture.World->SetGameMode(FURL())) + { + OutError = TEXT("Could not create the transient GameMode for the Core-off test."); + return false; + } + WorldFixture.World->InitializeActorsForPlay(FURL()); + WorldFixture.World->BeginPlay(); + if (Actor->bUseVoxelForgeCore || Actor->Generator == nullptr || Actor->Mesher == nullptr) + { + OutError = TEXT("Core-off owner world did not remain on its legacy generator and mesher."); + return false; + } + const FVector VoxelOrigin = Actor->LocalVoxelToWorld(FVector::ZeroVector); + const FVector NextVoxel = Actor->LocalVoxelToWorld(FVector(1.0, 0.0, 0.0)); + const double LegacyVoxelCm = FVector::Distance(VoxelOrigin, NextVoxel); + const FVector Samples[] = { + FVector(0.0, 0.0, -32.0), FVector(48.0, -64.0, -128.0), FVector(-160.0, 96.0, -512.0)}; + for (const FVector& Sample : Samples) + { + const float Actual = Actor->Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z); + const float Expected = ReferenceGenerator->GetDensityAt(Sample.X, Sample.Y, Sample.Z); + if (!FMath::IsFinite(Actual) || Actual != Expected) + { + OutError = FString::Printf(TEXT("Core-off legacy density changed at (%.0f,%.0f,%.0f): %.6f/%.6f."), + Sample.X, Sample.Y, Sample.Z, Actual, Expected); + return false; + } + } + float CoreDensity = 0.0f; + if (LegacyVoxelCm != VOXEL_SIZE + || Actor->QueryVoxelForgeCoreDensityAt(VoxelOrigin, CoreDensity)) + { + OutError = TEXT("Core-off owner world did not retain the 12.5 cm legacy scale and disabled Core queries."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreOffTest] legacy_unchanged=PASS core_disabled=1 voxel_cm=%.1f density_samples=3"), + LegacyVoxelCm); + return true; +} + bool RunCoreFloatingIslandActorIntegrationTest( const FExploreArguments& Arguments, FString& OutError) { @@ -14539,92 +15156,32 @@ bool RunCoreFloatingIslandActorIntegrationTest( return false; } - const FString ManifestPath = FPaths::ConvertRelativePathToFull( - Arguments.CoreWorldAssemblyManifestPath); - if (!IsUnderVoxelForgeSaved(ManifestPath)) - { - OutError = TEXT("Core island test manifest must be staged under the plugin Saved directory."); - return false; - } - FString ManifestJson; - if (!FFileHelper::LoadFileToString(ManifestJson, *ManifestPath)) - { - OutError = FString::Printf(TEXT("Could not read staged island manifest '%s'."), *ManifestPath); - return false; - } - FTCHARToUTF8 Utf8Manifest(*ManifestJson); - std::string CoreError; - const std::optional ParsedAssembly = - VoxelForgeCore::WorldAssemblyManifest::FromJson( - std::string_view(Utf8Manifest.Get(), static_cast(Utf8Manifest.Length())), - CoreError); - if (!ParsedAssembly || ParsedAssembly->floatingIslands.size() != 1u - || !ParsedAssembly->plan.strates.empty() - || ParsedAssembly->floatingIslands.front().strateIndex != 1) - { - OutError = FString::Printf(TEXT("Island manifest must contain one strate-1 island plan and no SurfaceWorld plans: %s"), - ParsedAssembly ? TEXT("manifest shape differs") : UTF8_TO_TCHAR(CoreError.c_str())); - return false; - } - int32 EffectiveSeed = AuthoredSettings->Seed; + int32 EffectiveSeed = ResolveCoreTestWorldSeed(AuthoredSettings); if (const UVoxelSeasonAsset* AuthoredSeason = AuthoredSettings->Season.LoadSynchronous()) - { EffectiveSeed = AuthoredSeason->GetSeasonSeed(); - } - if (ParsedAssembly->recipe.seed != static_cast(EffectiveSeed)) + TStrongObjectPtr OwnerSurfaceSettings; + TStrongObjectPtr OwnerSurfaceDefinition; + if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, EffectiveSeed, + OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError)) { - OutError = FString::Printf( - TEXT("Island manifest seed %u does not match the BuildHost settings seed %d."), - ParsedAssembly->recipe.seed, EffectiveSeed); return false; } - - // Resolve the current season first so this transient island fixture inherits its fixed water, - // atmosphere and authored content values without editing the cooked settings or strate assets. - // Resoudre la saison avant le fixture pour conserver l'eau et l'ambiance sans modifier les assets. TStrongObjectPtr SourceManager(NewObject( GetTransientPackage(), NAME_None, RF_Transient)); if (!SourceManager.IsValid() - || !SourceManager->Initialize(AuthoredSettings, EffectiveSeed)) + || !SourceManager->Initialize(OwnerSurfaceSettings.Get(), EffectiveSeed)) { - OutError = TEXT("Core island test could not resolve the BuildHost season layout."); + OutError = TEXT("Core island test could not resolve the staged owner settings layout."); return false; } - const auto FindSourceDefinition = [&](int32 StrateIndex) -> UVoxelStrateDefinition* + int32 IslandStrateIndex = INDEX_NONE; + TStrongObjectPtr TestSettings; + TArray> TestDefinitions; + if (!CreateLockedLayoutSettingsCopy(OwnerSurfaceSettings.Get(), SourceManager.Get(), EffectiveSeed, + true, TestSettings, TestDefinitions, IslandStrateIndex, OutError)) { - for (const FStrateSlot& Slot : SourceManager->GetLayout()) - { - if (Slot.StrateIndex == StrateIndex) return Slot.Definition; - } - for (const TSoftObjectPtr& SoftDefinition : AuthoredSettings->StratePool) - { - if (UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous()) return Definition; - } - for (const TPair>& Pair : AuthoredSettings->FixedStrates) - { - if (UVoxelStrateDefinition* Definition = Pair.Value.LoadSynchronous()) return Definition; - } - return nullptr; - }; - UVoxelStrateDefinition* SourceTop = FindSourceDefinition(0); - UVoxelStrateDefinition* SourceIsland = FindSourceDefinition(1); - if (SourceTop == nullptr || SourceIsland == nullptr) - { - OutError = TEXT("Core island test needs authored source definitions for strata 0 and 1."); return false; } - - TStrongObjectPtr TestSettings(DuplicateObject( - AuthoredSettings, GetTransientPackage(), NAME_None)); - if (!TestSettings.IsValid()) - { - OutError = TEXT("Could not duplicate transient Core island test settings."); - return false; - } - TestSettings->Season.Reset(); - TestSettings->Seed = EffectiveSeed; - TestSettings->TotalStrates = 2; - TestSettings->InterStrateGapChunks = 0; TestSettings->bEnableDensityVolume = false; TestSettings->DecorationRadiusChunks = 2; TestSettings->DecorationNearRadiusChunks = 1; @@ -14637,87 +15194,14 @@ bool RunCoreFloatingIslandActorIntegrationTest( TestSettings->MaxConcurrentDecorationTasks = 2; TestSettings->MaxDecorationCellsPerFrame = 4; - TStrongObjectPtr TopDefinition(DuplicateObject( - SourceTop, TestSettings.Get(), NAME_None)); - TStrongObjectPtr IslandDefinition(DuplicateObject( - SourceIsland, TestSettings.Get(), NAME_None)); - if (!TopDefinition.IsValid() || !IslandDefinition.IsValid()) + TSoftObjectPtr* IslandDefinitionSoft = + TestSettings->FixedStrates.Find(IslandStrateIndex); + UVoxelStrateDefinition* IslandDefinition = IslandDefinitionSoft != nullptr + ? IslandDefinitionSoft->Get() : nullptr; + if (IslandDefinition == nullptr + || IslandDefinition->GeneratorType != ECaveGeneratorType::FloatingIslands) { - OutError = TEXT("Could not create transient Core island test strate definitions."); - return false; - } - TopDefinition->StrateHeightInChunks = 16; - IslandDefinition->GeneratorType = ECaveGeneratorType::FloatingIslands; - IslandDefinition->StrateHeightInChunks = 64; - IslandDefinition->Landmarks.Empty(); - IslandDefinition->Biomes.Empty(); - IslandDefinition->Decorations.Empty(); - - // Keep the season's water datum when changing only the transient archetype to islands. - // Conserver le niveau d'eau de saison en ne changeant que l'archetype du fixture transitoire. - bool bFixedWaterEnabled = false; - float FixedWaterRelative = 0.0f; - for (const FStrateSlot& Slot : SourceManager->GetLayout()) - { - if (Slot.StrateIndex != 1 || Slot.Definition == nullptr) continue; - const int32 MidChunkZ = Slot.BottomChunkZ - + (Slot.TopChunkZ - Slot.BottomChunkZ) / 2; - const float WaterVoxelZ = SourceManager->GetWaterLevelWorldZForChunk( - FIntVector(0, 0, MidChunkZ)); - if (WaterVoxelZ != -FLT_MAX) - { - const float TopVoxelZ = (static_cast(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE; - const float BottomVoxelZ = static_cast(Slot.BottomChunkZ) * CHUNK_SIZE; - FixedWaterRelative = FMath::Clamp( - (WaterVoxelZ - BottomVoxelZ) / FMath::Max(TopVoxelZ - BottomVoxelZ, 1.0f), - 0.0f, 1.0f); - bFixedWaterEnabled = true; - } - break; - } - IslandDefinition->bHasWater = bFixedWaterEnabled; - IslandDefinition->GenerationParams.WaterLevelRelative = - bFixedWaterEnabled ? FixedWaterRelative : 0.0f; - IslandDefinition->SurfaceParams.WaterLevelRelative = - bFixedWaterEnabled ? FixedWaterRelative : 0.0f; - - UStaticMesh* ProbeTreeMesh = LoadObject( - nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); - if (ProbeTreeMesh == nullptr) - { - OutError = TEXT("Could not load the transient cube mesh used to verify island tree anchors."); - return false; - } - FStrateDecoration ProbeTree; - ProbeTree.Profile.InstancedMesh = ProbeTreeMesh; - ProbeTree.Profile.SurfacePlacement = ESurfaceType::Floor; - ProbeTree.Profile.bAlignToSurface = false; - ProbeTree.Profile.MinScale = 0.5f; - ProbeTree.Profile.MaxScale = 0.5f; - ProbeTree.Profile.CullDistance = 30000.0f; - ProbeTree.Profile.bCastShadow = false; - ProbeTree.StreamTier = EDecoStreamTier::Far; - ProbeTree.SpawnDensity = 1.0f; - ProbeTree.MaxPerChunk = 64; - IslandDefinition->Decorations.Add(ProbeTree); - TestSettings->FixedStrates.Add(0, TSoftObjectPtr(TopDefinition.Get())); - TestSettings->FixedStrates.Add(1, TSoftObjectPtr(IslandDefinition.Get())); - - FString ImportReport; - if (!TestSettings->SetWorldAssemblyManifestJson(ManifestJson, ImportReport)) - { - OutError = FString::Printf(TEXT("Could not import the floating-island manifest into transient settings: %s"), - *ImportReport); - return false; - } - FString CanonicalAssemblyJson; - FString ImportedAssemblyHash; - FString ImportError; - if (!TestSettings->GetWorldAssemblyManifestJson( - CanonicalAssemblyJson, ImportedAssemblyHash, ImportError)) - { - OutError = FString::Printf(TEXT("Transient island manifest failed its canonical hash check: %s"), - *ImportError); + OutError = TEXT("Could not resolve the transient owner SurfaceWorld strate as FloatingIslands."); return false; } @@ -14725,21 +15209,27 @@ bool RunCoreFloatingIslandActorIntegrationTest( GetTransientPackage(), NAME_None, RF_Transient)); if (!Manager.IsValid() || !Manager->Initialize(TestSettings.Get(), EffectiveSeed)) { - OutError = TEXT("Core island test could not build the transient two-strate layout."); + OutError = TEXT("Core island test could not rebuild the locked copy of the owner's layout."); return false; } const FStrateSlot* IslandSlot = nullptr; for (const FStrateSlot& Slot : Manager->GetLayout()) { - if (Slot.StrateIndex == 1) IslandSlot = &Slot; + if (Slot.StrateIndex == IslandStrateIndex) IslandSlot = &Slot; } - if (IslandSlot == nullptr || IslandSlot->TopChunkZ != -16 - || IslandSlot->BottomChunkZ != -79 + const FStrateSlot* SourceIslandSlot = SourceManager->GetLayout().FindByPredicate( + [IslandStrateIndex](const FStrateSlot& Slot) + { return Slot.StrateIndex == IslandStrateIndex; }); + if (IslandSlot == nullptr || SourceIslandSlot == nullptr + || IslandSlot->TopChunkZ != SourceIslandSlot->TopChunkZ + || IslandSlot->BottomChunkZ != SourceIslandSlot->BottomChunkZ || IslandSlot->Definition == nullptr || IslandSlot->Definition->GeneratorType != ECaveGeneratorType::FloatingIslands) { OutError = FString::Printf( - TEXT("Transient island strate does not match manifest bounds [-16,-79]: actual=[%d,%d]."), + TEXT("Transient island strate did not preserve owner bounds [%d,%d]: actual=[%d,%d]."), + SourceIslandSlot ? SourceIslandSlot->TopChunkZ : MAX_int32, + SourceIslandSlot ? SourceIslandSlot->BottomChunkZ : MIN_int32, IslandSlot ? IslandSlot->TopChunkZ : MAX_int32, IslandSlot ? IslandSlot->BottomChunkZ : MIN_int32); return false; @@ -14752,24 +15242,35 @@ bool RunCoreFloatingIslandActorIntegrationTest( return false; } FTCHARToUTF8 Utf8Runtime(*RuntimeJson); + std::string CoreError; std::optional Snapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson( std::string_view(Utf8Runtime.Get(), static_cast(Utf8Runtime.Length())), CoreError); - if (!Snapshot || Snapshot->worldAssembly() == nullptr - || Snapshot->worldAssembly()->contentHash != ParsedAssembly->contentHash - || Snapshot->worldAssembly()->floatingIslands.size() != 1u) + if (!Snapshot || Snapshot->worldAssembly() == nullptr) { - OutError = FString::Printf(TEXT("Core adapter did not preserve the imported floating-island manifest: %s"), - Snapshot ? TEXT("manifest hash or plan count differs") : UTF8_TO_TCHAR(CoreError.c_str())); + OutError = FString::Printf(TEXT("Core adapter did not generate an owner world assembly: %s"), + Snapshot ? TEXT("world assembly is missing") : UTF8_TO_TCHAR(CoreError.c_str())); return false; } - const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1& NativeIslandPlan = - ParsedAssembly->floatingIslands.front(); - if (NativeIslandPlan.strateIndex != 1 || NativeIslandPlan.manifest.plan.islands.size() != 4u + const VoxelForgeCore::WorldAssemblyManifest& NativeAssembly = *Snapshot->worldAssembly(); + const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* NativeIslandPlanPtr = nullptr; + for (const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1& Plan : NativeAssembly.floatingIslands) + if (Plan.strateIndex == IslandStrateIndex) NativeIslandPlanPtr = &Plan; + if (NativeIslandPlanPtr == nullptr + || NativeIslandPlanPtr->topChunkZ != IslandSlot->TopChunkZ + || NativeIslandPlanPtr->bottomChunkZ != IslandSlot->BottomChunkZ) + { + OutError = TEXT("Core did not generate a floating-island plan for the converted owner slot at its original heights."); + return false; + } + const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1 NativeIslandPlan = *NativeIslandPlanPtr; + if (NativeIslandPlan.strateIndex != IslandStrateIndex + || NativeIslandPlan.manifest.plan.islands.empty() + || NativeIslandPlan.manifest.plan.clusterEdges.empty() || NativeIslandPlan.manifest.plan.route.size() < 2u) { - OutError = TEXT("Core adapter island plan has the wrong strate, island count, or route."); + OutError = TEXT("Core generated island plan has the wrong owner strate or route."); return false; } VoxelForgeCore::FloatingIslandFieldV1 RouteField( @@ -14780,7 +15281,7 @@ bool RunCoreFloatingIslandActorIntegrationTest( VoxelForgeCore::AuditFloatingIslandThickness(NativeIslandPlan.manifest.plan); if (!RouteField.isValid() || !RouteAudit.passed || !ThicknessAudit.passed) { - OutError = TEXT("Imported Core island field failed route clearance or mass-thickness validation."); + OutError = TEXT("Generated Core island field failed route clearance or mass-thickness validation."); return false; } const VoxelForgeCore::WorldAssemblyRoutePointV1& StartRoute = @@ -14793,15 +15294,16 @@ bool RunCoreFloatingIslandActorIntegrationTest( static_cast(StartRoute.positionSubvoxels[2]) / SubvoxelsPerCoreVoxel); const FVector CameraLocalCm = CameraVoxels * CoreVoxelSizeCm; UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeCoreIslandTest] manifest_import=PASS hash=%s recipe_hash=%016llx plan_hash=%016llx scale_owner_cm=12.5 scale_core_cm=25 islands=%d connectors=%d route_segments=%llu route_clearance_subvoxels=%lld water_fixed=%d water_relative=%.6f"), - *ImportedAssemblyHash, + TEXT("[VoxelForgeCoreIslandTest] plan_generated=PASS hash=%016llx recipe_hash=%016llx plan_hash=%016llx seed=%d strate=%d owner_bounds=[%d,%d] scale_owner_cm=12.5 scale_core_cm=25 islands=%d connectors=%d route_segments=%llu route_clearance_subvoxels=%lld water_fixed=%d"), + static_cast(NativeAssembly.contentHash), static_cast(NativeIslandPlan.manifest.recipe.contentHash), static_cast(NativeIslandPlan.manifest.plan.contentHash), + EffectiveSeed, IslandStrateIndex, IslandSlot->TopChunkZ, IslandSlot->BottomChunkZ, static_cast(NativeIslandPlan.manifest.plan.islands.size()), static_cast(NativeIslandPlan.manifest.plan.clusterEdges.size()), static_cast(RouteAudit.segmentsTested), static_cast(RouteAudit.minimumClearanceSubvoxels), - bFixedWaterEnabled ? 1 : 0, FixedWaterRelative); + IslandSlot->Definition->bHasWater ? 1 : 0); // Generate the standalone reference before actor startup so two full Core worlds do not overlap. // Generer la reference Standalone avant l'acteur pour ne pas garder deux mondes Core complets. @@ -15538,9 +16040,9 @@ bool RunCoreFloatingIslandActorIntegrationTest( float IslandContentGroundCm = 0.0f; float SpanContentGroundCm = 0.0f; if (Actor->ContentManager == nullptr - || !Actor->ContentManager->QueryNativeFloatingIslandAnchor(1, + || !Actor->ContentManager->QueryNativeFloatingIslandAnchor(IslandStrateIndex, FVector2D(IslandTopXY.X, IslandTopXY.Y), IslandContentGroundCm) - || !Actor->ContentManager->QueryNativeFloatingIslandAnchor(1, + || !Actor->ContentManager->QueryNativeFloatingIslandAnchor(IslandStrateIndex, FVector2D(SpanXY.X, SpanXY.Y), SpanContentGroundCm)) { OutError = TEXT("Core island content could not resolve the island-top and connector anchors."); @@ -15582,25 +16084,26 @@ bool RunCoreFloatingIslandActorIntegrationTest( const int32 IslandStrate = Actor->GetStrateAtPosition(IslandGroundWorld); const int32 SpanStrate = Actor->GetStrateAtPosition(SpanGroundWorld); - if (IslandStrate != 1 || SpanStrate != 1 || Actor->AtmosphereManager == nullptr) + if (IslandStrate != IslandStrateIndex || SpanStrate != IslandStrateIndex + || Actor->AtmosphereManager == nullptr) { - OutError = FString::Printf(TEXT("Island top/span resolved to wrong strate: island=%d span=%d atmosphere=%d."), - IslandStrate, SpanStrate, Actor->AtmosphereManager ? 1 : 0); + OutError = FString::Printf(TEXT("Island top/span resolved to wrong strate: expected=%d island=%d span=%d atmosphere=%d."), + IslandStrateIndex, IslandStrate, SpanStrate, Actor->AtmosphereManager ? 1 : 0); return false; } Actor->AtmosphereManager->UpdateForPlayer( Actor->GetActorTransform().TransformPosition(CameraLocalCm)); const int32 AtmosphereStrate = Actor->AtmosphereManager->GetCurrentStrateIndex(); - if (AtmosphereStrate != 1) + if (AtmosphereStrate != IslandStrateIndex) { - OutError = FString::Printf(TEXT("Island route selected atmosphere strate %d instead of 1."), - AtmosphereStrate); + OutError = FString::Printf(TEXT("Island route selected atmosphere strate %d instead of %d."), + AtmosphereStrate, IslandStrateIndex); return false; } bool bContentHasWater = false; float ContentWaterLocalZCm = -FLT_MAX; if (!Actor->ContentManager->QueryStrateWaterAnchor( - 1, bContentHasWater, ContentWaterLocalZCm)) + IslandStrateIndex, bContentHasWater, ContentWaterLocalZCm)) { OutError = TEXT("Core island content did not return the fixed strate water datum."); return false; @@ -15667,57 +16170,8 @@ bool RunCoreFloatingIslandActorIntegrationTest( return false; } - // Let the real async decoration pipeline place a transient tree mesh, then prove each instance - // sits on the Core surface and the neighboring air/rock signs are correct. - const bool bTreesReady = TickWorldUntil(90.0, [&]() - { - TArray Components; - Actor->GetComponents(Components); - for (const UHierarchicalInstancedStaticMeshComponent* Component : Components) - if (Component != nullptr && Component->GetStaticMesh() == ProbeTreeMesh - && Component->GetInstanceCount() > 0) return true; - return false; - }); - if (!bTreesReady) - { - OutError = TEXT("Core island test did not stream the transient tree decoration onto the route island."); - return false; - } - TArray TreeComponents; - Actor->GetComponents(TreeComponents); - int32 TreeInstances = 0; - double MaximumTreeSurfaceDensity = 0.0; - for (const UHierarchicalInstancedStaticMeshComponent* Component : TreeComponents) - { - if (Component == nullptr || Component->GetStaticMesh() != ProbeTreeMesh) continue; - for (int32 InstanceIndex = 0; InstanceIndex < Component->GetInstanceCount(); ++InstanceIndex) - { - FTransform InstanceTransform; - if (!Component->GetInstanceTransform(InstanceIndex, InstanceTransform, true)) continue; - const FVector InstanceWorld = InstanceTransform.GetLocation(); - float AtSurface = 0.0f, BelowSurface = 0.0f, AboveSurface = 0.0f; - if (!Actor->QueryVoxelForgeCoreDensityAt(InstanceWorld, AtSurface) - || !Actor->QueryVoxelForgeCoreDensityAt(InstanceWorld - FVector(0, 0, 25), BelowSurface) - || !Actor->QueryVoxelForgeCoreDensityAt(InstanceWorld + FVector(0, 0, 25), AboveSurface) - || FMath::Abs(AtSurface) > 0.75f || BelowSurface >= 0.0f || AboveSurface <= 0.0f) - { - OutError = FString::Printf(TEXT("Transient island tree %d is not anchored at an air-to-rock Core crossing."), - TreeInstances); - return false; - } - MaximumTreeSurfaceDensity = FMath::Max(MaximumTreeSurfaceDensity, - static_cast(FMath::Abs(AtSurface))); - ++TreeInstances; - } - } - if (TreeInstances == 0) - { - OutError = TEXT("Core island transient tree validation found no instances."); - return false; - } UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeCoreIslandTest] content_anchor=PASS tree_instances=%d maximum_surface_density=%.5f no_floaters=1"), - TreeInstances, MaximumTreeSurfaceDensity); + TEXT("[VoxelForgeCoreIslandTest] content_anchor=PASS source=native_island_field anchors=island_top,connector_span terrain_agreement=PASS")); FVector CarveLocal(IslandTopXY.X, IslandTopXY.Y, IslandContentGroundCm - 40.0f); const FVector CarveWorld = Actor->GetActorTransform().TransformPosition(CarveLocal); @@ -15787,6 +16241,18 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin { return RunCoreFloatingIslandActorIntegrationTest(Arguments, OutError); } + if (Arguments.bCorePlanOnlyTest) + { + return RunCorePlanOnlyTest(Arguments, OutError); + } + if (Arguments.bCoreFallbackTest) + { + return RunCoreFallbackActorTest(Arguments, OutError); + } + if (Arguments.bCoreOffTest) + { + return RunCoreOffActorTest(Arguments, OutError); + } UVoxelSettings* AuthoredSettings = LoadObject( nullptr, *Arguments.SettingsReference); if (!AuthoredSettings) @@ -15804,6 +16270,16 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin return false; } + const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings); + TStrongObjectPtr OwnerSurfaceSettings; + TStrongObjectPtr OwnerSurfaceDefinition; + if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed, + OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError)) + { + return false; + } + AuthoredSettings = OwnerSurfaceSettings.Get(); + if (Arguments.bCoreHybridFixturesOnly) { UVoxelGenerator* FixtureGenerator = NewObject( @@ -15932,7 +16408,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin VoxelForgeCore::TileRegionCompileStats SurfaceTravelCompileStats; const VoxelForgeCore::Vec3d TravelCameraVoxels{-156.75164794921875, 25.093801498413086, -823.67364501953125}; - const VoxelForgeCore::Vec3d SurfaceCameraVoxels{37.0, 165.0, -2011.0}; + VoxelForgeCore::Vec3d SurfaceCameraVoxels{}; + int32 SurfaceStrateIndex = INDEX_NONE; uint32 NativeSurfaceBiomeId = 0; int32 NativeBiomeIdsSampled = 0; int32 NativeBiomeSampleCount = 0; @@ -15960,35 +16437,62 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin *AssemblyValidationError); return false; } + int32 ExpectedSurfacePlanCount = 0; + int32 ExpectedIslandPlanCount = 0; + for (const FStrateSlot& Slot : Manager->GetLayout()) + { + if (Slot.Definition == nullptr) continue; + const ECaveGeneratorType Type = Manager->GetGeneratorTypeForChunk( + FIntVector(0, 0, Slot.TopChunkZ)); + ExpectedSurfacePlanCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0; + ExpectedIslandPlanCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0; + if (SurfaceStrateIndex == INDEX_NONE && Type == ECaveGeneratorType::SurfaceWorld) + SurfaceStrateIndex = Slot.StrateIndex; + } + const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly(); + if (Assembly == nullptr + || Assembly->plan.strates.size() != static_cast(ExpectedSurfacePlanCount) + || Assembly->floatingIslands.size() != static_cast(ExpectedIslandPlanCount)) + { + OutError = FString::Printf( + TEXT("Core runtime adapter planned SurfaceWorld/island counts %d/%d, expected %d/%d from the resolved owner layout."), + Assembly ? static_cast(Assembly->plan.strates.size()) : 0, + Assembly ? static_cast(Assembly->floatingIslands.size()) : 0, + ExpectedSurfacePlanCount, ExpectedIslandPlanCount); + return false; + } if (!ExpectedAssemblyHash.IsEmpty()) { - const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly(); - const FString SnapshotHash = Assembly == nullptr ? FString() : FString::Printf( + const FString SnapshotHash = FString::Printf( TEXT("%016llx"), static_cast(Assembly->contentHash)); - if (Assembly == nullptr || !SnapshotHash.Equals(ExpectedAssemblyHash, ESearchCase::IgnoreCase)) + if (!SnapshotHash.Equals(ExpectedAssemblyHash, ESearchCase::IgnoreCase)) { OutError = TEXT("Core runtime adapter did not preserve the settings world assembly manifest."); return false; } - UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeCoreWorldTest] world_assembly_hash=%s plans=%d native=1"), - *SnapshotHash, static_cast(Assembly->plan.strates.size())); } - else if (Snapshot->worldAssembly() != nullptr) + else if (!ExpectedSurfacePlanCount && !ExpectedIslandPlanCount) { - OutError = TEXT("Core runtime adapter unexpectedly attached a world assembly manifest."); + OutError = TEXT("The owner layout has no SurfaceWorld or FloatingIslands plan for this cross-system test."); return false; } + const FString SnapshotHash = FString::Printf(TEXT("%016llx"), + static_cast(Assembly->contentHash)); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreWorldTest] world_assembly_source=%s world_assembly_hash=%s surface_strates=%d island_strates=%d native=1"), + ExpectedAssemblyHash.IsEmpty() ? TEXT("generated_at_startup") : TEXT("settings_override"), + *SnapshotHash, static_cast(Assembly->plan.strates.size()), + static_cast(Assembly->floatingIslands.size())); // Validate the native Core field's biome assignment, not the legacy decoration query. // Valider les biomes du champ Core natif, pas la requete historique des decorations. - const VoxelForgeCore::WorldAssemblyManifest* NativeAssembly = Snapshot->worldAssembly(); + const VoxelForgeCore::WorldAssemblyManifest* NativeAssembly = Assembly; const VoxelForgeCore::WorldAssemblyStratumPlanV1* NativeSurfacePlan = nullptr; if (NativeAssembly != nullptr) { for (const VoxelForgeCore::WorldAssemblyStratumPlanV1& Plan : NativeAssembly->plan.strates) { - if (Plan.strateIndex == 1) + if (Plan.strateIndex == SurfaceStrateIndex) { NativeSurfacePlan = &Plan; break; @@ -15997,7 +16501,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin } if (NativeAssembly == nullptr || NativeSurfacePlan == nullptr) { - OutError = TEXT("Core runtime snapshot has no native biome plan for SurfaceWorld strate 1."); + OutError = FString::Printf(TEXT("Core runtime snapshot has no native biome plan for owner SurfaceWorld strate %d."), + SurfaceStrateIndex); return false; } const VoxelForgeCore::Heightfield2D NativeSurfaceField( @@ -16008,6 +16513,15 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin return false; } const int64 SubvoxelsPerVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + if (NativeSurfacePlan->route.empty()) + { + OutError = TEXT("Generated owner SurfaceWorld plan has no walkable route point."); + return false; + } + SurfaceCameraVoxels = VoxelForgeCore::Vec3d{ + static_cast(NativeSurfacePlan->route.front().positionSubvoxels[0]) / SubvoxelsPerVoxel, + static_cast(NativeSurfacePlan->route.front().positionSubvoxels[1]) / SubvoxelsPerVoxel, + static_cast(NativeSurfacePlan->route.front().positionSubvoxels[2]) / SubvoxelsPerVoxel}; const VoxelForgeCore::HeightfieldPointSample SurfaceBiomeSample = NativeSurfaceField.point( static_cast(std::llround(SurfaceCameraVoxels.x * SubvoxelsPerVoxel)), static_cast(std::llround(SurfaceCameraVoxels.y * SubvoxelsPerVoxel)), 0); @@ -16267,7 +16781,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin static_cast(InitialAggregates[2]), static_cast(InitialAggregates[3])); - if (!Manager->GetLayout().IsEmpty() && Manager->GetLayout()[0].Definition != nullptr) + if (!Manager->GetLayout().IsEmpty() && Manager->GetLayout()[0].Definition != nullptr + && Manager->GetGeneratorTypeForChunk(FIntVector( + 0, 0, Manager->GetLayout()[0].TopChunkZ)) != ECaveGeneratorType::SurfaceWorld) { const FStrateSlot& TopSlot = Manager->GetLayout()[0]; const FIntVector MidChunk(0, 0, (TopSlot.TopChunkZ + TopSlot.BottomChunkZ) / 2); @@ -16316,6 +16832,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin return false; } } + else if (!Manager->GetLayout().IsEmpty() + && Manager->GetLayout()[0].Definition != nullptr + && Manager->GetGeneratorTypeForChunk(FIntVector( + 0, 0, Manager->GetLayout()[0].TopChunkZ)) == ECaveGeneratorType::SurfaceWorld) + { + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreWorldTest] origin_landing=SKIP reason=surface_world_uses_native_walkable_route")); + } if (!bFoundCarveVertex) { OutError = TEXT("Standalone Core reference has no level-0 surface vertex for the carve fixture."); @@ -17207,7 +17731,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin float ContentGroundLocalZ = 0.0f; bool bContentHasWater = false; float ContentWaterLocalZ = -FLT_MAX; - if (ResolvedStrateIndex != 1 + if (ResolvedStrateIndex != SurfaceStrateIndex || !Actor->ContentManager->QueryNativeSurfaceAnchors(ResolvedStrateIndex, FVector2D(SurfaceLocalPosition.X, SurfaceLocalPosition.Y), ContentGroundLocalZ, bContentHasWater, ContentWaterLocalZ)) diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 6480bfa..400c943 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -254,6 +254,10 @@ namespace VoxelForgeCore std::int32_t strateIndex = -1; std::int32_t topChunkZ = 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 capUndulationSubvoxels = 0; std::int64_t capWavelengthSubvoxels = 256 * 512; diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index ced1443..c70487b 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -342,6 +342,11 @@ namespace world_assembly_detail HashWord(hash, static_cast(stratum.strateIndex), 4); HashWord(hash, static_cast(stratum.topChunkZ), 4); HashWord(hash, static_cast(stratum.bottomChunkZ), 4); + if (stratum.hasRecipeOverride) + { + HashWord(hash, 0x5354524154455250ull, 8); + HashWord(hash, HashRecipe(stratum.recipeOverride), 8); + } HashWord(hash, static_cast(stratum.capHeightSubvoxels), 8); HashWord(hash, static_cast(stratum.capUndulationSubvoxels), 8); HashWord(hash, static_cast(stratum.capWavelengthSubvoxels), 8); @@ -554,6 +559,65 @@ namespace world_assembly_detail 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, std::string& error) { @@ -573,6 +637,12 @@ namespace world_assembly_detail || !ReadHash(item.Get("content_hash"), stratum.contentHash) || !item.Get("features").IsArray() || !item.Get("route").IsArray()) { 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(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 VFJson::Value& field = item.Get(name); @@ -1402,6 +1472,13 @@ bool WorldAssemblyManifest::finalize(std::string& error) std::vector strateIds; 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 (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 expectedManifest = contentHash; std::vector expectedStrates; - for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.contentHash); + std::vector expectedStratumRecipes; + for (const auto& stratum : plan.strates) + { + expectedStrates.push_back(stratum.contentHash); + expectedStratumRecipes.push_back(stratum.hasRecipeOverride + ? stratum.recipeOverride.contentHash : 0); + } std::vector expectedIslandManifests; for (const auto& islands : floatingIslands) expectedIslandManifests.push_back(islands.manifest.contentHash); if (!copy.finalize(error)) return false; if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash || expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size() + || expectedStratumRecipes.size() != copy.plan.strates.size() || expectedIslandManifests.size() != copy.floatingIslands.size()) { 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) - 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; } for (std::size_t i = 0; i < expectedIslandManifests.size(); ++i) if (expectedIslandManifests[i] != copy.floatingIslands[i].manifest.contentHash) @@ -1485,62 +1571,8 @@ std::string WorldAssemblyManifest::ToJson() const std::ostringstream out; out << "{\"schema_version\":" << value.schemaVersion << ",\"content_hash\":\"" << world_assembly_detail::HashText(value.contentHash) - << "\",\"recipe\":{\"schema_version\":" << value.recipe.schemaVersion - << ",\"evaluator_version\":" << value.recipe.evaluatorVersion - << ",\"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 + << "\",\"recipe\":" << world_assembly_detail::RecipeJson(value.recipe) + << ",\"plan\":{\"schema_version\":" << value.plan.schemaVersion << ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash) << "\",\"strates\":["; 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 << ",\"water_surface_q\":" << stratum.waterSurfaceSubvoxels << ",\"water_enabled\":" << (stratum.waterEnabled ? "true" : "false"); + if (stratum.hasRecipeOverride) + out << ",\"recipe_override\":" + << world_assembly_detail::RecipeJson(stratum.recipeOverride); out << ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash) << "\",\"features\":["; @@ -1626,7 +1661,7 @@ std::string WorldAssemblyManifest::ToJson() const Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe, const WorldAssemblyStratumPlanV1& plan) noexcept - : recipe_(&recipe), plan_(&plan) + : recipe_(plan.hasRecipeOverride ? &plan.recipeOverride : &recipe), plan_(&plan) {} bool Heightfield2D::isValid() const noexcept diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index 09f57eb..293b86e 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -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 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. diff --git a/Standalone/Research/WorldAssemblerM3/Round4/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round4/REPORT.md new file mode 100644 index 0000000..95cee2a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round4/REPORT.md @@ -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. diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/audit.json b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/audit.json new file mode 100644 index 0000000..a2d9b08 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/audit.json @@ -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]] +} diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/cluster_legacy.vfmesh b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/cluster_legacy.vfmesh new file mode 100644 index 0000000..05e1d87 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/cluster_legacy.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/cluster_native.vfmesh b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/cluster_native.vfmesh new file mode 100644 index 0000000..31fcb88 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/cluster_native.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/detail_legacy.vfmesh b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/detail_legacy.vfmesh new file mode 100644 index 0000000..e12a387 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/detail_legacy.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/detail_native.vfmesh b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/detail_native.vfmesh new file mode 100644 index 0000000..aba4f7e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/detail_native.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/floating_island_manifest.json b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/floating_island_manifest.json new file mode 100644 index 0000000..88ee29f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/floating_island_manifest.json @@ -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":"3ea33fae0f75b9e0","position_q":[12595,3208,-242473],"radius_q":1024},{"feature_id":"38a8dfe9adb37e83","position_q":[13312,3912,-242440],"radius_q":1024},{"feature_id":"dfde44af59268bf2","position_q":[14029,4616,-242419],"radius_q":1024},{"feature_id":"0378552bc60e49dd","position_q":[14745,5321,-242573],"radius_q":1024},{"feature_id":"40dd5b33f799a194","position_q":[15462,6025,-242552],"radius_q":1024},{"feature_id":"fc2c5b613f6a5cd7","position_q":[16179,6729,-242521],"radius_q":1024}]}} diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/run_ledger.txt b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/run_ledger.txt new file mode 100644 index 0000000..6df7ae8 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/run_ledger.txt @@ -0,0 +1 @@ +attempt=1 status=passed elapsed_s=1.66 peak_private_MiB=154.4 exit=0 diff --git a/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/world_assembly_floating_islands.json b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/world_assembly_floating_islands.json new file mode 100644 index 0000000..1b92aae --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round4/results/xy_cache_audit/world_assembly_floating_islands.json @@ -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":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":"3ea33fae0f75b9e0","position_q":[12595,3208,-242473],"radius_q":1024},{"feature_id":"38a8dfe9adb37e83","position_q":[13312,3912,-242440],"radius_q":1024},{"feature_id":"dfde44af59268bf2","position_q":[14029,4616,-242419],"radius_q":1024},{"feature_id":"0378552bc60e49dd","position_q":[14745,5321,-242573],"radius_q":1024},{"feature_id":"40dd5b33f799a194","position_q":[15462,6025,-242552],"radius_q":1024},{"feature_id":"fc2c5b613f6a5cd7","position_q":[16179,6729,-242521],"radius_q":1024}]}}}]} diff --git a/Tools/BuildVoxelForgeCoreHost.ps1 b/Tools/BuildVoxelForgeCoreHost.ps1 index aa314fc..739fa62 100644 --- a/Tools/BuildVoxelForgeCoreHost.ps1 +++ b/Tools/BuildVoxelForgeCoreHost.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [ValidateRange(1, 16)][int]$MaxParallelActions = 16, + [ValidateRange(1, 4)][int]$MaxParallelActions = 4, [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) { if (-not (Test-Path -LiteralPath $Source -PathType Container)) { 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. $hostAssetRoot = Join-Path $HostRoot 'Content\VoxelForge' $expectedAssetHashes = @{ - 'DA_Settings' = '5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7' - 'DA_Strate1' = 'CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9' + 'DA_Settings' = '8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454' + 'DA_Strate1' = '76063D691D5CE26D2014B587E1972A6724787BE1204E4652CA863BB8D5001BD0' 'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2' 'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C' - 'DA_Strate4' = '279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656' + 'DA_Strate4' = '7C0DE7D06876035AC7BC4F117B9166AFD77402021843BA4900B470F4B0D0185B' } foreach ($assetName in $expectedAssetHashes.Keys) { $stagedAsset = Join-Path $hostAssetRoot "$assetName.uasset" @@ -105,7 +141,7 @@ foreach ($assetName in $expectedAssetHashes.Keys) { } $stagedHash = (Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash 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' } $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." + 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 ` -RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr ` -WindowStyle Hidden -PassThru diff --git a/Tools/TestVoxelForgeCoreWorldHost.ps1 b/Tools/TestVoxelForgeCoreWorldHost.ps1 index 33ac887..df07cf6 100644 --- a/Tools/TestVoxelForgeCoreWorldHost.ps1 +++ b/Tools/TestVoxelForgeCoreWorldHost.ps1 @@ -6,7 +6,12 @@ param( [switch]$EnableHybridFixturePlacement, [switch]$HybridCliffOnly, [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. @@ -29,6 +34,25 @@ $ZenData = Join-Path $OutputRoot 'ZenData' $EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' $MemoryLimitBytes = 8GB $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) { $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') -Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved') -if ([string]::IsNullOrWhiteSpace($WorldAssemblyManifestPath)) { - $WorldAssemblyManifestPath = if ($FloatingIslandsOnly) { - Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM3\Round3\results\world_assembly_floating_islands.json' - } else { - Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM1\round4\world_assembly.json' +if (-not ('VoxelForgeRound4MemoryProbe' -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public static class VoxelForgeRound4MemoryProbe { + [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; } } -$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)) { if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { 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" } +if ($MismatchedManifestFallback) { $RunName = 'mismatched_fallback' } + $foreignEditors = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like 'UnrealEditor*' }) if ($foreignEditors.Count -gt 0) { @@ -67,11 +130,11 @@ if ($foreignEditors.Count -gt 0) { } $ExpectedAssetHashes = @{ - 'DA_Settings' = '5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7' - 'DA_Strate1' = 'CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9' + 'DA_Settings' = '8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454' + 'DA_Strate1' = '76063D691D5CE26D2014B587E1972A6724787BE1204E4652CA863BB8D5001BD0' 'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2' 'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C' - 'DA_Strate4' = '279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656' + 'DA_Strate4' = '7C0DE7D06876035AC7BC4F117B9166AFD77402021843BA4900B470F4B0D0185B' } foreach ($assetName in $ExpectedAssetHashes.Keys) { $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" } 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)) { [IO.Directory]::CreateDirectory($directory) | Out-Null } -Copy-Item -LiteralPath $WorldAssemblyManifestPath -Destination $StagedWorldAssembly -Force -$ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom-Json -if ($FloatingIslandsOnly) { - if ($ManifestData.content_hash -ne '94b563f1ce622def' -or - @($ManifestData.floating_islands).Count -ne 1 -or - @($ManifestData.plan.strates).Count -ne 0) { - throw "BuildHost world assembly input is not the reviewed 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 ($HasManifestOverride) { + Copy-Item -LiteralPath $WorldAssemblyManifestPath -Destination $StagedWorldAssembly -Force + $ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom-Json +} else { + $ManifestData = $null +} +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)" } if ($EnableHybridFixturePlacement -and $CoreMesherRecipe -eq 'MarchingCubesV1') { throw 'Hybrid fixture placement requires a WorldAssemblerHybrid recipe.' @@ -113,9 +174,8 @@ if ($HybridFixturesOnly -and $CoreMesherRecipe -eq 'MarchingCubesV1') { if ($HybridFixturesOnly -and ($HybridCliffOnly -or $EnableHybridFixturePlacement)) { throw '-HybridFixturesOnly cannot be combined with the other focused hybrid modes.' } -if ($FloatingIslandsOnly -and ($CoreMesherRecipe -ne 'MarchingCubesV1' -or - $EnableHybridFixturePlacement -or $HybridCliffOnly -or $HybridFixturesOnly)) { - throw '-FloatingIslandsOnly uses the reviewed MC island manifest and cannot be combined with hybrid modes.' +if (($FloatingIslandsOnly -or $CoreFallbackTest -or $CoreOffTest -or $CorePlanOnly) -and ($CoreMesherRecipe -ne 'MarchingCubesV1' -or $EnableHybridFixturePlacement -or $HybridCliffOnly -or $HybridFixturesOnly)) { + throw 'Core world integration modes use Marching Cubes and cannot be combined with hybrid modes.' } $CrashBefore = @{} @@ -150,6 +210,9 @@ $HybridFixturePlacementFlag = if ($EnableHybridFixturePlacement) { 1 } else { 0 $HybridCliffOnlyFlag = if ($HybridCliffOnly) { 1 } else { 0 } $HybridFixturesOnlyFlag = if ($HybridFixturesOnly) { 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 = @( ('-project="{0}"' -f $HostProject), '-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound', @@ -161,17 +224,27 @@ $BaseArguments = @( '-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0', '-modes=export', '-gameconfig=1', '-coreworldtest=1', ("-coreislandtest=$FloatingIslandsOnlyFlag"), + ("-corefallbacktest=$CoreFallbackTestFlag"), + ("-coreofftest=$CoreOffTestFlag"), + ("-coreplantest=$CorePlanOnlyFlag"), '-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings', - ('-coreworldassembly="{0}"' -f $StagedWorldAssembly), ("-corehybridmesher=$HybridMesherFlag"), ("-corehybridfixtureplacement=$HybridFixturePlacementFlag"), ("-corehybridcliffonly=$HybridCliffOnlyFlag"), ("-corehybridfixturesonly=$HybridFixturesOnlyFlag"), ('-out="{0}"' -f $OutputDirectory) ) -if ($FloatingIslandsOnly) { - # Keep this stationary route fixture out of the movement gate; island/span cooked contacts are probed directly. - # Garder ce fixture immobile hors du gate de mouvement ; les contacts cuisines ile/pont sont verifies directement. +if ($HasManifestOverride) { + $BaseArguments += ('-coreworldassembly="{0}"' -f $StagedWorldAssembly) +} +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' } @@ -181,19 +254,26 @@ try { $Succeeded = $false for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) { if ($Attempt -eq 1) { - $LogFileName = 'CoreWorldValidation.log' - $StdoutFileName = 'CoreWorldValidation.stdout.log' - $StderrFileName = 'CoreWorldValidation.stderr.log' + $LogFileName = "CoreWorldValidation-$RunName.log" + $StdoutFileName = "CoreWorldValidation-$RunName.stdout.log" + $StderrFileName = "CoreWorldValidation-$RunName.stderr.log" } else { - $LogFileName = 'CoreWorldValidation.retry.log' - $StdoutFileName = 'CoreWorldValidation.retry.stdout.log' - $StderrFileName = 'CoreWorldValidation.retry.stderr.log' + $LogFileName = "CoreWorldValidation-$RunName.retry.log" + $StdoutFileName = "CoreWorldValidation-$RunName.retry.stdout.log" + $StderrFileName = "CoreWorldValidation-$RunName.retry.stderr.log" } $LogPath = Join-Path $LogDirectory $LogFileName $StdoutPath = Join-Path $LogDirectory $StdoutFileName $StderrPath = Join-Path $LogDirectory $StderrFileName $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 ` -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath ` -WindowStyle Hidden -PassThru @@ -228,6 +308,17 @@ try { $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) { Write-Warning 'BuildHost Core actor commandlet exited with 0xC0000142; retrying once.' 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)) { - $FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log' + $FinalLog = Join-Path $LogDirectory "CoreWorldValidation-$RunName.retry.log" } $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) { foreach ($Marker in @( - '[VoxelForgeCoreIslandTest] manifest_import=PASS', + '[VoxelForgeCoreIslandTest] plan_generated=PASS', '[VoxelForgeCoreIslandTest] standalone_reference=PASS', '[VoxelForgeCoreIslandTest] worker_hash_matrix=1/8/16 stable=1', '[VoxelForgeCoreIslandTest] pass phase=initial', @@ -286,7 +424,7 @@ if ($FloatingIslandsOnly) { if ($NewCrashes.Count -gt 0) { 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 "Peak commandlet private bytes sampled: $PeakPrivateBytes" 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') { throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog." } -$ExpectedRuntimeAssemblyHash = $ManifestData.content_hash +$ExpectedRuntimeAssemblyHash = '' if ($EnableHybridFixturePlacement) { $FixtureHashMatch = [regex]::Match($Text, '\[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 } -$AssemblyMarker = '\[VoxelForgeCoreWorldTest\] world_assembly_hash=' + - [regex]::Escape($ExpectedRuntimeAssemblyHash) + ' plans=1 native=1' +if ($EnableHybridFixturePlacement) { + $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) { - 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=' + [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]') { 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." } foreach ($Marker in @(