WorldAssemblySurfaceProfileV1 on the native SurfaceWorld recipe and plan; version 0 'natural' is omitted from serialized identity, so the owner world is unchanged. periodicHills (seeded cosine hills, plan-time grade <= 15 deg), mirroredRelief (cap mirrors the floor by lambda, clearance checked) and flatField share one profile function for points, lattice classification and exact cosine-extrema intervals (0 interval misses in brute-force audits). Style-override fixtures with planned passages. Cold whole view about 1.33 s for each profile vs 1.74 s natural SW (Clang). Owner hashes identical to L52; fast/medium/full PASS. Research R22: SurfaceWorld critical path to about 1 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2565 lines
147 KiB
C++
2565 lines
147 KiB
C++
#pragma once
|
|
|
|
#include "Json.h"
|
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
#include <sstream>
|
|
|
|
namespace VoxelForgeCore
|
|
{
|
|
namespace
|
|
{
|
|
constexpr std::int64_t TransitionQ16 = 1ll << 16;
|
|
|
|
void TransitionHashWord(std::uint64_t& hash, std::uint64_t word)
|
|
{
|
|
for (int byte = 0; byte < 8; ++byte)
|
|
{
|
|
hash ^= static_cast<std::uint8_t>(word >> (byte * 8));
|
|
hash *= 1099511628211ull;
|
|
}
|
|
}
|
|
|
|
std::uint64_t TransitionHash(std::int32_t seed, std::int32_t interfaceIndex,
|
|
std::int64_t x, std::int64_t y, std::int64_t z, std::uint32_t salt)
|
|
{
|
|
std::uint64_t hash = 14695981039346656037ull;
|
|
TransitionHashWord(hash, static_cast<std::uint32_t>(seed));
|
|
TransitionHashWord(hash, static_cast<std::uint32_t>(interfaceIndex));
|
|
TransitionHashWord(hash, static_cast<std::uint64_t>(x));
|
|
TransitionHashWord(hash, static_cast<std::uint64_t>(y));
|
|
TransitionHashWord(hash, static_cast<std::uint64_t>(z));
|
|
TransitionHashWord(hash, salt);
|
|
hash ^= hash >> 30; hash *= 0xbf58476d1ce4e5b9ull;
|
|
hash ^= hash >> 27; hash *= 0x94d049bb133111ebull;
|
|
hash ^= hash >> 31;
|
|
return hash;
|
|
}
|
|
|
|
std::int64_t TransitionNoiseValue(std::int32_t seed, std::int32_t interfaceIndex,
|
|
std::int64_t x, std::int64_t y, std::int64_t z, std::uint32_t salt)
|
|
{
|
|
const std::uint32_t bits = static_cast<std::uint32_t>(
|
|
TransitionHash(seed, interfaceIndex, x, y, z, salt) >> 32);
|
|
return static_cast<std::int64_t>(bits >> 15) - TransitionQ16;
|
|
}
|
|
|
|
std::int64_t TransitionFloorDiv(std::int64_t value, std::int64_t divisor)
|
|
{
|
|
const std::int64_t quotient = value / divisor;
|
|
const std::int64_t remainder = value % divisor;
|
|
return quotient - (remainder < 0 ? 1 : 0);
|
|
}
|
|
|
|
std::int64_t TransitionFade(std::int64_t valueQ16)
|
|
{
|
|
valueQ16 = std::clamp(valueQ16, std::int64_t{0}, TransitionQ16);
|
|
const std::int64_t square = (valueQ16 * valueQ16) / TransitionQ16;
|
|
return (square * (3 * TransitionQ16 - 2 * valueQ16)) / TransitionQ16;
|
|
}
|
|
|
|
std::int64_t TransitionLerp(std::int64_t a, std::int64_t b, std::int64_t tQ16)
|
|
{ return a + ((b - a) * tQ16) / TransitionQ16; }
|
|
|
|
std::int64_t TransitionValueNoise2D(std::int32_t seed, std::int32_t interfaceIndex,
|
|
std::int64_t x, std::int64_t y, std::int64_t cell, std::uint32_t salt)
|
|
{
|
|
const std::int64_t ix = TransitionFloorDiv(x, cell), iy = TransitionFloorDiv(y, cell);
|
|
const std::int64_t tx = TransitionFade(((x - ix * cell) * TransitionQ16) / cell);
|
|
const std::int64_t ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell);
|
|
std::uint64_t prefix=14695981039346656037ull;
|
|
TransitionHashWord(prefix,static_cast<std::uint32_t>(seed));
|
|
TransitionHashWord(prefix,static_cast<std::uint32_t>(interfaceIndex));
|
|
const auto at = [&](std::int64_t dx, std::int64_t dy)
|
|
{
|
|
std::uint64_t hash=prefix;
|
|
TransitionHashWord(hash,static_cast<std::uint64_t>(ix+dx));
|
|
TransitionHashWord(hash,static_cast<std::uint64_t>(iy+dy));
|
|
TransitionHashWord(hash,0);
|
|
TransitionHashWord(hash,salt);
|
|
hash ^= hash >> 30; hash *= 0xbf58476d1ce4e5b9ull;
|
|
hash ^= hash >> 27; hash *= 0x94d049bb133111ebull;
|
|
hash ^= hash >> 31;
|
|
const std::uint32_t bits=static_cast<std::uint32_t>(hash>>32);
|
|
return static_cast<std::int64_t>(bits>>15)-TransitionQ16;
|
|
};
|
|
return TransitionLerp(TransitionLerp(at(0, 0), at(1, 0), tx),
|
|
TransitionLerp(at(0, 1), at(1, 1), tx), ty);
|
|
}
|
|
|
|
std::int64_t TransitionValueNoise3D(std::int32_t seed, std::int32_t interfaceIndex,
|
|
std::int64_t x, std::int64_t y, std::int64_t z, std::int64_t cell,
|
|
std::uint32_t salt)
|
|
{
|
|
const std::int64_t ix = TransitionFloorDiv(x, cell), iy = TransitionFloorDiv(y, cell);
|
|
const std::int64_t iz = TransitionFloorDiv(z, cell);
|
|
const std::int64_t tx = TransitionFade(((x - ix * cell) * TransitionQ16) / cell);
|
|
const std::int64_t ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell);
|
|
const std::int64_t tz = TransitionFade(((z - iz * cell) * TransitionQ16) / cell);
|
|
const auto at = [&](std::int64_t dx, std::int64_t dy, std::int64_t dz)
|
|
{ return TransitionNoiseValue(seed, interfaceIndex, ix + dx, iy + dy, iz + dz, salt); };
|
|
const std::int64_t z0 = TransitionLerp(
|
|
TransitionLerp(at(0, 0, 0), at(1, 0, 0), tx),
|
|
TransitionLerp(at(0, 1, 0), at(1, 1, 0), tx), ty);
|
|
const std::int64_t z1 = TransitionLerp(
|
|
TransitionLerp(at(0, 0, 1), at(1, 0, 1), tx),
|
|
TransitionLerp(at(0, 1, 1), at(1, 1, 1), tx), ty);
|
|
return TransitionLerp(z0, z1, tz);
|
|
}
|
|
|
|
std::int64_t LayoutTransitionLimit(const ResolvedLayoutStratumV2& stratum)
|
|
{
|
|
if (stratum.transitionMode == "Hard" || stratum.transitionFallsBackToHard)
|
|
return 0;
|
|
return std::max<std::int64_t>(0, stratum.transitionWidthMicrometres);
|
|
}
|
|
|
|
std::string LayoutEscape(std::string_view value)
|
|
{
|
|
std::string out="\"";
|
|
for(unsigned char c:value)
|
|
{
|
|
switch(c)
|
|
{
|
|
case '"':out+="\\\"";break;
|
|
case '\\':out+="\\\\";break;
|
|
case '\b':out+="\\b";break;
|
|
case '\f':out+="\\f";break;
|
|
case '\n':out+="\\n";break;
|
|
case '\r':out+="\\r";break;
|
|
case '\t':out+="\\t";break;
|
|
default:if(c<0x20){char b[7];std::snprintf(b,sizeof(b),"\\u%04x",c);out+=b;}
|
|
else out+=static_cast<char>(c);
|
|
}
|
|
}
|
|
out+='"';return out;
|
|
}
|
|
std::uint64_t LayoutHash(std::string_view value)
|
|
{
|
|
std::uint64_t hash=14695981039346656037ull;
|
|
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;
|
|
}
|
|
double LayoutSegmentDistance(const std::array<double,3>& p0,
|
|
const std::array<double,3>& p1,const std::array<double,3>& q0,
|
|
const std::array<double,3>& q1)
|
|
{
|
|
std::array<double,3> u{},v{},w{};
|
|
double a=0.0,b=0.0,c=0.0,d=0.0,e=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
u[axis]=p1[axis]-p0[axis];v[axis]=q1[axis]-q0[axis];
|
|
w[axis]=p0[axis]-q0[axis];
|
|
a+=u[axis]*u[axis];b+=u[axis]*v[axis];
|
|
c+=v[axis]*v[axis];d+=u[axis]*w[axis];e+=v[axis]*w[axis];
|
|
}
|
|
const double denominator=a*c-b*b;
|
|
double sN=0.0,sD=denominator,tN=0.0,tD=denominator;
|
|
if(denominator<1.0e-18){sN=0.0;sD=1.0;tN=e;tD=c;}
|
|
else
|
|
{
|
|
sN=b*e-c*d;tN=a*e-b*d;
|
|
if(sN<0.0){sN=0.0;tN=e;tD=c;}
|
|
else if(sN>sD){sN=sD;tN=e+b;tD=c;}
|
|
}
|
|
if(tN<0.0)
|
|
{
|
|
tN=0.0;
|
|
if(-d<0.0)sN=0.0;
|
|
else if(-d>a)sN=sD;
|
|
else{sN=-d;sD=a;}
|
|
}
|
|
else if(tN>tD)
|
|
{
|
|
tN=tD;
|
|
if(-d+b<0.0)sN=0.0;
|
|
else if(-d+b>a)sN=sD;
|
|
else{sN=-d+b;sD=a;}
|
|
}
|
|
const double s=std::abs(sN)<1.0e-18?0.0:sN/sD;
|
|
const double t=std::abs(tN)<1.0e-18?0.0:tN/tD;
|
|
double distanceSquared=0.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double delta=w[axis]+s*u[axis]-t*v[axis];
|
|
distanceSquared+=delta*delta;
|
|
}
|
|
return std::sqrt(std::max(0.0,distanceSquared));
|
|
}
|
|
std::array<double,3> LayoutOrePointUm(const std::array<std::int64_t,3>& point)
|
|
{return {static_cast<double>(point[0]),static_cast<double>(point[1]),
|
|
static_cast<double>(point[2])};}
|
|
bool LayoutOreVeinSafe(const ResolvedLayoutSnapshotV3& layout,
|
|
const ResolvedLayoutOreVeinV1& vein,std::string* reason=nullptr)
|
|
{
|
|
const auto fail=[&](const char* text)
|
|
{if(reason)*reason=text;return false;};
|
|
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
|
[&](const auto& value){return value.strateIndex==vein.strateIndex;});
|
|
if(strate==layout.strates.end()||!strate->content.presentInCurrentField)
|
|
return fail("vein strate has no current content band");
|
|
if(vein.boundsMinimumMicrometres[2]<=strate->content.bottomMicrometres
|
|
||vein.boundsMaximumMicrometres[2]>=strate->content.topMicrometres)
|
|
return fail("vein bounds touch or cross the ceiling/bedrock content faces");
|
|
for(const auto* band:{&strate->ceiling,&strate->bedrock})
|
|
if(band->presentInCurrentField
|
|
&&vein.boundsMinimumMicrometres[2]<band->topMicrometres
|
|
&&vein.boundsMaximumMicrometres[2]>band->bottomMicrometres)
|
|
return fail("vein bounds overlap a ceiling or bedrock band");
|
|
const auto veinA=LayoutOrePointUm(vein.endpointsMicrometres[0]);
|
|
const auto veinB=LayoutOrePointUm(vein.endpointsMicrometres[1]);
|
|
const double coreRadius=static_cast<double>(vein.radiusMicrometres
|
|
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres);
|
|
const double coreMicrometres=std::max<std::uint32_t>(1,layout.coreVoxelSizeMicrometres);
|
|
constexpr double RouteClearanceMicrometres=100000.0;
|
|
const auto qToUm=[&](const std::array<std::int64_t,3>& q)
|
|
{return std::array<double,3>{q[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
|
q[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
|
q[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};};
|
|
for(const auto& passage:layout.passages)
|
|
{
|
|
if(passage.upperStrateIndex!=vein.strateIndex
|
|
&&passage.lowerStrateIndex!=vein.strateIndex)continue;
|
|
const auto upper=qToUm(passage.upperMouthSubvoxels);
|
|
const auto lower=qToUm(passage.lowerMouthSubvoxels);
|
|
const double mouthRadius=passage.mouthRadiusSubvoxels*coreMicrometres
|
|
/WorldAssemblySubvoxelsPerVoxel;
|
|
if(LayoutSegmentDistance(veinA,veinB,upper,upper)
|
|
<=coreRadius+mouthRadius+RouteClearanceMicrometres
|
|
||LayoutSegmentDistance(veinA,veinB,lower,lower)
|
|
<=coreRadius+mouthRadius+RouteClearanceMicrometres)
|
|
return fail("vein overlaps a clear passage landing");
|
|
for(std::size_t i=0;i+1<passage.controlPoints.size();++i)
|
|
{
|
|
const auto a=qToUm(passage.controlPoints[i].positionSubvoxels);
|
|
const auto b=qToUm(passage.controlPoints[i+1].positionSubvoxels);
|
|
const double radius=std::max(passage.controlPoints[i].radiusSubvoxels,
|
|
passage.controlPoints[i+1].radiusSubvoxels)*coreMicrometres
|
|
/WorldAssemblySubvoxelsPerVoxel;
|
|
if(LayoutSegmentDistance(veinA,veinB,a,b)
|
|
<=coreRadius+radius+RouteClearanceMicrometres)
|
|
return fail("vein enters a clear passage corridor");
|
|
}
|
|
}
|
|
std::vector<const ResolvedLayoutAnchorV2*> route;
|
|
for(const auto& anchor:strate->anchors)
|
|
{
|
|
if(anchor.values.size()<3)continue;
|
|
if(anchor.kind=="route_point")route.push_back(&anchor);
|
|
if(anchor.kind.find("landing")!=std::string::npos)
|
|
{
|
|
const std::array<double,3> landing={
|
|
anchor.values[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
|
anchor.values[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
|
anchor.values[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};
|
|
const double radius=anchor.values.size()>=4
|
|
?anchor.values[3]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel:3000000.0;
|
|
if(LayoutSegmentDistance(veinA,veinB,landing,landing)
|
|
<=coreRadius+radius+RouteClearanceMicrometres)
|
|
return fail("vein overlaps a resolved landing footprint");
|
|
}
|
|
}
|
|
for(std::size_t i=0;i+1<route.size();++i)
|
|
{
|
|
const auto qPoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
|
{return std::array<double,3>{anchor.values[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
|
anchor.values[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
|
anchor.values[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};};
|
|
const auto a=qPoint(*route[i]),b=qPoint(*route[i+1]);
|
|
const double routeRadius=std::max(route[i]->values.size()>=4?route[i]->values[3]:0,
|
|
route[i+1]->values.size()>=4?route[i+1]->values[3]:0)
|
|
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel;
|
|
if(LayoutSegmentDistance(veinA,veinB,a,b)
|
|
<=coreRadius+routeRadius+RouteClearanceMicrometres)
|
|
return fail("vein enters a clear strate route corridor");
|
|
}
|
|
for(const auto& room:layout.cavernRooms)
|
|
if(room.strateIndex==vein.strateIndex)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(vein.boundsMinimumMicrometres[axis]
|
|
<=room.boundsMaximumMicrometres[axis]
|
|
&&vein.boundsMaximumMicrometres[axis]
|
|
>=room.boundsMinimumMicrometres[axis])
|
|
return fail("vein overlaps a cavern landmark footprint");
|
|
for(const auto& edge:layout.cavernEdges)
|
|
{
|
|
if(edge.strateIndex!=vein.strateIndex)continue;
|
|
std::array<double,3> previous{};
|
|
const auto bezier=[&](double t)
|
|
{
|
|
const double one=1.0-t;
|
|
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
|
3.0*one*t*t,t*t*t};
|
|
std::array<double,3> point{};
|
|
for(std::size_t i=0;i<4;++i)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
point[axis]+=weights[i]*edge.controlPointsMicrometres[i][axis];
|
|
return point;
|
|
};
|
|
previous=bezier(0.0);
|
|
for(int step=1;step<=256;++step)
|
|
{
|
|
const auto next=bezier(static_cast<double>(step)/256.0);
|
|
if(LayoutSegmentDistance(veinA,veinB,previous,next)
|
|
<=coreRadius+edge.boreRadiusMicrometres+RouteClearanceMicrometres)
|
|
return fail("vein enters a landmark passage corridor");
|
|
previous=next;
|
|
}
|
|
}
|
|
for(const auto& stamp:layout.stamps)
|
|
{
|
|
if(stamp.strateIndex!=vein.strateIndex
|
|
||(stamp.kind!=ResolvedSdfStampKindV1::fillDais
|
|
&&stamp.kind!=ResolvedSdfStampKindV1::fillPillar))continue;
|
|
bool overlaps=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlaps=overlaps&&vein.boundsMinimumMicrometres[axis]
|
|
<=stamp.boundsMaximumMicrometres[axis]
|
|
&&vein.boundsMaximumMicrometres[axis]
|
|
>=stamp.boundsMinimumMicrometres[axis];
|
|
if(overlaps)return fail("vein overlaps a resolved landmark solid footprint");
|
|
}
|
|
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)
|
|
{
|
|
std::ostringstream out;out<<"{\"kind\":"<<LayoutEscape(band.kind)
|
|
<<",\"bottom_um\":"<<band.bottomMicrometres<<",\"top_um\":"<<band.topMicrometres
|
|
<<",\"present\":"<<(band.presentInCurrentField?"true":"false")
|
|
<<",\"diggable\":"<<(band.diggable?"true":"false")
|
|
<<",\"protected\":"<<(band.protectedBand?"true":"false")
|
|
<<",\"solid_density_q16\":"<<band.solidDensityQ16
|
|
<<",\"behaviour\":"<<LayoutEscape(band.behaviour)
|
|
<<",\"density_side\":"<<LayoutEscape(band.densitySide)
|
|
<<",\"cap_path\":"<<LayoutEscape(band.capPath)<<'}';return out.str();
|
|
}
|
|
ResolvedLayoutBandV2 ParseLayoutBand(const VFJson::Value& value)
|
|
{
|
|
ResolvedLayoutBandV2 b;b.kind=value.Get("kind").String();
|
|
b.bottomMicrometres=value.Get("bottom_um").Integer();b.topMicrometres=value.Get("top_um").Integer();
|
|
b.presentInCurrentField=value.Get("present").Boolean();b.diggable=value.Get("diggable").Boolean(true);
|
|
b.protectedBand=value.Get("protected").Boolean();b.solidDensityQ16=value.Get("solid_density_q16").Integer(65536);
|
|
b.behaviour=value.Get("behaviour").String();
|
|
b.densitySide=value.Get("density_side").String();b.capPath=value.Get("cap_path").String();return b;
|
|
}
|
|
std::string LayoutBodyJson(const ResolvedLayoutSnapshotV3& s)
|
|
{
|
|
std::ostringstream out;out<<"{\"schema_version\":"<<s.schemaVersion
|
|
<<",\"world_identity\":"<<LayoutEscape(s.worldIdentity)
|
|
<<",\"owner_voxel_size_um\":"<<s.ownerVoxelSizeMicrometres
|
|
<<",\"core_voxel_size_um\":"<<s.coreVoxelSizeMicrometres
|
|
<<",\"strate_layout_chunk_size_um\":"<<s.strateLayoutChunkSizeMicrometres
|
|
<<",\"strate_layout_chunk_width_subvoxels\":"<<s.strateLayoutChunkWidthSubvoxels
|
|
<<",\"strates\":[";
|
|
for(std::size_t i=0;i<s.strates.size();++i)
|
|
{
|
|
if(i)out<<',';const auto& x=s.strates[i];
|
|
out<<"{\"stable_id\":"<<LayoutEscape(x.stableId)<<",\"order\":"<<x.order
|
|
<<",\"strate_index\":"<<x.strateIndex<<",\"generator_kind\":"<<LayoutEscape(x.generatorKind)
|
|
<<",\"recipe_id\":"<<LayoutEscape(x.recipeId)<<",\"recipe_version\":"<<x.recipeVersion
|
|
<<",\"interior_bottom_um\":"<<x.interiorBottomMicrometres
|
|
<<",\"interior_top_um\":"<<x.interiorTopMicrometres
|
|
<<",\"voxel_size_um\":"<<x.voxelSizeMicrometres
|
|
<<",\"lattice_origin_um\":["<<x.latticeOriginMicrometres[0]<<','
|
|
<<x.latticeOriginMicrometres[1]<<','<<x.latticeOriginMicrometres[2]<<']'
|
|
<<",\"ceiling\":"<<LayoutBandJson(x.ceiling)
|
|
<<",\"content\":"<<LayoutBandJson(x.content)
|
|
<<",\"bedrock\":"<<LayoutBandJson(x.bedrock)
|
|
<<",\"transition_mode\":"<<LayoutEscape(x.transitionMode)
|
|
<<",\"transition_blend_chunks\":"<<x.transitionBlendChunks
|
|
<<",\"transition_width_um\":"<<x.transitionWidthMicrometres
|
|
<<",\"transition_warp_amplitude_um\":"<<x.transitionWarpAmplitudeMicrometres
|
|
<<",\"transition_interpenetration_um\":"<<x.transitionInterpenetrationMicrometres
|
|
<<",\"transition_honoured_in_core\":"<<(x.transitionHonouredInCore?"true":"false")
|
|
<<",\"transition_falls_back_to_hard\":"<<(x.transitionFallsBackToHard?"true":"false")
|
|
<<",\"transition_dispatch_path\":"<<LayoutEscape(x.transitionDispatchPath)
|
|
<<",\"bound_dispatch_path\":"<<LayoutEscape(x.boundDispatchPath)
|
|
<<",\"query_dispatch_path\":"<<LayoutEscape(x.queryDispatchPath)
|
|
<<",\"manager_lookup\":"<<LayoutEscape(x.managerLookup)
|
|
<<",\"manager_chunk_width_um\":"<<x.managerChunkWidthMicrometres
|
|
<<",\"dispatch_chunk_width_um\":"<<x.dispatchChunkWidthMicrometres
|
|
<<",\"anchors\":[";
|
|
for(std::size_t a=0;a<x.anchors.size();++a)
|
|
{
|
|
if(a)out<<',';const auto& anchor=x.anchors[a];
|
|
out<<"{\"kind\":"<<LayoutEscape(anchor.kind)<<",\"stable_id\":"
|
|
<<LayoutEscape(anchor.stableId)<<",\"units\":"<<LayoutEscape(anchor.units)<<",\"values\":[";
|
|
for(std::size_t v=0;v<anchor.values.size();++v){if(v)out<<',';out<<anchor.values[v];}
|
|
out<<"]}";
|
|
}
|
|
out<<"]}";
|
|
}
|
|
out<<"],\"passages\":[";
|
|
for(std::size_t i=0;i<s.passages.size();++i)
|
|
{
|
|
if(i)out<<',';const auto& p=s.passages[i];
|
|
out<<"{\"stable_id\":"<<LayoutEscape(p.stableId)
|
|
<<",\"upper_strate_index\":"<<p.upperStrateIndex
|
|
<<",\"lower_strate_index\":"<<p.lowerStrateIndex
|
|
<<",\"style\":"<<p.style
|
|
<<",\"placement_radius_q\":"<<p.placementRadiusSubvoxels
|
|
<<",\"upper_mouth_q\":["<<p.upperMouthSubvoxels[0]<<','
|
|
<<p.upperMouthSubvoxels[1]<<','<<p.upperMouthSubvoxels[2]
|
|
<<"],\"lower_mouth_q\":["<<p.lowerMouthSubvoxels[0]<<','
|
|
<<p.lowerMouthSubvoxels[1]<<','<<p.lowerMouthSubvoxels[2]
|
|
<<"],\"mouth_radius_q\":"<<p.mouthRadiusSubvoxels
|
|
<<",\"mid_radius_q\":"<<p.midRadiusSubvoxels<<",\"control_points\":[";
|
|
for(std::size_t j=0;j<p.controlPoints.size();++j)
|
|
{
|
|
if(j)out<<',';const auto& point=p.controlPoints[j];
|
|
out<<"{\"position_q\":["<<point.positionSubvoxels[0]<<','
|
|
<<point.positionSubvoxels[1]<<','<<point.positionSubvoxels[2]
|
|
<<"],\"radius_q\":"<<point.radiusSubvoxels<<'}';
|
|
}
|
|
out<<"]}";
|
|
}
|
|
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<<']';
|
|
}
|
|
if(!s.cavernRooms.empty()||!s.cavernEdges.empty())
|
|
{
|
|
out<<",\"cavern_graph_vocabulary_version\":"
|
|
<<s.cavernGraphVocabularyVersion<<",\"cavern_rooms\":[";
|
|
for(std::size_t i=0;i<s.cavernRooms.size();++i)
|
|
{
|
|
if(i)out<<',';const auto& room=s.cavernRooms[i];
|
|
out<<"{\"stable_id\":"<<LayoutEscape(room.stableId)
|
|
<<",\"strate_index\":"<<room.strateIndex
|
|
<<",\"center_um\":["<<room.centerMicrometres[0]<<','
|
|
<<room.centerMicrometres[1]<<','<<room.centerMicrometres[2]
|
|
<<"],\"half_extents_um\":["<<room.halfExtentsMicrometres[0]<<','
|
|
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
|
|
<<"],\"floor_um\":"<<room.floorMicrometres
|
|
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres
|
|
<<",\"irregularity_amplitude_um\":"<<room.irregularityAmplitudeMicrometres
|
|
<<",\"irregularity_wavelength_um\":"<<room.irregularityWavelengthMicrometres
|
|
<<",\"irregularity_seed\":"<<room.irregularitySeed
|
|
<<",\"bounds_min_um\":["<<room.boundsMinimumMicrometres[0]<<','
|
|
<<room.boundsMinimumMicrometres[1]<<','<<room.boundsMinimumMicrometres[2]
|
|
<<"],\"bounds_max_um\":["<<room.boundsMaximumMicrometres[0]<<','
|
|
<<room.boundsMaximumMicrometres[1]<<','<<room.boundsMaximumMicrometres[2]<<"]}";
|
|
}
|
|
out<<"],\"cavern_edges\":[";
|
|
for(std::size_t i=0;i<s.cavernEdges.size();++i)
|
|
{
|
|
if(i)out<<',';const auto& edge=s.cavernEdges[i];
|
|
out<<"{\"stable_id\":"<<LayoutEscape(edge.stableId)
|
|
<<",\"start_anchor_stable_id\":"<<LayoutEscape(edge.startAnchorStableId)
|
|
<<",\"end_room_stable_id\":"<<LayoutEscape(edge.endRoomStableId)
|
|
<<",\"strate_index\":"<<edge.strateIndex<<",\"control_points_um\":[";
|
|
for(std::size_t point=0;point<edge.controlPointsMicrometres.size();++point)
|
|
{
|
|
if(point)out<<',';const auto& p=edge.controlPointsMicrometres[point];
|
|
out<<'['<<p[0]<<','<<p[1]<<','<<p[2]<<']';
|
|
}
|
|
out<<"],\"bore_radius_um\":"<<edge.boreRadiusMicrometres
|
|
<<",\"bore_height_um\":"<<edge.boreHeightMicrometres
|
|
<<",\"floor_support_depth_um\":"<<edge.floorSupportDepthMicrometres
|
|
<<",\"smooth_width_um\":"<<edge.smoothWidthMicrometres
|
|
<<",\"irregularity_amplitude_um\":"<<edge.irregularityAmplitudeMicrometres
|
|
<<",\"irregularity_wavelength_um\":"<<edge.irregularityWavelengthMicrometres
|
|
<<",\"irregularity_seed\":"<<edge.irregularitySeed
|
|
<<",\"bounds_min_um\":["<<edge.boundsMinimumMicrometres[0]<<','
|
|
<<edge.boundsMinimumMicrometres[1]<<','<<edge.boundsMinimumMicrometres[2]
|
|
<<"],\"bounds_max_um\":["<<edge.boundsMaximumMicrometres[0]<<','
|
|
<<edge.boundsMaximumMicrometres[1]<<','<<edge.boundsMaximumMicrometres[2]<<"]}";
|
|
}
|
|
out<<']';
|
|
}
|
|
if(!s.oreVeins.empty())
|
|
{
|
|
out<<",\"ore_vein_vocabulary_version\":"<<s.oreVeinVocabularyVersion
|
|
<<",\"ore_veins\":[";
|
|
for(std::size_t i=0;i<s.oreVeins.size();++i)
|
|
{
|
|
if(i)out<<',';const auto& vein=s.oreVeins[i];
|
|
out<<"{\"stable_id\":"<<LayoutEscape(vein.stableId)
|
|
<<",\"strate_index\":"<<vein.strateIndex
|
|
<<",\"resource_material_id\":"<<vein.resourceMaterialId
|
|
<<",\"endpoints_um\":[["<<vein.endpointsMicrometres[0][0]<<','
|
|
<<vein.endpointsMicrometres[0][1]<<','<<vein.endpointsMicrometres[0][2]
|
|
<<"],["<<vein.endpointsMicrometres[1][0]<<','
|
|
<<vein.endpointsMicrometres[1][1]<<','<<vein.endpointsMicrometres[1][2]
|
|
<<"]],\"bounds_min_um\":["<<vein.boundsMinimumMicrometres[0]<<','
|
|
<<vein.boundsMinimumMicrometres[1]<<','<<vein.boundsMinimumMicrometres[2]
|
|
<<"],\"bounds_max_um\":["<<vein.boundsMaximumMicrometres[0]<<','
|
|
<<vein.boundsMaximumMicrometres[1]<<','<<vein.boundsMaximumMicrometres[2]
|
|
<<"],\"radius_um\":"<<vein.radiusMicrometres
|
|
<<",\"irregularity_amplitude_um\":"<<vein.irregularityAmplitudeMicrometres
|
|
<<",\"irregularity_wavelength_um\":"<<vein.irregularityWavelengthMicrometres
|
|
<<",\"blend_width_um\":"<<vein.blendWidthMicrometres
|
|
<<",\"irregularity_seed\":"<<vein.irregularitySeed<<'}';
|
|
}
|
|
out<<']';
|
|
}
|
|
out<<'}';return out.str();
|
|
}
|
|
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
|
{return obj.Get(key).Integer();}
|
|
std::string LayoutString(const VFJson::Value& obj,std::string_view key)
|
|
{return obj.Get(key).String();}
|
|
bool LayoutBool(const VFJson::Value& obj,std::string_view key,bool fallback=false)
|
|
{return obj.Get(key).Boolean(fallback);}
|
|
std::uint64_t PassageHash(std::uint64_t value)
|
|
{
|
|
value+=0x9e3779b97f4a7c15ull;
|
|
value=(value^(value>>30))*0xbf58476d1ce4e5b9ull;
|
|
value=(value^(value>>27))*0x94d049bb133111ebull;
|
|
return value^(value>>31);
|
|
}
|
|
std::uint64_t PassageSeed(std::int32_t seed,std::int32_t upper,
|
|
std::int32_t lower,std::uint32_t connection,
|
|
std::uint32_t salt)
|
|
{
|
|
std::uint64_t h=PassageHash(static_cast<std::uint32_t>(seed));
|
|
h=PassageHash(h^static_cast<std::uint32_t>(upper));
|
|
h=PassageHash(h^static_cast<std::uint32_t>(lower));
|
|
h=PassageHash(h^connection);return PassageHash(h^salt);
|
|
}
|
|
std::uint64_t PassageIsqrt(std::uint64_t n)
|
|
{
|
|
std::uint64_t x=0,bit=std::uint64_t{1}<<62;
|
|
while(bit>n)bit>>=2;
|
|
while(bit)
|
|
{
|
|
if(n>=x+bit){n-=x+bit;x=(x>>1)+bit;}
|
|
else x>>=1;
|
|
bit>>=2;
|
|
}
|
|
return x;
|
|
}
|
|
std::int64_t PassageRoundRatio(std::int64_t value,std::int64_t numerator,
|
|
std::int64_t denominator)
|
|
{
|
|
if(denominator<=0)return 0;
|
|
const bool negative=value<0;
|
|
const std::uint64_t magnitude=static_cast<std::uint64_t>(negative?-value:value);
|
|
const std::uint64_t product=magnitude*static_cast<std::uint64_t>(numerator);
|
|
const std::uint64_t rounded=(product+static_cast<std::uint64_t>(denominator)/2)
|
|
/static_cast<std::uint64_t>(denominator);
|
|
return negative?-static_cast<std::int64_t>(rounded):static_cast<std::int64_t>(rounded);
|
|
}
|
|
std::array<std::int64_t,2> PassageRandomXY(std::uint64_t seed,
|
|
std::int64_t inner,std::int64_t outer)
|
|
{
|
|
if(outer<=0)return {};
|
|
inner=std::clamp(inner,std::int64_t{0},outer);
|
|
const std::uint64_t span=static_cast<std::uint64_t>(outer)*2+1;
|
|
for(std::uint32_t attempt=0;attempt<64;++attempt)
|
|
{
|
|
const std::uint64_t hx=PassageHash(seed+2ull*attempt);
|
|
const std::uint64_t hy=PassageHash(seed+2ull*attempt+1);
|
|
const std::int64_t x=static_cast<std::int64_t>(hx%span)-outer;
|
|
const std::int64_t y=static_cast<std::int64_t>(hy%span)-outer;
|
|
const std::uint64_t radiusSquared=static_cast<std::uint64_t>(x*x+y*y);
|
|
if(radiusSquared<=static_cast<std::uint64_t>(outer*outer)
|
|
&&radiusSquared>=static_cast<std::uint64_t>(inner*inner))return {x,y};
|
|
}
|
|
return {outer,0};
|
|
}
|
|
}
|
|
|
|
ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1(
|
|
const ResolvedLayoutStratumV2& upper, const ResolvedLayoutStratumV2& lower,
|
|
std::int32_t worldSeed, std::int64_t layoutChunkSizeMicrometres,
|
|
std::int64_t xMicrometres, std::int64_t yMicrometres)
|
|
{
|
|
ResolvedLayoutTransitionBandsV1 result;
|
|
const std::int64_t seam = upper.bedrock.bottomMicrometres;
|
|
const std::int64_t upperThickness = upper.bedrock.topMicrometres - seam;
|
|
const std::int64_t lowerThickness = lower.ceiling.topMicrometres
|
|
- lower.ceiling.bottomMicrometres;
|
|
result.upperBedrockBottomMicrometres = seam;
|
|
result.upperBedrockTopMicrometres = seam + std::max<std::int64_t>(0, upperThickness);
|
|
result.lowerCeilingBottomMicrometres = seam - std::max<std::int64_t>(0, lowerThickness);
|
|
result.lowerCeilingTopMicrometres = seam;
|
|
|
|
const std::int64_t width = upper.transitionFallsBackToHard
|
|
? 0 : std::max<std::int64_t>(0, upper.transitionWidthMicrometres);
|
|
if (upper.transitionMode == "Hard" || width == 0
|
|
|| (upper.transitionMode != "Gradient" && upper.transitionMode != "Interleaved")
|
|
|| !upper.bedrock.presentInCurrentField || !lower.ceiling.presentInCurrentField
|
|
|| upperThickness <= 0 || lowerThickness <= 0)
|
|
return result;
|
|
|
|
if (width <= 0) return result;
|
|
|
|
std::int64_t noise = 0;
|
|
if (upper.transitionMode == "Gradient")
|
|
{
|
|
const std::int64_t wavelength = std::max<std::int64_t>(
|
|
1000000, std::max<std::int64_t>(1, layoutChunkSizeMicrometres) * 4);
|
|
noise = TransitionValueNoise2D(worldSeed, upper.strateIndex,
|
|
xMicrometres, yMicrometres, wavelength, 0x47524144u);
|
|
}
|
|
else
|
|
{
|
|
// A face crossing wider than the sum of the authored bands would separate
|
|
// their far edges and leave an air slab. Limit the physical crossing to the
|
|
// smaller of the declared transition width and the combined band thickness.
|
|
const std::int64_t maximumContinuousCrossing=upperThickness+lowerThickness;
|
|
const std::int64_t crossingLimit=std::min(width,maximumContinuousCrossing);
|
|
const std::int64_t cell = std::max<std::int64_t>(1000000,
|
|
std::max<std::int64_t>(1, layoutChunkSizeMicrometres) * 2);
|
|
const std::int64_t warp = std::min(crossingLimit / 2, cell / 2);
|
|
const std::int64_t warpX = (TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
|
xMicrometres, yMicrometres, seam, cell, 0x49574158u) * warp) / TransitionQ16;
|
|
const std::int64_t warpY = (TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
|
xMicrometres, yMicrometres, seam, cell, 0x49574159u) * warp) / TransitionQ16;
|
|
const std::int64_t warpZ = (TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
|
xMicrometres, yMicrometres, seam, cell, 0x4957415au) * warp) / TransitionQ16;
|
|
noise = TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
|
xMicrometres + warpX, yMicrometres + warpY, seam + warpZ,
|
|
cell, 0x494e544cu);
|
|
|
|
// Interleaved means physical crossing: the two faces enter each other's
|
|
// bands by a total depth in [0, declared width]. Split that overlap across
|
|
// both faces so neither stratum owns the entire displacement. Since both
|
|
// resulting intervals are solid and overlap, the transition cannot open a gap.
|
|
const std::int64_t overlap = ((noise + TransitionQ16) * crossingLimit)
|
|
/ (2 * TransitionQ16);
|
|
const std::int64_t upperInset = overlap / 2;
|
|
const std::int64_t lowerInset = overlap - upperInset;
|
|
result.crossingDepthMicrometres = overlap;
|
|
result.upperBedrockBottomMicrometres = seam - upperInset;
|
|
result.upperBedrockTopMicrometres = result.upperBedrockBottomMicrometres + upperThickness;
|
|
result.lowerCeilingTopMicrometres = seam + lowerInset;
|
|
result.lowerCeilingBottomMicrometres = result.lowerCeilingTopMicrometres - lowerThickness;
|
|
return result;
|
|
}
|
|
|
|
const std::int64_t displacement = (noise * (width / 8)) / TransitionQ16;
|
|
const std::int64_t interfaceZ = seam + displacement;
|
|
result.upperBedrockBottomMicrometres = interfaceZ;
|
|
result.upperBedrockTopMicrometres = interfaceZ + upperThickness;
|
|
result.lowerCeilingBottomMicrometres = interfaceZ - lowerThickness;
|
|
result.lowerCeilingTopMicrometres = interfaceZ;
|
|
return result;
|
|
}
|
|
|
|
std::string ResolvedLayoutSnapshotV3::ToJson() const
|
|
{
|
|
const std::string body=LayoutBodyJson(*this);
|
|
return body.substr(0,body.size()-1)+",\"stable_identity\":"
|
|
+LayoutEscape(stableIdentity.empty()?"resolved-layout-v3-fnv64:"+LayoutHex(LayoutHash(body)):stableIdentity)+"}";
|
|
}
|
|
|
|
bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
|
{
|
|
error.clear();
|
|
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
|
|
||coreVoxelSizeMicrometres==0||strateLayoutChunkSizeMicrometres==0
|
|
||strateLayoutChunkWidthSubvoxels<=0)
|
|
{error="resolved layout has an unsupported version, empty world identity, or no strates";return false;}
|
|
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
|
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
|
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<b.order;});
|
|
std::sort(cavernRooms.begin(),cavernRooms.end(),[](const auto& a,const auto& b)
|
|
{return a.stableId<b.stableId;});
|
|
std::sort(cavernEdges.begin(),cavernEdges.end(),[](const auto& a,const auto& b)
|
|
{return a.stableId<b.stableId;});
|
|
std::sort(oreVeins.begin(),oreVeins.end(),[](const auto& a,const auto& b)
|
|
{return a.stableId<b.stableId;});
|
|
for(std::size_t i=0;i<strates.size();++i)
|
|
{
|
|
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
|
if(x.stableId.empty())x.stableId=worldIdentity+"/strate/"+std::to_string(x.strateIndex);
|
|
if(x.content.kind.empty())x.content.kind="content";
|
|
if(x.ceiling.kind.empty())x.ceiling.kind="ceiling";
|
|
if(x.bedrock.kind.empty())x.bedrock.kind="bedrock";
|
|
if(x.interiorTopMicrometres<=x.interiorBottomMicrometres||x.voxelSizeMicrometres==0)
|
|
{error="resolved layout contains an invalid interior interval or voxel size";return false;}
|
|
|
|
// Materialize generator-specific composition policy at layout resolution. The
|
|
// sampling/composition path consumes these capabilities without examining names.
|
|
x.passageCapabilities={};
|
|
x.lowerInterfaceCapabilities={};
|
|
if(x.generatorKind=="SurfaceWorld")
|
|
x.passageCapabilities.hasSurfaceLanding=true;
|
|
else if(x.generatorKind=="FloatingIslands")
|
|
{
|
|
x.passageCapabilities.hasFloatingIslandLanding=true;
|
|
x.passageCapabilities.usePassageAnchorForColumns=true;
|
|
}
|
|
if(i+1<strates.size())
|
|
{
|
|
const auto& lower=strates[i+1];
|
|
x.lowerInterfaceCapabilities.preserveUpperBedrockAirGap=
|
|
x.generatorKind=="FloatingIslands"&&x.transitionMode=="Gradient";
|
|
x.lowerInterfaceCapabilities.surfaceToTunnelLandingSupport=
|
|
x.generatorKind=="SurfaceWorld"&&lower.generatorKind=="TunnelNetwork";
|
|
}
|
|
}
|
|
const std::int64_t playerCenterAboveFloorQ=static_cast<std::int64_t>(std::llround(
|
|
1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres));
|
|
for(auto& passage:passages)
|
|
{
|
|
passage.lowerMouthControlIndex=passage.controlPoints.empty()?0:passage.controlPoints.size()-1;
|
|
for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
|
{
|
|
const auto& point=passage.controlPoints[i];
|
|
if(point.positionSubvoxels[0]==passage.lowerMouthSubvoxels[0]
|
|
&&point.positionSubvoxels[1]==passage.lowerMouthSubvoxels[1]
|
|
&&point.positionSubvoxels[2]-point.radiusSubvoxels+playerCenterAboveFloorQ
|
|
==passage.lowerMouthSubvoxels[2])
|
|
passage.lowerMouthControlIndex=i;
|
|
}
|
|
}
|
|
stableIdentity.clear();const std::string body=LayoutBodyJson(*this);
|
|
stableIdentity="resolved-layout-v3-fnv64:"+LayoutHex(LayoutHash(body));
|
|
std::vector<ResolvedLayoutIssueV2> issues;
|
|
return validate(ResolvedLayoutValidationModeV3::compatibility,issues,error);
|
|
}
|
|
|
|
bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|
std::vector<ResolvedLayoutIssueV2>& issues,std::string& error) const
|
|
{
|
|
issues.clear();error.clear();
|
|
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
|
|
||coreVoxelSizeMicrometres==0||strateLayoutChunkSizeMicrometres==0
|
|
||strateLayoutChunkWidthSubvoxels<=0)
|
|
{error="resolved layout schema/world/strata header is invalid";return false;}
|
|
const auto add=[&](std::string code,std::string detail)
|
|
{issues.push_back({std::move(code),std::move(detail)});};
|
|
const bool strict=mode==ResolvedLayoutValidationModeV3::strict;
|
|
for(std::size_t i=0;i<strates.size();++i)
|
|
{
|
|
const auto& s=strates[i];
|
|
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(s);
|
|
if(s.interiorTopMicrometres<=s.interiorBottomMicrometres)
|
|
add("invalid_interior",s.stableId+" has non-positive content height");
|
|
if(s.transitionInterpenetrationMicrometres>effectiveTransitionWidth
|
|
||s.transitionInterpenetrationMicrometres<0)
|
|
add("transition_overlap_exceeds_width",s.stableId+" transition interpenetration exceeds its declared width");
|
|
if(s.transitionWidthMicrometres<0||s.transitionBlendChunks<0)
|
|
add("invalid_transition_width",s.stableId+" has a negative transition width");
|
|
if(s.managerChunkWidthMicrometres>0&&s.dispatchChunkWidthMicrometres>0
|
|
&&s.managerChunkWidthMicrometres!=s.dispatchChunkWidthMicrometres)
|
|
add("ambiguous_dispatch_units",s.stableId+" manager chunk width differs from Core dispatch chunk width");
|
|
if(i+1<strates.size()&&s.transitionMode!="Hard"&&!s.transitionHonouredInCore
|
|
&&!s.transitionFallsBackToHard)
|
|
add("transition_not_honoured",s.stableId+" declares a blend/interleave that Core does not apply on this interface");
|
|
if(!s.ceiling.presentInCurrentField)add("missing_ceiling",s.stableId+" has no explicit ceiling band in today's field");
|
|
if(!s.bedrock.presentInCurrentField)add("missing_bedrock",s.stableId+" has no explicit bedrock floor in today's field");
|
|
if(strict)
|
|
{
|
|
if(s.order!=static_cast<std::int32_t>(i))add("wrong_order",s.stableId+" order is not top-to-bottom");
|
|
if(!s.ceiling.presentInCurrentField||!s.bedrock.presentInCurrentField)
|
|
add(!s.ceiling.presentInCurrentField?"missing_ceiling":"missing_bedrock",s.stableId+" lacks a required boundary band");
|
|
if(!s.content.presentInCurrentField)
|
|
add("missing_content",s.stableId+" has no content generator band");
|
|
const auto bandShape=[&](const ResolvedLayoutBandV2& b,std::string_view expected)
|
|
{
|
|
if(b.kind!=expected||b.topMicrometres<=b.bottomMicrometres)
|
|
add("wrong_order",s.stableId+" has an invalid "+std::string(expected)+" band");
|
|
const std::int64_t minimumThickness=2ll*coreVoxelSizeMicrometres;
|
|
if(b.topMicrometres-b.bottomMicrometres<minimumThickness)
|
|
add("band_thinner_than_minimum",s.stableId+" "+std::string(expected)
|
|
+" is thinner than the two-Core-voxel minimum");
|
|
if(b.protectedBand&&b.diggable)
|
|
add("invalid_protection",s.stableId+" band is both protected and diggable");
|
|
if(b.solidDensityQ16<=0)
|
|
add("invalid_solid_density",s.stableId+" has a non-positive solid band strength");
|
|
};
|
|
bandShape(s.ceiling,"ceiling");bandShape(s.content,"content");bandShape(s.bedrock,"bedrock");
|
|
if(s.ceiling.bottomMicrometres!=s.content.topMicrometres
|
|
||s.content.bottomMicrometres!=s.bedrock.topMicrometres)
|
|
add("wrong_order",s.stableId+" bands do not follow ceiling -> content -> bedrock");
|
|
if(i+1<strates.size()
|
|
&&s.bedrock.bottomMicrometres!=strates[i+1].ceiling.topMicrometres)
|
|
add(s.bedrock.bottomMicrometres>strates[i+1].ceiling.topMicrometres?"gap":"overlap",
|
|
s.stableId+" bedrock and next strate ceiling are not nominally contiguous");
|
|
if(s.transitionInterpenetrationMicrometres>effectiveTransitionWidth)
|
|
add("transition_overlap_exceeds_width",s.stableId+" interpenetration is wider than the declared transition zone");
|
|
}
|
|
}
|
|
if(!strict)
|
|
{
|
|
for(const auto& s:strates)
|
|
{
|
|
if(s.content.presentInCurrentField&&s.ceiling.presentInCurrentField)
|
|
{
|
|
if(s.ceiling.bottomMicrometres<s.content.topMicrometres)
|
|
add("band_overlap",s.stableId+" ceiling overlaps content by "+std::to_string(
|
|
s.content.topMicrometres-s.ceiling.bottomMicrometres)+" um");
|
|
else if(s.ceiling.bottomMicrometres>s.content.topMicrometres)
|
|
add("uncovered_interval",s.stableId+" has a "+std::to_string(
|
|
s.ceiling.bottomMicrometres-s.content.topMicrometres)+" um gap below its ceiling");
|
|
}
|
|
if(s.content.presentInCurrentField&&s.bedrock.presentInCurrentField)
|
|
{
|
|
if(s.bedrock.topMicrometres>s.content.bottomMicrometres)
|
|
add("band_overlap",s.stableId+" bedrock overlaps content by "+std::to_string(
|
|
s.bedrock.topMicrometres-s.content.bottomMicrometres)+" um");
|
|
else if(s.bedrock.topMicrometres<s.content.bottomMicrometres)
|
|
add("uncovered_interval",s.stableId+" has a "+std::to_string(
|
|
s.content.bottomMicrometres-s.bedrock.topMicrometres)+" um gap above its bedrock");
|
|
}
|
|
}
|
|
for(std::size_t i=0;i+1<strates.size();++i)
|
|
{
|
|
const auto& a=strates[i];const auto& b=strates[i+1];
|
|
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(a);
|
|
if(a.interiorBottomMicrometres>b.interiorTopMicrometres)
|
|
add("uncovered_interval",a.stableId+" to "+b.stableId+" leaves "+std::to_string(
|
|
a.interiorBottomMicrometres-b.interiorTopMicrometres)+" um uncovered");
|
|
else if(a.interiorBottomMicrometres<b.interiorTopMicrometres)
|
|
{
|
|
const std::int64_t overlap=b.interiorTopMicrometres-a.interiorBottomMicrometres;
|
|
if(overlap>effectiveTransitionWidth)
|
|
add("overlap",a.stableId+" and "+b.stableId+" interiors overlap by "+std::to_string(overlap)+" um");
|
|
else add("transition_interpenetration",a.stableId+" and "+b.stableId+" overlap inside the declared transition zone");
|
|
}
|
|
if(a.bedrock.presentInCurrentField&&b.ceiling.presentInCurrentField)
|
|
{
|
|
if(a.bedrock.bottomMicrometres>b.ceiling.topMicrometres)
|
|
add("uncovered_interval",a.stableId+" bedrock and "+b.stableId+" ceiling leave "+std::to_string(
|
|
a.bedrock.bottomMicrometres-b.ceiling.topMicrometres)+" um uncovered");
|
|
else if(a.bedrock.bottomMicrometres<b.ceiling.topMicrometres)
|
|
{
|
|
const std::int64_t overlap=b.ceiling.topMicrometres-a.bedrock.bottomMicrometres;
|
|
if(overlap>effectiveTransitionWidth)
|
|
add("overlap",a.stableId+" bedrock and "+b.stableId+" ceiling overlap by "+std::to_string(overlap)+" um");
|
|
else add("transition_interpenetration",a.stableId+" bedrock and "+b.stableId+" ceiling interpenetrate by "+std::to_string(overlap)+" um inside the declared transition zone");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const auto findStratum=[this](std::int32_t index)->const ResolvedLayoutStratumV2*
|
|
{
|
|
const auto it=std::find_if(strates.begin(),strates.end(),[index](const auto& s)
|
|
{return s.strateIndex==index;});
|
|
return it==strates.end()?nullptr:&*it;
|
|
};
|
|
std::vector<std::string> passageIds;
|
|
for(std::size_t i=0;strict&&i+1<strates.size();++i)
|
|
{
|
|
const auto& upper=strates[i];const auto& lower=strates[i+1];
|
|
if(std::none_of(passages.begin(),passages.end(),[&](const auto& p)
|
|
{return p.upperStrateIndex==upper.strateIndex&&p.lowerStrateIndex==lower.strateIndex;}))
|
|
add("missing_passage_interface",upper.stableId+" has no passage to "+lower.stableId);
|
|
}
|
|
for(const auto& passage:passages)
|
|
{
|
|
const auto* upper=findStratum(passage.upperStrateIndex);
|
|
const auto* lower=findStratum(passage.lowerStrateIndex);
|
|
if(passage.stableId.empty()||std::find(passageIds.begin(),passageIds.end(),passage.stableId)!=passageIds.end())
|
|
add("passage_identity", "passage has an empty or duplicate stable identity");
|
|
else passageIds.push_back(passage.stableId);
|
|
if(!upper||!lower||lower->order!=upper->order+1)
|
|
add("passage_interface",passage.stableId+" does not join adjacent resolved strates");
|
|
if(!upper||!lower||passage.placementRadiusSubvoxels<=0
|
|
||passage.mouthRadiusSubvoxels<=0||passage.midRadiusSubvoxels<=0
|
|
||passage.controlPoints.size()<2)
|
|
{
|
|
add("passage_geometry",passage.stableId+" has invalid radii, placement bound, or control points");
|
|
continue;
|
|
}
|
|
const auto insidePlacement=[&](const std::array<std::int64_t,3>& p)
|
|
{
|
|
const auto x=p[0],y=p[1],r=passage.placementRadiusSubvoxels;
|
|
return std::abs(x)<=r&&std::abs(y)<=r&&x*x+y*y<=r*r;
|
|
};
|
|
const auto insideContent=[&](const ResolvedLayoutStratumV2& s,
|
|
const std::array<std::int64_t,3>& p)
|
|
{
|
|
const std::int64_t zMicrometres=p[2]*static_cast<std::int64_t>(coreVoxelSizeMicrometres)
|
|
/WorldAssemblySubvoxelsPerVoxel;
|
|
return s.content.presentInCurrentField&&zMicrometres>=s.content.bottomMicrometres
|
|
&&zMicrometres<s.content.topMicrometres;
|
|
};
|
|
if(!insidePlacement(passage.upperMouthSubvoxels)||!insidePlacement(passage.lowerMouthSubvoxels))
|
|
add("passage_placement",passage.stableId+" mouth lies outside its (0,0) placement radius");
|
|
if(!insideContent(*upper,passage.upperMouthSubvoxels)
|
|
||!insideContent(*lower,passage.lowerMouthSubvoxels))
|
|
add("passage_mouth_content",passage.stableId+" mouth is outside its owned content band");
|
|
if(passage.controlPoints.size()<2)continue;
|
|
const std::int64_t PlayerCenterAboveFloorQ=coreVoxelSizeMicrometres>0
|
|
?static_cast<std::int64_t>(std::llround(1.13*WorldAssemblySubvoxelsPerVoxel
|
|
*1000000.0/coreVoxelSizeMicrometres)):1157;
|
|
const auto floorProfile=[&](const ResolvedLayoutPassagePointV1& p)
|
|
{return p.positionSubvoxels[2]-p.radiusSubvoxels+PlayerCenterAboveFloorQ;};
|
|
std::size_t lowerMouthControlIndex=passage.controlPoints.size();
|
|
for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
|
{
|
|
const auto& candidate=passage.controlPoints[i];
|
|
if(candidate.positionSubvoxels[0]==passage.lowerMouthSubvoxels[0]
|
|
&&candidate.positionSubvoxels[1]==passage.lowerMouthSubvoxels[1]
|
|
&&floorProfile(candidate)==passage.lowerMouthSubvoxels[2])
|
|
lowerMouthControlIndex=i;
|
|
}
|
|
if(lowerMouthControlIndex==passage.controlPoints.size())
|
|
{
|
|
add("passage_mouth_binding",passage.stableId+" has no control point bound to its lower mouth");
|
|
lowerMouthControlIndex=passage.controlPoints.size()-1;
|
|
}
|
|
for(std::size_t i=0;i<passage.controlPoints.size();++i)
|
|
{
|
|
const auto& point=passage.controlPoints[i];
|
|
if(point.radiusSubvoxels<=0||!insidePlacement(point.positionSubvoxels))
|
|
add("passage_control_point",passage.stableId+" has a non-positive radius or leaves its placement disk");
|
|
if(i==0||i==lowerMouthControlIndex)
|
|
{
|
|
const auto& mouth=i==0?passage.upperMouthSubvoxels:passage.lowerMouthSubvoxels;
|
|
if(point.positionSubvoxels[0]!=mouth[0]||point.positionSubvoxels[1]!=mouth[1]
|
|
||floorProfile(point)!=mouth[2])
|
|
add("passage_mouth_binding",passage.stableId+" control point does not bind to its mouth anchor");
|
|
}
|
|
if(i==0)continue;
|
|
const auto& previous=passage.controlPoints[i-1];
|
|
const std::int64_t dx=point.positionSubvoxels[0]-previous.positionSubvoxels[0];
|
|
const std::int64_t dy=point.positionSubvoxels[1]-previous.positionSubvoxels[1];
|
|
const auto horizontal=PassageIsqrt(static_cast<std::uint64_t>(dx*dx+dy*dy));
|
|
const std::int64_t signedDz=floorProfile(point)-floorProfile(previous);
|
|
const std::int64_t dz=std::abs(signedDz);
|
|
const bool landingExtension=i>lowerMouthControlIndex;
|
|
if((landingExtension?dz*65536>static_cast<std::int64_t>(horizontal)*17560
|
|
:signedDz>=0||dz*65536>static_cast<std::int64_t>(horizontal)*17560)
|
|
||horizontal==0)
|
|
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
|
|
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV2)
|
|
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1 or 2");
|
|
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(cavernRooms.empty()&&cavernEdges.empty())
|
|
{
|
|
if(cavernGraphVocabularyVersion!=0)
|
|
add("cavern_graph_vocabulary_version",
|
|
"cavern graph vocabulary version is set without graph records");
|
|
}
|
|
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1)
|
|
add("cavern_graph_vocabulary_version",
|
|
"cavern graph records require vocabulary version 1");
|
|
if(cavernGraphVocabularyVersion!=0)
|
|
{
|
|
if(cavernRooms.empty()||cavernEdges.empty())
|
|
add("cavern_graph_shape","a resolved cavern graph needs at least one room and one edge");
|
|
std::vector<std::string> graphIds;
|
|
const auto graphIdValid=[&](const std::string& id)
|
|
{
|
|
if(id.empty()||std::find(graphIds.begin(),graphIds.end(),id)!=graphIds.end())
|
|
return false;
|
|
graphIds.push_back(id);return true;
|
|
};
|
|
for(const auto& room:cavernRooms)
|
|
{
|
|
if(!graphIdValid(room.stableId))
|
|
add("cavern_graph_identity","room has an empty or duplicate stable identity");
|
|
if(!findStratum(room.strateIndex))
|
|
add("cavern_graph_strate",room.stableId+" names an unknown strate");
|
|
bool validExtents=true;
|
|
for(const auto extent:room.halfExtentsMicrometres)
|
|
validExtents=validExtents&&extent>=500000&&extent<=100000000;
|
|
if(!validExtents||room.smoothWidthMicrometres<0
|
|
||room.smoothWidthMicrometres>10000000
|
|
||room.irregularityAmplitudeMicrometres<0
|
|
||room.irregularityAmplitudeMicrometres>10000000
|
|
||room.irregularityWavelengthMicrometres<0
|
|
||room.irregularityWavelengthMicrometres>100000000
|
|
||(room.irregularityAmplitudeMicrometres>0
|
|
&&room.irregularityWavelengthMicrometres<=0))
|
|
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
|
|
const std::int64_t pad=room.smoothWidthMicrometres
|
|
+room.irregularityAmplitudeMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(room.boundsMinimumMicrometres[axis]
|
|
!=room.centerMicrometres[axis]-room.halfExtentsMicrometres[axis]-pad
|
|
||room.boundsMaximumMicrometres[axis]
|
|
!=room.centerMicrometres[axis]+room.halfExtentsMicrometres[axis]+pad)
|
|
add("cavern_room_bounds",room.stableId+" conservative bounds do not match its parameters");
|
|
if(room.floorMicrometres<room.centerMicrometres[2]-room.halfExtentsMicrometres[2]
|
|
||room.floorMicrometres>room.centerMicrometres[2]+room.halfExtentsMicrometres[2])
|
|
add("cavern_room_floor",room.stableId+" floor lies outside its room envelope");
|
|
}
|
|
for(const auto& edge:cavernEdges)
|
|
{
|
|
if(!graphIdValid(edge.stableId))
|
|
add("cavern_graph_identity","edge has an empty or duplicate stable identity");
|
|
if(edge.startAnchorStableId.empty()||edge.endRoomStableId.empty())
|
|
add("cavern_edge_binding",edge.stableId+" has a missing endpoint identity");
|
|
const auto room=std::find_if(cavernRooms.begin(),cavernRooms.end(),
|
|
[&](const auto& value){return value.stableId==edge.endRoomStableId;});
|
|
if(room==cavernRooms.end()||room->strateIndex!=edge.strateIndex)
|
|
add("cavern_edge_binding",edge.stableId+" does not terminate at a room in its strate");
|
|
if(!findStratum(edge.strateIndex))
|
|
add("cavern_graph_strate",edge.stableId+" names an unknown strate");
|
|
if(edge.boreRadiusMicrometres<500000||edge.boreRadiusMicrometres>10000000
|
|
||edge.boreHeightMicrometres<1000000||edge.boreHeightMicrometres>20000000
|
|
||edge.floorSupportDepthMicrometres<0
|
|
||edge.floorSupportDepthMicrometres>2000000
|
|
||edge.smoothWidthMicrometres<0||edge.smoothWidthMicrometres>10000000
|
|
||edge.irregularityAmplitudeMicrometres<0
|
|
||edge.irregularityAmplitudeMicrometres>10000000
|
|
||edge.irregularityWavelengthMicrometres<0
|
|
||edge.irregularityWavelengthMicrometres>100000000
|
|
||(edge.irregularityAmplitudeMicrometres>0
|
|
&&edge.irregularityWavelengthMicrometres<=0))
|
|
add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters");
|
|
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
|
std::array<std::int64_t,3> high=low;
|
|
for(const auto& point:edge.controlPointsMicrometres)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);}
|
|
const std::int64_t xyPad=edge.boreRadiusMicrometres
|
|
+edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres;
|
|
const std::int64_t zPad=edge.smoothWidthMicrometres
|
|
+edge.irregularityAmplitudeMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const std::int64_t pad=axis==2?zPad:xyPad;
|
|
const std::int64_t lowSupport=axis==2?edge.floorSupportDepthMicrometres:0;
|
|
const std::int64_t highBore=axis==2?edge.boreHeightMicrometres:0;
|
|
if(edge.boundsMinimumMicrometres[axis]!=low[axis]-pad-lowSupport
|
|
||edge.boundsMaximumMicrometres[axis]!=high[axis]+pad+highBore)
|
|
add("cavern_edge_bounds",edge.stableId+" conservative bounds do not match its parameters");
|
|
}
|
|
if(room!=cavernRooms.end())
|
|
{
|
|
const auto& endpoint=edge.controlPointsMicrometres.back();
|
|
if(endpoint[0]<room->centerMicrometres[0]-room->halfExtentsMicrometres[0]
|
|
||endpoint[0]>room->centerMicrometres[0]+room->halfExtentsMicrometres[0]
|
|
||endpoint[1]<room->centerMicrometres[1]-room->halfExtentsMicrometres[1]
|
|
||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1]
|
|
||std::abs(endpoint[2]-room->floorMicrometres)>500000)
|
|
add("cavern_edge_portal",edge.stableId+" endpoint does not meet its room floor portal");
|
|
}
|
|
}
|
|
}
|
|
if(oreVeins.empty())
|
|
{
|
|
if(oreVeinVocabularyVersion!=0)
|
|
add("ore_vein_vocabulary_version",
|
|
"ore-vein vocabulary version is set without records");
|
|
}
|
|
else if(oreVeinVocabularyVersion!=ResolvedLayoutOreVeinVocabularyVersionV1)
|
|
add("ore_vein_vocabulary_version",
|
|
"ore-vein records require vocabulary version 1");
|
|
std::vector<std::string> oreVeinIds;
|
|
for(const auto& vein:oreVeins)
|
|
{
|
|
if(vein.stableId.empty()
|
|
||std::find(oreVeinIds.begin(),oreVeinIds.end(),vein.stableId)!=oreVeinIds.end())
|
|
add("ore_vein_identity","ore vein has an empty or duplicate stable identity");
|
|
else oreVeinIds.push_back(vein.stableId);
|
|
if(vein.resourceMaterialId==0||vein.radiusMicrometres<250000
|
|
||vein.radiusMicrometres>20000000
|
|
||vein.irregularityAmplitudeMicrometres<0
|
|
||vein.irregularityAmplitudeMicrometres>10000000
|
|
||vein.irregularityWavelengthMicrometres<0
|
|
||vein.irregularityWavelengthMicrometres>100000000
|
|
||(vein.irregularityAmplitudeMicrometres>0
|
|
&&vein.irregularityWavelengthMicrometres<=0)
|
|
||vein.blendWidthMicrometres<50000
|
|
||vein.blendWidthMicrometres>2000000
|
|
||vein.irregularitySeed>0x001fffffffffffffull)
|
|
add("ore_vein_parameters",vein.stableId+" has invalid or unbounded parameters");
|
|
const std::int64_t pad=vein.radiusMicrometres
|
|
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
if(vein.endpointsMicrometres[0][axis]<-1000000000000ll
|
|
||vein.endpointsMicrometres[0][axis]>1000000000000ll
|
|
||vein.endpointsMicrometres[1][axis]<-1000000000000ll
|
|
||vein.endpointsMicrometres[1][axis]>1000000000000ll)
|
|
add("ore_vein_bounds",vein.stableId+" endpoint is outside the bounded world envelope");
|
|
if(vein.boundsMinimumMicrometres[axis]
|
|
!=std::min(vein.endpointsMicrometres[0][axis],
|
|
vein.endpointsMicrometres[1][axis])-pad
|
|
||vein.boundsMaximumMicrometres[axis]
|
|
!=std::max(vein.endpointsMicrometres[0][axis],
|
|
vein.endpointsMicrometres[1][axis])+pad)
|
|
add("ore_vein_bounds",vein.stableId+" conservative bounds do not match its parameters");
|
|
}
|
|
std::string safetyReason;
|
|
if(!LayoutOreVeinSafe(*this,vein,&safetyReason))
|
|
add("ore_vein_placement",vein.stableId+" is unsafe: "+safetyReason);
|
|
}
|
|
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
|
return true;
|
|
}
|
|
|
|
std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|
std::string_view json,std::string& error)
|
|
{
|
|
VFJson::Value root;if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt;
|
|
if(!root.IsObject()){error="resolved layout JSON root must be an object";return std::nullopt;}
|
|
ResolvedLayoutSnapshotV3 s;s.schemaVersion=static_cast<std::uint32_t>(LayoutInt(root,"schema_version"));
|
|
s.worldIdentity=LayoutString(root,"world_identity");
|
|
s.ownerVoxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"owner_voxel_size_um"));
|
|
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"));
|
|
s.cavernGraphVocabularyVersion=static_cast<std::uint32_t>(
|
|
LayoutInt(root,"cavern_graph_vocabulary_version"));
|
|
s.oreVeinVocabularyVersion=static_cast<std::uint32_t>(
|
|
LayoutInt(root,"ore_vein_vocabulary_version"));
|
|
for(const auto& v:root.Get("strates").AsArray())
|
|
{
|
|
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
|
x.order=static_cast<std::int32_t>(LayoutInt(v,"order"));x.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
|
x.generatorKind=LayoutString(v,"generator_kind");x.recipeId=LayoutString(v,"recipe_id");
|
|
x.recipeVersion=static_cast<std::uint32_t>(LayoutInt(v,"recipe_version"));
|
|
x.interiorBottomMicrometres=LayoutInt(v,"interior_bottom_um");x.interiorTopMicrometres=LayoutInt(v,"interior_top_um");
|
|
x.voxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(v,"voxel_size_um"));
|
|
const auto& origin=v.Get("lattice_origin_um").AsArray();for(std::size_t i=0;i<3&&i<origin.size();++i)x.latticeOriginMicrometres[i]=origin[i].Integer();
|
|
x.ceiling=ParseLayoutBand(v.Get("ceiling"));x.content=ParseLayoutBand(v.Get("content"));x.bedrock=ParseLayoutBand(v.Get("bedrock"));
|
|
x.transitionMode=LayoutString(v,"transition_mode");x.transitionBlendChunks=static_cast<std::int32_t>(LayoutInt(v,"transition_blend_chunks"));
|
|
x.transitionWidthMicrometres=LayoutInt(v,"transition_width_um");
|
|
x.transitionWarpAmplitudeMicrometres=LayoutInt(v,"transition_warp_amplitude_um");
|
|
x.transitionInterpenetrationMicrometres=LayoutInt(v,"transition_interpenetration_um");
|
|
x.transitionHonouredInCore=LayoutBool(v,"transition_honoured_in_core");
|
|
x.transitionFallsBackToHard=LayoutBool(v,"transition_falls_back_to_hard");
|
|
x.transitionDispatchPath=LayoutString(v,"transition_dispatch_path");
|
|
x.boundDispatchPath=LayoutString(v,"bound_dispatch_path");x.queryDispatchPath=LayoutString(v,"query_dispatch_path");
|
|
x.managerLookup=LayoutString(v,"manager_lookup");x.managerChunkWidthMicrometres=LayoutInt(v,"manager_chunk_width_um");
|
|
x.dispatchChunkWidthMicrometres=LayoutInt(v,"dispatch_chunk_width_um");
|
|
for(const auto& a:v.Get("anchors").AsArray())
|
|
{ResolvedLayoutAnchorV2 p;p.kind=LayoutString(a,"kind");p.stableId=LayoutString(a,"stable_id");p.units=LayoutString(a,"units");
|
|
for(const auto& value:a.Get("values").AsArray())p.values.push_back(value.Integer());x.anchors.push_back(std::move(p));}
|
|
s.strates.push_back(std::move(x));
|
|
}
|
|
for(const auto& v:root.Get("passages").AsArray())
|
|
{
|
|
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
|
|
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
|
|
p.lowerStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"lower_strate_index"));
|
|
if(v.Get("style").IsNumber())
|
|
p.style=static_cast<std::uint32_t>(LayoutInt(v,"style"));
|
|
p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q");
|
|
p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q");
|
|
const auto& upper=v.Get("upper_mouth_q").AsArray();
|
|
const auto& lower=v.Get("lower_mouth_q").AsArray();
|
|
for(std::size_t axis=0;axis<3&&axis<upper.size();++axis)p.upperMouthSubvoxels[axis]=upper[axis].Integer();
|
|
for(std::size_t axis=0;axis<3&&axis<lower.size();++axis)p.lowerMouthSubvoxels[axis]=lower[axis].Integer();
|
|
for(const auto& q:v.Get("control_points").AsArray())
|
|
{
|
|
ResolvedLayoutPassagePointV1 point;const auto& position=q.Get("position_q").AsArray();
|
|
for(std::size_t axis=0;axis<3&&axis<position.size();++axis)
|
|
point.positionSubvoxels[axis]=position[axis].Integer();
|
|
point.radiusSubvoxels=LayoutInt(q,"radius_q");p.controlPoints.push_back(point);
|
|
}
|
|
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));
|
|
}
|
|
for(const auto& v:root.Get("cavern_rooms").AsArray())
|
|
{
|
|
ResolvedCavernRoomV1 room;room.stableId=LayoutString(v,"stable_id");
|
|
room.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
|
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("center_um",room.centerMicrometres);
|
|
readTriplet("half_extents_um",room.halfExtentsMicrometres);
|
|
readTriplet("bounds_min_um",room.boundsMinimumMicrometres);
|
|
readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
|
|
room.floorMicrometres=LayoutInt(v,"floor_um");
|
|
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
|
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
|
room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
|
room.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
|
s.cavernRooms.push_back(std::move(room));
|
|
}
|
|
for(const auto& v:root.Get("cavern_edges").AsArray())
|
|
{
|
|
ResolvedCavernEdgeV1 edge;edge.stableId=LayoutString(v,"stable_id");
|
|
edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id");
|
|
edge.endRoomStableId=LayoutString(v,"end_room_stable_id");
|
|
edge.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
|
const auto& points=v.Get("control_points_um").AsArray();
|
|
for(std::size_t i=0;i<edge.controlPointsMicrometres.size()&&i<points.size();++i)
|
|
{
|
|
const auto& point=points[i].AsArray();
|
|
for(std::size_t axis=0;axis<3&&axis<point.size();++axis)
|
|
edge.controlPointsMicrometres[i][axis]=point[axis].Integer();
|
|
}
|
|
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("bounds_min_um",edge.boundsMinimumMicrometres);
|
|
readTriplet("bounds_max_um",edge.boundsMaximumMicrometres);
|
|
edge.boreRadiusMicrometres=LayoutInt(v,"bore_radius_um");
|
|
edge.boreHeightMicrometres=LayoutInt(v,"bore_height_um");
|
|
edge.floorSupportDepthMicrometres=LayoutInt(v,"floor_support_depth_um");
|
|
edge.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
|
edge.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
|
edge.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
|
edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
|
s.cavernEdges.push_back(std::move(edge));
|
|
}
|
|
for(const auto& v:root.Get("ore_veins").AsArray())
|
|
{
|
|
ResolvedLayoutOreVeinV1 vein;vein.stableId=LayoutString(v,"stable_id");
|
|
vein.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
|
vein.resourceMaterialId=static_cast<std::uint16_t>(LayoutInt(v,"resource_material_id"));
|
|
const auto& endpoints=v.Get("endpoints_um").AsArray();
|
|
for(std::size_t endpoint=0;endpoint<2&&endpoint<endpoints.size();++endpoint)
|
|
{
|
|
const auto& values=endpoints[endpoint].AsArray();
|
|
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
|
vein.endpointsMicrometres[endpoint][axis]=values[axis].Integer();
|
|
}
|
|
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("bounds_min_um",vein.boundsMinimumMicrometres);
|
|
readTriplet("bounds_max_um",vein.boundsMaximumMicrometres);
|
|
vein.radiusMicrometres=LayoutInt(v,"radius_um");
|
|
vein.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
|
vein.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
|
vein.blendWidthMicrometres=LayoutInt(v,"blend_width_um");
|
|
vein.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
|
s.oreVeins.push_back(std::move(vein));
|
|
}
|
|
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;
|
|
}
|
|
|
|
bool ResolveResolvedLayoutStampsV2(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
|
std::span<const ResolvedLayoutStampRequestV1> requests,std::string& error)
|
|
{
|
|
if(!ResolveResolvedLayoutStampsV1(layout,seed,requests,error))return false;
|
|
if(requests.empty())return true;
|
|
ResolvedLayoutSnapshotV3 candidate=layout;
|
|
candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV2;
|
|
if(!candidate.finalize(error))return false;
|
|
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
|
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
|
{error="strict stamp vocabulary-2 validation failed: "+strictError;return false;}
|
|
layout=std::move(candidate);return true;
|
|
}
|
|
|
|
bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
|
std::span<const ResolvedCavernRoomV1> rooms,
|
|
std::span<const ResolvedCavernEdgeV1> edges,std::string& error)
|
|
{
|
|
error.clear();
|
|
ResolvedLayoutSnapshotV3 candidate=layout;
|
|
candidate.cavernRooms.clear();candidate.cavernEdges.clear();
|
|
if(rooms.empty()&&edges.empty())
|
|
{
|
|
candidate.cavernGraphVocabularyVersion=0;
|
|
if(!candidate.finalize(error))return false;
|
|
layout=std::move(candidate);return true;
|
|
}
|
|
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV1;
|
|
candidate.cavernRooms.assign(rooms.begin(),rooms.end());
|
|
candidate.cavernEdges.assign(edges.begin(),edges.end());
|
|
for(auto& room:candidate.cavernRooms)
|
|
{
|
|
// The shared JSON value type stores numbers as IEEE-754 doubles. Keep the
|
|
// serialized seed in the exact integer range, as the stamp vocabulary does.
|
|
room.irregularitySeed=LayoutStampSeed(seed,room.strateIndex,room.stableId)
|
|
&0x001fffffffffffffull;
|
|
const std::int64_t pad=room.smoothWidthMicrometres
|
|
+room.irregularityAmplitudeMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
room.boundsMinimumMicrometres[axis]=room.centerMicrometres[axis]
|
|
-room.halfExtentsMicrometres[axis]-pad;
|
|
room.boundsMaximumMicrometres[axis]=room.centerMicrometres[axis]
|
|
+room.halfExtentsMicrometres[axis]+pad;
|
|
}
|
|
}
|
|
for(auto& edge:candidate.cavernEdges)
|
|
{
|
|
edge.irregularitySeed=LayoutStampSeed(seed,edge.strateIndex,edge.stableId)
|
|
&0x001fffffffffffffull;
|
|
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
|
std::array<std::int64_t,3> high=low;
|
|
for(const auto& point:edge.controlPointsMicrometres)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);}
|
|
const std::int64_t xyPad=edge.boreRadiusMicrometres
|
|
+edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres;
|
|
const std::int64_t zPad=edge.smoothWidthMicrometres
|
|
+edge.irregularityAmplitudeMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const std::int64_t pad=axis==2?zPad:xyPad;
|
|
edge.boundsMinimumMicrometres[axis]=low[axis]-pad
|
|
-(axis==2?edge.floorSupportDepthMicrometres:0);
|
|
edge.boundsMaximumMicrometres[axis]=high[axis]+pad
|
|
+(axis==2?edge.boreHeightMicrometres:0);
|
|
}
|
|
}
|
|
if(!candidate.finalize(error))return false;
|
|
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
|
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
|
{error="strict cavern graph validation failed: "+strictError;return false;}
|
|
layout=std::move(candidate);return true;
|
|
}
|
|
|
|
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
|
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
|
|
{
|
|
error.clear();
|
|
ResolvedLayoutSnapshotV3 candidate=layout;
|
|
candidate.oreVeins.clear();candidate.oreVeinVocabularyVersion=0;
|
|
if(requests.empty())
|
|
{
|
|
if(!candidate.finalize(error))return false;
|
|
layout=std::move(candidate);return true;
|
|
}
|
|
candidate.oreVeinVocabularyVersion=ResolvedLayoutOreVeinVocabularyVersionV1;
|
|
std::vector<ResolvedLayoutOreVeinRequestV1> ordered(requests.begin(),requests.end());
|
|
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
|
{return a.stableId<b.stableId;});
|
|
std::vector<std::string> requestIds;
|
|
candidate.oreVeins.reserve(ordered.size());
|
|
for(const auto& request:ordered)
|
|
{
|
|
if(request.stableId.empty()
|
|
||std::find(requestIds.begin(),requestIds.end(),request.stableId)!=requestIds.end())
|
|
{error="ore-vein requests require non-empty unique stable IDs";return false;}
|
|
requestIds.push_back(request.stableId);
|
|
if(request.resourceMaterialId==0||request.placementRadiusMicrometres<0
|
|
||request.placementRadiusMicrometres>10000000
|
|
||request.radiusMicrometres<250000||request.radiusMicrometres>20000000
|
|
||request.irregularityAmplitudeMicrometres<0
|
|
||request.irregularityAmplitudeMicrometres>10000000
|
|
||request.irregularityWavelengthMicrometres<0
|
|
||request.irregularityWavelengthMicrometres>100000000
|
|
||(request.irregularityAmplitudeMicrometres>0
|
|
&&request.irregularityWavelengthMicrometres<=0)
|
|
||request.blendWidthMicrometres<50000
|
|
||request.blendWidthMicrometres>2000000)
|
|
{error="ore-vein request "+request.stableId+" has invalid or unbounded parameters";return false;}
|
|
const auto strate=std::find_if(candidate.strates.begin(),candidate.strates.end(),
|
|
[&](const auto& value){return value.strateIndex==request.strateIndex;});
|
|
if(strate==candidate.strates.end())
|
|
{error="ore-vein request "+request.stableId+" names an unknown strate";return false;}
|
|
|
|
const std::uint64_t baseSeed=LayoutStampSeed(seed,request.strateIndex,
|
|
std::string("ore-vein/")+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;
|
|
std::string lastUnsafeReason="all candidates were outside the bounded content volume";
|
|
const std::uint32_t attempts=request.placementRadiusMicrometres==0?1u:512u;
|
|
for(std::uint32_t attempt=0;attempt<attempts;++attempt)
|
|
{
|
|
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=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;
|
|
}
|
|
ResolvedLayoutOreVeinV1 vein;vein.stableId=request.stableId;
|
|
vein.strateIndex=request.strateIndex;
|
|
vein.resourceMaterialId=request.resourceMaterialId;
|
|
vein.radiusMicrometres=request.radiusMicrometres;
|
|
vein.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
|
vein.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres;
|
|
vein.blendWidthMicrometres=request.blendWidthMicrometres;
|
|
vein.irregularitySeed=baseSeed;
|
|
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
vein.endpointsMicrometres[endpoint][axis]
|
|
=request.anchorEndpointsMicrometres[endpoint][axis]+offset[axis];
|
|
const std::int64_t pad=vein.radiusMicrometres
|
|
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
vein.boundsMinimumMicrometres[axis]=std::min(
|
|
vein.endpointsMicrometres[0][axis],vein.endpointsMicrometres[1][axis])-pad;
|
|
vein.boundsMaximumMicrometres[axis]=std::max(
|
|
vein.endpointsMicrometres[0][axis],vein.endpointsMicrometres[1][axis])+pad;
|
|
}
|
|
if(!LayoutOreVeinSafe(candidate,vein,&lastUnsafeReason))continue;
|
|
candidate.oreVeins.push_back(std::move(vein));placed=true;break;
|
|
}
|
|
if(!placed)
|
|
{error="ore-vein request "+request.stableId
|
|
+" has no deterministic passage-safe content placement: "+lastUnsafeReason;return false;}
|
|
}
|
|
if(!candidate.finalize(error))return false;
|
|
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
|
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
|
{error="strict ore-vein placement validation failed: "+strictError;return false;}
|
|
layout=std::move(candidate);return true;
|
|
}
|
|
|
|
ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
|
const ResolvedLayoutSnapshotV3& layout)
|
|
{
|
|
ResolvedLayoutOreVeinPlacementAuditV1 result;
|
|
if(layout.passages.empty()||layout.passages.front().controlPoints.empty())return result;
|
|
const auto& passage=layout.passages.front();
|
|
const auto& control=passage.controlPoints.front().positionSubvoxels;
|
|
ResolvedLayoutOreVeinV1 forced;forced.stableId="negative-control/forced-clear-passage";
|
|
forced.strateIndex=passage.upperStrateIndex;forced.resourceMaterialId=1;
|
|
forced.radiusMicrometres=500000;forced.blendWidthMicrometres=500000;
|
|
forced.irregularitySeed=1;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const std::int64_t point=static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(control[axis])*layout.coreVoxelSizeMicrometres
|
|
/WorldAssemblySubvoxelsPerVoxel));
|
|
forced.endpointsMicrometres[0][axis]=point;
|
|
forced.endpointsMicrometres[1][axis]=point;
|
|
forced.boundsMinimumMicrometres[axis]=point-1000000;
|
|
forced.boundsMaximumMicrometres[axis]=point+1000000;
|
|
}
|
|
ResolvedLayoutSnapshotV3 candidate=layout;
|
|
candidate.oreVeinVocabularyVersion=ResolvedLayoutOreVeinVocabularyVersionV1;
|
|
candidate.oreVeins.push_back(forced);
|
|
std::string finalizeError;
|
|
++result.negativeControlsRun;
|
|
if(candidate.finalize(finalizeError))
|
|
{
|
|
std::vector<ResolvedLayoutIssueV2> issues;std::string validationError;
|
|
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,validationError)
|
|
&&std::any_of(issues.begin(),issues.end(),[](const auto& issue)
|
|
{return issue.code=="ore_vein_placement"
|
|
&&issue.detail.find("clear passage")!=std::string::npos;}))
|
|
++result.negativeControlsRejected;
|
|
}
|
|
result.passed=result.negativeControlsRun>0
|
|
&&result.negativeControlsRejected==result.negativeControlsRun;
|
|
return result;
|
|
}
|
|
|
|
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)
|
|
{
|
|
error.clear();layout.passages.clear();
|
|
if(layout.strates.size()<2)return true;
|
|
std::stable_sort(layout.strates.begin(),layout.strates.end(),[](const auto& a,const auto& b)
|
|
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
|
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<b.order;});
|
|
for(std::size_t i=0;i<layout.strates.size();++i)
|
|
layout.strates[i].order=static_cast<std::int32_t>(i);
|
|
if(layout.coreVoxelSizeMicrometres==0)
|
|
{error="passage planner requires the resolved Core voxel size";return false;}
|
|
const std::int64_t qPerMetre=static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(WorldAssemblySubvoxelsPerVoxel)*1000000.0
|
|
/layout.coreVoxelSizeMicrometres));
|
|
if(qPerMetre<=0){error="passage planner could not resolve fixed-point metres";return false;}
|
|
const std::int64_t playerCenterAboveFloorQ=static_cast<std::int64_t>(std::llround(1.13*qPerMetre));
|
|
const std::array<std::array<std::int32_t,2>,16> directions{{
|
|
{{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}},
|
|
{{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}},
|
|
{{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}},
|
|
{{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}};
|
|
const auto configFor=[&](std::int32_t strate)->const ResolvedLayoutPassageConfigV1*
|
|
{
|
|
const auto it=std::find_if(configs.begin(),configs.end(),[strate](const auto& c)
|
|
{return c.upperStrateIndex==strate;});
|
|
return it==configs.end()?nullptr:&*it;
|
|
};
|
|
const auto routeAnchor=[](const ResolvedLayoutStratumV2& s,bool departure,
|
|
std::array<std::int64_t,3>& out)
|
|
{
|
|
if(departure)
|
|
for(const auto& a:s.anchors)
|
|
if(a.kind=="passage_anchor"&&a.values.size()>=10)
|
|
{out={a.values[7],a.values[8],a.values[9]};return true;}
|
|
if(!departure)
|
|
for(const auto& a:s.anchors)
|
|
if(a.kind=="passage_arrival"&&a.values.size()>=3)
|
|
{out={a.values[0],a.values[1],a.values[2]};return true;}
|
|
const ResolvedLayoutAnchorV2* selected=nullptr;
|
|
for(const auto& a:s.anchors)
|
|
if(a.kind=="route_point"&&a.values.size()>=3)
|
|
{selected=&a;if(!departure)break;}
|
|
if(!selected)return false;
|
|
out={selected->values[0],selected->values[1],selected->values[2]};return true;
|
|
};
|
|
const auto routeTangent=[](const ResolvedLayoutStratumV2& s,bool departure,
|
|
std::array<std::int64_t,2>& out)
|
|
{
|
|
const ResolvedLayoutAnchorV2* first=nullptr,*second=nullptr,*last=nullptr,*previous=nullptr;
|
|
for(const auto& a:s.anchors)
|
|
if(a.kind=="route_point"&&a.values.size()>=3)
|
|
{if(!first)first=&a;else if(!second)second=&a;previous=last;last=&a;}
|
|
if(!first||!second||!last||!previous)return false;
|
|
const auto* from=departure?previous:first;
|
|
const auto* to=departure?last:second;
|
|
out={to->values[0]-from->values[0],to->values[1]-from->values[1]};
|
|
return out[0]!=0||out[1]!=0;
|
|
};
|
|
const auto radialWithin=[](const std::array<std::int64_t,3>& p,std::int64_t radius)
|
|
{return radius>0&&p[0]*p[0]+p[1]*p[1]<=radius*radius;};
|
|
const auto reachValue=[](std::uint64_t hash,std::int64_t lo,std::int64_t hi)
|
|
{
|
|
if(hi<lo)std::swap(hi,lo);
|
|
return lo+(hi>lo?static_cast<std::int64_t>(hash%static_cast<std::uint64_t>(hi-lo+1)):0);
|
|
};
|
|
const auto contentZQ=[&](std::int64_t micrometres)
|
|
{return static_cast<std::int64_t>(std::llround(static_cast<double>(micrometres)
|
|
*WorldAssemblySubvoxelsPerVoxel/layout.coreVoxelSizeMicrometres));};
|
|
const auto distanceQ=[](std::int64_t dx,std::int64_t dy)
|
|
{
|
|
const auto square=static_cast<std::uint64_t>(dx*dx+dy*dy);
|
|
const auto floor=PassageIsqrt(square);return floor*floor==square?floor:floor+1;
|
|
};
|
|
for(std::size_t interface=0;interface+1<layout.strates.size();++interface)
|
|
{
|
|
const auto& upper=layout.strates[interface];const auto& lower=layout.strates[interface+1];
|
|
const auto* cfg=configFor(upper.strateIndex);
|
|
const std::int64_t outer=cfg?std::max(cfg->distanceMinimumSubvoxels,
|
|
cfg->distanceMaximumSubvoxels):24*qPerMetre;
|
|
const std::int64_t inner=cfg?std::min(cfg->distanceMinimumSubvoxels,
|
|
cfg->distanceMaximumSubvoxels):8*qPerMetre;
|
|
const std::int64_t placementRadius=std::max<std::int64_t>(1,outer);
|
|
// Leave room for the capsule and bounded wall relief at the floor/wall seam.
|
|
const bool upperIsSurfaceWorld=upper.generatorKind=="SurfaceWorld";
|
|
const std::int64_t minimumMouthRadius=static_cast<std::int64_t>(std::llround(
|
|
(upperIsSurfaceWorld?2.0:1.5)*qPerMetre));
|
|
const std::int64_t mouthRadius=std::max<std::int64_t>(minimumMouthRadius,
|
|
cfg?cfg->mouthRadiusSubvoxels:0);
|
|
const std::int64_t midRadius=std::max<std::int64_t>(
|
|
static_cast<std::int64_t>(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)),
|
|
cfg?cfg->midRadiusSubvoxels:mouthRadius);
|
|
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
|
|
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
|
|
const std::uint32_t style=cfg?std::min<std::uint32_t>(cfg->style,3u):1u;
|
|
const std::uint32_t segments=cfg?std::clamp<std::uint32_t>(cfg->segments,16u,48u):20u;
|
|
const std::int64_t wander=cfg?std::max<std::int64_t>(0,cfg->wanderSubvoxels):3*qPerMetre;
|
|
const std::int64_t spiralRadius=cfg?std::max<std::int64_t>(0,cfg->spiralRadiusSubvoxels):6*qPerMetre;
|
|
const std::int64_t spiralTurnsQ16=cfg?std::max<std::int64_t>(16384,cfg->spiralTurnsQ16):2ll<<16;
|
|
const std::int64_t verticalWobble=cfg?std::max<std::int64_t>(0,cfg->verticalWobbleSubvoxels):0;
|
|
const std::uint32_t connectionCount=cfg?static_cast<std::uint32_t>(std::clamp(cfg->connections,1,12)):1;
|
|
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
|
{
|
|
ResolvedLayoutPassageV1 passage;
|
|
passage.style=style;
|
|
passage.stableId="passage/"+std::to_string(upper.strateIndex)+"-"
|
|
+std::to_string(lower.strateIndex)+"/"+std::to_string(connection);
|
|
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
|
passage.placementRadiusSubvoxels=placementRadius;
|
|
passage.mouthRadiusSubvoxels=mouthRadius;passage.midRadiusSubvoxels=midRadius;
|
|
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
|
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
|
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
|
const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld;
|
|
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
|
if(connection==0)
|
|
for(const auto& anchor:upper.anchors)
|
|
if(anchor.kind=="passage_anchor"&&anchor.values.size()>=7)
|
|
{passageAnchor=&anchor;break;}
|
|
const bool anchoredIslandPassage=passageAnchor!=nullptr
|
|
&&upper.generatorKind=="FloatingIslands";
|
|
const bool anchoredSpiral=style==2&&anchoredIslandPassage;
|
|
if(!hasUpperRoute||!hasLowerRoute)
|
|
{
|
|
const auto xy=PassageRandomXY(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
|
connection,0x504c4143u),inner,placementRadius);
|
|
if(!hasUpperRoute)
|
|
{
|
|
upperMouth[0]=xy[0];upperMouth[1]=xy[1];
|
|
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
|
connection,0x55505248u),reachMinimum,reachMaximum);
|
|
upperMouth[2]=contentZQ(upper.content.bottomMicrometres)+reach;
|
|
}
|
|
if(!hasLowerRoute)
|
|
{
|
|
lowerMouth[0]=xy[0];lowerMouth[1]=xy[1];
|
|
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
|
connection,0x4c4f5752u),reachMinimum,reachMaximum);
|
|
lowerMouth[2]=contentZQ(lower.content.topMicrometres)-reach;
|
|
}
|
|
}
|
|
if(!radialWithin(upperMouth,placementRadius)||!radialWithin(lowerMouth,placementRadius))
|
|
{error=passage.stableId+" route mouth is outside the resolved PassageConfig.DistanceMax radius";return false;}
|
|
passage.upperMouthSubvoxels=upperMouth;passage.lowerMouthSubvoxels=lowerMouth;
|
|
|
|
constexpr std::int64_t LandingExtensionQ=8;
|
|
std::array<std::int64_t,2> upperTangent{},lowerTangent{};
|
|
const bool hasUpperTangent=hasUpperRoute
|
|
&&routeTangent(upper,true,upperTangent);
|
|
const bool hasPassageArrival=std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
|
[](const auto& anchor){return anchor.kind=="passage_arrival"
|
|
&&anchor.values.size()>=3;});
|
|
const bool hasLowerTangent=hasLowerRoute&&!hasPassageArrival
|
|
&&routeTangent(lower,false,lowerTangent);
|
|
const bool profiledSurfaceJoin=hasLowerTangent
|
|
&&std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
|
[](const auto& anchor){return anchor.kind=="heightfield_profile"
|
|
&&anchor.values.size()>=2&&anchor.values[0]>0;});
|
|
|
|
const std::int64_t outerRingRadius=std::max<std::int64_t>(1,
|
|
placementRadius-mouthRadius-qPerMetre/2);
|
|
const std::uint32_t pathStyle=anchoredSpiral?1u:style;
|
|
std::int64_t ringRadius=outerRingRadius;
|
|
if(pathStyle==1u)
|
|
ringRadius=std::min<std::int64_t>(outerRingRadius,
|
|
std::max<std::int64_t>(mouthRadius+2*qPerMetre,placementRadius*5/8));
|
|
else if(pathStyle==2u)
|
|
ringRadius=std::clamp(spiralRadius,
|
|
std::min(outerRingRadius,mouthRadius+qPerMetre/2),outerRingRadius);
|
|
else if(pathStyle==3u)
|
|
ringRadius=std::clamp(outerRingRadius
|
|
-std::min<std::int64_t>(outerRingRadius/3,
|
|
std::max<std::int64_t>(0,cfg?cfg->cascadeLedgeSubvoxels:0)
|
|
*static_cast<std::int64_t>(cfg?cfg->cascadeSteps:4)),
|
|
mouthRadius+2*qPerMetre,outerRingRadius);
|
|
if(surfaceGroundDeparture)
|
|
{
|
|
const double departureRadius=std::hypot(static_cast<double>(upperMouth[0]),
|
|
static_cast<double>(upperMouth[1]));
|
|
const std::int64_t clearRingRadius=static_cast<std::int64_t>(std::ceil(
|
|
departureRadius+mouthRadius+0.5*qPerMetre));
|
|
const std::int64_t maximumRingRadius=std::max<std::int64_t>(1,
|
|
placementRadius-qPerMetre);
|
|
ringRadius=std::min(maximumRingRadius,
|
|
std::max(ringRadius,clearRingRadius));
|
|
}
|
|
if(anchoredIslandPassage)
|
|
{
|
|
const std::int64_t craterRimRouteRadius=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=185
|
|
&&style==0&&!anchoredSpiral&&passageAnchor&&passageAnchor->values.size()>3
|
|
?std::max<std::int64_t>(midRadius,
|
|
passageAnchor->values[3]+2*qPerMetre)
|
|
:mouthRadius+qPerMetre/2;
|
|
ringRadius=std::min(ringRadius,anchoredSpiral
|
|
?mouthRadius+qPerMetre/2:craterRimRouteRadius);
|
|
}
|
|
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
|
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
|
if(hasUpperRoute)
|
|
{
|
|
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
|
const std::uint32_t hashedPhase=phase;
|
|
const std::int64_t phaseX=style==0&&anchoredIslandPassage
|
|
&&!anchoredSpiral&&passageAnchor
|
|
?upperMouth[0]-passageAnchor->values[0]
|
|
:(hasUpperTangent?upperTangent[0]:upperMouth[0]);
|
|
const std::int64_t phaseY=style==0&&anchoredIslandPassage
|
|
&&!anchoredSpiral&&passageAnchor
|
|
?upperMouth[1]-passageAnchor->values[1]
|
|
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
|
|
for(std::uint32_t offset=0;offset<16;++offset)
|
|
{
|
|
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
|
(hashedPhase+offset)%16);
|
|
const std::int64_t alignment=phaseX*directions[candidate][0]
|
|
+phaseY*directions[candidate][1];
|
|
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
|
{
|
|
const std::int64_t targetProjection=ringRadius*65536;
|
|
if(alignment<=0)continue;
|
|
const std::int64_t tangentError=std::abs(alignment-targetProjection);
|
|
if(tangentError<bestAlignment
|
|
||bestAlignment==std::numeric_limits<std::int64_t>::min())
|
|
{bestAlignment=tangentError;phase=candidate;}
|
|
}
|
|
else if(alignment>bestAlignment)
|
|
{bestAlignment=alignment;phase=candidate;}
|
|
}
|
|
}
|
|
if(hasUpperRoute&&upperIsSurfaceWorld)
|
|
{
|
|
const auto& directionAtPhase=directions[phase];
|
|
const std::int64_t radialAlignment=upperMouth[0]*directionAtPhase[0]
|
|
+upperMouth[1]*directionAtPhase[1];
|
|
if(radialAlignment<0)
|
|
{
|
|
std::int64_t bestOutwardAlignment=std::numeric_limits<std::int64_t>::min();
|
|
for(std::uint32_t candidate=0;candidate<directions.size();++candidate)
|
|
{
|
|
const std::int64_t alignment=upperMouth[0]*directions[candidate][0]
|
|
+upperMouth[1]*directions[candidate][1];
|
|
if(alignment>bestOutwardAlignment)
|
|
{bestOutwardAlignment=alignment;phase=candidate;}
|
|
}
|
|
}
|
|
}
|
|
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
|
connection,0x44495245u)&1u)!=0;
|
|
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
|
{
|
|
const auto& ringDirection=directions[phase];
|
|
const double radiusX=static_cast<double>(ringRadius)*ringDirection[0]/65536.0;
|
|
const double radiusY=static_cast<double>(ringRadius)*ringDirection[1]/65536.0;
|
|
const double anchorX=static_cast<double>(passageAnchor->values[0]);
|
|
const double anchorY=static_cast<double>(passageAnchor->values[1]);
|
|
const double connectorX=anchorX+radiusX-upperMouth[0];
|
|
const double connectorY=anchorY+radiusY-upperMouth[1];
|
|
clockwise=connectorX*(-radiusY)+connectorY*radiusX>=0.0;
|
|
}
|
|
else if(anchoredSpiral&&passageAnchor&&hasUpperTangent)
|
|
{
|
|
const double radialX=static_cast<double>(upperMouth[0]-passageAnchor->values[0]);
|
|
const double radialY=static_cast<double>(upperMouth[1]-passageAnchor->values[1]);
|
|
const double clockwiseTangentX=-radialY,clockwiseTangentY=radialX;
|
|
clockwise=clockwiseTangentX*upperTangent[0]
|
|
+clockwiseTangentY*upperTangent[1]>=0.0;
|
|
}
|
|
const int direction=clockwise?1:-1;
|
|
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract.
|
|
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
|
|
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
|
|
const std::int64_t wormCenterWobble=std::min<std::int64_t>(
|
|
std::max<std::int64_t>(0,wander/6),qPerMetre);
|
|
const auto ringPoint=[&](std::uint32_t directionIndex,std::uint32_t loopIndex=0)
|
|
{
|
|
const double angle=static_cast<double>(directionIndex)*
|
|
(3.14159265358979323846/8.0);
|
|
double radius=static_cast<double>(ringRadius);
|
|
double centerX=0.0,centerY=0.0;
|
|
if(pathStyle==1u)
|
|
{
|
|
const double loop=static_cast<double>(loopIndex);
|
|
const double phaseA=loop*0.6180339887498948+0.37;
|
|
const double phaseB=loop*0.4142135623730950+1.11;
|
|
const double jitterX=static_cast<double>(wormCenterWobble)*std::sin(phaseB);
|
|
const double jitterY=static_cast<double>(wormCenterWobble)*std::cos(phaseB);
|
|
radius+=static_cast<double>(wormRadialWobble)
|
|
*std::sin(angle+phaseA);
|
|
centerX=jitterX+(anchoredIslandPassage&&passageAnchor
|
|
?static_cast<double>(passageAnchor->values[0]):0.0);
|
|
centerY=jitterY+(anchoredIslandPassage&&passageAnchor
|
|
?static_cast<double>(passageAnchor->values[1]):0.0);
|
|
}
|
|
else if(pathStyle==3u)
|
|
{
|
|
const std::uint32_t steps=std::max<std::uint32_t>(1,
|
|
cfg?cfg->cascadeSteps:4u);
|
|
const double ledge=std::min<double>(ringRadius/4,
|
|
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
|
cfg->cascadeLedgeSubvoxels):0));
|
|
// Give each turn a seeded, shallow angular profile so the same cosine
|
|
// scallop cannot repeat at full ledge depth on every revolution.
|
|
const std::uint32_t controlCount=std::clamp(steps*2u,8u,
|
|
segments/2u);
|
|
const double coordinate=angle*controlCount
|
|
/(2.0*3.14159265358979323846);
|
|
const std::uint32_t control0=static_cast<std::uint32_t>(std::floor(coordinate));
|
|
const std::uint32_t control1=(control0+1u)%controlCount;
|
|
const double blend=coordinate-std::floor(coordinate);
|
|
const auto controlValue=[&](std::uint32_t control)
|
|
{
|
|
const std::uint32_t salt=0x43415343u
|
|
^(loopIndex*0x9e3779b9u)^(control*0x85ebca6bu);
|
|
const std::uint32_t bits=static_cast<std::uint32_t>(PassageSeed(seed,
|
|
upper.strateIndex,lower.strateIndex,connection,salt)>>40);
|
|
return 2.0*static_cast<double>(bits)/16777215.0-1.0;
|
|
};
|
|
const double smooth=blend*blend*(3.0-2.0*blend);
|
|
const double irregular=std::lerp(controlValue(control0),
|
|
controlValue(control1),smooth);
|
|
const std::uint32_t loopHash=static_cast<std::uint32_t>(PassageSeed(seed,
|
|
upper.strateIndex,lower.strateIndex,connection,
|
|
0x4c454447u+loopIndex)>>40);
|
|
const double amplitude=0.60+0.40
|
|
*static_cast<double>(loopHash)/16777215.0;
|
|
radius-=ledge*amplitude*(0.24+0.10*irregular);
|
|
}
|
|
if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor)
|
|
{
|
|
centerX=static_cast<double>(passageAnchor->values[0]);
|
|
centerY=static_cast<double>(passageAnchor->values[1]);
|
|
}
|
|
return std::array<std::int64_t,2>{
|
|
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
|
|
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
|
|
};
|
|
std::vector<std::array<std::int64_t,2>> xy;
|
|
const auto pushXY=[&](std::array<std::int64_t,2> p)
|
|
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
|
|
std::size_t helixEndIndex=0;
|
|
std::int64_t helixBedrockZ=0;
|
|
bool hasAnchorHelix=false;
|
|
pushXY({upperMouth[0],upperMouth[1]});
|
|
if(anchoredSpiral&&passageAnchor&&upper.bedrock.presentInCurrentField
|
|
&&spiralRadius>0)
|
|
{
|
|
const std::int64_t anchorX=passageAnchor->values[0];
|
|
const std::int64_t anchorY=passageAnchor->values[1];
|
|
const std::int64_t rootRadius=passageAnchor->values[2];
|
|
const std::int64_t rootBottom=passageAnchor->values[5];
|
|
const std::int64_t bedrockTop=rootBottom!=0?rootBottom
|
|
:contentZQ(upper.bedrock.topMicrometres);
|
|
// Passage route heights are player-center positions. End one capsule
|
|
// half-height above the bedrock top so the final ramp step is walkable.
|
|
helixBedrockZ=bedrockTop+qPerMetre;
|
|
const std::int64_t helixRadius=spiralRadius;
|
|
if(rootRadius<helixRadius+midRadius+qPerMetre/4
|
|
||std::hypot(static_cast<double>(upperMouth[0]-anchorX),
|
|
static_cast<double>(upperMouth[1]-anchorY))
|
|
>static_cast<double>(rootRadius+qPerMetre/2)
|
|
||upperMouth[2]<=helixBedrockZ)
|
|
{error=passage.stableId+" anchor root cannot contain its authored spiral mouth or bedrock descent";return false;}
|
|
const double drop=static_cast<double>(upperMouth[2]-helixBedrockZ);
|
|
const double pointsPerTurn=static_cast<double>(segments);
|
|
const double polygonCircumference=2.0*3.14159265358979323846
|
|
*static_cast<double>(helixRadius)
|
|
*(std::sin(3.14159265358979323846/pointsPerTurn)
|
|
/(3.14159265358979323846/pointsPerTurn));
|
|
const double horizontalPerTurn=polygonCircumference
|
|
*static_cast<double>(TargetGradeQ16)/65536.0;
|
|
const std::uint32_t authoredTurns=static_cast<std::uint32_t>(std::max<std::int64_t>(1,
|
|
(spiralTurnsQ16+65535)/65536));
|
|
const std::uint32_t gradeTurns=static_cast<std::uint32_t>(std::max(1.0,
|
|
std::ceil(drop/std::max(1.0,horizontalPerTurn))));
|
|
// The first coil grows from the island mouth offset to the full authored
|
|
// radius, so reserve one extra turn for that shorter opening revolution.
|
|
const std::uint32_t turns=std::max(authoredTurns,gradeTurns+1);
|
|
if(turns>512)
|
|
{error=passage.stableId+" authored spiral radius needs more than 512 turns to meet walkable grade";return false;}
|
|
const double startAngle=std::atan2(static_cast<double>(upperMouth[1]-anchorY),
|
|
static_cast<double>(upperMouth[0]-anchorX));
|
|
const std::uint32_t helixPoints=turns*segments;
|
|
for(std::uint32_t point=1;point<=helixPoints;++point)
|
|
{
|
|
const double t=static_cast<double>(point)/helixPoints;
|
|
const double angle=startAngle+static_cast<double>(clockwise?1:-1)
|
|
*2.0*3.14159265358979323846*t*turns;
|
|
const double radialT=std::min(1.0,static_cast<double>(point)/segments);
|
|
const double mouthOffset=std::clamp(static_cast<double>(passageAnchor->values[3]),
|
|
0.0,static_cast<double>(helixRadius));
|
|
const double radial=mouthOffset+(static_cast<double>(helixRadius)-mouthOffset)
|
|
*radialT;
|
|
const std::array<std::int64_t,2> p{
|
|
static_cast<std::int64_t>(std::llround(anchorX
|
|
+radial*std::cos(angle))),
|
|
static_cast<std::int64_t>(std::llround(anchorY
|
|
+radial*std::sin(angle)))};
|
|
pushXY(p);
|
|
}
|
|
helixEndIndex=xy.size()-1;
|
|
hasAnchorHelix=helixEndIndex>0;
|
|
}
|
|
const std::int64_t routeGradeQ16=profiledSurfaceJoin?8000:hasAnchorHelix
|
|
&&lower.generatorKind=="SurfaceWorld"?8000
|
|
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500:TargetGradeQ16;
|
|
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
|
if(hasAnchorHelix&&pathStyle==1u)
|
|
{
|
|
// Let the anchored helix widen into the winding inter-strate route
|
|
// over a full, smooth coil. A direct jump from the root radius to the
|
|
// outer worm ring creates a sharp support seam inside the rock neck.
|
|
const double anchorX=static_cast<double>(passageAnchor->values[0]);
|
|
const double anchorY=static_cast<double>(passageAnchor->values[1]);
|
|
const double startAngle=std::atan2(static_cast<double>(upperMouth[1])-anchorY,
|
|
static_cast<double>(upperMouth[0])-anchorX);
|
|
const double targetX=static_cast<double>(firstRing[0])-anchorX;
|
|
const double targetY=static_cast<double>(firstRing[1])-anchorY;
|
|
const double targetAngle=std::atan2(targetY,targetX);
|
|
const double angleDelta=static_cast<double>(direction)*2.0
|
|
*3.14159265358979323846
|
|
+std::remainder(targetAngle-startAngle,2.0*3.14159265358979323846);
|
|
const double startRadius=static_cast<double>(spiralRadius);
|
|
const std::int64_t targetDx=firstRing[0]-passageAnchor->values[0];
|
|
const std::int64_t targetDy=firstRing[1]-passageAnchor->values[1];
|
|
const double targetRadius=static_cast<double>(PassageIsqrt(
|
|
static_cast<std::uint64_t>(targetDx*targetDx+targetDy*targetDy)));
|
|
const std::uint32_t transitionSegments=static_cast<std::uint32_t>(
|
|
std::clamp(std::ceil(std::abs(angleDelta)*(startRadius+targetRadius)
|
|
/(2.0*0.75*qPerMetre)),static_cast<double>(segments),256.0));
|
|
for(std::uint32_t step=1;step<=transitionSegments;++step)
|
|
{
|
|
const double t=static_cast<double>(step)/transitionSegments;
|
|
const double smooth=t*t*(3.0-2.0*t);
|
|
const double radius=startRadius+(targetRadius-startRadius)*smooth;
|
|
const double angle=startAngle+angleDelta*t;
|
|
const std::array<std::int64_t,2> point=step==transitionSegments?firstRing:
|
|
std::array<std::int64_t,2>{
|
|
static_cast<std::int64_t>(std::llround(anchorX+radius*std::cos(angle))),
|
|
static_cast<std::int64_t>(std::llround(anchorY+radius*std::sin(angle)))};
|
|
pushXY(point);
|
|
}
|
|
}
|
|
pushXY(firstRing);
|
|
std::uint64_t totalHorizontal=0;std::uint32_t loops=0;
|
|
const std::int64_t verticalDistance=hasAnchorHelix
|
|
?std::abs(lowerMouth[2]-helixBedrockZ):std::abs(lowerMouth[2]-upperMouth[2]);
|
|
auto measurePath=[&]()
|
|
{
|
|
std::uint64_t total=0;
|
|
const std::size_t start=hasAnchorHelix?helixEndIndex:0;
|
|
for(std::size_t i=start+1;i<xy.size();++i)
|
|
total+=distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
|
|
if(!hasLowerTangent&&!xy.empty())
|
|
total+=distanceQ(lowerMouth[0]-xy.back()[0],lowerMouth[1]-xy.back()[1]);
|
|
return total;
|
|
};
|
|
while(loops<256)
|
|
{
|
|
totalHorizontal=measurePath();
|
|
if(totalHorizontal*static_cast<std::uint64_t>(routeGradeQ16)
|
|
>=static_cast<std::uint64_t>(verticalDistance)*65536ull)break;
|
|
for(int step=1;step<=16;++step)
|
|
{
|
|
int directionIndex=static_cast<int>(phase)+direction*step;
|
|
directionIndex=(directionIndex%16+16)%16;
|
|
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),
|
|
loops+(step==16?1u:0u)));
|
|
}
|
|
++loops;
|
|
}
|
|
if(hasLowerTangent)
|
|
{
|
|
std::uint32_t lowerPhase=static_cast<std::uint32_t>(PassageSeed(seed,
|
|
upper.strateIndex,lower.strateIndex,connection,0x4c505248u)%16);
|
|
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
|
bool lowerRouteReversed=false;
|
|
const std::uint32_t hashedLowerPhase=lowerPhase;
|
|
for(std::uint32_t offset=0;offset<16;++offset)
|
|
{
|
|
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
|
(hashedLowerPhase+offset)%16);
|
|
const auto terminal=ringPoint(candidate,loops);
|
|
const std::int64_t dx=lowerMouth[0]-terminal[0];
|
|
const std::int64_t dy=lowerMouth[1]-terminal[1];
|
|
const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1];
|
|
const std::int64_t cross=std::abs(dx*lowerTangent[1]-dy*lowerTangent[0]);
|
|
if(alignment<=0||cross==0)continue;
|
|
const auto length=distanceQ(dx,dy);
|
|
const std::int64_t score=alignment*65536/static_cast<std::int64_t>(length);
|
|
if(score>bestAlignment)
|
|
{bestAlignment=score;lowerPhase=candidate;}
|
|
}
|
|
// A route is walkable in either direction. If the approach ring lies on
|
|
// the wrong side of its first route tangent, attach to the same endpoint
|
|
// while traversing the route backwards instead of rejecting the layout.
|
|
if(bestAlignment==std::numeric_limits<std::int64_t>::min())
|
|
{
|
|
for(std::uint32_t offset=0;offset<16;++offset)
|
|
{
|
|
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
|
(hashedLowerPhase+offset)%16);
|
|
const auto terminal=ringPoint(candidate,loops);
|
|
const std::int64_t dx=lowerMouth[0]-terminal[0];
|
|
const std::int64_t dy=lowerMouth[1]-terminal[1];
|
|
const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1];
|
|
const std::int64_t cross=std::abs(dx*lowerTangent[1]-dy*lowerTangent[0]);
|
|
if(alignment>=0||cross==0)continue;
|
|
const auto length=distanceQ(dx,dy);
|
|
const std::int64_t score=-alignment*65536/static_cast<std::int64_t>(length);
|
|
if(score>bestAlignment)
|
|
{bestAlignment=score;lowerPhase=candidate;lowerRouteReversed=true;}
|
|
}
|
|
}
|
|
if(bestAlignment==std::numeric_limits<std::int64_t>::min())
|
|
{error=passage.stableId+" could not find a non-overlapping lower landing approach";return false;}
|
|
std::printf("[ResolvedPassageLowerApproach] id=%s direction=%s alignment_q16=%lld status=PASS\n",
|
|
passage.stableId.c_str(),lowerRouteReversed?"reverse":"forward",
|
|
static_cast<long long>(bestAlignment));
|
|
if(phase!=lowerPhase)
|
|
for(int step=1;step<=16;++step)
|
|
{
|
|
int directionIndex=static_cast<int>(phase)+direction*step;
|
|
directionIndex=(directionIndex%16+16)%16;
|
|
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
|
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
|
}
|
|
pushXY(ringPoint(lowerPhase));
|
|
}
|
|
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
|
std::size_t lowerMouthIndex=xy.size()-1;
|
|
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
|
if(hasLowerTangent)
|
|
{
|
|
std::array<std::int64_t,3> previousRoutePoint=lowerMouth;
|
|
std::int64_t remainingQ=LandingExtensionQ*qPerMetre;
|
|
bool skippedMouth=false;
|
|
for(const auto& anchor:lower.anchors)
|
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
|
{
|
|
const std::array<std::int64_t,3> routePoint{{anchor.values[0],
|
|
anchor.values[1],anchor.values[2]}};
|
|
if(!skippedMouth){skippedMouth=true;continue;}
|
|
if(remainingQ<=0)break;
|
|
const std::int64_t dx=routePoint[0]-previousRoutePoint[0];
|
|
const std::int64_t dy=routePoint[1]-previousRoutePoint[1];
|
|
const std::uint64_t segmentLength=distanceQ(dx,dy);
|
|
if(segmentLength==0)continue;
|
|
const std::int64_t advance=std::min<std::int64_t>(remainingQ,
|
|
static_cast<std::int64_t>(segmentLength));
|
|
std::array<std::int64_t,3> landingPoint{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
landingPoint[axis]=previousRoutePoint[axis]+PassageRoundRatio(
|
|
routePoint[axis]-previousRoutePoint[axis],advance,
|
|
static_cast<std::int64_t>(segmentLength));
|
|
if(!radialWithin(landingPoint,placementRadius))break;
|
|
lowerLandingPoints.push_back(landingPoint);
|
|
pushXY({landingPoint[0],landingPoint[1]});
|
|
previousRoutePoint=landingPoint;remainingQ-=advance;
|
|
if(advance<static_cast<std::int64_t>(segmentLength))break;
|
|
}
|
|
if(lowerLandingPoints.empty())
|
|
{error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;}
|
|
}
|
|
if(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188
|
|
&&style==0&&anchoredIslandPassage&&!anchoredSpiral&&xy.size()>4)
|
|
{
|
|
std::vector<std::array<std::int64_t,2>> refined;
|
|
refined.reserve(xy.size()*2);
|
|
for(std::size_t index=0;index+1<xy.size();++index)
|
|
{
|
|
refined.push_back(xy[index]);
|
|
if(index<3)continue;
|
|
refined.push_back({
|
|
static_cast<std::int64_t>(std::llround(0.5*(
|
|
static_cast<double>(xy[index][0])
|
|
+static_cast<double>(xy[index+1][0])))),
|
|
static_cast<std::int64_t>(std::llround(0.5*(
|
|
static_cast<double>(xy[index][1])
|
|
+static_cast<double>(xy[index+1][1]))))});
|
|
}
|
|
refined.push_back(xy.back());
|
|
if(lowerMouthIndex>3)lowerMouthIndex+=lowerMouthIndex-3;
|
|
xy.swap(refined);
|
|
}
|
|
totalHorizontal=measurePath();
|
|
if(loops>=256||totalHorizontal==0)
|
|
{error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;}
|
|
std::vector<std::uint64_t> cumulative(xy.size(),0);
|
|
for(std::size_t i=1;i<xy.size();++i)
|
|
cumulative[i]=cumulative[i-1]+distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
|
|
const std::uint64_t mouthHorizontal=cumulative[lowerMouthIndex];
|
|
const std::uint64_t helixHorizontal=hasAnchorHelix
|
|
?cumulative[helixEndIndex]:0;
|
|
if(mouthHorizontal==0)
|
|
{error=passage.stableId+" has no horizontal reach before its lower mouth";return false;}
|
|
if(hasAnchorHelix&&helixHorizontal==0)
|
|
{error=passage.stableId+" anchor spiral has no horizontal reach";return false;}
|
|
passage.controlPoints.reserve(xy.size());
|
|
const std::int64_t surfacePortalRadius=surfaceGroundDeparture
|
|
?std::min(mouthRadius,static_cast<std::int64_t>(std::llround(1.5*qPerMetre)))
|
|
:mouthRadius;
|
|
const std::uint64_t surfaceMouthBlendLength=static_cast<std::uint64_t>(
|
|
std::max<std::int64_t>(8*qPerMetre,2*mouthRadius));
|
|
for(std::size_t i=0;i<xy.size();++i)
|
|
{
|
|
std::int64_t radius=(i<=1||i==lowerMouthIndex
|
|
||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
|
if(surfaceGroundDeparture&&i<lowerMouthIndex)
|
|
{
|
|
const double t=std::clamp(static_cast<double>(cumulative[i])
|
|
/surfaceMouthBlendLength,0.0,1.0);
|
|
const double smooth=t*t*(3.0-2.0*t);
|
|
radius=surfacePortalRadius+static_cast<std::int64_t>(std::llround(
|
|
(midRadius-surfacePortalRadius)*smooth));
|
|
}
|
|
std::int64_t playerZ=0;
|
|
if(i>lowerMouthIndex)
|
|
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
|
|
else if(hasAnchorHelix&&i<=helixEndIndex)
|
|
playerZ=upperMouth[2]+PassageRoundRatio(helixBedrockZ-upperMouth[2],
|
|
static_cast<std::int64_t>(cumulative[i]),
|
|
static_cast<std::int64_t>(helixHorizontal));
|
|
else
|
|
{
|
|
const std::uint64_t segmentStart=hasAnchorHelix?helixHorizontal:0;
|
|
const std::int64_t startZ=hasAnchorHelix?helixBedrockZ:upperMouth[2];
|
|
const std::uint64_t segmentLength=mouthHorizontal-segmentStart;
|
|
playerZ=startZ+PassageRoundRatio(lowerMouth[2]-startZ,
|
|
static_cast<std::int64_t>(cumulative[i]-segmentStart),
|
|
static_cast<std::int64_t>(segmentLength));
|
|
}
|
|
ResolvedLayoutPassagePointV1 point;
|
|
point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ};
|
|
point.radiusSubvoxels=radius;passage.controlPoints.push_back(point);
|
|
}
|
|
if(surfaceGroundDeparture)
|
|
std::printf("[ResolvedSurfaceWorldDepartureMouth] passage=%s ground_floor_z_m=%.3f portal_radius_m=%.3f tunnel_radius_m=%.3f blend_length_m=%.3f departure_xy_m=(%.3f,%.3f) first_tunnel_xy_m=(%.3f,%.3f) first_tunnel_floor_z_m=%.3f first_tunnel_radius_m=%.3f\n",
|
|
passage.stableId.c_str(),(upperMouth[2]-static_cast<std::int64_t>(std::llround(0.88*qPerMetre)))/static_cast<double>(qPerMetre),
|
|
surfacePortalRadius/static_cast<double>(qPerMetre),
|
|
midRadius/static_cast<double>(qPerMetre),
|
|
surfaceMouthBlendLength/static_cast<double>(qPerMetre),
|
|
upperMouth[0]/static_cast<double>(qPerMetre),
|
|
upperMouth[1]/static_cast<double>(qPerMetre),
|
|
passage.controlPoints.size()>1?passage.controlPoints[1].positionSubvoxels[0]
|
|
/static_cast<double>(qPerMetre):0.0,
|
|
passage.controlPoints.size()>1?passage.controlPoints[1].positionSubvoxels[1]
|
|
/static_cast<double>(qPerMetre):0.0,
|
|
passage.controlPoints.size()>1?
|
|
(passage.controlPoints[1].positionSubvoxels[2]
|
|
-passage.controlPoints[1].radiusSubvoxels+playerCenterAboveFloorQ
|
|
-static_cast<std::int64_t>(std::llround(0.88*qPerMetre)))
|
|
/static_cast<double>(qPerMetre):0.0,
|
|
passage.controlPoints.size()>1?passage.controlPoints[1].radiusSubvoxels
|
|
/static_cast<double>(qPerMetre):0.0);
|
|
passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2]
|
|
+passage.controlPoints.front().radiusSubvoxels-playerCenterAboveFloorQ;
|
|
passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2]
|
|
+passage.controlPoints[lowerMouthIndex].radiusSubvoxels
|
|
-playerCenterAboveFloorQ;
|
|
layout.passages.push_back(std::move(passage));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3()
|
|
{
|
|
ResolvedLayoutValidatorAuditV3 result;
|
|
auto make=[](std::string id,std::int32_t order,std::int64_t top,std::int64_t bottom)
|
|
{
|
|
ResolvedLayoutStratumV2 s;s.stableId=std::move(id);s.order=order;s.strateIndex=order;s.generatorKind="synthetic";
|
|
s.interiorTopMicrometres=top;s.interiorBottomMicrometres=bottom;s.voxelSizeMicrometres=250000;
|
|
s.transitionMode="Gradient";s.transitionWidthMicrometres=4000000;
|
|
s.transitionHonouredInCore=true;
|
|
s.ceiling={"ceiling",top,top+1000000,true,true,false,"closed ceiling band","upper face","synthetic"};
|
|
s.content={"content",bottom,top,true,true,false,"generator content","none","none"};
|
|
s.bedrock={"bedrock",bottom-1000000,bottom,true,true,false,"breakable bedrock","lower face","none"};
|
|
return s;
|
|
};
|
|
const auto expectReject=[&](std::string name,ResolvedLayoutSnapshotV3 candidate)
|
|
{
|
|
++result.controlsRun;std::vector<ResolvedLayoutIssueV2> issues;std::string error;
|
|
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,error))++result.controlsRejectedAsExpected;
|
|
else result.failures.push_back({"negative_control_accepted",std::move(name)+" was accepted by strict validator"});
|
|
};
|
|
auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000;
|
|
base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)};
|
|
ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1;
|
|
passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280;
|
|
passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048;
|
|
passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240;
|
|
std::string planError;
|
|
if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError))
|
|
result.failures.push_back({"synthetic_passage_plan_failed",planError});
|
|
{std::vector<ResolvedLayoutIssueV2> issues;std::string error;
|
|
if(!base.validate(ResolvedLayoutValidationModeV3::strict,issues,error))
|
|
result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"});}
|
|
auto missingPassage=base;missingPassage.passages.clear();expectReject("missing interface passage",missingPassage);
|
|
auto outsidePassage=base;
|
|
if(!outsidePassage.passages.empty())
|
|
{
|
|
outsidePassage.passages.front().upperMouthSubvoxels[0]
|
|
=outsidePassage.passages.front().placementRadiusSubvoxels+1;
|
|
outsidePassage.passages.front().controlPoints.front().positionSubvoxels[0]
|
|
=outsidePassage.passages.front().upperMouthSubvoxels[0];
|
|
}
|
|
expectReject("passage mouth outside placement radius",outsidePassage);
|
|
auto boundaryGap=base;boundaryGap.strates[1].ceiling.topMicrometres-=1;
|
|
expectReject("bedrock/next-ceiling gap",boundaryGap);
|
|
auto missingBedrock=base;missingBedrock.strates[0].bedrock.presentInCurrentField=false;expectReject("missing bedrock",missingBedrock);
|
|
auto missingCeiling=base;missingCeiling.strates[0].ceiling.presentInCurrentField=false;expectReject("missing ceiling",missingCeiling);
|
|
auto wrongOrder=base;wrongOrder.strates[0].content.kind="bedrock";expectReject("wrong band order",wrongOrder);
|
|
auto largeOverlap=base;largeOverlap.strates[0].transitionInterpenetrationMicrometres=4000001;expectReject("interpenetration exceeds transition width",largeOverlap);
|
|
auto thinBand=base;
|
|
thinBand.strates[0].bedrock.topMicrometres=thinBand.strates[0].bedrock.bottomMicrometres
|
|
+2ll*base.coreVoxelSizeMicrometres-1;
|
|
thinBand.strates[0].content.bottomMicrometres=thinBand.strates[0].bedrock.topMicrometres;
|
|
expectReject("bedrock band below minimum thickness",thinBand);
|
|
auto genericOverlap=base;genericOverlap.strates[0].bedrock.bottomMicrometres-=1;expectReject("nominal band overlap",genericOverlap);
|
|
auto genericGap=base;genericGap.strates[0].bedrock.bottomMicrometres+=1;expectReject("nominal band gap",genericGap);
|
|
result.passed=result.controlsRun==result.controlsRejectedAsExpected&&result.failures.empty();return result;
|
|
}
|
|
}
|