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);
|
||||
|
||||
@@ -340,6 +340,14 @@ private:
|
||||
float SuppressRadiusWorld = 0.0f;
|
||||
};
|
||||
|
||||
// Game-thread lookup copied from the immutable resolved Core layout.
|
||||
// Recherche game-thread copiee depuis la disposition Core resolue et immuable.
|
||||
struct FResolvedCoreGeometryAnchor
|
||||
{
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
FVector LocalPositionCm = FVector::ZeroVector;
|
||||
};
|
||||
|
||||
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
||||
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
|
||||
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
|
||||
@@ -436,6 +444,7 @@ private:
|
||||
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
||||
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
||||
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
||||
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
|
||||
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
||||
|
||||
// Set in BeginDestroy; worker tasks check it before touching us.
|
||||
|
||||
@@ -21,6 +21,8 @@ class UVoxelSettings : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void PostLoad() override;
|
||||
|
||||
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
||||
int32 GetEffectiveWorldSeed() const;
|
||||
@@ -411,14 +413,16 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
||||
int32 TotalStrates = 10;
|
||||
|
||||
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
|
||||
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
|
||||
// between each biome that the player must dig through at (0,0), and that the
|
||||
// auto-carved passages tunnel across.
|
||||
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
|
||||
// Legacy count retained so pre-metre assets still deserialize during the upgrade.
|
||||
// Ancien compte conserve pour lire les assets precedant les metres.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
|
||||
int32 InterStrateGapChunks = 0;
|
||||
|
||||
// Physical separation between strata; the layout remains aligned to whole layout chunks.
|
||||
// Separation physique; la disposition reste alignee sur des chunks entiers.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (Units = "m", ClampMin = "0", ClampMax = "1024"))
|
||||
double InterStrateGapMeters = 0.0;
|
||||
|
||||
// Physical width of one vertical strate-layout chunk. This is independent of the
|
||||
// Core tile width (32^3 voxels) and of the source generator's voxel resolution.
|
||||
// 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels.
|
||||
|
||||
@@ -48,6 +48,7 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void PostLoad() override;
|
||||
|
||||
//=========================================================================
|
||||
@@ -66,14 +67,14 @@ public:
|
||||
// DIMENSIONS
|
||||
//=========================================================================
|
||||
|
||||
// How many chunks tall this strate is.
|
||||
// Each strate can have a different height!
|
||||
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
|
||||
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
|
||||
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
|
||||
// Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
|
||||
// Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
|
||||
int32 StrateHeightInChunks = 16;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
|
||||
double StrateHeightMeters = 128.0;
|
||||
|
||||
//=========================================================================
|
||||
// BOUNDARY TRANSITION
|
||||
//=========================================================================
|
||||
@@ -99,9 +100,13 @@ public:
|
||||
// 2 → moderate (good default)
|
||||
// 4 → wide, gentle blend across many chunks
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||
meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
|
||||
int32 TransitionBlendChunks = 2;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||
meta = (Units = "m", ClampMin = "0", ClampMax = "512", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||
double TransitionWidthMeters = 16.0;
|
||||
|
||||
//=========================================================================
|
||||
// GENERATOR TYPE
|
||||
//=========================================================================
|
||||
@@ -115,6 +120,11 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
||||
|
||||
// Optional native Core authoring; leaving it empty keeps the established generator path.
|
||||
// Edition Core native facultative; laissez vide pour garder le generateur existant.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Core Geometry")
|
||||
FVoxelForgeCoreGeometry CoreGeometry;
|
||||
|
||||
/**
|
||||
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
|
||||
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
|
||||
|
||||
@@ -200,6 +200,208 @@ enum class ECaveGeneratorType : uint8
|
||||
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
|
||||
};
|
||||
|
||||
// Optional native-Core recipe selected independently for one strate.
|
||||
// Recette Core native facultative, choisie independamment pour chaque strate.
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreGeneratorRecipe : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "Off (legacy recipe)"),
|
||||
NativeCaves UMETA(DisplayName = "Native caves"),
|
||||
NativeLattice UMETA(DisplayName = "Native lattice"),
|
||||
ProfiledHeightfield UMETA(DisplayName = "Profiled heightfield")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreHeightfieldProfile : uint8
|
||||
{
|
||||
PeriodicHills,
|
||||
MirroredRelief,
|
||||
FlatField
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreStampKind : uint8
|
||||
{
|
||||
CarveEllipsoid,
|
||||
CarveArch,
|
||||
FillPillar,
|
||||
FillDais,
|
||||
FillMonolith,
|
||||
FillPlatform
|
||||
};
|
||||
|
||||
// Stable authored volume stamp. All distances and positions are metres at the asset boundary.
|
||||
// Volume stable, defini par son auteur. Toutes les positions et distances sont en metres.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreStampDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreStampKind Kind = EVoxelForgeCoreStampKind::CarveEllipsoid;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector AnchorPositionMeters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double PlacementRadiusMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
FVector HalfExtentsMeters = FVector(2.0, 2.0, 2.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double RoundingRadiusMeters = 0.5;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double MinimumSpacingMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (ClampMin = "0", ClampMax = "3"))
|
||||
uint8 YawQuarterTurns = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
bool bAttachToFloor = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
double FloorMeters = 0.0;
|
||||
};
|
||||
|
||||
// Authored cavern chamber, in local world metres.
|
||||
// Salle cavernicole authored, en metres locaux du monde.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreRoomDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector CenterMeters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
FVector HalfExtentsMeters = FVector(8.0, 8.0, 4.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
double FloorMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double UnionWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
};
|
||||
|
||||
// Authored cubic tunnel between a stable start anchor and a stable room name.
|
||||
// Tunnel cubique authored entre une ancre stable et une salle nommee.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreTunnelDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StartAnchorStableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString EndRoomStableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint0Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint1Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint2Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint3Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
double BoreRadiusMeters = 2.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "1"))
|
||||
double BoreHeightMeters = 4.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double FloorSupportDepthMeters = 0.5;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double UnionWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
|
||||
// Lattice tunnels require an explicit bounded polyline; cave tunnels use the cubic curve.
|
||||
// Les tunnels en treillis exigent une polyligne bornee; les tunnels caves utilisent la courbe cubique.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVector> PolylineMeters;
|
||||
};
|
||||
|
||||
// Optional Core authored recipe. Empty arrays and None preserve the prior asset output.
|
||||
// Recette Core authored facultative. None et les tableaux vides conservent la sortie historique.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreGeometry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreGeneratorRecipe GeneratorRecipe = EVoxelForgeCoreGeneratorRecipe::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreHeightfieldProfile HeightfieldProfile = EVoxelForgeCoreHeightfieldProfile::PeriodicHills;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
||||
double HeightfieldBaseMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
||||
FVector2D HeightfieldAmplitudeMeters = FVector2D(20.0, 20.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
||||
FVector2D HeightfieldWavelengthMeters = FVector2D(256.0, 256.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (ClampMin = "0.01", ClampMax = "4.0"))
|
||||
double MirrorLambda = 1.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
||||
double MinimumArenaClearanceMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreStampDefinition> Stamps;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreRoomDefinition> Rooms;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreTunnelDefinition> Tunnels;
|
||||
};
|
||||
|
||||
/**
|
||||
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
||||
*
|
||||
@@ -2289,6 +2491,11 @@ struct VOXELFORGE_API FStrateLandmark
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
||||
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
||||
|
||||
// Optional stable Core feature binding; an empty value keeps lattice/mouth placement unchanged.
|
||||
// Liaison facultative vers une ancre Core stable; vide, le placement existant reste identique.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Core Geometry")
|
||||
FString CoreGeometryStableName;
|
||||
|
||||
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
|
||||
|
||||
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
|
||||
|
||||
@@ -614,6 +614,7 @@ public:
|
||||
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
||||
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
||||
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
||||
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||
@@ -778,6 +779,8 @@ private:
|
||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||
FString VoxelForgeCoreWorldAssemblyJson;
|
||||
FString VoxelForgeCoreWorldAssemblyHash;
|
||||
FString VoxelForgeCoreResolvedLayoutJson;
|
||||
FString VoxelForgeCoreResolvedLayoutIdentity;
|
||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
bool bCoreRegionCompilePending = false;
|
||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||
|
||||
@@ -18023,6 +18023,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const double CarveBrushRadiusCoreVoxels =
|
||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
|
||||
uint64 ExpectedEditDigest = 0;
|
||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||
std::array<std::uint64_t, 4> TravelAggregates{};
|
||||
@@ -18208,10 +18209,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
BiomeProbeSpanVoxels);
|
||||
|
||||
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
|
||||
// Match the actor's cold-start fallback camera when the transient world has no pawn.
|
||||
// Aligner la reference sur la camera de repli de l'acteur sans pawn au demarrage.
|
||||
const VoxelForgeCore::WorldConfiguration& ReferenceConfiguration =
|
||||
ReferenceWorld.configuration();
|
||||
const VoxelForgeCore::Vec3d InitialCameraVoxels = ReferenceConfiguration.viewerStartFound
|
||||
? ReferenceConfiguration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired =
|
||||
ReferenceWorld.desiredTiles(InitialCameraVoxels);
|
||||
if (Desired.empty())
|
||||
{
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the cold-start camera.");
|
||||
return false;
|
||||
}
|
||||
ExpectedInitialTileCount = Desired.size();
|
||||
@@ -18258,7 +18266,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
ReferenceWorld.retainTileRegions(Desired);
|
||||
// Match the runtime collision probe around the same cold-start viewer tile.
|
||||
// Reproduire la sonde de collision autour de la meme tuile de depart a froid.
|
||||
ColdStartCollisionCenter = FIntVector(
|
||||
FMath::FloorToInt(InitialCameraVoxels.x / 32.0),
|
||||
FMath::FloorToInt(InitialCameraVoxels.y / 32.0),
|
||||
FMath::FloorToInt(InitialCameraVoxels.z / 32.0));
|
||||
std::vector<VoxelForgeCore::TileKey> ColdStartCollisionTiles;
|
||||
ColdStartCollisionTiles.reserve(static_cast<std::size_t>(2 * CollisionRadius + 1)
|
||||
* static_cast<std::size_t>(2 * CollisionRadius + 1)
|
||||
* static_cast<std::size_t>(2 * CollisionRadius + 1));
|
||||
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
|
||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||
ColdStartCollisionTiles.push_back({
|
||||
{ColdStartCollisionCenter.X + X,
|
||||
ColdStartCollisionCenter.Y + Y,
|
||||
ColdStartCollisionCenter.Z + Z}, 0});
|
||||
|
||||
if (Arguments.bCoreHybridCliffOnly)
|
||||
{
|
||||
@@ -18332,6 +18356,47 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
else
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
|
||||
// Keep collision hashes outside ExpectedInitialDesiredSet; the collision cube is an
|
||||
// additional runtime request even where it overlaps the visual clipmap.
|
||||
// Garder les hachages de collision hors de l'ensemble visuel initial, meme s'ils se
|
||||
// recoupent avec le clipmap.
|
||||
std::vector<VoxelForgeCore::TileKey> RetainedReferenceTiles = Desired;
|
||||
RetainedReferenceTiles.insert(RetainedReferenceTiles.end(),
|
||||
ColdStartCollisionTiles.begin(), ColdStartCollisionTiles.end());
|
||||
ReferenceWorld.retainTileRegions(RetainedReferenceTiles);
|
||||
if (!ReferenceWorld.compileTileRegion(ColdStartCollisionTiles, 16, Scheduler, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone collision reference compile failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
std::size_t CollisionBodyTiles = 0;
|
||||
for (const VoxelForgeCore::TileKey& Key : ColdStartCollisionTiles)
|
||||
{
|
||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
||||
ReferenceWorld.generateTile(Key, Options, CoreError);
|
||||
if (!Mesh)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone collision tile generation failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
FVoxelForgeCoreTileHashRecord Record;
|
||||
Record.bEmpty = Mesh->indices.empty();
|
||||
Record.bHasHashes = Mesh->hasDensityHash && Mesh->hasMeshHash
|
||||
&& Mesh->hasPayloadHash && Mesh->hasTopologyHash;
|
||||
if (!Record.bEmpty) ++CollisionBodyTiles;
|
||||
ExpectedHashes.Add(FVoxelTileKey(
|
||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod), Record);
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] standalone_collision_reference=1 center=(%d,%d,%d) tiles=%d body=%llu no_body=%llu"),
|
||||
ColdStartCollisionCenter.X, ColdStartCollisionCenter.Y, ColdStartCollisionCenter.Z,
|
||||
static_cast<int32>(ColdStartCollisionTiles.size()),
|
||||
static_cast<unsigned long long>(CollisionBodyTiles),
|
||||
static_cast<unsigned long long>(ColdStartCollisionTiles.size() - CollisionBodyTiles));
|
||||
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||
AggregateRows.reserve(Desired.size());
|
||||
bool bFoundCarveVertex = false;
|
||||
@@ -19367,7 +19432,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||
{
|
||||
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
|
||||
const FVoxelTileKey Key(ColdStartCollisionCenter + FIntVector(X, Y, Z), 0);
|
||||
ExpectedCollisionSet.Add(Key);
|
||||
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
||||
if (Record == nullptr)
|
||||
@@ -21361,6 +21426,44 @@ int32 RunBatch(const FString& Params)
|
||||
|
||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
{
|
||||
if (FParse::Param(*Params, TEXT("coreassetupgradeaudit")))
|
||||
{
|
||||
// Load the untouched BuildHost copies and emit their in-memory conversions; never save a package.
|
||||
// Charge les copies BuildHost intactes et affiche les conversions en memoire, sans sauver de package.
|
||||
UVoxelSettings* SettingsAsset = LoadObject<UVoxelSettings>(nullptr,
|
||||
TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
||||
if (SettingsAsset == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing DA_Settings copy"));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_gap_chunks=%d new_gap_m=%.6f layout_chunk_m=%.6f"),
|
||||
*SettingsAsset->GetPathName(), SettingsAsset->InterStrateGapChunks,
|
||||
SettingsAsset->InterStrateGapMeters, SettingsAsset->StrateLayoutChunkSizeMeters);
|
||||
|
||||
const TCHAR* StratePaths[] = {
|
||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
||||
};
|
||||
for (const TCHAR* Path : StratePaths)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(nullptr, Path);
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing strate copy %s"), Path);
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_height_chunks=%d new_height_m=%.6f old_transition_chunks=%d new_transition_m=%.6f"),
|
||||
*Definition->GetPathName(), Definition->StrateHeightInChunks,
|
||||
Definition->StrateHeightMeters, Definition->TransitionBlendChunks,
|
||||
Definition->TransitionWidthMeters);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (FParse::Param(*Params, TEXT("step2f")))
|
||||
{
|
||||
return RunStep2f(Params);
|
||||
|
||||
Reference in new issue
Block a user