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
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Linux139 — Vertical Shafts on the Core planner
|
||||
|
||||
## Result
|
||||
|
||||
S3 now reports `Core.Caves.PrimitiveField` / `Caves-native`, using the shared Tunnel Network
|
||||
planner. The composed acceptance gate is not green: **15/16 seeds pass; seed 2 fails the passage
|
||||
shell and tile seam checks at the arrival from passage 2–3.** This worktree is not promoted as an
|
||||
accepted Vertical Shafts generator.
|
||||
|
||||
The basal primordial route law means arrival passage 2–3 to a walkable terminal landing above the
|
||||
bedrock seal. There is no departure mouth at the bottom of the owner world. The shared planner
|
||||
builds that walk-only spine, its access tunnels, and landings within the existing grade contract.
|
||||
The full-height open shafts and their ledges are an exploration tier: a drop or climb is not
|
||||
walk-only traversal, and climb or mine access stays in the reporting tiers. `Core.Lattice` was not
|
||||
used because L58's VerticalShafts lattice route looked like a corkscrew, not an open shaft.
|
||||
|
||||
## Plan and parameter mapping
|
||||
|
||||
`PlanResolvedCavernNetworkV1` is reused without a planner fork. `Core.Caves.PrimitiveField` is the
|
||||
shared evaluator, extended with `shapeKind=1`: the existing room/tunnel shapes do not represent a
|
||||
capped, full-height vertical bore, and the L58 lattice route was a corkscrew. Shaft records use
|
||||
that cylinder primitive with ledges, short access bores, and cross-shaft links. Generic cave
|
||||
branches are disabled in shaft mode.
|
||||
|
||||
| `FVerticalShaftParams` field(s) | Core setting |
|
||||
|---|---|
|
||||
| `ShaftSpacing` | Shared planner room spacing; owner voxels convert to integer micrometres. |
|
||||
| `ShaftDensity` | `roomDensityQ16`, controlling shaft placement; a nonzero setting guarantees the route's initial shaft. |
|
||||
| `ShaftMinRadius`, `ShaftMaxRadius` | Minimum and maximum shaft radii. |
|
||||
| `CrossConnectChance` | `tunnelDensityQ16` for links between shafts. |
|
||||
| `ConnectorRadius` | Equal minimum/maximum radius for access and connector bores. |
|
||||
| `LedgeSpacing`, `LedgeDepth` | Vertical shelf interval and intrusion; zero spacing disables shelves and zero depth makes them nonintrusive. |
|
||||
| `SurfaceRoughness` | Roughness amplitude; frequency derives as `max(0.5 m, 4 owner voxels)` because no frequency field exists. |
|
||||
| `BoundarySealThickness`, `BaseDensity` | Existing resolved top/basal boundary bands via `sealAndDensity`, outside planner settings. |
|
||||
|
||||
## Measurements
|
||||
|
||||
The final source stamp was `b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1`.
|
||||
The composed sweep covered seeds 0–15 with one run per seed (reported process time 320.485 s in
|
||||
total; individual runs 10.296–34.015 s). Seeds 0, 1, and 3–15 passed passages, route, seams, shell,
|
||||
and blocked-route controls. Seed 2's route grades/support, naturalness, mouth profile, ripple,
|
||||
dispatch, and negative controls passed; its composed passage, seam, and shell summaries failed.
|
||||
The shell audit reported eight `wall_missing` samples on `passage/2-3/0`, at route stations
|
||||
489.627–490.321 m. The final log records positive native density at those samples and negative
|
||||
wall-shell margins. See `Saved/Linux/l139/seeds/seed2-final.log` and
|
||||
`Saved/Linux/l139/seeds/summary.json`.
|
||||
|
||||
Three geometry variants were tried against this same shell gate: starting passage clearance at
|
||||
12 m, adding a guarded cubic lateral bow, and starting clearance at 8 m. The first and third still
|
||||
failed wall checks; the bow failed floor checks. Per the three-failure stop rule, these were
|
||||
discarded. The final binary retains the successful Q16 endpoint-normalization correction needed
|
||||
for seed 1 and no seed-2 geometry patch.
|
||||
|
||||
L80 ran after the seed sweep. The host gate was **load 1.31, CPU idle 99.4%**. It used one warm-up
|
||||
per build and stratum/worker cell, then five interleaved measured runs for each of three builds at
|
||||
W8 and W12 across all four strata: 120 measured runs total. Comparisons use `fa4417a`,
|
||||
`assembler/world`, and the L139 binary; the established L137 A/A noise floor is ±1% ([A/A report](../Linux137/REPORT.md)).
|
||||
Times are medians in seconds; deltas are L139 relative to each comparison build. Paired values in
|
||||
each cell are W8 / W12.
|
||||
|
||||
| Stratum | LOD0–2 L139 | Δ vs `fa4417a` | Δ vs `assembler/world` | Whole-view L139 | Δ vs `fa4417a` | Δ vs `assembler/world` |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| S0 Surface World | 0.838 / 0.886 | +1.71% / +3.97% | +2.92% / +5.06% | 1.862 / 1.804 | +2.42% / +1.63% | −0.11% / −0.33% |
|
||||
| S1 Floating Islands | 1.036 / 1.038 | +4.72% / +4.61% | +2.09% / +5.35% | 1.842 / 1.830 | +0.11% / +1.16% | −6.73% / −1.24% |
|
||||
| S2 Tunnel Network | 1.105 / 1.078 | +2.44% / +3.07% | +3.57% / +7.57% | 1.876 / 1.808 | −1.11% / +0.00% | −4.33% / −1.15% |
|
||||
| S3 Vertical Shafts | 0.899 / 0.884 | −3.81% / −0.94% | −0.95% / +2.00% | 1.706 / 1.657 | −6.57% / −2.53% | −6.21% / −4.77% |
|
||||
|
||||
Whole-view medians are numerically lower than `assembler/world` in all eight cells; the S0 W8 and
|
||||
W12 differences are within the ±1% noise floor. Seven of eight LOD0–2 cells are slower than
|
||||
`assembler/world`; the remaining cell is within the noise floor. This is characterization of an
|
||||
unaccepted build, not a promotion claim. Raw runs and medians are in
|
||||
`Saved/Linux/l139/timing/summary.json`.
|
||||
|
||||
## Checks and captures
|
||||
|
||||
- Fast tier: **PASS**, 284.0 s; the S3 dispatch line is `VerticalShafts` → `Core.Caves.PrimitiveField` v1 / `Caves-native` / `FCavesFieldAdapter`.
|
||||
- Clang and GCC determinism: **PASS**, W1/W8/W12 (six cases), 13 hash lines and resolved-layout identity matched. Both compilers built the final source stamp above.
|
||||
- Canonical v237 baselines added for Surface World and Floating Islands; both match their v236 values.
|
||||
- Five eye-height views were captured before and after: arrival mouth, shaft access landing, looking down, looking up from a ledge, and basal terminal landing. All five L139 views pass the existing quality threshold. Two L138 baseline views fail it: the old layer has no shaft at the selected site, so the down-shaft view is flat, and the upward view is low contrast. The threshold was not relaxed. Images and camera details are in `captures/`.
|
||||
- The outside views are `captures/after-whole-layer-density-cutaway.jpg` (coarse LOD density-mesh slice at y=−5 m) and `captures/after-whole-layer-side-elevation.svg` (resolved-plan projection that labels the walk route and all 53 shaft primitives). The cutaway passes the unchanged image-quality threshold; its plan companion makes the full shaft layout clearer.
|
||||
- Linux `VFHostEditor` Development build: **PASS**, 27 actions, 72.58 s, run after timing with `-NoUBA -MaxParallelActions=4` and no `prlimit`. It emitted three pre-existing deprecated RealtimeMesh API warnings from `VoxelWorld.cpp`; that file was not changed.
|
||||
- `git diff --check` and Python bytecode compilation of the L139 runners passed. Windows build and Windows determinism were not run.
|
||||
|
||||
Logs and source-stamped binaries are under `Saved/Linux/l139/`. The only changed Unreal `Source/`
|
||||
file is `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`.
|
||||
|
||||
## Remaining risk
|
||||
|
||||
The passage shell and seam break at the incoming mouth on seed 2 remain unresolved. The local
|
||||
clearance/heading premise failed three geometry attempts; a new route-to-mouth design premise is
|
||||
needed before another geometry change. Walk-only connectivity stays guaranteed by construction;
|
||||
vertical drops, climbs, and mine access remain reporting tiers.
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture comparable player-height views of the L138 and L139 owner shaft layer."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
LAYOUT = ROOT / "Saved/Linux/l139/seeds/seed0.json"
|
||||
OUT = ROOT / "Saved/Linux/l139/captures"
|
||||
ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux139/captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
VERSIONS = {
|
||||
"before-l138": {
|
||||
"headless": ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang",
|
||||
"renderer": ROOT / "Saved/Linux/l138/bin/VoxelForgeOffscreenCapture-l138-clang",
|
||||
"stamp": "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f",
|
||||
},
|
||||
"after-l139": {
|
||||
"headless": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang",
|
||||
"renderer": ROOT / "Saved/Linux/l139/bin/VoxelForgeOffscreenCapture-l139-clang",
|
||||
"stamp": "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1",
|
||||
},
|
||||
}
|
||||
STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M)
|
||||
|
||||
|
||||
def fmt(values: tuple[float, ...]) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def angles(camera: tuple[float, float, float],
|
||||
target: tuple[float, float, float]) -> tuple[float, float]:
|
||||
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 stamped_run(name: str, command: list[str], log: Path, expected: str,
|
||||
env: dict[str, str]) -> str:
|
||||
binary = Path(command[0])
|
||||
probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"],
|
||||
cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=20)
|
||||
match = STAMP_RE.search(probe.stdout or "")
|
||||
if probe.returncode != 0 or not match or match.group(1) != expected:
|
||||
raise RuntimeError(f"{name}: executable stamp mismatch for {binary}")
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
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")
|
||||
emitted = STAMP_RE.search(output)
|
||||
if (result.returncode != 0 or not emitted or emitted.group(1) != expected):
|
||||
raise RuntimeError(f"{name}: process failed or source changed; see {log}")
|
||||
print(f"[L139CaptureRun] step={name} elapsed_s={time.monotonic()-started:.1f} "
|
||||
f"log={log.relative_to(ROOT)}", flush=True)
|
||||
return output
|
||||
|
||||
|
||||
def point_um(values: list[int]) -> tuple[float, float, float]:
|
||||
return tuple(value / 1_000_000.0 for value in values)
|
||||
|
||||
|
||||
def make_views(layout: dict) -> list[dict]:
|
||||
edges = {row["stable_id"]: row for row in layout["cavern_edges"]}
|
||||
rooms = {row["stable_id"]: row for row in layout["cavern_rooms"]}
|
||||
shaft = rooms["vertical-shafts/strate-3/shaft-0/primitive-0"]
|
||||
access = edges["vertical-shafts/strate-3/shaft-access-0"]["polyline_um"]
|
||||
arrival = edges["vertical-shafts/strate-3/route-edge-0"]["polyline_um"]
|
||||
basal = rooms["vertical-shafts/strate-3/basal-landing/primitive-0"]
|
||||
center = point_um(shaft["center_um"])
|
||||
half = point_um(shaft["half_extents_um"])
|
||||
shaft_top = center[2] + half[2]
|
||||
shaft_bottom = center[2] - half[2]
|
||||
route_a, route_b = point_um(arrival[0]), point_um(arrival[1])
|
||||
tx, ty = route_b[0] - route_a[0], route_b[1] - route_a[1]
|
||||
tlen = max(1.0e-9, math.hypot(tx, ty))
|
||||
tangent = (tx / tlen, ty / tlen)
|
||||
mouth_camera = (route_a[0] - tangent[0] * 2.0,
|
||||
route_a[1] - tangent[1] * 2.0, route_a[2] + 1.45)
|
||||
mouth_target = (route_a[0] + tangent[0] * 8.0,
|
||||
route_a[1] + tangent[1] * 8.0, route_a[2] + 1.45)
|
||||
access_a, access_b = point_um(access[0]), point_um(access[-1])
|
||||
access_camera = (access_a[0], access_a[1], access_a[2] + 1.45)
|
||||
access_target = (access_b[0], access_b[1], access_b[2] + 1.45)
|
||||
shaft_lip_camera = (access_b[0], access_b[1], access_b[2] + 1.45)
|
||||
basal_center = point_um(basal["center_um"])
|
||||
basal_camera = (basal_center[0], basal_center[1], basal["floor_um"] / 1.0e6 + 1.45)
|
||||
basal_target = (basal_center[0] + tangent[0] * 8.0,
|
||||
basal_center[1] + tangent[1] * 8.0, basal_camera[2])
|
||||
ledge_z = shaft_top - 12 * (shaft["ledge_spacing_um"] / 1.0e6)
|
||||
return [
|
||||
{"name": "arrival-mouth", "camera": mouth_camera, "target": mouth_target,
|
||||
"focus": (route_a[0], route_a[1], route_a[2] + 1.4, 12.0),
|
||||
"note": "player eye at the passage arrival, looking into the connected layer"},
|
||||
{"name": "landing-at-shaft-access", "camera": access_camera,
|
||||
"target": access_target,
|
||||
"focus": (center[0], center[1], access_a[2], 14.0),
|
||||
"note": "walkable route landing and short shaft access tunnel"},
|
||||
{"name": "looking-down-shaft", "camera": shaft_lip_camera,
|
||||
"target": (center[0], center[1], shaft_top - 30.0),
|
||||
"focus": (center[0], center[1], shaft_top - 25.0, 14.0),
|
||||
"fov": 35.0,
|
||||
"note": "player eye at the upper access landing, looking down the open shaft"},
|
||||
{"name": "looking-up-from-ledge", "camera": (center[0], center[1], ledge_z + 1.45),
|
||||
"target": (center[0], center[1], shaft_top - 2.0),
|
||||
"focus": (center[0], center[1], shaft_top - 22.0, 20.0),
|
||||
"note": "player eye at an internal ledge, looking up; reaching this ledge is a climb tier"},
|
||||
{"name": "basal-terminal-landing", "camera": basal_camera,
|
||||
"target": basal_target,
|
||||
"focus": (basal_center[0], basal_center[1], basal_center[2], 16.0),
|
||||
"note": "walkable terminal landing above the basal seal"},
|
||||
]
|
||||
|
||||
|
||||
def write_overview(layout: dict, destination: Path) -> None:
|
||||
shafts = [room for room in layout["cavern_rooms"] if room.get("shape_kind") == 1]
|
||||
route_edges = sorted((edge for edge in layout["cavern_edges"]
|
||||
if edge["stable_id"].startswith("vertical-shafts/strate-3/route-edge-")),
|
||||
key=lambda edge: int(edge["stable_id"].rsplit("-", 1)[1]))
|
||||
if not shafts or not route_edges:
|
||||
raise ValueError("overview needs the resolved shaft primitives and walk route")
|
||||
points = [point_um(row) for edge in route_edges for row in edge["polyline_um"]]
|
||||
shaft_x_bounds = [(point_um(room["center_um"])[0]
|
||||
-room["half_extents_um"][0]/1e6,
|
||||
point_um(room["center_um"])[0]+room["half_extents_um"][0]/1e6)
|
||||
for room in shafts]
|
||||
x_min = min(min(bounds[0] for bounds in shaft_x_bounds),
|
||||
min(row[0] for row in points))
|
||||
x_max = max(x + room["half_extents_um"][0] / 1e6
|
||||
for room in shafts for x in [point_um(room["center_um"])[0]])
|
||||
x_max = max(x_max, max(row[0] for row in points))
|
||||
z_min = min(point_um(room["center_um"])[2] - room["half_extents_um"][2] / 1e6
|
||||
for room in shafts)
|
||||
z_max = max(point_um(room["center_um"])[2] + room["half_extents_um"][2] / 1e6
|
||||
for room in shafts)
|
||||
z_min = min(z_min, min(row[2] for row in points))
|
||||
z_max = max(z_max, max(row[2] for row in points))
|
||||
width, height, margin = 1800.0, 760.0, 72.0
|
||||
sx = (width - 2 * margin) / max(1.0, x_max - x_min)
|
||||
sz = (height - 2 * margin) / max(1.0, z_max - z_min)
|
||||
scale = min(sx, sz)
|
||||
x_offset = margin + ((width - 2 * margin) - (x_max - x_min) * scale) * 0.5
|
||||
z_offset = margin + ((height - 2 * margin) - (z_max - z_min) * scale) * 0.5
|
||||
def xy(x: float, z: float) -> tuple[float, float]:
|
||||
return x_offset + (x - x_min) * scale, height - (z_offset + (z - z_min) * scale)
|
||||
route_xy = [xy(p[0], p[2]) for p in points]
|
||||
route = " ".join(("M" if i == 0 else "L") + f"{x:.2f},{y:.2f}"
|
||||
for i, (x, y) in enumerate(route_xy))
|
||||
cylinders = []
|
||||
for room in shafts:
|
||||
x, _y, z = point_um(room["center_um"])
|
||||
radius = room["half_extents_um"][0] / 1e6
|
||||
half_z = room["half_extents_um"][2] / 1e6
|
||||
left, top = xy(x - radius, z + half_z)
|
||||
right, bottom = xy(x + radius, z - half_z)
|
||||
cylinders.append(f'<rect x="{left:.2f}" y="{top:.2f}" width="{max(1.2,right-left):.2f}" '
|
||||
f'height="{bottom-top:.2f}" rx="{min(7.0,(right-left)/2):.2f}" '
|
||||
'fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/>')
|
||||
rx0, ry0 = xy(x_min, 0.0)
|
||||
rx1, ry1 = xy(x_max, 0.0)
|
||||
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="1800" height="760" viewBox="0 0 1800 760">
|
||||
<rect width="1800" height="760" fill="#07111a"/>
|
||||
<text x="72" y="42" fill="#edf6fb" font-family="sans-serif" font-size="24">Vertical Shafts — whole-layer side elevation</text>
|
||||
<text x="72" y="64" fill="#9bb5c5" font-family="sans-serif" font-size="13">Resolved plan • seed 0 • strate 3 • vertical axis and route shown in metres • Y projected out</text>
|
||||
<line x1="{rx0:.1f}" y1="{ry0:.1f}" x2="{rx1:.1f}" y2="{ry1:.1f}" stroke="#315060" stroke-dasharray="5 8"/>
|
||||
{''.join(cylinders)}
|
||||
<path d="{route}" fill="none" stroke="#f4bf68" stroke-width="3.0" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="82" y="716" fill="#f4bf68" font-family="sans-serif" font-size="15">walk-only graded spine and landings</text>
|
||||
<rect x="416" y="704" width="15" height="15" fill="#93dded" fill-opacity="0.65" stroke="#42b8d1"/>
|
||||
<text x="440" y="716" fill="#b8dce5" font-family="sans-serif" font-size="15">open vertical shaft primitives (53)</text>
|
||||
<text x="1718" y="716" fill="#9bb5c5" text-anchor="end" font-family="sans-serif" font-size="12">Plan projection, not a density mesh</text>
|
||||
</svg>'''
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(svg, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not DATA.is_file() or not LAYOUT.is_file():
|
||||
raise SystemExit("missing owner fixture or passing seed-0 resolved layout")
|
||||
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||
views = make_views(layout)
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
quality = ROOT / "Standalone/Tools/verify/capture_quality.py"
|
||||
manifest = []
|
||||
quality_failures = []
|
||||
for version, config in VERSIONS.items():
|
||||
phase = "before" if version.startswith("before") else "after"
|
||||
for view in views:
|
||||
name = view["name"]
|
||||
camera, target = view["camera"], view["target"]
|
||||
yaw, pitch = angles(camera, target)
|
||||
mesh_dir = OUT / "meshes" / phase
|
||||
mesh_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = mesh_dir / f"{name}.vfmesh"
|
||||
capture_layout = mesh_dir / f"{name}-layout.json"
|
||||
export_log = OUT / f"{phase}-{name}-export.log"
|
||||
export = [config["headless"], DATA, "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=12",
|
||||
"--clip-radius=6", "--max-clip-level=0", "--max-level=0",
|
||||
"--force-generate", "--seed=0", "--recipe=surface-world",
|
||||
"--render-distance-chunks=0", f"--camera={fmt((*camera,yaw,pitch))}",
|
||||
f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"]
|
||||
stamped_run(f"{phase}-{name}-export", list(map(str, export)), export_log,
|
||||
config["stamp"], env)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{phase}-{name}: empty mesh export")
|
||||
png = OUT / f"{phase}-{name}.png"
|
||||
jpg = ARTIFACTS / f"{phase}-{name}.jpg"
|
||||
fov = float(view.get("fov", 78.0))
|
||||
render = [config["renderer"], mesh, png, "--size=1280x720",
|
||||
f"--fov-deg={fov:.1f}", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on", "--height-center-m=-950",
|
||||
"--height-range-m=60", "--tile-edges=off",
|
||||
f"--focus-region={fmt(view['focus'])}"]
|
||||
stamped_run(f"{phase}-{name}-render", list(map(str, render)),
|
||||
OUT / f"{phase}-{name}-render.log", config["stamp"], env)
|
||||
jpeg = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", png, "-frames:v", "1", "-q:v", "4", jpg]
|
||||
subprocess.run([*LIMIT, "timeout", "120s", *map(str, jpeg)], cwd=ROOT,
|
||||
env=env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
|
||||
timeout=125)
|
||||
check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable,
|
||||
quality, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=125)
|
||||
quality_ok = check.returncode == 0 and "status=PASS" in check.stdout
|
||||
quality_result = (check.stdout.strip().splitlines()[-1]
|
||||
if check.stdout.strip() else f"quality command exit={check.returncode}")
|
||||
if not quality_ok:
|
||||
quality_failures.append(f"{phase}-{name}")
|
||||
manifest.append({"phase": phase, "view": name, "file": str(jpg.relative_to(ROOT)),
|
||||
"bytes": jpg.stat().st_size, "camera_m": camera, "target_m": target,
|
||||
"yaw_deg": yaw, "pitch_deg": pitch, "fov_deg": fov,
|
||||
"note": view["note"],
|
||||
"quality_status": "PASS" if quality_ok else "FAIL",
|
||||
"quality": quality_result})
|
||||
print(f"[L139Capture] phase={phase} view={name} file={jpg.relative_to(ROOT)} "
|
||||
f"bytes={jpg.stat().st_size} quality={'PASS' if quality_ok else 'FAIL'}",
|
||||
flush=True)
|
||||
overview = ARTIFACTS / "after-whole-layer-side-elevation.svg"
|
||||
write_overview(layout, overview)
|
||||
manifest.append({"phase": "after", "view": "whole-layer-side-elevation",
|
||||
"file": str(overview.relative_to(ROOT)), "shafts": sum(
|
||||
room.get("shape_kind") == 1 for room in layout["cavern_rooms"]),
|
||||
"walk_route_edges": len([edge for edge in layout["cavern_edges"]
|
||||
if edge["stable_id"].startswith("vertical-shafts/strate-3/route-edge-")]),
|
||||
"note": "resolved-plan side projection; not a density-mesh capture"})
|
||||
(ARTIFACTS / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
status = "PASS" if not quality_failures else "FAIL"
|
||||
print(f"[L139CaptureSummary] spot_views={len(views)} comparisons={len(views)*2} "
|
||||
f"whole_layer_projection=1 quality_failures={len(quality_failures)} "
|
||||
f"status={status}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L139CaptureFailure] {error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare composed owner hashes and resolved graph identity at W1/W8/W12."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l139/determinism"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
STAMP = "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1"
|
||||
BINARIES = {
|
||||
"clang": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang",
|
||||
"gcc": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-gcc",
|
||||
}
|
||||
CAMERA = "-20.062500,-5.068359,-900.353516,0,0"
|
||||
HASH_RE = re.compile(
|
||||
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$",
|
||||
re.M)
|
||||
STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (DATA, *BINARIES.values())):
|
||||
raise SystemExit("missing L139 determinism binary or composed owner fixture")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
rows = []
|
||||
reference_hashes: tuple[str, ...] | None = None
|
||||
reference_layout_identity: str | None = None
|
||||
for compiler, binary in BINARIES.items():
|
||||
for workers in (1, 8, 12):
|
||||
name = f"determinism-{compiler}-w{workers}"
|
||||
log, layout = OUT / f"{name}.log", OUT / f"{name}.json"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", f"--workers={workers}",
|
||||
"--clip-radius=3", "--max-clip-level=8", "--max-level=0",
|
||||
f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=0",
|
||||
"--recipe=surface-world", "--render-distance-chunks=0",
|
||||
"--density-dispatch-report", f"--resolved-layout-output={layout}"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
proc = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = STAMP_RE.search(content)
|
||||
hashes = tuple(HASH_RE.findall(content))
|
||||
current_layout = json.loads(layout.read_text(encoding="utf-8"))
|
||||
stable_identity = current_layout.get("stable_identity")
|
||||
dispatch_ok = bool(re.search(
|
||||
r"^\[DensityDispatch\] strate=3 generator=VerticalShafts "
|
||||
r"recipe=Core\.Caves\.PrimitiveField version=1 "
|
||||
r"bound_path=Caves-native adapter=FCavesFieldAdapter$", content, re.M))
|
||||
passed = (proc.returncode == 0 and stamp is not None and stamp.group(1) == STAMP
|
||||
and len(hashes) >= 4 and dispatch_ok and bool(stable_identity))
|
||||
if reference_hashes is None and passed:
|
||||
reference_hashes = hashes
|
||||
reference_layout_identity = stable_identity
|
||||
else:
|
||||
passed = (passed and hashes == reference_hashes
|
||||
and stable_identity == reference_layout_identity)
|
||||
row = {"compiler": compiler, "workers": workers, "exit": proc.returncode,
|
||||
"stamp": stamp.group(1) if stamp else None, "hash_lines": len(hashes),
|
||||
"hashes_match_first": hashes == reference_hashes,
|
||||
"layout_identity": stable_identity,
|
||||
"layout_matches_first": stable_identity == reference_layout_identity,
|
||||
"shaft_dispatch": dispatch_ok, "passed": bool(passed),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))}
|
||||
rows.append(row)
|
||||
print("[L139DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n")
|
||||
if not passed:
|
||||
raise SystemExit(f"composed owner determinism mismatch; see {log}")
|
||||
(OUT / "summary.json").write_text(json.dumps({"status": "PASS", "stamp": STAMP,
|
||||
"layout_identity": reference_layout_identity, "runs": rows}, indent=2) + "\n")
|
||||
print(f"[L139DeterminismSummary] runs={len(rows)} source_stamp={STAMP} status=PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run interleaved L80 timings for L139, fa4417a, and assembler/world."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l139/timing"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"assembler-world": (
|
||||
ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a",
|
||||
"4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"),
|
||||
"fa4417a": (
|
||||
ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang",
|
||||
"9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f"),
|
||||
"l139": (
|
||||
ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang",
|
||||
"b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1"),
|
||||
}
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR = re.compile(
|
||||
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
|
||||
re.M)
|
||||
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M)
|
||||
|
||||
|
||||
def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
binary, expected_stamp = BINARIES[version]
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
label = f"s{strate}-w{workers}-{version}-{phase}{replicate}"
|
||||
log = OUT / f"{label}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", f"--workers={workers}",
|
||||
"--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||
f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
proc = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content)
|
||||
if (proc.returncode or pipeline is None or near is None or stamp is None
|
||||
or stamp.group(1) != expected_stamp):
|
||||
raise SystemExit(f"L80 timing failed or stamp mismatch: {log}; exit={proc.returncode}")
|
||||
fields = dict(FIELDS.findall(pipeline.group(1)))
|
||||
row = {
|
||||
"strate": strate, "workers": workers, "version": version,
|
||||
"phase": phase, "replicate": replicate, "source_stamp": stamp.group(1),
|
||||
"lod02_ready_s": float(near.group(3)),
|
||||
"whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": int(fields["whole_tiles"].split("/")[0]),
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L139FinalTimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def quiet_machine() -> tuple[float, float]:
|
||||
load_1m = os.getloadavg()[0]
|
||||
|
||||
def totals() -> tuple[int, int]:
|
||||
fields = Path("/proc/stat").read_text().splitlines()[0].split()
|
||||
values = [int(value) for value in fields[1:]]
|
||||
return sum(values), values[3] + values[4]
|
||||
|
||||
before = totals()
|
||||
time.sleep(2.0)
|
||||
after = totals()
|
||||
idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0])
|
||||
return load_1m, idle
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]):
|
||||
raise SystemExit("missing L139 timing input")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
load_1m, idle = quiet_machine()
|
||||
(OUT / "machine-load-before.txt").write_text(
|
||||
f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n")
|
||||
print(f"[L139FinalTimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True)
|
||||
if load_1m >= 1.5 or idle < 97.0:
|
||||
raise SystemExit("timing refused: machine is above the L80 quiet-load gate")
|
||||
|
||||
rows = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
for version in BINARIES:
|
||||
run(strate, workers, version, "warmup", 0)
|
||||
for replicate in range(1, 6):
|
||||
order = tuple(BINARIES)
|
||||
order = order[replicate % 3:] + order[:replicate % 3]
|
||||
if replicate % 2 == 0:
|
||||
order = tuple(reversed(order))
|
||||
for version in order:
|
||||
rows.append(run(strate, workers, version, "measured", replicate))
|
||||
(OUT / "runs-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
measured = [row for row in rows if row["phase"] == "measured"]
|
||||
summary = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
group = [row for row in measured
|
||||
if row["strate"] == strate and row["workers"] == workers]
|
||||
medians = {
|
||||
version: {
|
||||
key: statistics.median(row[key] for row in group
|
||||
if row["version"] == version)
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s")
|
||||
}
|
||||
for version in BINARIES
|
||||
}
|
||||
entry = {"strate": strate, "workers": workers,
|
||||
"replicates_per_version": 5, "medians": medians}
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
for baseline in ("fa4417a", "assembler-world"):
|
||||
entry[f"l139_vs_{baseline}_{key}_delta_pct"] = round(
|
||||
100.0 * (medians["l139"][key] / medians[baseline][key] - 1.0), 2)
|
||||
entry[f"l139_vs_fa4417a_{key}_paired_deltas_pct"] = [
|
||||
round(100.0 * (
|
||||
next(row[key] for row in group
|
||||
if row["version"] == "l139" and row["replicate"] == replicate)
|
||||
/ next(row[key] for row in group
|
||||
if row["version"] == "fa4417a" and row["replicate"] == replicate)
|
||||
- 1.0), 3) for replicate in range(1, 6)]
|
||||
entry[f"l139_vs_assembler-world_{key}_paired_deltas_pct"] = [
|
||||
round(100.0 * (
|
||||
next(row[key] for row in group
|
||||
if row["version"] == "l139" and row["replicate"] == replicate)
|
||||
/ next(row[key] for row in group
|
||||
if row["version"] == "assembler-world"
|
||||
and row["replicate"] == replicate) - 1.0), 3)
|
||||
for replicate in range(1, 6)]
|
||||
summary.append(entry)
|
||||
(OUT / "summary.json").write_text(json.dumps({
|
||||
"a_a_noise_floor_pct": 1.0,
|
||||
"fa4417a_commit": "fa4417a",
|
||||
"timing_protocol": (
|
||||
"one warmup per binary, five interleaved runs, W8/W12, all four owner layers"),
|
||||
"runs": rows,
|
||||
"summary": summary,
|
||||
}, indent=2) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the composed owner world, including the basal Vertical Shafts route."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l139/seeds"
|
||||
BIN = ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
STAMP = "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1"
|
||||
STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M)
|
||||
REQUIRED = {
|
||||
"passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M),
|
||||
"route": re.compile(
|
||||
r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 grade_failures=0 .*status=PASS$",
|
||||
re.M),
|
||||
"seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M),
|
||||
"shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M),
|
||||
"naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M),
|
||||
"mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M),
|
||||
"ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M),
|
||||
"shaft_dispatch": re.compile(
|
||||
r"^\[DensityDispatch\] strate=3 generator=VerticalShafts "
|
||||
r"recipe=Core\.Caves\.PrimitiveField version=1 "
|
||||
r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M),
|
||||
"shaft_blocked_route": re.compile(
|
||||
r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not BIN.is_file() or not DATA.is_file():
|
||||
raise SystemExit("missing L139 Clang binary or composed owner fixture")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
results = []
|
||||
partial = OUT / "runs-partial.json"
|
||||
if partial.is_file():
|
||||
for prior in json.loads(partial.read_text(encoding="utf-8")):
|
||||
seed = len(results)
|
||||
if prior.get("seed") != seed:
|
||||
break
|
||||
log = ROOT / prior.get("log", "")
|
||||
layout = ROOT / prior.get("layout", "")
|
||||
if not log.is_file() or not layout.is_file():
|
||||
break
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = STAMP_RE.search(content)
|
||||
checks = {name: bool(pattern.search(content))
|
||||
for name, pattern in REQUIRED.items()}
|
||||
checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP
|
||||
checks["blocked_passage_controls"] = len(re.findall(
|
||||
r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3
|
||||
checks["passage_count"] = len(re.findall(
|
||||
r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3
|
||||
passed = prior.get("exit") == 0 and all(checks.values())
|
||||
if checks != prior.get("checks") or passed != prior.get("passed"):
|
||||
break
|
||||
results.append(prior)
|
||||
print(f"[L139SeedSweepResume] reused={len(results)} source_stamp={STAMP}",
|
||||
flush=True)
|
||||
for seed in range(len(results), 16):
|
||||
log = OUT / f"seed{seed}.log"
|
||||
layout = OUT / f"seed{seed}.json"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}",
|
||||
"--recipe=surface-world", "--density-dispatch-report",
|
||||
"--audit-resolved-layout-passages", "--audit-passage-tile-seams",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
proc = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = STAMP_RE.search(content)
|
||||
checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()}
|
||||
checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP
|
||||
checks["blocked_passage_controls"] = len(re.findall(
|
||||
r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3
|
||||
checks["passage_count"] = len(re.findall(
|
||||
r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3
|
||||
passed = proc.returncode == 0 and all(checks.values())
|
||||
row = {"seed": seed, "exit": proc.returncode, "passed": passed,
|
||||
"checks": checks, "elapsed_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))}
|
||||
results.append(row)
|
||||
print("[L139SeedSweep] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
partial.write_text(json.dumps(results, indent=2) + "\n")
|
||||
passed_count = sum(row["passed"] for row in results)
|
||||
failed_count = len(results) - passed_count
|
||||
summary = {"source_stamp": STAMP, "seeds": list(range(len(results))),
|
||||
"passed": passed_count, "failed": failed_count, "results": results}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
|
||||
print(f"[L139SeedSweepSummary] passed={passed_count}/16 failed={failed_count} "
|
||||
f"status={'PASS' if len(results)==16 and failed_count==0 else 'FAIL'}")
|
||||
return 0 if len(results) == 16 and failed_count == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a coarse whole-strate cutaway from the L139 density mesh."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BIN = ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang"
|
||||
RENDERER = ROOT / "Saved/Linux/l139/bin/VoxelForgeOffscreenCapture-l139-clang"
|
||||
OUT = ROOT / "Saved/Linux/l139/captures"
|
||||
ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux139/captures"
|
||||
STAMP = "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1"
|
||||
STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M)
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
SLICE_Y = -5.0
|
||||
CAMERA = (-450.0, SLICE_Y - 100.0, -950.0)
|
||||
YAW, PITCH, FOV = 0.0, 0.0, 70.0
|
||||
|
||||
|
||||
def stamped_run(label: str, command: list[str], log: Path, env: dict[str, str]) -> str:
|
||||
probe = subprocess.run([*LIMIT, "timeout", "15s", str(command[0]), "--print-source-stamp"],
|
||||
cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=20)
|
||||
match = STAMP_RE.search(probe.stdout or "")
|
||||
if probe.returncode != 0 or not match or match.group(1) != STAMP:
|
||||
raise RuntimeError(f"{label}: source stamp mismatch")
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
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)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
emitted = STAMP_RE.search(content)
|
||||
if result.returncode != 0 or not emitted or emitted.group(1) != STAMP:
|
||||
raise RuntimeError(f"{label}: process failed or source changed; see {log}")
|
||||
print(f"[L139WholeLayerCapture] step={label} elapsed_s={time.monotonic()-started:.1f} "
|
||||
f"log={log.relative_to(ROOT)}", flush=True)
|
||||
return content
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not DATA.is_file() or not BIN.is_file() or not RENDERER.is_file():
|
||||
raise SystemExit("missing owner fixture or final L139 capture binaries")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
stem = "whole-layer-density-cutaway"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
layout = OUT / f"{stem}-layout.json"
|
||||
camera_text = ",".join(f"{value:.6f}" for value in (*CAMERA, YAW, PITCH))
|
||||
export = [BIN, DATA, "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--workers=12", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", "--force-generate", "--seed=0", "--recipe=surface-world",
|
||||
"--render-distance-chunks=0", "--capture-skip-walkability", "--density-dispatch-report",
|
||||
f"--camera={camera_text}", f"--resolved-layout-output={layout}", f"--mesh-export={mesh}"]
|
||||
content = stamped_run("export", export, OUT / f"{stem}-export.log", env)
|
||||
dispatch = re.search(
|
||||
r"^\[DensityDispatch\] strate=3 generator=VerticalShafts "
|
||||
r"recipe=Core\.Caves\.PrimitiveField version=1 "
|
||||
r"bound_path=Caves-native adapter=FCavesFieldAdapter$", content, re.M)
|
||||
if not dispatch or not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError("coarse density export is empty or lacks native shaft dispatch")
|
||||
png = OUT / f"{stem}.png"
|
||||
jpg = ARTIFACTS / "after-whole-layer-density-cutaway.jpg"
|
||||
render = [RENDERER, mesh, png, "--size=2200x320", f"--fov-deg={FOV:.1f}",
|
||||
"--camera-m=" + ",".join(f"{value:.6f}" for value in CAMERA),
|
||||
f"--yaw-deg={YAW:.6f}", f"--pitch-deg={PITCH:.6f}",
|
||||
f"--clip-plane=0,-1,0,{SLICE_Y:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=off", "--height-center-m=-950",
|
||||
"--height-range-m=80", "--tile-edges=off"]
|
||||
stamped_run("render", render, OUT / f"{stem}-render.log", env)
|
||||
subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "3", jpg],
|
||||
cwd=ROOT, env=env, check=True, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT, timeout=125)
|
||||
quality = subprocess.run([*LIMIT, "timeout", "120s", sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", jpg], cwd=ROOT, env=env,
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
check=False, timeout=125)
|
||||
quality_line = quality.stdout.strip().splitlines()[-1] if quality.stdout.strip() else ""
|
||||
ok = quality.returncode == 0 and "status=PASS" in quality_line
|
||||
manifest_path = ARTIFACTS / "manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.is_file() else []
|
||||
manifest = [row for row in manifest if row.get("view") != "whole-layer-density-cutaway"]
|
||||
manifest.append({"phase": "after", "view": "whole-layer-density-cutaway",
|
||||
"file": str(jpg.relative_to(ROOT)), "bytes": jpg.stat().st_size,
|
||||
"camera_m": CAMERA, "target_m": (-450.0, SLICE_Y, -950.0),
|
||||
"yaw_deg": YAW, "pitch_deg": PITCH, "fov_deg": FOV,
|
||||
"clip_plane": [0.0, -1.0, 0.0, SLICE_Y],
|
||||
"note": "outside side cutaway through the coarse LOD density mesh across the whole Vertical Shafts strate at y=-5 m",
|
||||
"quality_status": "PASS" if ok else "FAIL", "quality": quality_line})
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L139WholeLayerCaptureSummary] file={jpg.relative_to(ROOT)} "
|
||||
f"quality={'PASS' if ok else 'FAIL'} {quality_line}", flush=True)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1800" height="760" viewBox="0 0 1800 760">
|
||||
<rect width="1800" height="760" fill="#07111a"/>
|
||||
<text x="72" y="42" fill="#edf6fb" font-family="sans-serif" font-size="24">Vertical Shafts — whole-layer side elevation</text>
|
||||
<text x="72" y="64" fill="#9bb5c5" font-family="sans-serif" font-size="13">Resolved plan • seed 0 • strate 3 • vertical axis and route shown in metres • Y projected out</text>
|
||||
<line x1="72.0" y1="-1440.6" x2="1728.0" y2="-1440.6" stroke="#315060" stroke-dasharray="5 8"/>
|
||||
<rect x="1707.84" y="284.18" width="4.65" height="191.65" rx="2.33" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1667.56" y="288.45" width="5.72" height="187.37" rx="2.86" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1245.04" y="344.01" width="6.24" height="131.81" rx="3.12" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1206.94" y="349.61" width="5.68" height="126.22" rx="2.84" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1188.40" y="352.41" width="4.37" height="123.41" rx="2.18" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1168.07" y="355.22" width="6.64" height="120.60" rx="3.32" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1130.81" y="360.85" width="4.40" height="114.97" rx="2.20" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1092.31" y="366.48" width="4.62" height="109.35" rx="2.31" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1072.90" y="369.29" width="5.04" height="106.54" rx="2.52" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1053.43" y="372.09" width="5.61" height="103.74" rx="2.80" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1034.94" y="374.88" width="4.20" height="100.94" rx="2.10" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1015.20" y="377.67" width="5.28" height="98.16" rx="2.64" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1648.28" y="290.59" width="5.89" height="185.23" rx="2.95" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="995.75" y="380.44" width="5.81" height="95.38" rx="2.90" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="976.77" y="383.20" width="5.37" height="92.62" rx="2.68" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="938.19" y="388.67" width="5.75" height="87.15" rx="2.88" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="918.75" y="391.38" width="6.25" height="84.44" rx="3.13" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="896.89" y="394.44" width="6.22" height="81.38" rx="3.11" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="880.62" y="396.74" width="5.74" height="79.09" rx="2.87" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="862.36" y="399.38" width="3.87" height="76.45" rx="1.94" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="842.60" y="401.99" width="5.01" height="73.83" rx="2.50" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="804.64" y="407.14" width="4.16" height="68.69" rx="2.08" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="785.14" y="409.66" width="4.77" height="66.16" rx="2.39" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1629.77" y="292.79" width="4.53" height="183.04" rx="2.26" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="765.24" y="412.16" width="6.18" height="63.67" rx="3.09" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="746.73" y="414.61" width="4.82" height="61.21" rx="2.41" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="727.40" y="417.04" width="5.08" height="58.79" rx="2.54" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="707.91" y="419.42" width="5.68" height="56.41" rx="2.84" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="669.16" y="424.06" width="6.41" height="51.77" rx="3.20" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="650.97" y="426.31" width="4.40" height="49.51" rx="2.20" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="593.65" y="432.79" width="3.89" height="43.04" rx="1.94" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="573.84" y="434.85" width="5.11" height="40.98" rx="2.56" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="478.04" y="444.31" width="4.78" height="31.52" rx="2.39" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="440.09" y="447.66" width="3.90" height="28.16" rx="1.95" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="1590.94" y="297.31" width="5.40" height="178.52" rx="2.70" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rect x="420.88" y="449.24" width="3.93" height="26.58" rx="1.97" fill="#93dded" fill-opacity="0.45" stroke="#42b8d1" stroke-width="1.2"/><rLine truncated
|
||||
<path d="M1728.00,290.01 L1716.48,291.19 L1716.48,291.19 L1710.17,291.85 L1710.17,291.85 L1689.61,294.04 L1689.61,294.04 L1670.42,296.13 L1670.42,296.13 L1651.23,298.27 L1651.23,298.27 L1632.03,300.46 L1632.03,300.46 L1612.84,302.70 L1612.84,302.70 L1593.65,304.98 L1593.65,304.98 L1574.45,307.31 L1574.45,307.31 L1555.26,309.68 L1555.26,309.68 L1536.07,312.09 L1536.07,312.09 L1516.87,314.53 L1516.87,314.53 L1497.68,317.01 L1497.68,317.01 L1478.49,319.53 L1478.49,319.53 L1459.29,322.07 L1459.29,322.07 L1440.10,324.65 L1440.10,324.65 L1420.91,327.26 L1420.91,327.26 L1401.71,329.89 L1401.71,329.89 L1382.52,332.55 L1382.52,332.55 L1363.33,335.23 L1363.33,335.23 L1344.13,337.93 L1344.13,337.93 L1324.94,340.65 L1324.94,340.65 L1305.74,343.39 L1305.74,343.39 L1286.55,346.14 L1286.55,346.14 L1267.36,348.91 L1267.36,348.91 L1248.16,351.69 L1248.16,351.69 L1228.97,354.48 L1228.97,354.48 L1209.78,357.28 L1209.78,357.28 L1190.58,360.09 L1190.58,360.09 L1171.39,362.90 L1171.39,362.90 L1152.20,365.71 L1152.20,365.71 L1133.00,368.53 L1133.00,368.53 L1113.81,371.34 L1113.81,371.34 L1094.62,374.16 L1094.62,374.16 L1075.42,376.97 L1075.42,376.97 L1056.23,379.77 L1056.23,379.77 L1037.04,382.56 L1037.04,382.56 L1017.84,385.35 L1017.84,385.35 L998.65,388.12 L998.65,388.12 L979.46,390.88 L979.46,390.88 L960.26,393.62 L960.26,393.62 L941.07,396.35 L941.07,396.35 L921.88,399.06 L921.88,399.06 L902.68,401.75 L902.68,401.75 L900.00,402.12 L900.00,402.12 L883.49,404.41 L883.49,404.41 L864.30,407.05 L864.30,407.05 L845.10,409.67 L845.10,409.67 L825.91,412.26 L825.91,412.26 L806.72,414.82 L806.72,414.82 L787.52,417.34 L787.52,417.34 L768.33,419.83 L768.33,419.83 L749.14,422.29 L749.14,422.29 L729.94,424.71 L729.94,424.71 L710.75,427.09 L710.75,427.09 L691.56,429.44 L691.56,429.44 L672.36,431.73 L672.36,431.73 L653.17,433.99 L653.17,433.99 L633.98,436.20 L633.98,436.20 L614.78,438.36 L614.78,438.36 L595.59,440.47 L595.59,440.47 L576.39,442.53 L576.39,442.53 L557.20,444.53 L557.20,444.53 L538.01,446.48 L538.01,446.48 L518.81,448.37 L518.81,448.37 L499.62,450.21 L499.62,450.21 L480.43,451.98 L480.43,451.98 L461.23,453.69 L461.23,453.69 L442.04,455.34 L442.04,455.34 L422.85,456.92 L422.85,456.92 L403.65,458.43 L403.65,458.43 L384.46,459.88 L384.46,459.88 L365.27,461.25 L365.27,461.25 L346.07,462.55 L346.07,462.55 L326.88,463.77 L326.88,463.77 L307.69,464.91 L307.69,464.91 L288.49,465.98 L288.49,465.98 L269.30,466.97 L269.30,466.97 L250.11,467.87 L250.11,467.87 L230.91,468.69 L230.91,468.69 L211.72,469.42 L211.72,469.42 L192.53,470.06 L192.53,470.06 L173.33,470.62 L173.33,470.62 L154.14,471.08 L154.14,471.08 L134.95,471.45 L134.95,471.45 L115.75,471.73 L115.75,471.73 L96.56,471.90 L96.56,471.90 L89.83,471.94 L89.83,471.94 L83.52,471.97 L83.52,471.97 L77.37,471.98 L77.37,471.98 L72.00,471.99" fill="none" stroke="#f4bf68" stroke-width="3.0" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="82" y="716" fill="#f4bf68" font-family="sans-serif" font-size="15">walk-only graded spine and landings</text>
|
||||
<rect x="416" y="704" width="15" height="15" fill="#93dded" fill-opacity="0.65" stroke="#42b8d1"/>
|
||||
<text x="440" y="716" fill="#b8dce5" font-family="sans-serif" font-size="15">open vertical shaft primitives (53)</text>
|
||||
<text x="1718" y="716" fill="#9bb5c5" text-anchor="end" font-family="sans-serif" font-size="12">Plan projection, not a density mesh</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,258 @@
|
||||
[
|
||||
{
|
||||
"phase": "before",
|
||||
"view": "arrival-mouth",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg",
|
||||
"bytes": 21788,
|
||||
"camera_m": [
|
||||
-20.0625,
|
||||
-5.068359,
|
||||
-900.237499
|
||||
],
|
||||
"target_m": [
|
||||
-30.0625,
|
||||
-5.068359,
|
||||
-900.237499
|
||||
],
|
||||
"yaw_deg": -90.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "player eye at the passage arrival, looking into the connected layer",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg pixels=921600 mean_luma=110.102 stddev_luma=3.380 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"view": "landing-at-shaft-access",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg",
|
||||
"bytes": 15114,
|
||||
"camera_m": [
|
||||
-31.354577,
|
||||
-5.068359,
|
||||
-901.1982009999999
|
||||
],
|
||||
"target_m": [
|
||||
-31.354577,
|
||||
-9.889916,
|
||||
-901.1982009999999
|
||||
],
|
||||
"yaw_deg": 180.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "walkable route landing and short shaft access tunnel",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg pixels=921600 mean_luma=110.563 stddev_luma=3.056 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"view": "looking-down-shaft",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg",
|
||||
"bytes": 11005,
|
||||
"camera_m": [
|
||||
-31.354577,
|
||||
-9.889916,
|
||||
-901.1982009999999
|
||||
],
|
||||
"target_m": [
|
||||
-31.354577,
|
||||
-10.852462,
|
||||
-928.6482010000001
|
||||
],
|
||||
"yaw_deg": 180.0,
|
||||
"pitch_deg": -87.99172181188044,
|
||||
"fov_deg": 35.0,
|
||||
"note": "player eye at the upper access landing, looking down the open shaft",
|
||||
"quality_status": "FAIL",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg pixels=921600 mean_luma=112.000 stddev_luma=0.000 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL"
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"view": "looking-up-from-ledge",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg",
|
||||
"bytes": 15507,
|
||||
"camera_m": [
|
||||
-31.354577,
|
||||
-10.852462,
|
||||
-945.198201
|
||||
],
|
||||
"target_m": [
|
||||
-31.354577,
|
||||
-10.852462,
|
||||
-900.6482010000001
|
||||
],
|
||||
"yaw_deg": 0.0,
|
||||
"pitch_deg": 90.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "player eye at an internal ledge, looking up; reaching this ledge is a climb tier",
|
||||
"quality_status": "FAIL",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg pixels=921600 mean_luma=111.693 stddev_luma=1.515 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL"
|
||||
},
|
||||
{
|
||||
"phase": "before",
|
||||
"view": "basal-terminal-landing",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg",
|
||||
"bytes": 13933,
|
||||
"camera_m": [
|
||||
-884.858259,
|
||||
-5.068359,
|
||||
-995.05
|
||||
],
|
||||
"target_m": [
|
||||
-892.858259,
|
||||
-5.068359,
|
||||
-995.05
|
||||
],
|
||||
"yaw_deg": -90.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "walkable terminal landing above the basal seal",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg pixels=921600 mean_luma=108.996 stddev_luma=6.932 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "arrival-mouth",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg",
|
||||
"bytes": 19044,
|
||||
"camera_m": [
|
||||
-20.0625,
|
||||
-5.068359,
|
||||
-900.237499
|
||||
],
|
||||
"target_m": [
|
||||
-30.0625,
|
||||
-5.068359,
|
||||
-900.237499
|
||||
],
|
||||
"yaw_deg": -90.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "player eye at the passage arrival, looking into the connected layer",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg pixels=921600 mean_luma=125.004 stddev_luma=16.734 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "landing-at-shaft-access",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg",
|
||||
"bytes": 15595,
|
||||
"camera_m": [
|
||||
-31.354577,
|
||||
-5.068359,
|
||||
-901.1982009999999
|
||||
],
|
||||
"target_m": [
|
||||
-31.354577,
|
||||
-9.889916,
|
||||
-901.1982009999999
|
||||
],
|
||||
"yaw_deg": 180.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "walkable route landing and short shaft access tunnel",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg pixels=921600 mean_luma=117.622 stddev_luma=14.149 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "looking-down-shaft",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg",
|
||||
"bytes": 24396,
|
||||
"camera_m": [
|
||||
-31.354577,
|
||||
-9.889916,
|
||||
-901.1982009999999
|
||||
],
|
||||
"target_m": [
|
||||
-31.354577,
|
||||
-10.852462,
|
||||
-928.6482010000001
|
||||
],
|
||||
"yaw_deg": 180.0,
|
||||
"pitch_deg": -87.99172181188044,
|
||||
"fov_deg": 35.0,
|
||||
"note": "player eye at the upper access landing, looking down the open shaft",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg pixels=921600 mean_luma=112.365 stddev_luma=3.113 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "looking-up-from-ledge",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg",
|
||||
"bytes": 23172,
|
||||
"camera_m": [
|
||||
-31.354577,
|
||||
-10.852462,
|
||||
-945.198201
|
||||
],
|
||||
"target_m": [
|
||||
-31.354577,
|
||||
-10.852462,
|
||||
-900.6482010000001
|
||||
],
|
||||
"yaw_deg": 0.0,
|
||||
"pitch_deg": 90.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "player eye at an internal ledge, looking up; reaching this ledge is a climb tier",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg pixels=921600 mean_luma=111.549 stddev_luma=2.538 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "basal-terminal-landing",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg",
|
||||
"bytes": 12527,
|
||||
"camera_m": [
|
||||
-884.858259,
|
||||
-5.068359,
|
||||
-995.05
|
||||
],
|
||||
"target_m": [
|
||||
-892.858259,
|
||||
-5.068359,
|
||||
-995.05
|
||||
],
|
||||
"yaw_deg": -90.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 78.0,
|
||||
"note": "walkable terminal landing above the basal seal",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg pixels=921600 mean_luma=125.238 stddev_luma=3.555 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "whole-layer-side-elevation",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-side-elevation.svg",
|
||||
"shafts": 53,
|
||||
"walk_route_edges": 91,
|
||||
"note": "resolved-plan side projection; not a density-mesh capture"
|
||||
},
|
||||
{
|
||||
"phase": "after",
|
||||
"view": "whole-layer-density-cutaway",
|
||||
"file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg",
|
||||
"bytes": 20280,
|
||||
"camera_m": [
|
||||
-450.0,
|
||||
-105.0,
|
||||
-950.0
|
||||
],
|
||||
"target_m": [
|
||||
-450.0,
|
||||
-5.0,
|
||||
-950.0
|
||||
],
|
||||
"yaw_deg": 0.0,
|
||||
"pitch_deg": 0.0,
|
||||
"fov_deg": 70.0,
|
||||
"clip_plane": [
|
||||
0.0,
|
||||
-1.0,
|
||||
0.0,
|
||||
-5.0
|
||||
],
|
||||
"note": "outside side cutaway through the coarse LOD density mesh across the whole Vertical Shafts strate at the denser shaft row",
|
||||
"quality_status": "PASS",
|
||||
"quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg pixels=704000 mean_luma=113.286 stddev_luma=3.364 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS"
|
||||
}
|
||||
]
|
||||
@@ -5147,6 +5147,7 @@ namespace StandaloneStreamBenchmark
|
||||
}
|
||||
std::uint64_t routePoints=0,routePasses=0,routeWalkPasses=0,routeBindings=0,routeBindingFailures=0;
|
||||
std::uint64_t tunnelNetworkRoutes=0,tunnelNetworkBlockedControls=0;
|
||||
std::uint64_t verticalShaftRoutes=0,verticalShaftBlockedControls=0;
|
||||
for(const auto& stratum:ResolvedLayout->strates)
|
||||
{
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutAnchorV2*> anchors;
|
||||
@@ -5206,7 +5207,8 @@ namespace StandaloneStreamBenchmark
|
||||
supportMaximumMetres=routeConstraints->supportSearchDepthSubvoxels*qMetres;
|
||||
}
|
||||
}
|
||||
if(stratum.generatorKind=="TunnelNetwork")
|
||||
if(stratum.generatorKind=="TunnelNetwork"
|
||||
||stratum.generatorKind=="VerticalShafts")
|
||||
{
|
||||
std::int64_t floorSupportDepth=0;
|
||||
for(const auto& edge:ResolvedLayout->cavernEdges)
|
||||
@@ -5297,9 +5299,12 @@ namespace StandaloneStreamBenchmark
|
||||
stratum.strateIndex,routeAudit.firstUnsupported[i][0],
|
||||
routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2],
|
||||
routeAudit.firstUnsupported[i][3]);
|
||||
if(stratum.generatorKind=="TunnelNetwork")
|
||||
if(stratum.generatorKind=="TunnelNetwork"
|
||||
||stratum.generatorKind=="VerticalShafts")
|
||||
{
|
||||
++tunnelNetworkRoutes;
|
||||
const bool verticalShafts=stratum.generatorKind=="VerticalShafts";
|
||||
if(verticalShafts)++verticalShaftRoutes;
|
||||
else ++tunnelNetworkRoutes;
|
||||
auto controlRoute=route;
|
||||
constexpr double OffBoreControlOffsetMetres=12.0;
|
||||
const std::size_t controlIndex=controlRoute.size()/2;
|
||||
@@ -5319,11 +5324,22 @@ namespace StandaloneStreamBenchmark
|
||||
supportStartMetres,supportStepMetres,supportMaximumMetres,
|
||||
minimumSupportProbes,supportProbeRadiusMetres);
|
||||
const bool detected=control.blocked>0&&!control.passed;
|
||||
tunnelNetworkBlockedControls+=detected;
|
||||
std::printf("[TunnelNetworkBlockedRouteControl] strate=%d mode=off_bore_lateral_deviation_m=%.1f blocked_capsule_samples=%llu result=%s expected=FAIL status=%s\n",
|
||||
stratum.strateIndex,OffBoreControlOffsetMetres,
|
||||
static_cast<unsigned long long>(control.blocked),
|
||||
control.passed?"PASS":"FAIL",detected?"PASS":"FAIL");
|
||||
if(verticalShafts)
|
||||
{
|
||||
verticalShaftBlockedControls+=detected;
|
||||
std::printf("[VerticalShaftsBlockedRouteControl] strate=%d mode=off_bore_lateral_deviation_m=%.1f blocked_capsule_samples=%llu result=%s expected=FAIL status=%s\n",
|
||||
stratum.strateIndex,OffBoreControlOffsetMetres,
|
||||
static_cast<unsigned long long>(control.blocked),
|
||||
control.passed?"PASS":"FAIL",detected?"PASS":"FAIL");
|
||||
}
|
||||
else
|
||||
{
|
||||
tunnelNetworkBlockedControls+=detected;
|
||||
std::printf("[TunnelNetworkBlockedRouteControl] strate=%d mode=off_bore_lateral_deviation_m=%.1f blocked_capsule_samples=%llu result=%s expected=FAIL status=%s\n",
|
||||
stratum.strateIndex,OffBoreControlOffsetMetres,
|
||||
static_cast<unsigned long long>(control.blocked),
|
||||
control.passed?"PASS":"FAIL",detected?"PASS":"FAIL");
|
||||
}
|
||||
}
|
||||
}
|
||||
for(std::size_t passageIndex=0;
|
||||
@@ -5392,8 +5408,9 @@ namespace StandaloneStreamBenchmark
|
||||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||||
&&routeBindingFailures==0
|
||||
&&tunnelNetworkRoutes==tunnelNetworkBlockedControls
|
||||
&&verticalShaftRoutes==verticalShaftBlockedControls
|
||||
&&blockedCompiled&&blockedControls==passageCount;
|
||||
std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu tunnel_network_block_controls=%llu/%llu blocked_controls=%llu/%llu status=%s\n",
|
||||
std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu tunnel_network_block_controls=%llu/%llu vertical_shaft_block_controls=%llu/%llu blocked_controls=%llu/%llu status=%s\n",
|
||||
static_cast<unsigned long long>(passageCount),static_cast<unsigned long long>(passagePasses),
|
||||
static_cast<unsigned long long>(naturalPasses),static_cast<unsigned long long>(passageCount),
|
||||
static_cast<unsigned long long>(routePasses),static_cast<unsigned long long>(routeWalkPasses),
|
||||
@@ -5401,6 +5418,8 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(routeBindings),static_cast<unsigned long long>(routeBindingFailures),
|
||||
static_cast<unsigned long long>(tunnelNetworkBlockedControls),
|
||||
static_cast<unsigned long long>(tunnelNetworkRoutes),
|
||||
static_cast<unsigned long long>(verticalShaftBlockedControls),
|
||||
static_cast<unsigned long long>(verticalShaftRoutes),
|
||||
static_cast<unsigned long long>(blockedControls),static_cast<unsigned long long>(passageCount),
|
||||
auditPassed?"PASS":"FAIL");
|
||||
if(!auditPassed)
|
||||
|
||||
@@ -1517,13 +1517,16 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
if(!owner.Manager)
|
||||
{error="Tunnel Network cave planner requires the initialized owner director";return false;}
|
||||
std::vector<const FStrateSlot*> tunnelSlots;
|
||||
{error="native cave planner requires the initialized owner director";return false;}
|
||||
std::vector<std::pair<const FStrateSlot*,bool>> plannerSlots;
|
||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition
|
||||
||owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ))
|
||||
!=ECaveGeneratorType::TunnelNetwork)continue;
|
||||
if(!slot.Definition)continue;
|
||||
const ECaveGeneratorType generatorType=
|
||||
owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ));
|
||||
const bool tunnelNetwork=generatorType==ECaveGeneratorType::TunnelNetwork;
|
||||
const bool verticalShafts=generatorType==ECaveGeneratorType::VerticalShafts;
|
||||
if(!tunnelNetwork&&!verticalShafts)continue;
|
||||
const bool hasIncomingMouth=std::any_of(layout.passages.begin(),
|
||||
layout.passages.end(),[&](const auto& passage)
|
||||
{return passage.lowerStrateIndex==slot.StrateIndex
|
||||
@@ -1532,24 +1535,27 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
layout.passages.end(),[&](const auto& passage)
|
||||
{return passage.upperStrateIndex==slot.StrateIndex
|
||||
&&passage.lowerStrateIndex>slot.StrateIndex;});
|
||||
// A native planner route requires two mouths. Boundary Tunnel Network
|
||||
// strates keep their legacy field until they have both connections.
|
||||
if(!hasIncomingMouth||!hasOutgoingMouth)continue;
|
||||
// Tunnel Network connects two passages. The basal Vertical Shafts
|
||||
// stratum is terminal: it inherits its arrival from the preceding
|
||||
// strate and intentionally has no departure passage.
|
||||
if((tunnelNetwork&&(!hasIncomingMouth||!hasOutgoingMouth))
|
||||
||(verticalShafts&&(!hasIncomingMouth||hasOutgoingMouth)))continue;
|
||||
auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& item){return item.strateIndex==slot.StrateIndex;});
|
||||
if(stratum==layout.strates.end())
|
||||
{error="Tunnel Network strate is missing from the resolved layout";return false;}
|
||||
stratum->generatorKind="TunnelNetwork";
|
||||
stratum->generatorKind=verticalShafts?"VerticalShafts":"TunnelNetwork";
|
||||
stratum->recipeId="Core.Caves.PrimitiveField";
|
||||
stratum->recipeVersion=1;
|
||||
stratum->stableId=layout.worldIdentity+"/strate/"
|
||||
+std::to_string(slot.StrateIndex)+"/generator/TunnelNetwork";
|
||||
+std::to_string(slot.StrateIndex)+"/generator/"
|
||||
+(verticalShafts?"VerticalShafts":"TunnelNetwork");
|
||||
const bool alreadyPlanned=std::any_of(layout.cavernRooms.begin(),
|
||||
layout.cavernRooms.end(),[&](const auto& room)
|
||||
{return room.strateIndex==slot.StrateIndex;})
|
||||
&&std::any_of(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||||
[&](const auto& edge){return edge.strateIndex==slot.StrateIndex;});
|
||||
if(!alreadyPlanned)tunnelSlots.push_back(&slot);
|
||||
if(!alreadyPlanned)plannerSlots.emplace_back(&slot,verticalShafts);
|
||||
}
|
||||
if(!layout.finalize(error))return false;
|
||||
const auto toMicrometres=[&](double ownerVoxels)
|
||||
@@ -1562,14 +1568,55 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
return static_cast<std::int32_t>(std::llround(std::clamp(
|
||||
static_cast<double>(value),0.0,1.0)*65536.0));
|
||||
};
|
||||
if(!tunnelSlots.empty())
|
||||
if(!plannerSlots.empty())
|
||||
{
|
||||
std::vector<ResolvedCavernRoomV1> plannedRooms=layout.cavernRooms;
|
||||
std::vector<ResolvedCavernEdgeV1> plannedEdges=layout.cavernEdges;
|
||||
for(const FStrateSlot* slot:tunnelSlots)
|
||||
for(const auto& [slot,verticalShafts]:plannerSlots)
|
||||
{
|
||||
const FStrateGenerationParams& params=slot->Definition->GenerationParams;
|
||||
ResolvedCavernPlannerSettingsV1 settings;
|
||||
if(verticalShafts)
|
||||
{
|
||||
const FVerticalShaftParams& shaft=slot->Definition->VerticalShaftParams;
|
||||
settings.verticalShaftMode=true;
|
||||
settings.roomSpacingMicrometres=std::clamp<std::int64_t>(
|
||||
toMicrometres(shaft.ShaftSpacing),4000000,100000000);
|
||||
settings.minimumRoomRadiusMicrometres=std::max<std::int64_t>(
|
||||
500000,toMicrometres(shaft.ShaftMinRadius));
|
||||
settings.maximumRoomRadiusMicrometres=std::max(
|
||||
settings.minimumRoomRadiusMicrometres,
|
||||
toMicrometres(shaft.ShaftMaxRadius));
|
||||
settings.destinationRadiusMicrometres=std::max(
|
||||
settings.maximumRoomRadiusMicrometres,
|
||||
settings.maximumRoomRadiusMicrometres*135/100);
|
||||
settings.minimumTunnelRadiusMicrometres=std::max<std::int64_t>(0,
|
||||
toMicrometres(shaft.ConnectorRadius));
|
||||
settings.maximumTunnelRadiusMicrometres=
|
||||
settings.minimumTunnelRadiusMicrometres;
|
||||
settings.maximumTunnelLengthMicrometres=std::clamp<std::int64_t>(
|
||||
settings.roomSpacingMicrometres*2,4000000,100000000);
|
||||
settings.smoothWidthMicrometres=1500000;
|
||||
settings.floorSupportDepthMicrometres=2000000;
|
||||
settings.irregularityAmplitudeMicrometres=std::max<std::int64_t>(0,
|
||||
toMicrometres(shaft.SurfaceRoughness));
|
||||
settings.irregularityWavelengthMicrometres=std::max<std::int64_t>(
|
||||
500000,static_cast<std::int64_t>(owner.OwnerVoxelSizeMicrons)*4);
|
||||
settings.roomDensityQ16=toQ16(shaft.ShaftDensity);
|
||||
settings.tunnelDensityQ16=toQ16(shaft.CrossConnectChance);
|
||||
settings.roomShapeVarietyQ16=0;
|
||||
settings.roomHeightRatioQ16=65536;
|
||||
settings.roomFloorCutMinimumQ16=0;
|
||||
settings.roomFloorCutMaximumQ16=0;
|
||||
settings.tunnelHorizontalBiasQ16=65536;
|
||||
settings.originRoomMaximumConnections=32;
|
||||
settings.ledgeSpacingMicrometres=std::max<std::int64_t>(0,
|
||||
toMicrometres(shaft.LedgeSpacing));
|
||||
settings.ledgeDepthMicrometres=std::max<std::int64_t>(0,
|
||||
toMicrometres(shaft.LedgeDepth));
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.roomSpacingMicrometres=std::max<std::int64_t>(4000000,
|
||||
toMicrometres(params.RoomSpacing));
|
||||
settings.minimumRoomRadiusMicrometres=std::max<std::int64_t>(500000,
|
||||
@@ -1607,6 +1654,7 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
settings.tunnelHorizontalBiasQ16=toQ16(params.TunnelHorizontalBias);
|
||||
settings.originRoomMaximumConnections=params.OriginRoomMaxConnections;
|
||||
settings.tunnelsFlowTowardOrigin=params.bTunnelsFlowTowardOrigin;
|
||||
}
|
||||
std::vector<ResolvedCavernRoomV1> rooms;
|
||||
std::vector<ResolvedCavernEdgeV1> edges;
|
||||
if(!PlanResolvedCavernNetworkV1(layout,owner.Seed,slot->StrateIndex,
|
||||
@@ -1629,24 +1677,27 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
constexpr std::int64_t PlayerCenterAboveFloorMicrometres=1130000;
|
||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition
|
||||
||owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ))
|
||||
!=ECaveGeneratorType::TunnelNetwork)continue;
|
||||
if(!slot.Definition)continue;
|
||||
const ECaveGeneratorType generatorType=
|
||||
owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ));
|
||||
const bool verticalShafts=generatorType==ECaveGeneratorType::VerticalShafts;
|
||||
if(generatorType!=ECaveGeneratorType::TunnelNetwork&&!verticalShafts)continue;
|
||||
auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& item){return item.strateIndex==slot.StrateIndex;});
|
||||
if(stratum==layout.strates.end()
|
||||
||!IsNativeCaveFieldStratum(*stratum))continue;
|
||||
const std::string routePrefix="tunnel-network/strate-"
|
||||
const std::string routePrefix=(verticalShafts?"vertical-shafts/strate-"
|
||||
:"tunnel-network/strate-")
|
||||
+std::to_string(slot.StrateIndex)+"/";
|
||||
const bool hasPlannedArrival=std::any_of(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& edge)
|
||||
{return edge.strateIndex==slot.StrateIndex
|
||||
&&edge.stableId==routePrefix+"route-edge-0";});
|
||||
const bool hasPlannedDeparture=std::any_of(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& edge)
|
||||
{return edge.strateIndex==slot.StrateIndex
|
||||
const bool hasPlannedDeparture=!verticalShafts
|
||||
&&std::any_of(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||||
[&](const auto& edge){return edge.strateIndex==slot.StrateIndex
|
||||
&&edge.stableId==routePrefix+"departure-edge";});
|
||||
if(!hasPlannedArrival||!hasPlannedDeparture)continue;
|
||||
if(!hasPlannedArrival||(!verticalShafts&&!hasPlannedDeparture))continue;
|
||||
// Route-point anchors also drive the passage-mouth floor blend. The
|
||||
// native cave planner replaces the legacy finite-strate route, so
|
||||
// retaining its old anchors would join the passage floor to a route
|
||||
@@ -1663,13 +1714,14 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
if(passage.upperStrateIndex==slot.StrateIndex
|
||||
&&passage.lowerStrateIndex>slot.StrateIndex)departure=&passage;
|
||||
}
|
||||
if(!arrival||!departure)
|
||||
{error="Tunnel Network route anchors cannot bind both passage mouths";return false;}
|
||||
if(!arrival||(!verticalShafts&&!departure))
|
||||
{error="native cave route anchors cannot bind their required passage mouths";return false;}
|
||||
const auto appendRoutePoint=[&](std::string id,
|
||||
const std::array<std::int64_t,3>& point)
|
||||
{
|
||||
ResolvedLayoutAnchorV2 anchor;anchor.kind="route_point";
|
||||
anchor.stableId="tunnel-network/route/"+std::move(id);
|
||||
anchor.stableId=(verticalShafts?"vertical-shafts/route/"
|
||||
:"tunnel-network/route/")+std::move(id);
|
||||
anchor.units="1/256 Core voxel";
|
||||
anchor.values={point[0],point[1],point[2]};
|
||||
stratum->anchors.push_back(std::move(anchor));
|
||||
@@ -1684,14 +1736,23 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
appendRoutePoint("000-arrival",start);
|
||||
for(std::size_t edgeIndex=0;edgeIndex<256;++edgeIndex)
|
||||
{
|
||||
const std::string stableId="tunnel-network/strate-"
|
||||
+std::to_string(slot.StrateIndex)+"/route-edge-"
|
||||
const std::string stableId=routePrefix+"route-edge-"
|
||||
+std::to_string(edgeIndex);
|
||||
const auto edge=std::find_if(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& value)
|
||||
{return value.strateIndex==slot.StrateIndex
|
||||
&&value.stableId==stableId;});
|
||||
if(edge==layout.cavernEdges.end())break;
|
||||
bool terminalRouteEdge=false;
|
||||
if(verticalShafts)
|
||||
{
|
||||
const std::string nextRouteEdge=routePrefix+"route-edge-"
|
||||
+std::to_string(edgeIndex+1);
|
||||
terminalRouteEdge=std::none_of(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& candidate)
|
||||
{return candidate.strateIndex==slot.StrateIndex
|
||||
&&candidate.stableId==nextRouteEdge;});
|
||||
}
|
||||
if(edgeIndex==0)
|
||||
{
|
||||
auto inner=edge->controlPointsMicrometres.front();
|
||||
@@ -1708,18 +1769,20 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
}
|
||||
auto point=edge->controlPointsMicrometres.back();
|
||||
point[2]+=PlayerCenterAboveFloorMicrometres;
|
||||
appendRoutePoint(std::to_string(edgeIndex+1),
|
||||
appendRoutePoint(terminalRouteEdge?"999-basal-terminal"
|
||||
:std::to_string(edgeIndex+1),
|
||||
{toCoreSubvoxels(point[0]),toCoreSubvoxels(point[1]),
|
||||
toCoreSubvoxels(point[2])});
|
||||
}
|
||||
const std::string departureEdgeId="tunnel-network/strate-"
|
||||
+std::to_string(slot.StrateIndex)+"/departure-edge";
|
||||
const auto departureEdge=std::find_if(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& value)
|
||||
{return value.strateIndex==slot.StrateIndex
|
||||
&&value.stableId==departureEdgeId;});
|
||||
if(departureEdge!=layout.cavernEdges.end())
|
||||
if(departure)
|
||||
{
|
||||
const std::string departureEdgeId=routePrefix+"departure-edge";
|
||||
const auto departureEdge=std::find_if(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& value)
|
||||
{return value.strateIndex==slot.StrateIndex
|
||||
&&value.stableId==departureEdgeId;});
|
||||
if(departureEdge==layout.cavernEdges.end())
|
||||
{error="native cave route lost its planned departure edge";return false;}
|
||||
auto inner=departureEdge->controlPointsMicrometres.front();
|
||||
inner[2]+=PlayerCenterAboveFloorMicrometres;
|
||||
const std::array<std::int64_t,3> innerPoint={
|
||||
@@ -1728,9 +1791,17 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
auto end=departure->upperMouthSubvoxels;
|
||||
if(!sameRoutePoint(innerPoint,end))
|
||||
appendRoutePoint("998-departure-inner",innerPoint);
|
||||
appendRoutePoint("999-departure",departure->upperMouthSubvoxels);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto terminal=std::find_if(layout.cavernRooms.begin(),
|
||||
layout.cavernRooms.end(),[&](const auto& room)
|
||||
{return room.strateIndex==slot.StrateIndex
|
||||
&&room.stableId==routePrefix+"basal-landing/primitive-0";});
|
||||
if(terminal==layout.cavernRooms.end())
|
||||
{error="Vertical Shafts basal route is missing its terminal landing";return false;}
|
||||
}
|
||||
auto end=departure->upperMouthSubvoxels;
|
||||
appendRoutePoint("999-departure",end);
|
||||
}
|
||||
return layout.finalize(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173
|
||||
[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55
|
||||
[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a
|
||||
[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630
|
||||
[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1
|
||||
[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a
|
||||
[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494
|
||||
[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573
|
||||
[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618
|
||||
[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b
|
||||
[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04
|
||||
[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries
|
||||
@@ -0,0 +1,12 @@
|
||||
[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae
|
||||
[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7
|
||||
[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3
|
||||
[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100
|
||||
[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809
|
||||
[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733
|
||||
[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08
|
||||
[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89
|
||||
[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64
|
||||
[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72
|
||||
[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335
|
||||
[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries
|
||||
@@ -125,11 +125,11 @@ RECIPE_RE = re.compile(
|
||||
|
||||
def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]:
|
||||
if recipe == "surface-world" and style == 1:
|
||||
return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v236.baseline",
|
||||
"L135-v236-cave-mouth-joints")
|
||||
return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline",
|
||||
"L139-v237-vertical-shafts-core-planner")
|
||||
if recipe == "floating-islands" and style == 2:
|
||||
return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v236.baseline",
|
||||
"L135-v236-cave-mouth-joints")
|
||||
return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline",
|
||||
"L139-v237-vertical-shafts-core-planner")
|
||||
raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")
|
||||
|
||||
|
||||
|
||||