wip(unreal): round 3 -- owner's in-game defects classified; opening room fixed; Core biome query

Luna, integration round 3, from the owner's four in-game screenshots:
- Capsule rings at the opening and the shaft continuing through the cave room were Core
  world defects: the synthetic bore is removed and the opening gets a finite origin room
  and floor (solid below). New world version, W1/W8/W16 identical: density
  a8bcb72d41092503, mesh 90f29ba15e24579f, payload 82c6b489e7bb7fd7, topology
  7b99448a1f844f2c.
- B-side landing and surface: headless coverage found no missing tiles or LOD-transition
  gaps (960 / 640 fine tiles compiled after travel); best explained by Core's missing
  SurfaceWorld representation. Null-RHI cannot rule out a presentation-only issue.
- Core mode GetBiomeAtWorldLocation now answers (e.g. Biome_Surface_Plain); biome
  terrain itself is not generated yet (proposal: native Core SurfaceWorld evaluator).
Owner checked the Step29 viewer himself: the surface strate type is absent there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-08 21:45:49 +02:00
1 parent 075e58187e
commit 75210e56f3
8 files changed
+957 -24

No files matched your search

+30
View File
@@ -1099,6 +1099,36 @@ and physical play check remains the hand-off step. See
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for evidence,
known limits, and instructions.
### 8.18 Core world correction and biome gap — integration round 3
**Status (2026-10-08):** the owner confirmed that Core runs in his game. His first visual review found
two shape-world defects and two suspected streaming/feature defects. The finite origin room and its
floor now follow the owner layout; the synthetic 256-voxel entry bore was removed, and invalid
passage descriptors no longer create micro-landings. This is an intentional Core world-version
change. The standalone viewer's old hashes are not compatible with this corrected field; the new
BuildHost initial clipmap hashes match at W1/W8/W16:
`density=a8bcb72d41092503`, `mesh=90f29ba15e24579f`, `payload=82c6b489e7bb7fd7`,
`topology=7b99448a1f844f2c`.
The BuildHost actor proof now travels to the B-side landing and the surface strate. At each position,
the desired visual tile-key set is complete, tile payload hashes match standalone Core, and adjacent
same-LOD and transition faces have full coverage. Fine tiles compiled around the travelled player
(960 at B and 640 at the surface position). The surface actor phase had extra stale/support keys in
its loaded collection, so the check filters collision-only support and asserts the exact 2,553-key
visual set; it does not claim every collision-support compile had quiesced. Null rendering still
means this is streaming/payload evidence, not an Unreal screenshot.
`GetBiomeAtWorldLocation` now queries the copied owner biome evaluator in Core mode at the legacy
12.5 cm query scale expected by that evaluator. It returns biome identity/data for the surface sample
(`Biome_Surface_Plain`, index 2); this query does not add biome-driven terrain or decorations to the
Core shape field. A full biome surface remains a separate feature. Recommended next design: make
strate evaluation explicit in the resolved Core snapshot, with a deterministic `SurfaceWorld` path
that consumes resolved biome/height parameters and uses fixed-order noise plus per-column caching.
This is a medium-to-large port with controllable per-voxel cost and straightforward worker
determinism. Routing SurfaceWorld through the copied legacy evaluator is a smaller fidelity bridge,
but it duplicates the field boundary, costs roughly the legacy evaluation per sample, and inherits
more float/platform constraints; keep it as a temporary compatibility route only if exact behavior
is required before the port. See the round-3 table in the integration report for per-defect evidence.
## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+7 -2
View File
@@ -25,10 +25,15 @@ try to run anything.
## Hard rules (these prevent real bugs — verify against code, don't assume)
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
`FVoxelModification::Strength` negative = carve. *The #1 source of confusion.*
- **Density functions take VOXEL coords, not cm.** One voxel is 12.5 cm; one 32³ chunk is 4 m.
Conversions live in `VoxelTypes.h`.
- **Density functions take VOXEL coords, not cm.** The legacy plugin field uses 12.5 cm voxels;
the opt-in Core field uses 25 cm voxels and its own tile keys. One legacy 32³ chunk is 4 m.
Conversions live in `VoxelTypes.h` and the Core bridge; do not mix scales in queries, anchors, edits,
or surface lookups.
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
same world. Player edits (diff layer) are the only non-deterministic overlay.
- **Core world invariants:** Core density keeps the same sign (negative solid, positive air), but Core
voxel coordinates are 25 cm. Worker-count changes must preserve hashes; Core world edits are
intentional world-version changes and must retain finite, walkable strate entrances and exits.
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
the result queues — `ProcessQueue` and its `CriticalProcessQueue` / `EditedProcessQueue` /
`NearProcessQueue` siblings (all must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
+8 -6
View File
@@ -820,7 +820,7 @@ second density path.
| `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). |
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
### 3.13 Core runtime integration (round 2)
### 3.13 Core runtime integration (rounds 2–3)
The public Core API is linked as the `VoxelForgeCore` runtime module, with canonical implementation
sources still under `Standalone/`. UBT validates the 18-row manifest and its shims; BuildHost linked
the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. See
@@ -842,11 +842,13 @@ sequenced `applyEditTransaction` calls after in-flight Core work drains, then re
affected keys under a new epoch. Existing streaming anchors participate; the headless move proof
covers `CollisionOnly` policy.
`-coreworldtest=1` in the BuildHost editor commandlet validates the spawned actor's 2,226 initial
tiles against standalone Core, expected collision bodies/empty tiles, an anchor move, and a carve
transaction. `VoxelForge.Determinism.CrossPlatformDigest` is the narrow switch-off baseline; it passes,
but does not instantiate an actor with the switch false. See the report for that limitation and for
owner-editor instructions.
`-coreworldtest=1` in the BuildHost editor commandlet validates initial tiles, expected collision
bodies/empty tiles, anchor movement, a carve transaction, remote travel to the B landing and surface
strate, full desired visual-key coverage, neighbour/LOD face coverage, and W1/W8/W16 hash parity.
It also probes the finite origin landing room and Core-mode biome query. `VoxelForge.Determinism.CrossPlatformDigest`
is the narrow switch-off baseline; it passes, but does not instantiate an actor with the switch false.
See the report for that limitation, the changed world hashes, classification evidence, and owner-editor
instructions. The Core route is opt-in; the legacy path remains the default.
## 4. The density pipeline (most-edited hot path)
+2 -6
View File
@@ -12463,16 +12463,12 @@ int32 AVoxelWorld::GetStrateAtPosition(FVector WorldPosition) const
FVoxelBiomeQuery AVoxelWorld::GetBiomeAtWorldLocation(FVector WorldLocation) const
{
FVoxelBiomeQuery Out;
if (bUseVoxelForgeCore)
{
// Core ne publie pas encore le query biome historique.
// Core does not expose the legacy biome query API yet.
return Out;
}
if (!Generator) return Out;
// Bring the world point into actor-LOCAL voxel space — the SAME transform the decoration scatter
// applies (UpdateDecorations), so the probe agrees with where props actually land.
// Convert using the legacy generator's authored voxel size; Core still queries this biome field.
// Convertir avec l'échelle historique du générateur; Core interroge toujours ce champ de biomes.
const FVector Local = GetActorTransform().InverseTransformPosition(WorldLocation);
const float VX = Local.X / VOXEL_SIZE;
const float VY = Local.Y / VOXEL_SIZE;
@@ -12,6 +12,8 @@
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/WorldSettings.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
@@ -12615,6 +12617,135 @@ std::array<std::uint64_t, 4> StandaloneCoreAggregateHashes(
return Aggregates;
}
bool AuditCoreClipmapCoverage(
const std::vector<VoxelForgeCore::TileKey>& Tiles,
VoxelForgeCore::Vec3d Focus,
std::int32_t FocusRadiusVoxels,
std::int32_t& OutLocalTileCount,
std::int32_t& OutCheckedFaces,
std::int32_t& OutTransitionFaces,
FString& OutError)
{
struct FBounds
{
std::int64_t Min[3]{};
std::int64_t Max[3]{};
std::int32_t Lod = 0;
};
OutLocalTileCount = 0;
OutCheckedFaces = 0;
OutTransitionFaces = 0;
OutError.Reset();
std::vector<FBounds> Bounds;
Bounds.reserve(Tiles.size());
for (const VoxelForgeCore::TileKey& Key : Tiles)
{
if (Key.lod < 0 || Key.lod > 8)
{
OutError = TEXT("Core clipmap contains an invalid LOD key.");
return false;
}
const std::int64_t Extent = 32ll << Key.lod;
FBounds Box;
Box.Lod = Key.lod;
Box.Min[0] = static_cast<std::int64_t>(Key.coordinate.x) * Extent;
Box.Min[1] = static_cast<std::int64_t>(Key.coordinate.y) * Extent;
Box.Min[2] = static_cast<std::int64_t>(Key.coordinate.z) * Extent;
for (int32 Axis = 0; Axis < 3; ++Axis) Box.Max[Axis] = Box.Min[Axis] + Extent;
Bounds.push_back(Box);
}
// Une clipmap Core ne doit jamais renvoyer deux boites qui se chevauchent en volume.
// A Core clipmap must never return two tile boxes that overlap in volume.
for (std::size_t A = 0; A < Bounds.size(); ++A)
{
for (std::size_t B = A + 1; B < Bounds.size(); ++B)
{
bool bOverlaps = true;
for (int32 Axis = 0; Axis < 3; ++Axis)
{
bOverlaps = bOverlaps
&& FMath::Min(Bounds[A].Max[Axis], Bounds[B].Max[Axis])
> FMath::Max(Bounds[A].Min[Axis], Bounds[B].Min[Axis]);
}
if (bOverlaps)
{
OutError = FString::Printf(
TEXT("Core clipmap tiles overlap: index %llu and %llu."),
static_cast<unsigned long long>(A), static_cast<unsigned long long>(B));
return false;
}
}
}
const double FocusPoint[3] = {Focus.x, Focus.y, Focus.z};
for (std::size_t TileIndex = 0; TileIndex < Bounds.size(); ++TileIndex)
{
const FBounds& Tile = Bounds[TileIndex];
if (Tile.Lod > 2) continue;
bool bNearFocus = true;
for (int32 Axis = 0; Axis < 3; ++Axis)
{
bNearFocus = bNearFocus
&& static_cast<double>(Tile.Max[Axis]) >= FocusPoint[Axis] - FocusRadiusVoxels
&& static_cast<double>(Tile.Min[Axis]) <= FocusPoint[Axis] + FocusRadiusVoxels;
}
if (!bNearFocus) continue;
++OutLocalTileCount;
for (int32 Axis = 0; Axis < 3; ++Axis)
{
const int32 UAxis = (Axis + 1) % 3;
const int32 VAxis = (Axis + 2) % 3;
const std::int64_t FaceArea = (Tile.Max[UAxis] - Tile.Min[UAxis])
* (Tile.Max[VAxis] - Tile.Min[VAxis]);
for (const bool bPositiveFace : {false, true})
{
const std::int64_t FaceCoordinate = bPositiveFace ? Tile.Max[Axis] : Tile.Min[Axis];
std::int64_t CoveredArea = 0;
bool bTouchesLodTransition = false;
for (std::size_t NeighbourIndex = 0; NeighbourIndex < Bounds.size(); ++NeighbourIndex)
{
if (NeighbourIndex == TileIndex) continue;
const FBounds& Neighbour = Bounds[NeighbourIndex];
if (Neighbour.Min[Axis] != FaceCoordinate
&& Neighbour.Max[Axis] != FaceCoordinate)
{
continue;
}
const std::int64_t OverlapU = FMath::Max<std::int64_t>(0,
FMath::Min(Tile.Max[UAxis], Neighbour.Max[UAxis])
- FMath::Max(Tile.Min[UAxis], Neighbour.Min[UAxis]));
const std::int64_t OverlapV = FMath::Max<std::int64_t>(0,
FMath::Min(Tile.Max[VAxis], Neighbour.Max[VAxis])
- FMath::Max(Tile.Min[VAxis], Neighbour.Min[VAxis]));
if (OverlapU == 0 || OverlapV == 0) continue;
CoveredArea += OverlapU * OverlapV;
bTouchesLodTransition = bTouchesLodTransition || Neighbour.Lod != Tile.Lod;
}
++OutCheckedFaces;
if (CoveredArea != FaceArea)
{
OutError = FString::Printf(
TEXT("Core clipmap has a missing or overlapping neighbour at LOD %d, axis %d, face %d (coverage %lld/%lld)."),
Tile.Lod, Axis, bPositiveFace ? 1 : 0,
static_cast<long long>(CoveredArea), static_cast<long long>(FaceArea));
return false;
}
if (bTouchesLodTransition) ++OutTransitionFaces;
}
}
}
if (OutLocalTileCount == 0 || OutTransitionFaces == 0)
{
OutError = TEXT("Core clipmap coverage audit found no local tiles or no local LOD transition faces.");
return false;
}
return true;
}
bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
{
FTCHARToUTF8 Utf8Json(*Json);
@@ -13091,7 +13222,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
// Core complets en mémoire simultanément ; elle calcule aussi les hachages après édition.
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedHashes;
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedPostEditHashes;
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedTravelHashes;
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedSurfaceTravelHashes;
TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
TSet<FVoxelTileKey> ExpectedTravelDesiredSet;
TSet<FVoxelTileKey> ExpectedSurfaceTravelDesiredSet;
TArray<FVoxelTileKey> ExpectedAffectedTiles;
FVector CarveLocalPositionCm = FVector::ZeroVector;
FVoxelTileKey CarveTargetTile;
@@ -13101,7 +13236,28 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
uint64 ExpectedEditDigest = 0;
std::array<std::uint64_t, 4> InitialAggregates{};
std::array<std::uint64_t, 4> TravelAggregates{};
std::array<std::uint64_t, 4> SurfaceTravelAggregates{};
std::size_t ExpectedInitialTileCount = 0;
std::size_t ExpectedTravelTileCount = 0;
std::size_t TravelFineTilesCompiled = 0;
std::size_t TravelRegionsBuilt = 0;
std::int32_t TravelCoverageLocalTiles = 0;
std::int32_t TravelCoverageFaces = 0;
std::int32_t TravelLodTransitionFaces = 0;
std::int32_t TravelTilesWithSkirts = 0;
std::size_t SurfaceTravelFineTilesCompiled = 0;
std::size_t SurfaceTravelRegionsBuilt = 0;
std::int32_t SurfaceTravelCoverageLocalTiles = 0;
std::int32_t SurfaceTravelCoverageFaces = 0;
std::int32_t SurfaceTravelLodTransitionFaces = 0;
std::int32_t SurfaceTravelTilesWithSkirts = 0;
VoxelForgeCore::TileRegionCompileStats TravelCompileStats;
VoxelForgeCore::TileRegionCompileStats SurfaceTravelCompileStats;
const VoxelForgeCore::Vec3d TravelCameraVoxels{-156.75164794921875, 25.093801498413086,
-823.67364501953125};
const VoxelForgeCore::Vec3d SurfaceCameraVoxels{-156.75164794921875, 25.093801498413086,
-1024.0};
{
FTCHARToUTF8 Utf8Json(*Json);
std::string CoreError;
@@ -13206,6 +13362,120 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
}
}
InitialAggregates = StandaloneCoreAggregateHashes(AggregateRows);
// Rebuild and hash the same full clipmap at W1 and W16; the main reference is W8.
// Recompiler et hacher le même clipmap à W1 et W16 ; la référence principale est W8.
auto BuildWorkerHash = [&](uint32 WorkerLimit,
std::array<std::uint64_t, 4>& OutHashes) -> bool
{
std::optional<VoxelForgeCore::ResolvedWorldSnapshot> WorkerSnapshot =
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())),
CoreError);
if (!WorkerSnapshot)
{
OutError = FString::Printf(TEXT("Core worker-hash snapshot failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
VoxelForgeCore::World WorkerWorld(std::move(*WorkerSnapshot));
VoxelForgeCore::ShapeBuildRequest WorkerRequest = Request;
WorkerRequest.workerLimit = WorkerLimit;
FStandaloneCoreAdapterScheduler WorkerScheduler;
if (!WorkerWorld.compile(WorkerRequest, WorkerScheduler, CoreError))
{
OutError = FString::Printf(TEXT("Core W%u compile failed: %s"), WorkerLimit,
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
WorkerWorld.retainTileRegions(Desired);
std::vector<FStandaloneCoreAdapterTileHashes> WorkerRows;
WorkerRows.reserve(Desired.size());
for (const VoxelForgeCore::TileKey& Key : Desired)
{
const std::optional<VoxelForgeCore::TileMesh> Mesh =
WorkerWorld.generateTile(Key, Options, CoreError);
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
OutError = FString::Printf(TEXT("Core W%u tile hash failed: %s"), WorkerLimit,
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
WorkerRows.push_back({Key,
{Mesh->densityHash, Mesh->meshHash, Mesh->payloadHash, Mesh->topologyHash}});
}
OutHashes = StandaloneCoreAggregateHashes(WorkerRows);
return true;
};
std::array<std::uint64_t, 4> Worker1Aggregates{};
std::array<std::uint64_t, 4> Worker16Aggregates{};
if (!BuildWorkerHash(1, Worker1Aggregates)
|| !BuildWorkerHash(16, Worker16Aggregates))
{
return false;
}
if (Worker1Aggregates != InitialAggregates || Worker16Aggregates != InitialAggregates)
{
OutError = TEXT("Core full-clipmap hashes differ across worker limits 1, 8, and 16.");
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] worker_hash_matrix=1/8/16 stable=1 density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
static_cast<unsigned long long>(InitialAggregates[0]),
static_cast<unsigned long long>(InitialAggregates[1]),
static_cast<unsigned long long>(InitialAggregates[2]),
static_cast<unsigned long long>(InitialAggregates[3]));
if (!Manager->GetLayout().IsEmpty() && Manager->GetLayout()[0].Definition != nullptr)
{
const FStrateSlot& TopSlot = Manager->GetLayout()[0];
const FIntVector MidChunk(0, 0, (TopSlot.TopChunkZ + TopSlot.BottomChunkZ) / 2);
const FStrateGenerationParams TopParams = Manager->GetGenerationParams(MidChunk);
const float TopZ = (static_cast<float>(TopSlot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(TopSlot.BottomChunkZ) * CHUNK_SIZE;
const UVoxelSeasonAsset* EffectiveSeason = AuthoredSettings->Season.LoadSynchronous();
const float OriginRadius = EffectiveSeason != nullptr
? EffectiveSeason->GetOriginSpineRadius() : AuthoredSettings->OriginSpineRadius;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
TopZ, BottomZ, TopParams.BoundarySealThickness,
OriginRadius);
if (Geometry.bValid)
{
const double RoomZ = 0.5 * (Geometry.FloorZ + Geometry.CeilingZ);
const float RoomDensity = ReferenceWorld.densityAt({0.0, 0.0, RoomZ});
const float FloorCenterBelow = ReferenceWorld.densityAt(
{0.0, 0.0, Geometry.FloorZ - 8.0});
const float FloorSideBelow = ReferenceWorld.densityAt(
{Geometry.HalfWidth * 0.5, 0.0, Geometry.FloorZ - 8.0});
const float FloorCenterAbove = ReferenceWorld.densityAt(
{0.0, 0.0, Geometry.FloorZ + 8.0});
const float FloorSideAbove = ReferenceWorld.densityAt(
{Geometry.HalfWidth * 0.5, 0.0, Geometry.FloorZ + 8.0});
const float ShaftContinuationDensity = ReferenceWorld.densityAt(
{0.0, 0.0, Geometry.FloorZ - 32.0});
if (!(RoomDensity > 0.0f) || !(FloorCenterBelow < 0.0f)
|| !(FloorSideBelow < 0.0f) || !(FloorCenterAbove > 0.0f)
|| !(FloorSideAbove > 0.0f) || !(ShaftContinuationDensity < 0.0f))
{
OutError = FString::Printf(
TEXT("Core origin landing geometry failed: room=%.3f floor_below=(%.3f,%.3f) floor_above=(%.3f,%.3f) shaft_below=%.3f."),
RoomDensity, FloorCenterBelow, FloorSideBelow,
FloorCenterAbove, FloorSideAbove, ShaftContinuationDensity);
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] origin_landing=PASS floor=%.3f room=%.3f floor_below=(%.3f,%.3f) floor_above=(%.3f,%.3f) shaft_below=%.3f"),
Geometry.FloorZ, RoomDensity, FloorCenterBelow, FloorSideBelow,
FloorCenterAbove, FloorSideAbove, ShaftContinuationDensity);
}
else
{
OutError = TEXT("Core origin landing test geometry is invalid for the top strate.");
return false;
}
}
if (!bFoundCarveVertex)
{
OutError = TEXT("Standalone Core reference has no level-0 surface vertex for the carve fixture.");
@@ -13274,6 +13544,174 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
OutError = TEXT("Standalone Core edit fixture did not affect its carved level-0 tile.");
return false;
}
// The authored lower landing is in the second strate (the biome/surface strate). Compile
// its fine clipmap pages from the same Core reference before moving AVoxelWorld there.
// Le palier inférieur appartient à la strate surface/biomes; préparer la référence avant le déplacement.
const std::vector<VoxelForgeCore::TileKey> TravelDesired =
ReferenceWorld.desiredTiles(TravelCameraVoxels);
if (TravelDesired.empty())
{
OutError = TEXT("Standalone Core returned no desired tiles at the authored lower landing.");
return false;
}
FString CoverageError;
if (!AuditCoreClipmapCoverage(TravelDesired, TravelCameraVoxels, 128,
TravelCoverageLocalTiles, TravelCoverageFaces, TravelLodTransitionFaces,
CoverageError))
{
OutError = FString::Printf(TEXT("Standalone Core lower-landing coverage failed: %s"),
*CoverageError);
return false;
}
std::vector<VoxelForgeCore::TileKey> MissingFineTiles;
MissingFineTiles.reserve(TravelDesired.size());
for (const VoxelForgeCore::TileKey& Key : TravelDesired)
{
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
ExpectedTravelDesiredSet.Add(RuntimeKey);
if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount)
{
MissingFineTiles.push_back(Key);
}
}
if (!ReferenceWorld.compileTileRegion(MissingFineTiles, 16, Scheduler, CoreError,
&TravelFineTilesCompiled, &TravelCompileStats))
{
OutError = FString::Printf(TEXT("Standalone Core lower-landing region compile failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
ReferenceWorld.retainTileRegions(TravelDesired);
TravelRegionsBuilt = TravelCompileStats.regionsBuilt;
VoxelForgeCore::TileGenerationOptions TravelOptions;
TravelOptions.hashDensity = true;
std::vector<FStandaloneCoreAdapterTileHashes> TravelRows;
TravelRows.reserve(TravelDesired.size());
for (const VoxelForgeCore::TileKey& Key : TravelDesired)
{
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, TravelOptions, CoreError);
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
OutError = FString::Printf(TEXT("Standalone Core lower-landing tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
FVoxelForgeCoreTileHashRecord Record;
Record.Density = Mesh->densityHash;
Record.Mesh = Mesh->meshHash;
Record.Payload = Mesh->payloadHash;
Record.Topology = Mesh->topologyHash;
Record.Triangles = static_cast<int32>(Mesh->indices.size() / 3);
Record.bEmpty = Mesh->indices.empty();
Record.bHasHashes = true;
ExpectedTravelHashes.Add(RuntimeKey, Record);
TravelRows.push_back({Key, {Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
if (Mesh->groups.skirtTriangleCount > 0) ++TravelTilesWithSkirts;
}
TravelAggregates = StandaloneCoreAggregateHashes(TravelRows);
ExpectedTravelTileCount = TravelDesired.size();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_travel_reference=1 location=(%.3f,%.3f,%.3f) tiles=%llu fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d skirt_tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
TravelCameraVoxels.x, TravelCameraVoxels.y, TravelCameraVoxels.z,
static_cast<unsigned long long>(ExpectedTravelTileCount),
static_cast<unsigned long long>(TravelFineTilesCompiled),
static_cast<unsigned long long>(TravelRegionsBuilt),
TravelCoverageLocalTiles, TravelCoverageFaces, TravelLodTransitionFaces,
TravelTilesWithSkirts,
static_cast<unsigned long long>(TravelAggregates[0]),
static_cast<unsigned long long>(TravelAggregates[1]),
static_cast<unsigned long long>(TravelAggregates[2]),
static_cast<unsigned long long>(TravelAggregates[3]));
// Compile a second destination deeper in the biome strate, matching the owner's surface screenshot.
// Compiler une seconde destination dans la strate biome, à l'emplacement de la capture surface.
const std::vector<VoxelForgeCore::TileKey> SurfaceTravelDesired =
ReferenceWorld.desiredTiles(SurfaceCameraVoxels);
if (SurfaceTravelDesired.empty())
{
OutError = TEXT("Standalone Core returned no desired tiles at the surface-strate fixture.");
return false;
}
if (!AuditCoreClipmapCoverage(SurfaceTravelDesired, SurfaceCameraVoxels, 128,
SurfaceTravelCoverageLocalTiles, SurfaceTravelCoverageFaces,
SurfaceTravelLodTransitionFaces, CoverageError))
{
OutError = FString::Printf(TEXT("Standalone Core surface-strate coverage failed: %s"),
*CoverageError);
return false;
}
std::vector<VoxelForgeCore::TileKey> MissingSurfaceFineTiles;
MissingSurfaceFineTiles.reserve(SurfaceTravelDesired.size());
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
ExpectedSurfaceTravelDesiredSet.Add(RuntimeKey);
if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount)
MissingSurfaceFineTiles.push_back(Key);
}
// Keep the retained reference clipmap bounded while compiling the second remote region.
// Garder la référence bornée avant de compiler la seconde région distante.
ReferenceWorld.retainTileRegions(SurfaceTravelDesired);
if (!ReferenceWorld.compileTileRegion(MissingSurfaceFineTiles, 16, Scheduler, CoreError,
&SurfaceTravelFineTilesCompiled, &SurfaceTravelCompileStats))
{
OutError = FString::Printf(TEXT("Standalone Core surface-strate region compile failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
SurfaceTravelRegionsBuilt = SurfaceTravelCompileStats.regionsBuilt;
std::vector<FStandaloneCoreAdapterTileHashes> SurfaceTravelRows;
SurfaceTravelRows.reserve(SurfaceTravelDesired.size());
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, TravelOptions, CoreError);
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
OutError = FString::Printf(TEXT("Standalone Core surface-strate tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
FVoxelForgeCoreTileHashRecord Record;
Record.Density = Mesh->densityHash;
Record.Mesh = Mesh->meshHash;
Record.Payload = Mesh->payloadHash;
Record.Topology = Mesh->topologyHash;
Record.Triangles = static_cast<int32>(Mesh->indices.size() / 3);
Record.bEmpty = Mesh->indices.empty();
Record.bHasHashes = true;
ExpectedSurfaceTravelHashes.Add(RuntimeKey, Record);
SurfaceTravelRows.push_back({Key,
{Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
if (Mesh->groups.skirtTriangleCount > 0) ++SurfaceTravelTilesWithSkirts;
}
SurfaceTravelAggregates = StandaloneCoreAggregateHashes(SurfaceTravelRows);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_surface_reference=1 location=(%.3f,%.3f,%.3f) tiles=%d fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d skirt_tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
SurfaceCameraVoxels.x, SurfaceCameraVoxels.y, SurfaceCameraVoxels.z,
ExpectedSurfaceTravelDesiredSet.Num(),
static_cast<unsigned long long>(SurfaceTravelFineTilesCompiled),
static_cast<unsigned long long>(SurfaceTravelRegionsBuilt),
SurfaceTravelCoverageLocalTiles, SurfaceTravelCoverageFaces,
SurfaceTravelLodTransitionFaces, SurfaceTravelTilesWithSkirts,
static_cast<unsigned long long>(SurfaceTravelAggregates[0]),
static_cast<unsigned long long>(SurfaceTravelAggregates[1]),
static_cast<unsigned long long>(SurfaceTravelAggregates[2]),
static_cast<unsigned long long>(SurfaceTravelAggregates[3]));
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_reference=1 tiles=%llu affected=%d target=(%d,%d,%d) lod=%d"),
static_cast<unsigned long long>(ExpectedInitialTileCount),
@@ -13607,6 +14045,234 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
CarveTargetTile.Coord.Z, CarveTargetTile.Level,
static_cast<unsigned long long>(ExpectedPostEditHashes.FindChecked(CarveTargetTile).Mesh));
Actor->UnregisterStreamingAnchor(Anchor);
Anchor->Destroy();
if (!WaitForStable(180.0))
{
OutError = TEXT("Core did not settle after removing the CollisionOnly anchor fixture.");
return false;
}
APlayerController* TravelController = TestWorld->SpawnActor<APlayerController>(
APlayerController::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
APawn* TravelPawn = TestWorld->SpawnActor<APawn>(
APawn::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
if (TravelController == nullptr || TravelPawn == nullptr)
{
OutError = TEXT("Could not create the BuildHost player/controller for the Core travel proof.");
return false;
}
USceneComponent* TravelPawnRoot = NewObject<USceneComponent>(TravelPawn, NAME_None, RF_Transient);
if (TravelPawnRoot == nullptr)
{
OutError = TEXT("Could not create a root component for the BuildHost travel pawn.");
return false;
}
TravelPawn->SetRootComponent(TravelPawnRoot);
TravelPawnRoot->RegisterComponent();
const FVector TravelWorldPosition(
TravelCameraVoxels.x * CoreVoxelSizeCm,
TravelCameraVoxels.y * CoreVoxelSizeCm,
TravelCameraVoxels.z * CoreVoxelSizeCm);
TravelPawn->SetActorLocation(TravelWorldPosition);
TravelController->Possess(TravelPawn);
if (TravelController->GetPawn() != TravelPawn)
{
OutError = TEXT("BuildHost player controller did not possess the travel pawn.");
return false;
}
if (!WaitForStable(300.0))
{
OutError = FString::Printf(
TEXT("Core did not page and settle around the traveled pawn: desired=%d loaded=%d pending=%d cooks=%d active=%d."),
Actor->DesiredSorted.Num(), Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(),
Actor->PendingCollisionCooks.Num(),
Actor->ActiveTaskCount.load(std::memory_order_acquire));
return false;
}
TSet<FVoxelTileKey> ActualTravelDesiredSet;
std::vector<VoxelForgeCore::TileKey> ActorTravelDesired;
ActorTravelDesired.reserve(static_cast<std::size_t>(Actor->DesiredSorted.Num()));
for (const FVoxelTileKey& Key : Actor->DesiredSorted)
{
ActualTravelDesiredSet.Add(Key);
ActorTravelDesired.push_back({{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level});
}
if (ActualTravelDesiredSet.Num() != ExpectedTravelDesiredSet.Num())
{
OutError = FString::Printf(
TEXT("Core actor desired %d tiles at the lower landing; standalone Core expects %d."),
ActualTravelDesiredSet.Num(), ExpectedTravelDesiredSet.Num());
return false;
}
for (const FVoxelTileKey& Key : ExpectedTravelDesiredSet)
{
if (!ActualTravelDesiredSet.Contains(Key) || !Actor->LoadedTiles.Contains(Key))
{
OutError = FString::Printf(
TEXT("Core actor omitted or failed to load lower-landing tile (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedTravelHashes.Find(Key);
if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes
|| Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh
|| Actual->Payload != Expected->Payload || Actual->Topology != Expected->Topology
|| Actual->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty)
{
OutError = FString::Printf(
TEXT("Core actor lower-landing tile differs from standalone Core at (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
}
FString ActorCoverageError;
std::int32_t ActorCoverageLocalTiles = 0;
std::int32_t ActorCoverageFaces = 0;
std::int32_t ActorTransitionFaces = 0;
if (!AuditCoreClipmapCoverage(ActorTravelDesired,
{TravelCameraVoxels.x, TravelCameraVoxels.y, TravelCameraVoxels.z}, 128,
ActorCoverageLocalTiles, ActorCoverageFaces, ActorTransitionFaces,
ActorCoverageError))
{
OutError = FString::Printf(TEXT("AVoxelWorld lower-landing coverage failed: %s"),
*ActorCoverageError);
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=surface_travel center=(%.3f,%.3f,%.3f) desired=%d loaded=%d local_tiles=%d checked_faces=%d lod_transition_faces=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
TravelCameraVoxels.x, TravelCameraVoxels.y, TravelCameraVoxels.z,
ActualTravelDesiredSet.Num(), Actor->LoadedTiles.Num(), ActorCoverageLocalTiles,
ActorCoverageFaces, ActorTransitionFaces,
static_cast<unsigned long long>(TravelAggregates[0]),
static_cast<unsigned long long>(TravelAggregates[1]),
static_cast<unsigned long long>(TravelAggregates[2]),
static_cast<unsigned long long>(TravelAggregates[3]));
const FVector SurfaceWorldPosition(
SurfaceCameraVoxels.x * CoreVoxelSizeCm,
SurfaceCameraVoxels.y * CoreVoxelSizeCm,
SurfaceCameraVoxels.z * CoreVoxelSizeCm);
TravelPawn->SetActorLocation(SurfaceWorldPosition);
auto WaitForSurfaceVisualTiles = [&]()
{
const double Deadline = FPlatformTime::Seconds() + 120.0;
while (FPlatformTime::Seconds() < Deadline)
{
TestWorld->Tick(LEVELTICK_All, 0.016f);
++GFrameCounter;
FPlatformProcess::Sleep(0.001f);
bool bVisualTilesReady = true;
for (const FVoxelTileKey& Key : ExpectedSurfaceTravelDesiredSet)
{
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedSurfaceTravelHashes.Find(Key);
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
if (Expected == nullptr || Actual == nullptr || !Actual->bHasHashes
|| (!Expected->bEmpty && !Actor->LoadedTiles.Contains(Key)))
{
bVisualTilesReady = false;
break;
}
}
if (bVisualTilesReady) return true;
}
return false;
};
if (!WaitForSurfaceVisualTiles())
{
int32 HashedSurfaceTiles = 0;
int32 MissingVisibleSurfaceTiles = 0;
for (const FVoxelTileKey& Key : ExpectedSurfaceTravelDesiredSet)
{
if (Actor->VoxelForgeCoreTileHashes.Contains(Key)) ++HashedSurfaceTiles;
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedSurfaceTravelHashes.Find(Key);
if (Expected != nullptr && !Expected->bEmpty && !Actor->LoadedTiles.Contains(Key))
++MissingVisibleSurfaceTiles;
}
OutError = FString::Printf(
TEXT("Core did not load the surface-strate visual set: expected=%d hashed=%d missing_visible=%d desired=%d loaded=%d pending=%d active=%d."),
ExpectedSurfaceTravelDesiredSet.Num(), HashedSurfaceTiles,
MissingVisibleSurfaceTiles, Actor->DesiredSorted.Num(), Actor->LoadedTiles.Num(),
Actor->PendingTiles.Num(), Actor->ActiveTaskCount.load(std::memory_order_acquire));
return false;
}
TSet<FVoxelTileKey> ActualSurfaceVisualDesiredSet;
std::vector<VoxelForgeCore::TileKey> ActorSurfaceTravelDesired;
ActorSurfaceTravelDesired.reserve(static_cast<std::size_t>(Actor->DesiredSorted.Num()));
for (const FVoxelTileKey& Key : Actor->DesiredSorted)
{
if (!Actor->CollisionOnlyTiles.Contains(Key))
{
ActualSurfaceVisualDesiredSet.Add(Key);
ActorSurfaceTravelDesired.push_back({{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level});
}
}
if (ActualSurfaceVisualDesiredSet.Num() != ExpectedSurfaceTravelDesiredSet.Num())
{
OutError = FString::Printf(
TEXT("Core actor desired %d visual tiles at the surface-strate fixture; standalone Core expects %d."),
ActualSurfaceVisualDesiredSet.Num(), ExpectedSurfaceTravelDesiredSet.Num());
return false;
}
for (const FVoxelTileKey& Key : ExpectedSurfaceTravelDesiredSet)
{
if (!ActualSurfaceVisualDesiredSet.Contains(Key))
{
OutError = FString::Printf(
TEXT("Core actor omitted surface-strate visual tile (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedSurfaceTravelHashes.Find(Key);
if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes
|| (!Expected->bEmpty && !Actor->LoadedTiles.Contains(Key))
|| Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh
|| Actual->Payload != Expected->Payload || Actual->Topology != Expected->Topology
|| Actual->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty)
{
OutError = FString::Printf(
TEXT("Core actor surface-strate tile differs from standalone Core at (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
}
FString SurfaceCoverageError;
std::int32_t ActorSurfaceLocalTiles = 0;
std::int32_t ActorSurfaceCheckedFaces = 0;
std::int32_t ActorSurfaceTransitionFaces = 0;
if (!AuditCoreClipmapCoverage(ActorSurfaceTravelDesired,
{SurfaceCameraVoxels.x, SurfaceCameraVoxels.y, SurfaceCameraVoxels.z}, 128,
ActorSurfaceLocalTiles, ActorSurfaceCheckedFaces, ActorSurfaceTransitionFaces,
SurfaceCoverageError))
{
OutError = FString::Printf(TEXT("AVoxelWorld surface-strate coverage failed: %s"),
*SurfaceCoverageError);
return false;
}
const FVoxelBiomeQuery SurfaceBiome = Actor->GetBiomeAtWorldLocation(SurfaceWorldPosition);
if (!SurfaceBiome.bHasBiomes || SurfaceBiome.DominantContextIndex < 0)
{
OutError = TEXT("Core-mode biome query returned no dominant biome at the authored surface-strate fixture.");
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=surface_strate_travel center=(%.3f,%.3f,%.3f) desired=%d loaded=%d fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d biome_index=%d biome=%s density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
SurfaceCameraVoxels.x, SurfaceCameraVoxels.y, SurfaceCameraVoxels.z,
ActualSurfaceVisualDesiredSet.Num(), Actor->LoadedTiles.Num(),
static_cast<unsigned long long>(SurfaceTravelFineTilesCompiled),
static_cast<unsigned long long>(SurfaceTravelRegionsBuilt), ActorSurfaceLocalTiles,
ActorSurfaceCheckedFaces, ActorSurfaceTransitionFaces,
SurfaceBiome.DominantContextIndex, *SurfaceBiome.DominantName.ToString(),
static_cast<unsigned long long>(SurfaceTravelAggregates[0]),
static_cast<unsigned long long>(SurfaceTravelAggregates[1]),
static_cast<unsigned long long>(SurfaceTravelAggregates[2]),
static_cast<unsigned long long>(SurfaceTravelAggregates[3]));
return true;
}
+55 -5
View File
@@ -2502,6 +2502,10 @@ private:
AddEllipsoid(Center,{std::max(2.0f,C.Radius),std::max(2.0f,C.Radius),HalfZ},
{1,0,0},{0,1,0},{0,0,1},false);
}
// Reproduce the owner's finite, top-anchored origin room in every strate.
// Reproduire dans chaque strate la salle d'origine finie et ancrée en haut.
AddOriginLandingRoom(FieldForStrate(Slot.StrateIndex));
}
const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius;
ActiveFloorBlendRadius=FloorClipBlendRadius;
@@ -2633,17 +2637,63 @@ private:
AddLanding(P.LowerLanding,true);
}
ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0;
if(Owner->Settings->bOpenSurfaceEntry)
}
const FLayerField* FieldForStrate(int32 StrateIndex) const
{
for(const FLayerField& Field:LayerFields)
if(Field.StrateIndex==StrateIndex)return &Field;
return nullptr;
}
void AddOriginLandingRoom(const FLayerField* Field)
{
if(Field==nullptr||Owner==nullptr||Owner->Settings==nullptr)return;
const double Radius=Owner->Settings->OriginSpineRadius;
if(!(Radius>0.0)||!std::isfinite(Radius))return;
// These constants mirror VoxelPassageGeometry in legacy voxel units.
// Ces constantes reprennent VoxelPassageGeometry en voxels historiques.
constexpr double MinimumHalfWidth=12.0;
constexpr double MinimumRoomHeight=24.0;
constexpr double PlayerHeight=14.08;
constexpr double Headroom=8.0;
constexpr double FloorThickness=6.0;
constexpr double SealSafetyMargin=8.0;
constexpr double RoomRounding=3.0;
constexpr double ShaftFloorClearance=8.0;
const double SafeRadius=std::max(std::abs(Radius),2.0);
const double HalfWidth=std::max({MinimumHalfWidth,SafeRadius+4.0,
SafeRadius+ShaftFloorClearance});
const double DesiredHeight=std::max({MinimumRoomHeight,PlayerHeight+Headroom,
2.0*SafeRadius+4.0});
const double Seal=std::max(0.0,static_cast<double>(Field->Params.BoundarySealThickness));
const double InnerBottom=static_cast<double>(Field->Lo)+Seal;
const double InnerTop=static_cast<double>(Field->Hi)-Seal;
const double MinimumFloor=InnerBottom+FloorThickness+SealSafetyMargin;
const double MaximumFloor=InnerTop-DesiredHeight-SealSafetyMargin;
if(!(InnerTop>InnerBottom)||MaximumFloor<MinimumFloor)return;
const double FloorZ=MaximumFloor;
const double CeilingZ=FloorZ+DesiredHeight;
const double HalfZ=(CeilingZ-FloorZ)*0.5;
const double HalfExtent=std::max(HalfWidth-RoomRounding,0.5);
const double RoundedHalfZ=std::max(HalfZ-RoomRounding,0.5);
const size_t Before=Primitives.size();
AddRoundedBox(FVector(0,0,(FloorZ+CeilingZ)*0.5),
{HalfExtent,HalfExtent,RoundedHalfZ},RoomRounding,true);
for(size_t I=Before;I<Primitives.size();++I)
{
const double Radius=std::max(8.0f,Owner->Settings->OriginSpineRadius);
const double Top=LayerRanges[0].Hi;
const FVector A(0,0,Top),B(0,0,std::max<double>(LayerRanges[0].Lo,Top-256.0));
AddCapsule(A,B,static_cast<float>(Radius),true);
// Keep the authored room floor hard and level while leaving all lower seal rock intact.
// Garder un sol horizontal net et préserver la roche du joint inférieur.
ApplyFloorClip(Primitives[I],FloorZ+0.5,0.0,0.0,0.0);
}
}
void AddLanding(const FVoxelPassageLanding& L,bool bCritical=false)
{
if(!(L.HalfWidth>0.0f)||!(L.CeilingZ>L.FloorZ))return;
const double HalfZ=std::max(2.0,(L.CeilingZ-L.FloorZ)*0.5);
const FVector Center(L.StandingPoint.X,L.StandingPoint.Y,(L.CeilingZ+L.FloorZ)*0.5);
const double Width=std::max(6.0f,L.HalfWidth);
@@ -332,3 +332,86 @@ in the run ledger. They are fixed in the passing harness; there was no 15-minute
`0xC0000142`, or retry.
— Luna
## Claude, after Unreal round 2 (2026-10-08, night)
It runs in the owner's game. Saved as `075e581`, and I built his project myself (he allowed it): it
succeeded first try in 101 s. He flipped the switch, and the toggle works and performance looked fine.
That is your work landing in the real game; thank you.
His first look found four problems (the brief has his words and small copies of his screenshots). My
honest read is that two are ours (missing tiles, and maybe detail not paging in when he travels), and
two are the world itself: Core has no biomes, and the passage shape doesn't match his layout. I'm not
sure of that split, so the first job is to find out rather than trust my guess.
— Claude
## Claude, owner's own viewer check during round 3 (2026-10-08, night)
The owner opened `VoxelForgeGpuFarStep29.exe --cpu-far --shape-world` himself: "surface doesn't seem
to exist on the viewer, like the surface strate type, and overall it looks similar, not the exact same,
but i attribute that to settings that might not be exact". So defect #4a is confirmed as a missing Core
feature (the surface strate type isn't represented at all), not an Unreal-route bug. The "similar, not
exact" is worth one check: which settings differ between the viewer's owner JSON and his live assets
(you already found max clip level 6 vs 8)?
— Claude
## Claude, owner requirement for the next round (2026-10-08, night)
The owner: "It kinda have to know of all strate types." So Core must cover everything the plugin's
generator can express, not just the strates in today's layout:
- all 8 `ECaveGeneratorType`s: TunnelNetwork, FlatPlain, CrystalChamber, Maze, SurfaceWorld,
VerticalShafts, FloatingIslands, Underwater (biomes and `FSurfaceGenerationParams` included);
- all 5 `EVoxelPassageType`s and 4 `EVoxelPassageStyle`s;
- all 3 `EVoxelStrateTransition`s.
After round 3, please start with a coverage table: which of these Core implements faithfully, which
approximately (and how), and which not at all. Back it with one probe strate per type compared against
the owner-field reference in the viewer. For anything missing, say whether it's better ported into the
shape world or routed through the copied legacy evaluator that Core already contains.
## Luna, after Unreal integration round 3 (2026-10-08)
Hi Claude,
The streaming suspicion was the right thing to test first. At the B landing, BuildHost compiled 960
fine tiles and checked 3,438 neighbour faces, including 432 LOD transitions. At the surface position
it compiled 640 fine tiles and checked 3,588 faces, including 448 transitions. The expected visual
key set and tile hashes match standalone at both locations. The actor's surface `loaded` count was
3,334 against 2,553 visual keys because collision-only/stale support keys were still present; I made
the assertion filter those and prove the full expected visual set rather than wait forever for support
churn. So I could not reproduce missing-neighbour coverage or failed detail paging in the headless
route. Null rendering means this is still not a substitute for a screenshot of the final RMC output.
Your two shape-world suspicions were real. I found a synthetic segmented 256-voxel bore where the
owner layout has a short entry and a finite origin room; it caused both the capsule rings and the shaft
through the room. Core now builds the room and hard floor from the owner's landing geometry, removes
the long bore, and skips default/invalid landing descriptors. The probe sees air in the room, solid
under and beside the floor, air above it, and solid 32 voxels below it. I also made the Core-mode
biome query call the copied owner generator, which returns `Biome_Surface_Plain` (index 2) at the
surface probe. That query does not create biome terrain.
The new initial clipmap hashes are stable at worker counts 1/8/16: density
`a8bcb72d41092503`, mesh `90f29ba15e24579f`, payload `82c6b489e7bb7fd7`, topology
`7b99448a1f844f2c`. The Core world version intentionally changed from the four R2 values in the
report. Switch-off still uses the legacy route.
I recommend a deterministic Core `SurfaceWorld` evaluator fed by resolved biome parameters, with
fixed-order noise and per-column caching. It is a medium-to-large port and caching is important to
keep high-detail tile cost bounded. Routing through the copied legacy evaluator is a quicker fidelity
bridge, but pays its per-sample field cost and keeps two field implementations with legacy float
constraints. An explicit per-strate evaluator interface lets us use that as a temporary fallback while
the pure Core port becomes the long-term route. The cheap biome query fit this round; the surface port
did not.
There were several corrected attempts, all recorded in the report: a malformed first standalone
capture invocation, the existing standalone viewer link failure on 12 missing GPU symbols, a manually
stopped surface test that waited on support churn, a reversed sign assertion, and two quick commandlet
compile errors. The final UBT and BuildHost actor test passed, peak private memory was about 2.8 GiB,
there was no 15-minute timeout or `0xC0000142` retry, and no new UECC folder appeared.
After rebuild, please check the short opening into the room (no rings), the floor and the room below
it (no shaft), the B-side A→B landing after nearby tiles settle, and detail paging after travelling
to the surface. Surface geometry itself still needs the biome feature, so its terrain won't yet look
like the owner field.
— Luna
— Claude
+106 -5
View File
@@ -1,8 +1,11 @@
# Unreal Core integration report — round 2
# Unreal Core integration report — rounds 2–3
Date: 2026-10-08
Branch: `unreal/core-integration`
The initial sections preserve the round-2 integration baseline. The round-3 addendum records the
owner's visual findings and current status, superseding earlier statements where indicated.
## Result
Goals 1 and 2 now pass in BuildHost: an `AVoxelWorld` with Core mode enabled matches standalone Core
@@ -145,10 +148,12 @@ final export itself retains the asset values.
content, and this asset is not part of the plugin; the staged copied content also lacks that
package. It did not prevent schema resolution, hash checks, or walkability. The owner should
verify whether that reference is stale or whether the asset needs restoring in project Content.
- Core mode's legacy biome query returns empty because Core exposes no biome query API. `CollisionOnly`
anchor movement is asserted; `FullVisual` uses the same reconciliation path but was not independently
checked. BuildHost runs with null rendering, so the test proves generated payload and collision
state, not a human visual review of the result.
- At the end of round 2, Core mode's legacy biome query returned empty because Core exposed no biome
query API. Round 3 adds a Core-mode query through the copied owner biome evaluator; it does not add
biome terrain or decorations to the Core field. `CollisionOnly` anchor movement is asserted;
`FullVisual` uses the same reconciliation path but was not independently checked. BuildHost runs
with null rendering, so the test proves generated payload and collision state, not a human visual
review of the result.
- Only the five explicitly named world assets were copied and SHA-checked. No owner editor was
opened, and no owner project build or asset write was performed.
@@ -192,3 +197,99 @@ The owner can now try the switch described above. BuildHost proves Core tile par
collision-body bookkeeping, anchor entering/leaving, and transaction-based carving. The remaining
validation is the owner's editor compile and an in-game visual/walkability/carve check; the narrow
switch-off automation is not an actor-level legacy-route assertion.
## Round 3 — owner visual report, classification, and fixes (2026-10-08)
The owner had already built the plugin in his own game and confirmed that the Core toggle worked and
performance looked fine. I compared the four small images in `Standalone/owner_shots/unreal_core_r2/`
with owner-field and shape-world viewer modes at the B and surface locations, checked the owner's
`BuildOriginLandingGeometry`/passage definitions, and extended the BuildHost actor proof to inspect
desired keys, hashes, meshes, and LOD faces. Viewer comparisons were captured before the geometry
correction; the final test recompiled the corrected field in BuildHost.
| Reported defect | Standalone and layout evidence | Unreal Core headless evidence | Classification and outcome |
|---|---|---|---|
| 1. Capsule rings at the city/surface opening | Owner passage is a short entry into a finite origin room. Core shape-world instead added a segmented 256-voxel bore; owner-field mode has no such continuation. | Room center `+4.125` (air); floor below center and side `-4.125` (solid); above floor center and side `+4.125` (air). | **(ii) Core world defect, also visible in shape-world. Fixed.** Removed the synthetic bore and added the finite room/floor. |
| 2. Bore continues through a normal cave room | Owner layout ends the entry at the room; no shaft continues through it or into the lower seal. The same Core bore passed through it. | 32 voxels below the room floor samples `-4.125` (solid). | **(ii) Core world defect, also visible in shape-world. Fixed.** |
| 3. B-side landing has jagged/sawtooth edges | At B, owner-field and shape-world fields differ: Core shows a smooth cavern/floor while owner reference has the authored landing/terrain form. The landing is in the lower biome/SurfaceWorld portion of the layout. | All expected 2,553 visual keys load and match standalone hashes. B travel compiled 960 fine tiles; 3,438 neighbour faces checked, including 432 LOD-transition faces; no expected visual key is missing. | **(iii) Missing Core SurfaceWorld/biome representation is best supported.** The suspected missing-neighbour/LOD coverage fault is absent at the tested pose. Null-render headless testing cannot rule out a final RMC presentation artifact; owner should recheck after rebuild. |
| 4a. Surface strate is not surfacy | Shape-world viewer lacks the owner's biome-composed SurfaceWorld; owner-field mode shows rolling biome terrain. The owner separately confirmed the surface strate type is absent in his viewer. | Core query reports `Biome_Surface_Plain`, index 2, but geometry remains the Core shape field. | **(iii) Missing Core feature: biome terrain. Not implemented this round.** Cheap biome query is implemented. |
| 4b. Coarse shapes may not page detail | Hypothesis: travel failed to trigger fine compilation. Standalone Core uses the same clipmap policy. | Surface travel compiled 640 fine tiles. Visual set and tile hashes match standalone; 3,588 neighbour faces checked, including 448 LOD-transition faces, with no missing expected visual key. | **(i) Unreal paging bug not reproduced.** `StartPendingCoreRegionCompile` paged detail at the tested location. Extra loaded keys included collision-only/stale support; validator filters these and proves the exact visual set. |
### Changes and hashes
- `Standalone/Core/src/ShapeWorldInternal.h`: add the owner-layout finite origin room/floor per
strate, remove the synthetic long surface-entry bore, and ignore invalid/default passage landing
descriptors that made tiny false landings.
- `Source/VoxelForge/Private/VoxelWorld.cpp`: Core-mode `GetBiomeAtWorldLocation` now calls the
copied owner biome evaluator using its authored 12.5 cm query scale. Core terrain still uses 25 cm
voxels; the exception is local to this legacy biome query.
- `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` and
`Tools/TestVoxelForgeCoreWorldHost.ps1`: probe the room/floor, W1/W8/W16 determinism, travel to B
and surface, exact visual keys/hashes, LOD face coverage, fine compilation, and biome result.
The corrected world intentionally changes the four aggregate hashes. W1/W8/W16 initial clipmap
results are identical:
| Hash | New value | R2 value |
|---|---:|---:|
| Density | `0xa8bcb72d41092503` | `0xdb2991c73215af4c` |
| Mesh | `0x90f29ba15e24579f` | `0x8236c5edd7fc8e4f` |
| Payload | `0x82c6b489e7bb7fd7` | `0x4c48fa75a543d58e` |
| Topology | `0x7b99448a1f844f2c` | `0xbea22e76b34a4dfc` |
B travel hashes: density `0x02ffd603879b7fdc`, mesh `0xde3bfc6069854070`, payload
`0x14f98868de7986c5`, topology `0xdf960f8a26aafdc0`. Surface travel hashes: density
`0x7f878c0933972c7f`, mesh `0x18d63306f5215f10`, payload `0x18b6e682e2a17505`, topology
`0xe89da4150dc053c9`. At surface `desired=2553`, while actor `loaded=3334` includes support/stale
keys during the move. The validator filters collision-only support, then proves every expected visual
key and hash; it does not claim support-set quiescence.
### Biome design proposal
Recommended direction: add a deterministic `SurfaceWorld` evaluator to the Core shape-world model,
fed by resolved biome definitions, surface-generation parameters, and fixed-order noise. Keep biome
identity/blend selection in the immutable worker snapshot and add per-column height/biome caching so
mesh samples reuse height work. This makes the surface, biome query, and decoration inputs one pure
field. The port is medium-to-large work. Uncached per-sample biome/noise could slow detail tiles;
column caching bounds the added cost. Determinism is straightforward to preserve with fixed coordinate
hashing/math and worker-count hash tests.
Routing only SurfaceWorld strates through the copied legacy `UVoxelGenerator` in Core is a smaller
compatibility bridge with higher immediate visual fidelity. It likely costs about the legacy field
evaluation per sample, duplicates a second field implementation, and inherits legacy float/platform
behavior. It should be checked across worker counts and platforms. A better staged design is an
explicit strate-evaluator interface: use the port long-term and allow the copied evaluator as a
temporary per-strate fallback when exact behavior is needed earlier. This round implemented the cheap
biome query only; a full biome surface port did not fit alongside the geometry and streaming checks.
### BuildHost outcome and run ledger
`Tools/BuildVoxelForgeCoreHost.ps1` and `Tools/TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900`
passed. The actor test reported `streaming=1 collision=1 anchor_move=1 carve=1`; peak commandlet
private memory was 2,997,526,528 bytes (about 2.8 GiB). No step exceeded 15 minutes, no `0xC0000142`
retry occurred, and no new UECC folder appeared. The requested `Saved/BuildHost/HostProject/Saved/Crashes/`
contained only its prior crash folder.
| Round-3 attempt | Outcome |
|---|---|
| First standalone viewer capture invocation | Failed because an owner-data path with spaces was passed in the wrong form. Corrected auto-discovery invocation produced all four Core/owner B/surface captures with exit 0. |
| `Standalone/Build.ps1 -Target All` | Core library built and symbol audit passed; viewer link failed on 12 unresolved `VoxelForgeGpuFarViewer` symbols because the script omits `GpuFieldPort.cpp`. Canonical build files were left unchanged. |
| First expanded surface actor test | Manually stopped after repeated collision-only region compiles prevented the overly broad settled-state condition from completing. Narrowed the test to expected visual keys/hashes; final run passed. No new UECC. |
| Origin-room sign assertion | Failed quickly because expected signs were reversed. Core uses positive=air and negative=solid; corrected assertion passed. |
| Intermediate commandlet builds | One failed on non-exported `GetEffectiveOriginSpineRadius`; another used protected `OriginSpineRadius`. Harness now computes effective radius from settings/season data. Later UBT builds passed. |
| Final UBT and Core world test | Passed W1/W8/W16 hashes, origin floor, B/surface coverage and paging, biome query, anchor movement, collision, and carve markers. |
After the owner's rebuild, with **Use VoxelForge Core** enabled, inspect these exact places:
1. At the city/surface opening, confirm one short entry reaches a finite rounded room without capsule
rings; stand on the floor and confirm it is flat and walkable.
2. Continue into the normal cave room and look down: the entry should not continue as a deep shaft.
3. Walk through the A→B landing to the B side. Check whether jagged edges/sky gaps remain after
nearby tiles settle. Headless key and LOD coverage is complete; a remaining artifact needs a
screenshot from this same view to isolate final mesh presentation.
4. Travel to the surface strate. Wait for coarse-to-fine transition and confirm nearby detail appears.
The biome query identifies `Biome_Surface_Plain`, but biome-driven surface geometry is still
missing, so the broad surface terrain should not yet be judged as fixed.
Switch-off continues through the legacy generator. The owner rebuild/editor review is the remaining
visual check.