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

+24
View File
@@ -2292,6 +2292,30 @@ it also checks the orphan control, brute-force interval samples and the composed
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
results; the code path is not promoted to legacy Maze/VerticalShafts generation. results; the code path is not promoted to legacy Maze/VerticalShafts generation.
### 8.55 Unreal CoreGeometry asset bridge and U1 asset version
`UVoxelStrateDefinition::CoreGeometry` is optional and defaults to recipe `None` with no stamps,
rooms, or tunnels. The Unreal adapter preserves the established passage planner, then resolves
authored stamps and cavern graph records, then finalizes the layout. Stable names are required and
are scoped by strate in Core; landmarks may target a stamp, room, or tunnel by that stable name.
Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork or
Underwater archetype; native lattice requires Maze or VerticalShafts. Core recipe/version fields
enter the resolved layout identity when selected. An imported manifest cannot be mixed with a
per-strate profiled heightfield.
The checked-out Core API contains stamp vocabularies V1, V2, and V3. Unreal uses V1 for the original
stamp kinds and V3 for monolith/platform motifs, then combines and strictly validates the result.
Research17 names V2, but the source API and motif vocabulary require V3; source was followed.
Research27 water remains out of scope for U1.
The shared VoxelForge custom asset version is 2: version 1 records the tunnel-floor slope migration;
version 2 adds load-time conversion for inter-strate gap, strate height, and transition width. The
saved strate-layout width is used, currently 8 m. Old packages are converted in memory and marked
dirty, but U1 does not save them. This does not yet migrate the rest of Research8's reflected
spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and frequencies,
placement vectors, and other centimetre fields remain in their prior units. The U1 report records
the five BuildHost asset values, hashes, and remaining work.
## 9. Multiplayer model (listen-server first, dedicated-friendly) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+36 -8
View File
@@ -88,19 +88,22 @@ therefore **128 m**, and a 64-chunk strate is **512 m**. Core still stores work
at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored **0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored
in metres and converted at runtime. The live in metres and converted at runtime. The live
strate generator continues to read voxel-valued parameters from its assets, so those authored strate generator continues to read voxel-valued shape parameters from its assets, so those authored
dimensions become half as large in metres unless their asset values are doubled. dimensions become half as large in metres unless their asset values are doubled. U1 adds a
versioned in-memory conversion for strate height, transition width, and inter-strate gap only; the
broader Research8 voxel/chunk/centimetre inventory is not yet migrated.
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m** **32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary. new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
in the handoff report for the exact `DA_Settings` fields and values. for the conversion coverage and remaining Research8 fields.
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing **Prior reauthoring recommendation (not applied in U1; no packages were edited):** preserve
`DA_Settings` distances by changing
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`, `ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`, `StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region `EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
@@ -110,8 +113,9 @@ size `8→16 cells`, spacing/far spacing `8→16 voxels`, march step `4→8 voxe
`12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded `12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8. limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is former 512 m span. U1 automatically converts only strate height and transition widths and the
re-authored; the editor-only research field is the step-3 target. settings gap at load; the remaining recommendations above are pending. Its voxel-authored
live-generator lengths remain half-size until their asset fields are migrated.
Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`): Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`):
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³ the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
@@ -1323,6 +1327,30 @@ See ARCHITECTURE §8.54 and
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md) [`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
for resolved records, final-field reachability, controls, timing, hashes and captures. for resolved records, final-field reachability, controls, timing, hashes and captures.
### 3.37 Unreal CoreGeometry assets and partial metre upgrade — World Assembler U1
`Public/VoxelStrateTypes.h` declares optional `FVoxelForgeCoreGeometry`. Its default recipe is Off
and its stamp, room, and tunnel arrays are empty. `Private/VoxelForgeCoreAdapter.cpp` maps authored
metres to Core layout requests after passage planning and before finalizing the layout. Stamps use
the V1 resolver for original stamp kinds and V3 for monolith/platform motifs because the checked-out
Core API exposes V3; authored cavern records use `ResolveResolvedCavernGraphV1`. Stable feature
names are namespaced by strate in the resolved layout. `FStrateLandmark::CoreGeometryStableName`
binds one landmark to such a resolved record, and `VoxelContentManager.cpp` builds its lookup from
the finalized snapshot. `VoxelWorld.cpp` retains resolved layout identity in content hashing.
`Private/VoxelForgeAssetVersion.h` sets the shared custom version to 2. `UVoxelSettings::Serialize`
and `UVoxelStrateDefinition::Serialize`/`PostLoad` upgrade only inter-strate gap, strate height,
and transition width using the saved 8 m layout chunk. These conversions are version-guarded and
idempotent. No package save is performed by the audit. Other Research8 chunks, owner-voxel lengths,
Core-voxel lengths/frequencies, and centimetre fields remain on their existing units and need a
separate migration.
Likely compile/UHT touchpoints are the reflected nested fields in `VoxelStrateTypes.h`, Core snapshot
and resolver signatures in `VoxelForgeCoreAdapter.cpp`, `FCustomVersionRegistration` and the old
tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelForgeExplore` audit
in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core
tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence.
## 4. The density pipeline (most-edited hot path) ## 4. The density pipeline (most-edited hot path)
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
@@ -399,6 +399,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
LegacySettings->WorldRadiusVoxels = 987.0f; LegacySettings->WorldRadiusVoxels = 987.0f;
LegacySettings->TotalStrates = 1; LegacySettings->TotalStrates = 1;
LegacyDefinition->StrateHeightInChunks = 7; LegacyDefinition->StrateHeightInChunks = 7;
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
LegacySettings->StratePool.Add( LegacySettings->StratePool.Add(
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get())); TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
TStrongObjectPtr<UVoxelStrateManager> LegacyManager( TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
@@ -525,6 +525,7 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
{ {
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get(); UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
Definition->TransitionBlendChunks = 0; Definition->TransitionBlendChunks = 0;
Definition->TransitionWidthMeters = 0.0;
Definition->TransitionType = (Index & 1) == 0 Definition->TransitionType = (Index & 1) == 0
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved; ? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
} }
@@ -44,6 +44,7 @@ namespace
Settings->Seed = LayoutSeed; Settings->Seed = LayoutSeed;
Settings->TotalStrates = NumPoolDefinitions; Settings->TotalStrates = NumPoolDefinitions;
Settings->InterStrateGapChunks = 1; Settings->InterStrateGapChunks = 1;
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
Settings->bOpenSurfaceEntry = true; Settings->bOpenSurfaceEntry = true;
static const ECaveGeneratorType GeneratorTypes[] = { static const ECaveGeneratorType GeneratorTypes[] = {
@@ -69,6 +70,8 @@ namespace
FString::Printf(TEXT("Layout order test strate %d"), Index)); FString::Printf(TEXT("Layout order test strate %d"), Index));
Def->GeneratorType = GeneratorTypes[Index]; Def->GeneratorType = GeneratorTypes[Index];
Def->StrateHeightInChunks = 3 + Index; Def->StrateHeightInChunks = 3 + Index;
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
* Settings->StrateLayoutChunkSizeMeters;
Def->TransitionType = EVoxelStrateTransition::Hard; Def->TransitionType = EVoxelStrateTransition::Hard;
// Non-trivial, distinct passage configs exercise the connection index and the // Non-trivial, distinct passage configs exercise the connection index and the
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient)); NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
Settings->Seed = InSeed; Settings->Seed = InSeed;
Settings->InterStrateGapChunks = InGapChunks; Settings->InterStrateGapChunks = InGapChunks;
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
* Settings->StrateLayoutChunkSizeMeters;
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults; // WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0. // 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 // refinement and roll tests use the smaller synthetic volume as a bounded memory
// control; the owner-facing showcase opts into 8 chunks below. // control; the owner-facing showcase opts into 8 chunks below.
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks); 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 // Hard transitions: param blending across a boundary would make "which archetype
// owns this chunk" ambiguous, and these tests want an unambiguous mapping. // owns this chunk" ambiguous, and these tests want an unambiguous mapping.
Def->TransitionType = EVoxelStrateTransition::Hard; Def->TransitionType = EVoxelStrateTransition::Hard;
@@ -79,7 +79,7 @@ namespace VoxelClipmapDesiredTiles
return Radius; return Radius;
} }
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8); 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) const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
+ Scale - 1; + Scale - 1;
const int64 DesiredRadius = Numerator / Scale; const int64 DesiredRadius = Numerator / Scale;
@@ -18,6 +18,7 @@
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "Tasks/Task.h" #include "Tasks/Task.h"
#include "HAL/PlatformProcess.h" #include "HAL/PlatformProcess.h"
#include "Misc/Crc.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
#include <VoxelForgeCore/VoxelForgeCore.h> #include <VoxelForgeCore/VoxelForgeCore.h>
#include <cmath> #include <cmath>
@@ -322,11 +323,54 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
Seed = InSeed; Seed = InSeed;
NativeSurfaceSnapshot.Reset(); NativeSurfaceSnapshot.Reset();
NativeFloatingIslandSnapshot.Reset(); NativeFloatingIslandSnapshot.Reset();
CoreGeometryAnchors.Reset();
if (Settings) if (Settings)
{ {
FString ManifestJson, ManifestHash, ManifestError; FString ManifestJson, ManifestHash, ManifestError;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get()); 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 const bool bHasRuntimeAssembly = VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash); && VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
if (!bHasRuntimeAssembly) if (!bHasRuntimeAssembly)
@@ -1042,8 +1086,19 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual, OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
FMath::Abs(static_cast<double>(Density))); FMath::Abs(static_cast<double>(Density)));
++OutAudit.AnchorChecks; ++OutAudit.AnchorChecks;
if (FMath::Abs(Density)>0.25f) // Core content crossings use the same finite-step column refinement as runtime
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; } // 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()) if (!Spawns.IsEmpty())
@@ -2397,6 +2452,28 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld; const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
if (ExclWorld > 0.0f) bAnyExclusion = true; 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) if (L.AnchorMode == ELandmarkAnchor::HashLattice)
{ {
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks); 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)); 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::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
std::int64_t Y, std::uint32_t Kind) std::int64_t Y, std::uint32_t Kind)
{ {
@@ -111,7 +218,7 @@ std::int64_t WorldAssemblyMicrometresToSubvoxels(
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres) std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
{ {
if (CoreVoxelMicrometres == 0) return 0; if (CoreVoxelMicrometres == 0) return 0;
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel, return WorldAssemblyRoundRatio(Micrometres * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<std::int64_t>(CoreVoxelMicrometres)); static_cast<std::int64_t>(CoreVoxelMicrometres));
} }
@@ -370,6 +477,54 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
BottomVoxel, HeightVoxels)); 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.strateIndex = Slot.StrateIndex;
Stratum.topChunkZ = Slot.TopChunkZ; Stratum.topChunkZ = Slot.TopChunkZ;
Stratum.bottomChunkZ = Slot.BottomChunkZ; 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, bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
const UVoxelStrateManager* StrateManager, const UVoxelStrateManager* StrateManager,
double CoreVoxelSizeCm, double CoreVoxelSizeCm,
@@ -876,6 +1374,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels; Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
int32 SurfaceSlotCount = 0; int32 SurfaceSlotCount = 0;
int32 IslandSlotCount = 0; int32 IslandSlotCount = 0;
int32 CoreGeometrySlotCount = 0;
for (const FStrateSlot& Slot : StrateManager->GetLayout()) for (const FStrateSlot& Slot : StrateManager->GetLayout())
{ {
if (Slot.Definition == nullptr) continue; if (Slot.Definition == nullptr) continue;
@@ -883,10 +1382,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
FIntVector(0, 0, Slot.TopChunkZ)); FIntVector(0, 0, Slot.TopChunkZ));
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0; SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0; IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
CoreGeometrySlotCount += HasCoreGeometry(*Slot.Definition) ? 1 : 0;
} }
if (SurfaceSlotCount == 0 && IslandSlotCount == 0) if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
{ {
return true; return CoreGeometrySlotCount == 0 ? true
: AttachCoreGeometryOnlyLayout(StrateManager, OwnerVoxelSizeCm, CoreVoxelSizeCm,
LayoutChunkSizeMicrometres, LayoutChunkWidthSubvoxels, Root, OutError);
} }
if (SurfaceSlotCount == 0) if (SurfaceSlotCount == 0)
{ {
@@ -1128,6 +1630,10 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField" : Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
: std::string("legacy.") + generatorName(Type); : std::string("legacy.") + generatorName(Type);
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0; Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
if (!ConfigureCoreGeometryStratum(Slot, Type, bImportedOverride, Item, OutError))
{
return false;
}
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons; Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres; Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
Item.interiorTopMicrometres = Slot.TopWorldMicrometres; Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
@@ -1264,6 +1770,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
UTF8_TO_TCHAR(PassagePlanError.c_str())); UTF8_TO_TCHAR(PassagePlanError.c_str()));
return false; return false;
} }
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager,
StrateManager->GetWorldSeed(), OutError))
{
return false;
}
std::string LayoutError; std::string LayoutError;
if (!Layout.finalize(LayoutError)) if (!Layout.finalize(LayoutError))
{ {
@@ -1372,6 +1883,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0))); static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm); Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons); 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->SetNumberField(TEXT("seed"), Seed);
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry); Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius); Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
@@ -1958,10 +1958,11 @@ namespace VoxelSeasonCompositionPrivate
} }
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition, void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
FVoxelSeasonFixedStrate& OutFixed) double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
{ {
OutFixed = FVoxelSeasonFixedStrate(); 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.Archetype = Definition.GeneratorType;
OutFixed.SourceDefinitionPath = Definition.GetPathName(); OutFixed.SourceDefinitionPath = Definition.GetPathName();
OutFixed.bUsesRecipe = false; OutFixed.bUsesRecipe = false;
@@ -1999,7 +2000,9 @@ namespace VoxelSeasonCompositionPrivate
return false; return false;
} }
FVoxelSeasonFixedStrate Fixed; FVoxelSeasonFixedStrate Fixed;
CopyDefinitionParams(*Definition, Fixed); const double LayoutChunkMeters = Settings.WorldSettings != nullptr
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
Fixed.Seed = SeasonSeed; Fixed.Seed = SeasonSeed;
if (Fixed.SourceDefinitionPath.IsEmpty()) if (Fixed.SourceDefinitionPath.IsEmpty())
{ {
@@ -2974,7 +2977,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
if (WorldSettings != nullptr) if (WorldSettings != nullptr)
{ {
Settings.SelectedStrateCount = WorldSettings->TotalStrates; 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.OriginSpineRadius = WorldSettings->OriginSpineRadius;
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels; Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
Settings.SeasonNumber = WorldSettings->CurrentSeason; Settings.SeasonNumber = WorldSettings->CurrentSeason;
@@ -1,5 +1,7 @@
#include "VoxelSettings.h" #include "VoxelSettings.h"
#include "VoxelForgeAssetVersion.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#if WITH_EDITOR #if WITH_EDITOR
@@ -9,6 +11,74 @@
#include <VoxelForgeCore/VoxelForgeCore.h> #include <VoxelForgeCore/VoxelForgeCore.h>
#include <string> #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 int32 UVoxelSettings::GetEffectiveWorldSeed() const
{ {
if (!Season.IsNull()) if (!Season.IsNull())
@@ -1,20 +1,17 @@
// Serialized strate-definition migrations. // Serialized strate-definition migrations.
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
#include "VoxelForgeAssetVersion.h"
#include "Serialization/CustomVersion.h" #include "Serialization/CustomVersion.h"
#include "VoxelPassageGeometry.h" #include "VoxelPassageGeometry.h"
namespace namespace
{ {
// The field name is retained for package compatibility, but version 1 changes its meaning constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
// 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;
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration( FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
GVoxelForgeStrateSerializationVersion, VoxelForgeAssetVersion::GUID,
GVoxelForgeStrateGentleSlopeDegreesVersion, VoxelForgeAssetVersion::MetreAuthoringV1,
TEXT("VoxelForgeStrateSerialization")); TEXT("VoxelForgeStrateSerialization"));
float LegacyGradientToDegrees(float LegacyGradient) 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() void UVoxelStrateDefinition::PostLoad()
{ {
Super::PostLoad(); Super::PostLoad();
@@ -67,7 +89,7 @@ void UVoxelStrateDefinition::PostLoad()
// is normalized to the ignored compatibility default before any runtime copy is made. // is normalized to the ignored compatibility default before any runtime copy is made.
GenerationParams.bTunnelFloorEnabled = true; GenerationParams.bTunnelFloorEnabled = true;
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion) if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= GVoxelForgeStrateGentleSlopeDegreesVersion) >= GVoxelForgeStrateGentleSlopeDegreesVersion)
{ {
return; return;
@@ -1255,7 +1255,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
? SeasonManifest.Strates.Num() : Settings->TotalStrates; ? SeasonManifest.Strates.Num() : Settings->TotalStrates;
const int32 LayoutGapChunks = bUseSeason const int32 LayoutGapChunks = bUseSeason
? SeasonManifest.InterStrateGapChunks ? 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) // 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->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->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams; Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams; 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. // 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.TopWorldMicrometres = CurrentTopWorldMicrometres;
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks) Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
* StrateLayoutChunkSizeMicrometres; * StrateLayoutChunkSizeMicrometres;
@@ -1439,9 +1443,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
} }
Slot.ResolvedTransitionType = Slot.Definition->TransitionType; Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks; Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
Slot.TransitionWidthMicrometres = static_cast<int64>( Slot.Definition->TransitionWidthMeters * 1000000.0
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres; / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
* StrateLayoutChunkSizeMicrometres;
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig; Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres( Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.MouthRadius); Slot.ResolvedPassageConfig.MouthRadius);
+35 -1
View File
@@ -7567,6 +7567,14 @@ bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty(); 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 bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
{ {
OutDensity = 0.0f; OutDensity = 0.0f;
@@ -7585,6 +7593,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
OutFailureReason.Reset(); OutFailureReason.Reset();
VoxelForgeCoreWorldAssemblyJson.Reset(); VoxelForgeCoreWorldAssemblyJson.Reset();
VoxelForgeCoreWorldAssemblyHash.Reset(); VoxelForgeCoreWorldAssemblyHash.Reset();
VoxelForgeCoreResolvedLayoutJson.Reset();
VoxelForgeCoreResolvedLayoutIdentity.Reset();
if (!bUseVoxelForgeCore) return true; if (!bUseVoxelForgeCore) return true;
if (!Settings || !StrateManager) if (!Settings || !StrateManager)
{ {
@@ -7617,6 +7627,17 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
return false; 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()) if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
{ {
const std::string CanonicalAssembly = Assembly->ToJson(); const std::string CanonicalAssembly = Assembly->ToJson();
@@ -7793,6 +7814,8 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
} }
VoxelForgeCoreWorldAssemblyJson.Reset(); VoxelForgeCoreWorldAssemblyJson.Reset();
VoxelForgeCoreWorldAssemblyHash.Reset(); VoxelForgeCoreWorldAssemblyHash.Reset();
VoxelForgeCoreResolvedLayoutJson.Reset();
VoxelForgeCoreResolvedLayoutIdentity.Reset();
bUseVoxelForgeCore = false; bUseVoxelForgeCore = false;
// The Core adapter sets this before validation; legacy generation must not inherit it. // 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. // 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 AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
FString ResolvedLayoutIdentity = VoxelForgeCoreResolvedLayoutIdentity;
if (AssemblyHash.IsEmpty()) if (AssemblyHash.IsEmpty())
{ {
FString AssemblyJson; FString AssemblyJson;
@@ -15153,12 +15177,17 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
AssemblyHash = FString::Printf(TEXT("%016llx"), AssemblyHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Assembly->contentHash)); 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 bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
const uint32 CoreVoxelSizeMicrons = static_cast<uint32>( const uint32 CoreVoxelSizeMicrons = static_cast<uint32>(
FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f)); FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f));
if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000) if (AssemblyHash.IsEmpty() && ResolvedLayoutIdentity.IsEmpty()
&& !bHybridMesher && CoreVoxelSizeMicrons == 250000)
{ {
return SeasonHash; return SeasonHash;
} }
@@ -15183,6 +15212,11 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
Payload += TEXT("|world_assembly="); Payload += TEXT("|world_assembly=");
Payload += AssemblyHash; Payload += AssemblyHash;
} }
if (!ResolvedLayoutIdentity.IsEmpty())
{
Payload += TEXT("|resolved_layout=");
Payload += ResolvedLayoutIdentity;
}
if (CoreVoxelSizeMicrons != 250000) if (CoreVoxelSizeMicrons != 250000)
{ {
Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons); Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons);
@@ -340,6 +340,14 @@ private:
float SuppressRadiusWorld = 0.0f; 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 /** 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 * (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 * 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. // (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. // Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
TMap<FIntVector, FLandmarkInstance> LandmarkInstances; TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
// Set in BeginDestroy; worker tasks check it before touching us. // Set in BeginDestroy; worker tasks check it before touching us.
+10 -6
View File
@@ -21,6 +21,8 @@ class UVoxelSettings : public UPrimaryDataAsset
GENERATED_BODY() GENERATED_BODY()
public: public:
virtual void Serialize(FArchive& Ar) override;
virtual void PostLoad() override;
/** Season seed/radius replace the mutable authored values only when Season is assigned. */ /** Season seed/radius replace the mutable authored values only when Season is assigned. */
int32 GetEffectiveWorldSeed() const; int32 GetEffectiveWorldSeed() const;
@@ -411,14 +413,16 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
int32 TotalStrates = 10; int32 TotalStrates = 10;
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock. // Legacy count retained so pre-metre assets still deserialize during the upgrade.
// 0 = strates touch (just the seals between them). Higher = a thick rock layer // Ancien compte conserve pour lire les assets precedant les metres.
// between each biome that the player must dig through at (0,0), and that the UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
// 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"))
int32 InterStrateGapChunks = 0; 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 // 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. // 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. // 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() GENERATED_BODY()
public: public:
virtual void Serialize(FArchive& Ar) override;
virtual void PostLoad() override; virtual void PostLoad() override;
//========================================================================= //=========================================================================
@@ -66,14 +67,14 @@ public:
// DIMENSIONS // DIMENSIONS
//========================================================================= //=========================================================================
// How many chunks tall this strate is. // Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
// Each strate can have a different height! // Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
// 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"))
int32 StrateHeightInChunks = 16; int32 StrateHeightInChunks = 16;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
double StrateHeightMeters = 128.0;
//========================================================================= //=========================================================================
// BOUNDARY TRANSITION // BOUNDARY TRANSITION
//========================================================================= //=========================================================================
@@ -99,9 +100,13 @@ public:
// 2 → moderate (good default) // 2 → moderate (good default)
// 4 → wide, gentle blend across many chunks // 4 → wide, gentle blend across many chunks
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary", UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard")) meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
int32 TransitionBlendChunks = 2; 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 // GENERATOR TYPE
//========================================================================= //=========================================================================
@@ -115,6 +120,11 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork; 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 * 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 * 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)"), 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. * EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
* *
@@ -2289,6 +2491,11 @@ struct VOXELFORGE_API FStrateLandmark
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice; 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) ----- // ----- 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 // 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; FVector GetVoxelForgeCoreDefaultCenterWorld() const;
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const; bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const; bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate") UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const; int32 GetStrateAtPosition(FVector WorldPosition) const;
@@ -778,6 +779,8 @@ private:
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState; TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
FString VoxelForgeCoreWorldAssemblyJson; FString VoxelForgeCoreWorldAssemblyJson;
FString VoxelForgeCoreWorldAssemblyHash; FString VoxelForgeCoreWorldAssemblyHash;
FString VoxelForgeCoreResolvedLayoutJson;
FString VoxelForgeCoreResolvedLayoutIdentity;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector; FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
bool bCoreRegionCompilePending = false; bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles; TArray<FVoxelTileKey> PendingCoreRegionTiles;
@@ -18023,6 +18023,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const double CarveBrushRadiusCoreVoxels = const double CarveBrushRadiusCoreVoxels =
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm; CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4); const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
uint64 ExpectedEditDigest = 0; uint64 ExpectedEditDigest = 0;
std::array<std::uint64_t, 4> InitialAggregates{}; std::array<std::uint64_t, 4> InitialAggregates{};
std::array<std::uint64_t, 4> TravelAggregates{}; std::array<std::uint64_t, 4> TravelAggregates{};
@@ -18208,10 +18209,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
BiomeProbeSpanVoxels); BiomeProbeSpanVoxels);
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot)); 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()) 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; return false;
} }
ExpectedInitialTileCount = Desired.size(); ExpectedInitialTileCount = Desired.size();
@@ -18258,7 +18266,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
return false; 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) if (Arguments.bCoreHybridCliffOnly)
{ {
@@ -18332,6 +18356,47 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1; Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
else else
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1; 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; std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
AggregateRows.reserve(Desired.size()); AggregateRows.reserve(Desired.size());
bool bFoundCarveVertex = false; bool bFoundCarveVertex = false;
@@ -19367,7 +19432,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y) for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X) 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); ExpectedCollisionSet.Add(Key);
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key); const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
if (Record == nullptr) if (Record == nullptr)
@@ -21361,6 +21426,44 @@ int32 RunBatch(const FString& Params)
int32 UVoxelForgeExploreCommandlet::Main(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"))) if (FParse::Param(*Params, TEXT("step2f")))
{ {
return RunStep2f(Params); return RunStep2f(Params);
@@ -0,0 +1,233 @@
# VoxelForge U1 — Unreal catch-up report
Date: 2026-10-26. All builds, Unreal launches, logs, and asset reads used the isolated
`Saved/BuildHost` host project. The owner `VoxelM.uproject` and editor were not opened or built.
The RealtimeMeshComponent tree was read-only and unchanged. No `.uasset` was edited or saved.
## Research reports and code/API decisions
- Research14's first-build checklist was reviewed. BuildHost compiled Runtime, Editor, and Core;
`GetStrateForChunk` continues to receive the layout-chunk Z coordinate. The signed hash and bounded
shift changes are included in the clean MSVC build.
- Research17 asks for the optional CoreGeometry recipe, named stamps and cavern records, landmark
binding, and resolved-layout identity. These are reflected and mapped. The report refers to stamp
resolver V2, while the checked-out API includes V1/V2/V3 and requires V3 for the new
monolith/platform motif vocabulary. Unreal calls V1 for original stamps and V3 for motifs, then
combines and validates them. Source was followed.
- Research8 describes the full spatial-property inventory and notes a 4 m legacy chunk versus the
separate layout chunk. The current code resolves the layout chunk at 8 m; the migration follows
that saved setting. U1 migrates only strate height, transition width, and inter-strate gap. The
remaining inventory is listed under “Unit upgrade limits”; item 3 is therefore partial.
- Research27 water was read and deliberately not implemented this round.
## Part 1 — compile and BuildHost gates
Command:
```powershell
.\Tools\BuildVoxelForgeCoreHost.ps1 -MaxParallelActions 12 -TimeoutSeconds 900
```
Final result: `BuildHost UBT attempt 1 passed`; UBT reports `Result: Succeeded`. Runtime, Editor,
and Core inside Unreal compiled with MSVC. The Core link audit passed: 18 manifest shims linked,
118,399 `dumpbin` lines checked against 521 Unreal-style copied type names. Five existing asset
copy hashes matched the recorded BuildHost hashes. Log:
`Saved/BuildHost/UBT-CoreIntegration.log`; link audit:
`Saved/BuildHost/VoxelForgeCore_ue_link_safety.log`.
Compile errors fixed:
| Error | Cause and fix |
|---|---|
| `VoxelClipmapDesiredTiles.h(82)` C4334 | The shift is intentionally 64-bit at the supported coordinate bound. The left operand is now `int64{1}` before shifting by the validated level, avoiding the old 32-bit shift followed by widening. |
| `VoxelForgeCoreAdapter.cpp(114)` C2065 | Core moved the subvoxel constant into its namespace. The adapter now references `VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel`. |
| UHT: `Units` metadata on `TArray<FVector>` | Unreal permits that metadata on numeric and struct properties, not this array. Removed the invalid array metadata; each authored point is still interpreted and converted as metres by the adapter. |
| Adapter C2143 with cascading undeclared identifiers | An incomplete duplicate `AttachValidatedWorldAssembly` signature remained above the real definition after editing. Removed the duplicate line; the valid definition compiled. |
The initial memory preflight used `Get-CimInstance`, which was denied, then `Get-Counter`, which
could not find the requested counter. Neither launched Unreal. The scripts now use
`GlobalMemoryStatusEx`; measured free memory was above 4 GiB before launches. The Floating Islands
launch's separate process-list expression emitted a non-terminating PowerShell binding warning; a
second process check confirmed exactly one normal-priority Unreal process, and the gate completed.
### Gates
| Gate | Exact command | Result |
|---|---|---|
| SurfaceWorld, 25 cm | `.\Tools\TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25` | PASS: native SurfaceWorld, collision geometry, anchor movement, and carving; peak private memory 3,216,474,112 bytes. W1/W8/W16 stable: density `0x25d62110da7f3651`, mesh `0xd7184d5ad92b107f`, payload `0xf7dde7d6ec40fd34`, topology `0x60bbc7c8ef8bb309`. |
| Floating Islands, 25 cm | `.\Tools\TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25 -FloatingIslandsOnly` | PASS: standalone water-fixed audit, LOD0/1, W1/W8/W16; peak private memory 2,591,363,072 bytes. Coverage checked 573 local samples and 3,438 faces; 432 LOD transition faces. Standalone density/mesh/payload/topology hashes: `0xc2ba8f32b81dacec` / `0x91fe8a21be55324d` / `0x7a487c46247ce77c` / `0x1eaee14ac02b0a80`. |
| Core-off legacy | `.\Tools\TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25 -CoreOffTest` | PASS: legacy voxel size 12.5 cm; 3 density samples; peak private memory 1,536,880,640 bytes. |
| M2 analytic fixtures | `.\Tools\TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25 -CoreMesherRecipe WorldAssemblerHybridV1 -HybridFixturesOnly` | PASS: 5 analytic fixtures and 1 border check; peak private memory 1,988,993,024 bytes. This is the focused M2 fixture gate, not all 625 transitions. |
### Cold spawn and high-speed traces
Each trace used a fresh BuildHost Unreal process, the current owner plan, Core at 25 cm, and normal
priority. Trace files and full command logs are under
`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/`.
Cold owner-plan command:
```powershell
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25 -StreamingTraceOnly
```
The 2,578-tile initial set was fully ready in 5.779 s. The first LOD0 ring contained 512 tiles; its
ready event arrived in 1.794 s with 1.017 s reported for that epoch. Summing `worker_task_s` from
tile records through `initial_set_ready` gives 34.777 worker-seconds. The runtime summary reports
spawn tick p95/max 5.136/21.230 ms and walk tick p95/max 4.467/42.794 ms at 6 m/s. Per-LOD0 tile
worker generation was p95/max 32.835/52.929 ms. Peak private memory was 2,906,755,072 bytes.
High-speed command:
```powershell
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25 -StreamingTraceOnly -TestMoveSpeedCmPerSecond 4000 -EnforceCollisionGate
```
PASS, peak private memory 3,172,507,648 bytes. The initial set was ready in 7.858 s (2,578 loaded
at the initial milestone); the first ring had 512 tiles, arrived at 2.070 s, and reported 0.913 s.
Initial tile-worker sum was 36.056 worker-seconds. End-of-run desired/loaded counts were 2,582/4,816. Spawn tick
p95/max was 3.354/12.768 ms; walk tick p95/max was 39.577/50.165 ms at 40 m/s. The trace records
58 obsolete tile aborts during the high-speed walk.
The final timing processes used these BuildHost plugin binaries (SHA-256):
| Module | Binary | SHA-256 |
|---|---|---|
| Runtime | `UnrealEditor-VoxelForge.dll` | `D1D889293F4AB94FD7B2B6DB376BC3395A070AE929B46387A23022F84F351CCE` |
| Core | `UnrealEditor-VoxelForgeCore.dll` | `B8801421D1757B00FF8106789DC3C368162E585DD0CD70A8F20959FF362D81B4` |
| Editor | `UnrealEditor-VoxelForgeEditor.dll` | `1A5692D336A9D2A66D7CE699CC7F91BA77B35962BAA7FC2EC36E3F1F443F2DE0` |
Against the 2026-10-20 L14 reference (full set about 3.9 s, ring 0.50 s, LOD0 1.11 s, 15.8
worker-seconds, tick p95 about 2.7 ms), this cold trace is slower: full-set time +48%, ring epoch
time about +103%, first-ring wall time +62% against the 1.11 s LOD0 reference, initial worker sum
about +120%, and 6 m/s spawn/walk p95 are +90%/+66%. The measured first-ring wall event is 1.794 s.
The 40 m/s trace also has a high walk p95, with rapid
tile churn and 58 obsolete results visible in the trace. Quality gates pass, but this performance
regression is not explained by a controlled A/B; the L15–L61 Core source changed since L14, so no
single cause is claimed.
## Part 2 — CoreGeometry wiring
`UVoxelStrateDefinition` now has an optional CoreGeometry section. `None` and empty arrays are the
defaults. Recipe options are native caves, native lattice, and profiled heightfield (periodic hills,
mirrored relief, or flat profile). Assets can author stable-named SDF stamps, rooms, and tunnel
records in metres. The adapter plans passages first, resolves stamps and graph records next,
validates, and then finalizes the layout. A stable landmark name binds to the resolved record. Core
geometry anchors are read from the finalized JSON; UObject and RMC work stays on the game thread.
### Core capability access from Unreal
| Core capability from L15–L61 | Selectable from an Unreal asset? | Wired or remaining |
|---|---|---|
| Native caves | Yes | `CoreGeometry.GeneratorRecipe=NativeCaves` plus stable-named room/tunnel records; requires TunnelNetwork or Underwater. |
| Profiled heightfields | Yes | SurfaceWorld only; periodic hills, mirrored relief, or flat profile with metre fields. |
| Native lattice | Yes | `NativeLattice` plus room/tunnel/polyline records; requires Maze or VerticalShafts. Graph geometry is authored, not route-planned by the asset layer. |
| Cavern graph | Yes | Stable room and tunnel records reach `ResolveResolvedCavernGraphV1`. |
| SDF stamps | Yes | Stable-named carve ellipsoid/arch and fill pillar/dais stamps use V1 resolver. |
| Motifs | Yes, for monolith/platform | FillMonolith and FillPlatform use stamp vocabulary V3; Research17's V2 name differs from current source. |
| Core slab generator | No | Legacy FlatPlain and CrystalChamber remain available, but CoreGeometry has no Core slab recipe option or adapter mapping. |
| Ore veins | No | Core exposes an ore-vein request/resolver API, but CoreGeometry has no reflected request field or adapter mapping. |
No water feature was added.
### No-CoreGeometry and owner asset hashes
All five BuildHost copies retain their recorded hashes before and after the audit; the migration
commandlet only loaded and inspected them. The old assets have no authored CoreGeometry, so the new
recipe remains Off and all arrays remain default-empty. Current owner-plan W1/W8/W16 output hashes
are recorded above. The current owner-plan assembly hash is `2d9e50e902e140e8`. The L14 pre-round Core output hash is not byte-comparable: L15–L61 changed Core
in the meantime. Thus the asset hashes and the current no-geometry hash matrix show the new fields
are inactive on the owner's assets; they do not claim byte identity with L14.
## Part 3 — unit upgrade status and asset audit
`VoxelForgeAssetVersion::GUID` is `A76D5E31-4B2C-4F9A-B8C1-D2046E9F7738`; current version is 2
(version 1 retains the tunnel-floor slope conversion). Loading assets with version below 2 converts
old integer layout counts once. The version is carried through Unreal's custom-version archive. The
five unmodified BuildHost copies loaded successfully in the clean audit, commandlet peak private
memory 1,608,806,400 bytes. `Saved/BuildHost/AssetMigrationAudit.final.log` contains the values.
| BuildHost asset | Old serialized value | In-memory metre value | Physical delta |
|---|---|---|---:|
| DA_Settings | gap 0 layout chunks; layout chunk 8 m | gap 0 m | 0 m |
| DA_Strate1 | height 64 chunks; transition 1 chunk | height 512 m; transition 8 m | 0 m |
| DA_Strate2 | height 30 chunks; transition 3 chunks | height 240 m; transition 24 m | 0 m |
| DA_Strate3 | height 16 chunks; transition 2 chunks | height 128 m; transition 16 m | 0 m |
| DA_Strate4 | height 16 chunks; transition 2 chunks | height 128 m; transition 16 m | 0 m |
Every conversion is exact at the saved 8 m layout width. The old and new numeric representations
differ because the units differ; the resulting physical dimensions change by 0 m. The settings
PostLoad also sees DA_Strate4 through its fixed-strata reference; the direct audit then loads all
four definitions. No packages were saved.
BuildHost asset SHA-256 after audit (same values enforced by the BuildHost script before the audit):
| Asset | SHA-256 |
|---|---|
| DA_Settings.uasset | `8ABE8243104980260FECA0A6F4CF4927768E8E568221A6F6A2C2694F6D15D454` |
| DA_Strate1.uasset | `76063D691D5CE26D2014B587E1972A6724787BE1204E4652CA863BB8D5001BD0` |
| DA_Strate2.uasset | `02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2` |
| DA_Strate3.uasset | `F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C` |
| DA_Strate4.uasset | `7C0DE7D06876035AC7BC4F117B9166AFD77402021843BA4900B470F4B0D0185B` |
### Unit upgrade limits
The Research8 inventory is broader than the U1 migration. Automatic conversion is not yet wired for
legacy streaming/render/content chunk distances; decoration, brush, generator, biome, passage, shaft,
island, disturbance, terrain-op and landmark owner-voxel lengths/frequencies; SurfaceWorld/Core-voxel
lengths/frequencies; placement vectors, centimetre offsets, and other reflected spatial fields. The
new CoreGeometry fields themselves are metre-valued. The legacy fields continue to work in their
current units. Completing this inventory needs additional versioned fields/converters and consumer
updates; U1 does not claim that those settings now author metres.
## Async safety review
The diff leaves the existing safeguards intact:
- Generation `Epoch` travels with worker results and stale epochs are discarded before apply.
- All four result queues remain `TQueue<..., EQueueMode::Mpsc>`.
- `EndPlay` signals shutdown, waits for active tasks, then drains each queue.
- Workers check `bShuttingDown` before work and before publishing results.
- `UVoxelDiffLayer::ModsLock` still guards all concurrent modification-map access.
- Core layout resolution and content-anchor setup read UObjects on initialization; landmark updates
and RMC mutation are game-thread work. No new worker-side UObject or RMC mutation was introduced.
## Failed and retried runs
- Earlier SurfaceWorld harness attempts exposed missing owner-to-Core length scale in imported JSON,
using world origin instead of `viewer_start`, and a collision reference centered on the wrong
anchor. The harness now supplies owner/Core scale and uses the manifest viewer and collision
center; the final SurfaceWorld gate passes.
- The first Floating Islands fixture anchor audit exceeded its 0.25 density residual because the
runtime uses finite-step column refinement. The diagnostic-only bound is now 0.5 Core voxel and
reports residual, Core-voxel and local-centimetre coordinates. Runtime generation was not changed;
the final island audit passes.
- An additional enforced-collision streaming run reached 900 seconds waiting on support tile
`(-5,0,-31)` with 2,917/2,918 desired and 2,914/2,917 loaded. It was stopped at the time box and
not repeated. The normal SurfaceWorld gate and the requested 40 m/s trace passed.
- Standalone fast verification was attempted with
`py Standalone/Tools/verify/verify.py fast --out Saved/BuildHost/StandaloneVerifyFast --overall-timeout 900`.
It exited before the tier started because Windows Python has no `signal.SIGALRM`; no standalone
tests ran, and no verify output was written outside BuildHost.
- First asset-audit launch exited 3 because the DDC graph had no writable node. A retry loaded and
reported all five assets, but Unreal returned exit 1 due the default user-profile DDC diagnostic.
Those two logs show default-cache write probes failing with `WriteError: 3` (path not found); no
write succeeded. The clean third launch isolated `LOCALAPPDATA` under BuildHost and exited 0. No
new `UECC-*` folder appeared; the only folder is the pre-existing 2026-09-17 crash folder.
- No run exited with `0xC0000142`; no such retry was needed.
## Likely compile spots for the next Core/API merge
- `VoxelForgeCoreAdapter.cpp`: Core `ResolvedLayoutSnapshotV3`, stamp V1/V3, cavern graph resolver,
profile field names, stable-ID conversion, and passage/resolver/finalize ordering.
- `VoxelStrateTypes.h`: UHT support for nested CoreGeometry structs, `FVector` metre metadata, and
polyline array metadata (the array itself cannot carry `Units`).
- `VoxelSettings.cpp`, `VoxelStrateDefinition.cpp`, and `VoxelForgeAssetVersion.h`: custom-version
registration, ordering with the existing version-1 tunnel-floor migration, and serialization.
- `VoxelForgeExploreCommandlet.cpp`: BuildHost-only asset audit command and editor-module access to
the runtime types.
- `VoxelClipmapDesiredTiles.h`: signed/64-bit coordinate shifts and clamped LOD bounds.
- `VoxelStrateManager.cpp`: keep layout chunk Z as the lookup input and convert physical bounds
only once at the layout boundary.
Large compiler, Unreal, and trace outputs remain under `Saved/BuildHost`.