wip(assembler): M3 round 4 -- plans built inside Unreal from the owner's strates; no import needed
SurfaceWorld and FloatingIslands plans generate at startup from the resolved seed, strate types and heights (hashed, cached). Imported JSON is an opt-in override (off by default); an invalid override shows an on-screen notice and falls back to legacy. BuildHost on staged owner assets: SurfaceWorld cross-system checks, islands at owner height (route, collision, anchors, BP edits), seed 137501, W1/W8/W16, Core-off legacy. XY-column cache verified (hashes kept); island LOD0 p95 91 ms vs 133 ms sprint deadline. Not exercised: season-seed path (no Season in staged settings), rendered notice pixels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
48fa028086
commit
111cf3d262
21 files changed
+1934
-391
No files matched your search
@@ -283,8 +283,14 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
if (Settings)
|
||||
{
|
||||
FString ManifestJson, ManifestHash, ManifestError;
|
||||
if (Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError)
|
||||
&& !ManifestJson.IsEmpty())
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(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;
|
||||
|
||||
@@ -9,16 +9,477 @@
|
||||
#include "UObject/TextProperty.h"
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelBiomeDefinition.h"
|
||||
#include "VoxelBiomeTypes.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
using FCoreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
|
||||
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::int64_t>(std::llround(
|
||||
Voxels * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyQ16(double Value)
|
||||
{
|
||||
return static_cast<std::int64_t>(std::llround(Value * 65536.0));
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyWavelength(double Frequency, double FallbackVoxels)
|
||||
{
|
||||
const double Wavelength = FMath::IsFinite(Frequency) && Frequency > 0.0
|
||||
? 1.0 / Frequency : FallbackVoxels;
|
||||
return std::max<std::int64_t>(1, WorldAssemblyFixed(Wavelength));
|
||||
}
|
||||
|
||||
std::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
|
||||
std::int64_t Y, std::uint32_t Kind)
|
||||
{
|
||||
std::uint64_t Hash = 14695981039346656037ull;
|
||||
const auto Add = [&Hash](std::uint64_t Value, int Bytes)
|
||||
{
|
||||
for (int Index = 0; Index < Bytes; ++Index)
|
||||
{
|
||||
Hash ^= static_cast<std::uint8_t>(Value >> (Index * 8));
|
||||
Hash *= 1099511628211ull;
|
||||
}
|
||||
};
|
||||
Add(Seed, 4);
|
||||
Add(static_cast<std::uint64_t>(X), 8);
|
||||
Add(static_cast<std::uint64_t>(Y), 8);
|
||||
Add(Kind, 4);
|
||||
return Hash == 0 ? 1 : Hash;
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyRoundRatio(std::int64_t Numerator, std::int64_t Denominator)
|
||||
{
|
||||
if (Numerator >= 0) return (Numerator + Denominator / 2) / Denominator;
|
||||
return -((-Numerator + Denominator / 2) / Denominator);
|
||||
}
|
||||
|
||||
std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
|
||||
{
|
||||
std::uint64_t Root = 0;
|
||||
std::uint64_t Bit = 1ull << 62;
|
||||
while (Bit > Value) Bit >>= 2;
|
||||
while (Bit != 0)
|
||||
{
|
||||
if (Value >= Root + Bit)
|
||||
{
|
||||
Value -= Root + Bit;
|
||||
Root = (Root >> 1) + Bit;
|
||||
}
|
||||
else
|
||||
{
|
||||
Root >>= 1;
|
||||
}
|
||||
Bit >>= 2;
|
||||
}
|
||||
const std::uint64_t OriginalValue = Value + Root * Root;
|
||||
if (4 * OriginalValue >= (2 * Root + 1) * (2 * Root + 1)) ++Root;
|
||||
return Root;
|
||||
}
|
||||
|
||||
bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, std::string& Error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr std::int64_t PlayerRadiusQ = 348;
|
||||
constexpr std::int64_t PlayerHeightQ = 1802;
|
||||
constexpr std::int64_t HalfHeightQ = PlayerHeightQ / 2;
|
||||
constexpr std::int64_t StepQ = 128;
|
||||
constexpr std::int64_t ProbeRadiusQ = 174;
|
||||
constexpr std::int64_t SupportDepthQ = 256;
|
||||
constexpr std::int64_t GradeQ16 = 17560;
|
||||
constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel;
|
||||
struct FDirection { std::int32_t X; std::int32_t Y; };
|
||||
constexpr std::array<FDirection, 8> Directions{{
|
||||
{65536, 0}, {46341, 46341}, {0, 65536}, {-46341, 46341},
|
||||
{-65536, 0}, {-46341, -46341}, {0, -65536}, {46341, -46341}}};
|
||||
struct FCandidate
|
||||
{
|
||||
std::int64_t Score = 0;
|
||||
std::int32_t CenterX = 0;
|
||||
std::int32_t CenterY = 0;
|
||||
std::int32_t Direction = 0;
|
||||
std::vector<WorldAssemblyRoutePointV1> Points;
|
||||
};
|
||||
|
||||
auto& Constraints = Stratum.routeConstraints;
|
||||
Constraints.version = 1;
|
||||
Constraints.capsuleRadiusSubvoxels = PlayerRadiusQ;
|
||||
Constraints.capsuleHeightSubvoxels = PlayerHeightQ;
|
||||
Constraints.maximumGradeQ16 = GradeQ16;
|
||||
Constraints.sweepSpacingSubvoxels = StepQ;
|
||||
Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ;
|
||||
Constraints.supportSearchDepthSubvoxels = SupportDepthQ;
|
||||
Constraints.minimumClearanceSubvoxels = 8;
|
||||
Constraints.minimumSupportProbes = 3;
|
||||
|
||||
const Heightfield2D BaseField(Recipe, Stratum);
|
||||
if (!BaseField.isValid())
|
||||
{
|
||||
Error = "SurfaceWorld route planner received an invalid heightfield.";
|
||||
return false;
|
||||
}
|
||||
std::vector<FCandidate> Candidates;
|
||||
constexpr std::int32_t WaypointSpacing = 8;
|
||||
for (std::int32_t CenterY = -256; CenterY <= 256; CenterY += 64)
|
||||
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 64)
|
||||
for (std::int32_t DirectionIndex = 0;
|
||||
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
|
||||
{
|
||||
FCandidate Candidate;
|
||||
Candidate.CenterX = CenterX;
|
||||
Candidate.CenterY = CenterY;
|
||||
Candidate.Direction = DirectionIndex;
|
||||
Candidate.Points.reserve(33);
|
||||
std::int64_t PreviousGround = 0;
|
||||
std::int64_t PreviousX = 0;
|
||||
std::int64_t PreviousY = 0;
|
||||
bool bGradePass = true;
|
||||
for (std::int32_t Index = 0; Index <= 32; ++Index)
|
||||
{
|
||||
const std::int64_t Along = (Index - 16) * WaypointSpacing;
|
||||
const std::int64_t X = CenterX + WorldAssemblyRoundRatio(
|
||||
static_cast<std::int64_t>(Along) * Directions[DirectionIndex].X, 65536);
|
||||
const std::int64_t Y = CenterY + WorldAssemblyRoundRatio(
|
||||
static_cast<std::int64_t>(Along) * Directions[DirectionIndex].Y, 65536);
|
||||
const auto Column = BaseField.point(WorldAssemblyFixed(static_cast<double>(X)),
|
||||
WorldAssemblyFixed(static_cast<double>(Y)), 0);
|
||||
const std::int64_t DeltaX = X - PreviousX;
|
||||
const std::int64_t DeltaY = Y - PreviousY;
|
||||
const std::uint64_t HorizontalVoxels = WorldAssemblyRoundedSqrt(
|
||||
static_cast<std::uint64_t>(DeltaX * DeltaX + DeltaY * DeltaY));
|
||||
const std::int64_t HorizontalQ = static_cast<std::int64_t>(HorizontalVoxels)
|
||||
* WorldAssemblySubvoxelsPerVoxel;
|
||||
if (Index > 0)
|
||||
{
|
||||
const std::int64_t RiseQ = std::abs(Column.groundHeightSubvoxels - PreviousGround);
|
||||
if (HorizontalQ == 0 || RiseQ * 65536 > HorizontalQ * GradeQ16)
|
||||
{
|
||||
bGradePass = false;
|
||||
break;
|
||||
}
|
||||
Candidate.Score += RiseQ * 65536 / std::max<std::int64_t>(1, HorizontalQ);
|
||||
}
|
||||
WorldAssemblyRoutePointV1 Point;
|
||||
Point.positionSubvoxels = {WorldAssemblyFixed(static_cast<double>(X)),
|
||||
WorldAssemblyFixed(static_cast<double>(Y)),
|
||||
Column.groundHeightSubvoxels + HalfHeightQ};
|
||||
Point.radiusSubvoxels = RouteRadiusQ;
|
||||
Point.stableFeatureId = WorldAssemblyFeatureId(Recipe.seed, X, Y,
|
||||
0x52540000u + static_cast<std::uint32_t>(Index));
|
||||
Candidate.Points.push_back(Point);
|
||||
PreviousGround = Column.groundHeightSubvoxels;
|
||||
PreviousX = X;
|
||||
PreviousY = Y;
|
||||
}
|
||||
if (bGradePass) Candidates.push_back(std::move(Candidate));
|
||||
}
|
||||
std::stable_sort(Candidates.begin(), Candidates.end(), [](const FCandidate& A, const FCandidate& B)
|
||||
{
|
||||
if (A.Score != B.Score) return A.Score < B.Score;
|
||||
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
|
||||
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
|
||||
return A.Direction < B.Direction;
|
||||
});
|
||||
|
||||
for (const FCandidate& Candidate : Candidates)
|
||||
{
|
||||
Stratum.route = Candidate.Points;
|
||||
std::int64_t LiftQ = 0;
|
||||
constexpr std::int64_t RepairStepQ = 64;
|
||||
constexpr std::int64_t MaximumRepairLiftQ = SupportDepthQ - RepairStepQ;
|
||||
for (;;)
|
||||
{
|
||||
if (LiftQ > 0)
|
||||
{
|
||||
for (std::size_t PointIndex = 0; PointIndex < Stratum.route.size(); ++PointIndex)
|
||||
{
|
||||
Stratum.route[PointIndex].positionSubvoxels[2] =
|
||||
Candidate.Points[PointIndex].positionSubvoxels[2] + LiftQ;
|
||||
}
|
||||
}
|
||||
const Heightfield2D CandidateField(Recipe, Stratum);
|
||||
if (AuditHeightfieldRoute(CandidateField).passed) return true;
|
||||
if (LiftQ >= MaximumRepairLiftQ) break;
|
||||
LiftQ += RepairStepQ;
|
||||
}
|
||||
}
|
||||
Error = "SurfaceWorld route candidates failed the capsule and support sweep.";
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::WorldAssemblyBiomeProfileV1 BuildSurfaceProfile(
|
||||
std::uint32_t StableId, const FSurfaceGenerationParams& Params,
|
||||
const FBiomeResolved* Climate, double BottomVoxel, double HeightVoxels)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
WorldAssemblyBiomeProfileV1 Profile;
|
||||
Profile.stableId = StableId;
|
||||
Profile.baseHeightSubvoxels = WorldAssemblyFixed(BottomVoxel
|
||||
+ HeightVoxels * Params.BaseGroundRelative);
|
||||
Profile.continentAmplitudeSubvoxels = WorldAssemblyFixed(Params.ElevationRange);
|
||||
Profile.continentWavelengthSubvoxels = WorldAssemblyWavelength(Params.ContinentFrequency, 1000.0);
|
||||
Profile.mountainAmplitudeSubvoxels = WorldAssemblyFixed(Params.ElevationRange * Params.MountainStrength);
|
||||
Profile.mountainWavelengthSubvoxels = WorldAssemblyWavelength(Params.MountainFrequency, 500.0);
|
||||
Profile.detailAmplitudeSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness);
|
||||
Profile.detailWavelengthSubvoxels = WorldAssemblyWavelength(Params.DetailFrequency, 25.0);
|
||||
Profile.reliefMinimumQ16 = Climate ? WorldAssemblyQ16(Climate->ReliefMin) : 0;
|
||||
Profile.reliefMaximumQ16 = Climate ? WorldAssemblyQ16(Climate->ReliefMax) : 65536;
|
||||
Profile.moistureMinimumQ16 = Climate ? WorldAssemblyQ16(Climate->MoistureMin) : 0;
|
||||
Profile.moistureMaximumQ16 = Climate ? WorldAssemblyQ16(Climate->MoistureMax) : 65536;
|
||||
Profile.reliefStrengthQ16 = WorldAssemblyQ16(Params.ReliefStrength);
|
||||
Profile.reliefContrastQ16 = WorldAssemblyQ16(Params.ReliefContrast);
|
||||
Profile.reliefWavelengthSubvoxels = WorldAssemblyWavelength(Params.ReliefFrequency, 667.0);
|
||||
Profile.heightWarpStrengthSubvoxels = WorldAssemblyFixed(Params.HeightWarpStrength);
|
||||
Profile.heightWarpWavelengthSubvoxels = WorldAssemblyWavelength(Params.HeightWarpFrequency, 125.0);
|
||||
Profile.beachWidthSubvoxels = WorldAssemblyFixed(Params.BeachWidth);
|
||||
Profile.cliffStrengthQ16 = WorldAssemblyQ16(Params.CliffStrength);
|
||||
Profile.cliffSlopeThresholdQ16 = std::max<std::int64_t>(1, WorldAssemblyQ16(Params.CliffSlopeThreshold));
|
||||
Profile.cliffSharpnessQ16 = WorldAssemblyQ16(Params.CliffSharpness);
|
||||
Profile.cliffSampleDistanceSubvoxels = WorldAssemblyFixed(std::max(0.5f, Params.CliffSampleDist));
|
||||
const double StructuralAbsBound = static_cast<double>(Params.ElevationRange)
|
||||
+ static_cast<double>(Params.ElevationRange) * Params.MountainStrength
|
||||
+ 2.0 * Params.SurfaceRoughness;
|
||||
const double CliffAbsBound = 2.0 * StructuralAbsBound * Params.CliffStrength * Params.CliffSharpness;
|
||||
Profile.cliffMaximumDisplacementSubvoxels = WorldAssemblyFixed(Params.CliffStrength > 0.0f
|
||||
? std::max(1.0, CliffAbsBound) : 0.0);
|
||||
Profile.hasCapProfile = true;
|
||||
Profile.capHeightSubvoxels = WorldAssemblyFixed(BottomVoxel + HeightVoxels * Params.CeilingRelative);
|
||||
Profile.capWarpStrengthSubvoxels = WorldAssemblyFixed(Params.CeilingWarpStrength);
|
||||
Profile.capWarpWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingWarpFrequency, 100.0);
|
||||
Profile.capUndulationSubvoxels = WorldAssemblyFixed(Params.CeilingUndulation);
|
||||
Profile.capWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingUndulationFrequency, 500.0);
|
||||
Profile.capDetailAmplitudeSubvoxels = WorldAssemblyFixed(Params.CeilingRoughness);
|
||||
Profile.capDetailWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingRoughnessFrequency, 25.0);
|
||||
Profile.capRidgeAmplitudeSubvoxels = WorldAssemblyFixed(Params.CeilingRidgeStrength);
|
||||
Profile.capRidgeWavelengthSubvoxels = WorldAssemblyWavelength(Params.CeilingRidgeFrequency, 50.0);
|
||||
return Profile;
|
||||
}
|
||||
|
||||
bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
const FStrateSlot& Slot,
|
||||
VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||
const FSurfaceGenerationParams Surface = StrateManager->GetSurfaceParamsForChunk(Chunk);
|
||||
const FBiomeContext BiomeContext = StrateManager->GetBiomeContextForChunk(Chunk);
|
||||
const UVoxelStrateDefinition* Definition = Slot.Definition;
|
||||
const double BottomVoxel = static_cast<double>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const double HeightVoxels = static_cast<double>(Slot.TopChunkZ - Slot.BottomChunkZ + 1) * CHUNK_SIZE;
|
||||
const FBiomeMapParams& Map = BiomeContext.Map;
|
||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
Recipe.biomeCellSizeSubvoxels = WorldAssemblyFixed(Map.CellSize);
|
||||
Recipe.biomeWarpSubvoxels = WorldAssemblyFixed(Map.WarpStrength);
|
||||
Recipe.biomeWarpWavelengthSubvoxels = WorldAssemblyWavelength(Map.WarpFrequency, 556.0);
|
||||
Recipe.biomeBorderBlendSubvoxels = WorldAssemblyFixed(Map.BorderBlend);
|
||||
Recipe.biomeClimateReliefWavelengthSubvoxels = WorldAssemblyWavelength(Map.ReliefFrequency, 3333.0);
|
||||
Recipe.biomeClimateMoistureWavelengthSubvoxels = WorldAssemblyWavelength(Map.MoistureFrequency, 2500.0);
|
||||
Recipe.biomeClimateReliefContrastQ16 = WorldAssemblyQ16(Map.ReliefContrast);
|
||||
Recipe.biomes.clear();
|
||||
if (BiomeContext.IsValid() && Definition != nullptr)
|
||||
{
|
||||
for (const FBiomeResolved& Resolved : BiomeContext.Biomes)
|
||||
{
|
||||
const UVoxelBiomeDefinition* Biome = Definition->Biomes.IsValidIndex(Resolved.Index)
|
||||
? Definition->Biomes[Resolved.Index] : nullptr;
|
||||
FSurfaceGenerationParams Params = Surface;
|
||||
if (Biome != nullptr && Biome->bOverrideTerrain
|
||||
&& Biome->GeneratorType == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
Params = Biome->SurfaceParams;
|
||||
Params.StrateTopWorldZ = Surface.StrateTopWorldZ;
|
||||
Params.StrateBottomWorldZ = Surface.StrateBottomWorldZ;
|
||||
Params.BoundarySealThickness = Surface.BoundarySealThickness;
|
||||
Params.BaseDensity = Surface.BaseDensity;
|
||||
Params.WaterLevelRelative = Surface.WaterLevelRelative;
|
||||
}
|
||||
Recipe.biomes.push_back(BuildSurfaceProfile(
|
||||
0x1001u + static_cast<std::uint32_t>(Resolved.Index), Params, &Resolved,
|
||||
BottomVoxel, HeightVoxels));
|
||||
}
|
||||
}
|
||||
if (Recipe.biomes.empty())
|
||||
{
|
||||
Recipe.biomes.push_back(BuildSurfaceProfile(0x1001u, Surface, nullptr,
|
||||
BottomVoxel, HeightVoxels));
|
||||
}
|
||||
|
||||
Stratum.strateIndex = Slot.StrateIndex;
|
||||
Stratum.topChunkZ = Slot.TopChunkZ;
|
||||
Stratum.bottomChunkZ = Slot.BottomChunkZ;
|
||||
Stratum.capHeightSubvoxels = WorldAssemblyFixed(BottomVoxel + HeightVoxels * Surface.CeilingRelative);
|
||||
Stratum.capUndulationSubvoxels = WorldAssemblyFixed(Surface.CeilingUndulation);
|
||||
Stratum.capWavelengthSubvoxels = WorldAssemblyWavelength(Surface.CeilingUndulationFrequency, 250.0);
|
||||
Stratum.capDetailAmplitudeSubvoxels = WorldAssemblyFixed(Surface.CeilingRoughness);
|
||||
Stratum.capDetailWavelengthSubvoxels = WorldAssemblyWavelength(Surface.CeilingRoughnessFrequency, 25.0);
|
||||
Stratum.capRidgeAmplitudeSubvoxels = WorldAssemblyFixed(Surface.CeilingRidgeStrength);
|
||||
Stratum.capRidgeWavelengthSubvoxels = WorldAssemblyWavelength(Surface.CeilingRidgeFrequency, 50.0);
|
||||
Stratum.waterSurfaceSubvoxels = WorldAssemblyFixed(BottomVoxel
|
||||
+ HeightVoxels * Surface.WaterLevelRelative);
|
||||
Stratum.waterEnabled = Definition != nullptr && Definition->bHasWater
|
||||
&& Surface.WaterLevelRelative > 0.0f;
|
||||
|
||||
{
|
||||
// The hashed plan must not depend on platform-specific legacy float sampling.
|
||||
Heightfield2D BaseField(Recipe, Stratum);
|
||||
if (!BaseField.isValid())
|
||||
{
|
||||
OutError = TEXT("SurfaceWorld automatic plan could not initialize its fixed-point field.");
|
||||
return false;
|
||||
}
|
||||
constexpr int ProbeSide = 49;
|
||||
constexpr int ProbeStep = 32;
|
||||
std::array<std::int64_t, ProbeSide * ProbeSide> Heights{};
|
||||
for (int Y = 0; Y < ProbeSide; ++Y)
|
||||
for (int X = 0; X < ProbeSide; ++X)
|
||||
{
|
||||
const std::int64_t PX = static_cast<std::int64_t>(X - ProbeSide / 2) * ProbeStep;
|
||||
const std::int64_t PY = static_cast<std::int64_t>(Y - ProbeSide / 2) * ProbeStep;
|
||||
const std::size_t Index = static_cast<std::size_t>(Y) * ProbeSide + X;
|
||||
Heights[Index] = BaseField.point(WorldAssemblyFixed(static_cast<double>(PX)),
|
||||
WorldAssemblyFixed(static_cast<double>(PY)), 0).groundHeightSubvoxels;
|
||||
}
|
||||
std::int64_t SteepestNumerator = 0;
|
||||
int CliffX = 0;
|
||||
int CliffY = 0;
|
||||
bool bXAxis = true;
|
||||
int Downhill = 1;
|
||||
for (int Y = 1; Y < ProbeSide - 1; ++Y)
|
||||
for (int X = 1; X < ProbeSide - 1; ++X)
|
||||
{
|
||||
const std::size_t Center = static_cast<std::size_t>(Y) * ProbeSide + X;
|
||||
const std::int64_t DX = Heights[Center + 1] - Heights[Center - 1];
|
||||
const std::int64_t DY = Heights[Center + ProbeSide] - Heights[Center - ProbeSide];
|
||||
const std::int64_t SlopeNumerator = std::abs(DX) + std::abs(DY);
|
||||
if (SlopeNumerator > SteepestNumerator)
|
||||
{
|
||||
SteepestNumerator = SlopeNumerator;
|
||||
CliffX = (X - ProbeSide / 2) * ProbeStep;
|
||||
CliffY = (Y - ProbeSide / 2) * ProbeStep;
|
||||
bXAxis = std::abs(DX) >= std::abs(DY);
|
||||
Downhill = (bXAxis ? DX : DY) >= 0 ? -1 : 1;
|
||||
}
|
||||
}
|
||||
if (SteepestNumerator * 20
|
||||
> 4 * ProbeStep * WorldAssemblySubvoxelsPerVoxel)
|
||||
{
|
||||
WorldAssemblyFeatureV1 Shelf;
|
||||
Shelf.stableId = WorldAssemblyFeatureId(Recipe.seed, CliffX, CliffY, 0x4f564552u);
|
||||
Shelf.type = 4;
|
||||
constexpr int Reach = 72;
|
||||
constexpr int Width = 36;
|
||||
constexpr int ShelfHeight = 64;
|
||||
constexpr int Feather = 8;
|
||||
const int DirX = bXAxis ? Downhill : 0;
|
||||
const int DirY = bXAxis ? 0 : Downhill;
|
||||
const int MinX = bXAxis ? (DirX > 0 ? CliffX - Feather : CliffX - Reach - Feather)
|
||||
: CliffX - Width - Feather;
|
||||
const int MaxX = bXAxis ? (DirX > 0 ? CliffX + Reach + Feather : CliffX + Feather)
|
||||
: CliffX + Width + Feather;
|
||||
const int MinY = bXAxis ? CliffY - Width - Feather
|
||||
: (DirY > 0 ? CliffY - Feather : CliffY - Reach - Feather);
|
||||
const int MaxY = bXAxis ? CliffY + Width + Feather
|
||||
: (DirY > 0 ? CliffY + Reach + Feather : CliffY + Feather);
|
||||
std::int64_t LowestGround = std::numeric_limits<std::int64_t>::max();
|
||||
std::int64_t HighestGround = std::numeric_limits<std::int64_t>::min();
|
||||
for (int SampleY = MinY; SampleY <= MaxY; SampleY += 4)
|
||||
for (int SampleX = MinX; SampleX <= MaxX; SampleX += 4)
|
||||
{
|
||||
const auto Sample = BaseField.point(WorldAssemblyFixed(SampleX),
|
||||
WorldAssemblyFixed(SampleY), 0);
|
||||
LowestGround = std::min(LowestGround, Sample.groundHeightSubvoxels);
|
||||
HighestGround = std::max(HighestGround, Sample.groundHeightSubvoxels);
|
||||
}
|
||||
if (LowestGround == std::numeric_limits<std::int64_t>::max()
|
||||
|| HighestGround == std::numeric_limits<std::int64_t>::min())
|
||||
{
|
||||
OutError = TEXT("SurfaceWorld automatic plan could not bound its terrain shelf.");
|
||||
return false;
|
||||
}
|
||||
Shelf.minimumSubvoxels = {WorldAssemblyFixed(MinX), WorldAssemblyFixed(MinY),
|
||||
std::max(WorldAssemblyFixed(BottomVoxel), LowestGround - WorldAssemblyFixed(4.0))};
|
||||
Shelf.maximumSubvoxels = {WorldAssemblyFixed(MaxX), WorldAssemblyFixed(MaxY),
|
||||
std::min(WorldAssemblyFixed((Slot.TopChunkZ + 1) * CHUNK_SIZE),
|
||||
HighestGround + WorldAssemblyFixed(ShelfHeight + 4.0))};
|
||||
Shelf.parametersSubvoxels = {WorldAssemblyFixed(DirX), WorldAssemblyFixed(DirY),
|
||||
WorldAssemblyFixed(ShelfHeight), WorldAssemblyFixed(Reach)};
|
||||
Stratum.features.push_back(Shelf);
|
||||
}
|
||||
}
|
||||
std::string RouteError;
|
||||
if (!BuildFiniteSurfaceRoute(Recipe, Stratum, RouteError))
|
||||
{
|
||||
OutError = UTF8_TO_TCHAR(RouteError.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest,
|
||||
FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||
const FFloatingIslandParams Params = StrateManager->GetFloatingIslandParamsForChunk(Chunk);
|
||||
WorldAssemblyFloatingIslandPlanV1 IslandPlan;
|
||||
IslandPlan.strateIndex = Slot.StrateIndex;
|
||||
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
||||
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
||||
auto& Recipe = IslandPlan.manifest.recipe;
|
||||
Recipe.geometryVersion = 2;
|
||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
const std::int64_t BottomCoreVoxels = static_cast<std::int64_t>(Slot.BottomChunkZ) * 16;
|
||||
const std::int64_t TopCoreVoxels = static_cast<std::int64_t>(Slot.TopChunkZ + 1) * 16;
|
||||
Recipe.layoutMinimumZSubvoxels = BottomCoreVoxels * WorldAssemblySubvoxelsPerVoxel;
|
||||
Recipe.layoutMaximumZSubvoxels = TopCoreVoxels * WorldAssemblySubvoxelsPerVoxel;
|
||||
Recipe.layoutCenterTopZSubvoxels = (BottomCoreVoxels + TopCoreVoxels)
|
||||
* WorldAssemblySubvoxelsPerVoxel / 2;
|
||||
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
|
||||
Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale);
|
||||
Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale);
|
||||
Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale);
|
||||
Recipe.thicknessRatioQ16 = WorldAssemblyQ16(Params.ThicknessRatio);
|
||||
Recipe.verticalJitterQ16 = WorldAssemblyQ16(Params.VerticalJitter);
|
||||
Recipe.topFlattenQ16 = WorldAssemblyQ16(Params.TopFlatten);
|
||||
Recipe.roughnessSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness * OwnerToCoreScale);
|
||||
Recipe.blendRadiusSubvoxels = WorldAssemblyFixed(Params.SDFBlendRadius * OwnerToCoreScale);
|
||||
Recipe.boundarySealSubvoxels = WorldAssemblyFixed(Params.BoundarySealThickness * OwnerToCoreScale);
|
||||
std::string PlanError;
|
||||
const std::optional<FloatingIslandManifestV1> Planned = PlanFloatingIslandCluster(Recipe, PlanError);
|
||||
if (!Planned)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("FloatingIslands automatic plan for strate %d failed: %s"),
|
||||
Slot.StrateIndex, UTF8_TO_TCHAR(PlanError.c_str()));
|
||||
return false;
|
||||
}
|
||||
IslandPlan.manifest = *Planned;
|
||||
Manifest.floatingIslands.push_back(std::move(IslandPlan));
|
||||
return true;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonValue> SerializeCoreStruct(
|
||||
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
|
||||
@@ -287,21 +748,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ManifestJson.IsEmpty())
|
||||
std::string CoreError;
|
||||
std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest;
|
||||
const bool bImportedOverride = !ManifestJson.IsEmpty();
|
||||
if (bImportedOverride)
|
||||
{
|
||||
return true;
|
||||
FTCHARToUTF8 Utf8Json(*ManifestJson);
|
||||
Manifest = VoxelForgeCore::WorldAssemblyManifest::FromJson(
|
||||
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
|
||||
if (!Manifest)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core world assembly validation failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyManifest Generated;
|
||||
Generated.recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
int32 SurfaceSlotCount = 0;
|
||||
int32 IslandSlotCount = 0;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
|
||||
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
|
||||
}
|
||||
if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (SurfaceSlotCount == 0)
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyBiomeProfileV1 CarrierProfile;
|
||||
CarrierProfile.stableId = 0x1001u;
|
||||
Generated.recipe.biomes.push_back(CarrierProfile);
|
||||
}
|
||||
|
||||
int32 BuiltSurfaceCount = 0;
|
||||
std::vector<VoxelForgeCore::WorldAssemblyRecipeV1> SurfaceRecipes;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
if (Type == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyRecipeV1 SlotRecipe;
|
||||
SlotRecipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1 Stratum;
|
||||
if (!BuildSurfaceStratumPlan(StrateManager, Slot,
|
||||
SlotRecipe, Stratum, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (BuiltSurfaceCount == 0) Generated.recipe = SlotRecipe;
|
||||
SurfaceRecipes.push_back(SlotRecipe);
|
||||
Generated.plan.strates.push_back(std::move(Stratum));
|
||||
++BuiltSurfaceCount;
|
||||
}
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (BuiltSurfaceCount > 1)
|
||||
{
|
||||
for (std::size_t Index = 0; Index < Generated.plan.strates.size(); ++Index)
|
||||
{
|
||||
Generated.plan.strates[Index].hasRecipeOverride = true;
|
||||
Generated.plan.strates[Index].recipeOverride = SurfaceRecipes[Index];
|
||||
}
|
||||
}
|
||||
if (!Generated.finalize(CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core automatic world assembly planning failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
ManifestHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Generated.contentHash));
|
||||
Manifest = MoveTemp(Generated);
|
||||
}
|
||||
|
||||
FTCHARToUTF8 Utf8Json(*ManifestJson);
|
||||
std::string CoreError;
|
||||
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
|
||||
VoxelForgeCore::WorldAssemblyManifest::FromJson(
|
||||
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
|
||||
if (!Manifest)
|
||||
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<uint32>(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<int32>(Manifest->plan.strates.size()),
|
||||
static_cast<int32>(Manifest->floatingIslands.size()), *ManifestHash);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<unsigned long long>(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");
|
||||
|
||||
@@ -152,6 +152,7 @@ public:
|
||||
std::atomic<bool> bShuttingDown{false};
|
||||
std::atomic<uint64> 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<std::size_t>(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<unsigned long long>(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<unsigned long long>(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<VoxelForgeCore::ResolvedWorldSnapshot> Snapshot =
|
||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(Utf8Snapshot.Get(),
|
||||
static_cast<std::size_t>(Utf8Snapshot.Length())), CoreError);
|
||||
if (!Snapshot)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
AssemblyHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash));
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<FVoxelTileKey>& OutLeavers);
|
||||
@@ -740,6 +742,8 @@ private:
|
||||
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
|
||||
|
||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||
FString VoxelForgeCoreWorldAssemblyJson;
|
||||
FString VoxelForgeCoreWorldAssemblyHash;
|
||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
bool bCoreRegionCompilePending = false;
|
||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user