core: Vertical Shafts on the shared Core cave planner (L139, v237; 15/16 seeds)

Shaft mode reuses PlanResolvedCavernNetworkV1 with a capped full-height cylinder primitive (shapeKind=1), ledges,
access bores and cross links; 11 VerticalShaftParams fields mapped. Basal law: arrival 2-3 to a walkable terminal
landing; drops/climbs stay reporting tiers. Seed 2 fails shell/seams at the 2-3 arrival mouth (3 variants tried,
stopped per AGENTS.md). Whole view faster than assembler/world in 8/8 cells; near-ready slower in 7/8.
Source/: VoxelForgeCoreAdapter.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-11-01 05:34:00 +01:00
1 parent fa4417aad0
commit 318c5cf4f2
31 files changed
+1719 -106

No files matched your search

@@ -510,7 +510,8 @@ namespace VoxelForgeCore
// preserves open cave lanes through passage shell overlaps; 236 binds passage mouths to
// the planned graph route, preserves its walk corridor through shell overlaps, and keeps
// boundary-blended host rock from widening the authored passage wall.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 236;
// v237 adds the shared Core cave-graph shaft primitive and basal terminal route.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237;
enum class FieldChannel : std::uint32_t
{
@@ -906,6 +907,7 @@ namespace VoxelForgeCore
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV2 = 2;
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV3 = 3;
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
@@ -967,6 +969,10 @@ namespace VoxelForgeCore
{
std::string stableId;
std::int32_t strateIndex = -1;
// 0 = floor-backed room; 1 = a true vertical cylindrical shaft. The
// shaft form is part of the shared cave graph so other recipes can reuse
// the same graph, indexing, interval and density machinery.
std::uint8_t shapeKind = 0;
std::array<std::int64_t, 3> centerMicrometres{};
std::array<std::int64_t, 3> halfExtentsMicrometres{};
std::int64_t floorMicrometres = 0;
@@ -982,6 +988,11 @@ namespace VoxelForgeCore
std::int64_t irregularityAmplitudeMicrometres = 0;
std::int64_t irregularityWavelengthMicrometres = 0;
std::uint64_t irregularitySeed = 0;
// Used only by shapeKind=1. floorMicrometres is the shaft top; the
// bottom is center.z - halfExtents.z. Ledges face one cardinal side.
std::int64_t ledgeSpacingMicrometres = 0;
std::int64_t ledgeDepthMicrometres = 0;
std::uint8_t ledgeSide = 0;
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
};
@@ -1044,6 +1055,12 @@ namespace VoxelForgeCore
std::int32_t tunnelHorizontalBiasQ16 = 32768;
std::int32_t originRoomMaximumConnections = 4;
bool tunnelsFlowTowardOrigin = true;
// Vertical Shafts uses this same planner and graph field. It binds its
// incoming passage and resolves a bedrock terminal when no departure
// mouth exists, then adds shaft records beside the walk-only backbone.
bool verticalShaftMode = false;
std::int64_t ledgeSpacingMicrometres = 8000000;
std::int64_t ledgeDepthMicrometres = 1000000;
};
// Immutable material-only deposits. The capsule and its conservative bounds are in
@@ -1150,7 +1167,8 @@ namespace VoxelForgeCore
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
{
return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork")
return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork"
||stratum.generatorKind=="VerticalShafts")
&&stratum.recipeId=="Core.Caves.PrimitiveField"
&&stratum.recipeVersion==1;
}
+263 -31
View File
@@ -626,8 +626,12 @@ namespace
<<",\"floor_profile_distance_um\":"<<room.floorProfileDistanceMicrometres
<<",\"floor_gradient_q16\":["<<room.floorGradientQ16[0]<<','
<<room.floorGradientQ16[1]<<']'
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres
;
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres;
if(room.shapeKind!=0)
out<<",\"shape_kind\":"<<static_cast<unsigned>(room.shapeKind)
<<",\"ledge_spacing_um\":"<<room.ledgeSpacingMicrometres
<<",\"ledge_depth_um\":"<<room.ledgeDepthMicrometres
<<",\"ledge_side\":"<<static_cast<unsigned>(room.ledgeSide);
if(room.unionWidthMicrometres>0)
out<<",\"union_width_um\":"<<room.unionWidthMicrometres;
out<<",\"irregularity_amplitude_um\":"<<room.irregularityAmplitudeMicrometres
@@ -1294,7 +1298,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
"cavern graph vocabulary version is set without graph records");
}
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1
&&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV2)
&&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV2
&&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV3)
add("cavern_graph_vocabulary_version",
"cavern graph records require a supported vocabulary version");
if(cavernGraphVocabularyVersion!=0)
@@ -1327,6 +1332,33 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
||(room.irregularityAmplitudeMicrometres>0
&&room.irregularityWavelengthMicrometres<=0))
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
const auto* roomStratum=findStratum(room.strateIndex);
if(room.shapeKind>1)
add("cavern_room_shape",room.stableId+" has an unknown shape kind");
if(room.shapeKind==1)
{
const bool verticalShaftStratum=roomStratum
&&roomStratum->generatorKind=="VerticalShafts"
&&IsNativeCaveFieldStratum(*roomStratum);
if(cavernGraphVocabularyVersion<ResolvedLayoutCavernGraphVocabularyVersionV3
||!verticalShaftStratum
||room.halfExtentsMicrometres[0]!=room.halfExtentsMicrometres[1]
||room.ledgeSpacingMicrometres<0
||room.ledgeSpacingMicrometres>100000000
||room.ledgeDepthMicrometres<0
||room.ledgeDepthMicrometres>=room.halfExtentsMicrometres[0]
||room.ledgeSide>3
||room.floorMicrometres
!=room.centerMicrometres[2]+room.halfExtentsMicrometres[2]
||!room.floorProfileStableId.empty()
||room.floorGradientQ16[0]!=0||room.floorGradientQ16[1]!=0)
add("cavern_shaft_parameters",room.stableId+
" has invalid native shaft bounds, ledges, or recipe binding");
}
else if(room.ledgeSpacingMicrometres!=0||room.ledgeDepthMicrometres!=0
||room.ledgeSide!=0)
add("cavern_room_shape",room.stableId+
" carries shaft parameters on an ordinary room");
const std::int64_t gradientSquared=static_cast<std::int64_t>(room.floorGradientQ16[0])
*room.floorGradientQ16[0]+static_cast<std::int64_t>(room.floorGradientQ16[1])
*room.floorGradientQ16[1];
@@ -1415,11 +1447,37 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
if(room!=cavernRooms.end())
{
const auto& endpoint=edge.controlPointsMicrometres.back();
bool validRoomEndpoint=false;
if(room->shapeKind==1)
{
const std::int64_t shaftBottom=room->centerMicrometres[2]
-room->halfExtentsMicrometres[2];
const std::int64_t shaftTop=room->centerMicrometres[2]
+room->halfExtentsMicrometres[2];
const bool insideShaft=endpoint[2]>=shaftBottom
&&endpoint[2]<=shaftTop;
bool onLanding=room->ledgeSpacingMicrometres==0;
if(room->ledgeSpacingMicrometres>0)
{
const std::int64_t ledgeIndex=(shaftTop-endpoint[2]
+room->ledgeSpacingMicrometres/2)
/room->ledgeSpacingMicrometres;
const std::int64_t ledgeZ=shaftTop
-ledgeIndex*room->ledgeSpacingMicrometres;
const std::int64_t maximumLedge=(shaftTop-shaftBottom-1000000)
/room->ledgeSpacingMicrometres;
onLanding=ledgeIndex>=1&&ledgeIndex<=maximumLedge
&&std::abs(endpoint[2]-ledgeZ)<=500000;
}
validRoomEndpoint=insideShaft&&onLanding;
}
else validRoomEndpoint=
std::abs(endpoint[2]-room->floorMicrometres)<=500000;
if(endpoint[0]<room->centerMicrometres[0]-room->halfExtentsMicrometres[0]
||endpoint[0]>room->centerMicrometres[0]+room->halfExtentsMicrometres[0]
||endpoint[1]<room->centerMicrometres[1]-room->halfExtentsMicrometres[1]
||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1]
||std::abs(endpoint[2]-room->floorMicrometres)>500000)
||!validRoomEndpoint)
{
add("cavern_edge_portal",edge.stableId+" endpoint_um=("
+std::to_string(endpoint[0])+","+std::to_string(endpoint[1])+","+
@@ -1733,6 +1791,10 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
room.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
room.shapeKind=static_cast<std::uint8_t>(LayoutInt(v,"shape_kind"));
room.ledgeSpacingMicrometres=LayoutInt(v,"ledge_spacing_um");
room.ledgeDepthMicrometres=LayoutInt(v,"ledge_depth_um");
room.ledgeSide=static_cast<std::uint8_t>(LayoutInt(v,"ledge_side"));
s.cavernRooms.push_back(std::move(room));
}
for(const auto& v:root.Get("cavern_edges").AsArray())
@@ -2088,7 +2150,11 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
if(!candidate.finalize(error))return false;
layout=std::move(candidate);return true;
}
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV2;
const bool hasVerticalShaft=std::any_of(rooms.begin(),rooms.end(),
[](const auto& room){return room.shapeKind==1;});
candidate.cavernGraphVocabularyVersion=hasVerticalShaft
?ResolvedLayoutCavernGraphVocabularyVersionV3
:ResolvedLayoutCavernGraphVocabularyVersionV2;
candidate.cavernRooms.assign(rooms.begin(),rooms.end());
candidate.cavernEdges.assign(edges.begin(),edges.end());
const auto isNativeCavesStrate=[&candidate](std::int32_t strateIndex)
@@ -2268,7 +2334,11 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
||settings.irregularityAmplitudeMicrometres<0
||settings.irregularityAmplitudeMicrometres>10000000
||(settings.irregularityAmplitudeMicrometres>0
&&settings.irregularityWavelengthMicrometres<=0))
&&settings.irregularityWavelengthMicrometres<=0)
||(settings.ledgeSpacingMicrometres<0
||settings.ledgeSpacingMicrometres>100000000)
||settings.ledgeDepthMicrometres<0
||settings.ledgeDepthMicrometres>5000000)
{error="cavern planner settings are outside the bounded room, tunnel or relief ranges";return false;}
const ResolvedLayoutPassageV1* arrivalPassage=nullptr;
@@ -2282,12 +2352,17 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
&&(!departurePassage||passage.stableId<departurePassage->stableId))
departurePassage=&passage;
}
if(!arrivalPassage||!departurePassage)
{error="cavern planner needs an incoming lower mouth and an outgoing upper mouth in the target strate";return false;}
const bool terminalShaftRoute=settings.verticalShaftMode
&&stratum->generatorKind=="VerticalShafts"&&!departurePassage;
if(!arrivalPassage||(!departurePassage&&!terminalShaftRoute)
||(settings.verticalShaftMode&&!terminalShaftRoute))
{error="cavern planner needs an incoming mouth and either an outgoing mouth or a Vertical Shafts basal terminal";return false;}
if(layout.coreVoxelSizeMicrometres==0)
{error="cavern planner requires a nonzero Core voxel size";return false;}
using FPoint=std::array<std::int64_t,3>;
const std::string routePrefix=(terminalShaftRoute?"vertical-shafts/strate-"
:"tunnel-network/strate-")+std::to_string(strateIndex)+"/";
constexpr std::int64_t MicrometresPerMetre=1000000;
constexpr std::int64_t MinimumBoreRadius=1750000;
constexpr std::int64_t MinimumBoreHeight=2200000;
@@ -2490,19 +2565,47 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
std::array<std::int64_t,2> arrivalTangentQ16{},departureTangentQ16{};
std::int64_t arrivalFloorSlopeQ16=0,departureFloorSlopeQ16=0;
if(!mouthInnerPoint(*arrivalPassage,true,routeStart,arrivalTangentQ16,
arrivalFloorSlopeQ16)
||!mouthInnerPoint(*departurePassage,false,routeEnd,departureTangentQ16,
arrivalFloorSlopeQ16))
{error="cavern passage mouth is missing a usable horizontal route tangent";return false;}
if(!terminalShaftRoute
&&!mouthInnerPoint(*departurePassage,false,routeEnd,departureTangentQ16,
departureFloorSlopeQ16))
{error="cavern passage mouth is missing a usable horizontal route tangent";return false;}
if(terminalShaftRoute)
{
departureTangentQ16=arrivalTangentQ16;
routeEnd[2]=stratum->content.bottomMicrometres+3000000;
departureFloorSlopeQ16=0;
}
const std::int64_t dx=routeEnd[0]-routeStart[0];
const std::int64_t dy=routeEnd[1]-routeStart[1];
std::int64_t dx=routeEnd[0]-routeStart[0];
std::int64_t dy=routeEnd[1]-routeStart[1];
const std::int64_t dz=routeEnd[2]-routeStart[2];
const std::int64_t horizontalBias=std::clamp<std::int64_t>(
settings.tunnelHorizontalBiasQ16,0,65536);
const std::int64_t planGradeDenominator=MaximumWalkGradeDenominator
+horizontalBias/32768;
const std::int64_t requiredLength=(std::abs(dz)*planGradeDenominator*130)/100+1000;
if(terminalShaftRoute)
{
// The basal stratum has no next mouth by design. The walk-only spine starts
// at its incoming passage and ends on a supported landing above bedrock.
// Optional open shafts are side features with separate climb/fall reporting.
// Normalize by the truncated Q16 tangent magnitude: dividing by 65536
// shortens non-cardinal vectors enough to miss the strict grade-run bound.
const std::uint64_t tangentSquared=static_cast<std::uint64_t>(
departureTangentQ16[0]*departureTangentQ16[0]
+departureTangentQ16[1]*departureTangentQ16[1]);
const std::int64_t tangentMagnitudeQ16=static_cast<std::int64_t>(
integerSqrt(tangentSquared));
if(tangentMagnitudeQ16<=0)
{error="cavern planner terminal tangent has zero horizontal length";return false;}
const std::int64_t terminalProjectionLength=requiredLength+1000;
for(std::size_t axis=0;axis<2;++axis)
routeEnd[axis]=routeStart[axis]
+departureTangentQ16[axis]*terminalProjectionLength/tangentMagnitudeQ16;
dx=routeEnd[0]-routeStart[0];dy=routeEnd[1]-routeStart[1];
}
std::int64_t pathAxisX=dx,pathAxisY=dy;
FPoint routeLeadStart=routeStart,routeLeadEnd=routeEnd;
for(std::size_t axis=0;axis<2;++axis)
@@ -2648,7 +2751,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
}
};
appendPassageClearanceSpans(*arrivalPassage,true);
appendPassageClearanceSpans(*departurePassage,false);
if(departurePassage)appendPassageClearanceSpans(*departurePassage,false);
const auto segmentDistanceMicrometres=[](const std::array<double,3>& p0,
const std::array<double,3>& p1,const std::array<double,3>& q0,
const std::array<double,3>& q1)
@@ -2804,8 +2907,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
cumulative[i]=cumulative[i-1]+distance2d(routePoints[i-1],routePoints[i]);
if(cumulative.back()!=totalRouteLength)
{error="cavern planner route length accounting failed";return false;}
const std::string backboneFloorProfileId="tunnel-network/strate-"
+std::to_string(strateIndex)+"/backbone-floor";
const std::string backboneFloorProfileId=routePrefix+"backbone-floor";
constexpr std::int64_t ProfileOne=std::int64_t{1}<<30;
const auto roundedDivide=[](std::int64_t numerator,std::int64_t denominator)
{
@@ -3008,7 +3110,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
std::int64_t radiusMicrometres=0;
bool destination=false;
};
std::vector<FRoomNode> nodes;nodes.reserve(nodeDistances.size());
std::vector<FRoomNode> nodes;nodes.reserve(nodeDistances.size()+1);
const std::int64_t floorCutMinimum=clampQ16(settings.roomFloorCutMinimumQ16);
const std::int64_t floorCutMaximum=std::max(floorCutMinimum,
clampQ16(settings.roomFloorCutMaximumQ16));
@@ -3025,15 +3127,23 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
node.floorProfileStableId=backboneFloorProfileId;
node.floorProfileDistanceMicrometres=distance;
node.floorGradientQ16=routeFloorGradientAt(distance);
node.destination=distance==mainRouteMidpoint;
node.id="tunnel-network/strate-"+std::to_string(strateIndex)+"/room-"
+std::to_string(nodeIndex);
node.destination=!terminalShaftRoute&&distance==mainRouteMidpoint;
node.id=routePrefix+"room-"+std::to_string(nodeIndex);
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,node.id);
node.radiusMicrometres=node.destination?settings.destinationRadiusMicrometres
:settings.minimumRoomRadiusMicrometres
+static_cast<std::int64_t>(hash%static_cast<std::uint64_t>(baseRoomSpan+1));
nodes.push_back(std::move(node));
}
if(terminalShaftRoute)
{
FRoomNode terminal;terminal.id=routePrefix+"basal-landing";
terminal.point=routeEnd;terminal.floorProfileStableId=backboneFloorProfileId;
terminal.floorProfileDistanceMicrometres=totalRouteLength;
terminal.floorGradientQ16=routeFloorGradientAt(totalRouteLength);
terminal.radiusMicrometres=settings.destinationRadiusMicrometres;
terminal.destination=true;nodes.push_back(std::move(terminal));
}
const auto makeCompound=[&](const FRoomNode& node,bool destination,
std::vector<ResolvedCavernRoomV1>& outRooms)
{
@@ -3154,6 +3264,130 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
}
return edge;
};
struct FPlannedShaftLanding
{
FRoomNode node;
FPoint point{};
};
std::vector<FPlannedShaftLanding> shaftLandings;
std::size_t plannedShaftCount=0;
if(terminalShaftRoute&&settings.roomDensityQ16>0)
{
const std::int64_t ledgeSpacing=std::max<std::int64_t>(0,
settings.ledgeSpacingMicrometres);
const std::int64_t ledgeDepth=std::clamp<std::int64_t>(
settings.ledgeDepthMicrometres,0,5000000);
bool emittedShaft=false;
const std::int64_t routeLengthXY=std::max<std::int64_t>(1,
static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
pathAxisX*pathAxisX+pathAxisY*pathAxisY))));
const std::int64_t sideX=-pathAxisY*65536/routeLengthXY;
const std::int64_t sideY=pathAxisX*65536/routeLengthXY;
const auto selectedRadius=[&](const std::string& stableId)
{
const std::int64_t span=settings.maximumRoomRadiusMicrometres
-settings.minimumRoomRadiusMicrometres;
return settings.minimumRoomRadiusMicrometres+(span==0?0:
static_cast<std::int64_t>(LayoutStampSeed(seed,strateIndex,stableId)
%static_cast<std::uint64_t>(span+1)));
};
for(std::size_t nodeIndex=1;nodeIndex+1<nodes.size()&&rooms.size()<2048;
++nodeIndex)
{
const auto& parent=nodes[nodeIndex];
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,
parent.id+"/shaft");
if(emittedShaft&&static_cast<std::int64_t>(hash&0xffffu)
>=settings.roomDensityQ16)continue;
const std::int64_t radius=selectedRadius(parent.id+"/shaft-radius");
const std::int64_t top=parent.point[2]
+(ledgeSpacing>0?ledgeSpacing:1000000);
const std::int64_t minimumTop=stratum->content.bottomMicrometres+4000000;
if(top>stratum->content.topMicrometres-1000000
||top<minimumTop)continue;
// Shaft length has no independent owner parameter: use as much content
// as available, with the graph's 200 m half-extent bound as a hard cap.
const std::int64_t bottom=std::max(
stratum->content.bottomMicrometres+1000000,top-200000000);
if(ledgeSpacing>0&&top-bottom<ledgeSpacing+2000000)continue;
const bool positiveSide=(hash&0x10000u)!=0;
const std::int64_t sideXSign=(sideX>=0?1:-1)*(positiveSide?1:-1);
const std::int64_t sideYSign=(sideY>=0?1:-1)*(positiveSide?1:-1);
const bool useX=std::abs(sideX)>=std::abs(sideY);
const std::uint8_t outwardSide=useX
?static_cast<std::uint8_t>(sideXSign>0?0:2)
:static_cast<std::uint8_t>(sideYSign>0?1:3);
const std::array<std::array<std::int64_t,2>,4> cardinal={{{1,0},{0,1},
{-1,0},{0,-1}}};
const auto outward=cardinal[outwardSide];
const std::int64_t offset=std::max<std::int64_t>(
parent.radiusMicrometres+radius+3000000,
settings.roomSpacingMicrometres/2);
FPlannedShaftLanding landing;landing.node.id=routePrefix+"shaft-"
+std::to_string(shaftLandings.size());
landing.node.point={parent.point[0]+outward[0]*offset,
parent.point[1]+outward[1]*offset,parent.point[2]};
landing.node.radiusMicrometres=radius;
const std::int64_t landingZ=top-(ledgeSpacing>0?ledgeSpacing:1000000);
const std::array<std::int64_t,2> toParent={-outward[0],-outward[1]};
const std::uint8_t ledgeSide=static_cast<std::uint8_t>(
(outwardSide+2)%4);
ResolvedCavernRoomV1 shaft;shaft.stableId=landing.node.id+"/primitive-0";
shaft.strateIndex=strateIndex;shaft.shapeKind=1;
shaft.centerMicrometres={landing.node.point[0],landing.node.point[1],
(top+bottom)/2};
shaft.halfExtentsMicrometres={radius,radius,(top-bottom)/2};
shaft.floorMicrometres=top;shaft.smoothWidthMicrometres=smooth;
shaft.ledgeSpacingMicrometres=ledgeSpacing;
shaft.ledgeDepthMicrometres=ledgeDepth;shaft.ledgeSide=ledgeSide;
shaft.irregularityAmplitudeMicrometres=irregularity;
shaft.irregularityWavelengthMicrometres=
settings.irregularityWavelengthMicrometres;
shaft.irregularitySeed=LayoutStampSeed(seed,strateIndex,shaft.stableId);
rooms.push_back(std::move(shaft));
landing.point={landing.node.point[0]
+toParent[0]*(radius-ledgeDepth/2),
landing.node.point[1]+toParent[1]*(radius-ledgeDepth/2),landingZ};
landing.node.point=landing.point;
const std::string accessId=routePrefix+"shaft-access-"
+std::to_string(shaftLandings.size());
auto access=makeEdge(accessId,componentRoomId(parent),parent.point,
landing.node,routeBoreRadius,-1,-1);
// The connector's own floor SDF retains its tread. It must not enter
// the descending backbone's finite floor-cap arbitration, where its
// nearby but independent floor could suppress backbone support.
access.floorSupportDepthMicrometres=0;
const std::int64_t run=distance2d(parent.point,landing.point);
const std::int64_t rise=std::abs(parent.point[2]-landing.point[2]);
if(run<=0||rise*MaximumWalkGradeDenominator>run)
{
rooms.pop_back();
continue;
}
edges.push_back(std::move(access));
shaftLandings.push_back(std::move(landing));
emittedShaft=true;
}
plannedShaftCount=shaftLandings.size();
for(std::size_t index=1;index<shaftLandings.size();++index)
{
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,
shaftLandings[index-1].node.id+"/cross-"+std::to_string(index));
if(static_cast<std::int64_t>(hash&0xffffu)>=settings.tunnelDensityQ16)
continue;
const auto& from=shaftLandings[index-1];
const auto& to=shaftLandings[index];
const std::int64_t run=distance2d(from.point,to.point);
const std::int64_t rise=std::abs(from.point[2]-to.point[2]);
if(run<=0||run>settings.maximumTunnelLengthMicrometres
||rise*MaximumWalkGradeDenominator>run)continue;
auto link=makeEdge(routePrefix+"shaft-cross-"+std::to_string(index-1),
componentRoomId(from.node),from.point,to.node,routeBoreRadius,-1,-1);
link.floorSupportDepthMicrometres=0;
edges.push_back(std::move(link));
}
}
const auto arrivalPoint=routeStart;
FPoint previousPoint=arrivalPoint;
std::int64_t previousProfileDistance=0;
@@ -3161,8 +3395,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
for(std::size_t nodeIndex=0;nodeIndex<nodes.size();++nodeIndex)
{
const auto& node=nodes[nodeIndex];
const std::string edgeId="tunnel-network/strate-"+std::to_string(strateIndex)
+"/route-edge-"+std::to_string(nodeIndex);
const std::string edgeId=routePrefix+"route-edge-"+std::to_string(nodeIndex);
auto edge=makeEdge(edgeId,previousAnchor,previousPoint,node,
selectedBoreRadius(edgeId),
previousProfileDistance,node.floorProfileDistanceMicrometres);
@@ -3171,10 +3404,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
previousProfileDistance=node.floorProfileDistanceMicrometres;
previousAnchor=componentRoomId(node);
}
if(!nodes.empty())
if(!nodes.empty()&&departurePassage)
{
const std::string departureEdgeId="tunnel-network/strate-"
+std::to_string(strateIndex)+"/departure-edge";
const std::string departureEdgeId=routePrefix+"departure-edge";
auto edge=makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth",
routeEnd,nodes.back(),selectedBoreRadius(departureEdgeId),
totalRouteLength,nodes.back().floorProfileDistanceMicrometres);
@@ -3187,7 +3419,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
const std::size_t branchLimit=settings.originRoomMaximumConnections<=0
?32u:static_cast<std::size_t>(std::clamp(settings.originRoomMaximumConnections,0,32));
std::size_t branchCount=0;
for(std::size_t nodeIndex=1;nodeIndex+1<nodes.size()
for(std::size_t nodeIndex=1;!terminalShaftRoute&&nodeIndex+1<nodes.size()
&&branchCount<branchLimit&&rooms.size()<2048;++nodeIndex)
{
const auto& parent=nodes[nodeIndex];
@@ -3210,8 +3442,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
// A side branch leaves the backbone laterally, so it has its own local flat
// floor and must not reference the parent's one-dimensional h(s) profile.
makeCompound(branch,false,rooms);
const std::string branchEdgeId="tunnel-network/strate-"
+std::to_string(strateIndex)+"/side-edge-"+std::to_string(branchCount);
const std::string branchEdgeId=routePrefix+"side-edge-"+std::to_string(branchCount);
ResolvedCavernEdgeV1 branchEdge;
if(settings.tunnelsFlowTowardOrigin)
branchEdge=makeEdge(branchEdgeId,componentRoomId(branch),
@@ -3226,7 +3457,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
++branchCount;
}
if(nodes.empty()||rooms.empty()||edges.empty())
{error="cavern planner emitted an empty arrival-to-departure graph";return false;}
{error="cavern planner emitted an empty mouth-to-terminal graph";return false;}
double maximumGradeDegrees=0.0;
for(const auto& edge:edges)
@@ -3255,9 +3486,10 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
}
const double plannerMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-plannerStarted).count();
std::printf("[CavernPlannerV1] strate=%d seed=%d mouths=%s->%s route_nodes=%zu room_primitives=%zu edges=%zu branches=%zu route_horizontal_m=%.3f floor_delta_m=%.3f max_grade_deg=%.3f mouth_tangent_min_dot=%.3f grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.3f plan_ms=%.3f status=PASS\n",
strateIndex,seed,arrivalPassage->stableId.c_str(),departurePassage->stableId.c_str(),
nodes.size(),rooms.size(),edges.size(),branchCount,totalRouteLength/1.0e6,
std::printf("[CavernPlannerV1] strate=%d seed=%d route=%s->%s route_nodes=%zu room_primitives=%zu shafts=%zu edges=%zu branches=%zu route_horizontal_m=%.3f floor_delta_m=%.3f max_grade_deg=%.3f mouth_tangent_min_dot=%.3f grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.3f plan_ms=%.3f status=PASS\n",
strateIndex,seed,arrivalPassage->stableId.c_str(),
departurePassage?departurePassage->stableId.c_str():"basal-terminal",
nodes.size(),rooms.size(),plannedShaftCount,edges.size(),branchCount,totalRouteLength/1.0e6,
std::abs(dz)/1.0e6,maximumGradeDegrees,plannedMouthTangentAlignment,
routeBoreRadius/1.0e6,PlannedBoreHeight/1.0e6,
settings.destinationRadiusMicrometres/1.0e6,plannerMilliseconds);
+89 -6
View File
@@ -1284,7 +1284,8 @@ public:
const std::uint32_t reliefSeed=static_cast<std::uint32_t>(Owner?Owner->Seed:0);
const auto* queryStratum=FindBoundStratumForManagerZ(Z);
const bool plannedTunnelStratum=queryStratum&&queryStratum->Resolved
&&queryStratum->Resolved->generatorKind=="TunnelNetwork"
&&(queryStratum->Resolved->generatorKind=="TunnelNetwork"
||queryStratum->Resolved->generatorKind=="VerticalShafts")
&&queryStratum->Resolved->recipeId=="Core.Caves.PrimitiveField"
&&queryStratum->Resolved->recipeVersion==1;
const auto* routePassageIndices=passageTileScopeMatches
@@ -1617,9 +1618,12 @@ public:
const auto plannedPortalWalkCorridorContains=[&]()
{
if(!queryStratum||!queryStratum->Resolved
||queryStratum->Resolved->generatorKind!="TunnelNetwork"
||(queryStratum->Resolved->generatorKind!="TunnelNetwork"
&&queryStratum->Resolved->generatorKind!="VerticalShafts")
||queryStratum->Resolved->recipeId!="Core.Caves.PrimitiveField"
||queryStratum->Resolved->recipeVersion!=1)return false;
const bool terminalShaftRoute=
queryStratum->Resolved->generatorKind=="VerticalShafts";
const VoxelForgeCore::ResolvedLayoutAnchorV2* first=nullptr;
const VoxelForgeCore::ResolvedLayoutAnchorV2* second=nullptr;
const VoxelForgeCore::ResolvedLayoutAnchorV2* third=nullptr;
@@ -1638,8 +1642,12 @@ public:
}
if(routeCount<6||!first||!second||!third||!lastThree||!lastTwo||!last
||!first->stableId.ends_with("000-arrival")
||!second->stableId.ends_with("000-edge-start")
||!lastTwo->stableId.ends_with("998-departure-inner")
||!second->stableId.ends_with("000-edge-start"))return false;
if(terminalShaftRoute)
{
if(!last->stableId.ends_with("999-basal-terminal"))return false;
}
else if(!lastTwo->stableId.ends_with("998-departure-inner")
||!last->stableId.ends_with("999-departure"))return false;
const double metresPerSubvoxel=1.0/qPerMetre;
const auto position=[&](const VoxelForgeCore::ResolvedLayoutAnchorV2& anchor)
@@ -1677,6 +1685,7 @@ public:
if(nearRouteSegment(*first,*second,PortalWalkLeadMetres,false,0.0)
||nearRouteSegment(*second,*third,PortalWalkLeadMetres,false,
arrivalInnerDistance))return true;
if(terminalShaftRoute)return false;
const auto departureInner=position(*lastTwo),departure=position(*last);
const double departureTailDistance=std::hypot(
departure[0]-departureInner[0],departure[1]-departureInner[1]);
@@ -3835,8 +3844,11 @@ private:
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr;
bool nativeGraph=false;
std::uint8_t roomShapeKind=0;
std::uint8_t ledgeSide=0;
std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{};
std::array<double,3> centerMetres{},halfExtentsMetres{};
double ledgeSpacingMetres=0.0,ledgeDepthMetres=0.0;
struct FSegment
{
std::array<double,3> start{},end{},minimum{},maximum{};
@@ -4285,7 +4297,9 @@ private:
||(ResolvedLayout->cavernGraphVocabularyVersion
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1
&&ResolvedLayout->cavernGraphVocabularyVersion
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV2))return;
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV2
&&ResolvedLayout->cavernGraphVocabularyVersion
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV3))return;
const auto isNativeGraphStrate=[this](std::int32_t strateIndex)
{
const auto found=std::find_if(ResolvedLayout->strates.begin(),
@@ -4302,6 +4316,10 @@ private:
{
FCavernRuntimeFeature runtime;runtime.room=&room;
runtime.nativeGraph=isNativeGraphStrate(room.strateIndex);
runtime.roomShapeKind=room.shapeKind;
runtime.ledgeSide=room.ledgeSide;
runtime.ledgeSpacingMetres=room.ledgeSpacingMicrometres/1000000.0;
runtime.ledgeDepthMetres=room.ledgeDepthMicrometres/1000000.0;
runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres;
runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres;
for(std::size_t axis=0;axis<3;++axis)
@@ -4956,7 +4974,8 @@ private:
ResolvedLayout->strates.end(),[&](const auto& stratum)
{return stratum.strateIndex==passage.lowerStrateIndex;});
if(lowerStratum!=ResolvedLayout->strates.end()
&&lowerStratum->generatorKind=="TunnelNetwork"
&&(lowerStratum->generatorKind=="TunnelNetwork"
||lowerStratum->generatorKind=="VerticalShafts")
&&routeEnd<controls.size())
{
double routeHeight=0.0,distanceSquared=0.0;
@@ -5667,6 +5686,11 @@ public:
FMetricInterval featureVoid;
if(feature.room)
{
// Shaft wall and ledge SDFs are evaluated exactly per sample.
// Until a conservative interval bound for their carved shelves is
// available, keep intersecting boxes Mixed instead of proving a
// potentially false uniform sign.
if(feature.roomShapeKind==1)return {0.0,0.0,false};
double normalizedMinimumSquared=0.0,normalizedMaximumSquared=0.0;
double meanRadius=0.0;
for(std::size_t axis=0;axis<3;++axis)
@@ -7884,6 +7908,64 @@ private:
double edgeFloor=0.0;
if(feature.room)
{
if(feature.roomShapeKind==1)
{
const double radius=feature.halfExtentsMetres[0];
const double bottom=feature.centerMetres[2]-feature.halfExtentsMetres[2];
const double top=feature.centerMetres[2]+feature.halfExtentsMetres[2];
const double radial=std::hypot(point[0]-feature.centerMetres[0],
point[1]-feature.centerMetres[1]);
const double radialQ=radial-radius;
const double verticalQ=std::max(bottom-point[2],point[2]-top);
double shaftDistance=std::min(std::max(radialQ,verticalQ),0.0)
+std::hypot(std::max(radialQ,0.0),std::max(verticalQ,0.0));
if(feature.irregularityAmplitudeVoxels>0.0f)
shaftDistance+=StampValueNoise(
X-feature.centerVoxels[0],Y-feature.centerVoxels[1],
Z-feature.centerVoxels[2],feature.irregularityWavelengthVoxels,
feature.irregularitySeed)
*feature.irregularityAmplitudeVoxels*metresPerVoxel;
// A cardinal ledge faces the walkable backbone. Shelves are
// cut out of the otherwise open cylinder, leaving most of the
// shaft cross-section clear at every interval.
if(feature.ledgeSpacingMetres>0.0&&feature.ledgeDepthMetres>0.0)
{
const long long nearest=static_cast<long long>(std::llround(
(top-point[2])/feature.ledgeSpacingMetres));
const long long maximum=static_cast<long long>(std::floor(
(top-bottom-1000000.0)/feature.ledgeSpacingMetres));
if(nearest>=1&&nearest<=maximum)
{
const double ledgeZ=top-nearest*feature.ledgeSpacingMetres;
constexpr double LedgeThicknessMetres=0.6;
constexpr double LedgeFilletMetres=0.1;
const double dx=point[0]-feature.centerMetres[0];
const double dy=point[1]-feature.centerMetres[1];
const std::array<std::array<double,2>,4> sides={{{1.0,0.0},
{0.0,1.0},{-1.0,0.0},{0.0,-1.0}}};
const auto side=sides[feature.ledgeSide%4];
const double radialLocal=dx*side[0]+dy*side[1];
const double tangentLocal=-dx*side[1]+dy*side[0];
const double halfDepth=feature.ledgeDepthMetres*0.5;
const double halfWidth=radius*0.45;
const double halfThickness=LedgeThicknessMetres*0.5;
const double qx=std::abs(radialLocal-(radius-halfDepth))-halfDepth;
const double qy=std::abs(tangentLocal)-halfWidth;
const double qz=std::abs(point[2]-(ledgeZ-halfThickness))
-halfThickness;
const double outside=std::hypot(std::max(qx,0.0),
std::max(qy,0.0),std::max(qz,0.0));
const double platformDistance=outside
+std::min(std::max(qx,std::max(qy,qz)),0.0)
-LedgeFilletMetres;
shaftDistance=std::max(shaftDistance,-platformDistance);
}
}
featureVoid=shaftDistance;
}
else
{
double normalizedSquared=0.0,meanRadius=0.0;
for(std::size_t axis=0;axis<3;++axis)
{
@@ -7907,6 +7989,7 @@ private:
+feature.floorGradient[1]*(point[1]-feature.centerMetres[1]);
const double width=feature.room->smoothWidthMicrometres/1000000.0;
featureVoid=SmoothMaxMetres(ellipsoid,floor,width);
}
}
else if(feature.edge&&!feature.segments.empty())
{