fix(unreal): U1 -- Unreal compiles again on L61; CoreGeometry asset section; versioned layout-unit upgrade
Windows MSVC catch-up after abyss L15-L61 (no Unreal there): 64-bit clipmap shift (C4334), qualified VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, UHT metadata and duplicate-signature fixes. Optional CoreGeometry section on UVoxelStrateDefinition (native caves, lattice, profiled heightfields, named stamps, rooms/tunnels; off/empty by default), resolved after passage planning. VoxelForgeAssetVersion 2 converts strate height/transition/gap to metres on load (exact, 0 m change on the owner's 5 assets; packages never saved). Gates pass (SurfaceWorld, FloatingIslands, Core-off, M2, W1/W8/W16). KNOWN REGRESSION vs L14 (2026-10-20, same command, same PC): full set 3.9 -> 5.37 s, worker-seconds 15.8 -> 30, LOD0 1.11 -> 1.69 s, ring 0.50 -> 0.80 s; not yet attributed (Core L15-L61 vs Unreal side). Report: Standalone/Research/WorldAssemblerM3/Unreal1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32021930d2
commit
fc9227f9e0
22 files changed
+1420
-49
No files matched your search
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
Reference in new issue
Block a user