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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-23 21:50:56 +02:00
1 parent 497e974178
commit 422dd7761a
12 files changed
+1138 -2

No files matched your search

@@ -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<std::int64_t, 3> anchorPositionMicrometres{};
std::int64_t placementRadiusMicrometres = 0;
std::array<std::int64_t, 3> 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<std::int64_t, 3> positionMicrometres{};
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
std::array<std::int64_t, 3> 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<ResolvedLayoutStratumV2> strates;
std::vector<ResolvedLayoutPassageV1> passages;
// Kept absent from canonical layout identity when no feature requests stamps.
std::uint32_t stampVocabularyVersion = 0;
std::vector<ResolvedLayoutStampV1> stamps;
std::string stableIdentity;
bool finalize(std::string& error);
@@ -907,6 +952,19 @@ namespace VoxelForgeCore
const std::vector<ResolvedLayoutPassageConfigV1>& 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<const ResolvedLayoutStampRequestV1> requests,
std::string& error);
VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
const ResolvedLayoutSnapshotV3& layout);
struct FloatingIslandBoundsAuditResult
{
std::uint64_t boxesTested = 0;
+397 -1
View File
@@ -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<std::uint32_t>(seed));
TransitionHashWord(hash,static_cast<std::uint32_t>(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<std::uint8_t>(kind);
return value>=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::carveEllipsoid)
&&value<=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::fillDais);
}
double LayoutStampPointSegmentDistance(const std::array<std::int64_t,3>& point,
const std::array<std::int64_t,3>& a,const std::array<std::int64_t,3>& b)
{
double lengthSquared=0.0,dot=0.0;
for(std::size_t axis=0;axis<3;++axis)
{
const double delta=static_cast<double>(b[axis])-a[axis];
lengthSquared+=delta*delta;
dot+=(static_cast<double>(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<double>(a[axis])
+t*(static_cast<double>(b[axis])-a[axis]);
const double delta=static_cast<double>(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<double>(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]<band->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<std::uint32_t>(1,layout.coreVoxelSizeMicrometres);
constexpr double RouteClearanceMicrometres=1000000.0;
const auto routeSafe=[&](const std::array<std::int64_t,3>& a,
const std::array<std::int64_t,3>& b,double radiusSubvoxels)
{
const double radius=radiusSubvoxels*coreMicrometres
/WorldAssemblySubvoxelsPerVoxel;
return LayoutStampPointSegmentDistance(stamp.positionMicrometres,a,b)
>boundRadius+radius+RouteClearanceMicrometres;
};
const auto qToMicrometres=[&](const std::array<std::int64_t,3>& q)
{
std::array<std::int64_t,3> result{};
for(std::size_t axis=0;axis<3;++axis)
result[axis]=static_cast<std::int64_t>(std::llround(
static_cast<double>(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<double>(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<passage.controlPoints.size();++i)
{
const auto a=qToMicrometres(passage.controlPoints[i].positionSubvoxels);
const auto b=qToMicrometres(passage.controlPoints[i+1].positionSubvoxels);
const double radius=std::max(passage.controlPoints[i].radiusSubvoxels,
passage.controlPoints[i+1].radiusSubvoxels);
if(!routeSafe(a,b,radius))
{if(reason)*reason="stamp overlaps a resolved passage walk corridor";return false;}
}
}
std::vector<const ResolvedLayoutAnchorV2*> 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<route.size();++i)
{
const auto makePoint=[&](const ResolvedLayoutAnchorV2& anchor)
{return std::array<std::int64_t,3>{
static_cast<std::int64_t>(std::llround(static_cast<double>(anchor.values[0])
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel)),
static_cast<std::int64_t>(std::llround(static_cast<double>(anchor.values[1])
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel)),
static_cast<std::int64_t>(std::llround(static_cast<double>(anchor.values[2])
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel))};};
const auto a=makePoint(*route[i]);
const auto b=i+1<route.size()?makePoint(*route[i+1]):a;
const double radius=i+1<route.size()
?std::max(route[i]->values.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<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
std::string LayoutBandJson(const ResolvedLayoutBandV2& band)
@@ -238,7 +377,34 @@ namespace
}
out<<"]}";
}
out<<"]}";return out.str();
out<<']';
if(!s.stamps.empty())
{
out<<",\"stamp_vocabulary_version\":"<<s.stampVocabularyVersion
<<",\"stamps\":[";
for(std::size_t i=0;i<s.stamps.size();++i)
{
if(i)out<<',';const auto& stamp=s.stamps[i];
out<<"{\"stable_id\":"<<LayoutEscape(stamp.stableId)
<<",\"strate_index\":"<<stamp.strateIndex
<<",\"kind\":"<<static_cast<unsigned>(stamp.kind)
<<",\"position_um\":["<<stamp.positionMicrometres[0]<<','
<<stamp.positionMicrometres[1]<<','<<stamp.positionMicrometres[2]
<<"],\"bounds_min_um\":["<<stamp.boundsMinimumMicrometres[0]<<','
<<stamp.boundsMinimumMicrometres[1]<<','<<stamp.boundsMinimumMicrometres[2]
<<"],\"bounds_max_um\":["<<stamp.boundsMaximumMicrometres[0]<<','
<<stamp.boundsMaximumMicrometres[1]<<','<<stamp.boundsMaximumMicrometres[2]
<<"],\"half_extents_um\":["<<stamp.halfExtentsMicrometres[0]<<','
<<stamp.halfExtentsMicrometres[1]<<','<<stamp.halfExtentsMicrometres[2]
<<"],\"smooth_width_um\":"<<stamp.smoothWidthMicrometres
<<",\"irregularity_amplitude_um\":"<<stamp.irregularityAmplitudeMicrometres
<<",\"irregularity_wavelength_um\":"<<stamp.irregularityWavelengthMicrometres
<<",\"irregularity_seed\":"<<stamp.irregularitySeed
<<",\"yaw_quarter_turns\":"<<static_cast<unsigned>(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<std::string> 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<std::int64_t,3> 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> ResolvedLayoutSnapshotV3::FromJson(
s.coreVoxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"core_voxel_size_um"));
s.strateLayoutChunkSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"strate_layout_chunk_size_um"));
s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels");
s.stampVocabularyVersion=static_cast<std::uint32_t>(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> 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<std::int32_t>(LayoutInt(v,"strate_index"));
stamp.kind=static_cast<ResolvedSdfStampKindV1>(LayoutInt(v,"kind"));
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
{
const auto& values=v.Get(key).AsArray();
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
target[axis]=values[axis].Integer();
};
readTriplet("position_um",stamp.positionMicrometres);
readTriplet("bounds_min_um",stamp.boundsMinimumMicrometres);
readTriplet("bounds_max_um",stamp.boundsMaximumMicrometres);
readTriplet("half_extents_um",stamp.halfExtentsMicrometres);
stamp.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
stamp.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
stamp.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
stamp.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
stamp.yawQuarterTurns=static_cast<std::uint8_t>(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<const ResolvedLayoutStampRequestV1> 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<ResolvedLayoutStampRequestV1> ordered(requests.begin(),requests.end());
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
{return a.stableId<b.stableId;});
ResolvedLayoutSnapshotV3 candidate=layout;
candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV1;
candidate.stamps.clear();candidate.stamps.reserve(ordered.size());
for(std::size_t requestIndex=0;requestIndex<ordered.size();++requestIndex)
{
const auto& request=ordered[requestIndex];
if(request.stableId.empty()||(requestIndex>0
&&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<std::uint64_t>(radius)*2+1;
return static_cast<std::int64_t>(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<std::int64_t,3> offset{};
if(request.placementRadiusMicrometres>0)
{
offset={randomOffset(LayoutStampNext(state)),
randomOffset(LayoutStampNext(state)),
randomOffset(LayoutStampNext(state))/3};
const long double radius=static_cast<long double>(request.placementRadiusMicrometres);
const long double distanceSquared=static_cast<long double>(offset[0])*offset[0]
+static_cast<long double>(offset[1])*offset[1]
+static_cast<long double>(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<std::int64_t,3> 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<ResolvedLayoutIssueV2> 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<std::int64_t,3>& position,
std::int32_t strateIndex,std::array<std::int64_t,3> minimum,
std::array<std::int64_t,3> 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<ResolvedLayoutIssueV2> 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<double>(layout.coreVoxelSizeMicrometres)
/WorldAssemblySubvoxelsPerVoxel;
const std::size_t routeControlIndex=passage->lowerMouthControlIndex>4
?passage->lowerMouthControlIndex-4:0;
std::array<std::int64_t,3> position{};
for(std::size_t axis=0;axis<3;++axis)
position[axis]=static_cast<std::int64_t>(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<std::int64_t,3> 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<ResolvedLayoutPassageConfigV1>& configs,std::string& error)
{
+355 -1
View File
@@ -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<std::uint32_t>& stampCandidates() const{return stampIndices_;}
private:
const FStandaloneShapeWorld* shape_;
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
std::vector<std::uint32_t> featureIndices_;
std::vector<std::uint32_t> stampIndices_;
std::vector<std::uint8_t> 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<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
};
struct FStampSpatialNode
{
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
std::size_t first=0,end=0;
int left=-1,right=-1;
};
struct FStampRuntime
{
const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr;
std::array<float,3> 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<std::int64_t>::max());
node.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
for(std::size_t offset=first;offset<end;++offset)
{
const auto& bound=StampSpatialBounds[StampSpatialOrder[offset]];
for(std::size_t axis=0;axis<3;++axis)
{
node.minimumSubvoxels[axis]=std::min(node.minimumSubvoxels[axis],
bound.minimumSubvoxels[axis]);
node.maximumSubvoxels[axis]=std::max(node.maximumSubvoxels[axis],
bound.maximumSubvoxels[axis]);
}
}
const int nodeIndex=static_cast<int>(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<long double>(node.maximumSubvoxels[axis])
-node.minimumSubvoxels[axis]
>static_cast<long double>(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<long double>(
left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis];
const long double rightCenter=static_cast<long double>(
right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis];
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
});
StampSpatialNodes[nodeIndex].left=BuildStampSpatialNode(first,middle);
StampSpatialNodes[nodeIndex].right=BuildStampSpatialNode(middle,end);
}
return nodeIndex;
}
void QueryStampSpatialNode(int nodeIndex,
const std::array<std::int64_t,3>& minimum,
const std::array<std::int64_t,3>& maximum,
std::vector<std::uint32_t>& result) const
{
if(nodeIndex<0)return;
const auto& node=StampSpatialNodes[static_cast<std::size_t>(nodeIndex)];
for(std::size_t axis=0;axis<3;++axis)
if(node.maximumSubvoxels[axis]<minimum[axis]
||node.minimumSubvoxels[axis]>maximum[axis])return;
if(node.left<0)
{
for(std::size_t offset=node.first;offset<node.end;++offset)
{
const auto index=StampSpatialOrder[offset];
const auto& bound=StampSpatialBounds[index];bool overlap=true;
for(std::size_t axis=0;axis<3;++axis)
overlap=overlap&&bound.minimumSubvoxels[axis]<=maximum[axis]
&&bound.maximumSubvoxels[axis]>=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::int64_t>(std::floor(
static_cast<long double>(stamp.boundsMinimumMicrometres[axis])*scale));
bound.maximumSubvoxels[axis]=static_cast<std::int64_t>(std::ceil(
static_cast<long double>(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<float>(
static_cast<double>(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<float>(static_cast<double>(stamp.halfExtentsMicrometres[0])
/Owner->CoreVoxelSizeMicrons),
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[1])
/Owner->CoreVoxelSizeMicrons),
static_cast<float>(static_cast<double>(stamp.halfExtentsMicrometres[2])
/Owner->CoreVoxelSizeMicrons)};
(void)xAxis;(void)yAxis;
runtime.smoothWidthVoxels=static_cast<float>(
static_cast<double>(stamp.smoothWidthMicrometres)/Owner->CoreVoxelSizeMicrons);
runtime.irregularityAmplitudeVoxels=static_cast<float>(
static_cast<double>(stamp.irregularityAmplitudeMicrometres)
/Owner->CoreVoxelSizeMicrons);
runtime.irregularityWavelengthVoxels=static_cast<float>(
static_cast<double>(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<StampSpatialOrder.size();++i)
StampSpatialOrder[i]=static_cast<std::uint32_t>(i);
if(!StampSpatialOrder.empty())BuildStampSpatialNode(0,StampSpatialOrder.size());
}
std::vector<std::uint32_t> BuildStampTileList(
const std::array<std::int64_t,3>& minimum,
const std::array<std::int64_t,3>& maximum) const
{
std::vector<std::uint32_t> 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<result.size();++i)
result[i]=static_cast<std::uint32_t>(i);
return result;
}
QueryStampSpatialNode(0,minimum,maximum,result);
return result;
}
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> 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<std::int64_t,3> minimum={
static_cast<std::int64_t>(Origin.X-Pad)*Q,
static_cast<std::int64_t>(Origin.Y-Pad)*Q,
static_cast<std::int64_t>(BMin)*Q};
const std::array<std::int64_t,3> maximum={
static_cast<std::int64_t>(Origin.X+(Cells+1)*Step)*Q,
static_cast<std::int64_t>(Origin.Y+(Cells+1)*Step)*Q,
static_cast<std::int64_t>(BMax)*Q};
if(!BuildStampTileList(minimum,maximum).empty())return false;
}
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)/LayoutWidth);
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(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<std::int64_t,3> minimum={
static_cast<std::int64_t>(BMin[0])*Q,
static_cast<std::int64_t>(BMin[1])*Q,
static_cast<std::int64_t>(BMin[2])*Q};
const std::array<std::int64_t,3> maximum={
static_cast<std::int64_t>(BMax[0])*Q,
static_cast<std::int64_t>(BMax[1])*Q,
static_cast<std::int64_t>(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<std::uint64_t>(x)));
hash=StampMix64(hash^StampMix64(static_cast<std::uint64_t>(y)));
hash=StampMix64(hash^StampMix64(static_cast<std::uint64_t>(z)));
const double unit=static_cast<double>(hash>>11)
/static_cast<double>((std::uint64_t{1}<<53)-1);
return static_cast<float>(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<double>(x)/wavelength;
const double sy=static_cast<double>(y)/wavelength;
const double sz=static_cast<double>(z)/wavelength;
const std::int64_t ix=static_cast<std::int64_t>(std::floor(sx));
const std::int64_t iy=static_cast<std::int64_t>(std::floor(sy));
const std::int64_t iz=static_cast<std::int64_t>(std::floor(sz));
const auto smooth=[](float t){return t*t*(3.0f-2.0f*t);};
const float tx=smooth(static_cast<float>(sx-ix));
const float ty=smooth(static_cast<float>(sy-iy));
const float tz=smooth(static_cast<float>(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<float,3>& radius)
{
const float minimum=std::min({radius[0],radius[1],radius[2]});
if(!(minimum>0.0f))return std::numeric_limits<float>::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<float,3>& 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<float,3>& 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<float,3>& 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<float>::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<double,3> 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.boundsMinimumMicrometres[axis]
||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<StampRuntime.size();++index)
apply(static_cast<std::uint32_t>(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<FStampSpatialBound> StampSpatialBounds;
std::vector<FStampSpatialNode> StampSpatialNodes;
std::vector<std::uint32_t> StampSpatialOrder;
std::vector<FStampRuntime> StampRuntime;
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
std::vector<FPassageTileFeature> PassageTileFeatures;
std::vector<std::uint32_t> PassageFeatureOrder;
+27
View File
@@ -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<double>(minimum.X),
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
const double tileMaximum[3]={static_cast<double>(maximum.X),
static_cast<double>(maximum.Y),static_cast<double>(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<double>(stamp.boundsMinimumMicrometres[axis])
/coreVoxelSizeMicrons;
const double high=static_cast<double>(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;
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -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](</home/jahni/VoxelM/Plugins/VoxelForge/GDD.md:209>) The current edit layer changes density only, and its documentation leaves saving and loading to the game layer. [VoxelDiffLayer.h](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelDiffLayer.h:1>)
## 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](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:213>) · [VoxelWorld.cpp](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:697>) 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](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14453>) 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](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14659>) · [VoxelForgeCore.h](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:1396>) 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](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research6/REPORT.md:24>)
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](</home/jahni/VoxelM/Plugins/VoxelForge/GDD.md:209>)
- 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?
@@ -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<VoxelForgeCore::ResolvedLayoutStampRequestV1>& 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<double>(mouth[0])-prior[0];
const double dy=static_cast<double>(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<double>(layout.coreVoxelSizeMicrometres)
/WorldAssemblySubvoxelsPerVoxel;
std::array<std::int64_t,3> arrivalMicrometres{};
for(std::size_t axis=0;axis<3;++axis)
arrivalMicrometres[axis]=static_cast<std::int64_t>(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::int64_t>(std::llround(sideX*28000000.0)),
arrivalMicrometres[1]+static_cast<std::int64_t>(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::int64_t>(std::llround(entranceDirectionX*6500000.0)),
chamber.anchorPositionMicrometres[1]
+static_cast<std::int64_t>(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<VFJson::Value::Object>(&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<int>(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<VoxelForgeCore::ResolvedLayoutStampRequestV1> 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<unsigned>(stamp.kind),
stamp.positionMicrometres[0]/1000000.0,
stamp.positionMicrometres[1]/1000000.0,
stamp.positionMicrometres[2]/1000000.0,
static_cast<long long>(stamp.boundsMinimumMicrometres[0]),
static_cast<long long>(stamp.boundsMaximumMicrometres[0]),
static_cast<long long>(stamp.boundsMinimumMicrometres[1]),
static_cast<long long>(stamp.boundsMaximumMicrometres[1]),
static_cast<long long>(stamp.boundsMinimumMicrometres[2]),
static_cast<long long>(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<unsigned long long>(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)