From 422dd7761ad1a210a1b958b77ade3fe55d98efd8 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Fri, 23 Oct 2026 21:50:56 +0200 Subject: [PATCH] feat(core): deterministic bounded SDF landmark stamps, off by default (L43); research R10 Stamp vocabulary v1 (carve ellipsoid/arch, fill pillar/dais) with smooth CSG and seeded relief; the director resolves records (stable ID, strate, micrometre position, conservative bounds) from hash(seed, strate, ID) with up to 512 candidates and rejects any that touch a passage mouth, walk corridor, content face or ceiling/bedrock band; a stamp BVH skips tiles outside all bounds. No requests: canonical JSON and owner hashes unchanged. --landmark-chamber-demo adds a SW chamber (buried; see review). fast/medium/full PASS on clean builds. Research R10 (read-only): ore/resource channel design. Review also records a pre-existing W1/W12 determinism mismatch at a non-spawn camera (L37+, not S14). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- ARCHITECTURE.md | 29 ++ CODEMAP.md | 13 + .../include/VoxelForgeCore/VoxelForgeCore.h | 58 +++ Standalone/Core/src/ResolvedLayoutInternal.h | 398 +++++++++++++++++- Standalone/Core/src/ShapeWorldInternal.h | 356 +++++++++++++++- Standalone/Core/src/VoxelForgeCore.cpp | 27 ++ .../WorldAssemblerM3/Linux43/REPORT.md | 72 ++++ .../Linux43/captures/chamber-entrance.jpg | Bin 0 -> 26913 bytes .../Linux43/captures/chamber-inside.jpg | Bin 0 -> 29256 bytes .../Linux43/captures/chamber-overview.jpg | Bin 0 -> 43827 bytes .../WorldAssemblerM3/Research10/REPORT.md | 63 +++ Standalone/Source/StreamBenchmarkHeadless.cpp | 124 ++++++ 12 files changed, 1138 insertions(+), 2 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-entrance.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-inside.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-overview.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Research10/REPORT.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bb46bf2..ff7819f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2236,6 +2236,35 @@ also retains generator-specific anchor and default-mouth placement rules; those shared composition boundary policy. No Unreal source changed. See the L42 report for hashes, tier results and paired cost measurements. +### 8.53 Shared Core landmark SDF stamps — World Assembler M3 L43 + +`ResolvedLayoutStampV1` adds a small, version-1 SDF vocabulary to the shared resolved layout: +carve ellipsoid, carve arch, fill pillar and fill dais. Requests and resolved records use integer +micrometres and carry a stable ID, strate, position, conservative bounds, half extents, smooth-CSG +width, bounded irregularity amplitude/wavelength and quarter-turn orientation. A stamp's seeded +noise and bounded placement offsets derive from `(world seed, strate, stable ID)`; records are +sorted by stable ID before the layout identity is finalized. If no request is made, the stamp fields +are omitted from canonical JSON, preserving the no-stamp layout identity and density hashes. + +Resolution retries a finite, deterministic set of candidates and rejects a record if its complete +bound intersects a passage mouth or walk corridor, reaches a strate content face, or overlaps the +ceiling or bedrock band. The director reports failure if no safe candidate exists. The strict-layout +negative control forces a candidate onto a route and into a ceiling band and expects both rejections. + +`FStandaloneShapeWorld` applies stamps after the shared resolved-strate boundary and passage +composition. Carves use smooth maximum with the negated stamp SDF; fills use smooth minimum. Seeded +trilinear value noise adds low-frequency relief without changing the conservative bound. The tile +query scope builds a BVH candidate list from each tile's sample bounds. Samples in a tile outside +every stamp box do no stamp SDF evaluation. Box classification marks stamp-intersecting boxes mixed, +and native-only readiness refuses to skip them. + +The `--landmark-chamber-demo` recipe flag adds an irregular ellipsoid chamber, a separate arched +entrance and a low fill dais in the owner SurfaceWorld strate, about 31 m from the passage arrival +mouth and off the walk route. It stays disabled by default. Active records carry vocabulary version +1 in the layout identity, making the demo an explicit world change; no Unreal adapter was added. +L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier +results, captures and any failed runs. + ## 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 8f336b6..fbe1fb9 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1295,6 +1295,19 @@ Hash parity is unchanged from L37/L39; see ARCHITECTURE §8.50 and Owner hashes match L41; see ARCHITECTURE §8.52 and [`Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md). +### 3.35 Core landmark SDF stamps — World Assembler M3 L43 + +| Symbol | Canonical file | Role | +|---|---|---| +| `ResolvedLayoutStampRequestV1`, `ResolvedLayoutStampV1`, `ResolvedSdfStampKindV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Version-1 bounded stamp vocabulary and integer-micrometre request/resolved records shared by all Core generators. | +| `ResolveResolvedLayoutStampsV1`, `AuditResolvedLayoutStampPlacementV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Seed/strate/stable-ID placement with bounded retries; rejects passage-mouth, walk-corridor, content-face, ceiling and bedrock conflicts. Empty requests preserve the old canonical layout payload. | +| `BuildStampSpatialIndex`, `ApplyResolvedSdfStamps`, `StampSdf` | `Standalone/Core/src/ShapeWorldInternal.h` | BVH-built conservative bounds select stamps once per tile; shared density composition applies smooth carve/fill SDFs with seeded value-noise relief. Box classification and native-only readiness decline boxes touched by a stamp. | +| `--landmark-chamber-demo` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Opt-in owner-world recipe: one SW ellipsoid chamber, arched entrance and low fill dais, placed beside the passage arrival. Off by default. | + +See ARCHITECTURE §8.53 and +[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md) +for layout identities, owner hashes, outside-box timing, audits and captures. + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 1963627..cc4d176 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -705,8 +705,50 @@ namespace VoxelForgeCore // 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 ResolvedLayoutSchemaVersionV3 = 3; + inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; + // Stable, bounded geometry edits composed into every Core field after the shared + // strate boundaries and passages. Coordinates and extents use integer micrometres. + enum class ResolvedSdfStampKindV1 : std::uint8_t + { + carveEllipsoid = 1, + carveArch = 2, + fillPillar = 3, + fillDais = 4 + }; + + struct ResolvedLayoutStampRequestV1 + { + std::string stableId; + std::int32_t strateIndex = -1; + ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid; + // A preferred center and a bounded deterministic search radius. + std::array anchorPositionMicrometres{}; + std::int64_t placementRadiusMicrometres = 0; + std::array halfExtentsMicrometres{}; + std::int64_t smoothWidthMicrometres = 0; + std::int64_t irregularityAmplitudeMicrometres = 0; + std::int64_t irregularityWavelengthMicrometres = 0; + std::uint8_t yawQuarterTurns = 0; + }; + + struct ResolvedLayoutStampV1 + { + std::string stableId; + std::int32_t strateIndex = -1; + ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid; + std::array positionMicrometres{}; + std::array boundsMinimumMicrometres{}; + std::array boundsMaximumMicrometres{}; + std::array halfExtentsMicrometres{}; + std::int64_t smoothWidthMicrometres = 0; + std::int64_t irregularityAmplitudeMicrometres = 0; + std::int64_t irregularityWavelengthMicrometres = 0; + std::uint64_t irregularitySeed = 0; + std::uint8_t yawQuarterTurns = 0; + }; + struct ResolvedLayoutAnchorV2 { std::string kind; @@ -872,6 +914,9 @@ namespace VoxelForgeCore std::int64_t strateLayoutChunkWidthSubvoxels = 8192; std::vector strates; std::vector passages; + // Kept absent from canonical layout identity when no feature requests stamps. + std::uint32_t stampVocabularyVersion = 0; + std::vector stamps; std::string stableIdentity; bool finalize(std::string& error); @@ -907,6 +952,19 @@ namespace VoxelForgeCore const std::vector& configs, std::string& error); + struct ResolvedLayoutStampPlacementAuditV1 + { + std::uint32_t negativeControlsRun = 0; + std::uint32_t negativeControlsRejected = 0; + bool passed = false; + }; + VFCORE_API bool ResolveResolvedLayoutStampsV1( + ResolvedLayoutSnapshotV3& layout, std::int32_t seed, + std::span requests, + std::string& error); + VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1( + const ResolvedLayoutSnapshotV3& layout); + struct FloatingIslandBoundsAuditResult { std::uint64_t boxesTested = 0; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 0c02080..024730c 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -144,6 +144,145 @@ namespace for(unsigned char c:value){hash^=c;hash*=1099511628211ull;} return hash; } + std::uint64_t LayoutStampMix(std::uint64_t value) + { + value^=value>>30;value*=0xbf58476d1ce4e5b9ull; + value^=value>>27;value*=0x94d049bb133111ebull; + return value^(value>>31); + } + std::uint64_t LayoutStampSeed(std::int32_t seed,std::int32_t strate, + std::string_view stableId) + { + std::uint64_t hash=14695981039346656037ull; + TransitionHashWord(hash,static_cast(seed)); + TransitionHashWord(hash,static_cast(strate)); + for(unsigned char c:stableId){hash^=c;hash*=1099511628211ull;} + return LayoutStampMix(hash); + } + std::uint64_t LayoutStampNext(std::uint64_t& state) + {state=LayoutStampMix(state+0x9e3779b97f4a7c15ull);return state;} + bool LayoutStampKindValid(ResolvedSdfStampKindV1 kind) + { + const auto value=static_cast(kind); + return value>=static_cast(ResolvedSdfStampKindV1::carveEllipsoid) + &&value<=static_cast(ResolvedSdfStampKindV1::fillDais); + } + double LayoutStampPointSegmentDistance(const std::array& point, + const std::array& a,const std::array& b) + { + double lengthSquared=0.0,dot=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double delta=static_cast(b[axis])-a[axis]; + lengthSquared+=delta*delta; + dot+=(static_cast(point[axis])-a[axis])*delta; + } + const double t=lengthSquared>0.0?std::clamp(dot/lengthSquared,0.0,1.0):0.0; + double distanceSquared=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double nearest=static_cast(a[axis]) + +t*(static_cast(b[axis])-a[axis]); + const double delta=static_cast(point[axis])-nearest; + distanceSquared+=delta*delta; + } + return std::sqrt(distanceSquared); + } + double LayoutStampBoundRadius(const ResolvedLayoutStampV1& stamp) + { + double radiusSquared=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double extent=(static_cast(stamp.boundsMaximumMicrometres[axis]) + -stamp.boundsMinimumMicrometres[axis])*0.5; + radiusSquared+=extent*extent; + } + return std::sqrt(radiusSquared); + } + bool LayoutStampSafe(const ResolvedLayoutSnapshotV3& layout, + const ResolvedLayoutStampV1& stamp,std::string* reason=nullptr) + { + const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(), + [&](const auto& value){return value.strateIndex==stamp.strateIndex;}); + if(strate==layout.strates.end()||!strate->content.presentInCurrentField) + {if(reason)*reason="stamp strate has no current content band";return false;} + if(stamp.boundsMinimumMicrometres[2]<=strate->content.bottomMicrometres + ||stamp.boundsMaximumMicrometres[2]>=strate->content.topMicrometres) + {if(reason)*reason="stamp bounds touch or cross the ceiling/bedrock content faces";return false;} + for(const auto* band:{&strate->ceiling,&strate->bedrock}) + if(band->presentInCurrentField + &&stamp.boundsMinimumMicrometres[2]topMicrometres + &&stamp.boundsMaximumMicrometres[2]>band->bottomMicrometres) + {if(reason)*reason="stamp bounds overlap a ceiling or bedrock band";return false;} + + const double boundRadius=LayoutStampBoundRadius(stamp); + const double coreMicrometres=std::max(1,layout.coreVoxelSizeMicrometres); + constexpr double RouteClearanceMicrometres=1000000.0; + const auto routeSafe=[&](const std::array& a, + const std::array& b,double radiusSubvoxels) + { + const double radius=radiusSubvoxels*coreMicrometres + /WorldAssemblySubvoxelsPerVoxel; + return LayoutStampPointSegmentDistance(stamp.positionMicrometres,a,b) + >boundRadius+radius+RouteClearanceMicrometres; + }; + const auto qToMicrometres=[&](const std::array& q) + { + std::array result{}; + for(std::size_t axis=0;axis<3;++axis) + result[axis]=static_cast(std::llround( + static_cast(q[axis])*coreMicrometres + /WorldAssemblySubvoxelsPerVoxel)); + return result; + }; + for(const auto& passage:layout.passages) + { + if(passage.upperStrateIndex!=stamp.strateIndex + &&passage.lowerStrateIndex!=stamp.strateIndex)continue; + const auto upper=qToMicrometres(passage.upperMouthSubvoxels); + const auto lower=qToMicrometres(passage.lowerMouthSubvoxels); + const double mouthRadius=static_cast(passage.mouthRadiusSubvoxels) + *coreMicrometres/WorldAssemblySubvoxelsPerVoxel; + if(LayoutStampPointSegmentDistance(stamp.positionMicrometres,upper,upper) + <=boundRadius+mouthRadius+RouteClearanceMicrometres + ||LayoutStampPointSegmentDistance(stamp.positionMicrometres,lower,lower) + <=boundRadius+mouthRadius+RouteClearanceMicrometres) + {if(reason)*reason="stamp is inside a passage mouth safety volume";return false;} + for(std::size_t i=0;i+1 route; + for(const auto& anchor:strate->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back(&anchor); + for(std::size_t i=0;i{ + static_cast(std::llround(static_cast(anchor.values[0]) + *coreMicrometres/WorldAssemblySubvoxelsPerVoxel)), + static_cast(std::llround(static_cast(anchor.values[1]) + *coreMicrometres/WorldAssemblySubvoxelsPerVoxel)), + static_cast(std::llround(static_cast(anchor.values[2]) + *coreMicrometres/WorldAssemblySubvoxelsPerVoxel))};}; + const auto a=makePoint(*route[i]); + const auto b=i+1values.size()>=4?route[i]->values[3]:0, + route[i+1]->values.size()>=4?route[i+1]->values[3]:0) + :route[i]->values.size()>=4?route[i]->values[3]:0; + if(!routeSafe(a,b,radius)) + {if(reason)*reason="stamp overlaps a resolved strate walk route";return false;} + } + return true; + } std::string LayoutHex(std::uint64_t value) {std::ostringstream out;out<(stamp.kind) + <<",\"position_um\":["<(stamp.yawQuarterTurns)<<'}'; + } + out<<']'; + } + out<<'}';return out.str(); } std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key) {return obj.Get(key).Integer();} @@ -659,6 +825,50 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, add("passage_grade",passage.stableId+" is not strictly descending or exceeds the 15 degree grade limit"); } } + if(stamps.empty()) + { + if(stampVocabularyVersion!=0) + add("stamp_vocabulary_version","stamp vocabulary version is set without any stamp records"); + } + else if(stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV1) + add("stamp_vocabulary_version","resolved stamps require vocabulary version 1"); + std::vector stampIds; + for(const auto& stamp:stamps) + { + if(stamp.stableId.empty() + ||std::find(stampIds.begin(),stampIds.end(),stamp.stableId)!=stampIds.end()) + add("stamp_identity","stamp has an empty or duplicate stable identity"); + else stampIds.push_back(stamp.stableId); + if(!LayoutStampKindValid(stamp.kind)||stamp.yawQuarterTurns>3) + add("stamp_kind","stamp has an unsupported kind or orientation"); + bool validExtents=true; + for(const auto extent:stamp.halfExtentsMicrometres) + validExtents=validExtents&&extent>=500000&&extent<=100000000; + if(!validExtents||stamp.smoothWidthMicrometres<0 + ||stamp.smoothWidthMicrometres>10000000 + ||stamp.irregularityAmplitudeMicrometres<0 + ||stamp.irregularityAmplitudeMicrometres>10000000 + ||stamp.irregularityWavelengthMicrometres<0 + ||stamp.irregularityWavelengthMicrometres>100000000 + ||(stamp.irregularityAmplitudeMicrometres>0 + &&stamp.irregularityWavelengthMicrometres<=0)) + add("stamp_parameters",stamp.stableId+" has invalid or unbounded geometry parameters"); + const std::int64_t pad=stamp.smoothWidthMicrometres + +stamp.irregularityAmplitudeMicrometres; + const std::array extents={ + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad, + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad, + stamp.halfExtentsMicrometres[2]+pad}; + for(std::size_t axis=0;axis<3;++axis) + if(stamp.boundsMinimumMicrometres[axis] + !=stamp.positionMicrometres[axis]-extents[axis] + ||stamp.boundsMaximumMicrometres[axis] + !=stamp.positionMicrometres[axis]+extents[axis]) + add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters"); + std::string safetyReason; + if(!LayoutStampSafe(*this,stamp,&safetyReason)) + add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason); + } if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;} return true; } @@ -674,6 +884,7 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( s.coreVoxelSizeMicrometres=static_cast(LayoutInt(root,"core_voxel_size_um")); s.strateLayoutChunkSizeMicrometres=static_cast(LayoutInt(root,"strate_layout_chunk_size_um")); s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels"); + s.stampVocabularyVersion=static_cast(LayoutInt(root,"stamp_vocabulary_version")); for(const auto& v:root.Get("strates").AsArray()) { ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id"); @@ -721,12 +932,197 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( } s.passages.push_back(std::move(p)); } + for(const auto& v:root.Get("stamps").AsArray()) + { + ResolvedLayoutStampV1 stamp;stamp.stableId=LayoutString(v,"stable_id"); + stamp.strateIndex=static_cast(LayoutInt(v,"strate_index")); + stamp.kind=static_cast(LayoutInt(v,"kind")); + const auto readTriplet=[&](std::string_view key,std::array& target) + { + const auto& values=v.Get(key).AsArray(); + for(std::size_t axis=0;axis<3&&axis(LayoutInt(v,"irregularity_seed")); + stamp.yawQuarterTurns=static_cast(LayoutInt(v,"yaw_quarter_turns")); + s.stamps.push_back(std::move(stamp)); + } const std::string serializedIdentity=LayoutString(root,"stable_identity"); if(!s.finalize(error))return std::nullopt; if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;} return s; } +bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed, + std::span requests,std::string& error) +{ + error.clear(); + if(requests.empty()) + { + ResolvedLayoutSnapshotV3 candidate=layout; + candidate.stampVocabularyVersion=0;candidate.stamps.clear(); + if(!candidate.finalize(error))return false; + layout=std::move(candidate);return true; + } + std::vector ordered(requests.begin(),requests.end()); + std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b) + {return a.stableId0 + &&ordered[requestIndex-1].stableId==request.stableId)) + {error="stamp requests require non-empty unique stable IDs";return false;} + if(!LayoutStampKindValid(request.kind)||request.yawQuarterTurns>3 + ||request.placementRadiusMicrometres<0 + ||request.placementRadiusMicrometres>100000000 + ||request.smoothWidthMicrometres<0||request.smoothWidthMicrometres>10000000 + ||request.irregularityAmplitudeMicrometres<0 + ||request.irregularityAmplitudeMicrometres>10000000 + ||request.irregularityWavelengthMicrometres<0 + ||request.irregularityWavelengthMicrometres>100000000 + ||(request.irregularityAmplitudeMicrometres>0 + &&request.irregularityWavelengthMicrometres<=0)) + {error="stamp request "+request.stableId+" has invalid or unbounded parameters";return false;} + if(std::any_of(request.halfExtentsMicrometres.begin(),request.halfExtentsMicrometres.end(), + [](std::int64_t extent){return extent<500000||extent>100000000;})) + {error="stamp request "+request.stableId+" has an extent outside 0.5..100 m";return false;} + const auto stratum=std::find_if(candidate.strates.begin(),candidate.strates.end(), + [&](const auto& value){return value.strateIndex==request.strateIndex;}); + if(stratum==candidate.strates.end()) + {error="stamp request "+request.stableId+" names an unknown strate";return false;} + for(const auto coordinate:request.anchorPositionMicrometres) + if(coordinate<-1000000000000ll||coordinate>1000000000000ll) + {error="stamp request "+request.stableId+" has an unbounded anchor";return false;} + + // The shared JSON value type stores numbers as IEEE-754 doubles; keep the + // serialized seed in its exact integer range so the canonical round trip holds. + const std::uint64_t baseSeed=LayoutStampSeed(seed,request.strateIndex,request.stableId) + &0x001fffffffffffffull; + std::uint64_t state=baseSeed; + const auto randomOffset=[&](std::uint64_t bits) + { + const std::int64_t radius=request.placementRadiusMicrometres; + if(radius==0)return std::int64_t{0}; + const std::uint64_t span=static_cast(radius)*2+1; + return static_cast(bits%span)-radius; + }; + bool placed=false; + for(std::uint32_t attempt=0;attempt<(request.placementRadiusMicrometres==0?1u:512u);++attempt) + { + ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId; + stamp.strateIndex=request.strateIndex;stamp.kind=request.kind; + stamp.halfExtentsMicrometres=request.halfExtentsMicrometres; + stamp.smoothWidthMicrometres=request.smoothWidthMicrometres; + stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres; + stamp.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres; + stamp.irregularitySeed=baseSeed;stamp.yawQuarterTurns=request.yawQuarterTurns; + std::array offset{}; + if(request.placementRadiusMicrometres>0) + { + offset={randomOffset(LayoutStampNext(state)), + randomOffset(LayoutStampNext(state)), + randomOffset(LayoutStampNext(state))/3}; + const long double radius=static_cast(request.placementRadiusMicrometres); + const long double distanceSquared=static_cast(offset[0])*offset[0] + +static_cast(offset[1])*offset[1] + +static_cast(offset[2])*offset[2]; + if(distanceSquared>radius*radius)continue; + } + for(std::size_t axis=0;axis<3;++axis) + stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis]; + const std::int64_t pad=stamp.smoothWidthMicrometres + +stamp.irregularityAmplitudeMicrometres; + const std::array extents={ + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad, + stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad, + stamp.halfExtentsMicrometres[2]+pad}; + for(std::size_t axis=0;axis<3;++axis) + { + stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis]; + stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis]; + } + if(!LayoutStampSafe(candidate,stamp))continue; + candidate.stamps.push_back(std::move(stamp));placed=true;break; + } + if(!placed) + {error="stamp request "+request.stableId+" has no deterministic passage-safe content placement";return false;} + } + if(!candidate.finalize(error))return false; + std::vector issues;std::string strictError; + if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError)) + {error="strict stamp placement validation failed: "+strictError;return false;} + layout=std::move(candidate);return true; +} + +ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1( + const ResolvedLayoutSnapshotV3& layout) +{ + ResolvedLayoutStampPlacementAuditV1 result; + const auto makeControl=[&](const std::array& position, + std::int32_t strateIndex,std::array minimum, + std::array maximum,std::string id, + std::string_view expectedReason) + { + ResolvedLayoutSnapshotV3 candidate=layout; + ResolvedLayoutStampV1 stamp;stamp.stableId=std::move(id); + stamp.strateIndex=strateIndex;stamp.kind=ResolvedSdfStampKindV1::carveEllipsoid; + stamp.positionMicrometres=position;stamp.boundsMinimumMicrometres=minimum; + stamp.boundsMaximumMicrometres=maximum; + stamp.halfExtentsMicrometres={250000,250000,250000}; + stamp.irregularitySeed=1; + candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV1; + candidate.stamps={stamp}; + std::vector issues;std::string error; + ++result.negativeControlsRun; + if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,error) + &&std::any_of(issues.begin(),issues.end(),[&](const auto& issue) + {return issue.code=="stamp_placement" + &&issue.detail.find(expectedReason)!=std::string::npos;})) + ++result.negativeControlsRejected; + }; + const auto passage=std::find_if(layout.passages.begin(),layout.passages.end(), + [](const auto& value){return value.controlPoints.size()>=2;}); + if(passage!=layout.passages.end()) + { + const double scale=static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel; + const std::size_t routeControlIndex=passage->lowerMouthControlIndex>4 + ?passage->lowerMouthControlIndex-4:0; + std::array position{}; + for(std::size_t axis=0;axis<3;++axis) + position[axis]=static_cast(std::llround( + passage->controlPoints[routeControlIndex].positionSubvoxels[axis]*scale)); + makeControl(position,passage->lowerStrateIndex, + {position[0]-250000,position[1]-250000,position[2]-250000}, + {position[0]+250000,position[1]+250000,position[2]+250000}, + "negative-control/forced-on-route","resolved passage walk corridor"); + } + const auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(), + [](const auto& value){return value.content.presentInCurrentField;}); + if(stratum!=layout.strates.end()) + { + const std::array position={0,0,stratum->content.topMicrometres}; + makeControl(position,stratum->strateIndex, + {position[0]-250000,position[1]-250000,position[2]-250000}, + {position[0]+250000,position[1]+250000,position[2]+250000}, + "negative-control/forced-into-ceiling-band","touch or cross the ceiling/bedrock"); + } + result.passed=result.negativeControlsRun==2 + &&result.negativeControlsRejected==result.negativeControlsRun; + return result; +} + bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed, const std::vector& configs,std::string& error) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 61e9105..643b039 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -168,6 +168,7 @@ public: minimumSubvoxels_,maximumSubvoxels_); featureIndices_=shape.BuildPassageFeatureTileList( minimumSubvoxels_,maximumSubvoxels_); + stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_); passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0); passageColumns_=passageRoutes_; passageMouthSupports_=passageRoutes_; @@ -216,11 +217,14 @@ public: &&shape_->ResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres); } std::size_t featureCount() const{return featureIndices_.size();} + bool hasStampCandidates() const{return !stampIndices_.empty();} + const std::vector& stampCandidates() const{return stampIndices_;} private: const FStandaloneShapeWorld* shape_; std::array minimumSubvoxels_,maximumSubvoxels_; std::vector featureIndices_; + std::vector stampIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; bool routeFeatureOverlap_=false,columnZOverlap_=false; bool mayContainResolvedBoundaryBand_=true; @@ -1775,7 +1779,11 @@ public: } float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const - {return ApplyResolvedStratePassageCarves(ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);} + { + const float composed=ApplyResolvedStratePassageCarves( + ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z); + return StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z); + } bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const { @@ -3170,6 +3178,169 @@ private: std::size_t first=0,end=0; int left=-1,right=-1; }; + struct FStampSpatialBound + { + std::array minimumSubvoxels{},maximumSubvoxels{}; + }; + struct FStampSpatialNode + { + std::array minimumSubvoxels{},maximumSubvoxels{}; + std::size_t first=0,end=0; + int left=-1,right=-1; + }; + struct FStampRuntime + { + const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr; + std::array positionVoxels{},halfExtentsVoxels{}; + float smoothWidthVoxels=0.0f; + float irregularityAmplitudeVoxels=0.0f; + float irregularityWavelengthVoxels=0.0f; + std::uint64_t irregularitySeed=0; + std::uint8_t quarterTurns=0; + }; + + int BuildStampSpatialNode(std::size_t first,std::size_t end) + { + FStampSpatialNode node;node.first=first;node.end=end; + node.minimumSubvoxels.fill(std::numeric_limits::max()); + node.maximumSubvoxels.fill(std::numeric_limits::min()); + for(std::size_t offset=first;offset(StampSpatialNodes.size()); + StampSpatialNodes.push_back(node); + if(end-first>8) + { + std::size_t splitAxis=0; + for(std::size_t axis=1;axis<3;++axis) + if(static_cast(node.maximumSubvoxels[axis]) + -node.minimumSubvoxels[axis] + >static_cast(node.maximumSubvoxels[splitAxis]) + -node.minimumSubvoxels[splitAxis])splitAxis=axis; + const std::size_t middle=first+(end-first)/2; + std::nth_element(StampSpatialOrder.begin()+first, + StampSpatialOrder.begin()+middle,StampSpatialOrder.begin()+end, + [this,splitAxis](std::uint32_t a,std::uint32_t b) + { + const auto& left=StampSpatialBounds[a];const auto& right=StampSpatialBounds[b]; + const long double leftCenter=static_cast( + left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis]; + const long double rightCenter=static_cast( + right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis]; + return leftCenter==rightCenter?a& minimum, + const std::array& maximum, + std::vector& result) const + { + if(nodeIndex<0)return; + const auto& node=StampSpatialNodes[static_cast(nodeIndex)]; + for(std::size_t axis=0;axis<3;++axis) + if(node.maximumSubvoxels[axis]maximum[axis])return; + if(node.left<0) + { + for(std::size_t offset=node.first;offset=minimum[axis]; + if(overlap)result.push_back(index); + } + return; + } + QueryStampSpatialNode(node.left,minimum,maximum,result); + QueryStampSpatialNode(node.right,minimum,maximum,result); + } + + void BuildStampSpatialIndex() + { + StampSpatialBounds.clear();StampSpatialOrder.clear();StampSpatialNodes.clear(); + StampRuntime.clear(); + if(!ResolvedLayout||ResolvedLayout->stamps.empty()||!Owner + ||Owner->CoreVoxelSizeMicrons==0)return; + constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const long double scale=Q/Owner->CoreVoxelSizeMicrons; + StampSpatialBounds.reserve(ResolvedLayout->stamps.size()); + StampRuntime.reserve(ResolvedLayout->stamps.size()); + for(const auto& stamp:ResolvedLayout->stamps) + { + FStampSpatialBound bound; + for(std::size_t axis=0;axis<3;++axis) + { + bound.minimumSubvoxels[axis]=static_cast(std::floor( + static_cast(stamp.boundsMinimumMicrometres[axis])*scale)); + bound.maximumSubvoxels[axis]=static_cast(std::ceil( + static_cast(stamp.boundsMaximumMicrometres[axis])*scale)); + } + StampSpatialBounds.push_back(bound); + FStampRuntime runtime;runtime.record=&stamp; + for(std::size_t axis=0;axis<3;++axis) + runtime.positionVoxels[axis]=static_cast( + static_cast(stamp.positionMicrometres[axis]) + /Owner->CoreVoxelSizeMicrons); + const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0; + const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1; + runtime.halfExtentsVoxels={ + static_cast(static_cast(stamp.halfExtentsMicrometres[0]) + /Owner->CoreVoxelSizeMicrons), + static_cast(static_cast(stamp.halfExtentsMicrometres[1]) + /Owner->CoreVoxelSizeMicrons), + static_cast(static_cast(stamp.halfExtentsMicrometres[2]) + /Owner->CoreVoxelSizeMicrons)}; + (void)xAxis;(void)yAxis; + runtime.smoothWidthVoxels=static_cast( + static_cast(stamp.smoothWidthMicrometres)/Owner->CoreVoxelSizeMicrons); + runtime.irregularityAmplitudeVoxels=static_cast( + static_cast(stamp.irregularityAmplitudeMicrometres) + /Owner->CoreVoxelSizeMicrons); + runtime.irregularityWavelengthVoxels=static_cast( + static_cast(stamp.irregularityWavelengthMicrometres) + /Owner->CoreVoxelSizeMicrons); + runtime.irregularitySeed=stamp.irregularitySeed; + runtime.quarterTurns=stamp.yawQuarterTurns; + StampRuntime.push_back(runtime); + } + StampSpatialOrder.resize(StampSpatialBounds.size()); + for(std::size_t i=0;i(i); + if(!StampSpatialOrder.empty())BuildStampSpatialNode(0,StampSpatialOrder.size()); + } + + std::vector BuildStampTileList( + const std::array& minimum, + const std::array& maximum) const + { + std::vector result; + if(StampSpatialBounds.empty())return result; + for(std::size_t axis=0;axis<3;++axis) + if(minimum[axis]>maximum[axis]) + { + result.resize(StampSpatialBounds.size()); + for(std::size_t i=0;i(i); + return result; + } + QueryStampSpatialNode(0,minimum,maximum,result); + return result; + } int BuildPassageNearestNode(std::size_t passageIndex,std::vector segments) { @@ -3343,6 +3514,7 @@ private: void BuildPassageSpatialIndices() { + BuildStampSpatialIndex(); PassageTileFeatures.clear();PassageFeatureBounds.clear(); PassageFeatureOrder.clear();PassageFeatureNodes.clear(); PassageFeatureBoundsFailOpen=false; @@ -4270,6 +4442,19 @@ public: const int32 Pad=std::max(Step,1); const int32 BMin=Origin.Z-Pad; const int32 BMax=Origin.Z+(Cells+1)*Step; + if(!StampSpatialBounds.empty()) + { + constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::array minimum={ + static_cast(Origin.X-Pad)*Q, + static_cast(Origin.Y-Pad)*Q, + static_cast(BMin)*Q}; + const std::array maximum={ + static_cast(Origin.X+(Cells+1)*Step)*Q, + static_cast(Origin.Y+(Cells+1)*Step)*Q, + static_cast(BMax)*Q}; + if(!BuildStampTileList(minimum,maximum).empty())return false; + } const float LayoutWidth=static_cast(GetResolvedLayoutChunkWidthVoxels()); const int32 FirstChunk=FMath::FloorToInt(static_cast(BMin)/LayoutWidth); const int32 LastChunk=FMath::FloorToInt(static_cast(BMax)/LayoutWidth); @@ -4360,6 +4545,20 @@ public: else ++Counts.MixedBoxes; return Result; }; + if(!StampSpatialBounds.empty()) + { + constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::array minimum={ + static_cast(BMin[0])*Q, + static_cast(BMin[1])*Q, + static_cast(BMin[2])*Q}; + const std::array maximum={ + static_cast(BMax[0])*Q, + static_cast(BMax[1])*Q, + static_cast(BMax[2])*Q}; + if(!BuildStampTileList(minimum,maximum).empty()) + return Finish(EVoxelTileClass::Mixed,true); + } if(ResolvedPassageIntersectsBox(BMin,BMax)) return Finish(EVoxelTileClass::Mixed,true); if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] @@ -5368,6 +5567,157 @@ private: } static float SmoothMaxDistance(float A,float B,float Width) {return -SmoothMinDistance(-A,-B,Width);} + static std::uint64_t StampMix64(std::uint64_t value) + { + value^=value>>30;value*=0xbf58476d1ce4e5b9ull; + value^=value>>27;value*=0x94d049bb133111ebull; + return value^(value>>31); + } + static float StampLatticeValue(std::uint64_t seed,std::int64_t x, + std::int64_t y,std::int64_t z) + { + std::uint64_t hash=StampMix64(seed^StampMix64(static_cast(x))); + hash=StampMix64(hash^StampMix64(static_cast(y))); + hash=StampMix64(hash^StampMix64(static_cast(z))); + const double unit=static_cast(hash>>11) + /static_cast((std::uint64_t{1}<<53)-1); + return static_cast(unit*2.0-1.0); + } + static float StampValueNoise(float x,float y,float z,float wavelength, + std::uint64_t seed) + { + if(!(wavelength>0.0f))return 0.0f; + const double sx=static_cast(x)/wavelength; + const double sy=static_cast(y)/wavelength; + const double sz=static_cast(z)/wavelength; + const std::int64_t ix=static_cast(std::floor(sx)); + const std::int64_t iy=static_cast(std::floor(sy)); + const std::int64_t iz=static_cast(std::floor(sz)); + const auto smooth=[](float t){return t*t*(3.0f-2.0f*t);}; + const float tx=smooth(static_cast(sx-ix)); + const float ty=smooth(static_cast(sy-iy)); + const float tz=smooth(static_cast(sz-iz)); + const auto lerp=[](float a,float b,float t){return a+(b-a)*t;}; + const float z0=lerp(lerp(StampLatticeValue(seed,ix,iy,iz), + StampLatticeValue(seed,ix+1,iy,iz),tx), + lerp(StampLatticeValue(seed,ix,iy+1,iz), + StampLatticeValue(seed,ix+1,iy+1,iz),tx),ty); + const float z1=lerp(lerp(StampLatticeValue(seed,ix,iy,iz+1), + StampLatticeValue(seed,ix+1,iy,iz+1),tx), + lerp(StampLatticeValue(seed,ix,iy+1,iz+1), + StampLatticeValue(seed,ix+1,iy+1,iz+1),tx),ty); + return lerp(z0,z1,tz); + } + static float StampEllipsoidSdf(float x,float y,float z, + const std::array& radius) + { + const float minimum=std::min({radius[0],radius[1],radius[2]}); + if(!(minimum>0.0f))return std::numeric_limits::infinity(); + const float k0=std::sqrt((x/radius[0])*(x/radius[0]) + +(y/radius[1])*(y/radius[1])+(z/radius[2])*(z/radius[2])); + const float k1=std::sqrt((x/(radius[0]*radius[0]))*(x/(radius[0]*radius[0])) + +(y/(radius[1]*radius[1]))*(y/(radius[1]*radius[1])) + +(z/(radius[2]*radius[2]))*(z/(radius[2]*radius[2]))); + return k1>1.0e-8f?k0*(k0-1.0f)/k1:-minimum; + } + static float StampRoundedBoxSdf(float x,float y,float z, + const std::array& halfSize,float rounding) + { + const float qx=std::abs(x)-halfSize[0]+rounding; + const float qy=std::abs(y)-halfSize[1]+rounding; + const float qz=std::abs(z)-halfSize[2]+rounding; + const float outside=std::sqrt(std::max(qx,0.0f)*std::max(qx,0.0f) + +std::max(qy,0.0f)*std::max(qy,0.0f) + +std::max(qz,0.0f)*std::max(qz,0.0f)); + return outside+std::min(std::max({qx,qy,qz}),0.0f)-rounding; + } + static float StampPillarSdf(float x,float y,float z, + const std::array& halfSize) + { + const float radial=(std::sqrt((x/halfSize[0])*(x/halfSize[0]) + +(y/halfSize[1])*(y/halfSize[1]))-1.0f) + *std::min(halfSize[0],halfSize[1]); + const float vertical=std::abs(z)-halfSize[2]; + const float outside=std::sqrt(std::max(radial,0.0f)*std::max(radial,0.0f) + +std::max(vertical,0.0f)*std::max(vertical,0.0f)); + return outside+std::min(std::max(radial,vertical),0.0f); + } + static float StampArchSdf(float x,float y,float z, + const std::array& halfSize) + { + const float round=std::min(0.25f,std::min(halfSize[0],halfSize[1])*0.08f); + const float lower=StampRoundedBoxSdf(x,y,z+halfSize[2]*0.5f, + {halfSize[0],halfSize[1],halfSize[2]*0.5f},round); + const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z); + return SmoothMinDistance(lower,top,round*2.0f); + } + float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const + { + float localX=x-stamp.positionVoxels[0]; + float localY=y-stamp.positionVoxels[1]; + const float localZ=z-stamp.positionVoxels[2]; + switch(stamp.quarterTurns&3u) + { + case 1:{const float oldX=localX;localX=localY;localY=-oldX;break;} + case 2:localX=-localX;localY=-localY;break; + case 3:{const float oldX=localX;localX=-localY;localY=oldX;break;} + default:break; + } + float distance=0.0f; + switch(stamp.record->kind) + { + case VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid: + distance=StampEllipsoidSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break; + case VoxelForgeCore::ResolvedSdfStampKindV1::carveArch: + distance=StampArchSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break; + case VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar: + distance=StampPillarSdf(localX,localY,localZ,stamp.halfExtentsVoxels);break; + case VoxelForgeCore::ResolvedSdfStampKindV1::fillDais: + distance=StampRoundedBoxSdf(localX,localY,localZ,stamp.halfExtentsVoxels, + std::min(0.35f,std::min({stamp.halfExtentsVoxels[0], + stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]})*0.15f));break; + default:return std::numeric_limits::infinity(); + } + if(stamp.irregularityAmplitudeVoxels>0.0f) + distance+=StampValueNoise(localX,localY,localZ, + stamp.irregularityWavelengthVoxels,stamp.irregularitySeed) + *stamp.irregularityAmplitudeVoxels; + return distance; + } + float ApplyResolvedSdfStamps(float density,float X,float Y,float Z) const + { + if(StampRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density; + const auto* tileQueries=ActivePassageTileQueries; + const bool scoped=tileQueries&&tileQueries->matches(*this); + if(scoped&&!tileQueries->hasStampCandidates())return density; + const auto* bound=FindBoundStratumForManagerZ(Z); + if(!bound)return density; + const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons; + const std::array worldMicrometres={X*micronsPerVoxel, + Y*micronsPerVoxel,Z*micronsPerVoxel}; + float composed=density; + const auto apply=[&](std::uint32_t index) + { + if(index>=StampRuntime.size())return; + const auto& runtime=StampRuntime[index];const auto& record=*runtime.record; + if(record.strateIndex!=bound->StrateIndex)return; + for(std::size_t axis=0;axis<3;++axis) + if(worldMicrometres[axis]record.boundsMaximumMicrometres[axis])return; + const float distance=StampSdf(runtime,X,Y,Z); + if(record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid + ||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch) + composed=SmoothMaxDistance(composed,-distance,runtime.smoothWidthVoxels); + else + composed=SmoothMinDistance(composed,distance,runtime.smoothWidthVoxels); + }; + if(scoped) + for(const auto index:tileQueries->stampCandidates())apply(index); + else + for(std::size_t index=0;index(index)); + return composed; + } static float SmoothUnit(float V) { V=std::clamp(V,0.0f,1.0f); @@ -8614,6 +8964,10 @@ private: FBuildStats Stats; bool bAnalyticOnly=false; inline static thread_local const FScopedPassageTileQueries* ActivePassageTileQueries=nullptr; + std::vector StampSpatialBounds; + std::vector StampSpatialNodes; + std::vector StampSpatialOrder; + std::vector StampRuntime; std::vector PassageFeatureBounds; std::vector PassageTileFeatures; std::vector PassageFeatureOrder; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index c9408c6..a7d1e31 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1230,6 +1230,31 @@ namespace VoxelForgeCore } return false; } + + bool ResolvedStampTouchesBox(const ResolvedLayoutSnapshotV3& layout, + const FIntVector& minimum,const FIntVector& maximum, + std::uint32_t coreVoxelSizeMicrons) + { + if(layout.stamps.empty()||coreVoxelSizeMicrons==0)return false; + const double tileMinimum[3]={static_cast(minimum.X), + static_cast(minimum.Y),static_cast(minimum.Z)}; + const double tileMaximum[3]={static_cast(maximum.X), + static_cast(maximum.Y),static_cast(maximum.Z)}; + for(const auto& stamp:layout.stamps) + { + bool overlap=true; + for(std::size_t axis=0;axis<3;++axis) + { + const double low=static_cast(stamp.boundsMinimumMicrometres[axis]) + /coreVoxelSizeMicrons; + const double high=static_cast(stamp.boundsMaximumMicrometres[axis]) + /coreVoxelSizeMicrons; + overlap=overlap&&high>=tileMinimum[axis]&&low<=tileMaximum[axis]; + } + if(overlap)return true; + } + return false; + } } bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error) @@ -1312,6 +1337,8 @@ namespace VoxelForgeCore if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels, subMinimum,subMaximum))return false; if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false; + if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax, + impl_->owner->CoreVoxelSizeMicrons))return false; if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax, impl_->owner->CoreVoxelSizeMicrons))return false; if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false; diff --git a/Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md new file mode 100644 index 0000000..9560820 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md @@ -0,0 +1,72 @@ +# Linux 43 — Core landmark SDF stamps + +Source stamp: `18b89fb48febf887c5b61e14c699c95535b09613f23618ca57271bc5d2a455f3` (clean Clang and GCC builds). + +## Vocabulary and records + +Stamp vocabulary v1 contains `carveEllipsoid`, `carveArch`, `fillPillar`, and `fillDais`. A resolved record carries a stable ID, strate index, position in integer micrometres, conservative bounds, half extents, smooth-CSG width, irregularity amplitude and wavelength, irregularity seed, and quarter-turn yaw. Bounds include the smoothing and roughness support. + +Carves use smooth maximum with the negated SDF; fills use smooth minimum. Core keeps its density sign: negative is solid and positive is air. Bounded trilinear value noise adds seeded relief. + +## Placement and culling + +The resolver sorts requests by stable ID and derives placement offsets and irregularity from `(seed, strate, stable ID)`. It tries at most 512 deterministic candidates. The strict layout rejects bounds that intersect a passage mouth or walk corridor, touch a content face, or overlap a ceiling or bedrock band. No safe candidate is a placement error. The route and band negative controls were both rejected (`2/2`). + +The stamp BVH queries conservative bounds once per tile. A tile with no candidates performs no stamp SDF evaluation. Stamp-intersecting boxes classify as mixed, and native-only readiness cannot skip them. With no requests, stamp fields are omitted from canonical JSON, preserving L42 identities and hashes. + +## Owner demo and hashes + +`--landmark-chamber-demo` is off by default. On seed 0 it adds three records to the owner SurfaceWorld strate (strate 1); no Unreal files changed: + +- Ellipsoid chamber `landmark/sw-arrival-chamber-v1`: center `(-8.918, 31.950, -513.083) m`, half extents `(8.5, 7.5, 6.0) m`. +- Arch `landmark/sw-arrival-chamber-entrance-v1`: center `(-12.289, 37.027, -513.311) m`, half extents `(2.6, 4.2, 3.2) m`. +- Low dais `landmark/sw-arrival-chamber-dais-v1`: center `(-7.489, 30.688, -515.838) m`, half extents `(3.2, 2.8, 0.9) m`. + +The chamber center is 30.9 m from the arrival mouth at `(9.25, 9.25, -502.608) m`, off the walk route. The active layout carries vocabulary version 1 and identity `resolved-layout-v3-fnv64:91eaf2f0248f2c8b`. + +Without stamps, the FI/SW layout IDs remain `b7a7d9ecc48d8f26` / `8a52e84c26ed92d2`; the 12-record owner signatures remain L42's FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58` and SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`. + +At the same chamber-centered camera, W12 hashed 343 tiles. Stamp off: density `0x18d6f6cb3498deff`, mesh `0x293c0ddff66b99f9`, payload `0xeb07403f34d60134`, topology `0x002905a3f028c9f1`. Stamp on: density `0xbf8af855ee5db8fb`, mesh `0xbb64c203dc533070`, payload `0xd14d45e6b9faa9c0`, topology `0x44f1b422df20a159`. The stamp-on W1 and W12 signatures match. + +## Safety, cost, and tiers + +With the demo on, passage walkability passed `1545/1545`; the passage audit passed; endpoint probes passed `27/27`; the interface audit passed with `hole_columns=0` and `air_samples=0`. + +For outside-stamp tiles, five W1 pairs over 343 tiles measured median whole-view-ready `0.457 s` off / `0.466 s` on (`+1.97%`); paired changes ranged from `−2.76%` to `+3.08%`. Five W12 pairs over 125 tiles measured `0.189 s` / `0.191 s` (`+1.06%`). These tile bounds do not intersect a stamp, so the BVH returns no stamp candidates and no stamp SDF runs. The small timing deltas sit within pair-to-pair variation. Full-tier five-pair cost versus `HEAD`: FI `17.342 s` → `17.495 s` (`+0.88%`); SW `1.799 s` → `1.805 s` (`+0.33%`). + +| Tier | Result | +|---|---| +| Fast | PASS, 161.3 s, 41 checks | +| Medium | PASS, 468.3 s | +| Full | PASS, 1551.2 s, 539 checks; clean Clang/GCC build, 30-cell determinism, cost pairs | + +## Captures + +All JPEGs passed `capture_quality.py` and are below 300 KB: + +- [Entrance, eye height — 27 KB](captures/chamber-entrance.jpg) +- [Inside — 29 KB](captures/chamber-inside.jpg) +- [Overview — 43 KB](captures/chamber-overview.jpg) + +## Failed runs + +- An early route-control point hit the mouth guard before the corridor guard; its position was moved so the final route and band controls exercise the intended rejection checks. +- The first JSON round trip lost precision in a 64-bit irregularity seed. Seeds are now kept within the JSON number type's exact 53-bit integer range; canonical round-trip passes. +- A clip-plane capture was rejected by the renderer (`open_contours=10`); the final three views use the uncut mesh. +- A supplemental stamp-off, chamber-centered W1/W12 aggregate differed reproducibly (`density 0x68f495ba62aace05` / `0x18d6f6cb3498deff`), though its nine exact-query probes matched. The standard owner fast/full determinism checks pass, and the stamp-on chamber-centered aggregate matches W1/W12. + +## Orchestrator review (Claude) + +Merged as **infrastructure, off by default**: the stamp vocabulary, records, placement guards and BVH culling are +sound, the owner hashes are unchanged with no requests, and the route/band controls reject. Not accepted as a +**player-visible** landmark yet: +- the demo chamber is **buried**: its top is about 4.5 m below the SW ground near the arrival mouth, with no opening + to the walkable surface, so a player can never see it; +- the arch reads as a boxy notch with a sharp vertical corner; +- the entrance capture is unreadable. + +**Determinism finding (pre-existing, not caused by L43).** The "supplemental stamp-off W1/W12 mismatch" in the +failed-runs list is real. I reproduced it on committed L42 and on L37 (`density 0x68f495ba…` at W1 vs +`0x18d6f6cb…` at W12, camera `(-8.918, 31.950, -513.083)`, `--max-clip-level=0 --render-distance-chunks=0`). S14 is +stable there. It is a worker-count-dependent result in the production path, invisible to our tiers, which only test +the spawn camera and the 24 m crossing. It is the top priority for L44. diff --git a/Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-entrance.jpg b/Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-entrance.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4213b30a0d200385e16563099d7a0a0e650cbd89 GIT binary patch literal 26913 zcmeHudt6l2+W(#bbedAZ;ArM$nzF`GG0QX+btq92Q&2=j(bN?0wV98&+qcb@8jOO_gZVO^{i(- z-{-dWZ0i50e=0Z*oBa9Ip50x$_jc|6c6Xt^Q20<#C=@R!UU-om&0cEOa5Qh;+{wua z*JjO{H+OV&a%%Z30Z5ITYy`9{_5iNEO4ow{F>>T)GJGsp19}f0D`Q2Tj^W0zCFlznxlRx#o(sF*2 zm%megapr^9ckOs*%19?nBiPw1F_%bGF8l6f@9sP7TArEw#mHT^-JM>$WIz9dDetI% zT~Pl|!AW8FWfPs9tfe4tDcH3X>MsZ{flaxSos%#?`199Wi)}c2UVJWr=Ms1>f#(u< zE`jF~crJnG5_m3w=Ms1>f#(uf#(u3sH=S|+`sQZA<8z89?76+T7aI3dV;O5a_GGfF*wzL_*D%y%|%VyQYSFc&0@L5tp@s1Fx;8y&S=Tim_FS|lsb6CATJQ4t zIo~C&>TvB^*6q};UC!s_?YqCX$F#u-M_mVHv<}_8>A`_N_9O>*4z0d$@=Qegw6^JO z$|gUZH@oD;DS5eDT@#*ksd$CasBBClIkOZrnh$=4#-EHv@Aft{x^@or>^sP3>BY}V z$ESTcGkw?nwOuwQp9^|-PwCMStz3HhRa|N_Z_e`Q@o&z#S2QfQ_x$|Bhh2R$#}3-O zxxU*cq&kIfw0pUD0}WJhtTeIs2@wqR7dU$asjUV3a&+Alwh z%3HEvx9^^`dLi9BFlv8f@bQ*QVv=fO5ARy{aQ)ehvETQ3e{F8t&dtwE>!+JD1b0bJG{oW51ewp-b$J7h}>_Z}Trda=uON)R<2$1@ySRv#-a7n%5Sky8=E@I_Z6V4v^rQ#o>PJfkm@!%=f zdspVHUA8Olm(SuiJ@kFmd}66%#`qtWlos!s`(09GAK&KA3j>q~7mXY7K`YONEqY|0 zTf6(cyGMuqdSKq1OJnD)=~VVX*}Tcuqn6%D9khS!jj`{4oV%}cGsb$e-`w)R`Z31( z3%%36yp%z#?`XvOTRm@Y>BU&@^>($VmGw8SRldBcj96cFt$73MUrs1G@LAD6Mjbp* z7C8A@!=xU$7vI99Zf?(Zzw+iQs}F7(7jwLH+9toenbRZf`}q3h?~jO?*>z7s=Zu6~ zZSx9HnwQU;VqK z9)$jilF4l_nvgB;4}W#XiN#DZ)eVwKXWe7DAp2WwtWo#7Ri2_ruDo~&dflAq^(QvQ z)bC{5x0;$bV77Cb5Vb=pZRcHUSZCiYdv8!@f8KfS zl-zBZ@jEugy7pPT_p6+9lQpz4BW}fSqH~0`a}&mLYsT_) z#`0?mD$f17hq3G?vP@iT$qc$Zmss8v-s@|k%sQ(VRQ$m^A8FJ%{qwiQ&U}50Em8&@w$&qjP6@49S{6k9j1cb(qu+O;Q$&%@I1=B3pn??HS%GS4%8Z}8^L zh|ekST+bO=l1K45yWhGV3;c5r?z>pB{~w2cj_U8{`!w?NZ!Guro;g0ISM%zHD;|#j z$4?V#XO4d~?!r6K1y{y&oN%#kZtmktbNn_gER6}g5isLK`1rLGA{PEq)vH(4SG#6S zI~5s`9kVrB+i%``3qC&iH>A%W@6{KiCo>FDPJ0}Gf zo|$-W!PDUH9&CE0x?V`E7h+q@Nz5t<^awedQ(HW^ct*gfrubKT%$Ezb0Vc#ZyFe6{mWbQ{7g*u>W7F4hlK^+h3@*tL&Z~Fuh~) z!-a3hmehv6k^EuujesjimkKkEtX5tblQ0SCQs7rBB5SUv__zHq_r&ANbNnwZjEnXYUTK^HFLq0xp$n9d%{x=VwKia3yq;2PV zP53#e-?)ligB`0E?0>lVt$?;=H&%p9*!oV)@Am@s6p5s_ZegSsGSb^l^{R0DVBBoK z5mB4=#LWI^rrZ78+p(q1zb~A$cIJpR$&rheUM!;Vt2bxvWeNRFmb-6ex%*(L(Key~ z-))Ro^Tp-Qrq%s);*_6nYUs3SYbMR<{ZqpBSGz1)ocKn!bM->|_URobhC5f@zW8+g zyVwVXEV;HWWz7lN-{b`^o)`Mj#+oUMvfQeV`W+kKq z{MS1;^pG99*z~3RYIQ~X$x-htt&Cl@Zb(_powib26}7G2#@g&&%&&fZZu@BsZMW+h zwe@2CZvXO4|Ch4ANgq49D(!LU)1mdk*zFN>9*&=SENepPiqyLit;-T)n;*OJPVqPW zqk_se`4xBTl;h{Cym9;Ds`XvV4&Df!n2vCLcS}mlCy7zbj?H8pm#~g+A?a)Cx$0_< zje&zYc<#HG+cmJ&dZci6NiTMa{GwN>(!TXV-$>uxAAaw5z9!XgWNL8V7lVUSD^K5# ztBLV;#+O(w9!@$Z_kmvaUf#|!XPJvP741j6d-(_gs$Nc`rHiAF%vx?}aWvVG`?SAq)4?ydRx+v<1wmxa8Q+~>MyJa+Tj z{DU(3O~3i6+i#~b+8@7lGCl7mw*J?n{O5)Hta&`5>$uR%;cY%jDCzgc*Pi_&J%`GCpN~Qo+xMbF?XomQFiUdvb7b$madQ*oOaE8gVZD?2V|w*Lx2LsvKThH2&$9 zws$ig^vUX3R(Jia%BynxA=kUSh~^IU(jb+v@VS zA|&rT`AYEbl@E;;t=SMJ*DFm*K_i$oTJ_2yMy1*iX0q^`f)(`Cq7al8w1%0?a9tX? z)}%Jj0O=yv@KZruXmpq&PLPW|G)6&S6wMk9BS8bHTQjw$2Pih07;U_h){I%>jCiip zhYvNXEP{oqHJ0cTae`LR5)X0w(obu5LM&Pvsf+C?N2i=TlUtTnaks`|xeHR_KfMuz z`9-OxQ>?2+T5>*581*3@3~XAtNo+7^O>#2`D8u9^2o{w|p;c()g5IQtP!vYFTCdh) zDX1!p0#$j1p0O%52xt}*kP@UT=Tg<9$PP)2qO?JcQ$%NpfO_RsjX}iGq}3QLW=Jp2 zNQ~lcFq3dB#J^T+QVFbV0O;aEwMxAp7s+zs{EGG96gaZPiX4mz0{&|RLCLCAmUM;C zm7AHiXt8n)F^4Kn3sz4jX6Kau%c}u`Y=DE8XqAmlyHxz*d&AvK@{!a8l)+7yR){lV zIyH?`fHfjwQdvwI&R*@Txm3MK|$th9s+^Z*hqNfU>Hq{ zidh%zi4=#sRZt?gq_!x`S_Y6sGG%7XKnx9e5_D#5^pHl+ZO0I#%zrX32k*jILXW@x z6PcB4UBPC-MAmV_Z94} zX$%%bJ$Nw_zrmR)F)?~E8{{A`(-Vwpo7LH_l*%TnHe)gvDmzh9hrygRMwJ|XKxxFZ zW*9k4iYinjrND3sf&5PP39ToW38}*;P>lsi(hYV>iX_g$=VHO7k;F+D(4vMD(GElv zjbkBMfr6qLoeW(jN)jYYhz5s22TcmI3Oa3|`3)>uDM;i0QdPszs^atpvmjT(^7R&# zRx3B@Nq}Ig(W6dO5NnrKqpODR+_lE*G%!f&4hV%q7XCoENhPm}>VG1e>V7eQpki#+sz0u5(q=-l% zj{}Rz;9o^As#VY+JtU`s)#>FbrJ&GjL_-xQFfvs&2ujG}um4mgu%V!Yf%OX`rzR(6 z^@3c!@|tGl5aG0BgC+%p0)vr6skg`}l2KG*bHF6z2%Q{ZW(1&Wb>n=E&{H*Ypk&Gr zmK?$~ApRPmmJ|Wur!fJk2i&$rqq%D`!31HhQk>)Mg<2BJ?CTWD!{P+OJBa#x%ICdN)6iZucwJhos@~9TMMwFfzgh>-MEXXriniw$>6M+o}!7tDTplf?7AqWNx z$%%(kW=Kk-AZRsUL?fYy$VrH6jB0YQTzQlhSg|2aL-L_{k(`j`pqiu&V{X+f@0@=O z7G#iac~~~-CcEsjHlq{NM!Ng8l4S|Z>23`uG z4;G~W0lt-B%BH5dX-F7S5v0XvKT2La-LuZ@; zew(=hA}nqGNOiQ>079(Hry5KKCp<7BBGJh6oB7b%}wN$O*Smt^DABo#CBY_xI5q8W_b z)U-4s;ep)|K}SIlU`fatv>FxA#ik#6L^@163%SOihRQ&edd8!epK{wFCri1gmPaUb zpiXv|jh3D#G(9P`z$+No;t(NESgRtB2KAOByMQ&4NWd$+I~buY7f$Bz1QHC1h|MRc zXq8+hqI#=In;~^M)Y*)^1>_EK7=aCq5Wz#M1citnpBixz8*b1evnQ)W7)M74SHmTV z)Q4LlgIvmAkje16&6TEtVB~C9fS5}wVkfbIXa{yt;Lb|Hj1I~mV$aeV#yi2w2FT5z z2N(q&5_y1wC=3vc0$NP$^0zDmA{v1a)YnAYGrsjlwkraQSZOjcXvvpiR?HyhL6GT( z%|(SZK-u68Oa?(th?*6K=s)|(d+04lXG9FytF1g96)6v4(T?FF$`Nwl=^j{uMUfnA zCK0qs5;&b%zWu_)Uq2b9awTl0k0s=;L0{87M8jY zz;f(4$+b%E)JW#Z#D;)D+9!YnK$lLH67)&<3ei}?>=MI2Xht#cG$EQNj~+syhgqhQ zUVVyK0zFBn+_zyClL9Uf*2U6z<`zMm5QEtQF?a|;1D6iC$)*(Cs(8u^egG^UF)QPO z1yW>bOi!>gRT|*ZATW){DPjg(nqJM~wCD;LO4tj5l3>C6T@iLsv*G|8J)l#?0VO#o zl(-OO@M;wU9F+l0)Gk0Vj>WBg=Pv#mE%}-T+`%Wd=j4M8STum*IUS=;n-;b~#fqT9 z2G}06WxidZu&2j#v^VEji`r~4z(c9fz_f>_qhz*F4#pZJX4GRGUxA4%lq1UD*r6+V z?{yj4eQVX4o?=e(`Vb{~QY?gAs~Uj_nW|(FkS>A6VkHI(IJn_4qk~lB)TX3aV2)rH zl7Nnx;K>?*k~e@v_M(Q-a&Z#V#H^fRDKb*>nUE8Mor{)xahfws&ZMnU8%Tj97fU0^ z9=Or4W-=@|99SyAQDFjRM9>~dgGuN#6*E#22vbL+9xj$xmlLM?2Aru)+gTmgBj-dM zR1E^5$(i{YAZ#X58V0)(kO%-v1`K z3~V<@cf19ai>AYcNhZS*9T&OrUg?lb4_sgW_(IH%^5~CBO)fo)f4iM9}qS>A6L+ zH)6qz7oZ|z;&4s`98=1|yRa)cV6@V~0veu^)M@>3TDLg#!Qzoos-UNwAuLs|P|&u| zgb;_QBj`04LT~I;%$kT6S2*T;m9X6~YUA1^M!`NGSTumD2lSWSDD`B~4o%Heiaczk zA=i~dz+;W|OeBaCu@{g;l^jvVkbYA~X}HJIw?GL3@m5Mt9CX zHUnJI^x(w`A%!~{Y@}9NKMihlp~*xEV9h+NDLB8lGP9tRg22Ivw`j0gJ@y7_6P}T? zBk?d=;`79(Eu;;6Tca6W)cpO|gZAA&kLl8Kks5AOifPjGsb^iMH)fLQaFOhLyw2 zh^@3HFbD?pW+WvT#U_D3l+D07^c4w?Rcc~MP$8RyK*;@a3{?n6aD|ddap)j@VW++a zL(djJ$=4bixPi=U+2%f&`D{uHkulK0mY|@8sS$1=Uid{)P?#7;gyTkwM2KS{4??nn z#sV0@Zh*9qTr#j}EIDio=qTd)=;#jpl`Kiz)iG1UykbJhLK$3eG~@?FM15x^Eiwtp zC#+J${7exsnoI?q`Pt)`-7x)Hw#OyC75JGaJy#On5;}wrN0${e)ENdZ>I#!03$APi zC=JAi>Bt($KjFCu6wxDfRPi8X*=aB%4YkQI*y|M_2G%4#Nx(R``ZR1Qu#IFaq90xo zu`Ybg6sI^ov>P|UQgEseu@Kr|UGRADdV(7Ar)K(%0`Fjyl- zlq5O!Kn*cUDJBy!TOW68l5m)xV`hWdi3GzBmV-$ernVb`0Ei6DS0GWgeD|h`9)DbWT4CGaZ_IE5nV~y}V|k*@0E?9hBhW;2?Ogx{Kf};Bzo1 zcOBYwPF^y0r!t*QzV&u-8jbI+*xBY&*-tv#i`UEzUNgUKvQfb0TYQ(rPDh`HD!RM7 zd*f>}Ob%kK!;koQ3v9`ne2Y)qh(A326s+P4FLwIs_-yv5IXSyHdSNKO+7hVlNFRSW z+k4PgU;=BwH*r{r4?gf>Yv)6q1;JsUmyd^olTN_Wyq%qOg3OB=AR^4d>!78hQ38GP z=H%_pF80(Q^K$SaVdC>Nn$U$lAj79+g5ZcId{E|$&($31`!aO07i3_FzGZV3WP-=Q z(dZ>}5Okn2a7yy>rZ3!C{(88synXw<@A>^v+h=G{i{ftroz@=zdG>hk%2DZ0_bguW z&8%)Z;o;gVt!Zu*{^RANhU|n?UTb4;WM|k+a~on7gF1AaHsqx07(yr6oI#E^7NR}b z+7^`!2C*aWj(LXZ$7aL3vHA;V1OWvQ8K4yp`jXTgijT~o?Zo)goPh-t_cVN1-jzvr9u8i7JN{g0M!|+<{xN;lUFSEwx@42g_`DbaW@SEJI%j+h>aHUy`0x`RV4#!E(Vjh&c{%#%MuU=UQ|{0fEiVu1 zpS7Xik(N(tu+;d|7y24xcCI?uCj_hQXI5m1^?}b)~n?2dS}g(!LQ7Z9^Zf2%|1uJ z>Rfm`|H`=dG1Fg|-?3lX`Le6$TMz2neR=2iG!Iie*C@-MEZX%hdfj}Ik}@vd*T3W8 z4H@x67cLBM-n~4Hm}`7AJ+xdF#+w{PR-GXQpyV|Z!4H9tTO}DGxOd_!pg=iL=An~O ztE0?Ygq6|3;u=rkj*e`_xO!k+1UMhS>==vp@#5ynVSLsqy}e!V?Qz*WZ>mBA$~D-6#&~WVF12Lkl((zmXU{@xh7%XZ`EOoY+5V;G@oN+3NVZx_iZkg2S8C z3sDEl%6(H3AB1EiWVhJfY3TUCq9a*VQ-cOY@6OupcPKU>F3#gXWN(d9x?a+Qa86F@3q@dLwx+*@i5%f9qssDs^KXDfJsApPbAbR^ITMp78V^5 zOep*rG-e}qFej-vJ&Cxp|o2;P_*tg z1eWrPG1VgMM4W!D1#AtSzjsTEA?v}}^kNXX_92j}tzghp6J(2YT5fl) zU3Ra3N_xwV%ro=GbbgTeU|?L_`P(KNUjABa5%$Q=QDuJS0eAY!3NiIDp;5dc7ZS%Y*1 zNe{OF7(|jJ5paodxk^$&vm7t;*gHJ4fqJ7q-j*rktk`g|g@e>ZIvXIAdSEJR+M>59 ziL^vL65-RMm4nBU7da~QlfOlQOa|Ewat&jjfaDe#@Qi4qt+28*v1W_li_>vbGkk1vv$y_kgmj;`eEJ%{%F22n?YKGgyUz8( z0GF(-N8H*ZWcP3W&$*cPUmjk|nx1a0v+X7mMx*Wn-$S0(TjwPpFJ^9tYW7}mB;H>5 z8xKkY*g$lkltzlRaG;bN2oCNZpy7_>!9@lKNq-UJ0Ix;SpNCZRaYW3eU@Ce>P_#Wk z9sbyQf?7k`WStF<$xRZsbcpWVS%y0P289^cIr!KkeZgPhIN*;;JlOOew6$>Ya`g6a zgwq7ehyfVQH%%gK52Sb!aXho9zgeLv!1VOwk^c97S+XIzU7An4clw;9pk;4{51kj2 zG%T-fQ&RQBCn23rD$cZbC@#FeyhwX^TUP3dt-0N{y7p>0aeqltca>5;}PU>@Q{|-d`4frN_)pcgDQ1V&Ln^PqY2|DJt6AR|OuexWDLH_#G4 zvc3xkM{drn7w+WGjkx{bzWOGB`ipx)jC;V5Lb zc8UZ}hX;S5&|sgEW8kyNzBYSyn{nN>%j>C+J6}IF<3{_orMq`MoLjJ|UufZon`MUw z9M8Tou_!q0r0ZPox}{IYtlV?w-lmypXH)J{0#o#G@~()Qsfwf1M)zIl5jn9ibnr(R z6IQfczPw(TG+@M*qwQZRy;1S_;k@PL?PpcSbgCDURu0R4bIk6zw30#B0u)c(X3s6E z4ED<%msarm(hV_P(>}i&(PrI=IFj`L;Nb369AJa1&MH$tM4&i=FmilsIGs_0$K(4m zf(u>wtW3C|2(X;-2aL!DITgHJ(KX)>(4UQ*KgERqs&YWJP}UdNL0O*anhUKsSj@AJlMD`F4q`uV))#EsKx zN7v0Rd2r|Yf*ynKR24kQ9+4S4Hu+rYsWU%UPdj?q^IYM*JNHkPw0C~}M%ClmN1w;L z?XHS9m$<(1T55T{@L6mQ`P+y<(=FctdE3vPn-f(UJT=^7(XQP`?>s)Yauhk-@~3@U zFWTO6Vd3&^>jIrF9qHRS?d+{aa`?9@v=w{iX!_6PvMWmh zN+)=G3kaUr{bNsN?~FfCrGApFgB4MuY@)eN)-}ey!O>nHyF34KV#~U)!2wTJWGr>M z;PygsNq8Z)99_;9U99|dz_Bf;VNaDeJ_>2LTs3rGTG%&1R#KUTlepG5bDM3~E;DRFHCE>dOzKpchNxjfYTP zk;-B?WQ|g+#QZq$V&6hR6~7j$@D4lo9o-+7OdF{mJ2V=*@``??pEPeQ>!r&FH=S*AP`at*dqlf2C@f;Ce zb~DX0{9t%ecKP)i)jwSMbxT&i?9gVX5~iF>PQN=d>gtL6qkSKjUHt7*hofufW@eo% z$2R3=_{a@0sg=beqrK0M9?JRp53YYFSBVTD4g055f%H_o%r`Vj{4RksiIOBW`?QtB zOP|J2Ck8A1+6EADnAjEujr_+e`k>E5-S`1%T2HBs?h?ExDs0=ue1Ynt!w`oH~(D>3rU-m2t zt#q!PGylt&-3c`vW*vSs>e2qL-`6GHnvYzz=h&wMmfoH<|XU2%Hdspl~HMm#U z?JMmf3UA(**7Bd+|0}b!p@sbS8?g$I^sLCP5QGW83YxHggouoKE&URzXYU;7xr8Z@ zhWP?i3qF;Ei6A4!yKIAK!(w_V0>FsdUKF;j0T1-nhmAB)N?IiZrO62`K7?4L?JGG7 z`se~bQ}K5m?~eO<_?aUU=Cuutf*q9wB|2B-7e2UMSUPt5(7xSs?uP6-8eMTbF1y3{ zgRwX61(m-y`@$bUa(>6Utn5i0<|N!Z@YT|(cOMt*iQf6BB`L%9POG99oF5bT<73~B z1I8TQKWV-4d{x)8**m^i=k~(Hl3B%n6a{SB+1E4tP*hS{`Kin`8y3GE9-CctD%tX? z&bqw+s6g*5|D6L_(ZEkwNA%ZGLB?tbNX|Nz@$y`VA{pWql3p>DWWA6nA#6E&>)feI zedzt@vol+x8FhN*kOC?-+XPV5zyUuRnt3?2>BqApbd%lPd2f2=&BOiOZdS%$$VzD&>jp!E2t=FPwN1-fk9LxHzQ$25$K_jQ3OU1Fad&+gFWad852oB z+eV>YzAzMp5U@6Ws5^{!;zV@~E zDRlbP^k+=mV85HqR>h5qDjOAlQpBbku3Mh*gOGrp8Teftmw6qM3wsyJzyM~cSBup57XBLu>i16GwqEL>OnPLnh zi3kgcijy#sFiBW+p4c2Aq8dqYuN>9B6e` zqP@9Y`P-)Rm;AZctUpLlpaGJw*xWD8lY|yCul-r6eSZIK%Az;I*3LhC_)S7l<4HVsPDlC^od>gG;gC;GOY)|=(lV^fn`X7?i>JCwlQk3&vdh9)^5L-&l^6I~8 zfAxLuvw=31@jGhze)k@@*u8%HvBV-dW!a~`tMlJadDw4$+Z=2wDLd6Lc2)MNR_Stj z^iY_}<$9Zsa=TP>l`=HLvJPd|6*A9kzNu5u8(rv9u`J(oFy@h6qpX{j{Mm9P{lUsr zIreTodo9}Y_kBKgcJA}e>|axFL{%K^QoMSiaPwf;;?FAkLm%{dw?>)G)AYQO_MU4~ zJ&kSaKj@y6zt8XE>(|32=B@5EHwLsmhx`-}T&`07?3Xv5p@FTJCi5L%7<>rMc{U`M z(HQuqRH*o%4Ud_Jjidr5y&dwBe+ipF@# z%Hy%Y9d|4I0;-iRYR!?m*)dY@>$f;wI_-#qjpEUNVuBmwC#=WI3hFJb3=>bJhjg|@ zO6l+Zy{z?;<%UN#;U;Zn4JldbvZ{I43yiK8Ev-y^a3oy6A~5>XuvhCddVhSYxlCN9 zN`d7xmSGn+jXiJjRmrqe`K)`Uo>*k3JC9S-)eV!ZWH3w}ocO;&8Z(aL%ZJ|R)o|5#cA4cBO z(q07)`N}}#&7xw-3Xl~147XVW85$?9n{*Gl-;D*yjn z%SriP>+wv}cAQZ&J-pvg*1=~m#QJk^o8psqHFSHkwdIL>>@SV9T`J$Ri`!`DF`~K8 zWQ(6pMN~?+j*gekG2ic^!|UF>pQuJ}mzPp+(PFgDX5jFXw9GS00{VR3|GsPTrRPw4 z(U7}r$533Om2SHCu%`MD}Zu?dy=GDY2^dE3g}?adwA#`Wyws$3E*x`vx~8Ac2pZP^{% z^;l_V{;OME&gHK^uFp0~E0qc;Jfozjn7Pc}qwh(Vfu-8*pC(>9r8MNFI>+2-=`qV_ zHk;dN+B{j9c{g(lt#%swF38vK>e*#2`7b`z%J+@iXKY3gYurCsG-UhvVej!-2Pr4QD=1)~W&GedbV<2-r| zdi0o&&y=f~NLhOIM{e!QlR4W}|9@T zgdEn>wqz`)cPRO@-@AjYZT-O=S^8dq)sLV3)y+!zEiJ4@HJTL=Yc)SXKA$^YHsHBc z#r6y{QfXV6fq}uUL|Ikzvx;VYXeuyVxpvA|KKkm@K%d%MIcXc*GSuy*Gq(%|!jckK zP%u(Z&{p{4K#SRhfN2bDp&Il(&*YZnwcipYZpRIkN-Kutq(>Lhh8&gDRhxHAI4@Ru zq+w+-ENiXhyJ|~!bX%B5;qPU(iz$kV%7KAh(b3&!FIJ`QK4Z@6vNyEb;Ik>tt~t1% zXm0T*-$MZ@qcoqET|?G(TC)4Q71XcgkNvg6O8szwO$Nne*;Rd+zuk_<^p%^O^nRxq zVO7x(FRh;Y!B=)!zu9t+iQEMPu)L8Y$_}=)$m$sS9_Un^ld!D72uDABOS8lGPIO?EzMt1!3;mu{GCeqWGu9=Qz zXZxM(?Ky|qd@>%T%`;b54EXff$=azUL%5@2XtZ6Y=u-RJS5IU(ByQ16osdv|-=CJTYU!J{ zh-u8>mpF$bBW;ComW^8OpX+3jd)(Jn7M0e1|1wIxpQ-BMIr)0Z{T-_%`@xmk3I{{w zH(^rcFN(6I@4Cr;?yS#peNwL`^RzPL$cx}TRoC8GIG}J``f#!KFYOsjU|TbDoGwLY}6?))XLVREItH7EfxFZ zZX#_@ynL^j)jKb%YfW!&wQcq**YD8O^JWgcs{iYzgJI2J?yp5n+luBpM|VfZl*lCy zynJ6b;;S{~^?LN#sK!YRMc?CT%U=xMkhGB>KYrRiQ|sS&9~-Td z+G`SWtTkce)u*oP7Wc}#FB9XMr?EFmCMne)TGzRMtgKSgcsh+KMOAyj}@x%Rprqyq7 z-IwQhEP$1XxwE>@dj#X^@yr(tNq$P50)s+gzljY-$^$%t0Tc;eOuB(q;J*XTerSr_-u#T_v z%{LiZR|;&R+)qD#pBFNXX*r%aR;o3Udq%CMrD%=DcOPQhqAa$R>1|fbF|brDA7~1G z5uM|1y?-e7bLP^TTT04`Q$eHNC6j-r7tOa{pQsoR7!wn#qvKjuHnP3cRW(;HZ9{U+ zrkjd2CkMjP1`0QXSXO%LSezOeImErJwfdAzyQ!(EU&YeK!(*R=cPWJ_Z5h1tIX-D# zy)5l`ZmM#3(cwm??=|-)OmwF_ZlAn4yu7QQY1p#9$EcyEf66gYPTEf1#&b%o&@IP6 zr`@}GLv7fz@#a+lZ}f*Mje;wNOtLdAy1MGx;tT$7u)6fo~G{CV_7f_$GmG68I*8ZxZ+>fo~G{ zCV_7f_$GmG68I*8ZxZ+>fo~G{CV_7f_$GmG68I*8|DzIc;dQur#o5zUy+&>a*2Ued zs@N9TojFiwJsSObO$}vIQA#%;=2M`v)wQ1ZwzABV16|`{Lls6PTOMh+)ST$@^B&f? zm~%fe?X!jzsk8mc)k{atM%~(bE8eKW?`*nz)TWwgtSIHRhWb$TVCb>Iky6v@n-l%d zM-2zx@6D%}{p;n~p47JdOYbeWP;4vZpKi_=i>bPko1;`HU=#s4RaO z{2{L5dpB2$`{UoYyvv(-pXYUUM@Rb+-El+vv)QV*8b8hLAGGlqveMXoz&=;$OEH|X+QV0I_;`H^r%+vLq?3%_xo>w@%DM|>=jBHfRqjX&IUU8#HYP>#ZqRnry;ZI3R zhRx+ul)GE~hPH1WYuC}wHQYE*AKcdN^y@pfzL5uM8>4=%Ds?ig40E3oOn1wv>%26% zI{R4prV>|4n!=<~#j-*V@6Rij**;D?WBj62qia;@&K{G@_fBT-UUs~1cFX_eMbQz9 zaXmY?SXIa7!LunYM@}4fQnR{#)F$)RrypVx%VV>0ZuI1pHZ@l4e7n|TLx_5fPtv}O zErZo34trGBJyz&a;;=MU$I|}n()*gH zE9O?%Oda(oN*@bXsu>T;n^(W$e&>YVz5TXr$(=7=44ky>Uul~%AnCL8j`DG&l#fc0 zQr>KRp}b_|g~QQ9^ulLp12p-OmXx>UD=hXoczZV&G-#BpHgIYzD3vf(U4CWf(|FLa z-h~m}4v}}PPQSOY%Si5hrm*H+A?TRf*&QQyOZO@~Iu5p_lYYOmVb3V+dbU0&EED{dWubU zdB>=1-tQHKHPuNG)0z-k*1Ha;3DF(!Dx| z$0-@FJr|7ir+ilHzpPnwc4bK4R<&bYJ>x64aIU0O&p@vlB-)?VeYCcGxnH&R9 zkQwV;@);Vq|9kJ_l$19IM`>qNj0$UB+zh>^tLjk|`w{y3_ug9Qt8aA@w{4}WLbJMthO%;)2nx!0|bz$fwrl-;v*K@N0y7f+Frdzr7%_#}eeH-0ay<_Jq2c?6X^0Lg@syeJJdz~!66ql;e z-6o4W#y{)!wV0<2cAij3bInTbC{nu^AEAa`LoE$NAJF=sr#@mfIn2J_)rPPdNDE9dpCUAHf-;l-f-5;{-KLpe~Z~C`{vE&<1?z_ z|IP@hN=5ELgkSN6fL|6@j+@`UoZ~VPuRDJrOX{Xnb?nCtA*R8u&iS4lsfsP3ikc%z z+l&r7oprzLq6YY>0Ql);Vw$ULC{-7?KKWJOkfDD6D)SqIUBe1H4&k_}^jCzrO!JZA zsn+fjdB_MBY<&SnV22w)NqSHDXwlk$Yf?G}1-6oh%s2Kp*oW=D>iSl$`g_(90R11& zUoKC%Y-#c|xBppA@5=LCp>AN)=}NfmUVPEG;$`IGD|`1P(pug`sKz+Q9xIwZSmOPB z#I1CP+|CusKbmdM@ve(~eED$BE+f(I-kjl<<*#B?4nJ_+qVBz+CH1jX>4uF7($4U~ zVZv`Tn(BKy=Q}zYzFC)JTg?|MtZ&+2B;mc&d){jECs&^A{fj={QZ4qb0Y_`};r@ZcR~8M_{_U;2{n_Sb*{lBbMbAu!QX`TgH-;L?d0CpDb+f(Hrs|PX zRx!WJM%tGW=1h?(4&H9_;|=#dyMyl%+b?U(NwdrHHAoA8a%0Kea;au5t+*nUe7|Q$ zE1OadWMtP>4Yk+R^kl9reV-}yW=U5Cx!kEUPs!-4OVeniN55h8G1rXx@uKRQp27s_ zJjcxSZ<{;|f8RB=B;e-bZ16H6?g<7vFW!i2o;ceBLMxh|NV}p^;Jg0#qv{EVauxS& zk2B(LDBsT=WPP$ z6$6IL%5=P&uXut_$wEvgs$J{k#TD&16{$v4L_tA7IgmRLP>!8^YB4sUnCF>xZ;o(|DSge1ZLqtsvuw=o$nxlhrdmH4!!VO(uEJGsOPSS~ zyNi9YyYCD9#)Eqe)hFJ>KYLk{dSy^kccf9^H!R{DkbijBZM(auIIkqidL$!G*Fx{= z{B14kHDx{Z7cMDUzqVjt8tamYlgl>@h>|m3o40EDWZ(8FL@mPlDn`|}4ZFA@)BZPa zlJ5uZD4~zMr7ivjZ1|v%_f88LckldZ%;s^p{?a2G?z#HRtLzwE_iLEaK<1V%Tf{?d zEEzu9CTTa8FLP^X`}(o2N=m;;aE@DoL23EQ*y3k=Aio)j@`yWZw|J{-8 zUEF;CY3MWHf_@RV!(L{8II}xNbz}3O@~uIubeS~+*4Q5E_Fxh?jYxz<8<(qF-71|$mPCBzqr^2*1M8~pH+Pp>6&VDQ;eM^Ck z`j7qLoQbl~aAEJ(j=bY5Z2oN?|EO@K@YRQp=K1DU5oDUZpZFQtPWFJ zeCYMqoX+IvspyxFo|YTrYI+vXcVJM$4SWW505PbS16>wz%_IMW3{=@ul&3NEo7JVK8^R=& zinjF^t<3u8u8~_nRbi>U=`$mmfOL<%M}+A2n4^gJ6niS<-A5LX>vdz7}P zn{y+Tq)cw^b@EM)I35_*{%n`JLw{HP%B*VD3a{+(1Lf}q>M&+O&gBnlgr)@*zam+Zu#@6Zz*nzlYNnyEvM~{dt@Z` z_B;LiF0TzhGJnU^*iNuqC6$~#)ox>1@88@Xvs>00a`4XGLfY!Xq3TIzlh)skOY~Jz zQqmmj)Hq%<7=JJhOekXF^V6oB(qE^sl~L>|(e{)!)wacvu?fHBYt^ZNEo!w^eOSfG z8G+z~>jJTdS^K1%+t3ENPhOV7!%LBkUNQs|-SKG}JJ8o`dwm)U-}$ef{}8nA5?H#c z^5&k$^AEL#ZEJ&GRGgzF*3#$L#vn&CeIWJJWjC? zl>I9fashi(EW9XPUU3cg)S`5(-~v{_6Q*)HZYEa0j8DFFh!wkc;!T)|lkR)DQ#par z)wE-Q&(n(IpJclPhM(HjEUO?%hra;xNmP(|744T@bHgkr$HnA$!w0$ak1B<3hie1x z)Y~144jeNm*uONat#!zE<)%&t)93Hn?N?Z=lGC_QYUk$atg_c(@s3KRzQE|4mkQh+ zv@Z5=EmH4VgZ5?Ce2BYP`>{zm%7p7BvU69avQf_8Q8E9S0kqlBuua%<}! zB`Q)HCo0ZHOulS=Ib3`=taqj5=q9BK$HcQygHGN3dfxS13!)YaUeSSh=)@1Rqdfy4J*aQvYz{lZY}cHsK0yVOwzK#OuDOcV13Bp z{bA0FYrJh2FTGQ-^tsP?O!(8+g>{`;Y2VHA8y!F4I$2nYl=7{S!|3kb_j`>fJO6e3 z16TUq+;`zR*nea`k`M&8wmVK69f_&+QAGbvZ`!u%iP`|uqe@Mm3Yzz1+ znux-yley9C>>f|}E_P)Y>ATDWDcW>k79+6xgoR#xe zcVkpzPHlx*r}^)x$lOb6>~&w27F%AsYNKqo$5!!53OObw#JN2FcK(B{H*F&lk`EDfTctf(q>m*jJ)XvRM{L8U_F79d^>8OVQ~D=}$kasZ6~rJ9@f->^ zLv&{6P(;TX&l2IzwJR-%(Rj-sM$@~_ZJk*d^MmVVx{agKZ%Q|mhmW>OTEEOfjOMgE z{C(P|@`V}o@r%C?_Juf;?`YgtAct~awazr7S-)5_|GYOpbk7Jyp-f}^(d4@R)$dlB zncZ%vy0SP@DJezKsqQa>gte`GYg`=j ztgk#JON%u5DXK*s`AiGx-h=tyl>wVH1vWYL*}&<_k#^pRTVW`cdGFy3`H2c(wB8Ax zZz6v$4>t9&vvxb!e(Bh-ZJ1I>5!c#IuCW8p>gAxUUcO4j=x!oG_wcI(-Tz9K-Ar%% zcINnB`;?O6_KvrQz#k$jEDhnxDo9&ikPB~pI^HeuzcKo`x!+&eD!3?*W2fK@?Zx0X zXm$Y{3jr_+io^uSi=iTP1`E33e_E+jG0v>=0Dn4EqG22?K!}S0a8tzsFseKiupAX) z7>|q5#e#TrI)|1lPwaVId4F!Q5Iu;7<28D^7&-*Z$2eRexDx=J40U1Qz!8Ux5S$vs zrOOBKl0%Z|Tsl309>gm_g}fjfv*F?M0gC~Ri-YFF&r^BMQ|f$qKx@Eg&O>>w(i;DS zMd@N8zf#F;9#1UvXQ70ADeE64^<{qyv2D{XKI42zQszS$jn{W(zo`DvHA0~@`OUOb zzXUBHbGWD{TzRy`7v!992ZekQl)wvML1ZD05C={`IQ|&#?aRtTTU7K+?+nx1CWn0O|WWM)M*(qt@XYgoEfl<%T_g0o5&kNbTjo!6efYd6iMcB0Ods|0vqyrK; zOEo;ZAh)N3QhRBIsdZD;RvZ4lbLp-1hN0sT@`HzL&nT&T^}6?H6uI1kzpHo8QGX^; zem3d#pbhYIHLDwbLpM_0-z;=ma-u1>cU(QXv9Y$iw$m!76Ect6W1j$P_x4G(Eb1$X zDgV8kdudm`oup!%&Drd?`>*Wt8gElbcDo0^GS%RZD0naQec77i-?R~ z)z=xlBgIXS%)m2xyJltdN{@^hc~#ZDDva=kzvNtaUGG_H?SS=Fs|U4RZ%1VB^)r== zazjFf8d9EJyUl>%6fqVtmXu)VvP4W7qHr!)SWJ?nF-fp81jQrDg42rgn zF$)g+=$Z(+i8#kgnEYav$xlDfJS5mO37k0pUW3LY9E zs>hOCX3RPOds96TQ#~D1y3q@Wu-s)hw1Fx#yyoMby`y9`MDroJ&pIAy_wB>E*44!@`iC}{%ooc8@)O#gq>3f z(uU1g7+?{81?V5#1e_4r8ea5O!7DOG#}JX^vA>upvQavR%Hv_g%7GRTng~|ZAVFfgl*!?-@#U-V+HK_Pid!c${MtKQF;)s;6f1ZQ88U{9hFzW z<`s*Du#tQc(xpxtyC_B~ULrjOFf`ToSwl^Du!y4l>KuD3LzqEFLx-3;K)je(z|6p* z*QX*u!itVD@FIL%#Mnq$FkI*yA*BE#F)Eyg29eRgkdwg~0KTJ|XdzmnB@=@FGMe~B zsOIm_l?Sf=UmJNGg$XRi?}ZICA0&nZ30r3cE}^21ip$F#N*Pr@Dq?61V6YtgRv5v(V@$D8QeGXQ!US_qWR;pq0@!9 zF!V@)-~fM17+O#&q}0$G5F$L6aALwRcE3{Rg9p2S;*Wv=PT2+I)3`rz(2$i~Kx6il z3how@|0jUc`$SQjS^Ol9$%657X2^U1YwI};W_RwKnA#7~x^u06244mulSdV1i2=lp zdsdJDe!J#Q=A&9-@J|X6H-iZXZS@CDCQJ*LFd=z? z_z49NBd(yW5Sxm&gdApV#Xui|&?WxDSAJvwxU2nQ18@WJIIuAcTrMm$j3Y)P%L@T1 zfe;UP2&f_(UBHB3^tb_BARsU_H&2-A+I1v{D%}aTS_=N$Io8kLbfLX^yLS_&!PLhJ3BooZMq9zcVkkt&&>mpOgRdhu1##QAtTr zYKNs)g#_{19kvIqTI3fpHhxkW6)PMndUeyuee&~p`Q$LYhTvs2W95nyRtln$%#sFY zH~PtwpS7N#<71su@SWi!gy3^_aWom{2mrP|Q4ewntcDD%TENBX5E4wk4Gep4)NK#BiFx(qR zVX&Sf1@6JDS){-Uv%fZK273CN|Kli7E4r@1ip^%C!r=dJPB7r z$hI*H9zWCMS4U7u#~34_%Fqq?A(3>nC7B|o+8{h#mN>kk-D|OD|1Qr~#t!%8Uem5~ zx2V){v#0^(>&*^V;U=ryjq@(ipsOV*>)vSVWHzWg7-gC+IXqS>Cpt%N{h%A|6lHOz z8x~T>Va7*aJuo4ClzC)M zU*erg?H0T9+Ut3o?&!lz?OHglwL8bMekPQ#^`PXkj6Zl!E{p1|Z~BuNto#SdwXP0r zvV{_Em6yWi*((WDG}j_N60?5xinKgkUJSexa##>0AQBDMg}mD)#QYzzAxH9AQR9{EeLm>5j}V*o|yQD1K`oKA4QRDdJo?V%FC3^^NqC4cYas8vHitdecXMLULSyvAkf-v=Apyw&0m*FK;|M$? z^0k6wm_Ho{RIwloZs7v%(&b@5;E)L2P##SFF=PH_3^rT-n9F*c{_nz#zwf;G6ZWYr zZK2=qjiiYz_xA0RCu;{V=e~=JWqh!oJH$3T2ek(al@w>W|7+u(#^O`-Uz*JYqR2NKAm&F9T2XRSaU}gws7<}1b7^0E*2~Km7Wg|m{>$vff zMDbH`4Aa(S=+1elt%EER+3HtDiAc48R^yGq25u9DmISAw;(UOBif}KJDfyp21VtBA zAfc|JM(_s2iHJZ%7g|A9PGU+L!<`I_A%amvMRb6;Bgh2)ECrY)$&{3!T!#T9;T=Ga z##_NeNPvaLl9Hk<#M$#=REMA#$TOoAXB)sIftm{3 zE13#o&&Gfn3(@5_@qiZ2zY{cn&vj$FXS7pG4oNJFcEC=QZ*0J}MaVxfeD`r*3Z}I} z)zc)_;N;Toaz%C7jo2qM)x?V}r^k}sUG4t-ue4-zbgY4a(FsX$QH-=3Tk+)Y{Sq?g zZjrpu@{6Gvf$$& z;tEa%a!7?`0VSzFbYJ-Q2%1u`tT0{Rez+N`fEXtZAiU!m49gK!eX{)z~LVxmMlL_;vFV2dB5wj z4vUyB2NjS2&^d%HKR`-ktgw}2CNfQz!|u93{z!`2Oa=@Chzcn?g&|G|5*acq3`~@P z_9)gRq0wUCI->UkZX-sCg`r|%5g`#iob+F|3D&A(3PR<#E~o%|e2`G!OO(y>`51xb-h@t~g#!x^fSg9C)w23kQ?{NoSi=pTdtq^u{VT{4v<%t)9 zEtym3q32wbt9hoV)#d4E3m(UZA!VV8*LTU7%01gm$Dm)4? zcA}g71N;^?oCNV`C>Y|PXaE$D$`QoT@(}J}K~!pjbutRZVA2Kd#=z4M=!Ld;AO!I) zy1-wA_~L;(128cv-X^{QEf=h8bT5w#;5JK^P=wouKtMw1CNfM9Q4s||z8eJtoB%wy zVw2f;Fb)ks>JWxub}-3iL%&gWfr`H~3eq8TBR>}l`*_H}*yY*}63car?H~c7W30o# zIU=7af>PkR!QW^j&qFB6S6dR;4k!=>`5PSg#988|IJ*UK1v@|rq%AqaGsRi1yVlFy zpHMULPm^$dA2OLfjh(C=SNZOtS1|tJ0_Rf=Aj|CWVF%YW>_AXuHtRD`G8oMWp%ieP}t5C z>GRsnh}QAI^`OumBqNAY*&y;+0WBY6K+52k#Hdsbj!z(RSlIZMjJ@KbAptB4Km~+Q zL9F=6*TFb%Cho+K(Bt_5{MUO_l_yW9(m;#&%7Y*IBh|yz1a5%gf))BhhUcTvrvlsb zC&*0z#MoroDVjVDgFu@@L&k*bHLgowT|nT3IBe8!xBlH?76&J=Z~A2(!3wQ zJjCmoPK&os`~cj6}G))DJNB0%d5 zqQmj)%>a9tcs1k#KQgfK-$UD@G6zK!ZdN(IK30eIfmbYsq6s9Z)K1 z)`pEp81lYcNM*87efF&21U2@Bx`EOPv7rR=JREQdLiB|2-xLST@8}b2VMRL34<=@?2gw@kdAv!2V6+`%sjD*W(BUOb^0%0?u^$;WBX2Yfu zQ~_*247bLu@&Zf~B);%EupuCp8-Tw8yaxY$1((qtdO!OVWTe0w`q*folNkn;*cTVvq>si1CnEP#OuXfDR}LCkA%_=#v4Hq4R*d zkgEaPllWIUnhN=MsSDc7a}qiNllhwL1D1*k`hyq;pLLK}K)ZlxkhD<(fyg2drbuWa zAG5(0`8*Vvb@$ht)bGPh?LLAr?LLy#lz?BDFxBPEZ6#D@2v3< z$M`4;XY|-CV+lOuKsLa_P|5=7D+`zfT5#}(2ZQc{^+Cu3oCaq)6z$tOU`Y@XKtCW7 z0^)+ym{4TADr}%l!6Ckf83_s#y7*NgIw9zeAcrA|zqJI>mN;&3a0BA80#m{#6-2;# z5T8ZU4T#=iBEXcOlPI;q!hptU8;d}41+_1Z9)Oz1L?M{sNXiUfh~~8Y%pg}`Yp(lq zffA6xv4UIRU;w=+--);oG7ng(s0FkLh$@O&1E?HSBqR?>V!puy1%bmxx+f$EVTs%& z6yQ3;&%EMcHU#TJ2??_ABv)NDACb%b6q(=E*8JJ{!p`U2PZYbgnI#W@(q*~aHej;c zACk%SLZKM2ZU%Tbyi0`ppu;gbB6@=RFKNcbU-BBTOI0UsqZVD}+{r|lZc}}njvRDusOr8xnYLr5lnNWZkOdyp3Y#tTm ztl7x=G03QcFxlt`Cqy6|Duzsr1|P{lGsv|rppyF&f0LLoDLn>I89c!T8i+v%I}-r| zqKm@R5e5LSjR&X*;n9zTwl=Pv$Sp9Sk~sJ{#He*y6iK`QH3a&FTNRG^pwsv&f!H_h zE8!o7mvKoK3xE2;@0LY>+jqRFec3G~D{XDN$gV|u_03CoZGZg1R@oNKR=c)eVmX?_ zI>?X^)jKCCA&QCPZowFNFk(Ua49`T23c@jDaHb4x21x`=9d|Gy*iq&?xK)D4p9re) z_e6|>pTPS|qHF^H3SmkAu|)y_rjItWR%9Z?N{B*Rk^u%TE~*U)4&0h35JQSU*sD#E zK*1w)hQibaGvY7MYuq^ThcUqT82FL62n(Vd(~Qp-aeYfrp`r;1xL3h4~ zN^?J5OEuV=)Gj`>Cimy`T2!jfs^!}(2x&WyWnDK?N`bFI3k3WG4 zOI=`Up9kj$SV74x2W#Oid z!=&}Ey3C2o`7gpV2A`%({W7VwKIc~j<8!V0dt?r3EZjW@TWBatUD!b-aY)y7=bVNf zE=Sk`0iq~+EG#H=Km(5BuC}o@%1PiC066fl6CMnS;)A9L!2e?bHjG}2P%1$(Z8nq% zj-eio;xqLbf-8c(AbAI!Lo5Lnaa?0TZxN{goZ}}{bP&5Byau`pkP+zIWDyjyGBw*&C1F(QgC00TF2c^j3(dv&T z{{i*T%0)LLpaCs`YzjVCi?|7Z2)-cKj1)Ww41&you;BZ$n$OpLuSV{&>e8_T4TC=^O*&xkssJQd}tE7qL)`$B_qgCF9>TKu^FN*i_UmVhDmsZ%{%G z(oBTCWI7w~%T^6C1_=@lx))4q2J8qE;?CI7EbGA|5aNEp7ElOHL*xCQ9ing%IN=K} zBd}~B3uFvH4#E|Pj*(9QDuE*({?Qh6CJ4+QfhjD&9iUBtO#vM8s2s?@aM*YY@M4&l z*n*$`rS>Mh-2W01*XPKa+25@Nb|QqEKYZWtr8892ApY+7_z@iw3?3R)f4&#PkhKa1 z*`E*q8|or)5i5jGh*$!IabO_vX?ZdDuU>u;9@=v9<|;d_IKsS2pX@YgT(hn6<-HLI zi6eG7lVrxD{S)pkBI!LBaUkij=bR&5gK!Ws?w*c$R@xg{IL{>S)hOXrhrJGW-vS$hQJsM5n9k;>CUF8G z$^KY*hpe*ngafAL*j10YSp>5;!yKjE>jfFdKrV^+1Ta2Fi^ntsaf7!)!3z#AFqa{Y zuA^Wbpb5uL2w;%=!oMm)V1T5CV-ta_0^!0$0yF{+&h9sD=EMVpg{#U<%s!qUwV5b= zqHA<)dy?ow@L0|UOzq+);I@v^5Vtsp)g#Zv76bFe=%7G^Ve#K52KtGlK}JT; zR~jMOsef=9hzku}#h0&84IvtaCZ}OShDc5j{TB2dz>45^IFRETK|6vy9x{P|V1@&O z1P>-If*CW!87TR~A2}FJm|~UyZbxJz1l@q4#1BQ$nBn6N2&qkiTr@Vv5zBHdW2ZLN zY$D(4cvf^T;MVSjz`MhPLGK;zWjp;6)YiML;c$S8xuIq~2hHa{HI|M~(mB9eXjq+a zNXJaJFP@|8-c)mke5?Dx=$~_KmSS#-$L7@C6F%Aq$&f?mV8GEU!=yZVHlCnDeL*?} z=^iN1Mng?FAihXY%*+pM`E*|pMl&GE0_q@vec>ZzCsp{CnZsg@{kk(rrz0eS^tAqj16>%7>zR^Ivy%=|EXGds3a!iQO9eHss6D3r>dC({A^S*o9kB-L zf=yTz1QFoGkoySYLd1>CYbIC+BLi%vBF`vrkLdLTZkRte1<;Oj#x?j?N^~tHv(L_> zetVqu3~TicsLqLd>^<}b!^JILZ3p|EgX-2xb7{!jT{d>simY#!$=wd=B=4Ktl{Bx}X9qWFaw>8-+W_ zn9?!P?8Pv?i>Yfjb;pR9#<-RR%-d89pJl>j9yEw1#lCDn+^7Y7!Cx5L@SK#Jh{@c( z_&2FZO4X6K(BMEvJxdoxnQ|fEmLUwkh?(4>N+~+^U9YNJ1gYgtS3PU4Uv<(UW9i@r zZkOib1K&^BzUw}UMV4zL#o|`B|H4AB`K6dctbc-me*&n1Sm-7(aH;&^2Y-nc@R|I` zoyd#f%nW_tB7fl*;92x-%PX!;V@~H@`hGv@6D}zU*8K)0ZXV@A07Di&9~dBTminl$~mQ9}HK#zI3Pgj~R0@DN*| k&HsTvmxG33QUDn{TFAr>wrq5SM@91ZL^C?Yw5IF+Kkj_Y{{R30 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux43/captures/chamber-overview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1fbce31706dc516eb4a6570ceff09bfcefd86054 GIT binary patch literal 43827 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 DWNk3~ literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Research10/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research10/REPORT.md new file mode 100644 index 0000000..f987906 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research10/REPORT.md @@ -0,0 +1,63 @@ +# Research R10 — Ore and resources as a material channel + +Read-only review; no code or files changed. The GDD asks for recipe-critical resources to be guaranteed by density within reach of the spine, while optional materials remain random and depth-locked by difficulty tier rather than fixed to particular strates. [GDD §9]() The current edit layer changes density only, and its documentation leaves saving and loading to the game layer. [VoxelDiffLayer.h]() + +## Representation choice + +**Rank 1 — Bounded vein records in the resolved layout, queried analytically for material.** Treat each vein as immutable, versioned data with a stable ID, resource ID, shape and bounds, material, and yield density. Generate the records before tile jobs, then use a per-tile candidate list to restrict membership checks. This is deterministic, uses memory proportional to the number of veins rather than world voxels, and costs roughly one membership test per nearby candidate. Keep the records separate from density so ore does not alter rock or passage topology. Resolve overlaps by stable ID order. + +At exposed mesh points, query the material and blend across a configured vein boundary. Mesh attributes need an explicit material identity and blend weight: current F6 vertex color already carries biome, slope, edge blend, and neighboring-biome data, while F17’s two material groups separate ground from sky-cap. [VoxelMarchingCubesMesher.cpp]() · [VoxelWorld.cpp]() Far LODs will naturally undersample narrow veins; define a minimum visible feature size or a distance fade so they don’t flicker. Do not create one mesh section per vein. + +**Rank 2 — Material IDs stored or sampled per voxel alongside density.** This is conceptually simple, and a joint density/material sample can reuse the mesher’s lattice work. But storing IDs per chunk adds memory, cache invalidation, save/load, and cross-LOD rules. A sampled ID alone also produces hard boundaries; blending still needs boundary distances or weights. If sampled on demand rather than stored, it is effectively the material query in Rank 1, with the vein records still needed as its source. + +**Rank 3 — A separate general resource field.** A continuous field can blend smoothly and represent broad concentrations, but adds another evaluation path or sampled volume. A general noise field also makes exact deposit bounds, resource yields, and recipe guarantees harder to author and audit. It may suit diffuse materials later; it is a poor first representation for countable veins. + +## Mining and authority + +The games point to two useful patterns: material-bearing terrain that yields resources when mined, and deposit objects that produce collectible units. Minecraft’s ore blocks yield specific items, with Fortune affecting yield and Silk Touch preserving the block. Space Engineers maps mined voxel materials to ore types and ratios. 7 Days to Die harvests resources as blocks are damaged or destroyed, with tools affecting mining efficiency. [Minecraft ores](https://minecraft.wiki/w/Ore) · [Space Engineers voxel materials](https://spaceengineers.wiki.gg/wiki/Modding/Reference/SBC/VoxelMaterial_Definition) · [7 Days to Die mining](https://7daystodie.wiki.gg/wiki/Mining) + +Deep Rock Galactic makes mining and carrying part of the mission loop: mined veins enter a limited carried inventory, then get deposited or carried to extraction for team rewards. Astroneer’s terrain tool breaks natural deposits into resource nuggets, while automated extractors increase the yield. [DRG pickaxe](https://deeprockgalactic.wiki.gg/wiki/Pickaxe) · [DRG resources](https://deeprockgalactic.wiki.gg/wiki/Minerals) · [Astroneer resources](https://astroneer.wiki.gg/wiki/Atmospheric_Resources) **For VoxelForge, the useful combination is material-bearing rock plus a carry-and-return inventory loop.** + +Recommended flow: + +1. The client requests a mining action with a unique sequence, brush, and tool. +2. The server validates reach, tool, limits, and sequence; determines which solid material the edit actually removes at a fixed canonical resolution. +3. In one ordered transaction, the server applies the carve and grants yield only for newly removed solid resource material. A repeated request must not award it twice. +4. The server persists the accepted edit and reward, then replicates the edit and result so all players see the same depleted vein. Late joiners load the same edit state. + +The current legacy path writes to `DiffLayer` in `AVoxelWorld::ApplyModification`; it is not a mining-reward flow. [VoxelWorld.cpp]() Core’s edit transaction already has a sequence and digest, but its adapter comment calls that boundary a future authority path. It currently returns a digest and affected tiles, not harvested material. [VoxelWorld.cpp]() · [VoxelForgeCore.h]() So persistence, replication, and server-side reward accounting remain game-layer work. + +## Contract extension + +Add a **companion `materialAt(xUm, yUm, zUm) -> MaterialSample` query** to the immutable evaluator bound to the resolved strate. Return the host material, up to two resource material IDs, and a blend weight. Include a material detail envelope if the mesher needs to suppress or fade veins below a safe LOD size. + +Resolve versioned vein records and their stable IDs in the director’s snapshot; give tiles a conservative candidate list. Sample materials at mesh surface vertices and during the mining transaction. Keep density culling certificates density-only: materials do not create geometry, so they should not change solid/air proofs. This extends R6’s immutable, coordinate-stable evaluator contract without adding resource work to every density call. [Research6 §2]() + +For mining, extend the edit result with deterministic removed-material quantities. Calculate them from the before/after solid state at a fixed world resolution; Unreal awards inventory only after the server accepts the edit. This prevents yield from already-air space and makes replay and duplicate-sequence handling auditable. + +## Placement and authoring + +- Guarantee a minimum reachable density of every recipe-critical material in each **eligible** strate. Keep depth eligibility tagged by difficulty tier, as the GDD specifies; don’t assign every resource to a fixed strate. Measure “reachable” against the spine and feasible side-pocket dig distance, rather than a coordinate promise. [GDD §9]() +- Bias veins along passage routes and toward side pockets, with a mix of visible surface outcrops and material just behind the wall. Keep the clear passage, landings, and landmark footprints open. Use resolved passage and landmark bounds as placement inputs so resource placement follows planned content rather than tile order. +- The owner authors resource definitions and looks, rarity and depth-tier tables, vein size and yield, and guarantee quotas for recipe-critical materials. The Unreal material palette supplies the visual blend and emissive treatment. + +## Implementation plan + +1. **Core data:** add versioned vein records to the resolved layout, deterministic generation and stable overlap ordering, plus a tile candidate index. Include the records and material-table version in world identity. +2. **Core query:** add `materialAt` and a material detail envelope. Verify repeatability at fixed physical coordinates across worker counts, tile order, and LOD. +3. **Core mesh:** add explicit material IDs and weights to mesh output; sample at surface vertices and blend at boundaries. Leave density and geometry hashes unchanged. +4. **Core mining result:** return removed-material quantities with the edit result, using a fixed canonical sampling resolution and an idempotent edit sequence. +5. **Unreal integration:** connect ore assets and the new mesh stream/material shader; route mining through server validation, edit persistence, reward persistence, and replication. Ensure both supported edit paths use the same authority flow. + +## Measurements for implementation + +- Material-query and mesh cost versus the density-only baseline; candidate counts by tile; peak vein-index memory; additional bytes per vertex and upload time. +- Material ID and weight repeatability across workers and tile order; density and topology hash parity; boundary consistency across neighboring tiles. +- Ore visibility and popping across LODs, especially for veins near the minimum feature size; boundary blend width at close and far LOD. +- Yield for each brush shape against newly removed solid ore; zero yield from air and no duplicate yield on replay; persistence and late-join agreement. +- Per eligible strate: recipe-critical yield density, distance to spine, travel and dig distance, and counts of blocked passages or overlapping landmark footprints. + +## Owner questions + +1. Should yield be proportional to newly removed solid resource volume (recommended for freeform carving), or awarded once per deposit? +2. What spine distance and side-pocket digging effort count as “within reach” for recipe guarantees? \ No newline at end of file diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index e9eb600..49f164d 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -54,6 +54,7 @@ namespace bool HasRenderDistanceOverride=false; int RenderDistanceOverride=0; bool FloatingIslandsRecipe=false; + bool LandmarkChamberDemo=false; bool TlsProbeRequested=false; int TlsProbeWorkers=0; std::string TlsProbeTouch="none"; @@ -107,6 +108,91 @@ namespace return true; } + bool MakeLandmarkChamberDemoRequests( + const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, + std::int32_t seed, + std::vector& requests, + std::string& error) + { + using namespace VoxelForgeCore; + requests.clear(); + const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(), + [](const auto& value){return value.generatorKind=="SurfaceWorld";}); + if(surface==layout.strates.end()) + {error="landmark chamber demo needs a SurfaceWorld strate";return false;} + const auto passage=std::find_if(layout.passages.begin(),layout.passages.end(), + [&](const auto& value){return value.lowerStrateIndex==surface->strateIndex + &&value.controlPoints.size()>=2;}); + if(passage==layout.passages.end()) + {error="landmark chamber demo needs a passage arrival mouth in SurfaceWorld";return false;} + const std::size_t mouthIndex=std::min(passage->lowerMouthControlIndex, + passage->controlPoints.size()-1); + const auto& mouth=passage->controlPoints[mouthIndex].positionSubvoxels; + const std::size_t priorIndex=mouthIndex>0?mouthIndex-1:mouthIndex; + const auto& prior=passage->controlPoints[priorIndex].positionSubvoxels; + const double dx=static_cast(mouth[0])-prior[0]; + const double dy=static_cast(mouth[1])-prior[1]; + const double length=std::hypot(dx,dy); + if(!(length>0.0)) + {error="landmark chamber demo passage arrival has no horizontal tangent";return false;} + const double sideX=-dy/length,sideY=dx/length; + const double micrometresPerSubvoxel=static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel; + std::array arrivalMicrometres{}; + for(std::size_t axis=0;axis<3;++axis) + arrivalMicrometres[axis]=static_cast(std::llround( + mouth[axis]*micrometresPerSubvoxel)); + ResolvedLayoutStampRequestV1 chamber; + chamber.stableId="landmark/sw-arrival-chamber-v1"; + chamber.strateIndex=surface->strateIndex; + chamber.kind=ResolvedSdfStampKindV1::carveEllipsoid; + chamber.anchorPositionMicrometres={ + arrivalMicrometres[0]+static_cast(std::llround(sideX*28000000.0)), + arrivalMicrometres[1]+static_cast(std::llround(sideY*28000000.0)), + arrivalMicrometres[2]-14000000}; + chamber.placementRadiusMicrometres=2000000; + chamber.halfExtentsMicrometres={8500000,7500000,6000000}; + chamber.smoothWidthMicrometres=800000; + chamber.irregularityAmplitudeMicrometres=700000; + chamber.irregularityWavelengthMicrometres=4800000; + + ResolvedLayoutStampRequestV1 dais; + dais.stableId="landmark/sw-arrival-chamber-dais-v1"; + dais.strateIndex=surface->strateIndex; + dais.kind=ResolvedSdfStampKindV1::fillDais; + dais.anchorPositionMicrometres=chamber.anchorPositionMicrometres; + dais.anchorPositionMicrometres[2]-=2600000; + dais.placementRadiusMicrometres=1000000; + dais.halfExtentsMicrometres={3200000,2800000,900000}; + dais.smoothWidthMicrometres=300000; + dais.irregularityAmplitudeMicrometres=180000; + dais.irregularityWavelengthMicrometres=2200000; + + const double entranceDirectionX=sideX,entranceDirectionY=sideY; + ResolvedLayoutStampRequestV1 entrance; + entrance.stableId="landmark/sw-arrival-chamber-entrance-v1"; + entrance.strateIndex=surface->strateIndex; + entrance.kind=ResolvedSdfStampKindV1::carveArch; + entrance.anchorPositionMicrometres={ + chamber.anchorPositionMicrometres[0] + +static_cast(std::llround(entranceDirectionX*6500000.0)), + chamber.anchorPositionMicrometres[1] + +static_cast(std::llround(entranceDirectionY*6500000.0)), + chamber.anchorPositionMicrometres[2]}; + entrance.placementRadiusMicrometres=1000000; + entrance.halfExtentsMicrometres={2600000,4200000,3200000}; + entrance.smoothWidthMicrometres=350000; + entrance.irregularityAmplitudeMicrometres=250000; + entrance.irregularityWavelengthMicrometres=2600000; + if(std::abs(entranceDirectionX)>std::abs(entranceDirectionY)) + entrance.yawQuarterTurns=entranceDirectionX>0.0?3:1; + else + entrance.yawQuarterTurns=entranceDirectionY>0.0?0:2; + requests={std::move(chamber),std::move(entrance),std::move(dais)}; + (void)seed; + return true; + } + bool OverrideTransitions(VFJson::Value& root,int mode,int widthChunks,std::string& error) { auto* object=std::get_if(&root.Data); @@ -159,6 +245,7 @@ namespace " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --seed=N --recipe=surface-world|floating-islands\n" + " --landmark-chamber-demo (opt-in version-1 SDF chamber and entry arch)\n" " --resolved-layout-output=FILE (canonical plan-time layout snapshot)\n" " --resolved-layout-only (exit after plan and snapshot validation)\n" " --audit-resolved-layout-boundaries (query Core at each recorded endpoint)\n" @@ -223,6 +310,7 @@ namespace else if(recipe=="floating-islands")options.FloatingIslandsRecipe=true; else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast(recipe.size()),recipe.data());return false;} } + else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true; else if(arg.starts_with("--resolved-layout-output=")) options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size())); else if(arg.starts_with("--seed=")) @@ -400,6 +488,42 @@ int main(int argc,char** argv) if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot( *ownerWorld,worldAssembly,variant,resolvedLayout,error)) {std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;} + if(options.LandmarkChamberDemo) + { + std::vector requests; + if(!MakeLandmarkChamberDemoRequests(resolvedLayout,seed,requests,error) + ||!VoxelForgeCore::ResolveResolvedLayoutStampsV1( + resolvedLayout,seed,requests,error)) + {std::fprintf(stderr,"Landmark chamber stamp resolution failed: %s\n",error.c_str());return 2;} + const auto audit=VoxelForgeCore::AuditResolvedLayoutStampPlacementV1(resolvedLayout); + std::printf("[ResolvedStampPlacementAudit] vocabulary=%u records=%zu route_control=rejected band_control=rejected controls=%u/%u status=%s\n", + resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size(), + audit.negativeControlsRejected,audit.negativeControlsRun, + audit.passed?"PASS":"FAIL"); + if(!audit.passed) + {std::fprintf(stderr,"Resolved stamp placement negative control failed.\n");return 2;} + for(const auto& stamp:resolvedLayout.stamps) + std::printf("[ResolvedSdfStamp] id=%s strate=%d kind=%u position_m=(%.3f,%.3f,%.3f) bounds_um=[%lld,%lld)x[%lld,%lld)x[%lld,%lld) extents_m=(%.2f,%.2f,%.2f) irregularity_m=%.2f/%.2f seed=%llu\n", + stamp.stableId.c_str(),stamp.strateIndex,static_cast(stamp.kind), + stamp.positionMicrometres[0]/1000000.0, + stamp.positionMicrometres[1]/1000000.0, + stamp.positionMicrometres[2]/1000000.0, + static_cast(stamp.boundsMinimumMicrometres[0]), + static_cast(stamp.boundsMaximumMicrometres[0]), + static_cast(stamp.boundsMinimumMicrometres[1]), + static_cast(stamp.boundsMaximumMicrometres[1]), + static_cast(stamp.boundsMinimumMicrometres[2]), + static_cast(stamp.boundsMaximumMicrometres[2]), + stamp.halfExtentsMicrometres[0]/1000000.0, + stamp.halfExtentsMicrometres[1]/1000000.0, + stamp.halfExtentsMicrometres[2]/1000000.0, + stamp.irregularityAmplitudeMicrometres/1000000.0, + stamp.irregularityWavelengthMicrometres/1000000.0, + static_cast(stamp.irregularitySeed)); + std::printf("[LandmarkStampDemo] enabled=true seed=%d layout_id=%s passage_safe=true walk_route_clear=true version=%u deterministic_hash=seed,strate,stable_id status=PASS\n", + seed,resolvedLayout.stableIdentity.c_str(), + resolvedLayout.stampVocabularyVersion); + } const std::string layoutJson=resolvedLayout.ToJson(); const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(layoutJson,error); if(!roundTrip||roundTrip->ToJson()!=layoutJson||roundTrip->stableIdentity!=resolvedLayout.stableIdentity)