wip: opt-in landing-pad contract + Core.Liminal.Field v2 pads (L145)
Layers may publish certified arrival/departure pads; the passage planner binds endpoints and tangents to them; no-pad layouts serialize unchanged (owner canonical hashes match v227). Pads removed the endpoint grade failures; seeds 0-4 passed at a 45 m margin, but seed 5 still has 289 blocked passage-capsule samples ~29 m before the departure pad, where the composed field stays solid (-0.162) inside the tube over native ground (-3.6). Ripple audit now keeps true route distance across masked gaps (with a synthetic control). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
a45ba9bb10
commit
7d654ff861
14 files changed
+1228
-66
No files matched your search
@@ -599,6 +599,7 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
inline constexpr std::string_view CoreLiminalFieldRecipeIdV1 = "Core.Liminal.Field";
|
inline constexpr std::string_view CoreLiminalFieldRecipeIdV1 = "Core.Liminal.Field";
|
||||||
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV1 = 1;
|
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV1 = 1;
|
||||||
|
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV2 = 2;
|
||||||
inline constexpr std::uint32_t CoreLiminalFieldVariantCountV1 = 4;
|
inline constexpr std::uint32_t CoreLiminalFieldVariantCountV1 = 4;
|
||||||
|
|
||||||
enum class WorldAssemblyLiminalVariantV1 : std::uint32_t
|
enum class WorldAssemblyLiminalVariantV1 : std::uint32_t
|
||||||
@@ -1145,6 +1146,27 @@ namespace VoxelForgeCore
|
|||||||
bool surfaceToTunnelLandingSupport = false;
|
bool surfaceToTunnelLandingSupport = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class ResolvedLayoutLandingPadRoleV1 : std::uint32_t
|
||||||
|
{
|
||||||
|
arrival = 0,
|
||||||
|
departure = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResolvedLayoutLandingPadV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
ResolvedLayoutLandingPadRoleV1 role = ResolvedLayoutLandingPadRoleV1::arrival;
|
||||||
|
// Capsule-center position. floorHeightSubvoxels is the supported floor below it.
|
||||||
|
std::array<std::int64_t, 3> positionSubvoxels{};
|
||||||
|
std::int64_t floorHeightSubvoxels = 0;
|
||||||
|
// Unit horizontal travel direction, signed Q16, oriented into the layer route.
|
||||||
|
std::array<std::int32_t, 2> approachDirectionQ16{};
|
||||||
|
// Certified horizontal free radius around the capsule center and support below the floor.
|
||||||
|
std::int64_t clearanceSubvoxels = 0;
|
||||||
|
std::int64_t supportDepthSubvoxels = 0;
|
||||||
|
bool certifiedWalkableWithSupport = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResolvedLayoutStratumV2
|
struct ResolvedLayoutStratumV2
|
||||||
{
|
{
|
||||||
std::string stableId;
|
std::string stableId;
|
||||||
@@ -1174,6 +1196,7 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t managerChunkWidthMicrometres = 0;
|
std::int64_t managerChunkWidthMicrometres = 0;
|
||||||
std::int64_t dispatchChunkWidthMicrometres = 0;
|
std::int64_t dispatchChunkWidthMicrometres = 0;
|
||||||
std::vector<ResolvedLayoutAnchorV2> anchors;
|
std::vector<ResolvedLayoutAnchorV2> anchors;
|
||||||
|
std::vector<ResolvedLayoutLandingPadV1> landingPads;
|
||||||
ResolvedLayoutStratePassageCapabilitiesV1 passageCapabilities;
|
ResolvedLayoutStratePassageCapabilitiesV1 passageCapabilities;
|
||||||
ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities;
|
ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities;
|
||||||
};
|
};
|
||||||
@@ -1316,6 +1339,12 @@ namespace VoxelForgeCore
|
|||||||
VFCORE_API bool PlanCoreLiminalFieldV1(
|
VFCORE_API bool PlanCoreLiminalFieldV1(
|
||||||
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
||||||
const ResolvedLayoutSnapshotV3& layout, std::string& error);
|
const ResolvedLayoutSnapshotV3& layout, std::string& error);
|
||||||
|
VFCORE_API bool PrepareCoreLiminalLandingPadsV2(
|
||||||
|
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::string& error);
|
||||||
|
VFCORE_API bool PlanCoreLiminalFieldV2(
|
||||||
|
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
||||||
|
const ResolvedLayoutSnapshotV3& layout, std::string& error);
|
||||||
|
|
||||||
struct ResolvedLayoutStampPlacementAuditV1
|
struct ResolvedLayoutStampPlacementAuditV1
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -613,7 +613,28 @@ namespace
|
|||||||
for(std::size_t v=0;v<anchor.values.size();++v){if(v)out<<',';out<<anchor.values[v];}
|
for(std::size_t v=0;v<anchor.values.size();++v){if(v)out<<',';out<<anchor.values[v];}
|
||||||
out<<"]}";
|
out<<"]}";
|
||||||
}
|
}
|
||||||
out<<"]}";
|
out<<']';
|
||||||
|
if(!x.landingPads.empty())
|
||||||
|
{
|
||||||
|
out<<",\"landing_pads\":[";
|
||||||
|
for(std::size_t p=0;p<x.landingPads.size();++p)
|
||||||
|
{
|
||||||
|
if(p)out<<',';const auto& pad=x.landingPads[p];
|
||||||
|
out<<"{\"stable_id\":"<<LayoutEscape(pad.stableId)
|
||||||
|
<<",\"role\":"<<static_cast<std::uint32_t>(pad.role)
|
||||||
|
<<",\"position_q\":["<<pad.positionSubvoxels[0]<<','
|
||||||
|
<<pad.positionSubvoxels[1]<<','<<pad.positionSubvoxels[2]
|
||||||
|
<<"],\"floor_q\":"<<pad.floorHeightSubvoxels
|
||||||
|
<<",\"approach_q16\":["<<pad.approachDirectionQ16[0]<<','
|
||||||
|
<<pad.approachDirectionQ16[1]<<']'
|
||||||
|
<<",\"clearance_q\":"<<pad.clearanceSubvoxels
|
||||||
|
<<",\"support_depth_q\":"<<pad.supportDepthSubvoxels
|
||||||
|
<<",\"certified\":"
|
||||||
|
<<(pad.certifiedWalkableWithSupport?"true":"false")<<'}';
|
||||||
|
}
|
||||||
|
out<<']';
|
||||||
|
}
|
||||||
|
out<<'}';
|
||||||
}
|
}
|
||||||
out<<"],\"passages\":[";
|
out<<"],\"passages\":[";
|
||||||
for(std::size_t i=0;i<s.passages.size();++i)
|
for(std::size_t i=0;i<s.passages.size();++i)
|
||||||
@@ -957,6 +978,9 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
|||||||
for(std::size_t i=0;i<strates.size();++i)
|
for(std::size_t i=0;i<strates.size();++i)
|
||||||
{
|
{
|
||||||
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
||||||
|
std::stable_sort(x.landingPads.begin(),x.landingPads.end(),[](const auto& a,const auto& b)
|
||||||
|
{return a.role!=b.role?static_cast<std::uint32_t>(a.role)
|
||||||
|
<static_cast<std::uint32_t>(b.role):a.stableId<b.stableId;});
|
||||||
if(x.stableId.empty())x.stableId=worldIdentity+"/strate/"+std::to_string(x.strateIndex);
|
if(x.stableId.empty())x.stableId=worldIdentity+"/strate/"+std::to_string(x.strateIndex);
|
||||||
if(x.content.kind.empty())x.content.kind="content";
|
if(x.content.kind.empty())x.content.kind="content";
|
||||||
if(x.ceiling.kind.empty())x.ceiling.kind="ceiling";
|
if(x.ceiling.kind.empty())x.ceiling.kind="ceiling";
|
||||||
@@ -1017,9 +1041,40 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
const auto add=[&](std::string code,std::string detail)
|
const auto add=[&](std::string code,std::string detail)
|
||||||
{issues.push_back({std::move(code),std::move(detail)});};
|
{issues.push_back({std::move(code),std::move(detail)});};
|
||||||
const bool strict=mode==ResolvedLayoutValidationModeV3::strict;
|
const bool strict=mode==ResolvedLayoutValidationModeV3::strict;
|
||||||
|
const std::int64_t playerCenterAboveFloorQ=static_cast<std::int64_t>(std::llround(
|
||||||
|
1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres));
|
||||||
for(std::size_t i=0;i<strates.size();++i)
|
for(std::size_t i=0;i<strates.size();++i)
|
||||||
{
|
{
|
||||||
const auto& s=strates[i];
|
const auto& s=strates[i];
|
||||||
|
std::vector<std::string> landingPadIds;
|
||||||
|
for(const auto& pad:s.landingPads)
|
||||||
|
{
|
||||||
|
if(pad.stableId.empty()
|
||||||
|
||std::find(landingPadIds.begin(),landingPadIds.end(),pad.stableId)
|
||||||
|
!=landingPadIds.end())
|
||||||
|
add("landing_pad_identity",s.stableId+" has an empty or duplicate landing-pad ID");
|
||||||
|
else landingPadIds.push_back(pad.stableId);
|
||||||
|
const auto role=static_cast<std::uint32_t>(pad.role);
|
||||||
|
const std::int64_t directionX=pad.approachDirectionQ16[0];
|
||||||
|
const std::int64_t directionY=pad.approachDirectionQ16[1];
|
||||||
|
const std::int64_t directionLengthSquared=directionX*directionX+directionY*directionY;
|
||||||
|
constexpr std::int64_t UnitQ16Squared=1ll<<32;
|
||||||
|
const bool unitDirection=directionLengthSquared>=UnitQ16Squared*98/100
|
||||||
|
&&directionLengthSquared<=UnitQ16Squared*102/100;
|
||||||
|
const std::int64_t floorMicrometres=pad.floorHeightSubvoxels
|
||||||
|
*static_cast<std::int64_t>(coreVoxelSizeMicrometres)
|
||||||
|
/WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
if(role>static_cast<std::uint32_t>(ResolvedLayoutLandingPadRoleV1::departure)
|
||||||
|
||pad.positionSubvoxels[2]
|
||||||
|
!=pad.floorHeightSubvoxels+playerCenterAboveFloorQ
|
||||||
|
||!s.content.presentInCurrentField
|
||||||
|
||floorMicrometres<s.content.bottomMicrometres
|
||||||
|
||floorMicrometres>=s.content.topMicrometres
|
||||||
|
||!unitDirection||pad.clearanceSubvoxels<=0
|
||||||
|
||pad.supportDepthSubvoxels<=0
|
||||||
|
||!pad.certifiedWalkableWithSupport)
|
||||||
|
add("landing_pad_contract",s.stableId+" publishes an uncertified or malformed landing pad");
|
||||||
|
}
|
||||||
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(s);
|
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(s);
|
||||||
if(s.interiorTopMicrometres<=s.interiorBottomMicrometres)
|
if(s.interiorTopMicrometres<=s.interiorBottomMicrometres)
|
||||||
add("invalid_interior",s.stableId+" has non-positive content height");
|
add("invalid_interior",s.stableId+" has non-positive content height");
|
||||||
@@ -1643,6 +1698,22 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
for(const auto& a:v.Get("anchors").AsArray())
|
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");
|
{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));}
|
for(const auto& value:a.Get("values").AsArray())p.values.push_back(value.Integer());x.anchors.push_back(std::move(p));}
|
||||||
|
for(const auto& value:v.Get("landing_pads").AsArray())
|
||||||
|
{
|
||||||
|
ResolvedLayoutLandingPadV1 pad;pad.stableId=LayoutString(value,"stable_id");
|
||||||
|
pad.role=static_cast<ResolvedLayoutLandingPadRoleV1>(LayoutInt(value,"role"));
|
||||||
|
const auto& position=value.Get("position_q").AsArray();
|
||||||
|
for(std::size_t axis=0;axis<3&&axis<position.size();++axis)
|
||||||
|
pad.positionSubvoxels[axis]=position[axis].Integer();
|
||||||
|
pad.floorHeightSubvoxels=LayoutInt(value,"floor_q");
|
||||||
|
const auto& approach=value.Get("approach_q16").AsArray();
|
||||||
|
for(std::size_t axis=0;axis<2&&axis<approach.size();++axis)
|
||||||
|
pad.approachDirectionQ16[axis]=static_cast<std::int32_t>(approach[axis].Integer());
|
||||||
|
pad.clearanceSubvoxels=LayoutInt(value,"clearance_q");
|
||||||
|
pad.supportDepthSubvoxels=LayoutInt(value,"support_depth_q");
|
||||||
|
pad.certifiedWalkableWithSupport=LayoutBool(value,"certified");
|
||||||
|
x.landingPads.push_back(std::move(pad));
|
||||||
|
}
|
||||||
s.strates.push_back(std::move(x));
|
s.strates.push_back(std::move(x));
|
||||||
}
|
}
|
||||||
for(const auto& v:root.Get("passages").AsArray())
|
for(const auto& v:root.Get("passages").AsArray())
|
||||||
@@ -2566,6 +2637,17 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
{return c.upperStrateIndex==strate;});
|
{return c.upperStrateIndex==strate;});
|
||||||
return it==configs.end()?nullptr:&*it;
|
return it==configs.end()?nullptr:&*it;
|
||||||
};
|
};
|
||||||
|
const auto landingPadFor=[](const ResolvedLayoutStratumV2& stratum,
|
||||||
|
ResolvedLayoutLandingPadRoleV1 role,std::uint32_t ordinal)
|
||||||
|
->const ResolvedLayoutLandingPadV1*
|
||||||
|
{
|
||||||
|
std::vector<const ResolvedLayoutLandingPadV1*> candidates;
|
||||||
|
for(const auto& pad:stratum.landingPads)
|
||||||
|
if(pad.role==role)candidates.push_back(&pad);
|
||||||
|
std::sort(candidates.begin(),candidates.end(),[](const auto* a,const auto* b)
|
||||||
|
{return a->stableId<b->stableId;});
|
||||||
|
return ordinal<candidates.size()?candidates[ordinal]:nullptr;
|
||||||
|
};
|
||||||
const auto routeAnchor=[](const ResolvedLayoutStratumV2& s,bool departure,
|
const auto routeAnchor=[](const ResolvedLayoutStratumV2& s,bool departure,
|
||||||
std::array<std::int64_t,3>& out)
|
std::array<std::int64_t,3>& out)
|
||||||
{
|
{
|
||||||
@@ -2662,9 +2744,37 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
||||||
passage.placementRadiusSubvoxels=placementRadius;
|
passage.placementRadiusSubvoxels=placementRadius;
|
||||||
passage.mouthRadiusSubvoxels=mouthRadius;passage.midRadiusSubvoxels=midRadius;
|
passage.mouthRadiusSubvoxels=mouthRadius;passage.midRadiusSubvoxels=midRadius;
|
||||||
|
const auto* upperDeparturePad=landingPadFor(upper,
|
||||||
|
ResolvedLayoutLandingPadRoleV1::departure,connection);
|
||||||
|
const auto* lowerArrivalPad=landingPadFor(lower,
|
||||||
|
ResolvedLayoutLandingPadRoleV1::arrival,connection);
|
||||||
|
const auto validateLandingPad=[&](const ResolvedLayoutLandingPadV1* pad,
|
||||||
|
std::string_view side)
|
||||||
|
{
|
||||||
|
if(!pad)return true;
|
||||||
|
const std::int64_t requiredClearance=mouthRadius
|
||||||
|
+static_cast<std::int64_t>(std::llround(0.34*qPerMetre));
|
||||||
|
if(!pad->certifiedWalkableWithSupport||pad->clearanceSubvoxels<requiredClearance
|
||||||
|
||pad->supportDepthSubvoxels<static_cast<std::int64_t>(std::llround(0.50*qPerMetre))
|
||||||
|
||!radialWithin(pad->positionSubvoxels,placementRadius)
|
||||||
|
||pad->positionSubvoxels[2]
|
||||||
|
!=pad->floorHeightSubvoxels+playerCenterAboveFloorQ)
|
||||||
|
{
|
||||||
|
error=passage.stableId+" "+std::string(side)
|
||||||
|
+" landing pad is uncertified, unsupported, too tight, or outside PassageConfig.DistanceMax";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if(!validateLandingPad(upperDeparturePad,"departure")
|
||||||
|
||!validateLandingPad(lowerArrivalPad,"arrival"))return false;
|
||||||
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
||||||
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
const bool hasUpperRoute=upperDeparturePad
|
||||||
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
?(upperMouth=upperDeparturePad->positionSubvoxels,true)
|
||||||
|
:connection==0&&routeAnchor(upper,true,upperMouth);
|
||||||
|
const bool hasLowerRoute=lowerArrivalPad
|
||||||
|
?(lowerMouth=lowerArrivalPad->positionSubvoxels,true)
|
||||||
|
:connection==0&&routeAnchor(lower,false,lowerMouth);
|
||||||
const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld;
|
const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld;
|
||||||
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
||||||
if(connection==0)
|
if(connection==0)
|
||||||
@@ -2713,13 +2823,17 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
|
|
||||||
constexpr std::int64_t LandingExtensionQ=8;
|
constexpr std::int64_t LandingExtensionQ=8;
|
||||||
std::array<std::int64_t,2> upperTangent{},lowerTangent{};
|
std::array<std::int64_t,2> upperTangent{},lowerTangent{};
|
||||||
const bool hasUpperTangent=hasUpperRoute
|
const bool hasUpperTangent=upperDeparturePad
|
||||||
&&routeTangent(upper,true,upperTangent);
|
?(upperTangent={upperDeparturePad->approachDirectionQ16[0],
|
||||||
|
upperDeparturePad->approachDirectionQ16[1]},true)
|
||||||
|
:hasUpperRoute&&routeTangent(upper,true,upperTangent);
|
||||||
const bool hasPassageArrival=std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
const bool hasPassageArrival=std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
||||||
[](const auto& anchor){return anchor.kind=="passage_arrival"
|
[](const auto& anchor){return anchor.kind=="passage_arrival"
|
||||||
&&anchor.values.size()>=3;});
|
&&anchor.values.size()>=3;});
|
||||||
const bool hasLowerTangent=hasLowerRoute&&!hasPassageArrival
|
const bool hasLowerTangent=lowerArrivalPad
|
||||||
&&routeTangent(lower,false,lowerTangent);
|
?(lowerTangent={lowerArrivalPad->approachDirectionQ16[0],
|
||||||
|
lowerArrivalPad->approachDirectionQ16[1]},true)
|
||||||
|
:hasLowerRoute&&!hasPassageArrival&&routeTangent(lower,false,lowerTangent);
|
||||||
const bool profiledSurfaceJoin=hasLowerTangent
|
const bool profiledSurfaceJoin=hasLowerTangent
|
||||||
&&std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
&&std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
||||||
[](const auto& anchor){return anchor.kind=="heightfield_profile"
|
[](const auto& anchor){return anchor.kind=="heightfield_profile"
|
||||||
|
|||||||
@@ -542,7 +542,8 @@ namespace world_assembly_detail
|
|||||||
const auto& liminal = stratum.liminalField;
|
const auto& liminal = stratum.liminalField;
|
||||||
if (liminal.version != 0)
|
if (liminal.version != 0)
|
||||||
{
|
{
|
||||||
if (liminal.version != CoreLiminalFieldRecipeVersionV1
|
if ((liminal.version != CoreLiminalFieldRecipeVersionV1
|
||||||
|
&& liminal.version != CoreLiminalFieldRecipeVersionV2)
|
||||||
|| static_cast<std::uint32_t>(liminal.variant) >= CoreLiminalFieldVariantCountV1
|
|| static_cast<std::uint32_t>(liminal.variant) >= CoreLiminalFieldVariantCountV1
|
||||||
|| liminal.seed != effectiveRecipe.seed
|
|| liminal.seed != effectiveRecipe.seed
|
||||||
|| liminal.variant != SelectCoreLiminalFieldVariantV1(liminal.seed)
|
|| liminal.variant != SelectCoreLiminalFieldVariantV1(liminal.seed)
|
||||||
@@ -2719,6 +2720,402 @@ bool PlanCoreLiminalFieldV1(WorldAssemblyRecipeV1& recipe,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<std::array<std::int64_t,2>,6> CoreLiminalRouteXYV2(
|
||||||
|
std::int64_t startX,std::int64_t startY,std::int64_t finishX,
|
||||||
|
std::int64_t finishY,std::int64_t qPerMetre)
|
||||||
|
{
|
||||||
|
return {{{{startX,startY}},{{-40*qPerMetre,40*qPerMetre}},
|
||||||
|
{{-40*qPerMetre,-40*qPerMetre}},{{40*qPerMetre,-40*qPerMetre}},
|
||||||
|
{{40*qPerMetre,40*qPerMetre}},{{finishX,finishY}}}};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<WorldAssemblyRoutePointV1> BuildCoreLiminalRouteV2(
|
||||||
|
const WorldAssemblySurfaceProfileV1& profile,
|
||||||
|
const ResolvedLayoutLandingPadV1& arrivalPad,
|
||||||
|
const ResolvedLayoutLandingPadV1& departurePad,
|
||||||
|
const WorldAssemblyLiminalVariantDefinitionV1& definition,
|
||||||
|
std::uint32_t seed,std::int32_t strateIndex,std::int64_t coreMicrons,
|
||||||
|
std::int64_t qPerMetre,
|
||||||
|
std::uint64_t& totalHorizontal,std::string& error)
|
||||||
|
{
|
||||||
|
error.clear();totalHorizontal=0;
|
||||||
|
const auto groundHeightAt=[&](std::int64_t x,std::int64_t y)
|
||||||
|
{
|
||||||
|
if(profile.kind==WorldAssemblySurfaceProfileKindV1::flatField)
|
||||||
|
return profile.h0Subvoxels;
|
||||||
|
const std::int64_t turnX=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||||
|
x,profile.wavelengthXSubvoxels,profile.phaseXTurnsQ32);
|
||||||
|
const std::int64_t turnY=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||||
|
y,profile.wavelengthYSubvoxels,profile.phaseYTurnsQ32);
|
||||||
|
return profile.h0Subvoxels
|
||||||
|
+world_assembly_detail::MulQ30(profile.amplitudeXSubvoxels,
|
||||||
|
world_assembly_detail::CosineQ30(turnX))
|
||||||
|
+world_assembly_detail::MulQ30(profile.amplitudeYSubvoxels,
|
||||||
|
world_assembly_detail::CosineQ30(turnY));
|
||||||
|
};
|
||||||
|
const auto horizontalLengthQ=[](std::int64_t dx,std::int64_t dy)
|
||||||
|
{return world_assembly_detail::IntegerSqrt(static_cast<std::uint64_t>(dx*dx+dy*dy));};
|
||||||
|
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||||
|
{return (micrometres*WorldAssemblySubvoxelsPerVoxel+coreMicrons/2)/coreMicrons;};
|
||||||
|
const auto routeXY=CoreLiminalRouteXYV2(arrivalPad.positionSubvoxels[0],
|
||||||
|
arrivalPad.positionSubvoxels[1],departurePad.positionSubvoxels[0],
|
||||||
|
departurePad.positionSubvoxels[1],qPerMetre);
|
||||||
|
std::vector<WorldAssemblyRoutePointV1> route;
|
||||||
|
const std::int64_t pointSpacing=12*qPerMetre;
|
||||||
|
const std::int64_t routeRadius=std::max<std::int64_t>(
|
||||||
|
micrometresToQ(definition.routeHalfWidthMicrometres),micrometresToQ(500000));
|
||||||
|
const auto routeId=[&](std::size_t ordinal)
|
||||||
|
{
|
||||||
|
std::uint64_t id=14695981039346656037ull;
|
||||||
|
const auto add=[&](std::uint64_t value,std::uint32_t bytes)
|
||||||
|
{for(std::uint32_t byte=0;byte<bytes;++byte)
|
||||||
|
{id^=static_cast<std::uint8_t>(value>>(byte*8));id*=1099511628211ull;}};
|
||||||
|
add(seed,4);add(static_cast<std::uint32_t>(strateIndex),4);add(ordinal,4);
|
||||||
|
return id?id:1;
|
||||||
|
};
|
||||||
|
const auto appendRoutePoint=[&](std::int64_t x,std::int64_t y)
|
||||||
|
{
|
||||||
|
WorldAssemblyRoutePointV1 point;
|
||||||
|
point.positionSubvoxels={x,y,groundHeightAt(x,y)};
|
||||||
|
point.radiusSubvoxels=routeRadius;
|
||||||
|
point.stableFeatureId=routeId(route.size());
|
||||||
|
route.push_back(point);
|
||||||
|
};
|
||||||
|
const auto interpolate=[](std::int64_t start,std::int64_t delta,
|
||||||
|
std::uint64_t step,std::uint64_t steps)
|
||||||
|
{
|
||||||
|
const std::int64_t numerator=delta*static_cast<std::int64_t>(step);
|
||||||
|
const std::int64_t divisor=static_cast<std::int64_t>(steps);
|
||||||
|
return start+(numerator>=0?(numerator+divisor/2)/divisor
|
||||||
|
:(numerator-divisor/2)/divisor);
|
||||||
|
};
|
||||||
|
appendRoutePoint(routeXY.front()[0],routeXY.front()[1]);
|
||||||
|
for(std::size_t segment=1;segment<routeXY.size();++segment)
|
||||||
|
{
|
||||||
|
const std::int64_t dx=routeXY[segment][0]-routeXY[segment-1][0];
|
||||||
|
const std::int64_t dy=routeXY[segment][1]-routeXY[segment-1][1];
|
||||||
|
const std::uint64_t length=horizontalLengthQ(dx,dy);
|
||||||
|
if(length==0){error="Core.Liminal.Field v2 route contains a zero-horizontal segment";return {};}
|
||||||
|
totalHorizontal+=length;
|
||||||
|
const std::uint64_t steps=std::max<std::uint64_t>(1,
|
||||||
|
(length+static_cast<std::uint64_t>(pointSpacing)-1)
|
||||||
|
/static_cast<std::uint64_t>(pointSpacing));
|
||||||
|
for(std::uint64_t step=1;step<=steps;++step)
|
||||||
|
appendRoutePoint(interpolate(routeXY[segment-1][0],dx,step,steps),
|
||||||
|
interpolate(routeXY[segment-1][1],dy,step,steps));
|
||||||
|
}
|
||||||
|
if(route.size()<2||totalHorizontal<300*static_cast<std::uint64_t>(qPerMetre)
|
||||||
|
||route.front().positionSubvoxels[2]!=arrivalPad.floorHeightSubvoxels
|
||||||
|
||route.back().positionSubvoxels[2]!=departurePad.floorHeightSubvoxels)
|
||||||
|
{error="Core.Liminal.Field v2 pad-to-pad route is incomplete or too short";return {};}
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PrepareCoreLiminalLandingPadsV2(WorldAssemblyRecipeV1& recipe,
|
||||||
|
WorldAssemblyStratumPlanV1& stratum,ResolvedLayoutSnapshotV3& layout,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
error.clear();
|
||||||
|
if(layout.coreVoxelSizeMicrometres==0||stratum.strateIndex<0
|
||||||
|
||stratum.topChunkZ<stratum.bottomChunkZ)
|
||||||
|
{error="Core.Liminal.Field v2 needs a finite stratum and resolved Core voxel size";return false;}
|
||||||
|
auto resolved=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
|
[&](const auto& value){return value.strateIndex==stratum.strateIndex
|
||||||
|
&&value.generatorKind=="SurfaceWorld";});
|
||||||
|
if(resolved==layout.strates.end()||!resolved->content.presentInCurrentField)
|
||||||
|
{error="Core.Liminal.Field v2 must target a resolved SurfaceWorld content band";return false;}
|
||||||
|
|
||||||
|
constexpr std::int64_t Q=WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
const std::int64_t coreMicrons=layout.coreVoxelSizeMicrometres;
|
||||||
|
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||||
|
{
|
||||||
|
const std::int64_t numerator=micrometres*Q;
|
||||||
|
return numerator>=0?(numerator+coreMicrons/2)/coreMicrons
|
||||||
|
:(numerator-coreMicrons/2)/coreMicrons;
|
||||||
|
};
|
||||||
|
const std::int64_t qPerMetre=micrometresToQ(1000000);
|
||||||
|
if(qPerMetre<=0){error="Core.Liminal.Field v2 could not resolve fixed-scale metres";return false;}
|
||||||
|
|
||||||
|
const std::uint32_t seed=recipe.seed;
|
||||||
|
const WorldAssemblyLiminalVariantV1 variant=SelectCoreLiminalFieldVariantV1(seed);
|
||||||
|
const auto& definition=CoreLiminalFieldVariantsV1[static_cast<std::size_t>(variant)];
|
||||||
|
const std::int64_t bottom=static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||||
|
*recipe.layoutChunkWidthSubvoxels;
|
||||||
|
const std::int64_t top=(static_cast<std::int64_t>(stratum.topChunkZ)+1)
|
||||||
|
*recipe.layoutChunkWidthSubvoxels;
|
||||||
|
const std::int64_t contentBottom=micrometresToQ(resolved->content.bottomMicrometres);
|
||||||
|
const std::int64_t profileRelief=variant==WorldAssemblyLiminalVariantV1::repeatingHills
|
||||||
|
?2*micrometresToQ(definition.hillAmplitudeMicrometres):0;
|
||||||
|
const std::int64_t deckSweepRadius=static_cast<std::int64_t>(std::ceil(std::hypot(
|
||||||
|
static_cast<double>(definition.routeHalfWidthMicrometres+250000),
|
||||||
|
static_cast<double>(definition.routeHalfThicknessMicrometres+250000))));
|
||||||
|
const std::int64_t floorClearance=definition.routeHalfThicknessMicrometres
|
||||||
|
+deckSweepRadius+250000;
|
||||||
|
// Keep the field above the lower tunnel's mouth transition so the routed
|
||||||
|
// departure has a clean open section before it reaches the cave wall.
|
||||||
|
constexpr std::int64_t MinimumFieldGroundMarginMicrometres=20000000;
|
||||||
|
const std::int64_t fieldGroundMargin=std::max(floorClearance,
|
||||||
|
MinimumFieldGroundMarginMicrometres);
|
||||||
|
const std::int64_t floorHeight=contentBottom+profileRelief
|
||||||
|
+micrometresToQ(fieldGroundMargin);
|
||||||
|
if(floorHeight<=bottom||top<=floorHeight+8*qPerMetre)
|
||||||
|
{error="Core.Liminal.Field v2 has no room for its supported field and cap";return false;}
|
||||||
|
|
||||||
|
WorldAssemblySurfaceProfileV1 profile;
|
||||||
|
profile.version=1;
|
||||||
|
profile.kind=definition.surfaceKind;
|
||||||
|
profile.h0Subvoxels=floorHeight;
|
||||||
|
if(profile.kind==WorldAssemblySurfaceProfileKindV1::periodicHills)
|
||||||
|
{
|
||||||
|
profile.amplitudeXSubvoxels=micrometresToQ(definition.hillAmplitudeMicrometres);
|
||||||
|
profile.amplitudeYSubvoxels=profile.amplitudeXSubvoxels;
|
||||||
|
profile.wavelengthXSubvoxels=micrometresToQ(definition.hillWavelengthMicrometres);
|
||||||
|
profile.wavelengthYSubvoxels=profile.wavelengthXSubvoxels;
|
||||||
|
ResolveWorldAssemblySurfaceProfilePhasesV1(profile,seed);
|
||||||
|
}
|
||||||
|
if(!world_assembly_detail::ValidateSurfaceProfile(profile,seed,error))return false;
|
||||||
|
|
||||||
|
stratum.hasRecipeOverride=true;
|
||||||
|
stratum.recipeOverride=recipe;
|
||||||
|
stratum.recipeOverride.surfaceProfile=profile;
|
||||||
|
stratum.recipeOverride.contentHash=0;
|
||||||
|
stratum.surfaceProfile=profile;
|
||||||
|
stratum.capHeightSubvoxels=top-4*qPerMetre;
|
||||||
|
stratum.capUndulationSubvoxels=0;
|
||||||
|
stratum.capDetailAmplitudeSubvoxels=0;
|
||||||
|
stratum.capRidgeAmplitudeSubvoxels=0;
|
||||||
|
stratum.waterEnabled=false;
|
||||||
|
stratum.waterSurfaceSubvoxels=0;
|
||||||
|
|
||||||
|
const auto groundHeightAt=[&](std::int64_t x,std::int64_t y)
|
||||||
|
{
|
||||||
|
if(profile.kind==WorldAssemblySurfaceProfileKindV1::flatField)
|
||||||
|
return profile.h0Subvoxels;
|
||||||
|
const std::int64_t turnX=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||||
|
x,profile.wavelengthXSubvoxels,profile.phaseXTurnsQ32);
|
||||||
|
const std::int64_t turnY=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||||
|
y,profile.wavelengthYSubvoxels,profile.phaseYTurnsQ32);
|
||||||
|
return profile.h0Subvoxels
|
||||||
|
+world_assembly_detail::MulQ30(profile.amplitudeXSubvoxels,
|
||||||
|
world_assembly_detail::CosineQ30(turnX))
|
||||||
|
+world_assembly_detail::MulQ30(profile.amplitudeYSubvoxels,
|
||||||
|
world_assembly_detail::CosineQ30(turnY));
|
||||||
|
};
|
||||||
|
const auto routeXY=CoreLiminalRouteXYV2(-12*qPerMetre,0,12*qPerMetre,0,qPerMetre);
|
||||||
|
const auto squareRoot=[](std::uint64_t value)
|
||||||
|
{return world_assembly_detail::IntegerSqrt(value);};
|
||||||
|
const auto directionQ16=[&](std::int64_t dx,std::int64_t dy)
|
||||||
|
{
|
||||||
|
const std::uint64_t length=squareRoot(static_cast<std::uint64_t>(dx*dx+dy*dy));
|
||||||
|
if(length==0)return std::array<std::int32_t,2>{{0,0}};
|
||||||
|
const auto rounded=[&](std::int64_t value)
|
||||||
|
{return value>=0?(value+static_cast<std::int64_t>(length/2))
|
||||||
|
/static_cast<std::int64_t>(length)
|
||||||
|
:(value-static_cast<std::int64_t>(length/2))
|
||||||
|
/static_cast<std::int64_t>(length);};
|
||||||
|
return std::array<std::int32_t,2>{{
|
||||||
|
static_cast<std::int32_t>(rounded(dx*65536)),
|
||||||
|
static_cast<std::int32_t>(rounded(dy*65536))}};
|
||||||
|
};
|
||||||
|
const auto arrivalDirection=directionQ16(routeXY[1][0]-routeXY[0][0],
|
||||||
|
routeXY[1][1]-routeXY[0][1]);
|
||||||
|
const auto departureDirection=directionQ16(routeXY.back()[0]-routeXY[routeXY.size()-2][0],
|
||||||
|
routeXY.back()[1]-routeXY[routeXY.size()-2][1]);
|
||||||
|
const std::int64_t playerCenterAboveFloor=micrometresToQ(1130000);
|
||||||
|
const std::int64_t landingClearance=micrometresToQ(6000000);
|
||||||
|
const std::int64_t supportDepth=micrometresToQ(2000000);
|
||||||
|
const auto makePad=[&](std::string id,ResolvedLayoutLandingPadRoleV1 role,
|
||||||
|
const std::array<std::int64_t,2>& xy,const std::array<std::int32_t,2>& direction)
|
||||||
|
{
|
||||||
|
ResolvedLayoutLandingPadV1 pad;
|
||||||
|
pad.stableId=std::move(id);pad.role=role;
|
||||||
|
pad.floorHeightSubvoxels=groundHeightAt(xy[0],xy[1]);
|
||||||
|
pad.positionSubvoxels={xy[0],xy[1],pad.floorHeightSubvoxels+playerCenterAboveFloor};
|
||||||
|
pad.approachDirectionQ16=direction;
|
||||||
|
pad.clearanceSubvoxels=landingClearance;
|
||||||
|
pad.supportDepthSubvoxels=supportDepth;
|
||||||
|
pad.certifiedWalkableWithSupport=true;
|
||||||
|
return pad;
|
||||||
|
};
|
||||||
|
resolved->recipeId=std::string(CoreLiminalFieldRecipeIdV1);
|
||||||
|
resolved->recipeVersion=CoreLiminalFieldRecipeVersionV2;
|
||||||
|
resolved->landingPads.clear();
|
||||||
|
resolved->landingPads.push_back(makePad("core/liminal-field/landing/arrival/000",
|
||||||
|
ResolvedLayoutLandingPadRoleV1::arrival,routeXY.front(),arrivalDirection));
|
||||||
|
resolved->landingPads.push_back(makePad("core/liminal-field/landing/departure/000",
|
||||||
|
ResolvedLayoutLandingPadRoleV1::departure,routeXY.back(),departureDirection));
|
||||||
|
std::uint64_t routeLength=0;
|
||||||
|
stratum.route=BuildCoreLiminalRouteV2(profile,resolved->landingPads[0],
|
||||||
|
resolved->landingPads[1],definition,seed,stratum.strateIndex,coreMicrons,
|
||||||
|
qPerMetre,routeLength,error);
|
||||||
|
if(!error.empty())return false;
|
||||||
|
stratum.routeConstraints={1,micrometresToQ(340000),micrometresToQ(1760000),
|
||||||
|
17560,micrometresToQ(125000),micrometresToQ(250000),
|
||||||
|
micrometresToQ(1000000),micrometresToQ(120000),3};
|
||||||
|
resolved->anchors.erase(std::remove_if(resolved->anchors.begin(),resolved->anchors.end(),
|
||||||
|
[](const auto& anchor){return anchor.kind=="route_point"
|
||||||
|
||anchor.kind=="liminal_route_point"||anchor.kind=="liminal_variant"
|
||||||
|
||anchor.kind=="heightfield_profile";}),resolved->anchors.end());
|
||||||
|
ResolvedLayoutAnchorV2 profileAnchor;
|
||||||
|
profileAnchor.kind="heightfield_profile";
|
||||||
|
profileAnchor.stableId="heightfield-profile/liminal/"
|
||||||
|
+std::to_string(static_cast<std::uint32_t>(profile.kind));
|
||||||
|
profileAnchor.units="profile kind enum";
|
||||||
|
profileAnchor.values={1,static_cast<std::int64_t>(profile.kind)};
|
||||||
|
resolved->anchors.push_back(std::move(profileAnchor));
|
||||||
|
std::uint64_t routeAnchorDistance=0,lastRouteAnchorDistance=0;
|
||||||
|
for(std::size_t pointIndex=0;pointIndex<stratum.route.size();++pointIndex)
|
||||||
|
{
|
||||||
|
if(pointIndex>0)
|
||||||
|
{
|
||||||
|
const auto& previous=stratum.route[pointIndex-1].positionSubvoxels;
|
||||||
|
const auto& current=stratum.route[pointIndex].positionSubvoxels;
|
||||||
|
const auto dx=static_cast<std::uint64_t>(current[0]-previous[0]);
|
||||||
|
const auto dy=static_cast<std::uint64_t>(current[1]-previous[1]);
|
||||||
|
routeAnchorDistance+=world_assembly_detail::IntegerSqrt(dx*dx+dy*dy);
|
||||||
|
}
|
||||||
|
const bool first=pointIndex==0;
|
||||||
|
const bool last=pointIndex+1==stratum.route.size();
|
||||||
|
const bool spaced=routeAnchorDistance-lastRouteAnchorDistance
|
||||||
|
>=static_cast<std::uint64_t>(8*qPerMetre);
|
||||||
|
if(!first&&!last&&!spaced)continue;
|
||||||
|
const auto& point=stratum.route[pointIndex];
|
||||||
|
ResolvedLayoutAnchorV2 routeAnchor;
|
||||||
|
routeAnchor.kind="route_point";
|
||||||
|
routeAnchor.stableId="route/"+std::to_string(pointIndex);
|
||||||
|
routeAnchor.units="1/256 Core voxel";
|
||||||
|
routeAnchor.values={point.positionSubvoxels[0],point.positionSubvoxels[1],
|
||||||
|
point.positionSubvoxels[2]+playerCenterAboveFloor,point.radiusSubvoxels};
|
||||||
|
resolved->anchors.push_back(std::move(routeAnchor));
|
||||||
|
lastRouteAnchorDistance=routeAnchorDistance;
|
||||||
|
}
|
||||||
|
if(!layout.finalize(error))return false;
|
||||||
|
const auto reportPad=[&](const ResolvedLayoutLandingPadV1& pad)
|
||||||
|
{
|
||||||
|
std::printf("[ResolvedLandingPad] strate=%d id=%s role=%s position_m=(%.3f,%.3f,%.3f) "
|
||||||
|
"floor_m=%.3f approach_q16=(%d,%d) clearance_m=%.3f support_m=%.3f "
|
||||||
|
"certified_walkable_with_support=true status=PASS\n",stratum.strateIndex,
|
||||||
|
pad.stableId.c_str(),pad.role==ResolvedLayoutLandingPadRoleV1::arrival?"arrival":"departure",
|
||||||
|
pad.positionSubvoxels[0]/static_cast<double>(qPerMetre),
|
||||||
|
pad.positionSubvoxels[1]/static_cast<double>(qPerMetre),
|
||||||
|
pad.positionSubvoxels[2]/static_cast<double>(qPerMetre),
|
||||||
|
pad.floorHeightSubvoxels/static_cast<double>(qPerMetre),
|
||||||
|
pad.approachDirectionQ16[0],pad.approachDirectionQ16[1],
|
||||||
|
pad.clearanceSubvoxels/static_cast<double>(qPerMetre),
|
||||||
|
pad.supportDepthSubvoxels/static_cast<double>(qPerMetre));
|
||||||
|
};
|
||||||
|
reportPad(resolved->landingPads[0]);reportPad(resolved->landingPads[1]);
|
||||||
|
std::printf("[LiminalPadRoute] points=%zu path_length_m=%.3f spacing_max_m=4.000 "
|
||||||
|
"profile_ground_following=true status=PASS\n",stratum.route.size(),
|
||||||
|
routeLength/static_cast<double>(qPerMetre));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlanCoreLiminalFieldV2(WorldAssemblyRecipeV1& recipe,
|
||||||
|
WorldAssemblyStratumPlanV1& stratum,const ResolvedLayoutSnapshotV3& layout,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
error.clear();
|
||||||
|
if(layout.coreVoxelSizeMicrometres==0||stratum.strateIndex<0
|
||||||
|
||stratum.topChunkZ<stratum.bottomChunkZ)
|
||||||
|
{error="Core.Liminal.Field v2 needs a finite stratum and resolved Core voxel size";return false;}
|
||||||
|
const auto resolved=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
|
[&](const auto& value){return value.strateIndex==stratum.strateIndex
|
||||||
|
&&value.recipeId==CoreLiminalFieldRecipeIdV1
|
||||||
|
&&value.recipeVersion==CoreLiminalFieldRecipeVersionV2;});
|
||||||
|
if(resolved==layout.strates.end()||!resolved->content.presentInCurrentField
|
||||||
|
||stratum.surfaceProfile.version!=1)
|
||||||
|
{error="Core.Liminal.Field v2 needs its prepared profile and resolved landing pads";return false;}
|
||||||
|
const ResolvedLayoutLandingPadV1* arrivalPad=nullptr;
|
||||||
|
const ResolvedLayoutLandingPadV1* departurePad=nullptr;
|
||||||
|
for(const auto& pad:resolved->landingPads)
|
||||||
|
{
|
||||||
|
if(pad.role==ResolvedLayoutLandingPadRoleV1::arrival)
|
||||||
|
{if(arrivalPad){error="Core.Liminal.Field v2 has multiple arrival pads";return false;}arrivalPad=&pad;}
|
||||||
|
if(pad.role==ResolvedLayoutLandingPadRoleV1::departure)
|
||||||
|
{if(departurePad){error="Core.Liminal.Field v2 has multiple departure pads";return false;}departurePad=&pad;}
|
||||||
|
}
|
||||||
|
if(!arrivalPad||!departurePad||!arrivalPad->certifiedWalkableWithSupport
|
||||||
|
||!departurePad->certifiedWalkableWithSupport)
|
||||||
|
{error="Core.Liminal.Field v2 requires certified arrival and departure pads";return false;}
|
||||||
|
|
||||||
|
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||||||
|
const ResolvedLayoutPassageV1* departure=nullptr;
|
||||||
|
for(const auto& passage:layout.passages)
|
||||||
|
{
|
||||||
|
if(passage.lowerStrateIndex==stratum.strateIndex)
|
||||||
|
{if(arrival){error="Core.Liminal.Field v2 requires one arrival passage";return false;}arrival=&passage;}
|
||||||
|
if(passage.upperStrateIndex==stratum.strateIndex)
|
||||||
|
{if(departure){error="Core.Liminal.Field v2 requires one departure passage";return false;}departure=&passage;}
|
||||||
|
}
|
||||||
|
if(!arrival||!departure||arrival->lowerMouthSubvoxels!=arrivalPad->positionSubvoxels
|
||||||
|
||departure->upperMouthSubvoxels!=departurePad->positionSubvoxels)
|
||||||
|
{error="Core.Liminal.Field v2 passage mouths are not bound to the certified layer pads";return false;}
|
||||||
|
|
||||||
|
constexpr std::int64_t Q=WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
const std::int64_t coreMicrons=layout.coreVoxelSizeMicrometres;
|
||||||
|
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||||
|
{
|
||||||
|
const std::int64_t numerator=micrometres*Q;
|
||||||
|
return numerator>=0?(numerator+coreMicrons/2)/coreMicrons
|
||||||
|
:(numerator-coreMicrons/2)/coreMicrons;
|
||||||
|
};
|
||||||
|
const std::int64_t qPerMetre=micrometresToQ(1000000);
|
||||||
|
const std::int64_t playerCenterAboveFloor=micrometresToQ(1130000);
|
||||||
|
if(qPerMetre<=0||arrivalPad->positionSubvoxels[2]
|
||||||
|
!=arrivalPad->floorHeightSubvoxels+playerCenterAboveFloor
|
||||||
|
||departurePad->positionSubvoxels[2]
|
||||||
|
!=departurePad->floorHeightSubvoxels+playerCenterAboveFloor)
|
||||||
|
{error="Core.Liminal.Field v2 pad floor and capsule-center heights differ";return false;}
|
||||||
|
|
||||||
|
const std::uint32_t seed=recipe.seed;
|
||||||
|
const WorldAssemblyLiminalVariantV1 variant=SelectCoreLiminalFieldVariantV1(seed);
|
||||||
|
const auto& definition=CoreLiminalFieldVariantsV1[static_cast<std::size_t>(variant)];
|
||||||
|
const auto& profile=stratum.surfaceProfile;
|
||||||
|
std::uint64_t totalHorizontal=0;
|
||||||
|
std::vector<WorldAssemblyRoutePointV1> route=BuildCoreLiminalRouteV2(profile,
|
||||||
|
*arrivalPad,*departurePad,definition,seed,stratum.strateIndex,coreMicrons,
|
||||||
|
qPerMetre,totalHorizontal,error);
|
||||||
|
if(!error.empty())return false;
|
||||||
|
if(route.size()!=stratum.route.size())
|
||||||
|
{error="Core.Liminal.Field v2 route differs from the path used to certify its pads";return false;}
|
||||||
|
for(std::size_t index=0;index<route.size();++index)
|
||||||
|
if(route[index].stableFeatureId!=stratum.route[index].stableFeatureId
|
||||||
|
||route[index].positionSubvoxels!=stratum.route[index].positionSubvoxels
|
||||||
|
||route[index].radiusSubvoxels!=stratum.route[index].radiusSubvoxels)
|
||||||
|
{error="Core.Liminal.Field v2 route differs from the path used to certify its pads";return false;}
|
||||||
|
|
||||||
|
WorldAssemblyLiminalFieldPlanV1 liminal;
|
||||||
|
liminal.version=CoreLiminalFieldRecipeVersionV2;
|
||||||
|
liminal.variant=variant;liminal.seed=seed;
|
||||||
|
liminal.arrivalMouthSubvoxels=arrivalPad->positionSubvoxels;
|
||||||
|
liminal.departureMouthSubvoxels=departurePad->positionSubvoxels;
|
||||||
|
liminal.floorHeightSubvoxels=profile.h0Subvoxels;
|
||||||
|
liminal.routeHalfWidthSubvoxels=micrometresToQ(definition.routeHalfWidthMicrometres);
|
||||||
|
liminal.routeHalfThicknessSubvoxels=micrometresToQ(definition.routeHalfThicknessMicrometres);
|
||||||
|
liminal.routeOffsetSubvoxels=150*qPerMetre;
|
||||||
|
liminal.supportSpacingSubvoxels=micrometresToQ(definition.supportSpacingMicrometres);
|
||||||
|
liminal.supportRadiusSubvoxels=micrometresToQ(definition.supportRadiusMicrometres);
|
||||||
|
liminal.mouthFloorOffsetSubvoxels=playerCenterAboveFloor;
|
||||||
|
stratum.route=std::move(route);
|
||||||
|
stratum.routeConstraints={1,micrometresToQ(340000),micrometresToQ(1760000),
|
||||||
|
17560,micrometresToQ(125000),micrometresToQ(250000),
|
||||||
|
micrometresToQ(1000000),micrometresToQ(120000),3};
|
||||||
|
stratum.liminalField=liminal;
|
||||||
|
std::string validationError;
|
||||||
|
if(!world_assembly_detail::ValidateSurfaceProfile(stratum.surfaceProfile,seed,validationError)
|
||||||
|
||!world_assembly_detail::ValidateStratum(stratum,recipe,validationError))
|
||||||
|
{error=validationError.empty()?"Core.Liminal.Field v2 produced an invalid plan":validationError;return false;}
|
||||||
|
std::printf("[CoreLiminalFieldPlan] recipe=%.*s version=%u variant=%.*s seed=%u "
|
||||||
|
"arrival_pad=%s departure_pad=%s route_points=%zu path_length_m=%.3f "
|
||||||
|
"profile_ground_following=true route_grade_limit_deg=15.000 status=PASS\n",
|
||||||
|
static_cast<int>(CoreLiminalFieldRecipeIdV1.size()),CoreLiminalFieldRecipeIdV1.data(),
|
||||||
|
liminal.version,static_cast<int>(definition.name.size()),definition.name.data(),seed,
|
||||||
|
arrivalPad->stableId.c_str(),departurePad->stableId.c_str(),stratum.route.size(),
|
||||||
|
totalHorizontal/static_cast<double>(qPerMetre));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string WorldAssemblyManifest::ToJson() const
|
std::string WorldAssemblyManifest::ToJson() const
|
||||||
{
|
{
|
||||||
std::string error;
|
std::string error;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the 3-strata composed Core.Liminal.Field v2 fixture."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||||
|
ROOT / "Standalone/data/owner_world.json"))
|
||||||
|
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l145"))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
|
world = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||||
|
if len(world.get("strates", [])) < 3:
|
||||||
|
raise SystemExit("owner fixture needs at least three authored strate slots")
|
||||||
|
|
||||||
|
world["strates"] = world["strates"][:3]
|
||||||
|
world["total_strates"] = 3
|
||||||
|
world["settings_asset"]["TotalStrates"] = 3
|
||||||
|
world["settings_asset"]["FixedStrates"] = world["settings_asset"]["FixedStrates"][:3]
|
||||||
|
# Keep the resolved arrival near the departure depth. The owner's 5000-unit
|
||||||
|
# reach clamps to liminal bedrock on most seeds and leaves no field floor.
|
||||||
|
world["strates"][0]["passage_config"]["ReachMin"] = 1536
|
||||||
|
world["strates"][0]["passage_config"]["ReachMax"] = 1792
|
||||||
|
world["surface_profile_override"] = {
|
||||||
|
"version": 1,
|
||||||
|
"kind": "flatField",
|
||||||
|
"minimum_arena_clearance_m": 0.0,
|
||||||
|
}
|
||||||
|
middle = world["strates"][1]
|
||||||
|
middle["asset"]["GeneratorType"] = 4 # ECaveGeneratorType::SurfaceWorld
|
||||||
|
middle["asset"]["StrateName"] = "Liminal Field"
|
||||||
|
# Keep the departure mouth 24–32 m above liminal bedrock so every variant
|
||||||
|
# has room for its floor, road landing and support footings.
|
||||||
|
middle["passage_config"]["ReachMin"] = 192
|
||||||
|
middle["passage_config"]["ReachMax"] = 256
|
||||||
|
world["core_liminal_field"] = {
|
||||||
|
"version": 2,
|
||||||
|
"strate_index": int(middle["index"]),
|
||||||
|
"variant_policy": "seed_modulo_4",
|
||||||
|
}
|
||||||
|
|
||||||
|
output = args.out / "core-liminal-field-3-strata.json"
|
||||||
|
output.write_text(json.dumps(world, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||||
|
print(f"[L145Fixture] path={output} strates=SurfaceWorld,Core.Liminal.Field,TunnelNetwork status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Capture one arrival-mouth eye view per Core.Liminal.Field v2 variant."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
OUT = ROOT / "Saved/Linux/l145"
|
||||||
|
CAPTURES = HERE / "captures"
|
||||||
|
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||||
|
Q_PER_METRE = 1024.0
|
||||||
|
|
||||||
|
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||||
|
from runner import bounded_run, current_stamp, first_stamp
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values) -> str:
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def run_step(name: str, command: list[object], log: Path, env: dict[str, str],
|
||||||
|
stamped: bool = False) -> str:
|
||||||
|
expected_stamp = current_stamp() if stamped else None
|
||||||
|
if stamped:
|
||||||
|
binary = Path(str(command[0]))
|
||||||
|
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||||
|
if probe is None or probe.returncode != 0 \
|
||||||
|
or first_stamp(probe.stdout or "") != expected_stamp:
|
||||||
|
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||||
|
started = time.monotonic()
|
||||||
|
log.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)],
|
||||||
|
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1745, check=False)
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[L145Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||||
|
flush=True)
|
||||||
|
if stamped and (first_stamp(output) != expected_stamp
|
||||||
|
or current_stamp() != expected_stamp):
|
||||||
|
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"{name} exit={result.returncode}; see {log}")
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def camera_angles(camera, target):
|
||||||
|
dx, dy, dz = (target[i] - camera[i] for i in range(3))
|
||||||
|
return math.degrees(math.atan2(dx, dy)), math.degrees(
|
||||||
|
math.atan2(dz, math.hypot(dx, dy)))
|
||||||
|
|
||||||
|
|
||||||
|
def route_views(manifest_path: Path, layout_path: Path, name: str):
|
||||||
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
|
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||||
|
plan = next(row for row in manifest["plan"]["strates"]
|
||||||
|
if row["strate_index"] == 1 and row.get("liminal_field", {}).get("version") == 2)
|
||||||
|
points = [tuple(value / Q_PER_METRE for value in row["position_q"])
|
||||||
|
for row in plan["route"]]
|
||||||
|
first, second = points[0], points[1]
|
||||||
|
dx, dy = second[0] - first[0], second[1] - first[1]
|
||||||
|
length = math.hypot(dx, dy)
|
||||||
|
if length <= 1.0e-6:
|
||||||
|
raise RuntimeError("Core.Liminal.Field arrival tangent is degenerate")
|
||||||
|
tx, ty = dx / length, dy / length
|
||||||
|
eye_camera = (first[0] - tx * 1.5, first[1] - ty * 1.5, first[2] + 1.6)
|
||||||
|
third = points[min(2, len(points) - 1)]
|
||||||
|
dx, dy = third[0] - first[0], third[1] - first[1]
|
||||||
|
tangent_length = math.hypot(dx, dy)
|
||||||
|
tx, ty = dx / tangent_length, dy / tangent_length
|
||||||
|
dz_per_metre = (third[2] - first[2]) / tangent_length
|
||||||
|
eye_target = (first[0] + tx * 40.0, first[1] + ty * 40.0,
|
||||||
|
first[2] + dz_per_metre * 40.0 + 1.6)
|
||||||
|
|
||||||
|
middle = len(points) // 2
|
||||||
|
before, center, after = points[middle - 1], points[middle], points[middle + 1]
|
||||||
|
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||||
|
length = math.hypot(dx, dy)
|
||||||
|
if length <= 1.0e-6:
|
||||||
|
raise RuntimeError("Core.Liminal.Field wide-view tangent is degenerate")
|
||||||
|
tx, ty = dx / length, dy / length
|
||||||
|
nx, ny = -ty, tx
|
||||||
|
floor = plan["liminal_field"]["floor_height_q"] / Q_PER_METRE
|
||||||
|
if name == "flatField-monolith":
|
||||||
|
monolith = next(row for row in layout.get("stamps", [])
|
||||||
|
if row.get("stable_id") == "core/liminal-field/monolith/000")
|
||||||
|
monolith_center = tuple(value / 1.0e6 for value in monolith["position_um"])
|
||||||
|
floor = monolith_center[2] - monolith["half_extents_um"][2] / 1.0e6
|
||||||
|
wide_camera = (monolith_center[0] - tx * 30.0 + nx * 18.0,
|
||||||
|
monolith_center[1] - ty * 30.0 + ny * 18.0, floor + 25.0)
|
||||||
|
wide_target = monolith_center
|
||||||
|
elif name == "periodicHills":
|
||||||
|
wide_camera = (center[0] - 20.0, center[1] - 25.0, floor + 10.0)
|
||||||
|
wide_target = (center[0] + 10.0, center[1] + 8.0, floor + 1.0)
|
||||||
|
elif name == "pillar-grid":
|
||||||
|
pillars = [row for row in layout.get("stamps", [])
|
||||||
|
if row.get("stable_id", "").startswith("core/liminal-field/pillars/")]
|
||||||
|
if not pillars:
|
||||||
|
raise RuntimeError("pillar-grid layout has no resolved grid stamps")
|
||||||
|
pairs = []
|
||||||
|
for i, first_pillar in enumerate(pillars):
|
||||||
|
for second_pillar in pillars[i + 1:]:
|
||||||
|
a = first_pillar["position_um"]
|
||||||
|
b = second_pillar["position_um"]
|
||||||
|
dx, dy = (b[0] - a[0]) / 1.0e6, (b[1] - a[1]) / 1.0e6
|
||||||
|
separation = math.hypot(dx, dy)
|
||||||
|
if 60.0 <= separation <= 90.0:
|
||||||
|
mx = (a[0] + b[0]) / 2.0e6
|
||||||
|
my = (a[1] + b[1]) / 2.0e6
|
||||||
|
pairs.append((math.hypot(mx - center[0], my - center[1]),
|
||||||
|
mx, my, dx / separation, dy / separation,
|
||||||
|
(a[2] + b[2]) / 2.0e6,
|
||||||
|
first_pillar["half_extents_um"][2] / 1.0e6))
|
||||||
|
if not pairs:
|
||||||
|
raise RuntimeError("pillar-grid has no neighboring resolved columns")
|
||||||
|
_, mx, my, tx, ty, pillar_z, half_height = min(pairs)
|
||||||
|
floor = pillar_z - half_height
|
||||||
|
nx, ny = -ty, tx
|
||||||
|
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||||
|
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||||
|
wide_target = (center[0], center[1], floor + 2.0)
|
||||||
|
else:
|
||||||
|
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||||
|
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||||
|
wide_target = (center[0], center[1], floor + 2.0)
|
||||||
|
return {"eye": (eye_camera, eye_target), "wide": (wide_camera, wide_target)}
|
||||||
|
|
||||||
|
|
||||||
|
def capture_variant(seed: int, name: str, *, headless: Path, renderer: Path,
|
||||||
|
fixture: Path, env: dict[str, str]):
|
||||||
|
stem = f"core-liminal-{name}-seed{seed:02d}"
|
||||||
|
plan_dir = OUT / "capture-plans" / stem
|
||||||
|
plan_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
manifest = plan_dir / "manifest.json"
|
||||||
|
layout = plan_dir / "layout.json"
|
||||||
|
command = [headless, fixture, "--workers=1", f"--seed={seed}",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", "--camera=0,0,-540,0,0",
|
||||||
|
f"--world-assembler-manifest-output={manifest}",
|
||||||
|
f"--resolved-layout-output={layout}"]
|
||||||
|
output = run_step(f"plan-{stem}", command, OUT / "logs" / f"capture-{stem}-plan.log",
|
||||||
|
env, stamped=True)
|
||||||
|
if "[CoreLiminalFieldDraw]" not in output:
|
||||||
|
raise RuntimeError(f"{stem} plan omitted its Core liminal deck")
|
||||||
|
views = route_views(manifest, layout, name)
|
||||||
|
for view, (eye, target) in {"eye": views["eye"]}.items():
|
||||||
|
work = OUT / "captures" / f"{stem}-{view}"
|
||||||
|
work.mkdir(parents=True, exist_ok=True)
|
||||||
|
view_manifest = work / "manifest.json"
|
||||||
|
view_layout = work / "layout.json"
|
||||||
|
mesh = work / "world.vfmesh"
|
||||||
|
yaw, pitch = camera_angles(eye, target)
|
||||||
|
camera = (*eye, yaw, pitch)
|
||||||
|
view_clip_radius = (5 if view == "wide" and name in {
|
||||||
|
"flatField-monolith", "periodicHills"} else
|
||||||
|
3)
|
||||||
|
command = [headless, fixture, "--workers=12", f"--seed={seed}",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", f"--clip-radius={view_clip_radius}",
|
||||||
|
"--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", "--capture-skip-walkability",
|
||||||
|
f"--camera={fmt(camera)}", f"--mesh-export={mesh}",
|
||||||
|
f"--world-assembler-manifest-output={view_manifest}",
|
||||||
|
f"--resolved-layout-output={view_layout}"]
|
||||||
|
output = run_step(f"export-{stem}-{view}", command,
|
||||||
|
OUT / "logs" / f"capture-{stem}-{view}-export.log", env, stamped=True)
|
||||||
|
if "[CoreLiminalFieldDraw]" not in output or not mesh.is_file() \
|
||||||
|
or mesh.stat().st_size == 0:
|
||||||
|
raise RuntimeError(f"{stem}-{view} export omitted its Core liminal deck or mesh")
|
||||||
|
png = work / f"{view}.png"
|
||||||
|
image = CAPTURES / f"{stem}-{view}.jpg"
|
||||||
|
fog = {
|
||||||
|
"flatField-monolith": (30, 180, "0.66,0.73,0.77", -708.0, 46.0),
|
||||||
|
"periodicHills": (140, 850, "0.78,0.84,0.88", -720.0, 14.0),
|
||||||
|
"road-over-cloud-floor": (24, 420, "0.88,0.93,0.95", -720.0, 65.0),
|
||||||
|
"pillar-grid": (180, 1200, "0.82,0.88,0.91", -720.0, 80.0),
|
||||||
|
}[name]
|
||||||
|
fog_start, fog_end, fog_color, height_center, height_range = fog
|
||||||
|
render = [renderer, mesh, png, "--size=1600x900", "--fov-deg=72",
|
||||||
|
f"--camera-m={fmt(eye)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||||
|
"--tile-skirts=on", "--shade=readable", "--headlamp=off", "--height-tint=on",
|
||||||
|
f"--height-center-m={height_center:.6f}", f"--height-range-m={height_range:.6f}",
|
||||||
|
"--light-direction=-0.42,-0.58,0.70", "--fog=on", f"--fog-start-m={fog_start}",
|
||||||
|
f"--fog-end-m={fog_end}", f"--fog-color={fog_color}"]
|
||||||
|
run_step(f"render-{stem}-{view}", render,
|
||||||
|
OUT / "logs" / f"capture-{stem}-{view}-render.log", env, stamped=True)
|
||||||
|
run_step(f"jpeg-{stem}-{view}", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
|
||||||
|
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image],
|
||||||
|
OUT / "logs" / f"capture-{stem}-{view}-jpeg.log", env)
|
||||||
|
run_step(f"quality-{stem}-{view}", [sys.executable,
|
||||||
|
ROOT / "Standalone/Tools/verify/capture_quality.py", image],
|
||||||
|
OUT / "logs" / f"capture-{stem}-{view}-quality.log", env)
|
||||||
|
print(f"[L145Capture] variant={name} seed={seed} view={view} "
|
||||||
|
f"eye_height={'1.600m' if view == 'eye' else 'wide'} bytes={image.stat().st_size} "
|
||||||
|
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
headless = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l145-clang"
|
||||||
|
renderer = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l145-clang"
|
||||||
|
fixture = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||||
|
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||||
|
env["LP_NUM_THREADS"] = "12"
|
||||||
|
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||||
|
variants = ("flatField-monolith", "periodicHills",
|
||||||
|
"road-over-cloud-floor", "pillar-grid")
|
||||||
|
selected = set(os.environ.get("L145_CAPTURE_VARIANTS", "").split(","))
|
||||||
|
for seed, name in enumerate(variants):
|
||||||
|
if selected and name not in selected:
|
||||||
|
continue
|
||||||
|
capture_variant(seed, name, headless=headless, renderer=renderer,
|
||||||
|
fixture=fixture, env=env)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as error:
|
||||||
|
print(f"[L145Capture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Linux145 — Core.Liminal.Field v2 landing pads
|
||||||
|
|
||||||
|
**Result: endpoint landing pads alone did not make the liminal join solvable.** The pads bind the passage mouth to certified ground and remove grade failures in the final seed 5 probe, but the passage capsule still intersects native Surface World terrain about 29 m from the departure pad. This matches R48's warning that a pad must cover the approach and tube exclusion envelope; stop here and use a bounded joint plan.
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
|
||||||
|
- Added an opt-in landing-pad record to resolved strata: stable ID, arrival/departure role, position, floor height, Q16 approach direction, clearance, support depth, and a certification bit. The passage planner validates the record and binds the endpoint and tangent to it. Empty pad lists are omitted from canonical serialization, preserving legacy owner layouts and hashes.
|
||||||
|
- Added `Core.Liminal.Field` v2 pads on the field profile: flat ground, hill/valley profile, road endpoints, and pillar-grid clearings. Its layer route connects the pads; the road uses that route as its road centerline.
|
||||||
|
- Added an uncertified-pad negative control. Corrected the ripple audit to retain each valid sample's actual route distance across masked gaps; a 5 m synthetic gap control now confirms that the measurement detects a 5.8 cm ripple and rejects the same smooth cubic control.
|
||||||
|
- Fixed the composed audit's footing placement so it counts actual structural supports. No Unreal `Source/` files changed.
|
||||||
|
|
||||||
|
## Count first: failure cause
|
||||||
|
|
||||||
|
L144's fixed-mouth route already showed both end-connector grade and capsule blockage. For the flat-field seed 0 probe, the liminal route had 222 blocked capsule samples and 7 grade failures, with no unsupported samples or structural-support failures. Pillar-grid seed 3 had 289 blocked samples and 9 grade failures. In the separate seed 5 audit, interface 1–2 had 10 blocked passage-capsule samples and the liminal route had 9 blocked samples, both with zero grade failures. These results say support alone was not the cause; the fixed endpoint geometry sometimes made the approach too steep, while other cases had occupied space at the join. [L144 report](../Linux144/REPORT.md); logs: `Saved/Linux/l144/logs/probe8-seed00.log`, `probe8-seed03.log`, `audit-seed05.log`.
|
||||||
|
|
||||||
|
The pad experiment removed the endpoint-grade part in seed 5, but not the occupied-space part:
|
||||||
|
|
||||||
|
| Probe | Passage 1–2 capsule | Grade / support | Shell | Liminal route |
|
||||||
|
|---|---:|---|---|---|
|
||||||
|
| L144 seed 5, fixed mouths | 10 blocked | 0 grade failures; 0 unsupported | not the failing gate | 9 blocked; 0 grade failures |
|
||||||
|
| L145 seed 5, 45 m ground margin | 140 blocked; 20 interior shell violations | 0 grade failures; 0 unsupported | FAIL | PASS |
|
||||||
|
| L145 seed 5, final 20 m ground margin | 289 blocked | 0 grade failures; 0 unsupported | PASS | PASS |
|
||||||
|
|
||||||
|
The final L145 Clang W1 probe (source stamp `82e3b58c…`) certified both pads. The departure pad was `(12, 0, -716.451) m`, with floor `-717.581 m`, 6 m clearance, and 2 m support depth. Yet the interface-1–2 passage reported 289/50,904 capsule samples blocked, with 0/4,040 unsupported and 0 grade failures. A blocked sample at `(-16.087, 2.915, -722.743) m` had native density `-3.629` and composed density `-0.162`; it is about 29 m from the departure pad. The sample is inside the passage tube, so the layer's field remains solid across the approach even though its pad itself is certified. The lower interface, both shell audits, both ripple audits, and the liminal walk audit passed; the final composed passage summary failed and seam audit did not run for this seed. See `Saved/Linux/l145/logs/audit-seed05-clang-w1.log`.
|
||||||
|
|
||||||
|
For the 45 m attempt, the original seed 0–4 Clang W1 probes passed their composed gates, then seed 5 failed. The seed 5 log was overwritten by the final 20 m rerun; its counts above are from that earlier run. The corrected ripple fitter also removed a false 5.99 cm reading caused by compressing masked samples: at 45 m, seed 0 measured 2.87 cm after retaining route distances. These are probe results, not timing measurements.
|
||||||
|
|
||||||
|
The third repeated seed 5 failure reaches the requested stop condition. This does not show that a full candidate-and-approach contract is impossible; it shows the tested endpoint-only pad contract is too weak. The passage planner needs to choose a candidate while checking the full capsule approach and tube against the layer field, as R48 recommends.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
One 1.6 m eye-height capture was rendered at each arrival pad. All four passed `capture_quality.py`; mesh export used `--capture-skip-walkability`, so these images show appearance and are not walkability evidence.
|
||||||
|
|
||||||
|
| Variant | Capture |
|
||||||
|
|---|---|
|
||||||
|
| Flat field + monolith |  |
|
||||||
|
| Repeating hills |  |
|
||||||
|
| Road over cloud floor |  |
|
||||||
|
| Pillar grid |  |
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Clang 19 headless and offscreen-capture binaries reported the final source stamp. Four arrival captures passed quality checks.
|
||||||
|
- Fast tier passed on the final source: `Saved/Linux/l145/fast/fast-summary.json` (280.8 s tier duration). The initial default-output attempt refused a stale L42 binary before tests started; rerunning into the isolated L145 output directory with the current L145 binary passed.
|
||||||
|
- Owner canonical hash checks passed: Surface World style 1 and Floating Islands style 2 each matched all 12 pinned L130-v227 rows at W1 and W12, and each recipe's W1/W12 rows matched. These exercise the no-pad owner path.
|
||||||
|
- `git diff --check` passed.
|
||||||
|
- The landing-pad composed matrix stopped at seed 5 after the repeated failure. It did not complete 8 seeds per variant (32 runs). Clang/GCC W1/W8/W12 identity for the liminal fixture was not run. Final-source seeds 0–4 were not rerun after the margin returned to 20 m. No performance timing was collected.
|
||||||
|
|
||||||
|
Changed standalone files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/WorldAssemblerInternal.h`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`.
|
||||||
|
|
||||||
|
Remaining risk: the current pad record certifies a local landing and direction, not an approach corridor. A future joint planner must reserve/check the full capsule and tube against the layer before finalizing pad choice and passage geometry.
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L145's composed liminal audits and the worker/compiler identity matrix."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l145"
|
||||||
|
FIXTURE = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||||
|
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||||
|
AUDIT_FLAGS = ("--audit-resolved-layout-passages", "--audit-liminal-road-walk",
|
||||||
|
"--audit-passage-tile-seams")
|
||||||
|
VARIANTS = ("flatField-monolith", "periodicHills",
|
||||||
|
"road-over-cloud-floor", "pillar-grid")
|
||||||
|
PASSAGE_RE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||||
|
SHELL_RE = re.compile(r"^\[PassageShellAudit\].*status=PASS$", re.M)
|
||||||
|
|
||||||
|
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||||
|
from runner import current_stamp, first_stamp
|
||||||
|
|
||||||
|
|
||||||
|
def parse_seeds(value: str) -> list[int]:
|
||||||
|
result: set[int] = set()
|
||||||
|
for token in value.split(","):
|
||||||
|
token = token.strip()
|
||||||
|
if not token:
|
||||||
|
continue
|
||||||
|
if "-" in token:
|
||||||
|
first, last = (int(part) for part in token.split("-", 1))
|
||||||
|
if first > last:
|
||||||
|
raise argparse.ArgumentTypeError(f"reversed seed range: {token}")
|
||||||
|
result.update(range(first, last + 1))
|
||||||
|
else:
|
||||||
|
result.add(int(token))
|
||||||
|
if not result or min(result) < 0:
|
||||||
|
raise argparse.ArgumentTypeError("seeds must be nonnegative and nonempty")
|
||||||
|
return sorted(result)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_audit(output: str, seed: int, compiler: str, workers: int) -> None:
|
||||||
|
checks = {
|
||||||
|
"landing-pad negative control":
|
||||||
|
"[LandingPadNegativeControl] uncertified_rejected=true status=PASS" in output,
|
||||||
|
"passage walk audits": len(PASSAGE_RE.findall(output)) == 2,
|
||||||
|
"passage shell audits": len(SHELL_RE.findall(output)) == 2,
|
||||||
|
"passage ripple audits": len(re.findall(
|
||||||
|
r"^\[PassageRippleAudit\].*status=PASS$", output, re.M)) == 2,
|
||||||
|
"masked ripple sampling controls": len(re.findall(
|
||||||
|
r"^\[PassageRippleSamplingControl\].*status=PASS$", output, re.M)) == 2,
|
||||||
|
"passage shell summary": bool(re.search(
|
||||||
|
r"^\[PassageShellAuditSummary\].*status=PASS$", output, re.M)),
|
||||||
|
"liminal walk audit": bool(re.search(
|
||||||
|
r"^\[LiminalLayerWalkAudit\].*status=PASS$", output, re.M)),
|
||||||
|
"liminal negative control":
|
||||||
|
"[LiminalLayerNegativeControl] altered_departure_rejected=true status=PASS" in output,
|
||||||
|
"passage summary": bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)),
|
||||||
|
"seam audit": bool(re.search(
|
||||||
|
r"^\[PassageTileSeamAudit\].*status=PASS$", output, re.M)),
|
||||||
|
}
|
||||||
|
failed = [name for name, passed in checks.items() if not passed]
|
||||||
|
if failed:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"seed={seed} compiler={compiler} workers={workers} failed checks: "
|
||||||
|
+ ", ".join(failed))
|
||||||
|
|
||||||
|
|
||||||
|
def run_audit(seed: int, compiler: str, workers: int, *, audit: bool = True) -> dict:
|
||||||
|
binary = ROOT / f"Standalone/bin/linux/VoxelForgeCoreHeadless-l145-{compiler}"
|
||||||
|
if not binary.is_file():
|
||||||
|
raise RuntimeError(f"missing binary: {binary}")
|
||||||
|
expected_stamp = current_stamp()
|
||||||
|
probe = subprocess.run([str(binary), "--print-source-stamp"], cwd=ROOT,
|
||||||
|
capture_output=True, text=True, timeout=15, check=False)
|
||||||
|
if probe.returncode != 0 or first_stamp(probe.stdout) != expected_stamp:
|
||||||
|
raise RuntimeError(f"stale executable: {binary}")
|
||||||
|
|
||||||
|
tag = f"seed{seed:02d}-{compiler}-w{workers}"
|
||||||
|
log = OUT / "logs" / f"audit-{tag}.log"
|
||||||
|
manifest = OUT / "manifests" / f"audit-{tag}.json"
|
||||||
|
layout = OUT / "layouts" / f"audit-{tag}.json"
|
||||||
|
manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
command = [str(binary), str(FIXTURE), f"--workers={workers}",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", "--camera=0,0,-540,0,0", f"--seed={seed}",
|
||||||
|
f"--world-assembler-manifest-output={manifest}",
|
||||||
|
f"--resolved-layout-output={layout}"]
|
||||||
|
if audit:
|
||||||
|
command.extend(AUDIT_FLAGS)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||||
|
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||||
|
env["LP_NUM_THREADS"] = "12"
|
||||||
|
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run([*LIMIT, "timeout", "1740s", *command],
|
||||||
|
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1745, check=False)
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"{tag} exit={result.returncode}; see {log}")
|
||||||
|
if first_stamp(output) != expected_stamp or current_stamp() != expected_stamp:
|
||||||
|
raise RuntimeError(f"{tag} source stamp changed during run; see {log}")
|
||||||
|
if audit:
|
||||||
|
validate_audit(output, seed, compiler, workers)
|
||||||
|
if not manifest.is_file() or not layout.is_file():
|
||||||
|
raise RuntimeError(f"{tag} omitted canonical manifest or layout")
|
||||||
|
manifest_value = json.loads(manifest.read_text(encoding="utf-8"))
|
||||||
|
layout_value = json.loads(layout.read_text(encoding="utf-8"))
|
||||||
|
identity = {
|
||||||
|
"seed": seed,
|
||||||
|
"variant": VARIANTS[seed % len(VARIANTS)],
|
||||||
|
"compiler": compiler,
|
||||||
|
"workers": workers,
|
||||||
|
"manifest_hash": manifest_value["content_hash"],
|
||||||
|
"resolved_layout_identity": layout_value["stable_identity"],
|
||||||
|
"log": str(log.relative_to(ROOT)),
|
||||||
|
"elapsed_s": round(time.monotonic() - started, 3),
|
||||||
|
"audited": audit,
|
||||||
|
}
|
||||||
|
print(f"[L145AuditRun] seed={seed} variant={identity['variant']} compiler={compiler} "
|
||||||
|
f"workers={workers} manifest={identity['manifest_hash']} "
|
||||||
|
f"layout={identity['resolved_layout_identity']} "
|
||||||
|
f"elapsed_s={identity['elapsed_s']:.3f} status=PASS", flush=True)
|
||||||
|
return identity
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--seeds", default="0-31", type=parse_seeds,
|
||||||
|
help="audit seeds, e.g. 0-31 or 0,1,2,3")
|
||||||
|
parser.add_argument("--determinism", action="store_true",
|
||||||
|
help="also compare seed 0 on Clang/GCC at W1/W8/W12")
|
||||||
|
parser.add_argument("--only-determinism", action="store_true",
|
||||||
|
help="run only the Clang/GCC seed-0 W1/W8/W12 identity matrix")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not FIXTURE.is_file():
|
||||||
|
raise SystemExit(f"missing fixture: {FIXTURE}; run BuildL145Fixtures.py first")
|
||||||
|
|
||||||
|
records = []
|
||||||
|
if not args.only_determinism:
|
||||||
|
for seed in args.seeds:
|
||||||
|
records.append(run_audit(seed, "clang", 1))
|
||||||
|
if args.determinism or args.only_determinism:
|
||||||
|
matrix = [run_audit(0, compiler, workers)
|
||||||
|
for compiler in ("clang", "gcc") for workers in (1, 8, 12)]
|
||||||
|
identities = {(row["manifest_hash"], row["resolved_layout_identity"])
|
||||||
|
for row in matrix}
|
||||||
|
if len(identities) != 1:
|
||||||
|
raise RuntimeError(f"Clang/GCC W1/W8/W12 identity mismatch: {matrix}")
|
||||||
|
records.extend(matrix)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"audit_seeds": args.seeds if not args.only_determinism else [],
|
||||||
|
"variant_counts": {
|
||||||
|
name: sum(1 for seed in args.seeds if seed % len(VARIANTS) == index)
|
||||||
|
for index, name in enumerate(VARIANTS)
|
||||||
|
} if not args.only_determinism else {},
|
||||||
|
"records": records,
|
||||||
|
"determinism_pass": bool(args.determinism or args.only_determinism),
|
||||||
|
"status": "PASS",
|
||||||
|
}
|
||||||
|
summary_path = OUT / "audit-matrix-summary.json"
|
||||||
|
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||||
|
print(f"[L145AuditMatrix] records={len(records)} summary={summary_path} status=PASS",
|
||||||
|
flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as error:
|
||||||
|
print(f"[L145AuditMatrix] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||||
|
raise SystemExit(1)
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1842,6 +1842,9 @@ int main(int argc,char** argv)
|
|||||||
}
|
}
|
||||||
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
||||||
{
|
{
|
||||||
|
const VFJson::Value& coreLiminalConfig=ownerJsonRoot.Get("core_liminal_field");
|
||||||
|
const bool hasCoreLiminalField=!std::holds_alternative<std::nullptr_t>(
|
||||||
|
coreLiminalConfig.Data);
|
||||||
const std::string variant=options.LatticeRecipe
|
const std::string variant=options.LatticeRecipe
|
||||||
?"lattice/"+std::to_string(options.LatticeStrateIndex)
|
?"lattice/"+std::to_string(options.LatticeStrateIndex)
|
||||||
:options.CavesRecipe?"caves"
|
:options.CavesRecipe?"caves"
|
||||||
@@ -1852,16 +1855,14 @@ int main(int argc,char** argv)
|
|||||||
?StandaloneWorldAssemblyPrototype::ImportExportedResolvedLayout(
|
?StandaloneWorldAssemblyPrototype::ImportExportedResolvedLayout(
|
||||||
ownerJsonRoot,resolvedLayout,error)
|
ownerJsonRoot,resolvedLayout,error)
|
||||||
:StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
:StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||||
*ownerWorld,worldAssembly,variant,resolvedLayout,error);
|
*ownerWorld,worldAssembly,variant,resolvedLayout,error,
|
||||||
|
!hasCoreLiminalField);
|
||||||
if(!layoutResolved)
|
if(!layoutResolved)
|
||||||
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
||||||
if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe
|
if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe
|
||||||
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
||||||
*ownerWorld,worldAssembly,resolvedLayout,error))
|
*ownerWorld,worldAssembly,resolvedLayout,error))
|
||||||
{std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;}
|
||||||
const VFJson::Value& coreLiminalConfig=ownerJsonRoot.Get("core_liminal_field");
|
|
||||||
const bool hasCoreLiminalField=!std::holds_alternative<std::nullptr_t>(
|
|
||||||
coreLiminalConfig.Data);
|
|
||||||
if(hasCoreLiminalField)
|
if(hasCoreLiminalField)
|
||||||
{
|
{
|
||||||
const VFJson::Value& versionValue=coreLiminalConfig.Get("version");
|
const VFJson::Value& versionValue=coreLiminalConfig.Get("version");
|
||||||
@@ -1871,7 +1872,7 @@ int main(int argc,char** argv)
|
|||||||
const auto* policy=std::get_if<std::string>(&coreLiminalConfig.Get("variant_policy").Data);
|
const auto* policy=std::get_if<std::string>(&coreLiminalConfig.Get("variant_policy").Data);
|
||||||
if(!coreLiminalConfig.IsObject()
|
if(!coreLiminalConfig.IsObject()
|
||||||
||!versionValue.IsNumber()
|
||!versionValue.IsNumber()
|
||||||
||versionValue.Number()!=VoxelForgeCore::CoreLiminalFieldRecipeVersionV1
|
||versionValue.Number()!=VoxelForgeCore::CoreLiminalFieldRecipeVersionV2
|
||||||
||!strateValue.IsNumber()
|
||!strateValue.IsNumber()
|
||||||
||std::trunc(strateValue.Number())!=strateValue.Number()
|
||std::trunc(strateValue.Number())!=strateValue.Number()
|
||||||
||strateValue.Number()<0.0
|
||strateValue.Number()<0.0
|
||||||
@@ -1896,7 +1897,34 @@ int main(int argc,char** argv)
|
|||||||
liminalPlan.strateIndex=static_cast<std::int32_t>(strateIndex);
|
liminalPlan.strateIndex=static_cast<std::int32_t>(strateIndex);
|
||||||
liminalPlan.topChunkZ=slot->TopChunkZ;
|
liminalPlan.topChunkZ=slot->TopChunkZ;
|
||||||
liminalPlan.bottomChunkZ=slot->BottomChunkZ;
|
liminalPlan.bottomChunkZ=slot->BottomChunkZ;
|
||||||
if(!VoxelForgeCore::PlanCoreLiminalFieldV1(worldAssembly.recipe,
|
resolvedLayout.passages.clear();
|
||||||
|
if(!VoxelForgeCore::PrepareCoreLiminalLandingPadsV2(worldAssembly.recipe,
|
||||||
|
liminalPlan,resolvedLayout,error))
|
||||||
|
{std::fprintf(stderr,"Core.Liminal.Field landing-pad preparation failed: %s\n",error.c_str());return 2;}
|
||||||
|
const auto passageConfigs=
|
||||||
|
StandaloneWorldAssemblyPrototype::BuildResolvedLayoutPassageConfigs(*ownerWorld);
|
||||||
|
auto uncertifiedControl=resolvedLayout;
|
||||||
|
const auto controlStratum=std::find_if(uncertifiedControl.strates.begin(),
|
||||||
|
uncertifiedControl.strates.end(),[&](const auto& value)
|
||||||
|
{return value.strateIndex==strateIndex;});
|
||||||
|
bool landingPadNegativeControlRejected=false;
|
||||||
|
if(controlStratum!=uncertifiedControl.strates.end()
|
||||||
|
&&!controlStratum->landingPads.empty())
|
||||||
|
{
|
||||||
|
controlStratum->landingPads.front().certifiedWalkableWithSupport=false;
|
||||||
|
std::string controlError;
|
||||||
|
landingPadNegativeControlRejected=
|
||||||
|
!VoxelForgeCore::PlanResolvedLayoutPassagesV1(uncertifiedControl,
|
||||||
|
ownerWorld->Seed,passageConfigs,controlError);
|
||||||
|
}
|
||||||
|
std::printf("[LandingPadNegativeControl] uncertified_rejected=%s status=%s\n",
|
||||||
|
landingPadNegativeControlRejected?"true":"false",
|
||||||
|
landingPadNegativeControlRejected?"PASS":"FAIL");
|
||||||
|
if(!landingPadNegativeControlRejected)return 2;
|
||||||
|
if(!VoxelForgeCore::PlanResolvedLayoutPassagesV1(resolvedLayout,
|
||||||
|
ownerWorld->Seed,passageConfigs,error))
|
||||||
|
{std::fprintf(stderr,"Core.Liminal.Field pad-bound passage planning failed: %s\n",error.c_str());return 2;}
|
||||||
|
if(!VoxelForgeCore::PlanCoreLiminalFieldV2(worldAssembly.recipe,
|
||||||
liminalPlan,resolvedLayout,error))
|
liminalPlan,resolvedLayout,error))
|
||||||
{std::fprintf(stderr,"Core.Liminal.Field planning failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Core.Liminal.Field planning failed: %s\n",error.c_str());return 2;}
|
||||||
worldAssembly.plan.strates.push_back(std::move(liminalPlan));
|
worldAssembly.plan.strates.push_back(std::move(liminalPlan));
|
||||||
@@ -1910,17 +1938,19 @@ int main(int argc,char** argv)
|
|||||||
{std::fprintf(stderr,"Core.Liminal.Field manifest failed canonical JSON round-trip: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Core.Liminal.Field manifest failed canonical JSON round-trip: %s\n",error.c_str());return 2;}
|
||||||
const auto plan=std::find_if(worldAssembly.plan.strates.begin(),
|
const auto plan=std::find_if(worldAssembly.plan.strates.begin(),
|
||||||
worldAssembly.plan.strates.end(),[&](const auto& value)
|
worldAssembly.plan.strates.end(),[&](const auto& value)
|
||||||
{return value.strateIndex==strateIndex&&value.liminalField.version!=0;});
|
{return value.strateIndex==strateIndex
|
||||||
|
&&value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV2;});
|
||||||
if(plan==worldAssembly.plan.strates.end())
|
if(plan==worldAssembly.plan.strates.end())
|
||||||
{std::fprintf(stderr,"Core.Liminal.Field finalized plan is missing\n");return 2;}
|
{std::fprintf(stderr,"Core.Liminal.Field finalized plan is missing\n");return 2;}
|
||||||
auto& resolvedLiminal=*std::find_if(resolvedLayout.strates.begin(),
|
auto& resolvedLiminal=*std::find_if(resolvedLayout.strates.begin(),
|
||||||
resolvedLayout.strates.end(),[&](const auto& value)
|
resolvedLayout.strates.end(),[&](const auto& value)
|
||||||
{return value.strateIndex==strateIndex;});
|
{return value.strateIndex==strateIndex;});
|
||||||
resolvedLiminal.recipeId=std::string(VoxelForgeCore::CoreLiminalFieldRecipeIdV1);
|
resolvedLiminal.recipeId=std::string(VoxelForgeCore::CoreLiminalFieldRecipeIdV1);
|
||||||
resolvedLiminal.recipeVersion=VoxelForgeCore::CoreLiminalFieldRecipeVersionV1;
|
resolvedLiminal.recipeVersion=VoxelForgeCore::CoreLiminalFieldRecipeVersionV2;
|
||||||
resolvedLiminal.anchors.erase(std::remove_if(resolvedLiminal.anchors.begin(),
|
resolvedLiminal.anchors.erase(std::remove_if(resolvedLiminal.anchors.begin(),
|
||||||
resolvedLiminal.anchors.end(),[](const auto& anchor)
|
resolvedLiminal.anchors.end(),[](const auto& anchor)
|
||||||
{return anchor.kind=="heightfield_profile";}),resolvedLiminal.anchors.end());
|
{return anchor.kind=="heightfield_profile"||anchor.kind=="route_point";}),
|
||||||
|
resolvedLiminal.anchors.end());
|
||||||
VoxelForgeCore::ResolvedLayoutAnchorV2 profileAnchor;
|
VoxelForgeCore::ResolvedLayoutAnchorV2 profileAnchor;
|
||||||
profileAnchor.kind="heightfield_profile";
|
profileAnchor.kind="heightfield_profile";
|
||||||
profileAnchor.stableId="heightfield-profile/liminal/"
|
profileAnchor.stableId="heightfield-profile/liminal/"
|
||||||
|
|||||||
@@ -3074,7 +3074,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
const auto plan=Assembly?std::find_if(Assembly->plan.strates.begin(),
|
const auto plan=Assembly?std::find_if(Assembly->plan.strates.begin(),
|
||||||
Assembly->plan.strates.end(),[&](const auto& value)
|
Assembly->plan.strates.end(),[&](const auto& value)
|
||||||
{return value.strateIndex==coreLiminalStratum->strateIndex
|
{return value.strateIndex==coreLiminalStratum->strateIndex
|
||||||
&&value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV1;})
|
&&(value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV1
|
||||||
|
||value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV2);})
|
||||||
:std::vector<VoxelForgeCore::WorldAssemblyStratumPlanV1>::const_iterator{};
|
:std::vector<VoxelForgeCore::WorldAssemblyStratumPlanV1>::const_iterator{};
|
||||||
if(!Assembly||plan==Assembly->plan.strates.end())fixedMouthBinding=false;
|
if(!Assembly||plan==Assembly->plan.strates.end())fixedMouthBinding=false;
|
||||||
else
|
else
|
||||||
@@ -4124,8 +4125,9 @@ namespace StandaloneStreamBenchmark
|
|||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
};
|
};
|
||||||
const auto appendDetrendedProfile=[&](const std::vector<double>& values,
|
const auto appendDetrendedProfile=[&](
|
||||||
double startDistance,std::vector<std::array<double,2>>& output)
|
const std::vector<std::array<double,2>>& values,
|
||||||
|
std::vector<std::array<double,2>>& output)
|
||||||
{
|
{
|
||||||
if(values.size()<64)return;
|
if(values.size()<64)return;
|
||||||
// Remove a smooth mouth taper across each 32 m window while
|
// Remove a smooth mouth taper across each 32 m window while
|
||||||
@@ -4134,11 +4136,12 @@ namespace StandaloneStreamBenchmark
|
|||||||
// 0.5–8 m periods that this audit is meant to detect.
|
// 0.5–8 m periods that this audit is meant to detect.
|
||||||
constexpr std::size_t Degree=3;
|
constexpr std::size_t Degree=3;
|
||||||
std::array<std::array<double,Degree+2>,Degree+1> normal{};
|
std::array<std::array<double,Degree+2>,Degree+1> normal{};
|
||||||
const double meanIndex=0.5*static_cast<double>(values.size()-1);
|
const double meanDistance=0.5*(values.front()[0]+values.back()[0]);
|
||||||
const double halfSpan=std::max(1.0,meanIndex);
|
const double halfSpan=std::max(1.0,
|
||||||
for(std::size_t i=0;i<values.size();++i)
|
0.5*(values.back()[0]-values.front()[0]));
|
||||||
|
for(const auto& value:values)
|
||||||
{
|
{
|
||||||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
const double x=(value[0]-meanDistance)/halfSpan;
|
||||||
std::array<double,2*Degree+1> powers{};
|
std::array<double,2*Degree+1> powers{};
|
||||||
powers[0]=1.0;
|
powers[0]=1.0;
|
||||||
for(std::size_t power=1;power<powers.size();++power)
|
for(std::size_t power=1;power<powers.size();++power)
|
||||||
@@ -4147,7 +4150,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
{
|
{
|
||||||
for(std::size_t column=0;column<=Degree;++column)
|
for(std::size_t column=0;column<=Degree;++column)
|
||||||
normal[row][column]+=powers[row+column];
|
normal[row][column]+=powers[row+column];
|
||||||
normal[row][Degree+1]+=values[i]*powers[row];
|
normal[row][Degree+1]+=value[1]*powers[row];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(std::size_t column=0;column<=Degree;++column)
|
for(std::size_t column=0;column<=Degree;++column)
|
||||||
@@ -4169,14 +4172,13 @@ namespace StandaloneStreamBenchmark
|
|||||||
normal[row][value]-=scale*normal[column][value];
|
normal[row][value]-=scale*normal[column][value];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(std::size_t i=0;i<values.size();++i)
|
for(const auto& value:values)
|
||||||
{
|
{
|
||||||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
const double x=(value[0]-meanDistance)/halfSpan;
|
||||||
double trend=normal[Degree][Degree+1];
|
double trend=normal[Degree][Degree+1];
|
||||||
for(std::size_t coefficient=Degree;coefficient-->0;)
|
for(std::size_t coefficient=Degree;coefficient-->0;)
|
||||||
trend=trend*x+normal[coefficient][Degree+1];
|
trend=trend*x+normal[coefficient][Degree+1];
|
||||||
output.push_back({startDistance+static_cast<double>(i)*SampleSpacingMetres,
|
output.push_back({value[0],value[1]-trend});
|
||||||
values[i]-trend});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const auto appendDetrendedPairs=[](
|
const auto appendDetrendedPairs=[](
|
||||||
@@ -4199,7 +4201,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
output.push_back({value[0],value[1]-meanY
|
output.push_back({value[0],value[1]-meanY
|
||||||
-slope*(value[0]-meanX)});
|
-slope*(value[0]-meanX)});
|
||||||
};
|
};
|
||||||
std::vector<double> beadControl;
|
std::vector<std::array<double,2>> beadControl;
|
||||||
beadControl.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
beadControl.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||||
constexpr double BeadSpacingMetres=1.5;
|
constexpr double BeadSpacingMetres=1.5;
|
||||||
constexpr double BeadSphereRadiusMetres=2.0;
|
constexpr double BeadSphereRadiusMetres=2.0;
|
||||||
@@ -4207,14 +4209,49 @@ namespace StandaloneStreamBenchmark
|
|||||||
{
|
{
|
||||||
const double x=static_cast<double>(i)*SampleSpacingMetres;
|
const double x=static_cast<double>(i)*SampleSpacingMetres;
|
||||||
const double nearest=std::abs(std::remainder(x,BeadSpacingMetres));
|
const double nearest=std::abs(std::remainder(x,BeadSpacingMetres));
|
||||||
beadControl.push_back(std::sqrt(std::max(0.0,
|
beadControl.push_back({x,std::sqrt(std::max(0.0,
|
||||||
BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest)));
|
BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest))});
|
||||||
}
|
}
|
||||||
std::vector<std::array<double,2>> beadSamples;
|
std::vector<std::array<double,2>> beadSamples;
|
||||||
appendDetrendedProfile(beadControl,0.0,beadSamples);
|
appendDetrendedProfile(beadControl,beadSamples);
|
||||||
double ignoredPeriod=0.0;
|
double ignoredPeriod=0.0;
|
||||||
audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod);
|
audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod);
|
||||||
audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres;
|
audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres;
|
||||||
|
std::vector<std::array<double,2>> maskedSineControl,maskedSmoothControl;
|
||||||
|
constexpr double SamplingControlGapStartMetres=10.0;
|
||||||
|
constexpr double SamplingControlGapEndMetres=15.0;
|
||||||
|
for(std::size_t sample=0;
|
||||||
|
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||||||
|
++sample)
|
||||||
|
{
|
||||||
|
const double distance=static_cast<double>(sample)*SampleSpacingMetres;
|
||||||
|
if(distance>=SamplingControlGapStartMetres
|
||||||
|
&&distance<SamplingControlGapEndMetres)continue;
|
||||||
|
const double phase=6.2831853071795864769*distance/4.0;
|
||||||
|
maskedSineControl.push_back({distance,0.060*std::sin(phase)});
|
||||||
|
const double x=(distance-16.0)/16.0;
|
||||||
|
maskedSmoothControl.push_back({distance,0.4*x*x*x-0.15*x*x+0.08*x});
|
||||||
|
}
|
||||||
|
std::vector<std::array<double,2>> maskedSineResiduals,maskedSmoothResiduals;
|
||||||
|
appendDetrendedProfile(maskedSineControl,maskedSineResiduals);
|
||||||
|
appendDetrendedProfile(maskedSmoothControl,maskedSmoothResiduals);
|
||||||
|
double maskedSinePeriod=0.0,maskedSmoothPeriod=0.0;
|
||||||
|
const double maskedSineAmplitude=sinusoidAmplitude(
|
||||||
|
maskedSineResiduals,maskedSinePeriod);
|
||||||
|
const double maskedSmoothAmplitude=sinusoidAmplitude(
|
||||||
|
maskedSmoothResiduals,maskedSmoothPeriod);
|
||||||
|
const bool maskedSamplingControlPassed=
|
||||||
|
maskedSineAmplitude>=RippleAmplitudeLimitMetres
|
||||||
|
&&maskedSmoothAmplitude<RippleAmplitudeLimitMetres;
|
||||||
|
std::printf("[PassageRippleSamplingControl] missing_samples=%zu "
|
||||||
|
"sine_amplitude_m=%.4f sine_detected=%s smooth_amplitude_m=%.4f "
|
||||||
|
"smooth_rejected=%s status=%s\n",
|
||||||
|
static_cast<std::size_t>((SamplingControlGapEndMetres
|
||||||
|
-SamplingControlGapStartMetres)/SampleSpacingMetres),
|
||||||
|
maskedSineAmplitude,maskedSineAmplitude>=RippleAmplitudeLimitMetres
|
||||||
|
?"true":"false",maskedSmoothAmplitude,
|
||||||
|
maskedSmoothAmplitude<RippleAmplitudeLimitMetres?"true":"false",
|
||||||
|
maskedSamplingControlPassed?"PASS":"FAIL");
|
||||||
audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres;
|
audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres;
|
||||||
audit.floorControlDetected=audit.floorControlAmplitudeMetres
|
audit.floorControlDetected=audit.floorControlAmplitudeMetres
|
||||||
>=FloorRippleAmplitudeLimitMetres;
|
>=FloorRippleAmplitudeLimitMetres;
|
||||||
@@ -4268,7 +4305,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
for(const double fraction:windowStarts)
|
for(const double fraction:windowStarts)
|
||||||
{
|
{
|
||||||
const double start=(total-WindowLengthMetres)*fraction;
|
const double start=(total-WindowLengthMetres)*fraction;
|
||||||
std::vector<double> radii;
|
std::vector<std::array<double,2>> radii;
|
||||||
radii.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
radii.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||||
std::size_t wallProbeNonfinite=0,wallProbeOutOfBand=0;
|
std::size_t wallProbeNonfinite=0,wallProbeOutOfBand=0;
|
||||||
double wallProbeMinimum=std::numeric_limits<double>::infinity();
|
double wallProbeMinimum=std::numeric_limits<double>::infinity();
|
||||||
@@ -4368,7 +4405,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
// The probes sit 0.18 m inside the authored radius. Add that
|
// The probes sit 0.18 m inside the authored radius. Add that
|
||||||
// radius back to the signed wall distance so radius-carried ribs
|
// radius back to the signed wall distance so radius-carried ribs
|
||||||
// remain visible; the cubic detrend removes the smooth mouth flare.
|
// remain visible; the cubic detrend removes the smooth mouth flare.
|
||||||
radii.push_back(sampleRadius+0.5*(left+right));
|
radii.push_back({distance,sampleRadius+0.5*(left+right)});
|
||||||
++audit.samples;
|
++audit.samples;
|
||||||
}
|
}
|
||||||
if(radii.size()<static_cast<std::size_t>(WindowLengthMetres/
|
if(radii.size()<static_cast<std::size_t>(WindowLengthMetres/
|
||||||
@@ -4389,7 +4426,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
audit.wallWindowExpectedSamples+=static_cast<std::size_t>(
|
audit.wallWindowExpectedSamples+=static_cast<std::size_t>(
|
||||||
WindowLengthMetres/SampleSpacingMetres)+1;
|
WindowLengthMetres/SampleSpacingMetres)+1;
|
||||||
std::vector<std::array<double,2>> windowSamples;
|
std::vector<std::array<double,2>> windowSamples;
|
||||||
appendDetrendedProfile(radii,start,windowSamples);
|
appendDetrendedProfile(radii,windowSamples);
|
||||||
double localPeriod=0.0;
|
double localPeriod=0.0;
|
||||||
const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod);
|
const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod);
|
||||||
std::printf("[PassageWallRippleWindow] id=%s start_m=%.3f "
|
std::printf("[PassageWallRippleWindow] id=%s start_m=%.3f "
|
||||||
@@ -4459,6 +4496,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
audit.passed=audit.controlDetected&&audit.wallQualifiedWindows>0
|
audit.passed=audit.controlDetected&&audit.wallQualifiedWindows>0
|
||||||
&&wallWindowCoverage>=0.75
|
&&wallWindowCoverage>=0.75
|
||||||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres
|
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres
|
||||||
|
&&maskedSamplingControlPassed
|
||||||
&&audit.floorControlDetected&&audit.floorStepControlDetected
|
&&audit.floorControlDetected&&audit.floorStepControlDetected
|
||||||
&&(audit.floorOverlapOnly||(floorCoverage>=0.75
|
&&(audit.floorOverlapOnly||(floorCoverage>=0.75
|
||||||
&&audit.floorAmplitudeMetres<FloorRippleAmplitudeLimitMetres));
|
&&audit.floorAmplitudeMetres<FloorRippleAmplitudeLimitMetres));
|
||||||
|
|||||||
@@ -834,7 +834,8 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
if(owner.CoreVoxelSizeMicrons==0)
|
if(owner.CoreVoxelSizeMicrons==0)
|
||||||
{error="Core.Liminal.Field stamp planning needs the resolved Core voxel size";return false;}
|
{error="Core.Liminal.Field stamp planning needs the resolved Core voxel size";return false;}
|
||||||
const auto plan=std::find_if(assembly.plan.strates.begin(),assembly.plan.strates.end(),
|
const auto plan=std::find_if(assembly.plan.strates.begin(),assembly.plan.strates.end(),
|
||||||
[](const auto& value){return value.liminalField.version==CoreLiminalFieldRecipeVersionV1;});
|
[](const auto& value){return value.liminalField.version==CoreLiminalFieldRecipeVersionV1
|
||||||
|
||value.liminalField.version==CoreLiminalFieldRecipeVersionV2;});
|
||||||
if(plan==assembly.plan.strates.end()||plan->route.size()<2)
|
if(plan==assembly.plan.strates.end()||plan->route.size()<2)
|
||||||
{error="Core.Liminal.Field has no planned route for stamp resolution";return false;}
|
{error="Core.Liminal.Field has no planned route for stamp resolution";return false;}
|
||||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
@@ -860,7 +861,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
const std::int64_t profileReliefUm=plan->liminalField.variant
|
const std::int64_t profileReliefUm=plan->liminalField.variant
|
||||||
==WorldAssemblyLiminalVariantV1::repeatingHills
|
==WorldAssemblyLiminalVariantV1::repeatingHills
|
||||||
?2*definition.hillAmplitudeMicrometres:0;
|
?2*definition.hillAmplitudeMicrometres:0;
|
||||||
const std::int64_t supportBaseUm=floorUm-profileReliefUm+100000;
|
const std::int64_t supportBaseUm=strate->content.bottomMicrometres+100000;
|
||||||
|
|
||||||
std::vector<std::array<std::int64_t,3>> controls;
|
std::vector<std::array<std::int64_t,3>> controls;
|
||||||
controls.reserve(plan->route.size());
|
controls.reserve(plan->route.size());
|
||||||
@@ -1712,9 +1713,46 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1>
|
||||||
|
BuildResolvedLayoutPassageConfigs(const FStandaloneOwnerWorld& owner)
|
||||||
|
{
|
||||||
|
std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1> passageConfigs;
|
||||||
|
if(!owner.Manager)return passageConfigs;
|
||||||
|
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||||
|
{
|
||||||
|
if(!slot.Definition)continue;
|
||||||
|
const auto& authored=slot.Definition->PassageConfig;
|
||||||
|
const auto ownerVoxelsToCoreQ=[&](double value)
|
||||||
|
{return static_cast<std::int64_t>(std::llround(value*owner.OwnerVoxelSizeMicrons
|
||||||
|
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));};
|
||||||
|
VoxelForgeCore::ResolvedLayoutPassageConfigV1 config;
|
||||||
|
config.upperStrateIndex=slot.StrateIndex;
|
||||||
|
config.connections=authored.Connections;
|
||||||
|
config.style=static_cast<std::uint32_t>(authored.Style);
|
||||||
|
config.mouthRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MouthRadius),
|
||||||
|
ownerVoxelsToCoreQ(36.0));
|
||||||
|
config.midRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MidRadius),
|
||||||
|
ownerVoxelsToCoreQ(24.0));
|
||||||
|
config.reachMinimumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMin);
|
||||||
|
config.reachMaximumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMax);
|
||||||
|
config.distanceMinimumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMin);
|
||||||
|
config.distanceMaximumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMax);
|
||||||
|
config.wanderSubvoxels=ownerVoxelsToCoreQ(authored.Wander);
|
||||||
|
config.segments=static_cast<std::uint32_t>(std::clamp(authored.Segments,1,48));
|
||||||
|
config.verticalWobbleSubvoxels=ownerVoxelsToCoreQ(authored.VerticalWobble);
|
||||||
|
config.spiralRadiusSubvoxels=ownerVoxelsToCoreQ(authored.SpiralRadius);
|
||||||
|
config.spiralTurnsQ16=static_cast<std::int64_t>(std::llround(authored.SpiralTurns*65536.0));
|
||||||
|
config.cascadeSteps=static_cast<std::uint32_t>(std::clamp(authored.CascadeSteps,1,16));
|
||||||
|
config.cascadeLedgeSubvoxels=ownerVoxelsToCoreQ(authored.CascadeLedge);
|
||||||
|
passageConfigs.push_back(config);
|
||||||
|
}
|
||||||
|
return passageConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool BuildResolvedLayoutSnapshot(const FStandaloneOwnerWorld& owner,
|
inline bool BuildResolvedLayoutSnapshot(const FStandaloneOwnerWorld& owner,
|
||||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,std::string_view recipeVariant,
|
const VoxelForgeCore::WorldAssemblyManifest& assembly,std::string_view recipeVariant,
|
||||||
VoxelForgeCore::ResolvedLayoutSnapshotV3& out,std::string& error)
|
VoxelForgeCore::ResolvedLayoutSnapshotV3& out,std::string& error,
|
||||||
|
bool planPassages=true)
|
||||||
{
|
{
|
||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;}
|
if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;}
|
||||||
@@ -1938,35 +1976,11 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
}
|
}
|
||||||
out.strates.push_back(std::move(s));
|
out.strates.push_back(std::move(s));
|
||||||
}
|
}
|
||||||
std::vector<ResolvedLayoutPassageConfigV1> passageConfigs;
|
if(planPassages)
|
||||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
|
||||||
{
|
{
|
||||||
if(!slot.Definition)continue;
|
const auto passageConfigs=BuildResolvedLayoutPassageConfigs(owner);
|
||||||
const auto& authored=slot.Definition->PassageConfig;
|
|
||||||
const auto ownerVoxelsToCoreQ=[&](double value)
|
|
||||||
{return static_cast<std::int64_t>(std::llround(value*owner.OwnerVoxelSizeMicrons
|
|
||||||
*WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));};
|
|
||||||
ResolvedLayoutPassageConfigV1 config;config.upperStrateIndex=slot.StrateIndex;
|
|
||||||
config.connections=authored.Connections;
|
|
||||||
config.style=static_cast<std::uint32_t>(authored.Style);
|
|
||||||
config.mouthRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MouthRadius),
|
|
||||||
ownerVoxelsToCoreQ(36.0));
|
|
||||||
config.midRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MidRadius),
|
|
||||||
ownerVoxelsToCoreQ(24.0));
|
|
||||||
config.reachMinimumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMin);
|
|
||||||
config.reachMaximumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMax);
|
|
||||||
config.distanceMinimumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMin);
|
|
||||||
config.distanceMaximumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMax);
|
|
||||||
config.wanderSubvoxels=ownerVoxelsToCoreQ(authored.Wander);
|
|
||||||
config.segments=static_cast<std::uint32_t>(std::clamp(authored.Segments,1,48));
|
|
||||||
config.verticalWobbleSubvoxels=ownerVoxelsToCoreQ(authored.VerticalWobble);
|
|
||||||
config.spiralRadiusSubvoxels=ownerVoxelsToCoreQ(authored.SpiralRadius);
|
|
||||||
config.spiralTurnsQ16=static_cast<std::int64_t>(std::llround(authored.SpiralTurns*65536.0));
|
|
||||||
config.cascadeSteps=static_cast<std::uint32_t>(std::clamp(authored.CascadeSteps,1,16));
|
|
||||||
config.cascadeLedgeSubvoxels=ownerVoxelsToCoreQ(authored.CascadeLedge);
|
|
||||||
passageConfigs.push_back(config);
|
|
||||||
}
|
|
||||||
if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false;
|
if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false;
|
||||||
|
}
|
||||||
return out.finalize(error);
|
return out.finalize(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user