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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-26 12:12:08 +01:00
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);
+35 -1
View File
@@ -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.
+10 -6
View File
@@ -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
+207
View File
@@ -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
+3
View File
@@ -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);