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:
1 parent
075e58187e
commit
75210e56f3
8 files changed
+957
-24
No files matched your search
@@ -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,
|
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for evidence,
|
||||||
known limits, and instructions.
|
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)
|
## 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
|
||||||
|
|||||||
@@ -25,10 +25,15 @@ try to run anything.
|
|||||||
## Hard rules (these prevent real bugs — verify against code, don't assume)
|
## Hard rules (these prevent real bugs — verify against code, don't assume)
|
||||||
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
|
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
|
||||||
`FVoxelModification::Strength` negative = carve. *The #1 source of confusion.*
|
`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.
|
- **Density functions take VOXEL coords, not cm.** The legacy plugin field uses 12.5 cm voxels;
|
||||||
Conversions live in `VoxelTypes.h`.
|
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 ⇒
|
- **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.
|
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
|
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
|
||||||
the result queues — `ProcessQueue` and its `CriticalProcessQueue` / `EditedProcessQueue` /
|
the result queues — `ProcessQueue` and its `CriticalProcessQueue` / `EditedProcessQueue` /
|
||||||
`NearProcessQueue` siblings (all must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
|
`NearProcessQueue` siblings (all must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
|
||||||
|
|||||||
+8
-6
@@ -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`). |
|
| `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). |
|
| `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
|
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
|
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
|
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
|
affected keys under a new epoch. Existing streaming anchors participate; the headless move proof
|
||||||
covers `CollisionOnly` policy.
|
covers `CollisionOnly` policy.
|
||||||
|
|
||||||
`-coreworldtest=1` in the BuildHost editor commandlet validates the spawned actor's 2,226 initial
|
`-coreworldtest=1` in the BuildHost editor commandlet validates initial tiles, expected collision
|
||||||
tiles against standalone Core, expected collision bodies/empty tiles, an anchor move, and a carve
|
bodies/empty tiles, anchor movement, a carve transaction, remote travel to the B landing and surface
|
||||||
transaction. `VoxelForge.Determinism.CrossPlatformDigest` is the narrow switch-off baseline; it passes,
|
strate, full desired visual-key coverage, neighbour/LOD face coverage, and W1/W8/W16 hash parity.
|
||||||
but does not instantiate an actor with the switch false. See the report for that limitation and for
|
It also probes the finite origin landing room and Core-mode biome query. `VoxelForge.Determinism.CrossPlatformDigest`
|
||||||
owner-editor instructions.
|
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)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
|
|||||||
@@ -12463,16 +12463,12 @@ int32 AVoxelWorld::GetStrateAtPosition(FVector WorldPosition) const
|
|||||||
FVoxelBiomeQuery AVoxelWorld::GetBiomeAtWorldLocation(FVector WorldLocation) const
|
FVoxelBiomeQuery AVoxelWorld::GetBiomeAtWorldLocation(FVector WorldLocation) const
|
||||||
{
|
{
|
||||||
FVoxelBiomeQuery Out;
|
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;
|
if (!Generator) return Out;
|
||||||
|
|
||||||
// Bring the world point into actor-LOCAL voxel space — the SAME transform the decoration scatter
|
// 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.
|
// 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 FVector Local = GetActorTransform().InverseTransformPosition(WorldLocation);
|
||||||
const float VX = Local.X / VOXEL_SIZE;
|
const float VX = Local.X / VOXEL_SIZE;
|
||||||
const float VY = Local.Y / VOXEL_SIZE;
|
const float VY = Local.Y / VOXEL_SIZE;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "GameFramework/Actor.h"
|
#include "GameFramework/Actor.h"
|
||||||
#include "GameFramework/GameModeBase.h"
|
#include "GameFramework/GameModeBase.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "GameFramework/WorldSettings.h"
|
#include "GameFramework/WorldSettings.h"
|
||||||
#include "HAL/FileManager.h"
|
#include "HAL/FileManager.h"
|
||||||
#include "HAL/PlatformProcess.h"
|
#include "HAL/PlatformProcess.h"
|
||||||
@@ -12615,6 +12617,135 @@ std::array<std::uint64_t, 4> StandaloneCoreAggregateHashes(
|
|||||||
return Aggregates;
|
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)
|
bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||||
{
|
{
|
||||||
FTCHARToUTF8 Utf8Json(*Json);
|
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.
|
// Core complets en mémoire simultanément ; elle calcule aussi les hachages après édition.
|
||||||
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedHashes;
|
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedHashes;
|
||||||
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedPostEditHashes;
|
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedPostEditHashes;
|
||||||
|
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedTravelHashes;
|
||||||
|
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedSurfaceTravelHashes;
|
||||||
TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
|
TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
|
||||||
|
TSet<FVoxelTileKey> ExpectedTravelDesiredSet;
|
||||||
|
TSet<FVoxelTileKey> ExpectedSurfaceTravelDesiredSet;
|
||||||
TArray<FVoxelTileKey> ExpectedAffectedTiles;
|
TArray<FVoxelTileKey> ExpectedAffectedTiles;
|
||||||
FVector CarveLocalPositionCm = FVector::ZeroVector;
|
FVector CarveLocalPositionCm = FVector::ZeroVector;
|
||||||
FVoxelTileKey CarveTargetTile;
|
FVoxelTileKey CarveTargetTile;
|
||||||
@@ -13101,7 +13236,28 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||||
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> SurfaceTravelAggregates{};
|
||||||
std::size_t ExpectedInitialTileCount = 0;
|
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);
|
FTCHARToUTF8 Utf8Json(*Json);
|
||||||
std::string CoreError;
|
std::string CoreError;
|
||||||
@@ -13206,6 +13362,120 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
InitialAggregates = StandaloneCoreAggregateHashes(AggregateRows);
|
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)
|
if (!bFoundCarveVertex)
|
||||||
{
|
{
|
||||||
OutError = TEXT("Standalone Core reference has no level-0 surface vertex for the carve fixture.");
|
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.");
|
OutError = TEXT("Standalone Core edit fixture did not affect its carved level-0 tile.");
|
||||||
return false;
|
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,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeCoreWorldTest] standalone_reference=1 tiles=%llu affected=%d target=(%d,%d,%d) lod=%d"),
|
TEXT("[VoxelForgeCoreWorldTest] standalone_reference=1 tiles=%llu affected=%d target=(%d,%d,%d) lod=%d"),
|
||||||
static_cast<unsigned long long>(ExpectedInitialTileCount),
|
static_cast<unsigned long long>(ExpectedInitialTileCount),
|
||||||
@@ -13607,6 +14045,234 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
|
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
|
||||||
CarveTargetTile.Coord.Z, CarveTargetTile.Level,
|
CarveTargetTile.Coord.Z, CarveTargetTile.Level,
|
||||||
static_cast<unsigned long long>(ExpectedPostEditHashes.FindChecked(CarveTargetTile).Mesh));
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2502,6 +2502,10 @@ private:
|
|||||||
AddEllipsoid(Center,{std::max(2.0f,C.Radius),std::max(2.0f,C.Radius),HalfZ},
|
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);
|
{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;
|
const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius;
|
||||||
ActiveFloorBlendRadius=FloorClipBlendRadius;
|
ActiveFloorBlendRadius=FloorClipBlendRadius;
|
||||||
@@ -2633,17 +2637,63 @@ private:
|
|||||||
AddLanding(P.LowerLanding,true);
|
AddLanding(P.LowerLanding,true);
|
||||||
}
|
}
|
||||||
ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0;
|
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);
|
// Keep the authored room floor hard and level while leaving all lower seal rock intact.
|
||||||
const double Top=LayerRanges[0].Hi;
|
// Garder un sol horizontal net et préserver la roche du joint inférieur.
|
||||||
const FVector A(0,0,Top),B(0,0,std::max<double>(LayerRanges[0].Lo,Top-256.0));
|
ApplyFloorClip(Primitives[I],FloorZ+0.5,0.0,0.0,0.0);
|
||||||
AddCapsule(A,B,static_cast<float>(Radius),true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddLanding(const FVoxelPassageLanding& L,bool bCritical=false)
|
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 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 FVector Center(L.StandingPoint.X,L.StandingPoint.Y,(L.CeilingZ+L.FloorZ)*0.5);
|
||||||
const double Width=std::max(6.0f,L.HalfWidth);
|
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.
|
`0xC0000142`, or retry.
|
||||||
|
|
||||||
— Luna
|
— 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
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
# Unreal Core integration report — round 2
|
# Unreal Core integration report — rounds 2–3
|
||||||
|
|
||||||
Date: 2026-10-08
|
Date: 2026-10-08
|
||||||
Branch: `unreal/core-integration`
|
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
|
## Result
|
||||||
|
|
||||||
Goals 1 and 2 now pass in BuildHost: an `AVoxelWorld` with Core mode enabled matches standalone Core
|
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
|
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
|
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.
|
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`
|
- At the end of round 2, Core mode's legacy biome query returned empty because Core exposed no biome
|
||||||
anchor movement is asserted; `FullVisual` uses the same reconciliation path but was not independently
|
query API. Round 3 adds a Core-mode query through the copied owner biome evaluator; it does not add
|
||||||
checked. BuildHost runs with null rendering, so the test proves generated payload and collision
|
biome terrain or decorations to the Core field. `CollisionOnly` anchor movement is asserted;
|
||||||
state, not a human visual review of the result.
|
`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
|
- 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.
|
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
|
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
|
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.
|
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.
|
||||||
Reference in new issue
Block a user