wip(assembler): round L23 -- inter-strate passages planned from the resolved layout and carved

After L22 the FI -> SW passage no longer connected the strates (legacy owner-voxel coordinates,
lower end 23 m above SW), and Core never carved passages through the bands. Now the director
plans passages from the resolved layout (PlanResolvedLayoutPassagesV1; deterministic hash of
seed/strates/connection; placement disk from PassageConfig, 8-24 m around (0,0)); shared
composition applies generator field -> bands -> bounded passage carve and support shelf, opening
the bands only along the tunnel. Mouths bind to the strate routes. Capsule audit: 0 blocked /
0 unsupported (325,962 samples FI, 284,319 TunnelNetwork), negative controls detected; all 8
generator kinds exercised. SW away from the passage is bit-identical. Layout V3, evaluator 9.
Open: look not yet natural (stepped tube in FI void, terraced SW arrival); SW-as-upper route
planning fails; Unreal adapter uncompiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-20 18:22:12 +02:00
1 parent 55c26a05a7
commit 71d09a8c52
14 files changed
+1465 -112

No files matched your search

@@ -249,8 +249,8 @@ namespace VoxelForgeCore
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
// Version 7 binds plans to the physical strate-layout chunk width and shared boundary bands.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 7;
// Version 9 indexes passage landings so non-monotone route tails stay carveable.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 9;
enum class FieldChannel : std::uint32_t
{
@@ -573,8 +573,8 @@ namespace VoxelForgeCore
// Read-only, versioned account of the physical layout resolved by the director and the
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
// source fixed-point units so this record never rounds the recipe itself.
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV2 = 2;
enum class ResolvedLayoutValidationModeV2 : std::uint8_t { compatibility, strict };
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
struct ResolvedLayoutAnchorV2
{
@@ -635,34 +635,73 @@ namespace VoxelForgeCore
std::string detail;
};
struct VFCORE_API ResolvedLayoutSnapshotV2
struct ResolvedLayoutPassagePointV1
{
std::uint32_t schemaVersion = ResolvedLayoutSchemaVersionV2;
std::array<std::int64_t, 3> positionSubvoxels{};
std::int64_t radiusSubvoxels = 0;
};
struct ResolvedLayoutPassageV1
{
std::string stableId;
std::int32_t upperStrateIndex = -1;
std::int32_t lowerStrateIndex = -1;
std::int64_t placementRadiusSubvoxels = 0;
std::array<std::int64_t, 3> upperMouthSubvoxels{};
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
std::int64_t mouthRadiusSubvoxels = 0;
std::int64_t midRadiusSubvoxels = 0;
// Derived from the mouth binding; the main descent is monotone through this point.
std::size_t lowerMouthControlIndex = 0;
std::vector<ResolvedLayoutPassagePointV1> controlPoints;
};
struct ResolvedLayoutPassageConfigV1
{
std::int32_t upperStrateIndex = -1;
std::int32_t connections = 1;
std::int64_t mouthRadiusSubvoxels = 0;
std::int64_t midRadiusSubvoxels = 0;
std::int64_t reachMinimumSubvoxels = 0;
std::int64_t reachMaximumSubvoxels = 0;
std::int64_t distanceMinimumSubvoxels = 0;
std::int64_t distanceMaximumSubvoxels = 0;
std::int64_t wanderSubvoxels = 0;
};
struct VFCORE_API ResolvedLayoutSnapshotV3
{
std::uint32_t schemaVersion = ResolvedLayoutSchemaVersionV3;
std::string worldIdentity;
std::uint32_t ownerVoxelSizeMicrometres = 0;
std::uint32_t coreVoxelSizeMicrometres = 0;
std::uint32_t strateLayoutChunkSizeMicrometres = 8000000;
std::int64_t strateLayoutChunkWidthSubvoxels = 8192;
std::vector<ResolvedLayoutStratumV2> strates;
std::vector<ResolvedLayoutPassageV1> passages;
std::string stableIdentity;
bool finalize(std::string& error);
bool validate(ResolvedLayoutValidationModeV2 mode,
bool validate(ResolvedLayoutValidationModeV3 mode,
std::vector<ResolvedLayoutIssueV2>& issues,
std::string& error) const;
std::string ToJson() const;
static std::optional<ResolvedLayoutSnapshotV2> FromJson(
static std::optional<ResolvedLayoutSnapshotV3> FromJson(
std::string_view json, std::string& error);
};
struct ResolvedLayoutValidatorAuditV2
struct ResolvedLayoutValidatorAuditV3
{
std::uint32_t controlsRun = 0;
std::uint32_t controlsRejectedAsExpected = 0;
bool passed = false;
std::vector<ResolvedLayoutIssueV2> failures;
};
VFCORE_API ResolvedLayoutValidatorAuditV2 AuditResolvedLayoutValidatorV2();
VFCORE_API ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3();
VFCORE_API bool PlanResolvedLayoutPassagesV1(
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
std::string& error);
struct FloatingIslandBoundsAuditResult
{
@@ -941,7 +980,8 @@ namespace VoxelForgeCore
explicit operator bool() const noexcept { return static_cast<bool>(data_); }
const WorldConfiguration& configuration() const;
const WorldAssemblyManifest* worldAssembly() const noexcept;
const ResolvedLayoutSnapshotV2* resolvedLayout() const noexcept;
const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept;
ResolvedWorldSnapshot withoutResolvedLayoutPassages() const;
private:
struct Data;
+526 -18
View File
@@ -4,7 +4,9 @@
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <limits>
#include <sstream>
namespace VoxelForgeCore
@@ -66,7 +68,7 @@ namespace
b.behaviour=value.Get("behaviour").String();
b.densitySide=value.Get("density_side").String();b.capPath=value.Get("cap_path").String();return b;
}
std::string LayoutBodyJson(const ResolvedLayoutSnapshotV2& s)
std::string LayoutBodyJson(const ResolvedLayoutSnapshotV3& s)
{
std::ostringstream out;out<<"{\"schema_version\":"<<s.schemaVersion
<<",\"world_identity\":"<<LayoutEscape(s.worldIdentity)
@@ -113,6 +115,29 @@ namespace
}
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
<<",\"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<<"]}";return out.str();
}
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
@@ -121,19 +146,76 @@ namespace
{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};
}
}
std::string ResolvedLayoutSnapshotV2::ToJson() const
std::string ResolvedLayoutSnapshotV3::ToJson() const
{
const std::string body=LayoutBodyJson(*this);
return body.substr(0,body.size()-1)+",\"stable_identity\":"
+LayoutEscape(stableIdentity.empty()?"resolved-layout-v2-fnv64:"+LayoutHex(LayoutHash(body)):stableIdentity)+"}";
+LayoutEscape(stableIdentity.empty()?"resolved-layout-v3-fnv64:"+LayoutHex(LayoutHash(body)):stableIdentity)+"}";
}
bool ResolvedLayoutSnapshotV2::finalize(std::string& error)
bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
{
error.clear();
if(schemaVersion!=ResolvedLayoutSchemaVersionV2||worldIdentity.empty()||strates.empty()
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
||strateLayoutChunkSizeMicrometres==0||strateLayoutChunkWidthSubvoxels<=0)
{error="resolved layout has an unsupported version, empty world identity, or no strates";return false;}
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
@@ -149,22 +231,37 @@ bool ResolvedLayoutSnapshotV2::finalize(std::string& error)
if(x.interiorTopMicrometres<=x.interiorBottomMicrometres||x.voxelSizeMicrometres==0)
{error="resolved layout contains an invalid interior interval or voxel size";return false;}
}
const std::int64_t playerCenterAboveFloorQ=static_cast<std::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-v2-fnv64:"+LayoutHex(LayoutHash(body));
stableIdentity="resolved-layout-v3-fnv64:"+LayoutHex(LayoutHash(body));
std::vector<ResolvedLayoutIssueV2> issues;
return validate(ResolvedLayoutValidationModeV2::compatibility,issues,error);
return validate(ResolvedLayoutValidationModeV3::compatibility,issues,error);
}
bool ResolvedLayoutSnapshotV2::validate(ResolvedLayoutValidationModeV2 mode,
bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
std::vector<ResolvedLayoutIssueV2>& issues,std::string& error) const
{
issues.clear();error.clear();
if(schemaVersion!=ResolvedLayoutSchemaVersionV2||worldIdentity.empty()||strates.empty()
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
||strateLayoutChunkSizeMicrometres==0||strateLayoutChunkWidthSubvoxels<=0)
{error="resolved layout schema/world/strata header is invalid";return false;}
const auto add=[&](std::string code,std::string detail)
{issues.push_back({std::move(code),std::move(detail)});};
const bool strict=mode==ResolvedLayoutValidationModeV2::strict;
const bool strict=mode==ResolvedLayoutValidationModeV3::strict;
for(std::size_t i=0;i<strates.size();++i)
{
const auto& s=strates[i];
@@ -262,16 +359,110 @@ bool ResolvedLayoutSnapshotV2::validate(ResolvedLayoutValidationModeV2 mode,
}
}
}
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(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
return true;
}
std::optional<ResolvedLayoutSnapshotV2> ResolvedLayoutSnapshotV2::FromJson(
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;}
ResolvedLayoutSnapshotV2 s;s.schemaVersion=static_cast<std::uint32_t>(LayoutInt(root,"schema_version"));
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"));
@@ -302,15 +493,314 @@ std::optional<ResolvedLayoutSnapshotV2> ResolvedLayoutSnapshotV2::FromJson(
for(const auto& value:a.Get("values").AsArray())p.values.push_back(value.Integer());x.anchors.push_back(std::move(p));}
s.strates.push_back(std::move(x));
}
for(const auto& v:root.Get("passages").AsArray())
{
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
p.lowerStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"lower_strate_index"));
p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q");
p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q");
const auto& upper=v.Get("upper_mouth_q").AsArray();
const auto& lower=v.Get("lower_mouth_q").AsArray();
for(std::size_t axis=0;axis<3&&axis<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));
}
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;
}
ResolvedLayoutValidatorAuditV2 AuditResolvedLayoutValidatorV2()
bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
const std::vector<ResolvedLayoutPassageConfigV1>& configs,std::string& error)
{
ResolvedLayoutValidatorAuditV2 result;
error.clear();layout.passages.clear();
if(layout.strates.size()<2)return true;
std::stable_sort(layout.strates.begin(),layout.strates.end(),[](const auto& a,const auto& b)
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<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)
{
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);
const std::int64_t mouthRadius=std::max<std::int64_t>(static_cast<std::int64_t>(std::llround(1.25*qPerMetre)),
cfg?cfg->mouthRadiusSubvoxels:0);
const std::int64_t midRadius=std::max<std::int64_t>(mouthRadius,
cfg?cfg->midRadiusSubvoxels:0);
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 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.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);
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 hasLowerTangent=hasLowerRoute
&&routeTangent(lower,false,lowerTangent);
const std::int64_t ringRadius=std::max<std::int64_t>(1,
placementRadius-mouthRadius-qPerMetre/2);
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;
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=(hasUpperTangent?upperTangent[0]:upperMouth[0])
*directions[candidate][0]
+(hasUpperTangent?upperTangent[1]:upperMouth[1])
*directions[candidate][1];
if(alignment>bestAlignment)
{bestAlignment=alignment;phase=candidate;}
}
}
const bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
connection,0x44495245u)&1u)!=0;
const int direction=clockwise?1:-1;
const auto ringPoint=[&](std::uint32_t directionIndex)
{
return std::array<std::int64_t,2>{
static_cast<std::int64_t>(ringRadius)*directions[directionIndex][0]/65536,
static_cast<std::int64_t>(ringRadius)*directions[directionIndex][1]/65536};
};
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);};
const auto firstRing=ringPoint(phase);
pushXY({upperMouth[0],upperMouth[1]});
pushXY(firstRing);
std::uint64_t totalHorizontal=0;std::uint32_t loops=0;
const std::int64_t verticalDistance=std::abs(lowerMouth[2]-upperMouth[2]);
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract.
auto measurePath=[&]()
{
std::uint64_t total=0;
for(std::size_t i=1;i<xy.size();++i)
total+=distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
return total;
};
while(loops<256)
{
totalHorizontal=measurePath();
if(totalHorizontal*static_cast<std::uint64_t>(TargetGradeQ16)
>=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;
}
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();
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);
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;}
}
if(bestAlignment==std::numeric_limits<std::int64_t>::min())
{error=passage.stableId+" could not find a non-overlapping lower landing approach";return false;}
if(phase!=lowerPhase)
for(int step=1;step<=16;++step)
{
int directionIndex=static_cast<int>(phase)+direction*step;
directionIndex=(directionIndex%16+16)%16;
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex)));
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
}
pushXY(ringPoint(lowerPhase));
}
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
const 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;}
}
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];
if(mouthHorizontal==0)
{error=passage.stableId+" has no horizontal reach before its lower mouth";return false;}
passage.controlPoints.reserve(xy.size());
for(std::size_t i=0;i<xy.size();++i)
{
const std::int64_t radius=(i<=1||i+1>=xy.size()-1)?mouthRadius:midRadius;
const std::int64_t playerZ=i<=lowerMouthIndex
?upperMouth[2]+PassageRoundRatio(lowerMouth[2]-upperMouth[2],
static_cast<std::int64_t>(cumulative[i]),static_cast<std::int64_t>(mouthHorizontal))
:lowerLandingPoints[i-lowerMouthIndex-1][2];
ResolvedLayoutPassagePointV1 point;
point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ};
point.radiusSubvoxels=radius;passage.controlPoints.push_back(point);
}
passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2]+mouthRadius-playerCenterAboveFloorQ;
passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=
lowerMouth[2]+mouthRadius-playerCenterAboveFloorQ;
layout.passages.push_back(std::move(passage));
}
}
return true;
}
ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3()
{
ResolvedLayoutValidatorAuditV3 result;
auto make=[](std::string id,std::int32_t order,std::int64_t top,std::int64_t bottom)
{
ResolvedLayoutStratumV2 s;s.stableId=std::move(id);s.order=order;s.strateIndex=order;s.generatorKind="synthetic";
@@ -322,16 +812,34 @@ ResolvedLayoutValidatorAuditV2 AuditResolvedLayoutValidatorV2()
s.bedrock={"bedrock",bottom-1000000,bottom,true,true,false,"breakable bedrock","lower face","none"};
return s;
};
const auto expectReject=[&](std::string name,ResolvedLayoutSnapshotV2 candidate)
const auto expectReject=[&](std::string name,ResolvedLayoutSnapshotV3 candidate)
{
++result.controlsRun;std::vector<ResolvedLayoutIssueV2> issues;std::string error;
if(!candidate.validate(ResolvedLayoutValidationModeV2::strict,issues,error))++result.controlsRejectedAsExpected;
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,error))++result.controlsRejectedAsExpected;
else result.failures.push_back({"negative_control_accepted",std::move(name)+" was accepted by strict validator"});
};
auto base=ResolvedLayoutSnapshotV2{};base.worldIdentity="synthetic";base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)};
auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000;
base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)};
ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1;
passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280;
passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048;
passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240;
std::string planError;
if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError))
result.failures.push_back({"synthetic_passage_plan_failed",planError});
{std::vector<ResolvedLayoutIssueV2> issues;std::string error;
if(!base.validate(ResolvedLayoutValidationModeV2::strict,issues,error))
if(!base.validate(ResolvedLayoutValidationModeV3::strict,issues,error))
result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"});}
auto missingPassage=base;missingPassage.passages.clear();expectReject("missing interface passage",missingPassage);
auto outsidePassage=base;
if(!outsidePassage.passages.empty())
{
outsidePassage.passages.front().upperMouthSubvoxels[0]
=outsidePassage.passages.front().placementRadiusSubvoxels+1;
outsidePassage.passages.front().controlPoints.front().positionSubvoxels[0]
=outsidePassage.passages.front().upperMouthSubvoxels[0];
}
expectReject("passage mouth outside placement radius",outsidePassage);
auto boundaryGap=base;boundaryGap.strates[1].ceiling.topMicrometres-=1;
expectReject("bedrock/next-ceiling gap",boundaryGap);
auto missingBedrock=base;missingBedrock.strates[0].bedrock.presentInCurrentField=false;expectReject("missing bedrock",missingBedrock);
+188 -22
View File
@@ -33,7 +33,7 @@
class FStandaloneShapeWorld
{
public:
const VoxelForgeCore::ResolvedLayoutSnapshotV2* ResolvedLayout=nullptr;
const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr;
struct FResolvedStratumBounds
{
double BottomZ=0.0;
@@ -97,6 +97,170 @@ public:
return -std::max(0.05f,static_cast<float>(band->solidDensityQ16)/65536.0f);
}
float ApplyResolvedStratePassageCarves(float density,float X,float Y,float Z) const
{
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
||!std::isfinite(Y)||!std::isfinite(Z))return density;
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,qz=static_cast<double>(Z)*Q;
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
const double floorThicknessVoxels=qPerMetre/Q;
const double playerCenterAboveFloorQ=1.13*qPerMetre;
float composed=density;
for(const auto& passage:ResolvedLayout->passages)
{
if(passage.controlPoints.size()<2)continue;
const double placementRadiusQ=static_cast<double>(passage.placementRadiusSubvoxels)
+static_cast<double>(passage.midRadiusSubvoxels);
if(qx*qx+qy*qy>placementRadiusQ*placementRadiusQ)continue;
const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2]
-passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ;
const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2]
-passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ;
if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)continue;
const auto floorProfile=[&](const auto& point)
{return point.positionSubvoxels[2]-point.radiusSubvoxels+playerCenterAboveFloorQ;};
const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>(passage.controlPoints.size())-1;
const std::ptrdiff_t lowerMouthIndex=std::clamp<std::ptrdiff_t>(
static_cast<std::ptrdiff_t>(passage.lowerMouthControlIndex),1,lastIndex);
const auto mainEnd=passage.controlPoints.begin()+lowerMouthIndex+1;
const auto carveFound=std::lower_bound(passage.controlPoints.begin(),mainEnd,qz,
[&](const auto& point,double value){return floorProfile(point)>value;});
const std::ptrdiff_t carveCenter=carveFound-passage.controlPoints.begin();
const std::ptrdiff_t carveBegin=std::max<std::ptrdiff_t>(0,carveCenter-3);
const std::ptrdiff_t carveEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,carveCenter+2);
double bestSignedDistance=std::numeric_limits<double>::infinity();
double bestT=0.0;
std::ptrdiff_t bestIndex=0;
bool hasSupportFloor=false;
double bestSupportFloorVoxels=-std::numeric_limits<double>::infinity();
double bestSupportDensity=0.0;
const auto considerCarveSegment=[&](std::ptrdiff_t index)
{
const auto& a=passage.controlPoints[static_cast<std::size_t>(index)];
const auto& b=passage.controlPoints[static_cast<std::size_t>(index+1)];
const double ax=static_cast<double>(a.positionSubvoxels[0]);
const double ay=static_cast<double>(a.positionSubvoxels[1]);
const double az=static_cast<double>(a.positionSubvoxels[2]);
const double dx=static_cast<double>(b.positionSubvoxels[0])-ax;
const double dy=static_cast<double>(b.positionSubvoxels[1])-ay;
const double dz=static_cast<double>(b.positionSubvoxels[2])-az;
const double lengthSq=dx*dx+dy*dy+dz*dz;
if(lengthSq<=0.0)return;
const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0);
const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
const double radius=static_cast<double>(a.radiusSubvoxels)
+t*(b.radiusSubvoxels-a.radiusSubvoxels);
const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
if(signedDistance<bestSignedDistance)
{bestSignedDistance=signedDistance;bestT=t;bestIndex=index;}
};
for(std::ptrdiff_t index=carveBegin;index<=carveEnd;++index)
considerCarveSegment(index);
// The route-bound landing can rise and fall after the lower mouth. Scan
// that short tail directly; binary search is only valid on the descent.
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
considerCarveSegment(index);
if(!std::isfinite(bestSignedDistance))continue;
// Positive density is air; the passage SDF is applied after both owner bands.
composed=std::max(composed,static_cast<float>(-bestSignedDistance/Q));
const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)];
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
const double nearestRadius=nearestA.radiusSubvoxels+bestT*(
static_cast<double>(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels);
const double nearestPlayerCenterZ=nearestA.positionSubvoxels[2]+bestT*(
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])
-nearestRadius+playerCenterAboveFloorQ;
// Only compose a support shelf below the capsule feet. This keeps support
// probes on the matching floor while capsule samples remain air at bends.
if(qz<nearestPlayerCenterZ-0.90*qPerMetre)
{
const double supportCenterQueryQ=qz+playerCenterAboveFloorQ;
const auto supportFound=std::lower_bound(passage.controlPoints.begin(),
mainEnd,supportCenterQueryQ,
[&](const auto& point,double value){return floorProfile(point)>value;});
const std::ptrdiff_t supportCenter=supportFound-passage.controlPoints.begin();
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,supportCenter-3);
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,supportCenter+2);
const auto considerSupportSegment=[&](std::ptrdiff_t index)
{
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
const double dx=static_cast<double>(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0];
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
const double horizontalLengthSq=dx*dx+dy*dy;
const double supportT=horizontalLengthSq>0.0
?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0)
:bestT;
const double supportCenterX=segmentA.positionSubvoxels[0]+supportT*dx;
const double supportCenterY=segmentA.positionSubvoxels[1]+supportT*dy;
const double supportCenterZ=segmentA.positionSubvoxels[2]+supportT*(
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
const double supportRadius=segmentA.radiusSubvoxels+supportT*(
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
// Leave a 12 cm settle gap below the capsule feet while staying inside
// the route support probes' search depth.
const double floorQ=supportCenterZ-supportRadius+0.12*qPerMetre;
const double floorVoxels=floorQ/Q;
const double horizontalX=qx-supportCenterX,horizontalY=qy-supportCenterY;
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY);
const double supportHalfWidth=std::max(0.0,supportRadius-0.15*qPerMetre);
if(horizontalDistance<=supportHalfWidth&&Z>=floorVoxels-floorThicknessVoxels
&&Z<=floorVoxels+1.0/Q)
{
if(!hasSupportFloor||floorVoxels>bestSupportFloorVoxels)
{
bestSupportFloorVoxels=floorVoxels;hasSupportFloor=true;
bestSupportDensity=std::clamp(Z-floorVoxels,-floorThicknessVoxels,0.0);
}
}
};
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
considerSupportSegment(index);
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
considerSupportSegment(index);
}
if(hasSupportFloor)
composed=std::min(composed,static_cast<float>(bestSupportDensity));
}
return composed;
}
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
{return ApplyResolvedStratePassageCarves(ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);}
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
{
if(!ResolvedLayout||ResolvedLayout->passages.empty())return false;
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
for(const auto& passage:ResolvedLayout->passages)
{
for(std::size_t i=1;i<passage.controlPoints.size();++i)
{
const auto& a=passage.controlPoints[i-1];const auto& b=passage.controlPoints[i];
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
const double floorThicknessVoxels=Owner&&Owner->CoreVoxelSizeMicrons>0
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
const double low[3]={
std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-radius,
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-radius,
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-floorThicknessVoxels};
const double high[3]={
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+radius,
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+radius,
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
bool overlap=true;
for(int axis=0;axis<3;++axis)
overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis];
if(overlap)return true;
}
}
return false;
}
static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness,
FResolvedStratumBounds& Out)
{
@@ -410,7 +574,7 @@ public:
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InGpuVisualDetailTiles={},
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={},
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr,
const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout=nullptr)
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr)
{
using Clock=std::chrono::steady_clock;
const auto TotalStart=Clock::now();
@@ -571,7 +735,7 @@ public:
const FIntVector& ClipMaxVoxels,EDetailVariant Variant,
bool bInRoundTunnels,
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout,
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout,
std::string& Error)
{
using Clock=std::chrono::steady_clock;
@@ -611,7 +775,7 @@ public:
void InitializeNativeSurfaceOnly(FStandaloneOwnerWorld& World,
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout,
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout,
const FIntVector& InMinimum,const FIntVector& InMaximum)
{
Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout;
@@ -622,7 +786,7 @@ public:
bool BuildShapeDependencyProof(FStandaloneOwnerWorld& World,
const FIntVector& MinVoxels,const FIntVector& MaxVoxels,bool bInRoundTunnels,
const VoxelForgeCore::ResolvedLayoutSnapshotV2* InResolvedLayout=nullptr)
const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr)
{
Owner=&World;ResolvedLayout=InResolvedLayout;WorkerCount=1;RoundTunnelsEnabled=bInRoundTunnels;
bAnalyticOnly=true;bNativeOnly=false;Stats={};
@@ -1129,19 +1293,19 @@ public:
float NativeDensity=0.0f;
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{RecordNativeOnlyViolation();return 0.0f;}
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
}
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
++ThreadClassificationStats().NativeFloatFieldCalls;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z);
}
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{
++ThreadClassificationStats().NativeFloatFieldCalls;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
}
if(UsesLegacySurfaceField(X,Y,Z))
return LegacySurfaceDensityAt(X,Y,Z);
@@ -1191,21 +1355,21 @@ public:
if(!NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),NativeDensity))
{RecordNativeOnlyViolation();return 0.0f;}
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
}
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),IslandDensity))
{
++ThreadClassificationStats().NativeLatticeFieldCalls;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z);
}
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),NativeDensity))
{
++ThreadClassificationStats().NativeLatticeFieldCalls;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
}
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z)))
@@ -1229,19 +1393,19 @@ public:
float NativeDensity=0.0f;
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{RecordNativeOnlyViolation();return 0.0f;}
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
}
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
++ThreadClassificationStats().NativeUnclampedFieldCalls;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(IslandDensity,X,Y,Z),X,Y,Z);
}
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{
++ThreadClassificationStats().NativeUnclampedFieldCalls;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
}
if(UsesLegacySurfaceField(X,Y,Z))
return LegacySurfaceDensityAt(X,Y,Z);
@@ -1371,13 +1535,13 @@ private:
float Density=static_cast<float>(Proxy)*(1.0f/256.0f);
if((Density<0.0f)!=bRock)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f);
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(Density,FX,FY,FZ),FX,FY,FZ);
return ApplyEditBrushes(ApplyResolvedStrateComposition(Density,FX,FY,FZ),FX,FY,FZ);
}
// Match the fixed fallback to the edge of the stored proxy band. The
// displacement is tapered to zero there, so this avoids a magnitude
// jump along the candidate bit-mask boundary.
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(
return ApplyEditBrushes(ApplyResolvedStrateComposition(
bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ),FX,FY,FZ);
}
@@ -1395,7 +1559,7 @@ private:
float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
const float composed=ApplyResolvedStrateBoundaryBand(IslandDensity,X,Y,Z);
const float composed=ApplyResolvedStrateComposition(IslandDensity,X,Y,Z);
return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed;
}
if(UsesLegacySurfaceField(X,Y,Z))
@@ -1403,11 +1567,11 @@ private:
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{
NativeDensity=ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z);
NativeDensity=ApplyResolvedStrateComposition(NativeDensity,X,Y,Z);
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
}
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
const float composed=ApplyResolvedStrateBoundaryBand(Density,X,Y,Z);
const float composed=ApplyResolvedStrateComposition(Density,X,Y,Z);
return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed;
}
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]
@@ -1422,7 +1586,7 @@ private:
if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f))
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),
bClampNearZero?0.05f:1.0e-7f);
const float composed=ApplyResolvedStrateBoundaryBand(Density,X,Y,Z);
const float composed=ApplyResolvedStrateComposition(Density,X,Y,Z);
return bApplyEdits?ApplyEditBrushes(composed,X,Y,Z):composed;
}
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
@@ -1452,7 +1616,7 @@ private:
if((Density<0.0f)!=bRock||(bClampNearZero&&std::abs(Density)<0.05f))
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),
bClampNearZero?0.05f:1.0e-7f);
Density=ApplyResolvedStrateBoundaryBand(Density,X,Y,Z);
Density=ApplyResolvedStrateComposition(Density,X,Y,Z);
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
}
@@ -1590,6 +1754,8 @@ public:
else ++Counts.MixedBoxes;
return Result;
};
if(ResolvedPassageIntersectsBox(BMin,BMax))
return Finish(EVoxelTileClass::Mixed,true);
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
&&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2])
@@ -2319,7 +2485,7 @@ private:
// SurfaceWorld consumes biome overrides, terrain ops, water level and its sky cap in
// the legacy stack. Reuse that immutable evaluator as an exact temporary bridge.
// SurfaceWorld lit biomes, ops de terrain, niveau d'eau et plafond; reutiliser ce champ.
const float density=ApplyResolvedStrateBoundaryBand(Owner->Generator->GetDensityAt(X,Y,Z),X,Y,Z);
const float density=ApplyResolvedStrateComposition(Owner->Generator->GetDensityAt(X,Y,Z),X,Y,Z);
return ApplyEditBrushes(density,X,Y,Z);
}
+24 -12
View File
@@ -34,7 +34,7 @@ namespace VoxelForgeCore
{
std::shared_ptr<FStandaloneOwnerWorld> owner;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
std::shared_ptr<const ResolvedLayoutSnapshotV2> resolvedLayout;
std::shared_ptr<const ResolvedLayoutSnapshotV3> resolvedLayout;
WorldConfiguration configuration;
};
@@ -869,7 +869,7 @@ namespace VoxelForgeCore
{
static ResolvedWorldSnapshot FromOwner(std::shared_ptr<FStandaloneOwnerWorld> owner,
std::shared_ptr<const WorldAssemblyManifest> worldAssembly = {},
std::shared_ptr<const ResolvedLayoutSnapshotV2> resolvedLayout = {})
std::shared_ptr<const ResolvedLayoutSnapshotV3> resolvedLayout = {})
{
if(!owner||!owner->Settings||!owner->Manager||!owner->Generator)return {};
auto data=std::make_shared<ResolvedWorldSnapshot::Data>();
@@ -894,15 +894,15 @@ namespace VoxelForgeCore
if(!parsed)return {};
worldAssembly=std::make_shared<WorldAssemblyManifest>(std::move(*parsed));
}
std::shared_ptr<const ResolvedLayoutSnapshotV2> resolvedLayout;
std::shared_ptr<const ResolvedLayoutSnapshotV3> resolvedLayout;
if(jsonRoot.Get("resolved_layout_json").IsString())
{
auto parsed=ResolvedLayoutSnapshotV2::FromJson(jsonRoot.Get("resolved_layout_json").String(),error);
auto parsed=ResolvedLayoutSnapshotV3::FromJson(jsonRoot.Get("resolved_layout_json").String(),error);
if(!parsed)return {};
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
if(!parsed->validate(ResolvedLayoutValidationModeV2::strict,issues,strictError))
if(!parsed->validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
{error="strict resolved-layout validation failed: "+strictError;return {};}
resolvedLayout=std::make_shared<ResolvedLayoutSnapshotV2>(std::move(*parsed));
resolvedLayout=std::make_shared<ResolvedLayoutSnapshotV3>(std::move(*parsed));
}
const bool seedChanged=owner->Seed!=seed;
@@ -943,24 +943,36 @@ namespace VoxelForgeCore
if(!parsed)return std::nullopt;
worldAssembly=std::make_shared<WorldAssemblyManifest>(std::move(*parsed));
}
std::shared_ptr<const ResolvedLayoutSnapshotV2> resolvedLayout;
std::shared_ptr<const ResolvedLayoutSnapshotV3> resolvedLayout;
if(root.Get("resolved_layout_json").IsString())
{
auto parsed=ResolvedLayoutSnapshotV2::FromJson(root.Get("resolved_layout_json").String(),error);
auto parsed=ResolvedLayoutSnapshotV3::FromJson(root.Get("resolved_layout_json").String(),error);
if(!parsed)return std::nullopt;
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
if(!parsed->validate(ResolvedLayoutValidationModeV2::strict,issues,strictError))
if(!parsed->validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
{error="strict resolved-layout validation failed: "+strictError;return std::nullopt;}
resolvedLayout=std::make_shared<ResolvedLayoutSnapshotV2>(std::move(*parsed));
resolvedLayout=std::make_shared<ResolvedLayoutSnapshotV3>(std::move(*parsed));
}
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner),std::move(worldAssembly),std::move(resolvedLayout));
}
const WorldAssemblyManifest* ResolvedWorldSnapshot::worldAssembly() const noexcept
{ return data_?data_->worldAssembly.get():nullptr; }
const ResolvedLayoutSnapshotV2* ResolvedWorldSnapshot::resolvedLayout() const noexcept
const ResolvedLayoutSnapshotV3* ResolvedWorldSnapshot::resolvedLayout() const noexcept
{ return data_?data_->resolvedLayout.get():nullptr; }
ResolvedWorldSnapshot ResolvedWorldSnapshot::withoutResolvedLayoutPassages() const
{
if(!data_||!data_->resolvedLayout)return {};
auto copy=std::make_shared<Data>(*data_);
auto layout=std::make_shared<ResolvedLayoutSnapshotV3>(*data_->resolvedLayout);
layout->passages.clear();
std::string error;
if(!layout->finalize(error))return {};
copy->resolvedLayout=std::move(layout);
return ResolvedWorldSnapshot(std::move(copy));
}
const WorldConfiguration& ResolvedWorldSnapshot::configuration() const
{
static const WorldConfiguration empty{};
@@ -971,7 +983,7 @@ namespace VoxelForgeCore
{
std::shared_ptr<FStandaloneOwnerWorld> owner;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
std::shared_ptr<const ResolvedLayoutSnapshotV2> resolvedLayout;
std::shared_ptr<const ResolvedLayoutSnapshotV3> resolvedLayout;
WorldConfiguration configuration;
std::unique_ptr<FStandaloneShapeWorld> shape;
std::atomic<bool> shapeReady{false};