From 71d09a8c52f37e11f59ea4850803be2d78e27d67 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 20 Oct 2026 18:22:12 +0200 Subject: [PATCH] wip(assembler): round L23 -- inter-strate passages planned from the resolved layout and carved After L22 the FI -> SW passage no longer connected the strates (legacy owner-voxel coordinates, lower end 23 m above SW), and Core never carved passages through the bands. Now the director plans passages from the resolved layout (PlanResolvedLayoutPassagesV1; deterministic hash of seed/strates/connection; placement disk from PassageConfig, 8-24 m around (0,0)); shared composition applies generator field -> bands -> bounded passage carve and support shelf, opening the bands only along the tunnel. Mouths bind to the strate routes. Capsule audit: 0 blocked / 0 unsupported (325,962 samples FI, 284,319 TunnelNetwork), negative controls detected; all 8 generator kinds exercised. SW away from the passage is bit-identical. Layout V3, evaluator 9. Open: look not yet natural (stepped tube in FI void, terraced SW arrival); SW-as-upper route planning fails; Unreal adapter uncompiled. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX --- ARCHITECTURE.md | 27 + CODEMAP.md | 13 +- .../Private/VoxelForgeCoreAdapter.cpp | 63 +- .../include/VoxelForgeCore/VoxelForgeCore.h | 62 +- Standalone/Core/src/ResolvedLayoutInternal.h | 544 +++++++++++++++++- Standalone/Core/src/ShapeWorldInternal.h | 210 ++++++- Standalone/Core/src/VoxelForgeCore.cpp | 36 +- .../WorldAssemblerM3/Linux23/REPORT.md | 158 +++++ .../Linux23/captures/fi-mouth.jpg | Bin 0 -> 27270 bytes .../Linux23/captures/sw-arrival.jpg | Bin 0 -> 23849 bytes .../Linux23/captures/tunnel-interface.jpg | Bin 0 -> 16495 bytes Standalone/Source/StreamBenchmarkHeadless.cpp | 14 +- Standalone/Source/StreamBenchmarkPipeline.h | 346 ++++++++++- Standalone/Source/WorldAssemblyPrototype.h | 104 +++- 14 files changed, 1465 insertions(+), 112 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux23/captures/fi-mouth.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux23/captures/sw-arrival.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux23/captures/tunnel-interface.jpg diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 88c9221..f10aeb4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1924,6 +1924,33 @@ controls continue to reject malformed bands. Evidence and owner caveats are in [`Standalone/Research/WorldAssemblerM3/Linux22/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux22/REPORT.md). +### 8.41 Director-resolved inter-strate passages and walkable landings — World Assembler L23 + +Core passage geometry is derived after the director has resolved physical strate bands and native +route anchors. `PlanResolvedLayoutPassagesV1` joins every adjacent resolved pair. Its placement hash +uses `(seed, upper strate, lower strate, connection, salt)`; `PassageConfig.DistanceMax` is the +outer radius around world `(0,0)` (24 m for the owner's first interface), while `DistanceMin` is +the inner radius used when a mouth has no route anchor. A route-bound mouth reuses the upper +strate's departure or lower strate's arrival anchor, after checking it lies within `DistanceMax`. +The legacy `UVoxelStrateManager::GeneratePassages` and its owner-voxel control points remain the +legacy generator path; Core does not import the exported legacy `passages` array. The season +manifest carries recipes and plans, and the adapter rebuilds the resolved layout and its passages +from the current manager slots and route anchors each time it attaches the Core world. + +`ApplyResolvedStrateComposition` composes the current generator field, applies the resolved ceiling +and bedrock bands, then unions the passage carve and floor support. Only the hashed capsule path +opens a band; the support strip stays below the player capsule. The lower mouth index splits the +monotone descent from the short route-bound landing tail, whose height may vary with the route. +Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the +negative control compiles the same world without passage carving and must detect a blocked capsule. +The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands +or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons, +the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming, +and async rules remain unchanged. + +[`Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md) +records the audit and captures. + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index 1a552d3..4eb9b23 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -167,7 +167,7 @@ remains `0`, and lateral regions remain gated off. | `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. | | `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. | -| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV2`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, transition and dispatch provenance, recipe anchors, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40. | +| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, transition and dispatch provenance, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.41. | | `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. | | `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. | @@ -1155,6 +1155,17 @@ Geometry version 3 removes the FI lower all-XY seal while retaining its ceiling upper-bound path. Versions 1 and 2 retain their serialized two-sided behavior. See ARCHITECTURE §8.37 and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md). +### 3.24 Resolved inter-strate passages — World Assembler M3 L23 + +| Symbol | Canonical file | Role | +|---|---|---| +| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. | +| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. | +| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. | + +The evaluator version is 9. See ARCHITECTURE §8.41 and +[`Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 65526bd..7d136d4 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -991,7 +991,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, // Keep the inspectable physical-layout export separate from the manifest identity: it is // assembled from the same resolved slots/plans, but adding it must not salt evaluation hashes. - VoxelForgeCore::ResolvedLayoutSnapshotV2 Layout; + VoxelForgeCore::ResolvedLayoutSnapshotV3 Layout; Layout.worldIdentity = std::string("owner-world/seed/") + std::to_string(StrateManager->GetWorldSeed()); Layout.ownerVoxelSizeMicrometres = static_cast(std::llround(OwnerVoxelSizeCm * 10000.0)); @@ -1029,9 +1029,27 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, return static_cast(std::llround(static_cast(Fixed) * ExpectedCoreVoxelMicrons / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)); }; + std::vector PassageConfigs; for (const FStrateSlot& Slot : StrateManager->GetLayout()) { if (Slot.Definition == nullptr) continue; + const FStratePassageConfig& AuthoredPassage = Slot.Definition->PassageConfig; + const auto OwnerVoxelsToCoreQ = [OwnerVoxelMicrons, ExpectedCoreVoxelMicrons](double Value) + { + return static_cast(std::llround(Value * OwnerVoxelMicrons + * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel / ExpectedCoreVoxelMicrons)); + }; + VoxelForgeCore::ResolvedLayoutPassageConfigV1 PassageConfig; + PassageConfig.upperStrateIndex = Slot.StrateIndex; + PassageConfig.connections = AuthoredPassage.Connections; + PassageConfig.mouthRadiusSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.MouthRadius); + PassageConfig.midRadiusSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.MidRadius); + PassageConfig.reachMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMin); + PassageConfig.reachMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMax); + PassageConfig.distanceMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMin); + PassageConfig.distanceMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMax); + PassageConfig.wanderSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.Wander); + PassageConfigs.push_back(PassageConfig); const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk( FIntVector(0, 0, Slot.TopChunkZ)); VoxelForgeCore::ResolvedLayoutStratumV2 Item; @@ -1065,8 +1083,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate"; if (Type == ECaveGeneratorType::SurfaceWorld) { - Item.boundDispatchPath = "BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV2"; - Item.queryDispatchPath = "NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV2"; + Item.boundDispatchPath = "BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3"; + Item.queryDispatchPath = "NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3"; const auto Plan = std::find_if(Manifest->plan.strates.begin(), Manifest->plan.strates.end(), [&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; }); if (Plan != Manifest->plan.strates.end()) @@ -1097,8 +1115,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, } else if (Type == ECaveGeneratorType::FloatingIslands) { - Item.boundDispatchPath = "BuildFloatingIslandStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV2"; - Item.queryDispatchPath = "NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV2"; + Item.boundDispatchPath = "BuildFloatingIslandStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3"; + Item.queryDispatchPath = "NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3"; const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(), [&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; }); std::int64_t SealMicrometres = 0; @@ -1117,11 +1135,21 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels }; Item.anchors.push_back(std::move(Anchor)); } + for (std::size_t RouteIndex = 0; RouteIndex < Plan->manifest.plan.route.size(); ++RouteIndex) + { + const auto& Point = Plan->manifest.plan.route[RouteIndex]; + VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor; + Anchor.kind = "route_point"; Anchor.stableId = "route/" + std::to_string(RouteIndex); + Anchor.units = "1/256 Core voxel"; + Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1], + Point.positionSubvoxels[2], Point.radiusSubvoxels }; + Item.anchors.push_back(std::move(Anchor)); + } } } else { - Item.boundDispatchPath = "resolved manager slot scaled by ResolvedLayoutSnapshotV2 chunk width"; + Item.boundDispatchPath = "resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width"; Item.queryDispatchPath = "legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)"; } const std::int64_t SealMicrometres=static_cast(std::llround( @@ -1142,23 +1170,16 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, "none", "none" }; Item.content.bottomMicrometres+=SealMicrometres; Item.content.topMicrometres-=SealMicrometres; - for (std::size_t PassageIndex = 0; PassageIndex < StrateManager->GetPassages().Num(); ++PassageIndex) - { - const FVoxelPassage& Passage = StrateManager->GetPassages()[static_cast(PassageIndex)]; - if (Passage.UpperStrateIndex != Slot.StrateIndex && Passage.LowerStrateIndex != Slot.StrateIndex) continue; - VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor; - Anchor.kind = "passage"; Anchor.stableId = "passage/" + std::to_string(PassageIndex); - Anchor.units = "milli owner voxels"; - Anchor.values = { static_cast(std::llround(Passage.UpperPoint.X * 1000.0)), - static_cast(std::llround(Passage.UpperPoint.Y * 1000.0)), - static_cast(std::llround(Passage.UpperPoint.Z * 1000.0)), - static_cast(std::llround(Passage.LowerPoint.X * 1000.0)), - static_cast(std::llround(Passage.LowerPoint.Y * 1000.0)), - static_cast(std::llround(Passage.LowerPoint.Z * 1000.0)) }; - Item.anchors.push_back(std::move(Anchor)); - } Layout.strates.push_back(std::move(Item)); } + std::string PassagePlanError; + if (!VoxelForgeCore::PlanResolvedLayoutPassagesV1(Layout, + StrateManager->GetWorldSeed(), PassageConfigs, PassagePlanError)) + { + OutError = FString::Printf(TEXT("Resolved inter-strate passage planning failed: %s"), + UTF8_TO_TCHAR(PassagePlanError.c_str())); + return false; + } std::string LayoutError; if (!Layout.finalize(LayoutError)) { diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 1d326dc..9efcdf7 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -249,8 +249,8 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2; - // Version 7 binds plans to the physical strate-layout chunk width and shared boundary bands. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 7; + // Version 9 indexes passage landings so non-monotone route tails stay carveable. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 9; enum class FieldChannel : std::uint32_t { @@ -573,8 +573,8 @@ namespace VoxelForgeCore // Read-only, versioned account of the physical layout resolved by the director and the // Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their // source fixed-point units so this record never rounds the recipe itself. - inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV2 = 2; - enum class ResolvedLayoutValidationModeV2 : std::uint8_t { compatibility, strict }; + inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3; + enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; struct ResolvedLayoutAnchorV2 { @@ -635,34 +635,73 @@ namespace VoxelForgeCore std::string detail; }; - struct VFCORE_API ResolvedLayoutSnapshotV2 + struct ResolvedLayoutPassagePointV1 { - std::uint32_t schemaVersion = ResolvedLayoutSchemaVersionV2; + std::array positionSubvoxels{}; + std::int64_t radiusSubvoxels = 0; + }; + + struct ResolvedLayoutPassageV1 + { + std::string stableId; + std::int32_t upperStrateIndex = -1; + std::int32_t lowerStrateIndex = -1; + std::int64_t placementRadiusSubvoxels = 0; + std::array upperMouthSubvoxels{}; + std::array lowerMouthSubvoxels{}; + std::int64_t mouthRadiusSubvoxels = 0; + std::int64_t midRadiusSubvoxels = 0; + // Derived from the mouth binding; the main descent is monotone through this point. + std::size_t lowerMouthControlIndex = 0; + std::vector controlPoints; + }; + + struct ResolvedLayoutPassageConfigV1 + { + std::int32_t upperStrateIndex = -1; + std::int32_t connections = 1; + std::int64_t mouthRadiusSubvoxels = 0; + std::int64_t midRadiusSubvoxels = 0; + std::int64_t reachMinimumSubvoxels = 0; + std::int64_t reachMaximumSubvoxels = 0; + std::int64_t distanceMinimumSubvoxels = 0; + std::int64_t distanceMaximumSubvoxels = 0; + std::int64_t wanderSubvoxels = 0; + }; + + struct VFCORE_API ResolvedLayoutSnapshotV3 + { + std::uint32_t schemaVersion = ResolvedLayoutSchemaVersionV3; std::string worldIdentity; std::uint32_t ownerVoxelSizeMicrometres = 0; std::uint32_t coreVoxelSizeMicrometres = 0; std::uint32_t strateLayoutChunkSizeMicrometres = 8000000; std::int64_t strateLayoutChunkWidthSubvoxels = 8192; std::vector strates; + std::vector passages; std::string stableIdentity; bool finalize(std::string& error); - bool validate(ResolvedLayoutValidationModeV2 mode, + bool validate(ResolvedLayoutValidationModeV3 mode, std::vector& issues, std::string& error) const; std::string ToJson() const; - static std::optional FromJson( + static std::optional FromJson( std::string_view json, std::string& error); }; - struct ResolvedLayoutValidatorAuditV2 + struct ResolvedLayoutValidatorAuditV3 { std::uint32_t controlsRun = 0; std::uint32_t controlsRejectedAsExpected = 0; bool passed = false; std::vector failures; }; - VFCORE_API ResolvedLayoutValidatorAuditV2 AuditResolvedLayoutValidatorV2(); + VFCORE_API ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3(); + VFCORE_API bool PlanResolvedLayoutPassagesV1( + ResolvedLayoutSnapshotV3& layout, std::int32_t seed, + const std::vector& configs, + std::string& error); struct FloatingIslandBoundsAuditResult { @@ -941,7 +980,8 @@ namespace VoxelForgeCore explicit operator bool() const noexcept { return static_cast(data_); } const WorldConfiguration& configuration() const; const WorldAssemblyManifest* worldAssembly() const noexcept; - const ResolvedLayoutSnapshotV2* resolvedLayout() const noexcept; + const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept; + ResolvedWorldSnapshot withoutResolvedLayoutPassages() const; private: struct Data; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index b670a41..dec76ce 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -4,7 +4,9 @@ #include "VoxelForgeCore/VoxelForgeCore.h" #include +#include #include +#include #include namespace VoxelForgeCore @@ -66,7 +68,7 @@ namespace b.behaviour=value.Get("behaviour").String(); b.densitySide=value.Get("density_side").String();b.capPath=value.Get("cap_path").String();return b; } - std::string LayoutBodyJson(const ResolvedLayoutSnapshotV2& s) + std::string LayoutBodyJson(const ResolvedLayoutSnapshotV3& s) { std::ostringstream out;out<<"{\"schema_version\":"<>30))*0xbf58476d1ce4e5b9ull; + value=(value^(value>>27))*0x94d049bb133111ebull; + return value^(value>>31); + } + std::uint64_t PassageSeed(std::int32_t seed,std::int32_t upper, + std::int32_t lower,std::uint32_t connection, + std::uint32_t salt) + { + std::uint64_t h=PassageHash(static_cast(seed)); + h=PassageHash(h^static_cast(upper)); + h=PassageHash(h^static_cast(lower)); + h=PassageHash(h^connection);return PassageHash(h^salt); + } + std::uint64_t PassageIsqrt(std::uint64_t n) + { + std::uint64_t x=0,bit=std::uint64_t{1}<<62; + while(bit>n)bit>>=2; + while(bit) + { + if(n>=x+bit){n-=x+bit;x=(x>>1)+bit;} + else x>>=1; + bit>>=2; + } + return x; + } + std::int64_t PassageRoundRatio(std::int64_t value,std::int64_t numerator, + std::int64_t denominator) + { + if(denominator<=0)return 0; + const bool negative=value<0; + const std::uint64_t magnitude=static_cast(negative?-value:value); + const std::uint64_t product=magnitude*static_cast(numerator); + const std::uint64_t rounded=(product+static_cast(denominator)/2) + /static_cast(denominator); + return negative?-static_cast(rounded):static_cast(rounded); + } + std::array PassageRandomXY(std::uint64_t seed, + std::int64_t inner,std::int64_t outer) + { + if(outer<=0)return {}; + inner=std::clamp(inner,std::int64_t{0},outer); + const std::uint64_t span=static_cast(outer)*2+1; + for(std::uint32_t attempt=0;attempt<64;++attempt) + { + const std::uint64_t hx=PassageHash(seed+2ull*attempt); + const std::uint64_t hy=PassageHash(seed+2ull*attempt+1); + const std::int64_t x=static_cast(hx%span)-outer; + const std::int64_t y=static_cast(hy%span)-outer; + const std::uint64_t radiusSquared=static_cast(x*x+y*y); + if(radiusSquared<=static_cast(outer*outer) + &&radiusSquared>=static_cast(inner*inner))return {x,y}; + } + return {outer,0}; + } } -std::string ResolvedLayoutSnapshotV2::ToJson() const +std::string ResolvedLayoutSnapshotV3::ToJson() const { const std::string body=LayoutBodyJson(*this); return body.substr(0,body.size()-1)+",\"stable_identity\":" - +LayoutEscape(stableIdentity.empty()?"resolved-layout-v2-fnv64:"+LayoutHex(LayoutHash(body)):stableIdentity)+"}"; + +LayoutEscape(stableIdentity.empty()?"resolved-layout-v3-fnv64:"+LayoutHex(LayoutHash(body)):stableIdentity)+"}"; } -bool ResolvedLayoutSnapshotV2::finalize(std::string& error) +bool ResolvedLayoutSnapshotV3::finalize(std::string& error) { error.clear(); - if(schemaVersion!=ResolvedLayoutSchemaVersionV2||worldIdentity.empty()||strates.empty() + if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty() ||strateLayoutChunkSizeMicrometres==0||strateLayoutChunkWidthSubvoxels<=0) {error="resolved layout has an unsupported version, empty world identity, or no strates";return false;} std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b) @@ -149,22 +231,37 @@ bool ResolvedLayoutSnapshotV2::finalize(std::string& error) if(x.interiorTopMicrometres<=x.interiorBottomMicrometres||x.voxelSizeMicrometres==0) {error="resolved layout contains an invalid interior interval or voxel size";return false;} } + const std::int64_t playerCenterAboveFloorQ=static_cast(std::llround( + 1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres)); + for(auto& passage:passages) + { + passage.lowerMouthControlIndex=passage.controlPoints.empty()?0:passage.controlPoints.size()-1; + for(std::size_t i=1;i issues; - return validate(ResolvedLayoutValidationModeV2::compatibility,issues,error); + return validate(ResolvedLayoutValidationModeV3::compatibility,issues,error); } -bool ResolvedLayoutSnapshotV2::validate(ResolvedLayoutValidationModeV2 mode, +bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, std::vector& issues,std::string& error) const { issues.clear();error.clear(); - if(schemaVersion!=ResolvedLayoutSchemaVersionV2||worldIdentity.empty()||strates.empty() + if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty() ||strateLayoutChunkSizeMicrometres==0||strateLayoutChunkWidthSubvoxels<=0) {error="resolved layout schema/world/strata header is invalid";return false;} const auto add=[&](std::string code,std::string detail) {issues.push_back({std::move(code),std::move(detail)});}; - const bool strict=mode==ResolvedLayoutValidationModeV2::strict; + const bool strict=mode==ResolvedLayoutValidationModeV3::strict; for(std::size_t i=0;iconst ResolvedLayoutStratumV2* + { + const auto it=std::find_if(strates.begin(),strates.end(),[index](const auto& s) + {return s.strateIndex==index;}); + return it==strates.end()?nullptr:&*it; + }; + std::vector passageIds; + for(std::size_t i=0;strict&&i+1order!=upper->order+1) + add("passage_interface",passage.stableId+" does not join adjacent resolved strates"); + if(!upper||!lower||passage.placementRadiusSubvoxels<=0 + ||passage.mouthRadiusSubvoxels<=0||passage.midRadiusSubvoxels<=0 + ||passage.controlPoints.size()<2) + { + add("passage_geometry",passage.stableId+" has invalid radii, placement bound, or control points"); + continue; + } + const auto insidePlacement=[&](const std::array& p) + { + const auto x=p[0],y=p[1],r=passage.placementRadiusSubvoxels; + return std::abs(x)<=r&&std::abs(y)<=r&&x*x+y*y<=r*r; + }; + const auto insideContent=[&](const ResolvedLayoutStratumV2& s, + const std::array& p) + { + const std::int64_t zMicrometres=p[2]*static_cast(coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel; + return s.content.presentInCurrentField&&zMicrometres>=s.content.bottomMicrometres + &&zMicrometres0 + ?static_cast(std::llround(1.13*WorldAssemblySubvoxelsPerVoxel + *1000000.0/coreVoxelSizeMicrometres)):1157; + const auto floorProfile=[&](const ResolvedLayoutPassagePointV1& p) + {return p.positionSubvoxels[2]-p.radiusSubvoxels+PlayerCenterAboveFloorQ;}; + std::size_t lowerMouthControlIndex=passage.controlPoints.size(); + for(std::size_t i=1;i(dx*dx+dy*dy)); + const std::int64_t signedDz=floorProfile(point)-floorProfile(previous); + const std::int64_t dz=std::abs(signedDz); + const bool landingExtension=i>lowerMouthControlIndex; + if((landingExtension?dz*65536>static_cast(horizontal)*17560 + :signedDz>=0||dz*65536>static_cast(horizontal)*17560) + ||horizontal==0) + add("passage_grade",passage.stableId+" is not strictly descending or exceeds the 15 degree grade limit"); + } + } if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;} return true; } -std::optional ResolvedLayoutSnapshotV2::FromJson( +std::optional ResolvedLayoutSnapshotV3::FromJson( std::string_view json,std::string& error) { VFJson::Value root;if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt; if(!root.IsObject()){error="resolved layout JSON root must be an object";return std::nullopt;} - ResolvedLayoutSnapshotV2 s;s.schemaVersion=static_cast(LayoutInt(root,"schema_version")); + ResolvedLayoutSnapshotV3 s;s.schemaVersion=static_cast(LayoutInt(root,"schema_version")); s.worldIdentity=LayoutString(root,"world_identity"); s.ownerVoxelSizeMicrometres=static_cast(LayoutInt(root,"owner_voxel_size_um")); s.coreVoxelSizeMicrometres=static_cast(LayoutInt(root,"core_voxel_size_um")); @@ -302,15 +493,314 @@ std::optional ResolvedLayoutSnapshotV2::FromJson( for(const auto& value:a.Get("values").AsArray())p.values.push_back(value.Integer());x.anchors.push_back(std::move(p));} s.strates.push_back(std::move(x)); } + for(const auto& v:root.Get("passages").AsArray()) + { + ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id"); + p.upperStrateIndex=static_cast(LayoutInt(v,"upper_strate_index")); + p.lowerStrateIndex=static_cast(LayoutInt(v,"lower_strate_index")); + p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q"); + p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q"); + const auto& upper=v.Get("upper_mouth_q").AsArray(); + const auto& lower=v.Get("lower_mouth_q").AsArray(); + for(std::size_t axis=0;axis<3&&axis& configs,std::string& error) { - ResolvedLayoutValidatorAuditV2 result; + error.clear();layout.passages.clear(); + if(layout.strates.size()<2)return true; + std::stable_sort(layout.strates.begin(),layout.strates.end(),[](const auto& a,const auto& b) + {return a.interiorTopMicrometres!=b.interiorTopMicrometres + ?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order(i); + if(layout.coreVoxelSizeMicrometres==0) + {error="passage planner requires the resolved Core voxel size";return false;} + const std::int64_t qPerMetre=static_cast(std::llround( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /layout.coreVoxelSizeMicrometres)); + if(qPerMetre<=0){error="passage planner could not resolve fixed-point metres";return false;} + const std::int64_t playerCenterAboveFloorQ=static_cast(std::llround(1.13*qPerMetre)); + const std::array,16> directions{{ + {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, + {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}}, + {{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}}, + {{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}}; + const auto configFor=[&](std::int32_t strate)->const ResolvedLayoutPassageConfigV1* + { + const auto it=std::find_if(configs.begin(),configs.end(),[strate](const auto& c) + {return c.upperStrateIndex==strate;}); + return it==configs.end()?nullptr:&*it; + }; + const auto routeAnchor=[](const ResolvedLayoutStratumV2& s,bool departure, + std::array& out) + { + const ResolvedLayoutAnchorV2* selected=nullptr; + for(const auto& a:s.anchors) + if(a.kind=="route_point"&&a.values.size()>=3) + {selected=&a;if(!departure)break;} + if(!selected)return false; + out={selected->values[0],selected->values[1],selected->values[2]};return true; + }; + const auto routeTangent=[](const ResolvedLayoutStratumV2& s,bool departure, + std::array& out) + { + const ResolvedLayoutAnchorV2* first=nullptr,*second=nullptr,*last=nullptr,*previous=nullptr; + for(const auto& a:s.anchors) + if(a.kind=="route_point"&&a.values.size()>=3) + {if(!first)first=&a;else if(!second)second=&a;previous=last;last=&a;} + if(!first||!second||!last||!previous)return false; + const auto* from=departure?previous:first; + const auto* to=departure?last:second; + out={to->values[0]-from->values[0],to->values[1]-from->values[1]}; + return out[0]!=0||out[1]!=0; + }; + const auto radialWithin=[](const std::array& p,std::int64_t radius) + {return radius>0&&p[0]*p[0]+p[1]*p[1]<=radius*radius;}; + const auto reachValue=[](std::uint64_t hash,std::int64_t lo,std::int64_t hi) + { + if(hilo?static_cast(hash%static_cast(hi-lo+1)):0); + }; + const auto contentZQ=[&](std::int64_t micrometres) + {return static_cast(std::llround(static_cast(micrometres) + *WorldAssemblySubvoxelsPerVoxel/layout.coreVoxelSizeMicrometres));}; + const auto distanceQ=[](std::int64_t dx,std::int64_t dy) + { + const auto square=static_cast(dx*dx+dy*dy); + const auto floor=PassageIsqrt(square);return floor*floor==square?floor:floor+1; + }; + for(std::size_t interface=0;interface+1distanceMinimumSubvoxels, + cfg->distanceMaximumSubvoxels):24*qPerMetre; + const std::int64_t inner=cfg?std::min(cfg->distanceMinimumSubvoxels, + cfg->distanceMaximumSubvoxels):8*qPerMetre; + const std::int64_t placementRadius=std::max(1,outer); + const std::int64_t mouthRadius=std::max(static_cast(std::llround(1.25*qPerMetre)), + cfg?cfg->mouthRadiusSubvoxels:0); + const std::int64_t midRadius=std::max(mouthRadius, + cfg?cfg->midRadiusSubvoxels:0); + const std::int64_t reachMinimum=cfg?std::max(0,cfg->reachMinimumSubvoxels):8*qPerMetre; + const std::int64_t reachMaximum=cfg?std::max(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre; + const std::uint32_t connectionCount=cfg?static_cast(std::clamp(cfg->connections,1,12)):1; + for(std::uint32_t connection=0;connection upperMouth{},lowerMouth{}; + const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth); + const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth); + if(!hasUpperRoute||!hasLowerRoute) + { + const auto xy=PassageRandomXY(PassageSeed(seed,upper.strateIndex,lower.strateIndex, + connection,0x504c4143u),inner,placementRadius); + if(!hasUpperRoute) + { + upperMouth[0]=xy[0];upperMouth[1]=xy[1]; + const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex, + connection,0x55505248u),reachMinimum,reachMaximum); + upperMouth[2]=contentZQ(upper.content.bottomMicrometres)+reach; + } + if(!hasLowerRoute) + { + lowerMouth[0]=xy[0];lowerMouth[1]=xy[1]; + const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex, + connection,0x4c4f5752u),reachMinimum,reachMaximum); + lowerMouth[2]=contentZQ(lower.content.topMicrometres)-reach; + } + } + if(!radialWithin(upperMouth,placementRadius)||!radialWithin(lowerMouth,placementRadius)) + {error=passage.stableId+" route mouth is outside the resolved PassageConfig.DistanceMax radius";return false;} + passage.upperMouthSubvoxels=upperMouth;passage.lowerMouthSubvoxels=lowerMouth; + + constexpr std::int64_t LandingExtensionQ=8; + std::array upperTangent{},lowerTangent{}; + const bool hasUpperTangent=hasUpperRoute + &&routeTangent(upper,true,upperTangent); + const bool hasLowerTangent=hasLowerRoute + &&routeTangent(lower,false,lowerTangent); + + const std::int64_t ringRadius=std::max(1, + placementRadius-mouthRadius-qPerMetre/2); + std::uint32_t phase=static_cast(PassageSeed(seed, + upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16); + if(hasUpperRoute) + { + std::int64_t bestAlignment=std::numeric_limits::min(); + const std::uint32_t hashedPhase=phase; + for(std::uint32_t offset=0;offset<16;++offset) + { + const std::uint32_t candidate=static_cast( + (hashedPhase+offset)%16); + const std::int64_t alignment=(hasUpperTangent?upperTangent[0]:upperMouth[0]) + *directions[candidate][0] + +(hasUpperTangent?upperTangent[1]:upperMouth[1]) + *directions[candidate][1]; + if(alignment>bestAlignment) + {bestAlignment=alignment;phase=candidate;} + } + } + const bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex, + connection,0x44495245u)&1u)!=0; + const int direction=clockwise?1:-1; + const auto ringPoint=[&](std::uint32_t directionIndex) + { + return std::array{ + static_cast(ringRadius)*directions[directionIndex][0]/65536, + static_cast(ringRadius)*directions[directionIndex][1]/65536}; + }; + std::vector> xy; + const auto pushXY=[&](std::array p) + {if(xy.empty()||xy.back()!=p)xy.push_back(p);}; + const auto firstRing=ringPoint(phase); + pushXY({upperMouth[0],upperMouth[1]}); + pushXY(firstRing); + std::uint64_t totalHorizontal=0;std::uint32_t loops=0; + const std::int64_t verticalDistance=std::abs(lowerMouth[2]-upperMouth[2]); + constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract. + auto measurePath=[&]() + { + std::uint64_t total=0; + for(std::size_t i=1;i(TargetGradeQ16) + >=static_cast(verticalDistance)*65536ull)break; + for(int step=1;step<=16;++step) + { + int directionIndex=static_cast(phase)+direction*step; + directionIndex=(directionIndex%16+16)%16; + pushXY(ringPoint(static_cast(directionIndex))); + } + ++loops; + } + if(hasLowerTangent) + { + std::uint32_t lowerPhase=static_cast(PassageSeed(seed, + upper.strateIndex,lower.strateIndex,connection,0x4c505248u)%16); + std::int64_t bestAlignment=std::numeric_limits::min(); + const std::uint32_t hashedLowerPhase=lowerPhase; + for(std::uint32_t offset=0;offset<16;++offset) + { + const std::uint32_t candidate=static_cast( + (hashedLowerPhase+offset)%16); + const auto terminal=ringPoint(candidate); + const std::int64_t dx=lowerMouth[0]-terminal[0]; + const std::int64_t dy=lowerMouth[1]-terminal[1]; + const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1]; + const std::int64_t cross=std::abs(dx*lowerTangent[1]-dy*lowerTangent[0]); + if(alignment<=0||cross==0)continue; + const auto length=distanceQ(dx,dy); + const std::int64_t score=alignment*65536/static_cast(length); + if(score>bestAlignment) + {bestAlignment=score;lowerPhase=candidate;} + } + if(bestAlignment==std::numeric_limits::min()) + {error=passage.stableId+" could not find a non-overlapping lower landing approach";return false;} + if(phase!=lowerPhase) + for(int step=1;step<=16;++step) + { + int directionIndex=static_cast(phase)+direction*step; + directionIndex=(directionIndex%16+16)%16; + pushXY(ringPoint(static_cast(directionIndex))); + if(static_cast(directionIndex)==lowerPhase)break; + } + pushXY(ringPoint(lowerPhase)); + } + pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath(); + const std::size_t lowerMouthIndex=xy.size()-1; + std::vector> lowerLandingPoints; + if(hasLowerTangent) + { + std::array previousRoutePoint=lowerMouth; + std::int64_t remainingQ=LandingExtensionQ*qPerMetre; + bool skippedMouth=false; + for(const auto& anchor:lower.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + { + const std::array routePoint{{anchor.values[0], + anchor.values[1],anchor.values[2]}}; + if(!skippedMouth){skippedMouth=true;continue;} + if(remainingQ<=0)break; + const std::int64_t dx=routePoint[0]-previousRoutePoint[0]; + const std::int64_t dy=routePoint[1]-previousRoutePoint[1]; + const std::uint64_t segmentLength=distanceQ(dx,dy); + if(segmentLength==0)continue; + const std::int64_t advance=std::min(remainingQ, + static_cast(segmentLength)); + std::array landingPoint{}; + for(std::size_t axis=0;axis<3;++axis) + landingPoint[axis]=previousRoutePoint[axis]+PassageRoundRatio( + routePoint[axis]-previousRoutePoint[axis],advance, + static_cast(segmentLength)); + if(!radialWithin(landingPoint,placementRadius))break; + lowerLandingPoints.push_back(landingPoint); + pushXY({landingPoint[0],landingPoint[1]}); + previousRoutePoint=landingPoint;remainingQ-=advance; + if(advance(segmentLength))break; + } + if(lowerLandingPoints.empty()) + {error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;} + } + totalHorizontal=measurePath(); + if(loops>=256||totalHorizontal==0) + {error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;} + std::vector cumulative(xy.size(),0); + for(std::size_t i=1;i=xy.size()-1)?mouthRadius:midRadius; + const std::int64_t playerZ=i<=lowerMouthIndex + ?upperMouth[2]+PassageRoundRatio(lowerMouth[2]-upperMouth[2], + static_cast(cumulative[i]),static_cast(mouthHorizontal)) + :lowerLandingPoints[i-lowerMouthIndex-1][2]; + ResolvedLayoutPassagePointV1 point; + point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ}; + point.radiusSubvoxels=radius;passage.controlPoints.push_back(point); + } + passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2]+mouthRadius-playerCenterAboveFloorQ; + passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]= + lowerMouth[2]+mouthRadius-playerCenterAboveFloorQ; + layout.passages.push_back(std::move(passage)); + } + } + return true; +} + +ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3() +{ + ResolvedLayoutValidatorAuditV3 result; auto make=[](std::string id,std::int32_t order,std::int64_t top,std::int64_t bottom) { ResolvedLayoutStratumV2 s;s.stableId=std::move(id);s.order=order;s.strateIndex=order;s.generatorKind="synthetic"; @@ -322,16 +812,34 @@ ResolvedLayoutValidatorAuditV2 AuditResolvedLayoutValidatorV2() s.bedrock={"bedrock",bottom-1000000,bottom,true,true,false,"breakable bedrock","lower face","none"}; return s; }; - const auto expectReject=[&](std::string name,ResolvedLayoutSnapshotV2 candidate) + const auto expectReject=[&](std::string name,ResolvedLayoutSnapshotV3 candidate) { ++result.controlsRun;std::vector issues;std::string error; - if(!candidate.validate(ResolvedLayoutValidationModeV2::strict,issues,error))++result.controlsRejectedAsExpected; + if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,error))++result.controlsRejectedAsExpected; else result.failures.push_back({"negative_control_accepted",std::move(name)+" was accepted by strict validator"}); }; - auto base=ResolvedLayoutSnapshotV2{};base.worldIdentity="synthetic";base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)}; + auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000; + base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)}; + ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1; + passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280; + passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048; + passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240; + std::string planError; + if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError)) + result.failures.push_back({"synthetic_passage_plan_failed",planError}); {std::vector issues;std::string error; - if(!base.validate(ResolvedLayoutValidationModeV2::strict,issues,error)) + if(!base.validate(ResolvedLayoutValidationModeV3::strict,issues,error)) result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"});} + auto missingPassage=base;missingPassage.passages.clear();expectReject("missing interface passage",missingPassage); + auto outsidePassage=base; + if(!outsidePassage.passages.empty()) + { + outsidePassage.passages.front().upperMouthSubvoxels[0] + =outsidePassage.passages.front().placementRadiusSubvoxels+1; + outsidePassage.passages.front().controlPoints.front().positionSubvoxels[0] + =outsidePassage.passages.front().upperMouthSubvoxels[0]; + } + expectReject("passage mouth outside placement radius",outsidePassage); auto boundaryGap=base;boundaryGap.strates[1].ceiling.topMicrometres-=1; expectReject("bedrock/next-ceiling gap",boundaryGap); auto missingBedrock=base;missingBedrock.strates[0].bedrock.presentInCurrentField=false;expectReject("missing bedrock",missingBedrock); diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 2d3d665..b84e19f 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -33,7 +33,7 @@ class FStandaloneShapeWorld { public: - const VoxelForgeCore::ResolvedLayoutSnapshotV2* ResolvedLayout=nullptr; + const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr; struct FResolvedStratumBounds { double BottomZ=0.0; @@ -97,6 +97,170 @@ public: return -std::max(0.05f,static_cast(band->solidDensityQ16)/65536.0f); } + float ApplyResolvedStratePassageCarves(float density,float X,float Y,float Z) const + { + if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X) + ||!std::isfinite(Y)||!std::isfinite(Z))return density; + constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q,qz=static_cast(Z)*Q; + const double qPerMetre=Q*1000000.0/std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); + const double floorThicknessVoxels=qPerMetre/Q; + const double playerCenterAboveFloorQ=1.13*qPerMetre; + float composed=density; + for(const auto& passage:ResolvedLayout->passages) + { + if(passage.controlPoints.size()<2)continue; + const double placementRadiusQ=static_cast(passage.placementRadiusSubvoxels) + +static_cast(passage.midRadiusSubvoxels); + if(qx*qx+qy*qy>placementRadiusQ*placementRadiusQ)continue; + const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2] + -passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ; + const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2] + -passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ; + if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels + ||qz(passage.controlPoints.size())-1; + const std::ptrdiff_t lowerMouthIndex=std::clamp( + static_cast(passage.lowerMouthControlIndex),1,lastIndex); + const auto mainEnd=passage.controlPoints.begin()+lowerMouthIndex+1; + const auto carveFound=std::lower_bound(passage.controlPoints.begin(),mainEnd,qz, + [&](const auto& point,double value){return floorProfile(point)>value;}); + const std::ptrdiff_t carveCenter=carveFound-passage.controlPoints.begin(); + const std::ptrdiff_t carveBegin=std::max(0,carveCenter-3); + const std::ptrdiff_t carveEnd=std::min(lowerMouthIndex-1,carveCenter+2); + double bestSignedDistance=std::numeric_limits::infinity(); + double bestT=0.0; + std::ptrdiff_t bestIndex=0; + bool hasSupportFloor=false; + double bestSupportFloorVoxels=-std::numeric_limits::infinity(); + double bestSupportDensity=0.0; + const auto considerCarveSegment=[&](std::ptrdiff_t index) + { + const auto& a=passage.controlPoints[static_cast(index)]; + const auto& b=passage.controlPoints[static_cast(index+1)]; + const double ax=static_cast(a.positionSubvoxels[0]); + const double ay=static_cast(a.positionSubvoxels[1]); + const double az=static_cast(a.positionSubvoxels[2]); + const double dx=static_cast(b.positionSubvoxels[0])-ax; + const double dy=static_cast(b.positionSubvoxels[1])-ay; + const double dz=static_cast(b.positionSubvoxels[2])-az; + const double lengthSq=dx*dx+dy*dy+dz*dz; + if(lengthSq<=0.0)return; + const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0); + const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz; + const double ex=qx-cx,ey=qy-cy,ez=qz-cz; + const double radius=static_cast(a.radiusSubvoxels) + +t*(b.radiusSubvoxels-a.radiusSubvoxels); + const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius; + if(signedDistance(-bestSignedDistance/Q)); + const auto& nearestA=passage.controlPoints[static_cast(bestIndex)]; + const auto& nearestB=passage.controlPoints[static_cast(bestIndex+1)]; + const double nearestRadius=nearestA.radiusSubvoxels+bestT*( + static_cast(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels); + const double nearestPlayerCenterZ=nearestA.positionSubvoxels[2]+bestT*( + static_cast(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2]) + -nearestRadius+playerCenterAboveFloorQ; + // Only compose a support shelf below the capsule feet. This keeps support + // probes on the matching floor while capsule samples remain air at bends. + if(qzvalue;}); + const std::ptrdiff_t supportCenter=supportFound-passage.controlPoints.begin(); + const std::ptrdiff_t supportBegin=std::max(0,supportCenter-3); + const std::ptrdiff_t supportEnd=std::min(lowerMouthIndex-1,supportCenter+2); + const auto considerSupportSegment=[&](std::ptrdiff_t index) + { + const auto& segmentA=passage.controlPoints[static_cast(index)]; + const auto& segmentB=passage.controlPoints[static_cast(index+1)]; + const double dx=static_cast(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0]; + const double dy=static_cast(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1]; + const double horizontalLengthSq=dx*dx+dy*dy; + const double supportT=horizontalLengthSq>0.0 + ?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx + +(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0) + :bestT; + const double supportCenterX=segmentA.positionSubvoxels[0]+supportT*dx; + const double supportCenterY=segmentA.positionSubvoxels[1]+supportT*dy; + const double supportCenterZ=segmentA.positionSubvoxels[2]+supportT*( + static_cast(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]); + const double supportRadius=segmentA.radiusSubvoxels+supportT*( + static_cast(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels); + // Leave a 12 cm settle gap below the capsule feet while staying inside + // the route support probes' search depth. + const double floorQ=supportCenterZ-supportRadius+0.12*qPerMetre; + const double floorVoxels=floorQ/Q; + const double horizontalX=qx-supportCenterX,horizontalY=qy-supportCenterY; + const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY); + const double supportHalfWidth=std::max(0.0,supportRadius-0.15*qPerMetre); + if(horizontalDistance<=supportHalfWidth&&Z>=floorVoxels-floorThicknessVoxels + &&Z<=floorVoxels+1.0/Q) + { + if(!hasSupportFloor||floorVoxels>bestSupportFloorVoxels) + { + bestSupportFloorVoxels=floorVoxels;hasSupportFloor=true; + bestSupportDensity=std::clamp(Z-floorVoxels,-floorThicknessVoxels,0.0); + } + } + }; + for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index) + considerSupportSegment(index); + for(std::ptrdiff_t index=lowerMouthIndex;index(bestSupportDensity)); + } + return composed; + } + + float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const + {return ApplyResolvedStratePassageCarves(ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);} + + bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const + { + if(!ResolvedLayout||ResolvedLayout->passages.empty())return false; + constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + for(const auto& passage:ResolvedLayout->passages) + { + for(std::size_t i=1;i(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q; + const double floorThicknessVoxels=Owner&&Owner->CoreVoxelSizeMicrons>0 + ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; + const double low[3]={ + std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-radius, + std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-radius, + std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-floorThicknessVoxels}; + const double high[3]={ + std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+radius, + std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+radius, + std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; + bool overlap=true; + for(int axis=0;axis<3;++axis) + overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis]; + if(overlap)return true; + } + } + return false; + } + static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness, FResolvedStratumBounds& Out) { @@ -410,7 +574,7 @@ public: const std::vector& InGpuVisualDetailTiles={}, const std::vector& InCpuVisualDetailTiles={}, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr, - const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout=nullptr) + const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr) { using Clock=std::chrono::steady_clock; const auto TotalStart=Clock::now(); @@ -571,7 +735,7 @@ public: const FIntVector& ClipMaxVoxels,EDetailVariant Variant, bool bInRoundTunnels, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly, - const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout, + const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout, std::string& Error) { using Clock=std::chrono::steady_clock; @@ -611,7 +775,7 @@ public: void InitializeNativeSurfaceOnly(FStandaloneOwnerWorld& World, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly, - const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout, + const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout, const FIntVector& InMinimum,const FIntVector& InMaximum) { Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout; @@ -622,7 +786,7 @@ public: bool BuildShapeDependencyProof(FStandaloneOwnerWorld& World, const FIntVector& MinVoxels,const FIntVector& MaxVoxels,bool bInRoundTunnels, - const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout=nullptr) + const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr) { Owner=&World;ResolvedLayout=InResolvedLayout;WorkerCount=1;RoundTunnelsEnabled=bInRoundTunnels; bAnalyticOnly=true;bNativeOnly=false;Stats={}; @@ -1129,19 +1293,19 @@ public: float NativeDensity=0.0f; if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) {RecordNativeOnlyViolation();return 0.0f;} - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) { ++ThreadClassificationStats().NativeFloatFieldCalls; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z); } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { ++ThreadClassificationStats().NativeFloatFieldCalls; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } if(UsesLegacySurfaceField(X,Y,Z)) return LegacySurfaceDensityAt(X,Y,Z); @@ -1191,21 +1355,21 @@ public: if(!NativeSurfaceDensityAt(static_cast(X),static_cast(Y), static_cast(Z),NativeDensity)) {RecordNativeOnlyViolation();return 0.0f;} - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(static_cast(X),static_cast(Y), static_cast(Z),IslandDensity)) { ++ThreadClassificationStats().NativeLatticeFieldCalls; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z); } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(static_cast(X),static_cast(Y), static_cast(Z),NativeDensity)) { ++ThreadClassificationStats().NativeLatticeFieldCalls; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } if(UsesLegacySurfaceField(static_cast(X),static_cast(Y), static_cast(Z))) @@ -1229,19 +1393,19 @@ public: float NativeDensity=0.0f; if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) {RecordNativeOnlyViolation();return 0.0f;} - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) { ++ThreadClassificationStats().NativeUnclampedFieldCalls; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z); } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { ++ThreadClassificationStats().NativeUnclampedFieldCalls; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z); + return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z); } if(UsesLegacySurfaceField(X,Y,Z)) return LegacySurfaceDensityAt(X,Y,Z); @@ -1371,13 +1535,13 @@ private: float Density=static_cast(Proxy)*(1.0f/256.0f); if((Density<0.0f)!=bRock) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f); - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(Density,FX,FY,FZ),FX,FY,FZ); + return ApplyEditBrushes(ApplyResolvedStrateComposition(Density,FX,FY,FZ),FX,FY,FZ); } // Match the fixed fallback to the edge of the stored proxy band. The // displacement is tapered to zero there, so this avoids a magnitude // jump along the candidate bit-mask boundary. constexpr float ProxyBandEdge=DetailSupremum+0.125f; - return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand( + return ApplyEditBrushes(ApplyResolvedStrateComposition( bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ),FX,FY,FZ); } @@ -1395,7 +1559,7 @@ private: float IslandDensity=0.0f; if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) { - const float composed=ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z); + const float composed=ApplyResolvedStrateComposition(IslandDensity,X,Y,Z); return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed; } if(UsesLegacySurfaceField(X,Y,Z)) @@ -1403,11 +1567,11 @@ private: float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { - NativeDensity=ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z); + NativeDensity=ApplyResolvedStrateComposition(NativeDensity,X,Y,Z); return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity; } const float Density=Owner->Generator->GetDensityAt(X,Y,Z); - const float composed=ApplyResolvedStrateBoundaryBand(Density,X,Y,Z); + const float composed=ApplyResolvedStrateComposition(Density,X,Y,Z); return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed; } const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] @@ -1422,7 +1586,7 @@ private: if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f)) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density), bClampNearZero?0.05f:1.0e-7f); - const float composed=ApplyResolvedStrateBoundaryBand(Density,X,Y,Z); + const float composed=ApplyResolvedStrateComposition(Density,X,Y,Z); return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed; } ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; @@ -1452,7 +1616,7 @@ private: if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f)) Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density), bClampNearZero?0.05f:1.0e-7f); - Density=ApplyResolvedStrateBoundaryBand(Density,X,Y,Z); + Density=ApplyResolvedStrateComposition(Density,X,Y,Z); return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density; } @@ -1590,6 +1754,8 @@ public: else ++Counts.MixedBoxes; return Result; }; + if(ResolvedPassageIntersectsBox(BMin,BMax)) + return Finish(EVoxelTileClass::Mixed,true); if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] &&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1] &&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2]) @@ -2319,7 +2485,7 @@ private: // SurfaceWorld consumes biome overrides, terrain ops, water level and its sky cap in // the legacy stack. Reuse that immutable evaluator as an exact temporary bridge. // SurfaceWorld lit biomes, ops de terrain, niveau d'eau et plafond; reutiliser ce champ. - const float density=ApplyResolvedStrateBoundaryBand(Owner->Generator->GetDensityAt(X,Y,Z),X,Y,Z); + const float density=ApplyResolvedStrateComposition(Owner->Generator->GetDensityAt(X,Y,Z),X,Y,Z); return ApplyEditBrushes(density,X,Y,Z); } diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index aaab81b..c6e8c89 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -34,7 +34,7 @@ namespace VoxelForgeCore { std::shared_ptr owner; std::shared_ptr worldAssembly; - std::shared_ptr resolvedLayout; + std::shared_ptr resolvedLayout; WorldConfiguration configuration; }; @@ -869,7 +869,7 @@ namespace VoxelForgeCore { static ResolvedWorldSnapshot FromOwner(std::shared_ptr owner, std::shared_ptr worldAssembly = {}, - std::shared_ptr resolvedLayout = {}) + std::shared_ptr resolvedLayout = {}) { if(!owner||!owner->Settings||!owner->Manager||!owner->Generator)return {}; auto data=std::make_shared(); @@ -894,15 +894,15 @@ namespace VoxelForgeCore if(!parsed)return {}; worldAssembly=std::make_shared(std::move(*parsed)); } - std::shared_ptr resolvedLayout; + std::shared_ptr resolvedLayout; if(jsonRoot.Get("resolved_layout_json").IsString()) { - auto parsed=ResolvedLayoutSnapshotV2::FromJson(jsonRoot.Get("resolved_layout_json").String(),error); + auto parsed=ResolvedLayoutSnapshotV3::FromJson(jsonRoot.Get("resolved_layout_json").String(),error); if(!parsed)return {}; std::vector issues;std::string strictError; - if(!parsed->validate(ResolvedLayoutValidationModeV2::strict,issues,strictError)) + if(!parsed->validate(ResolvedLayoutValidationModeV3::strict,issues,strictError)) {error="strict resolved-layout validation failed: "+strictError;return {};} - resolvedLayout=std::make_shared(std::move(*parsed)); + resolvedLayout=std::make_shared(std::move(*parsed)); } const bool seedChanged=owner->Seed!=seed; @@ -943,24 +943,36 @@ namespace VoxelForgeCore if(!parsed)return std::nullopt; worldAssembly=std::make_shared(std::move(*parsed)); } - std::shared_ptr resolvedLayout; + std::shared_ptr resolvedLayout; if(root.Get("resolved_layout_json").IsString()) { - auto parsed=ResolvedLayoutSnapshotV2::FromJson(root.Get("resolved_layout_json").String(),error); + auto parsed=ResolvedLayoutSnapshotV3::FromJson(root.Get("resolved_layout_json").String(),error); if(!parsed)return std::nullopt; std::vector issues;std::string strictError; - if(!parsed->validate(ResolvedLayoutValidationModeV2::strict,issues,strictError)) + if(!parsed->validate(ResolvedLayoutValidationModeV3::strict,issues,strictError)) {error="strict resolved-layout validation failed: "+strictError;return std::nullopt;} - resolvedLayout=std::make_shared(std::move(*parsed)); + resolvedLayout=std::make_shared(std::move(*parsed)); } return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner),std::move(worldAssembly),std::move(resolvedLayout)); } const WorldAssemblyManifest* ResolvedWorldSnapshot::worldAssembly() const noexcept { return data_?data_->worldAssembly.get():nullptr; } - const ResolvedLayoutSnapshotV2* ResolvedWorldSnapshot::resolvedLayout() const noexcept + const ResolvedLayoutSnapshotV3* ResolvedWorldSnapshot::resolvedLayout() const noexcept { return data_?data_->resolvedLayout.get():nullptr; } + ResolvedWorldSnapshot ResolvedWorldSnapshot::withoutResolvedLayoutPassages() const + { + if(!data_||!data_->resolvedLayout)return {}; + auto copy=std::make_shared(*data_); + auto layout=std::make_shared(*data_->resolvedLayout); + layout->passages.clear(); + std::string error; + if(!layout->finalize(error))return {}; + copy->resolvedLayout=std::move(layout); + return ResolvedWorldSnapshot(std::move(copy)); + } + const WorldConfiguration& ResolvedWorldSnapshot::configuration() const { static const WorldConfiguration empty{}; @@ -971,7 +983,7 @@ namespace VoxelForgeCore { std::shared_ptr owner; std::shared_ptr worldAssembly; - std::shared_ptr resolvedLayout; + std::shared_ptr resolvedLayout; WorldConfiguration configuration; std::unique_ptr shape; std::atomic shapeReady{false}; diff --git a/Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md new file mode 100644 index 0000000..4589cdc --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md @@ -0,0 +1,158 @@ +# L23 — inter-strate passage + +## Owner answer + +**After L22, Core did not have a resolved-layout passage through the new bands.** L22 exported +`FVoxelPassage` points as `milli owner voxels` metadata, but the native FloatingIslands and +SurfaceWorld Core paths did not use that metadata to carve the resolved bands. The legacy manager +still has `UVoxelStrateManager::GeneratePassages`; its coordinates are in owner voxels, not metres. + +The owner voxel is 12.5 cm (`OwnerVoxelSizeMicrons=125000`); `voxel_size_cm=25` is the Core voxel. +The legacy 0→1 passage therefore had upper `(-19.594, 3.137, -14.255) m` and lower +`(-19.594, 3.137, -102.959) m`, with radius 1 m. Its lower point was still in FloatingIslands +content (`-119.5…+7.5 m`), 23.29 m above SurfaceWorld content (`-625.75…-126.25 m`). It did not +open the FI bedrock (`-120…-119.5 m`) and SW ceiling (`-126.25…-120 m`). The JSON also contains a +legacy 0→0 passage; L23 plans only adjacent resolved strata. + +L23 now derives the passage from the director-resolved bands and route anchors. For the FI recipe, +mouths are `(9.800, 4.571, -54.831) m` and `(9.250, 9.250, -502.608) m`. For the SurfaceWorld +recipe the unanchored upper mouth is `(10.732, -5.775, -107.766) m`; the lower mouth is the same. +Both locations are inside the actual content and within 24 m of `(0,0)`. + +## Planner, radius, and routes + +`UVoxelStrateManager::GeneratePassages` remains the legacy Unreal planner. L22's season manifest +carried recipes/routes, while the adapter copied manager passage points into V2 anchors with the +legacy units. L23's adapter and headless director convert each upper strate's `PassageConfig` to +Core fixed units, build a V3 layout with route anchors, then call `PlanResolvedLayoutPassagesV1`. +The planner hashes seed, upper/lower strate, connection, and salt, so peers with the same resolved +input choose the same path. Strate moves and route changes trigger a new plan; exported root +`passages` are not imported. + +The placement disk comes from `PassageConfig.DistanceMax=192` owner voxels = 24 m; when no route +anchor is available, `DistanceMin=64` owner voxels = 8 m supplies the inner annulus. `OriginSpineRadius` +and spawn radius are separate settings. `MouthRadius`/`MidRadius` set tunnel cross-section; Core +enforces a 1.25 m minimum radius for the player capsule. Neither ceiling nor cap parameters changed. + +In the FI snapshot, route point 28 (last of 29) equals the FI departure mouth; SW route point 0 +(first of 33) equals its arrival mouth. Both route audits pass. This owner layout has exactly two +strata, so SW has no next interface/departure mouth; its free route endpoint is +`(54.750, 54.750, -502.308) m`. + +## Passage audit + +Capsule is 0.34 m × 1.76 m. A passage fails on any blocked capsule sample, unsupported floor probe, +grade failure, mouth outside content/radius, route-mouth mismatch, or if the no-passage control +does not detect blockage. + +| Recipe | Interfaces | Center samples | Capsule samples | Blocked | Support probes / unsupported | Grade failures | Mouths in content / radius | +|---|---:|---:|---:|---:|---:|---:|---:| +| FloatingIslands → SurfaceWorld | 1/1 | 5,174 | 325,962 | 0 | 40,412 / 0 | 0 | 2/2 · 2/2 | +| TunnelNetwork → SurfaceWorld | 1/1 | 4,513 | 284,319 | 0 | 35,254 / 0 | 0 | 2/2 · 2/2 | + +The deliberate no-passage controls found 17,218 blocked capsule samples (FI recipe) and 23,876 +(SurfaceWorld recipe); both were detected. FI’s 29-point route and SW’s 33-point route pass with +0 blocked samples, 0 unsupported probes, 0 grade failures, and mouth binding 2/2. The +SurfaceWorld-only route passes 33 points with the same zero failures and binding 1/1. + +The shared composition layer applies each generator field, then bedrock/ceiling bands, then the +bounded passage carve and support shelf. Only the tunnel path opens a band. The full 0→1 capsule +audit crosses the FI bedrock and SW ceiling; support stays below the capsule. +The audit samples the resolved Core field by interface owner and does not reuse the old ring-table +probe, so it does not pair band and content samples from different surface owners. + +Runtime passage checks covered all eight generator kinds: TunnelNetwork, FlatPlain, CrystalChamber, +Maze, VerticalShafts, and Underwater as upper-strate fixtures; FloatingIslands in the FI recipe; +SurfaceWorld as the real lower strate in both owner-world recipes. The SurfaceWorld-as-upper fixture +could not produce its separate route: all 648 route candidates failed the grade filter. That fixture +is recorded as a failed route setup, not counted as a passage pass. The real SurfaceWorld lower +strate audits pass. + +## Determinism, proofs, and content hashes + +Clang 19 (`--fp-contract=off`) and GCC 14 (`--fp-contract=default`) builds passed. Both executables +ran the mandatory Core FP-contraction self-test without rejection. For both recipes, W1/W8/W12×5 +across both compilers produced one distinct result per hash class. The same 28-run matrix passed the +24 m region crossing: 314 moving tiles, stable within each recipe. Route parity passed on both +compilers: 8/8 page pairs, 14 higher-LOD exact pairs, 782,026 exact-query samples, and one edit, +all with zero mismatches. V3 strict validation accepted both layouts with 0 issues; all 9 malformed +layout controls were rejected. + +Passage-centered uniform-proof boxes are conservatively unknown/fallback (FI: 21,504 fallback and +unknown, 0 violations); no false certificate is emitted around the new carve. Away from the passage, +SurfaceWorld at `(512,512,-500) m` matches the L22 baseline bit-for-bit: 27,368 certified blocks, +36,426,808 samples, 1,304 fallbacks, 0 unknowns/violations. Full-detail hash is +`66ac781666579e3e / 914873894a0e38c4 / ec8b03ade039cd56 / a2b621b0321d43dc` (density/mesh/payload/ +topology); whole-view hash is `ddc6d1942db48f52 / bce985905e70dac4 / 266f5a008bcf4491 / +e6fa520c5ad2817b`. LOD0–3 and 9 exact-query hashes also match. The earlier unbounded comparison +included the far horizon and was discarded; this comparison used render-distance 0 and max LOD 3. + +Changes are limited to resolved passage planning, band openings along those paths, and supported +landings. SurfaceWorld content away from the passage is unchanged by the hashes above. L3 FP +contract setup/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proof behavior, L11 readiness/crash +path, L13 route binding, L18/L19 shared ceiling, L21/L22 layout bands, ARCHITECTURE §8.10, and +`CLAUDE.md` async/Epoch rules remain intact. The route and passage audits exercise L13 binding; the +async/cache files were not edited. + +## Captures + +Small 1280×720 JPEGs (all render inputs/PNG intermediates stay in `Saved/Linux/linux23/`): + +- [FI passage mouth](captures/fi-mouth.jpg) +- [Tunnel at the FI bedrock / SW ceiling interface](captures/tunnel-interface.jpg) +- [Arrival in SurfaceWorld](captures/sw-arrival.jpg) + +## Changes and Windows checklist + +Version bumps: resolved-layout snapshot V3, evaluator 9, FloatingIslands geometry 4. The only +Unreal-side source edit is `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`: it binds V3 +routes/configs and invokes the resolved planner. Core and standalone sources build here; the Unreal +adapter cannot be compiled on this Linux host. + +On Windows: + +1. Build the Editor/plugin with the updated Core header and adapter; check UHT and link. +2. Load the owner world at seed 0 and verify strict V3 validation and stable mouth coordinates. +3. Walk the FI route, passage, and SW route with the 0.34 m × 1.76 m capsule; inspect all three + capture viewpoints in-engine. +4. Move a strate or route anchor, reload, and verify the passage is re-derived and peer-identical. +5. Confirm SW away from the passage and its ceiling/cap parameters remain unchanged. + +## Failed and retried runs + +Failed development audit logs (all retained under `Saved/Linux/linux23/`): FI +`audit-floating-islands.log`, `r2–r5`, `r17–r19`, `r21–r36`, `r38–r41`; SurfaceWorld +`audit-surface-world.log`, `audit-surface-world-diagnostic.log`, `audit-surface-world-diagnostic-r2/r3`, +and `audit-surface-world-r2–r7`, `r19`, `r21–r22`, `r24–r26`, `r28–r31`, `r33`, `r35`. The early +failures were blocked capsule samples or unsupported landing probes. r35 still had 10 unsupported +passage support probes; r36’s correction left 1,977 blocked capsule samples. r37 build failed on +undeclared `begin/end`; r38 regressed to 10 unsupported probes. r39–r41 diagnostics traced support +search across a non-monotone lower route tail; r42 build failed on `continue` inside a lambda. The +lower-mouth split/index fix passed r43; final Clang/GCC builds and audits pass. + +Other retries: `plan-surface-world-r2.log` used the wrong data path; two layout-only calls omitted +the required benchmark/pipeline flags and exited 2. The first upper-SurfaceWorld fixture failed its +648-candidate route search as noted above. Initial offscreen renders hit the read-only Mesa cache +location and were rerun with cache disabled and the CPU/address-space limits. Initial captures were +also rerendered under affinity/memory limits. Camera trials were replaced by the three listed JPEGs. +The first SurfaceWorld away hash comparison included the far horizon and was replaced with the +bounded L22/L23 comparison. The hash parser initially reported missing rows because its regex was +not multiline; corrected parsing found 28/28 runs and zero mismatches. + +Runs were pinned to CPUs 0–11 and capped at 6 GiB address space; no task exceeded 30 minutes or +stalled for 15. This shell inherited nice level 5; an attempt to restore normal priority was denied, +so I made no priority or governor change. No commit, push, or stash was made. + +## Orchestrator review (Claude) + +- Merged as a correctness fix: the strates were not connected after L22 (and Core never carved the + passage at all); now a planned, deterministic passage crosses FI bedrock and the SW ceiling, with a + capsule audit of 0 blocked / 0 unsupported and a detected negative control. +- **The look is not yet "natural" (my capture review):** `fi-mouth.jpg` shows a stepped tube hanging in + FI's open void (there is no rock to carve between the island-level mouth at −54.8 m and the bedrock at + −119.5 m); `sw-arrival.jpg` shows flat stacked terraces; `tunnel-interface.jpg` is too close to read. + Owner input needed on where an FI passage should start (island vs bedrock floor) and how it should + look. +- Open: the SurfaceWorld-as-upper-strate fixture can't plan its route (648/648 grade-rejected), a real + limit for future layouts. +- Unreal adapter edit uncompiled here; Windows checklist above. diff --git a/Standalone/Research/WorldAssemblerM3/Linux23/captures/fi-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux23/captures/fi-mouth.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b3e6e576f6affc769ed5786d5fa5b1f661e59fa GIT binary patch literal 27270 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 Dj4OzD literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux23/captures/sw-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux23/captures/sw-arrival.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1524d43e38d59fcee1add74b8f4b6d632bed45e GIT binary patch literal 23849 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 g2@HPS{;4>BI5^i_>Kf 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 u#GA*~@||713ic7M69<2tNiToJson()!=layoutJson||roundTrip->stableIdentity!=resolvedLayout.stableIdentity) {if(error.empty())error="resolved-layout canonical serialization did not round-trip"; std::fprintf(stderr,"Resolved-layout round trip failed: %s\n",error.c_str());return 2;} @@ -352,16 +355,16 @@ int main(int argc,char** argv) output.write(layoutJson.data(),static_cast(layoutJson.size())); if(!output){std::fprintf(stderr,"Resolved-layout snapshot write failed: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;} } - const auto controls=VoxelForgeCore::AuditResolvedLayoutValidatorV2(); + const auto controls=VoxelForgeCore::AuditResolvedLayoutValidatorV3(); std::vector compatibilityIssues; std::string compatibilityError; const bool compatibilityAccepted=resolvedLayout.validate( - VoxelForgeCore::ResolvedLayoutValidationModeV2::compatibility, + VoxelForgeCore::ResolvedLayoutValidationModeV3::compatibility, compatibilityIssues,compatibilityError); std::vector strictIssues; std::string strictError; const bool strictAccepted=resolvedLayout.validate( - VoxelForgeCore::ResolvedLayoutValidationModeV2::strict,strictIssues,strictError); + VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,strictIssues,strictError); std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n", resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size()); std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n", @@ -548,5 +551,6 @@ int main(int argc,char** argv) options.RegionCrossing,options.AuditTileShapeReadiness, options.DiagnosticSlowShapeCompileMs,options.AuditBasePageEquivalence, options.AuditRouteParity,options.AuditResolvedLayoutBoundaries, + options.AuditResolvedLayoutPassages, &resolvedLayout); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index ce77c2e..7979fe7 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -255,7 +255,8 @@ namespace StandaloneStreamBenchmark bool bAuditTileShapeReadiness=false,int DiagnosticSlowShapeCompileMs=0, bool bAuditBasePageEquivalence=false,bool bAuditRouteParity=false, bool bAuditResolvedLayoutBoundaries=false, - const VoxelForgeCore::ResolvedLayoutSnapshotV2* ResolvedLayout=nullptr) + bool bAuditResolvedLayoutPassages=false, + const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr) { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; @@ -440,6 +441,349 @@ namespace StandaloneStreamBenchmark Streamer.Stop(); std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2; } + if(bAuditResolvedLayoutPassages) + { + if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout()) + {Streamer.Stop();std::fprintf(stderr,"[ResolvedPassageAudit] missing resolved-layout snapshot\n");return 2;} + struct FWalkAudit + { + std::uint64_t segments=0,centers=0,capsuleSamples=0,blocked=0; + std::uint64_t supportSamples=0,unsupported=0,gradeFailures=0; + std::array,8> firstBlocked{}; + std::array,8> firstUnsupported{}; + std::size_t firstBlockedCount=0,firstUnsupportedCount=0; + bool passed=false; + }; + const double coreMetres=static_cast(World.CoreVoxelSizeMicrons)/1000000.0; + const auto auditPlayerPath=[&](const VoxelForgeCore::World& Field, + const std::vector>& path, + double supportStartMetres,double supportStepMetres, + double supportMaximumMetres,std::uint32_t minimumSupportProbes, + double supportProbeRadiusMetres) + { + FWalkAudit audit; + if(path.size()<2)return audit; + constexpr std::array heights{{-0.84,-0.60,-0.30,0.0,0.30,0.60,0.84}}; + constexpr std::array,8> directions{{ + {{1.0,0.0}},{{0.7071067811865475,0.7071067811865475}},{{0.0,1.0}}, + {{-0.7071067811865475,0.7071067811865475}},{{-1.0,0.0}}, + {{-0.7071067811865475,-0.7071067811865475}},{{0.0,-1.0}}, + {{0.7071067811865475,-0.7071067811865475}}}}; + const auto solidAt=[&](double xMetres,double yMetres,double zMetres) + { + ++audit.capsuleSamples; + const VoxelForgeCore::Vec3d position{xMetres/coreMetres,yMetres/coreMetres,zMetres/coreMetres}; + const float density=Field.densityAt(position); + if(density<0.0f) + { + if(audit.firstBlockedCounthorizontal*0.2679491924311227) + ++audit.gradeFailures; + const double length=std::sqrt(dx*dx+dy*dy+dz*dz)*coreMetres; + const std::uint64_t intervals=std::max(1, + static_cast(std::ceil(length/0.5))); + for(std::uint64_t step=0;step<=intervals;++step) + { + if(segment>1&&step==0)continue; + const double t=static_cast(step)/intervals; + const double x=(a[0]+dx*t)*coreMetres; + const double y=(a[1]+dy*t)*coreMetres; + const double z=(a[2]+dz*t)*coreMetres; + ++audit.centers; + for(const double h:heights) + { + const double capOffset=std::max(0.0,std::abs(h)-0.54); + const double radial=std::abs(h)<=0.54?0.34 + :std::sqrt(std::max(0.0,0.34*0.34-capOffset*capOffset)); + solidAt(x,y,z+h); + for(const auto& direction:directions) + solidAt(x+direction[0]*radial,y+direction[1]*radial,z+h); + } + const double footZ=z-0.88; + const std::array,5> supportOffsets{{ + {{0.0,0.0}},{{supportProbeRadiusMetres,0.0}}, + {{-supportProbeRadiusMetres,0.0}}, + {{0.0,supportProbeRadiusMetres}}, + {{0.0,-supportProbeRadiusMetres}}}}; + std::uint32_t supportedProbes=0; + double firstUnsupportedX=x,firstUnsupportedY=y,lastUnsupportedDensity=0.0; + for(const auto& offset:supportOffsets) + { + const double supportX=x+offset[0],supportY=y+offset[1]; + bool supported=false; + for(double depth=supportStartMetres; + depth<=supportMaximumMetres+1.0e-6;depth+=supportStepMetres) + { + ++audit.supportSamples; + lastUnsupportedDensity=Field.densityAt({supportX/coreMetres, + supportY/coreMetres,(footZ-depth)/coreMetres}); + if(lastUnsupportedDensity<0.0) + { + supported=true;break; + } + } + if(supported)++supportedProbes; + else{firstUnsupportedX=supportX;firstUnsupportedY=supportY;} + } + if(supportedProbes0&&audit.blocked==0&&audit.unsupported==0 + &&audit.gradeFailures==0; + return audit; + }; + const auto qToMetres=[&](std::int64_t q) + {return static_cast(q)*World.CoreVoxelSizeMicrons + /(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0);}; + const auto findStratum=[&](std::int32_t index)->const VoxelForgeCore::ResolvedLayoutStratumV2* + { + const auto it=std::find_if(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(), + [index](const auto& s){return s.strateIndex==index;}); + return it==ResolvedLayout->strates.end()?nullptr:&*it; + }; + const auto mouthInContent=[&](std::int32_t index,const std::array& mouth) + { + const auto* stratum=findStratum(index);if(!stratum)return false; + const std::int64_t zUm=static_cast(std::llround( + static_cast(mouth[2])*World.CoreVoxelSizeMicrons + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)); + return zUm>=stratum->content.bottomMicrometres&&zUmcontent.topMicrometres; + }; + std::uint64_t passageCount=0,passagePasses=0,blockedControls=0; + for(const auto& passage:ResolvedLayout->passages) + { + std::vector> path;path.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + { + const double playerZ=static_cast(point.positionSubvoxels[2] + -point.radiusSubvoxels)+1.13*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + *1000000.0/World.CoreVoxelSizeMicrons; + path.push_back({qToMetres(point.positionSubvoxels[0])/coreMetres, + qToMetres(point.positionSubvoxels[1])/coreMetres,playerZ/256.0}); + } + const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.125,0.125,0.25,3,0.45); + const auto radialSquared=[](const std::array& p) + {return p[0]*p[0]+p[1]*p[1];}; + const bool mouthsInRadius=radialSquared(passage.upperMouthSubvoxels) + <=passage.placementRadiusSubvoxels*passage.placementRadiusSubvoxels + &&radialSquared(passage.lowerMouthSubvoxels) + <=passage.placementRadiusSubvoxels*passage.placementRadiusSubvoxels; + const bool mouthsInContent=mouthInContent(passage.upperStrateIndex,passage.upperMouthSubvoxels) + &&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels); + ++passageCount;passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent; + std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n", + passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex, + passage.controlPoints.size(),static_cast(passageAudit.centers), + static_cast(passageAudit.capsuleSamples), + static_cast(passageAudit.blocked), + static_cast(passageAudit.supportSamples), + static_cast(passageAudit.unsupported), + static_cast(passageAudit.gradeFailures),mouthsInContent?2:0, + mouthsInRadius?2:0,passageAudit.passed?"PASS":"FAIL"); + for(std::size_t i=0;i> path; + double supportStart=0.125,supportStep=0.125,supportMaximum=0.25, + supportProbeRadius=0.45; + std::uint32_t minimumSupportProbes=3; + }; + std::vector routeAuditInputs; + for(const auto& stratum:ResolvedLayout->strates) + { + std::vector anchors; + for(const auto& anchor:stratum.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3)anchors.push_back(&anchor); + if(anchors.size()<2)continue; + std::vector> route;route.reserve(anchors.size()); + for(const auto* anchor:anchors) + { + route.push_back({qToMetres(anchor->values[0])/coreMetres, + qToMetres(anchor->values[1])/coreMetres, + qToMetres(anchor->values[2])/coreMetres}); + ++routePoints; + } + double supportStartMetres=0.125,supportStepMetres=0.125, + supportMaximumMetres=0.25,supportProbeRadiusMetres=0.45; + std::uint32_t minimumSupportProbes=3; + const VoxelForgeCore::WorldAssemblyRouteConstraintsV1* routeConstraints=nullptr; + if(Assembly) + { + const auto worldRoute=std::find_if(Assembly->plan.strates.begin(), + Assembly->plan.strates.end(),[&](const auto& candidate) + {return candidate.strateIndex==stratum.strateIndex;}); + if(worldRoute!=Assembly->plan.strates.end() + &&worldRoute->routeConstraints.version==1) + routeConstraints=&worldRoute->routeConstraints; + const auto islandRoute=std::find_if(Assembly->floatingIslands.begin(), + Assembly->floatingIslands.end(),[&](const auto& candidate) + {return candidate.strateIndex==stratum.strateIndex;}); + if(islandRoute!=Assembly->floatingIslands.end() + &&islandRoute->manifest.recipe.routeConstraints.version==1) + routeConstraints=&islandRoute->manifest.recipe.routeConstraints; + } + if(routeConstraints) + { + const double qMetres=coreMetres + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + minimumSupportProbes=std::clamp( + routeConstraints->minimumSupportProbes,1,5); + supportProbeRadiusMetres=routeConstraints->supportProbeRadiusSubvoxels*qMetres; + if(stratum.generatorKind=="FloatingIslands") + { + supportStartMetres=(routeConstraints->minimumClearanceSubvoxels + +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/2)*qMetres; + supportStepMetres=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + *0.5*qMetres; + supportMaximumMetres=(routeConstraints->minimumClearanceSubvoxels + +routeConstraints->supportSearchDepthSubvoxels)*qMetres; + } + else + { + const std::int64_t spacing=std::min( + routeConstraints->sweepSpacingSubvoxels, + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/4); + supportStepMetres=std::max(1,spacing)*qMetres; + supportStartMetres=supportStepMetres; + supportMaximumMetres=routeConstraints->supportSearchDepthSubvoxels*qMetres; + } + } + const FWalkAudit routeAudit=auditPlayerPath(Shape,route,supportStartMetres, + supportStepMetres,supportMaximumMetres,minimumSupportProbes, + supportProbeRadiusMetres); + routeAuditInputs.push_back({stratum.strateIndex,route,supportStartMetres, + supportStepMetres,supportMaximumMetres,supportProbeRadiusMetres, + minimumSupportProbes}); + bool binding=true; + for(const auto& passage:ResolvedLayout->passages) + { + const auto equalPosition=[](const auto& a,const auto& b) + {return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];}; + if(passage.upperStrateIndex==stratum.strateIndex) + { + const std::array departure{{anchors.back()->values[0], + anchors.back()->values[1],anchors.back()->values[2]}}; + binding&=equalPosition(passage.upperMouthSubvoxels, + departure); + } + if(passage.lowerStrateIndex==stratum.strateIndex) + { + const std::array arrival{{anchors.front()->values[0], + anchors.front()->values[1],anchors.front()->values[2]}}; + binding&=equalPosition(passage.lowerMouthSubvoxels, + arrival); + } + } + ++routePasses;routeWalkPasses+=routeAudit.passed; + routeBindings+=binding;routeBindingFailures+=!binding; + std::printf("[ResolvedStrateRouteAudit] strate=%d route_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_binding=%s status=%s\n", + stratum.strateIndex,anchors.size(),static_cast(routeAudit.centers), + static_cast(routeAudit.capsuleSamples), + static_cast(routeAudit.blocked), + static_cast(routeAudit.supportSamples), + static_cast(routeAudit.unsupported), + static_cast(routeAudit.gradeFailures),binding?"PASS":"FAIL", + routeAudit.passed&&binding?"PASS":"FAIL"); + for(std::size_t i=0;ipassages) + { + std::vector> path;path.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + { + const double playerZ=static_cast(point.positionSubvoxels[2] + -point.radiusSubvoxels)+1.13*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + *1000000.0/World.CoreVoxelSizeMicrons; + path.push_back({qToMetres(point.positionSubvoxels[0])/coreMetres, + qToMetres(point.positionSubvoxels[1])/coreMetres,playerZ/256.0}); + } + const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, + 0.125,0.125,0.25,3,0.45); + blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; + std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n", + passage.stableId.c_str(),static_cast(blockedAudit.blocked), + blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false"); + } + else + std::fprintf(stderr,"[ResolvedPassageBlockedControl] compile_failed=%s\n",blockedCompileError.c_str()); + if(blockedCompiled) + for(const auto& input:routeAuditInputs) + { + const FWalkAudit controlRoute=auditPlayerPath(blockedWorld,input.path, + input.supportStart,input.supportStep,input.supportMaximum, + input.minimumSupportProbes,input.supportProbeRadius); + std::printf("[ResolvedStrateRouteBlockedControl] strate=%d blocked=%llu unsupported=%llu status=%s\n", + input.strateIndex,static_cast(controlRoute.blocked), + static_cast(controlRoute.unsupported), + controlRoute.passed?"PASS":"FAIL"); + } + const bool auditPassed=passageCount>0&&passagePasses==passageCount + &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings + &&routeBindingFailures==0 + &&blockedCompiled&&blockedControls==passageCount; + std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu blocked_controls=%llu/%llu status=%s\n", + static_cast(passageCount),static_cast(passagePasses), + static_cast(routePasses),static_cast(routeWalkPasses), + static_cast(routePoints), + static_cast(routeBindings),static_cast(routeBindingFailures), + static_cast(blockedControls),static_cast(passageCount), + auditPassed?"PASS":"FAIL"); + if(!auditPassed) + {Streamer.Stop();return 10;} + } if(bAuditResolvedLayoutBoundaries) { if(ResolvedLayout==nullptr) diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 996c4f2..6734e65 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -43,7 +43,9 @@ namespace StandaloneWorldAssemblyPrototype inline bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe, VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested, - std::uint64_t& gradeRejected,std::uint64_t& sweepRejected,std::string& error) + std::uint64_t& gradeRejected,std::uint64_t& placementRejected, + std::uint64_t& sweepRejected,std::int64_t arrivalRadiusSubvoxels, + std::int64_t departureRadiusSubvoxels,std::string& error) { using namespace VoxelForgeCore; constexpr std::int64_t PlayerRadiusQ=348; // 0.34 m in Core's 0.25 m voxel scale. @@ -73,7 +75,9 @@ namespace StandaloneWorldAssemblyPrototype constraints.sweepSpacingSubvoxels=StepQ; constraints.supportProbeRadiusSubvoxels=ProbeRadiusQ; constraints.supportSearchDepthSubvoxels=SupportDepthQ; - constraints.minimumClearanceSubvoxels=8; + // Match the FI route contract's half-voxel body clearance. Core's native + // field can differ slightly from the recipe sweep at the lower capsule cap. + constraints.minimumClearanceSubvoxels=128; constraints.minimumSupportProbes=3; const Heightfield2D finalField(recipe,stratum); @@ -90,7 +94,7 @@ namespace StandaloneWorldAssemblyPrototype candidate.centerX=centerX;candidate.centerY=centerY;candidate.direction=directionIndex; candidate.points.reserve(33); std::int64_t previousGround=0,previousX=0,previousY=0; - bool gradePass=true; + bool gradePass=true,placementPass=true; for(std::int32_t index=0;index<=32;++index) { const std::int64_t along=(index-16)*waypointSpacing; @@ -98,6 +102,13 @@ namespace StandaloneWorldAssemblyPrototype static_cast(along)*directions[directionIndex].x/65536.0); const std::int64_t y=centerY+std::llround( static_cast(along)*directions[directionIndex].y/65536.0); + const std::int64_t xQ=WorldAssemblyFixed(static_cast(x)); + const std::int64_t yQ=WorldAssemblyFixed(static_cast(y)); + const auto inRadius=[](std::int64_t xValue,std::int64_t yValue,std::int64_t radius) + {return radius<=0||xValue*xValue+yValue*yValue<=radius*radius;}; + if((index==0&&!inRadius(xQ,yQ,arrivalRadiusSubvoxels)) + ||(index==32&&!inRadius(xQ,yQ,departureRadiusSubvoxels))) + {placementPass=false;break;} const auto column=finalField.point(WorldAssemblyFixed(static_cast(x)), WorldAssemblyFixed(static_cast(y)),0); const std::int64_t horizontalQ=static_cast(std::llround( @@ -121,7 +132,8 @@ namespace StandaloneWorldAssemblyPrototype previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y; } ++tested; - if(gradePass)candidates.push_back(std::move(candidate)); + if(placementPass&&gradePass)candidates.push_back(std::move(candidate)); + else if(!placementPass)++placementRejected; else ++gradeRejected; } std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b) @@ -382,14 +394,33 @@ namespace StandaloneWorldAssemblyPrototype stratum.features.push_back(shelf); } } - std::uint64_t routeCandidates=0,routeGradeRejected=0,routeSweepRejected=0; + std::int64_t arrivalRadiusQ=0,departureRadiusQ=0; + const auto ownerVoxelsToCoreQ=[&](double value) + {return static_cast(std::llround(value*owner.OwnerVoxelSizeMicrons + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));}; + for(const FStrateSlot& slot:owner.Manager->GetLayout()) + { + if(!slot.Definition)continue; + if(slot.StrateIndex+1==stratum.strateIndex) + arrivalRadiusQ=std::max(ownerVoxelsToCoreQ(slot.Definition->PassageConfig.DistanceMin), + ownerVoxelsToCoreQ(slot.Definition->PassageConfig.DistanceMax)); + if(slot.StrateIndex==stratum.strateIndex+1&&definition) + departureRadiusQ=std::max(ownerVoxelsToCoreQ(definition->PassageConfig.DistanceMin), + ownerVoxelsToCoreQ(definition->PassageConfig.DistanceMax)); + } + std::uint64_t routeCandidates=0,routeGradeRejected=0,routePlacementRejected=0,routeSweepRejected=0; if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected, - routeSweepRejected,error))return false; - std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u status=PASS\n", + routePlacementRejected,routeSweepRejected,arrivalRadiusQ,departureRadiusQ,error))return false; + std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu placement_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u arrival_radius_m=%.3f departure_radius_m=%.3f status=PASS\n", static_cast(routeCandidates), static_cast(routeGradeRejected), + static_cast(routePlacementRejected), static_cast(routeSweepRejected),stratum.route.size(), - stratum.routeConstraints.minimumSupportProbes); + stratum.routeConstraints.minimumSupportProbes, + static_cast(arrivalRadiusQ)*owner.CoreVoxelSizeMicrons + /(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0), + static_cast(departureRadiusQ)*owner.CoreVoxelSizeMicrons + /(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0)); manifest.plan.strates.push_back(std::move(stratum)); if(!manifest.finalize(error))return false; if(outManifest)*outManifest=manifest; @@ -537,6 +568,18 @@ namespace StandaloneWorldAssemblyPrototype auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error); if(!planned)return false; + const VoxelForgeCore::FloatingIslandFieldV1 routeField( + planned->recipe,planned->plan); + const auto routeAudit=VoxelForgeCore::AuditFloatingIslandRoute(routeField); + std::printf("[FloatingIslandRouteAudit] points=%zu centers=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + planned->plan.route.size(), + static_cast(routeAudit.centerSamples), + static_cast(routeAudit.capsuleFieldSamples), + static_cast(routeAudit.blockedCapsuleSamples), + static_cast(routeAudit.supportProbeSamples), + static_cast(routeAudit.unsupportedCenterSamples), + static_cast(routeAudit.gradeFailures), + routeAudit.passed?"PASS":"FAIL"); const double metresPerSubvoxel=static_cast(owner.CoreVoxelSizeMicrons) /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double bedrockTopMetres=(minimumZ+recipe.boundarySealSubvoxels)*metresPerSubvoxel; @@ -598,7 +641,7 @@ namespace StandaloneWorldAssemblyPrototype inline bool BuildResolvedLayoutSnapshot(const FStandaloneOwnerWorld& owner, const VoxelForgeCore::WorldAssemblyManifest& assembly,std::string_view recipeVariant, - VoxelForgeCore::ResolvedLayoutSnapshotV2& out,std::string& error) + VoxelForgeCore::ResolvedLayoutSnapshotV3& out,std::string& error) { using namespace VoxelForgeCore; if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;} @@ -661,6 +704,15 @@ namespace StandaloneWorldAssemblyPrototype anchor.values={a.centerSubvoxels[0],a.centerSubvoxels[1],a.centerSubvoxels[2],a.radiusSubvoxels,a.topHalfSubvoxels}; s.anchors.push_back(std::move(anchor)); } + for(std::size_t n=0;nmanifest.plan.route.size();++n) + { + const auto& p=it->manifest.plan.route[n];ResolvedLayoutAnchorV2 anchor; + anchor.kind="route_point";anchor.stableId="route/"+std::to_string(n); + anchor.units="1/256 Core voxel"; + anchor.values={p.positionSubvoxels[0],p.positionSubvoxels[1], + p.positionSubvoxels[2],p.radiusSubvoxels}; + s.anchors.push_back(std::move(anchor)); + } } } s.interiorBottomMicrometres=bottom;s.interiorTopMicrometres=top; @@ -678,8 +730,8 @@ namespace StandaloneWorldAssemblyPrototype s.managerLookup="UVoxelStrateManager selects the slot from the director-resolved layout chunk width"; s.managerChunkWidthMicrometres=owner.StrateLayoutChunkSizeMicrometres; s.dispatchChunkWidthMicrometres=owner.StrateLayoutChunkSizeMicrometres; - s.boundDispatchPath="all generator plans use ResolvedLayoutSnapshotV2 layoutChunkWidthSubvoxels"; - s.queryDispatchPath="all generator dispatch uses floor(CoreZ / ResolvedLayoutSnapshotV2 layoutChunkWidthSubvoxels)"; + s.boundDispatchPath="all generator plans use ResolvedLayoutSnapshotV3 layoutChunkWidthSubvoxels"; + s.queryDispatchPath="all generator dispatch uses floor(CoreZ / ResolvedLayoutSnapshotV3 layoutChunkWidthSubvoxels)"; const std::int64_t solidDensity=std::max(1,WorldAssemblyQ16(std::max(0.05f,authored[1]))); s.ceiling={"ceiling",top-seal,top,true,true,false,"solid ceiling from strate BoundarySealThickness; breakable by default", "interior-facing lower face","shared resolved-layout density band",solidDensity}; @@ -701,18 +753,28 @@ namespace StandaloneWorldAssemblyPrototype a.units="1/256 Core voxel";a.values={p.positionSubvoxels[0],p.positionSubvoxels[1],p.positionSubvoxels[2],p.radiusSubvoxels}; s.anchors.push_back(std::move(a));} } - for(std::size_t n=0;nGetPassages().Num();++n) - { - const auto& passage=owner.Manager->GetPassages()[static_cast(n)]; - if(passage.UpperStrateIndex!=slot.StrateIndex&&passage.LowerStrateIndex!=slot.StrateIndex)continue; - ResolvedLayoutAnchorV2 a;a.kind="passage";a.stableId="passage/"+std::to_string(n);a.units="milli owner voxels"; - a.values={std::llround(passage.UpperPoint.X*1000.0),std::llround(passage.UpperPoint.Y*1000.0), - std::llround(passage.UpperPoint.Z*1000.0),std::llround(passage.LowerPoint.X*1000.0), - std::llround(passage.LowerPoint.Y*1000.0),std::llround(passage.LowerPoint.Z*1000.0)}; - s.anchors.push_back(std::move(a)); - } out.strates.push_back(std::move(s)); } + std::vector passageConfigs; + for(const FStrateSlot& slot:owner.Manager->GetLayout()) + { + if(!slot.Definition)continue; + const auto& authored=slot.Definition->PassageConfig; + const auto ownerVoxelsToCoreQ=[&](double value) + {return static_cast(std::llround(value*owner.OwnerVoxelSizeMicrons + *WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));}; + ResolvedLayoutPassageConfigV1 config;config.upperStrateIndex=slot.StrateIndex; + config.connections=authored.Connections; + config.mouthRadiusSubvoxels=ownerVoxelsToCoreQ(authored.MouthRadius); + config.midRadiusSubvoxels=ownerVoxelsToCoreQ(authored.MidRadius); + config.reachMinimumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMin); + config.reachMaximumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMax); + config.distanceMinimumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMin); + config.distanceMaximumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMax); + config.wanderSubvoxels=ownerVoxelsToCoreQ(authored.Wander); + passageConfigs.push_back(config); + } + if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false; return out.finalize(error); }