fix(unreal): U1 -- Unreal compiles again on L61; CoreGeometry asset section; versioned layout-unit upgrade
Windows MSVC catch-up after abyss L15-L61 (no Unreal there): 64-bit clipmap shift (C4334), qualified VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, UHT metadata and duplicate-signature fixes. Optional CoreGeometry section on UVoxelStrateDefinition (native caves, lattice, profiled heightfields, named stamps, rooms/tunnels; off/empty by default), resolved after passage planning. VoxelForgeAssetVersion 2 converts strate height/transition/gap to metres on load (exact, 0 m change on the owner's 5 assets; packages never saved). Gates pass (SurfaceWorld, FloatingIslands, Core-off, M2, W1/W8/W16). KNOWN REGRESSION vs L14 (2026-10-20, same command, same PC): full set 3.9 -> 5.37 s, worker-seconds 15.8 -> 30, LOD0 1.11 -> 1.69 s, ring 0.50 -> 0.80 s; not yet attributed (Core L15-L61 vs Unreal side). Report: Standalone/Research/WorldAssemblerM3/Unreal1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32021930d2
commit
fc9227f9e0
22 files changed
+1420
-49
No files matched your search
@@ -399,6 +399,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
||||
LegacySettings->WorldRadiusVoxels = 987.0f;
|
||||
LegacySettings->TotalStrates = 1;
|
||||
LegacyDefinition->StrateHeightInChunks = 7;
|
||||
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
|
||||
LegacySettings->StratePool.Add(
|
||||
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
||||
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
||||
|
||||
@@ -525,6 +525,7 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
||||
Definition->TransitionBlendChunks = 0;
|
||||
Definition->TransitionWidthMeters = 0.0;
|
||||
Definition->TransitionType = (Index & 1) == 0
|
||||
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace
|
||||
Settings->Seed = LayoutSeed;
|
||||
Settings->TotalStrates = NumPoolDefinitions;
|
||||
Settings->InterStrateGapChunks = 1;
|
||||
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
|
||||
Settings->bOpenSurfaceEntry = true;
|
||||
|
||||
static const ECaveGeneratorType GeneratorTypes[] = {
|
||||
@@ -69,6 +70,8 @@ namespace
|
||||
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
||||
Def->GeneratorType = GeneratorTypes[Index];
|
||||
Def->StrateHeightInChunks = 3 + Index;
|
||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||
|
||||
// Non-trivial, distinct passage configs exercise the connection index and the
|
||||
|
||||
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
Settings->Seed = InSeed;
|
||||
Settings->InterStrateGapChunks = InGapChunks;
|
||||
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
||||
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
|
||||
|
||||
@@ -110,6 +112,8 @@ namespace VoxelForgeTest
|
||||
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
||||
// control; the owner-facing showcase opts into 8 chunks below.
|
||||
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
// Hard transitions: param blending across a boundary would make "which archetype
|
||||
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace VoxelClipmapDesiredTiles
|
||||
return Radius;
|
||||
}
|
||||
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
||||
const int64 Scale = static_cast<int64>(uint32{1} << static_cast<uint32>(SafeLevel));
|
||||
const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
|
||||
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
|
||||
+ Scale - 1;
|
||||
const int64 DesiredRadius = Numerator / Scale;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Tasks/Task.h"
|
||||
#include "HAL/PlatformProcess.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <cmath>
|
||||
@@ -322,11 +323,54 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
Seed = InSeed;
|
||||
NativeSurfaceSnapshot.Reset();
|
||||
NativeFloatingIslandSnapshot.Reset();
|
||||
CoreGeometryAnchors.Reset();
|
||||
|
||||
if (Settings)
|
||||
{
|
||||
FString ManifestJson, ManifestHash, ManifestError;
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
||||
FString ResolvedLayoutJson, ResolvedLayoutIdentity;
|
||||
if (VoxelWorld != nullptr
|
||||
&& VoxelWorld->GetVoxelForgeCoreResolvedLayout(ResolvedLayoutJson, ResolvedLayoutIdentity))
|
||||
{
|
||||
FTCHARToUTF8 Utf8Layout(*ResolvedLayoutJson);
|
||||
std::string LayoutError;
|
||||
const std::optional<VoxelForgeCore::ResolvedLayoutSnapshotV3> ResolvedLayout =
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(
|
||||
std::string_view(Utf8Layout.Get(), static_cast<std::size_t>(Utf8Layout.Length())), LayoutError);
|
||||
if (ResolvedLayout)
|
||||
{
|
||||
const auto AddAnchor = [this](const std::string& StableId, int32 StrateIndex,
|
||||
const std::array<std::int64_t, 3>& PositionMicrometres)
|
||||
{
|
||||
const FVector PositionCm(
|
||||
static_cast<double>(PositionMicrometres[0]) / 10000.0,
|
||||
static_cast<double>(PositionMicrometres[1]) / 10000.0,
|
||||
static_cast<double>(PositionMicrometres[2]) / 10000.0);
|
||||
const FString FullName = UTF8_TO_TCHAR(StableId.c_str());
|
||||
FResolvedCoreGeometryAnchor Anchor{ StrateIndex, PositionCm };
|
||||
CoreGeometryAnchors.Add(FullName, Anchor);
|
||||
const int32 Marker = FullName.Find(TEXT("/core/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
||||
if (Marker != INDEX_NONE)
|
||||
{
|
||||
const FString ShortName = FullName.Mid(Marker + 6);
|
||||
if (!ShortName.IsEmpty()) CoreGeometryAnchors.Add(ShortName, Anchor);
|
||||
}
|
||||
};
|
||||
for (const VoxelForgeCore::ResolvedLayoutStampV1& Stamp : ResolvedLayout->stamps)
|
||||
AddAnchor(Stamp.stableId, Stamp.strateIndex, Stamp.positionMicrometres);
|
||||
for (const VoxelForgeCore::ResolvedCavernRoomV1& Room : ResolvedLayout->cavernRooms)
|
||||
AddAnchor(Room.stableId, Room.strateIndex, Room.centerMicrometres);
|
||||
for (const VoxelForgeCore::ResolvedCavernEdgeV1& Edge : ResolvedLayout->cavernEdges)
|
||||
if (!Edge.polylineMicrometres.empty())
|
||||
AddAnchor(Edge.stableId, Edge.strateIndex, Edge.polylineMicrometres.front());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] Could not read resolved Core geometry anchors: %s"),
|
||||
UTF8_TO_TCHAR(LayoutError.c_str()));
|
||||
}
|
||||
}
|
||||
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
|
||||
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
||||
if (!bHasRuntimeAssembly)
|
||||
@@ -1042,8 +1086,19 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
|
||||
FMath::Abs(static_cast<double>(Density)));
|
||||
++OutAudit.AnchorChecks;
|
||||
if (FMath::Abs(Density)>0.25f)
|
||||
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
|
||||
// Core content crossings use the same finite-step column refinement as runtime
|
||||
// placement. Allow at most half a Core voxel of field residual in this audit.
|
||||
// Les croisements de contenu Core gardent le raffinement discret du runtime;
|
||||
// l'audit tolere au plus un demi-voxel Core de residu de champ.
|
||||
if (FMath::Abs(Density)>0.5f)
|
||||
{
|
||||
OutError=FString::Printf(
|
||||
TEXT("Populated fixture is not anchored on the canonical field: residual=%.4f core_vox=(%.3f,%.3f,%.3f) local_cm=(%.2f,%.2f,%.2f)."),
|
||||
Density, Local.X/CurrentCtx.VoxelSizeCm,
|
||||
Local.Y/CurrentCtx.VoxelSizeCm, Local.Z/CurrentCtx.VoxelSizeCm,
|
||||
Local.X, Local.Y, Local.Z);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Spawns.IsEmpty())
|
||||
@@ -2397,6 +2452,28 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
||||
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
||||
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
||||
|
||||
if (!L.CoreGeometryStableName.IsEmpty())
|
||||
{
|
||||
const FResolvedCoreGeometryAnchor* Anchor = CoreGeometryAnchors.Find(L.CoreGeometryStableName);
|
||||
if (Anchor == nullptr || Anchor->StrateIndex != StrateIndex) continue;
|
||||
const uint32 StableNameHash = FCrc::StrCrc32(*L.CoreGeometryStableName);
|
||||
const uint32 H = DecoHash(static_cast<int32>(StableNameHash), StrateIndex,
|
||||
0, 0, 0, EntryIdx, LocalSeed, 0xC04E6E0u);
|
||||
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
|
||||
const float LocalX = static_cast<float>(Anchor->LocalPositionCm.X);
|
||||
const float LocalY = static_cast<float>(Anchor->LocalPositionCm.Y);
|
||||
const float dx = LocalX - LocalPlayer.X, dy = LocalY - LocalPlayer.Y;
|
||||
const float DistSq = dx * dx + dy * dy;
|
||||
if (DistSq <= GatherWorld * GatherWorld)
|
||||
{
|
||||
// A Core-bound landmark is one stable candidate, independent of the hash lattice.
|
||||
// Un landmark lie a Core reste un candidat stable, independant de la grille de hachage.
|
||||
Cands.Add({ FIntVector(EntryIdx, 0, EntryIdx), EntryIdx, H, LocalX, LocalY,
|
||||
ExclWorld, L.Priority, DistSq <= RadiusWorld * RadiusWorld });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
||||
{
|
||||
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
// Shared custom package version for idempotent VoxelForge asset migrations.
|
||||
// Version de package partagee pour les migrations VoxelForge idempotentes.
|
||||
namespace VoxelForgeAssetVersion
|
||||
{
|
||||
inline const FGuid GUID(0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
||||
inline constexpr int32 TunnelFloorSlopeDegrees = 1;
|
||||
inline constexpr int32 MetreAuthoringV1 = 2;
|
||||
}
|
||||
@@ -82,6 +82,113 @@ std::int64_t WorldAssemblyWavelength(double Frequency, double FallbackVoxels)
|
||||
return std::max<std::int64_t>(1, WorldAssemblyFixed(Wavelength));
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyMicrometresToSubvoxels(
|
||||
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres);
|
||||
|
||||
bool WorldAssemblyMetresToMicrometres(double Metres, std::int64_t& OutMicrometres)
|
||||
{
|
||||
if (!std::isfinite(Metres)
|
||||
|| std::abs(Metres) > static_cast<double>(std::numeric_limits<std::int64_t>::max()) / 1000000.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutMicrometres = static_cast<std::int64_t>(std::llround(Metres * 1000000.0));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldAssemblyMetresToSubvoxels(double Metres, std::uint32_t CoreVoxelMicrometres,
|
||||
std::int64_t& OutSubvoxels)
|
||||
{
|
||||
std::int64_t Micrometres = 0;
|
||||
if (!WorldAssemblyMetresToMicrometres(Metres, Micrometres) || CoreVoxelMicrometres == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutSubvoxels = WorldAssemblyMicrometresToSubvoxels(Micrometres, CoreVoxelMicrometres);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldAssemblyVectorToMicrometres(const FVector& Metres, std::array<std::int64_t, 3>& Out)
|
||||
{
|
||||
return WorldAssemblyMetresToMicrometres(Metres.X, Out[0])
|
||||
&& WorldAssemblyMetresToMicrometres(Metres.Y, Out[1])
|
||||
&& WorldAssemblyMetresToMicrometres(Metres.Z, Out[2]);
|
||||
}
|
||||
|
||||
bool WorldAssemblyVectorToMicrometres(const FVector2D& Metres, std::array<std::int64_t, 2>& Out)
|
||||
{
|
||||
return WorldAssemblyMetresToMicrometres(Metres.X, Out[0])
|
||||
&& WorldAssemblyMetresToMicrometres(Metres.Y, Out[1]);
|
||||
}
|
||||
|
||||
std::string CoreGeometryStableId(int32 StrateIndex, const FString& StableName)
|
||||
{
|
||||
FTCHARToUTF8 Utf8Name(*StableName);
|
||||
return std::string("strate/") + std::to_string(StrateIndex) + "/core/" + Utf8Name.Get();
|
||||
}
|
||||
|
||||
bool HasCoreGeometry(const UVoxelStrateDefinition& Definition)
|
||||
{
|
||||
const FVoxelForgeCoreGeometry& Geometry = Definition.CoreGeometry;
|
||||
return Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::None
|
||||
|| !Geometry.Stamps.IsEmpty() || !Geometry.Rooms.IsEmpty() || !Geometry.Tunnels.IsEmpty();
|
||||
}
|
||||
|
||||
bool ConfigureCoreGeometryStratum(const FStrateSlot& Slot, ECaveGeneratorType GeneratorType,
|
||||
bool bImportedOverride, VoxelForgeCore::ResolvedLayoutStratumV2& Item, FString& OutError)
|
||||
{
|
||||
const FVoxelForgeCoreGeometry& Geometry = Slot.Definition->CoreGeometry;
|
||||
switch (Geometry.GeneratorRecipe)
|
||||
{
|
||||
case EVoxelForgeCoreGeneratorRecipe::None:
|
||||
return true;
|
||||
case EVoxelForgeCoreGeneratorRecipe::NativeCaves:
|
||||
if (GeneratorType != ECaveGeneratorType::TunnelNetwork
|
||||
&& GeneratorType != ECaveGeneratorType::Underwater)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d selects NativeCaves but its generator archetype is not TunnelNetwork/Underwater."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Item.generatorKind = "Caves";
|
||||
Item.recipeId = "Core.Caves.PrimitiveField";
|
||||
Item.recipeVersion = 1;
|
||||
Item.boundDispatchPath = "Core native cave graph uses resolved room and tunnel records";
|
||||
Item.queryDispatchPath = "NativeCaveDensityAt evaluates the resolved cavern graph";
|
||||
return true;
|
||||
case EVoxelForgeCoreGeneratorRecipe::NativeLattice:
|
||||
if (GeneratorType != ECaveGeneratorType::Maze
|
||||
&& GeneratorType != ECaveGeneratorType::VerticalShafts)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d selects NativeLattice but its generator archetype is not Maze/VerticalShafts."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Item.recipeId = "Core.Lattice.PrimitiveField";
|
||||
Item.recipeVersion = 1;
|
||||
Item.boundDispatchPath = "Core native lattice uses resolved room and explicit tunnel path records";
|
||||
Item.queryDispatchPath = "NativeLatticeDensityAt evaluates the resolved cavern graph";
|
||||
return true;
|
||||
case EVoxelForgeCoreGeneratorRecipe::ProfiledHeightfield:
|
||||
if (GeneratorType != ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d selects ProfiledHeightfield but its generator archetype is not SurfaceWorld."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
if (bImportedOverride)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d has ProfiledHeightfield authoring, but an imported WorldAssemblyManifest owns that recipe. Clear the imported override to use per-strate CoreGeometry."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Item.recipeId = "SurfaceWorld.ProfiledHeightfield";
|
||||
Item.recipeVersion = 1;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
|
||||
std::int64_t Y, std::uint32_t Kind)
|
||||
{
|
||||
@@ -111,7 +218,7 @@ std::int64_t WorldAssemblyMicrometresToSubvoxels(
|
||||
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
|
||||
{
|
||||
if (CoreVoxelMicrometres == 0) return 0;
|
||||
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel,
|
||||
return WorldAssemblyRoundRatio(Micrometres * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<std::int64_t>(CoreVoxelMicrometres));
|
||||
}
|
||||
|
||||
@@ -370,6 +477,54 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
BottomVoxel, HeightVoxels));
|
||||
}
|
||||
|
||||
if (Definition != nullptr
|
||||
&& Definition->CoreGeometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::ProfiledHeightfield)
|
||||
{
|
||||
const FVoxelForgeCoreGeometry& Geometry = Definition->CoreGeometry;
|
||||
WorldAssemblySurfaceProfileV1 Profile;
|
||||
Profile.version = 1;
|
||||
switch (Geometry.HeightfieldProfile)
|
||||
{
|
||||
case EVoxelForgeCoreHeightfieldProfile::PeriodicHills:
|
||||
Profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills;
|
||||
break;
|
||||
case EVoxelForgeCoreHeightfieldProfile::MirroredRelief:
|
||||
Profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief;
|
||||
break;
|
||||
case EVoxelForgeCoreHeightfieldProfile::FlatField:
|
||||
Profile.kind = WorldAssemblySurfaceProfileKindV1::flatField;
|
||||
break;
|
||||
}
|
||||
std::array<std::int64_t, 2> AmplitudeMicrometres{};
|
||||
std::array<std::int64_t, 2> WavelengthMicrometres{};
|
||||
std::int64_t BaseSubvoxels = 0;
|
||||
std::int64_t ClearanceSubvoxels = 0;
|
||||
if (!WorldAssemblyVectorToMicrometres(Geometry.HeightfieldAmplitudeMeters, AmplitudeMicrometres)
|
||||
|| !WorldAssemblyVectorToMicrometres(Geometry.HeightfieldWavelengthMeters, WavelengthMicrometres)
|
||||
|| !WorldAssemblyMetresToSubvoxels(Geometry.HeightfieldBaseMeters, CoreVoxelMicrometres, BaseSubvoxels)
|
||||
|| !WorldAssemblyMetresToSubvoxels(Geometry.MinimumArenaClearanceMeters,
|
||||
CoreVoxelMicrometres, ClearanceSubvoxels)
|
||||
|| !std::isfinite(Geometry.MirrorLambda))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d Core profiled-heightfield fields contain a non-finite or out-of-range metre value."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Profile.h0Subvoxels = BaseSubvoxels;
|
||||
Profile.amplitudeXSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
AmplitudeMicrometres[0], CoreVoxelMicrometres);
|
||||
Profile.amplitudeYSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
AmplitudeMicrometres[1], CoreVoxelMicrometres);
|
||||
Profile.wavelengthXSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
WavelengthMicrometres[0], CoreVoxelMicrometres);
|
||||
Profile.wavelengthYSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
WavelengthMicrometres[1], CoreVoxelMicrometres);
|
||||
Profile.mirrorLambdaQ16 = WorldAssemblyQ16(Geometry.MirrorLambda);
|
||||
Profile.minimumArenaClearanceSubvoxels = ClearanceSubvoxels;
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(Profile, Recipe.seed);
|
||||
Recipe.surfaceProfile = Profile;
|
||||
}
|
||||
|
||||
Stratum.strateIndex = Slot.StrateIndex;
|
||||
Stratum.topChunkZ = Slot.TopChunkZ;
|
||||
Stratum.bottomChunkZ = Slot.BottomChunkZ;
|
||||
@@ -836,6 +991,349 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
|
||||
}
|
||||
}
|
||||
|
||||
bool ResolveAuthoredCoreGeometry(VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout,
|
||||
const UVoxelStrateManager* StrateManager, std::int32_t Seed, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
std::vector<ResolvedLayoutStampRequestV1> LegacyStamps, MotifStamps;
|
||||
std::vector<ResolvedCavernRoomV1> Rooms;
|
||||
std::vector<ResolvedCavernEdgeV1> Edges;
|
||||
TSet<FString> FeatureNames;
|
||||
TSet<int32> NativeGraphStrates;
|
||||
const auto StableId = [&FeatureNames, &OutError](const FString& Name, int32 StrateIndex,
|
||||
const TCHAR* Kind, std::string& OutId)
|
||||
{
|
||||
if (Name.IsEmpty() || Name != Name.TrimStartAndEnd())
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core %s on strate %d needs a non-empty stable name without surrounding whitespace."), Kind, StrateIndex);
|
||||
return false;
|
||||
}
|
||||
if (FeatureNames.Contains(Name))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core stable name '%s' is duplicated across this world."), *Name);
|
||||
return false;
|
||||
}
|
||||
FeatureNames.Add(Name);
|
||||
OutId = CoreGeometryStableId(StrateIndex, Name);
|
||||
return true;
|
||||
};
|
||||
const auto Microns = [&OutError](double Value, int32 StrateIndex, const TCHAR* Field, std::int64_t& Out)
|
||||
{
|
||||
if (WorldAssemblyMetresToMicrometres(Value, Out)) return true;
|
||||
OutError = FString::Printf(TEXT("Strate %d CoreGeometry.%s must be finite and bounded in metres."), StrateIndex, Field);
|
||||
return false;
|
||||
};
|
||||
const auto VectorMicrons = [&OutError](const FVector& Value, int32 StrateIndex, const TCHAR* Field,
|
||||
std::array<std::int64_t, 3>& Out)
|
||||
{
|
||||
if (WorldAssemblyVectorToMicrometres(Value, Out)) return true;
|
||||
OutError = FString::Printf(TEXT("Strate %d CoreGeometry.%s must contain finite, bounded metre coordinates."), StrateIndex, Field);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
const UVoxelStrateDefinition* Definition = Slot.Definition;
|
||||
if (!Definition || !HasCoreGeometry(*Definition)) continue;
|
||||
const FVoxelForgeCoreGeometry& Geometry = Definition->CoreGeometry;
|
||||
if (Geometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::NativeCaves
|
||||
|| Geometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::NativeLattice)
|
||||
NativeGraphStrates.Add(Slot.StrateIndex);
|
||||
|
||||
for (const FVoxelForgeCoreStampDefinition& Authored : Geometry.Stamps)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 Request;
|
||||
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("stamp"), Request.stableId)
|
||||
|| !VectorMicrons(Authored.AnchorPositionMeters, Slot.StrateIndex, TEXT("Stamps.AnchorPositionMeters"), Request.anchorPositionMicrometres)
|
||||
|| !VectorMicrons(Authored.HalfExtentsMeters, Slot.StrateIndex, TEXT("Stamps.HalfExtentsMeters"), Request.halfExtentsMicrometres)
|
||||
|| !Microns(Authored.PlacementRadiusMeters, Slot.StrateIndex, TEXT("Stamps.PlacementRadiusMeters"), Request.placementRadiusMicrometres)
|
||||
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Stamps.SmoothWidthMeters"), Request.smoothWidthMicrometres)
|
||||
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Stamps.IrregularityAmplitudeMeters"), Request.irregularityAmplitudeMicrometres)
|
||||
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Stamps.IrregularityWavelengthMeters"), Request.irregularityWavelengthMicrometres)
|
||||
|| !Microns(Authored.RoundingRadiusMeters, Slot.StrateIndex, TEXT("Stamps.RoundingRadiusMeters"), Request.roundingRadiusMicrometres)
|
||||
|| !Microns(Authored.MinimumSpacingMeters, Slot.StrateIndex, TEXT("Stamps.MinimumSpacingMeters"), Request.minimumSpacingMicrometres)
|
||||
|| !Microns(Authored.FloorMeters, Slot.StrateIndex, TEXT("Stamps.FloorMeters"), Request.floorMicrometres)) return false;
|
||||
Request.strateIndex = Slot.StrateIndex;
|
||||
switch (Authored.Kind)
|
||||
{
|
||||
case EVoxelForgeCoreStampKind::CarveEllipsoid: Request.kind = ResolvedSdfStampKindV1::carveEllipsoid; break;
|
||||
case EVoxelForgeCoreStampKind::CarveArch: Request.kind = ResolvedSdfStampKindV1::carveArch; break;
|
||||
case EVoxelForgeCoreStampKind::FillPillar: Request.kind = ResolvedSdfStampKindV1::fillPillar; break;
|
||||
case EVoxelForgeCoreStampKind::FillDais: Request.kind = ResolvedSdfStampKindV1::fillDais; break;
|
||||
case EVoxelForgeCoreStampKind::FillMonolith: Request.kind = ResolvedSdfStampKindV1::fillMonolith; break;
|
||||
case EVoxelForgeCoreStampKind::FillPlatform: Request.kind = ResolvedSdfStampKindV1::fillPlatform; break;
|
||||
}
|
||||
Request.attachmentPolicy = Authored.bAttachToFloor ? 1u : 0u;
|
||||
Request.yawQuarterTurns = Authored.YawQuarterTurns;
|
||||
if (Request.kind == ResolvedSdfStampKindV1::fillMonolith || Request.kind == ResolvedSdfStampKindV1::fillPlatform)
|
||||
MotifStamps.push_back(std::move(Request));
|
||||
else LegacyStamps.push_back(std::move(Request));
|
||||
}
|
||||
|
||||
TSet<FString> RoomNames;
|
||||
for (const FVoxelForgeCoreRoomDefinition& Authored : Geometry.Rooms)
|
||||
{
|
||||
ResolvedCavernRoomV1 Room;
|
||||
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("room"), Room.stableId)
|
||||
|| !VectorMicrons(Authored.CenterMeters, Slot.StrateIndex, TEXT("Rooms.CenterMeters"), Room.centerMicrometres)
|
||||
|| !VectorMicrons(Authored.HalfExtentsMeters, Slot.StrateIndex, TEXT("Rooms.HalfExtentsMeters"), Room.halfExtentsMicrometres)
|
||||
|| !Microns(Authored.FloorMeters, Slot.StrateIndex, TEXT("Rooms.FloorMeters"), Room.floorMicrometres)
|
||||
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Rooms.SmoothWidthMeters"), Room.smoothWidthMicrometres)
|
||||
|| !Microns(Authored.UnionWidthMeters, Slot.StrateIndex, TEXT("Rooms.UnionWidthMeters"), Room.unionWidthMicrometres)
|
||||
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Rooms.IrregularityAmplitudeMeters"), Room.irregularityAmplitudeMicrometres)
|
||||
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Rooms.IrregularityWavelengthMeters"), Room.irregularityWavelengthMicrometres)) return false;
|
||||
Room.strateIndex = Slot.StrateIndex;
|
||||
RoomNames.Add(Authored.StableName);
|
||||
Rooms.push_back(std::move(Room));
|
||||
}
|
||||
|
||||
for (const FVoxelForgeCoreTunnelDefinition& Authored : Geometry.Tunnels)
|
||||
{
|
||||
ResolvedCavernEdgeV1 Edge;
|
||||
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("tunnel"), Edge.stableId)) return false;
|
||||
if (Authored.StartAnchorStableName.IsEmpty() || Authored.EndRoomStableName.IsEmpty()
|
||||
|| !RoomNames.Contains(Authored.EndRoomStableName))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core tunnel '%s' needs a start-anchor stable name and an end-room name that identifies a room on strate %d."), *Authored.StableName, Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
FTCHARToUTF8 StartAnchorUtf8(*Authored.StartAnchorStableName);
|
||||
Edge.startAnchorStableId = StartAnchorUtf8.Get();
|
||||
Edge.endRoomStableId = CoreGeometryStableId(Slot.StrateIndex, Authored.EndRoomStableName);
|
||||
Edge.strateIndex = Slot.StrateIndex;
|
||||
const FVector ControlPoints[4] = { Authored.ControlPoint0Meters, Authored.ControlPoint1Meters,
|
||||
Authored.ControlPoint2Meters, Authored.ControlPoint3Meters };
|
||||
for (std::size_t Point = 0; Point < 4; ++Point)
|
||||
if (!WorldAssemblyVectorToMicrometres(ControlPoints[Point], Edge.controlPointsMicrometres[Point]))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core tunnel '%s' has a non-finite or out-of-range control point in metres."), *Authored.StableName);
|
||||
return false;
|
||||
}
|
||||
if (!Microns(Authored.BoreRadiusMeters, Slot.StrateIndex, TEXT("Tunnels.BoreRadiusMeters"), Edge.boreRadiusMicrometres)
|
||||
|| !Microns(Authored.BoreHeightMeters, Slot.StrateIndex, TEXT("Tunnels.BoreHeightMeters"), Edge.boreHeightMicrometres)
|
||||
|| !Microns(Authored.FloorSupportDepthMeters, Slot.StrateIndex, TEXT("Tunnels.FloorSupportDepthMeters"), Edge.floorSupportDepthMicrometres)
|
||||
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Tunnels.SmoothWidthMeters"), Edge.smoothWidthMicrometres)
|
||||
|| !Microns(Authored.UnionWidthMeters, Slot.StrateIndex, TEXT("Tunnels.UnionWidthMeters"), Edge.unionWidthMicrometres)
|
||||
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Tunnels.IrregularityAmplitudeMeters"), Edge.irregularityAmplitudeMicrometres)
|
||||
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Tunnels.IrregularityWavelengthMeters"), Edge.irregularityWavelengthMicrometres)) return false;
|
||||
for (const FVector& Point : Authored.PolylineMeters)
|
||||
{
|
||||
std::array<std::int64_t, 3> Converted{};
|
||||
if (!WorldAssemblyVectorToMicrometres(Point, Converted))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core tunnel '%s' has an invalid metre polyline point."), *Authored.StableName);
|
||||
return false;
|
||||
}
|
||||
Edge.polylineMicrometres.push_back(Converted);
|
||||
}
|
||||
Edges.push_back(std::move(Edge));
|
||||
}
|
||||
if ((!Geometry.Rooms.IsEmpty() || !Geometry.Tunnels.IsEmpty())
|
||||
&& Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::NativeCaves
|
||||
&& Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::NativeLattice)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d authors a cavern graph without selecting NativeCaves or NativeLattice."), Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (!Slot.Definition) continue;
|
||||
for (const FStrateLandmark& Landmark : Slot.Definition->Landmarks)
|
||||
if (!Landmark.CoreGeometryStableName.IsEmpty() && !FeatureNames.Contains(Landmark.CoreGeometryStableName))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Landmark on strate %d references unknown Core stable name '%s'."), Slot.StrateIndex, *Landmark.CoreGeometryStableName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int32 StrateIndex : NativeGraphStrates)
|
||||
{
|
||||
const bool bHasRoom = std::any_of(Rooms.begin(), Rooms.end(), [StrateIndex](const auto& Room) { return Room.strateIndex == StrateIndex; });
|
||||
const bool bHasEdge = std::any_of(Edges.begin(), Edges.end(), [StrateIndex](const auto& Edge) { return Edge.strateIndex == StrateIndex; });
|
||||
if (!bHasRoom || !bHasEdge)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Native cave/lattice recipe on strate %d needs at least one authored room and one tunnel."), StrateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!LegacyStamps.empty() || !MotifStamps.empty())
|
||||
{
|
||||
std::string CoreError;
|
||||
const ResolvedLayoutSnapshotV3 BaseLayout = Layout;
|
||||
if (!LegacyStamps.empty() && !ResolveResolvedLayoutStampsV1(Layout, Seed, LegacyStamps, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core authored stamp resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!MotifStamps.empty())
|
||||
{
|
||||
ResolvedLayoutSnapshotV3 MotifLayout = BaseLayout;
|
||||
if (!ResolveResolvedLayoutStampsV3(MotifLayout, Seed, MotifStamps, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core authored motif resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (LegacyStamps.empty()) Layout = std::move(MotifLayout);
|
||||
else
|
||||
{
|
||||
Layout.stamps.insert(Layout.stamps.end(), MotifLayout.stamps.begin(), MotifLayout.stamps.end());
|
||||
Layout.stampVocabularyVersion = ResolvedLayoutStampVocabularyVersionV3;
|
||||
std::vector<ResolvedLayoutIssueV2> Issues;
|
||||
if (!Layout.finalize(CoreError) || !Layout.validate(ResolvedLayoutValidationModeV3::strict, Issues, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Combined Core stamp layout failed validation: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Rooms.empty() || !Edges.empty())
|
||||
{
|
||||
std::string CoreError;
|
||||
if (!ResolveResolvedCavernGraphV1(Layout, Seed, Rooms, Edges, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core authored cavern graph resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttachCoreGeometryOnlyLayout(const UVoxelStrateManager* StrateManager,
|
||||
double OwnerVoxelCm, double CoreVoxelCm, std::uint32_t LayoutChunkMicrometres,
|
||||
std::int64_t LayoutChunkSubvoxels, const TSharedRef<FJsonObject>& Root, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
ResolvedLayoutSnapshotV3 Layout;
|
||||
Layout.worldIdentity = std::string("owner-world/seed/") + std::to_string(StrateManager->GetWorldSeed());
|
||||
Layout.ownerVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(OwnerVoxelCm * 10000.0));
|
||||
Layout.coreVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(CoreVoxelCm * 10000.0));
|
||||
Layout.strateLayoutChunkSizeMicrometres = LayoutChunkMicrometres;
|
||||
Layout.strateLayoutChunkWidthSubvoxels = LayoutChunkSubvoxels;
|
||||
const std::uint32_t CoreVoxelMicrometres = Layout.coreVoxelSizeMicrometres;
|
||||
const auto GeneratorName = [](ECaveGeneratorType Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return "TunnelNetwork";
|
||||
case ECaveGeneratorType::FlatPlain: return "FlatPlain";
|
||||
case ECaveGeneratorType::CrystalChamber: return "CrystalChamber";
|
||||
case ECaveGeneratorType::Maze: return "Maze";
|
||||
case ECaveGeneratorType::SurfaceWorld: return "SurfaceWorld";
|
||||
case ECaveGeneratorType::VerticalShafts: return "VerticalShafts";
|
||||
case ECaveGeneratorType::FloatingIslands: return "FloatingIslands";
|
||||
case ECaveGeneratorType::Underwater: return "Underwater";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
const auto TransitionName = [](EVoxelStrateTransition Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EVoxelStrateTransition::Gradient: return "Gradient";
|
||||
case EVoxelStrateTransition::Hard: return "Hard";
|
||||
case EVoxelStrateTransition::Interleaved: return "Interleaved";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
std::vector<ResolvedLayoutPassageConfigV1> PassageConfigs;
|
||||
const std::int64_t OwnerVoxelMicrometres = Layout.ownerVoxelSizeMicrometres;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (!Slot.Definition) continue;
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ));
|
||||
ResolvedLayoutStratumV2 Item;
|
||||
Item.strateIndex = Slot.StrateIndex;
|
||||
Item.generatorKind = GeneratorName(Type);
|
||||
Item.stableId = Layout.worldIdentity + "/strate/" + std::to_string(Slot.StrateIndex)
|
||||
+ "/generator/" + GeneratorName(Type);
|
||||
Item.recipeId = Type == ECaveGeneratorType::SurfaceWorld ? "SurfaceWorld.Heightfield2D"
|
||||
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
|
||||
: std::string("legacy.") + GeneratorName(Type);
|
||||
Item.recipeVersion = 0;
|
||||
if (!ConfigureCoreGeometryStratum(Slot, Type, false, Item, OutError)) return false;
|
||||
Item.voxelSizeMicrometres = CoreVoxelMicrometres;
|
||||
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
|
||||
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
|
||||
Item.managerChunkWidthMicrometres = LayoutChunkMicrometres;
|
||||
Item.dispatchChunkWidthMicrometres = LayoutChunkMicrometres;
|
||||
Item.transitionMode = TransitionName(Slot.ResolvedTransitionType);
|
||||
Item.transitionBlendChunks = Slot.ResolvedTransitionBlendChunks;
|
||||
Item.transitionWidthMicrometres = Slot.TransitionWidthMicrometres;
|
||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||
? std::min(Item.transitionWidthMicrometres / 2, static_cast<std::int64_t>(LayoutChunkMicrometres)) : 0;
|
||||
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
|
||||
? Item.transitionWidthMicrometres : Item.transitionMode == "Gradient" ? Item.transitionWidthMicrometres / 4 : 0;
|
||||
Item.transitionFallsBackToHard = Item.transitionMode != "Hard" && Item.transitionWidthMicrometres <= 0;
|
||||
Item.transitionHonouredInCore = Item.transitionMode == "Hard" || !Item.transitionFallsBackToHard;
|
||||
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; upper strate owns transitions";
|
||||
Item.boundDispatchPath = "CoreGeometry-only resolved strate layout";
|
||||
Item.queryDispatchPath = "CoreGeometry-only native layout field";
|
||||
|
||||
const std::int64_t SealMicrometres = static_cast<std::int64_t>(std::llround(
|
||||
std::max(0.0f, StrateBoundarySealThickness(*Slot.Definition)) * OwnerVoxelMicrometres));
|
||||
if (SealMicrometres <= 0)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d has no positive BoundarySealThickness for the shared ceiling and bedrock bands."), Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
const std::int64_t SolidDensityQ16 = std::max<std::int64_t>(1,
|
||||
WorldAssemblyQ16(std::max(0.05f, StrateBoundarySolidDensity(*Slot.Definition))));
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres - SealMicrometres,
|
||||
Item.interiorTopMicrometres, true, true, false, "solid ceiling from strate BoundarySealThickness",
|
||||
"interior-facing lower surface", "shared resolved-layout density band", SolidDensityQ16 };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
|
||||
Item.interiorBottomMicrometres + SealMicrometres, true, true, false,
|
||||
"solid bedrock floor from strate BoundarySealThickness", "interior-facing upper surface",
|
||||
"shared resolved-layout density band", SolidDensityQ16 };
|
||||
Item.content = { "content", Item.interiorBottomMicrometres + SealMicrometres,
|
||||
Item.interiorTopMicrometres - SealMicrometres, true, true, false,
|
||||
"resolved generator interior", "none", "none" };
|
||||
Layout.strates.push_back(std::move(Item));
|
||||
|
||||
const FStratePassageConfig& Passage = Slot.ResolvedPassageConfig;
|
||||
ResolvedLayoutPassageConfigV1 Config;
|
||||
Config.upperStrateIndex = Slot.StrateIndex;
|
||||
Config.connections = Passage.Connections;
|
||||
Config.style = static_cast<std::uint32_t>(Passage.Style);
|
||||
Config.mouthRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMouthRadiusMicrometres, CoreVoxelMicrometres),
|
||||
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
Config.midRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMidRadiusMicrometres, CoreVoxelMicrometres),
|
||||
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
Config.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMinimumMicrometres, CoreVoxelMicrometres);
|
||||
Config.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMaximumMicrometres, CoreVoxelMicrometres);
|
||||
Config.distanceMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMinimumMicrometres, CoreVoxelMicrometres);
|
||||
Config.distanceMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMaximumMicrometres, CoreVoxelMicrometres);
|
||||
Config.wanderSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres, CoreVoxelMicrometres);
|
||||
Config.segments = static_cast<std::uint32_t>(FMath::Clamp(Passage.Segments, 1, 48));
|
||||
Config.verticalWobbleSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageVerticalWobbleMicrometres, CoreVoxelMicrometres);
|
||||
Config.spiralRadiusSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageSpiralRadiusMicrometres, CoreVoxelMicrometres);
|
||||
Config.spiralTurnsQ16 = static_cast<std::int64_t>(std::llround(static_cast<double>(Passage.SpiralTurns) * 65536.0));
|
||||
Config.cascadeSteps = static_cast<std::uint32_t>(FMath::Clamp(Passage.CascadeSteps, 1, 16));
|
||||
Config.cascadeLedgeSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageCascadeLedgeMicrometres, CoreVoxelMicrometres);
|
||||
PassageConfigs.push_back(Config);
|
||||
}
|
||||
std::string CoreError;
|
||||
if (!PlanResolvedLayoutPassagesV1(Layout, StrateManager->GetWorldSeed(), PassageConfigs, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Resolved inter-strate passage planning failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager, StrateManager->GetWorldSeed(), OutError)) return false;
|
||||
if (!Layout.finalize(CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core resolved-layout export failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
const std::string Json = Layout.ToJson();
|
||||
Root->SetStringField(TEXT("resolved_layout_json"), UTF8_TO_TCHAR(Json.c_str()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
double CoreVoxelSizeCm,
|
||||
@@ -876,6 +1374,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
|
||||
int32 SurfaceSlotCount = 0;
|
||||
int32 IslandSlotCount = 0;
|
||||
int32 CoreGeometrySlotCount = 0;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
@@ -883,10 +1382,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
|
||||
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
|
||||
CoreGeometrySlotCount += HasCoreGeometry(*Slot.Definition) ? 1 : 0;
|
||||
}
|
||||
if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
|
||||
{
|
||||
return true;
|
||||
return CoreGeometrySlotCount == 0 ? true
|
||||
: AttachCoreGeometryOnlyLayout(StrateManager, OwnerVoxelSizeCm, CoreVoxelSizeCm,
|
||||
LayoutChunkSizeMicrometres, LayoutChunkWidthSubvoxels, Root, OutError);
|
||||
}
|
||||
if (SurfaceSlotCount == 0)
|
||||
{
|
||||
@@ -1128,6 +1630,10 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
|
||||
: std::string("legacy.") + generatorName(Type);
|
||||
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
||||
if (!ConfigureCoreGeometryStratum(Slot, Type, bImportedOverride, Item, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
|
||||
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
|
||||
@@ -1264,6 +1770,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
UTF8_TO_TCHAR(PassagePlanError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager,
|
||||
StrateManager->GetWorldSeed(), OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::string LayoutError;
|
||||
if (!Layout.finalize(LayoutError))
|
||||
{
|
||||
@@ -1372,6 +1883,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
|
||||
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
|
||||
// Core import des longueurs owner avec le rapport physique resolu, pas un ratio suppose.
|
||||
// Core imports owner lengths with the resolved physical ratio, never an assumed scale.
|
||||
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
|
||||
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("seed"), Seed);
|
||||
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
|
||||
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
|
||||
@@ -1958,10 +1958,11 @@ namespace VoxelSeasonCompositionPrivate
|
||||
}
|
||||
|
||||
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
||||
FVoxelSeasonFixedStrate& OutFixed)
|
||||
double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
|
||||
{
|
||||
OutFixed = FVoxelSeasonFixedStrate();
|
||||
OutFixed.HeightInChunks = Definition.StrateHeightInChunks;
|
||||
OutFixed.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||
Definition.StrateHeightMeters / FMath::Max(0.001, LayoutChunkMeters)));
|
||||
OutFixed.Archetype = Definition.GeneratorType;
|
||||
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
||||
OutFixed.bUsesRecipe = false;
|
||||
@@ -1999,7 +2000,9 @@ namespace VoxelSeasonCompositionPrivate
|
||||
return false;
|
||||
}
|
||||
FVoxelSeasonFixedStrate Fixed;
|
||||
CopyDefinitionParams(*Definition, Fixed);
|
||||
const double LayoutChunkMeters = Settings.WorldSettings != nullptr
|
||||
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
|
||||
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
|
||||
Fixed.Seed = SeasonSeed;
|
||||
if (Fixed.SourceDefinitionPath.IsEmpty())
|
||||
{
|
||||
@@ -2974,7 +2977,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
|
||||
if (WorldSettings != nullptr)
|
||||
{
|
||||
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
|
||||
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks;
|
||||
Settings.InterStrateGapChunks = FMath::Max(0, FMath::RoundToInt(
|
||||
WorldSettings->InterStrateGapMeters * 1000000.0
|
||||
/ FMath::Max(1.0, static_cast<double>(WorldSettings->StrateLayoutChunkSizeMeters) * 1000000.0)));
|
||||
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
|
||||
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
||||
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "VoxelSettings.h"
|
||||
|
||||
#include "VoxelForgeAssetVersion.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -9,6 +11,74 @@
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <string>
|
||||
|
||||
void UVoxelSettings::Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
||||
Super::Serialize(Ar);
|
||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
||||
* LayoutChunkMeters;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
|
||||
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelSettings::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the legacy count against this settings asset's saved physical layout width.
|
||||
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
|
||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
||||
* LayoutChunkMeters;
|
||||
|
||||
TSet<UVoxelStrateDefinition*> Migrated;
|
||||
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
|
||||
if (Definition == nullptr || Migrated.Contains(Definition)
|
||||
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Migrated.Add(Definition);
|
||||
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
|
||||
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
|
||||
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
|
||||
* LayoutChunkMeters;
|
||||
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
|
||||
* LayoutChunkMeters;
|
||||
Definition->MarkPackageDirty();
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
|
||||
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
|
||||
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
|
||||
};
|
||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
|
||||
{
|
||||
MigrateStrate(Definition);
|
||||
}
|
||||
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
|
||||
{
|
||||
MigrateStrate(Definition.Value);
|
||||
}
|
||||
MarkPackageDirty();
|
||||
}
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
// Serialized strate-definition migrations.
|
||||
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelForgeAssetVersion.h"
|
||||
|
||||
#include "Serialization/CustomVersion.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// The field name is retained for package compatibility, but version 1 changes its meaning
|
||||
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
|
||||
const FGuid GVoxelForgeStrateSerializationVersion(
|
||||
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
|
||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
|
||||
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
||||
GVoxelForgeStrateSerializationVersion,
|
||||
GVoxelForgeStrateGentleSlopeDegreesVersion,
|
||||
VoxelForgeAssetVersion::GUID,
|
||||
VoxelForgeAssetVersion::MetreAuthoringV1,
|
||||
TEXT("VoxelForgeStrateSerialization"));
|
||||
|
||||
float LegacyGradientToDegrees(float LegacyGradient)
|
||||
@@ -58,6 +55,31 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateDefinition::Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
||||
Super::Serialize(Ar);
|
||||
|
||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
// A stand-alone strate has no owning settings object during Serialize. Convert using the
|
||||
// current 8 m layout default here; UVoxelSettings corrects referenced old strates using
|
||||
// their saved effective width during its own load upgrade.
|
||||
// Une strate seule n'a pas ses reglages proprietaires ici; UVoxelSettings corrige ensuite
|
||||
// les references avec leur largeur physique sauvegardee.
|
||||
constexpr double LegacyDefaultLayoutChunkMeters = 8.0;
|
||||
StrateHeightMeters = static_cast<double>(FMath::Max(1, StrateHeightInChunks))
|
||||
* LegacyDefaultLayoutChunkMeters;
|
||||
TransitionWidthMeters = static_cast<double>(FMath::Max(0, TransitionBlendChunks))
|
||||
* LegacyDefaultLayoutChunkMeters;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f fallback_layout_chunk_m=%.6f"),
|
||||
*GetPathName(), StrateHeightInChunks, StrateHeightMeters,
|
||||
TransitionBlendChunks, TransitionWidthMeters, LegacyDefaultLayoutChunkMeters);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateDefinition::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
@@ -67,7 +89,7 @@ void UVoxelStrateDefinition::PostLoad()
|
||||
// is normalized to the ignored compatibility default before any runtime copy is made.
|
||||
GenerationParams.bTunnelFloorEnabled = true;
|
||||
|
||||
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
|
||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1255,7 +1255,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
||||
const int32 LayoutGapChunks = bUseSeason
|
||||
? SeasonManifest.InterStrateGapChunks
|
||||
: FMath::Max(0, Settings->InterStrateGapChunks);
|
||||
: FMath::Max(0, FMath::RoundToInt(static_cast<double>(Settings->InterStrateGapMeters)
|
||||
* 1000000.0 / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
|
||||
//=========================================================================
|
||||
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||
@@ -1369,7 +1370,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
||||
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
||||
Slot.Definition->StrateHeightMeters = static_cast<double>(SeasonStrate.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres / 1000000.0;
|
||||
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
||||
@@ -1426,7 +1428,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
// Resolve the authored chunk height into an exact physical interval.
|
||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||
Slot.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||
Slot.Definition->StrateHeightMeters * 1000000.0
|
||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
||||
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
@@ -1439,9 +1443,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
||||
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||
Slot.TransitionWidthMicrometres = static_cast<int64>(
|
||||
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres;
|
||||
Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
|
||||
Slot.Definition->TransitionWidthMeters * 1000000.0
|
||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
|
||||
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.MouthRadius);
|
||||
|
||||
@@ -7567,6 +7567,14 @@ bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::GetVoxelForgeCoreResolvedLayout(
|
||||
FString& OutCanonicalJson, FString& OutStableIdentity) const
|
||||
{
|
||||
OutCanonicalJson = VoxelForgeCoreResolvedLayoutJson;
|
||||
OutStableIdentity = VoxelForgeCoreResolvedLayoutIdentity;
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutStableIdentity.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
|
||||
{
|
||||
OutDensity = 0.0f;
|
||||
@@ -7585,6 +7593,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
OutFailureReason.Reset();
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
VoxelForgeCoreResolvedLayoutJson.Reset();
|
||||
VoxelForgeCoreResolvedLayoutIdentity.Reset();
|
||||
if (!bUseVoxelForgeCore) return true;
|
||||
if (!Settings || !StrateManager)
|
||||
{
|
||||
@@ -7617,6 +7627,17 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
|
||||
return false;
|
||||
}
|
||||
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
|
||||
{
|
||||
const std::string CanonicalLayout = ResolvedLayout->ToJson();
|
||||
if (CanonicalLayout.empty())
|
||||
{
|
||||
OutFailureReason = TEXT("the resolved Core layout could not be serialized canonically.");
|
||||
return false;
|
||||
}
|
||||
VoxelForgeCoreResolvedLayoutJson = UTF8_TO_TCHAR(CanonicalLayout.c_str());
|
||||
VoxelForgeCoreResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
const std::string CanonicalAssembly = Assembly->ToJson();
|
||||
@@ -7793,6 +7814,8 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
|
||||
}
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
VoxelForgeCoreResolvedLayoutJson.Reset();
|
||||
VoxelForgeCoreResolvedLayoutIdentity.Reset();
|
||||
bUseVoxelForgeCore = false;
|
||||
// The Core adapter sets this before validation; legacy generation must not inherit it.
|
||||
// L'adaptateur Core l'active avant validation ; le générateur historique ne doit pas en hériter.
|
||||
@@ -15117,6 +15140,7 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
}
|
||||
|
||||
FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
|
||||
FString ResolvedLayoutIdentity = VoxelForgeCoreResolvedLayoutIdentity;
|
||||
if (AssemblyHash.IsEmpty())
|
||||
{
|
||||
FString AssemblyJson;
|
||||
@@ -15153,12 +15177,17 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
AssemblyHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash));
|
||||
}
|
||||
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
|
||||
{
|
||||
ResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
const uint32 CoreVoxelSizeMicrons = static_cast<uint32>(
|
||||
FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f));
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
if (AssemblyHash.IsEmpty() && ResolvedLayoutIdentity.IsEmpty()
|
||||
&& !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
@@ -15183,6 +15212,11 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
}
|
||||
if (!ResolvedLayoutIdentity.IsEmpty())
|
||||
{
|
||||
Payload += TEXT("|resolved_layout=");
|
||||
Payload += ResolvedLayoutIdentity;
|
||||
}
|
||||
if (CoreVoxelSizeMicrons != 250000)
|
||||
{
|
||||
Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons);
|
||||
|
||||
Reference in new issue
Block a user