Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3c8e3687d | ||
|
|
4a97023b24 | ||
|
|
379093401e | ||
|
|
75c6ad251d |
No files matched your search
@@ -119,6 +119,48 @@ namespace VoxelForgeCore::PassageSpline
|
|||||||
return std::lerp(plannedFloor,receivingFloor,blend);
|
return std::lerp(plannedFloor,receivingFloor,blend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void BlendEndpointFloorToRoute(std::span<const FPoint> controls,
|
||||||
|
std::vector<double>& floorProfile,std::size_t endpoint,
|
||||||
|
std::span<const std::array<double,3>> receivingRoute,double unitsPerMetre,
|
||||||
|
double playerCenterAboveFloorMetres=1.13,double maximumGradeDegrees=12.0)
|
||||||
|
{
|
||||||
|
if(controls.size()!=floorProfile.size()||endpoint>=floorProfile.size()
|
||||||
|
||receivingRoute.empty()||!(unitsPerMetre>0.0)
|
||||||
|
||!(maximumGradeDegrees>0.0&&maximumGradeDegrees<45.0))return;
|
||||||
|
const double maximumGrade=std::tan(maximumGradeDegrees
|
||||||
|
*3.14159265358979323846/180.0);
|
||||||
|
const double routeReach=controls[endpoint][3]/unitsPerMetre+0.5;
|
||||||
|
constexpr double MouthProfileBlendDistanceMetres=10.0;
|
||||||
|
double routeHeight=0.0,routeDistanceSquared=0.0;
|
||||||
|
if(!ClosestRouteHeightAtXY(receivingRoute,controls[endpoint][0],
|
||||||
|
controls[endpoint][1],routeHeight,routeDistanceSquared)
|
||||||
|
||routeDistanceSquared>std::pow(routeReach*unitsPerMetre,2.0))return;
|
||||||
|
floorProfile[endpoint]=routeHeight
|
||||||
|
-playerCenterAboveFloorMetres*unitsPerMetre;
|
||||||
|
double pathDistance=0.0;
|
||||||
|
for(std::size_t step=1;step<controls.size();++step)
|
||||||
|
{
|
||||||
|
if(endpoint>0&&step>endpoint)break;
|
||||||
|
const std::size_t index=endpoint==0?step:endpoint-step;
|
||||||
|
const std::size_t previous=endpoint==0?index-1:index+1;
|
||||||
|
const double dx=(controls[index][0]-controls[previous][0])/unitsPerMetre;
|
||||||
|
const double dy=(controls[index][1]-controls[previous][1])/unitsPerMetre;
|
||||||
|
const double horizontal=std::hypot(dx,dy);
|
||||||
|
pathDistance+=horizontal;
|
||||||
|
double localRouteHeight=0.0,localRouteDistanceSquared=0.0;
|
||||||
|
const bool followsRoute=pathDistance<=MouthProfileBlendDistanceMetres
|
||||||
|
&&ClosestRouteHeightAtXY(receivingRoute,
|
||||||
|
controls[index][0],controls[index][1],localRouteHeight,localRouteDistanceSquared)
|
||||||
|
&&localRouteDistanceSquared<=std::pow(routeReach*unitsPerMetre,2.0);
|
||||||
|
double target=floorProfile[index];
|
||||||
|
if(followsRoute)
|
||||||
|
target=localRouteHeight-playerCenterAboveFloorMetres*unitsPerMetre;
|
||||||
|
const double maximumStep=maximumGrade*horizontal*unitsPerMetre;
|
||||||
|
floorProfile[index]=std::clamp(target,
|
||||||
|
floorProfile[previous]-maximumStep,floorProfile[previous]+maximumStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
|
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
|
||||||
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
|
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
|
||||||
std::span<const double> plannedFloorProfile={},
|
std::span<const double> plannedFloorProfile={},
|
||||||
|
|||||||
@@ -496,8 +496,10 @@ namespace VoxelForgeCore
|
|||||||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
||||||
// Version 226 unifies the passage influence reach in metres and converts it to core
|
// Version 226 unifies the passage influence reach in metres and converts it to core
|
||||||
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
||||||
// support width/6 when deriving that reach.
|
// support width/6 when deriving that reach. Version 228 composes deterministic cave
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
|
// graphs from fixed passage mouths and the owner Tunnel Network settings. Version 229
|
||||||
|
// budgets cave floor support; 230 records a shared backbone floor profile and joint cap.
|
||||||
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 230;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
@@ -892,6 +894,7 @@ namespace VoxelForgeCore
|
|||||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV2 = 2;
|
||||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||||
|
|
||||||
@@ -956,6 +959,11 @@ namespace VoxelForgeCore
|
|||||||
std::array<std::int64_t, 3> centerMicrometres{};
|
std::array<std::int64_t, 3> centerMicrometres{};
|
||||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||||
std::int64_t floorMicrometres = 0;
|
std::int64_t floorMicrometres = 0;
|
||||||
|
// Optional shared backbone profile reference. The room floor plane is
|
||||||
|
// floorMicrometres plus this Q16 XY gradient from centerMicrometres.
|
||||||
|
std::string floorProfileStableId;
|
||||||
|
std::int64_t floorProfileDistanceMicrometres = -1;
|
||||||
|
std::array<std::int32_t, 2> floorGradientQ16{};
|
||||||
std::int64_t smoothWidthMicrometres = 0;
|
std::int64_t smoothWidthMicrometres = 0;
|
||||||
// Optional feature-to-feature union width. Zero preserves the historical
|
// Optional feature-to-feature union width. Zero preserves the historical
|
||||||
// behavior where smoothWidthMicrometres also controls the graph union.
|
// behavior where smoothWidthMicrometres also controls the graph union.
|
||||||
@@ -975,6 +983,11 @@ namespace VoxelForgeCore
|
|||||||
std::int32_t strateIndex = -1;
|
std::int32_t strateIndex = -1;
|
||||||
// A single cubic Bezier in XY. Its four Z values are the cubic floor profile.
|
// A single cubic Bezier in XY. Its four Z values are the cubic floor profile.
|
||||||
std::array<std::array<std::int64_t, 3>, 4> controlPointsMicrometres{};
|
std::array<std::array<std::int64_t, 3>, 4> controlPointsMicrometres{};
|
||||||
|
// Edges with the same profile ID are adjacent pieces of one shared h(s).
|
||||||
|
// Distances are signed along that profile, so a terminal edge may be reversed.
|
||||||
|
std::string floorProfileStableId;
|
||||||
|
std::int64_t floorProfileStartDistanceMicrometres = -1;
|
||||||
|
std::int64_t floorProfileEndDistanceMicrometres = -1;
|
||||||
// Resolve-time, error-bounded approximation of the cubic centerline. Each
|
// Resolve-time, error-bounded approximation of the cubic centerline. Each
|
||||||
// consecutive pair is a physical segment used by the native cave field.
|
// consecutive pair is a physical segment used by the native cave field.
|
||||||
std::vector<std::array<std::int64_t, 3>> polylineMicrometres;
|
std::vector<std::array<std::int64_t, 3>> polylineMicrometres;
|
||||||
@@ -991,6 +1004,37 @@ namespace VoxelForgeCore
|
|||||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Inputs to the reusable, plan-time cave graph composer. All distances are
|
||||||
|
// integer micrometres and unit-interval values are Q16 so the resolved graph
|
||||||
|
// does not depend on worker count or floating-point random distributions.
|
||||||
|
struct ResolvedCavernPlannerSettingsV1
|
||||||
|
{
|
||||||
|
std::int64_t roomSpacingMicrometres = 16000000;
|
||||||
|
std::int64_t minimumRoomRadiusMicrometres = 2000000;
|
||||||
|
std::int64_t maximumRoomRadiusMicrometres = 5000000;
|
||||||
|
std::int64_t destinationRadiusMicrometres = 6000000;
|
||||||
|
// Solid floor backing below the planned edge floor, preserved after graph
|
||||||
|
// unions and passage composition. This is separate from bore height.
|
||||||
|
std::int64_t floorSupportDepthMicrometres = 750000;
|
||||||
|
std::int64_t minimumTunnelRadiusMicrometres = 750000;
|
||||||
|
std::int64_t maximumTunnelRadiusMicrometres = 1000000;
|
||||||
|
std::int64_t maximumTunnelLengthMicrometres = 45000000;
|
||||||
|
std::int64_t tunnelWarpMicrometres = 0;
|
||||||
|
std::int64_t caveWarpMicrometres = 0;
|
||||||
|
std::int64_t smoothWidthMicrometres = 500000;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::int32_t roomDensityQ16 = 22938;
|
||||||
|
std::int32_t tunnelDensityQ16 = 26214;
|
||||||
|
std::int32_t roomShapeVarietyQ16 = 32768;
|
||||||
|
std::int32_t roomHeightRatioQ16 = 55706;
|
||||||
|
std::int32_t roomFloorCutMinimumQ16 = 45875;
|
||||||
|
std::int32_t roomFloorCutMaximumQ16 = 58982;
|
||||||
|
std::int32_t tunnelHorizontalBiasQ16 = 32768;
|
||||||
|
std::int32_t originRoomMaximumConnections = 4;
|
||||||
|
bool tunnelsFlowTowardOrigin = true;
|
||||||
|
};
|
||||||
|
|
||||||
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
||||||
// micrometres; bounds include irregularity and the blend envelope.
|
// micrometres; bounds include irregularity and the blend envelope.
|
||||||
struct ResolvedLayoutOreVeinRequestV1
|
struct ResolvedLayoutOreVeinRequestV1
|
||||||
@@ -1095,7 +1139,7 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||||
{
|
{
|
||||||
return stratum.generatorKind=="Caves"
|
return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork")
|
||||||
&&stratum.recipeId=="Core.Caves.PrimitiveField"
|
&&stratum.recipeId=="Core.Caves.PrimitiveField"
|
||||||
&&stratum.recipeVersion==1;
|
&&stratum.recipeVersion==1;
|
||||||
}
|
}
|
||||||
@@ -1256,6 +1300,17 @@ namespace VoxelForgeCore
|
|||||||
std::span<const ResolvedCavernRoomV1> rooms,
|
std::span<const ResolvedCavernRoomV1> rooms,
|
||||||
std::span<const ResolvedCavernEdgeV1> edges,
|
std::span<const ResolvedCavernEdgeV1> edges,
|
||||||
std::string& error);
|
std::string& error);
|
||||||
|
// Build a deterministic, walkable room-and-tunnel graph between the two
|
||||||
|
// passage mouths that terminate in strateIndex. Compound rooms are emitted
|
||||||
|
// as overlapping room primitives; the route is planned below a 15 degree
|
||||||
|
// walking grade and every corridor carries a 4.5 m vertical clearance.
|
||||||
|
VFCORE_API bool PlanResolvedCavernNetworkV1(
|
||||||
|
const ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::int32_t strateIndex,
|
||||||
|
const ResolvedCavernPlannerSettingsV1& settings,
|
||||||
|
std::vector<ResolvedCavernRoomV1>& rooms,
|
||||||
|
std::vector<ResolvedCavernEdgeV1>& edges,
|
||||||
|
std::string& error);
|
||||||
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
||||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
||||||
|
|||||||
@@ -621,6 +621,10 @@ namespace
|
|||||||
<<"],\"half_extents_um\":["<<room.halfExtentsMicrometres[0]<<','
|
<<"],\"half_extents_um\":["<<room.halfExtentsMicrometres[0]<<','
|
||||||
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
|
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
|
||||||
<<"],\"floor_um\":"<<room.floorMicrometres
|
<<"],\"floor_um\":"<<room.floorMicrometres
|
||||||
|
<<",\"floor_profile_id\":"<<LayoutEscape(room.floorProfileStableId)
|
||||||
|
<<",\"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.unionWidthMicrometres>0)
|
if(room.unionWidthMicrometres>0)
|
||||||
@@ -647,6 +651,11 @@ namespace
|
|||||||
out<<'['<<p[0]<<','<<p[1]<<','<<p[2]<<']';
|
out<<'['<<p[0]<<','<<p[1]<<','<<p[2]<<']';
|
||||||
}
|
}
|
||||||
out<<']';
|
out<<']';
|
||||||
|
out<<",\"floor_profile_id\":"<<LayoutEscape(edge.floorProfileStableId)
|
||||||
|
<<",\"floor_profile_start_distance_um\":"
|
||||||
|
<<edge.floorProfileStartDistanceMicrometres
|
||||||
|
<<",\"floor_profile_end_distance_um\":"
|
||||||
|
<<edge.floorProfileEndDistanceMicrometres;
|
||||||
if(!edge.polylineMicrometres.empty())
|
if(!edge.polylineMicrometres.empty())
|
||||||
{
|
{
|
||||||
out<<",\"polyline_um\":[";
|
out<<",\"polyline_um\":[";
|
||||||
@@ -1235,9 +1244,10 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
add("cavern_graph_vocabulary_version",
|
add("cavern_graph_vocabulary_version",
|
||||||
"cavern graph vocabulary version is set without graph records");
|
"cavern graph vocabulary version is set without graph records");
|
||||||
}
|
}
|
||||||
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1)
|
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1
|
||||||
|
&&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV2)
|
||||||
add("cavern_graph_vocabulary_version",
|
add("cavern_graph_vocabulary_version",
|
||||||
"cavern graph records require vocabulary version 1");
|
"cavern graph records require a supported vocabulary version");
|
||||||
if(cavernGraphVocabularyVersion!=0)
|
if(cavernGraphVocabularyVersion!=0)
|
||||||
{
|
{
|
||||||
if(cavernRooms.empty()||cavernEdges.empty())
|
if(cavernRooms.empty()||cavernEdges.empty())
|
||||||
@@ -1268,6 +1278,16 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
||(room.irregularityAmplitudeMicrometres>0
|
||(room.irregularityAmplitudeMicrometres>0
|
||||||
&&room.irregularityWavelengthMicrometres<=0))
|
&&room.irregularityWavelengthMicrometres<=0))
|
||||||
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
|
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
|
||||||
|
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];
|
||||||
|
if((room.floorProfileStableId.empty()
|
||||||
|
&&room.floorProfileDistanceMicrometres!=-1)
|
||||||
|
||(!room.floorProfileStableId.empty()
|
||||||
|
&&room.floorProfileDistanceMicrometres<0)
|
||||||
|
||gradientSquared>static_cast<std::int64_t>(13108)*13108)
|
||||||
|
add("cavern_room_floor_profile",room.stableId+
|
||||||
|
" has an invalid shared profile reference or floor grade");
|
||||||
const std::int64_t pad=room.smoothWidthMicrometres
|
const std::int64_t pad=room.smoothWidthMicrometres
|
||||||
+room.unionWidthMicrometres+room.irregularityAmplitudeMicrometres;
|
+room.unionWidthMicrometres+room.irregularityAmplitudeMicrometres;
|
||||||
for(std::size_t axis=0;axis<3;++axis)
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
@@ -1308,6 +1328,15 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
||(edge.irregularityAmplitudeMicrometres>0
|
||(edge.irregularityAmplitudeMicrometres>0
|
||||||
&&edge.irregularityWavelengthMicrometres<=0))
|
&&edge.irregularityWavelengthMicrometres<=0))
|
||||||
add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters");
|
add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters");
|
||||||
|
const bool hasFloorProfile=!edge.floorProfileStableId.empty();
|
||||||
|
if((hasFloorProfile&&(edge.floorProfileStartDistanceMicrometres<0
|
||||||
|
||edge.floorProfileEndDistanceMicrometres<0
|
||||||
|
||edge.floorProfileStartDistanceMicrometres
|
||||||
|
==edge.floorProfileEndDistanceMicrometres))
|
||||||
|
||(!hasFloorProfile&&(edge.floorProfileStartDistanceMicrometres!=-1
|
||||||
|
||edge.floorProfileEndDistanceMicrometres!=-1)))
|
||||||
|
add("cavern_edge_floor_profile",edge.stableId+
|
||||||
|
" has an invalid shared profile distance interval");
|
||||||
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
||||||
std::array<std::int64_t,3> high=low;
|
std::array<std::int64_t,3> high=low;
|
||||||
if(nativeLattice&&!edge.polylineMicrometres.empty())
|
if(nativeLattice&&!edge.polylineMicrometres.empty())
|
||||||
@@ -1363,6 +1392,118 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
add("cavern_edge_polyline",edge.stableId+
|
add("cavern_edge_polyline",edge.stableId+
|
||||||
" has no bounded resolve-time polyline with matching endpoints");
|
" has no bounded resolve-time polyline with matching endpoints");
|
||||||
}
|
}
|
||||||
|
std::vector<std::string> floorProfileIds;
|
||||||
|
for(const auto& edge:cavernEdges)
|
||||||
|
if(!edge.floorProfileStableId.empty()
|
||||||
|
&&std::find(floorProfileIds.begin(),floorProfileIds.end(),
|
||||||
|
edge.floorProfileStableId)==floorProfileIds.end())
|
||||||
|
floorProfileIds.push_back(edge.floorProfileStableId);
|
||||||
|
for(const std::string& profileId:floorProfileIds)
|
||||||
|
{
|
||||||
|
std::vector<const ResolvedCavernEdgeV1*> profileEdges;
|
||||||
|
for(const auto& edge:cavernEdges)
|
||||||
|
if(edge.floorProfileStableId==profileId)profileEdges.push_back(&edge);
|
||||||
|
std::sort(profileEdges.begin(),profileEdges.end(),[](const auto* a,const auto* b)
|
||||||
|
{
|
||||||
|
return std::min(a->floorProfileStartDistanceMicrometres,
|
||||||
|
a->floorProfileEndDistanceMicrometres)
|
||||||
|
<std::min(b->floorProfileStartDistanceMicrometres,
|
||||||
|
b->floorProfileEndDistanceMicrometres);
|
||||||
|
});
|
||||||
|
const auto endpointZ=[](const ResolvedCavernEdgeV1& edge,bool atMinimum)
|
||||||
|
{
|
||||||
|
const bool forward=edge.floorProfileStartDistanceMicrometres
|
||||||
|
<edge.floorProfileEndDistanceMicrometres;
|
||||||
|
const std::size_t point=(atMinimum==forward)?0:3;
|
||||||
|
return edge.controlPointsMicrometres[point][2];
|
||||||
|
};
|
||||||
|
const auto endpointSlope=[](const ResolvedCavernEdgeV1& edge,bool atMinimum)
|
||||||
|
{
|
||||||
|
const double span=static_cast<double>(edge.floorProfileEndDistanceMicrometres
|
||||||
|
-edge.floorProfileStartDistanceMicrometres);
|
||||||
|
if(span==0.0)return std::numeric_limits<double>::infinity();
|
||||||
|
const bool forward=edge.floorProfileStartDistanceMicrometres
|
||||||
|
<edge.floorProfileEndDistanceMicrometres;
|
||||||
|
const std::size_t control=atMinimum==forward?0:2;
|
||||||
|
return 3.0*(edge.controlPointsMicrometres[control+1][2]
|
||||||
|
-edge.controlPointsMicrometres[control][2])/span;
|
||||||
|
};
|
||||||
|
const ResolvedCavernEdgeV1* previous=nullptr;
|
||||||
|
for(const auto* edge:profileEdges)
|
||||||
|
{
|
||||||
|
const std::int64_t low=std::min(edge->floorProfileStartDistanceMicrometres,
|
||||||
|
edge->floorProfileEndDistanceMicrometres);
|
||||||
|
const std::int64_t high=std::max(edge->floorProfileStartDistanceMicrometres,
|
||||||
|
edge->floorProfileEndDistanceMicrometres);
|
||||||
|
const double slope0=endpointSlope(*edge,true);
|
||||||
|
const double slope1=endpointSlope(*edge,false);
|
||||||
|
if(std::max(std::abs(slope0),std::abs(slope1))>0.200001)
|
||||||
|
add("cavern_floor_profile_grade",edge->stableId+
|
||||||
|
" exceeds the shared backbone 1:5 grade limit");
|
||||||
|
if(previous)
|
||||||
|
{
|
||||||
|
const std::int64_t previousHigh=std::max(
|
||||||
|
previous->floorProfileStartDistanceMicrometres,
|
||||||
|
previous->floorProfileEndDistanceMicrometres);
|
||||||
|
if(low!=previousHigh
|
||||||
|
||std::abs(endpointZ(*previous,false)-endpointZ(*edge,true))>1000
|
||||||
|
||std::abs(endpointSlope(*previous,false)-slope0)>0.00001)
|
||||||
|
add("cavern_floor_profile_joint",profileId+" between "+previous->stableId
|
||||||
|
+" and "+edge->stableId+" has distance gap="
|
||||||
|
+std::to_string(low-previousHigh)+" um height step="
|
||||||
|
+std::to_string(endpointZ(*previous,false)-endpointZ(*edge,true))
|
||||||
|
+" um slope step="
|
||||||
|
+std::to_string(endpointSlope(*previous,false)-slope0));
|
||||||
|
}
|
||||||
|
previous=edge;
|
||||||
|
(void)high;
|
||||||
|
}
|
||||||
|
for(const auto& room:cavernRooms)
|
||||||
|
{
|
||||||
|
if(room.floorProfileStableId!=profileId)continue;
|
||||||
|
const ResolvedCavernEdgeV1* containing=nullptr;
|
||||||
|
for(const auto* edge:profileEdges)
|
||||||
|
{
|
||||||
|
const auto low=std::min(edge->floorProfileStartDistanceMicrometres,
|
||||||
|
edge->floorProfileEndDistanceMicrometres);
|
||||||
|
const auto high=std::max(edge->floorProfileStartDistanceMicrometres,
|
||||||
|
edge->floorProfileEndDistanceMicrometres);
|
||||||
|
if(room.floorProfileDistanceMicrometres>=low
|
||||||
|
&&room.floorProfileDistanceMicrometres<=high)
|
||||||
|
{containing=edge;break;}
|
||||||
|
}
|
||||||
|
if(!containing)
|
||||||
|
{
|
||||||
|
add("cavern_room_floor_profile",room.stableId+
|
||||||
|
" does not lie on its referenced continuous floor profile");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& edge=*containing;
|
||||||
|
const double t=static_cast<double>(room.floorProfileDistanceMicrometres
|
||||||
|
-edge.floorProfileStartDistanceMicrometres)
|
||||||
|
/(edge.floorProfileEndDistanceMicrometres
|
||||||
|
-edge.floorProfileStartDistanceMicrometres);
|
||||||
|
const double one=1.0-t;
|
||||||
|
const double expected=one*one*one*edge.controlPointsMicrometres[0][2]
|
||||||
|
+3.0*one*one*t*edge.controlPointsMicrometres[1][2]
|
||||||
|
+3.0*one*t*t*edge.controlPointsMicrometres[2][2]
|
||||||
|
+t*t*t*edge.controlPointsMicrometres[3][2];
|
||||||
|
std::array<double,2> routeXY{};
|
||||||
|
for(std::size_t axis=0;axis<2;++axis)
|
||||||
|
routeXY[axis]=one*one*one*edge.controlPointsMicrometres[0][axis]
|
||||||
|
+3.0*one*one*t*edge.controlPointsMicrometres[1][axis]
|
||||||
|
+3.0*one*t*t*edge.controlPointsMicrometres[2][axis]
|
||||||
|
+t*t*t*edge.controlPointsMicrometres[3][axis];
|
||||||
|
const double roomFloorAtProfile=room.floorMicrometres
|
||||||
|
+room.floorGradientQ16[0]/65536.0
|
||||||
|
*(routeXY[0]-room.centerMicrometres[0])
|
||||||
|
+room.floorGradientQ16[1]/65536.0
|
||||||
|
*(routeXY[1]-room.centerMicrometres[1]);
|
||||||
|
if(std::abs(expected-roomFloorAtProfile)>1000.0)
|
||||||
|
add("cavern_room_floor_profile",room.stableId+
|
||||||
|
" floor does not match its referenced h(s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(oreVeins.empty())
|
if(oreVeins.empty())
|
||||||
{
|
{
|
||||||
@@ -1526,6 +1667,12 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
readTriplet("bounds_min_um",room.boundsMinimumMicrometres);
|
readTriplet("bounds_min_um",room.boundsMinimumMicrometres);
|
||||||
readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
|
readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
|
||||||
room.floorMicrometres=LayoutInt(v,"floor_um");
|
room.floorMicrometres=LayoutInt(v,"floor_um");
|
||||||
|
room.floorProfileStableId=LayoutString(v,"floor_profile_id");
|
||||||
|
if(v.Get("floor_profile_distance_um").IsNumber())
|
||||||
|
room.floorProfileDistanceMicrometres=LayoutInt(v,"floor_profile_distance_um");
|
||||||
|
const auto& floorGradient=v.Get("floor_gradient_q16").AsArray();
|
||||||
|
for(std::size_t axis=0;axis<2&&axis<floorGradient.size();++axis)
|
||||||
|
room.floorGradientQ16[axis]=static_cast<std::int32_t>(floorGradient[axis].Integer());
|
||||||
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||||
room.unionWidthMicrometres=LayoutInt(v,"union_width_um");
|
room.unionWidthMicrometres=LayoutInt(v,"union_width_um");
|
||||||
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||||
@@ -1539,6 +1686,13 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id");
|
edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id");
|
||||||
edge.endRoomStableId=LayoutString(v,"end_room_stable_id");
|
edge.endRoomStableId=LayoutString(v,"end_room_stable_id");
|
||||||
edge.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
edge.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||||
|
edge.floorProfileStableId=LayoutString(v,"floor_profile_id");
|
||||||
|
if(v.Get("floor_profile_start_distance_um").IsNumber())
|
||||||
|
edge.floorProfileStartDistanceMicrometres=
|
||||||
|
LayoutInt(v,"floor_profile_start_distance_um");
|
||||||
|
if(v.Get("floor_profile_end_distance_um").IsNumber())
|
||||||
|
edge.floorProfileEndDistanceMicrometres=
|
||||||
|
LayoutInt(v,"floor_profile_end_distance_um");
|
||||||
const auto& points=v.Get("control_points_um").AsArray();
|
const auto& points=v.Get("control_points_um").AsArray();
|
||||||
for(std::size_t i=0;i<edge.controlPointsMicrometres.size()&&i<points.size();++i)
|
for(std::size_t i=0;i<edge.controlPointsMicrometres.size()&&i<points.size();++i)
|
||||||
{
|
{
|
||||||
@@ -1879,7 +2033,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
if(!candidate.finalize(error))return false;
|
if(!candidate.finalize(error))return false;
|
||||||
layout=std::move(candidate);return true;
|
layout=std::move(candidate);return true;
|
||||||
}
|
}
|
||||||
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV1;
|
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV2;
|
||||||
candidate.cavernRooms.assign(rooms.begin(),rooms.end());
|
candidate.cavernRooms.assign(rooms.begin(),rooms.end());
|
||||||
candidate.cavernEdges.assign(edges.begin(),edges.end());
|
candidate.cavernEdges.assign(edges.begin(),edges.end());
|
||||||
const auto isNativeCavesStrate=[&candidate](std::int32_t strateIndex)
|
const auto isNativeCavesStrate=[&candidate](std::int32_t strateIndex)
|
||||||
@@ -2031,6 +2185,540 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
layout=std::move(candidate);return true;
|
layout=std::move(candidate);return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
||||||
|
std::int32_t seed,std::int32_t strateIndex,
|
||||||
|
const ResolvedCavernPlannerSettingsV1& settings,
|
||||||
|
std::vector<ResolvedCavernRoomV1>& rooms,
|
||||||
|
std::vector<ResolvedCavernEdgeV1>& edges,std::string& error)
|
||||||
|
{
|
||||||
|
rooms.clear();edges.clear();error.clear();
|
||||||
|
const auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
|
[strateIndex](const auto& value){return value.strateIndex==strateIndex;});
|
||||||
|
if(stratum==layout.strates.end()||!IsNativeCaveFieldStratum(*stratum))
|
||||||
|
{error="cavern planner target is not a resolved native cave stratum";return false;}
|
||||||
|
if(settings.roomSpacingMicrometres<=0
|
||||||
|
||settings.minimumRoomRadiusMicrometres<500000
|
||||||
|
||settings.maximumRoomRadiusMicrometres<settings.minimumRoomRadiusMicrometres
|
||||||
|
||settings.maximumRoomRadiusMicrometres>100000000
|
||||||
|
||settings.destinationRadiusMicrometres<settings.maximumRoomRadiusMicrometres
|
||||||
|
||settings.destinationRadiusMicrometres>100000000
|
||||||
|
||settings.floorSupportDepthMicrometres<250000
|
||||||
|
||settings.floorSupportDepthMicrometres>2000000
|
||||||
|
||settings.maximumTunnelLengthMicrometres<=0
|
||||||
|
||settings.minimumTunnelRadiusMicrometres<0
|
||||||
|
||settings.maximumTunnelRadiusMicrometres<settings.minimumTunnelRadiusMicrometres
|
||||||
|
||settings.smoothWidthMicrometres<0
|
||||||
|
||settings.smoothWidthMicrometres>10000000
|
||||||
|
||settings.irregularityAmplitudeMicrometres<0
|
||||||
|
||settings.irregularityAmplitudeMicrometres>10000000
|
||||||
|
||(settings.irregularityAmplitudeMicrometres>0
|
||||||
|
&&settings.irregularityWavelengthMicrometres<=0))
|
||||||
|
{error="cavern planner settings are outside the bounded room, tunnel or relief ranges";return false;}
|
||||||
|
|
||||||
|
const ResolvedLayoutPassageV1* arrivalPassage=nullptr;
|
||||||
|
const ResolvedLayoutPassageV1* departurePassage=nullptr;
|
||||||
|
for(const auto& passage:layout.passages)
|
||||||
|
{
|
||||||
|
if(passage.lowerStrateIndex==strateIndex&&passage.upperStrateIndex<strateIndex
|
||||||
|
&&(!arrivalPassage||passage.stableId<arrivalPassage->stableId))
|
||||||
|
arrivalPassage=&passage;
|
||||||
|
if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex
|
||||||
|
&&(!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;}
|
||||||
|
if(layout.coreVoxelSizeMicrometres==0)
|
||||||
|
{error="cavern planner requires a nonzero Core voxel size";return false;}
|
||||||
|
|
||||||
|
using FPoint=std::array<std::int64_t,3>;
|
||||||
|
constexpr std::int64_t MicrometresPerMetre=1000000;
|
||||||
|
constexpr std::int64_t MinimumBoreRadius=1750000;
|
||||||
|
constexpr std::int64_t MinimumBoreHeight=2200000;
|
||||||
|
constexpr std::int64_t PlannedBoreHeight=4500000;
|
||||||
|
constexpr std::int64_t MouthOverlap=3000000;
|
||||||
|
constexpr std::int64_t MaximumBoreRadius=10000000;
|
||||||
|
constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees.
|
||||||
|
const auto integerSqrt=[](std::uint64_t value)
|
||||||
|
{
|
||||||
|
std::uint64_t result=0;
|
||||||
|
std::uint64_t bit=std::uint64_t{1}<<62;
|
||||||
|
while(bit>value)bit>>=2;
|
||||||
|
while(bit!=0)
|
||||||
|
{
|
||||||
|
if(value>=result+bit)
|
||||||
|
{value-=result+bit;result=(result>>1)+bit;}
|
||||||
|
else result>>=1;
|
||||||
|
bit>>=2;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const auto distance2d=[&](const FPoint& a,const FPoint& b)
|
||||||
|
{
|
||||||
|
const std::int64_t dx=b[0]-a[0],dy=b[1]-a[1];
|
||||||
|
const auto squared=static_cast<std::uint64_t>(dx*dx+dy*dy);
|
||||||
|
return static_cast<std::int64_t>(integerSqrt(squared));
|
||||||
|
};
|
||||||
|
const auto qToMicrometres=[&](std::int64_t q)
|
||||||
|
{return q*static_cast<std::int64_t>(layout.coreVoxelSizeMicrometres)
|
||||||
|
/WorldAssemblySubvoxelsPerVoxel;};
|
||||||
|
const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage,
|
||||||
|
bool lowerSide,FPoint& point)
|
||||||
|
{
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis]
|
||||||
|
:passage.upperMouthSubvoxels[axis]);
|
||||||
|
if(passage.controlPoints.size()<2)return false;
|
||||||
|
const std::size_t mouthIndex=lowerSide
|
||||||
|
?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0;
|
||||||
|
const std::size_t tangentIndex=lowerSide
|
||||||
|
?(mouthIndex>0?mouthIndex-1:1):1;
|
||||||
|
const std::int64_t tx=passage.controlPoints[mouthIndex].positionSubvoxels[0]
|
||||||
|
-passage.controlPoints[tangentIndex].positionSubvoxels[0];
|
||||||
|
const std::int64_t ty=passage.controlPoints[mouthIndex].positionSubvoxels[1]
|
||||||
|
-passage.controlPoints[tangentIndex].positionSubvoxels[1];
|
||||||
|
const auto tangentLength=static_cast<std::int64_t>(integerSqrt(
|
||||||
|
static_cast<std::uint64_t>(tx*tx+ty*ty)));
|
||||||
|
if(tangentLength<=0)return false;
|
||||||
|
point[0]+=tx*MouthOverlap/tangentLength;
|
||||||
|
point[1]+=ty*MouthOverlap/tangentLength;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
FPoint routeStart{},routeEnd{};
|
||||||
|
if(!mouthInnerPoint(*arrivalPassage,true,routeStart)
|
||||||
|
||!mouthInnerPoint(*departurePassage,false,routeEnd))
|
||||||
|
{error="cavern passage mouth is missing a usable horizontal route tangent";return false;}
|
||||||
|
|
||||||
|
const std::int64_t dx=routeEnd[0]-routeStart[0];
|
||||||
|
const std::int64_t dy=routeEnd[1]-routeStart[1];
|
||||||
|
const std::int64_t dz=routeEnd[2]-routeStart[2];
|
||||||
|
const std::int64_t straightLength=distance2d(routeStart,routeEnd);
|
||||||
|
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+1000;
|
||||||
|
std::int64_t lateral=std::max<std::int64_t>(1000000,
|
||||||
|
std::max(settings.tunnelWarpMicrometres,settings.caveWarpMicrometres)/2);
|
||||||
|
const std::uint64_t routeHash=LayoutStampSeed(seed,strateIndex,
|
||||||
|
"tunnel-network/main-route");
|
||||||
|
if((routeHash&1u)!=0)lateral=-lateral;
|
||||||
|
std::int64_t pathAxisX=dx,pathAxisY=dy;
|
||||||
|
const auto makeRoute=[&](std::int64_t lateralOffset)
|
||||||
|
{
|
||||||
|
std::array<FPoint,4> points{};
|
||||||
|
points[0]=routeStart;points[3]=routeEnd;
|
||||||
|
std::int64_t axisX=dx,axisY=dy;
|
||||||
|
std::int64_t axisLength=straightLength;
|
||||||
|
if(axisLength<=0)
|
||||||
|
{
|
||||||
|
axisX=1;axisY=0;axisLength=1;
|
||||||
|
}
|
||||||
|
pathAxisX=axisX;pathAxisY=axisY;
|
||||||
|
const std::int64_t perpX=-axisY*lateralOffset/axisLength;
|
||||||
|
const std::int64_t perpY= axisX*lateralOffset/axisLength;
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
{
|
||||||
|
const std::int64_t delta=routeEnd[axis]-routeStart[axis];
|
||||||
|
points[1][axis]=routeStart[axis]+delta/3;
|
||||||
|
points[2][axis]=routeStart[axis]+(delta*2)/3;
|
||||||
|
}
|
||||||
|
points[1][0]+=perpX;points[1][1]+=perpY;
|
||||||
|
// Keep both control points on the same side of the mouth axis. Opposite
|
||||||
|
// offsets create a tight hairpin whose descending bores overlap in XY.
|
||||||
|
points[2][0]+=perpX;points[2][1]+=perpY;
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
const auto routeLength=[&](const std::array<FPoint,4>& points)
|
||||||
|
{
|
||||||
|
std::int64_t length=0;
|
||||||
|
for(std::size_t i=1;i<points.size();++i)length+=distance2d(points[i-1],points[i]);
|
||||||
|
return length;
|
||||||
|
};
|
||||||
|
auto routePoints=makeRoute(lateral);
|
||||||
|
for(int attempt=0;routeLength(routePoints)<requiredLength&&attempt<32;++attempt)
|
||||||
|
{
|
||||||
|
if(lateral>0)lateral=std::min<std::int64_t>(lateral*2,1000000000ll);
|
||||||
|
else lateral=std::max<std::int64_t>(lateral*2,-1000000000ll);
|
||||||
|
routePoints=makeRoute(lateral);
|
||||||
|
}
|
||||||
|
const std::int64_t totalRouteLength=routeLength(routePoints);
|
||||||
|
if(totalRouteLength<=0||totalRouteLength<requiredLength)
|
||||||
|
{error="cavern planner cannot fit the fixed mouth floor difference under the 1:5 grade budget";return false;}
|
||||||
|
|
||||||
|
std::array<std::int64_t,4> cumulative{};
|
||||||
|
for(std::size_t i=1;i<routePoints.size();++i)
|
||||||
|
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 auto routeFloorAt=[&](std::int64_t distance)
|
||||||
|
{
|
||||||
|
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
|
||||||
|
return routeStart[2]+dz*distance/totalRouteLength;
|
||||||
|
};
|
||||||
|
const auto routeFloorGradientAt=[&](std::int64_t distance)
|
||||||
|
{
|
||||||
|
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
|
||||||
|
std::size_t segment=1;
|
||||||
|
while(segment<cumulative.size()-1&&distance>cumulative[segment])++segment;
|
||||||
|
const auto unitTangent=[&](std::size_t index)
|
||||||
|
{
|
||||||
|
const double tx=static_cast<double>(routePoints[index][0]
|
||||||
|
-routePoints[index-1][0]);
|
||||||
|
const double ty=static_cast<double>(routePoints[index][1]
|
||||||
|
-routePoints[index-1][1]);
|
||||||
|
const double length=std::hypot(tx,ty);
|
||||||
|
return length>0.0?std::array<double,2>{tx/length,ty/length}
|
||||||
|
:std::array<double,2>{0.0,0.0};
|
||||||
|
};
|
||||||
|
auto tangent=unitTangent(segment);
|
||||||
|
if(segment+1<cumulative.size()&&distance==cumulative[segment])
|
||||||
|
{
|
||||||
|
const auto next=unitTangent(segment+1);
|
||||||
|
tangent[0]+=next[0];tangent[1]+=next[1];
|
||||||
|
const double length=std::hypot(tangent[0],tangent[1]);
|
||||||
|
if(length>1.0e-12){tangent[0]/=length;tangent[1]/=length;}
|
||||||
|
}
|
||||||
|
constexpr double Q16=65536.0;
|
||||||
|
const double profileSlope=static_cast<double>(dz)/totalRouteLength;
|
||||||
|
return std::array<std::int32_t,2>{
|
||||||
|
static_cast<std::int32_t>(std::llround(tangent[0]*profileSlope*Q16)),
|
||||||
|
static_cast<std::int32_t>(std::llround(tangent[1]*profileSlope*Q16))};
|
||||||
|
};
|
||||||
|
const auto pointAt=[&](std::int64_t distance)
|
||||||
|
{
|
||||||
|
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
|
||||||
|
std::size_t segment=1;
|
||||||
|
while(segment<cumulative.size()-1&&distance>cumulative[segment])++segment;
|
||||||
|
const std::int64_t startDistance=cumulative[segment-1];
|
||||||
|
const std::int64_t segmentLength=std::max<std::int64_t>(1,
|
||||||
|
cumulative[segment]-startDistance);
|
||||||
|
const std::int64_t local=distance-startDistance;
|
||||||
|
FPoint point{};
|
||||||
|
for(std::size_t axis=0;axis<2;++axis)
|
||||||
|
{
|
||||||
|
const std::int64_t delta=routePoints[segment][axis]
|
||||||
|
-routePoints[segment-1][axis];
|
||||||
|
point[axis]=routePoints[segment-1][axis]+delta*local/segmentLength;
|
||||||
|
}
|
||||||
|
point[2]=routeFloorAt(distance);
|
||||||
|
return point;
|
||||||
|
};
|
||||||
|
std::int64_t spacing=std::min(std::max<std::int64_t>(settings.roomSpacingMicrometres,
|
||||||
|
4000000),settings.maximumTunnelLengthMicrometres);
|
||||||
|
spacing=std::max<std::int64_t>(4000000,spacing);
|
||||||
|
if(spacing>settings.maximumTunnelLengthMicrometres)
|
||||||
|
spacing=settings.maximumTunnelLengthMicrometres;
|
||||||
|
if(spacing<=0)
|
||||||
|
{error="cavern planner has no positive route spacing after applying tunnel length limits";return false;}
|
||||||
|
std::vector<std::int64_t> nodeDistances;
|
||||||
|
for(std::size_t leg=1;leg<routePoints.size();++leg)
|
||||||
|
{
|
||||||
|
const std::int64_t legStart=cumulative[leg-1];
|
||||||
|
const std::int64_t legLength=cumulative[leg]-legStart;
|
||||||
|
const std::int64_t segments=std::max<std::int64_t>(1,
|
||||||
|
(legLength+spacing-1)/spacing);
|
||||||
|
if(segments>256)
|
||||||
|
{error="cavern planner would exceed the 256-node bounded backbone";return false;}
|
||||||
|
for(std::int64_t part=1;part<=segments;++part)
|
||||||
|
{
|
||||||
|
const std::int64_t distance=legStart+legLength*part/segments;
|
||||||
|
if(distance>0&&distance<totalRouteLength)nodeDistances.push_back(distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeDistances.push_back(totalRouteLength/2);
|
||||||
|
std::sort(nodeDistances.begin(),nodeDistances.end());
|
||||||
|
nodeDistances.erase(std::unique(nodeDistances.begin(),nodeDistances.end()),nodeDistances.end());
|
||||||
|
if(nodeDistances.empty())nodeDistances.push_back(totalRouteLength/2);
|
||||||
|
if(nodeDistances.size()>256)
|
||||||
|
{error="cavern planner would exceed the 256-node bounded backbone";return false;}
|
||||||
|
|
||||||
|
const auto clampQ16=[](std::int32_t value)
|
||||||
|
{return std::clamp<std::int64_t>(value,0,65536);};
|
||||||
|
const std::int64_t smooth=std::clamp<std::int64_t>(
|
||||||
|
std::max<std::int64_t>(1500000,settings.smoothWidthMicrometres),
|
||||||
|
50000,10000000);
|
||||||
|
const std::int64_t irregularity=std::clamp<std::int64_t>(
|
||||||
|
settings.irregularityAmplitudeMicrometres,0,500000);
|
||||||
|
const std::int64_t routeBoreRadius=std::clamp<std::int64_t>(
|
||||||
|
std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres),
|
||||||
|
MinimumBoreRadius,MaximumBoreRadius);
|
||||||
|
const std::int64_t maximumRouteBoreRadius=std::clamp<std::int64_t>(
|
||||||
|
std::max(routeBoreRadius,settings.maximumTunnelRadiusMicrometres),
|
||||||
|
routeBoreRadius,MaximumBoreRadius);
|
||||||
|
const auto selectedBoreRadius=[&](const std::string& stableId)
|
||||||
|
{
|
||||||
|
const std::uint64_t span=static_cast<std::uint64_t>(
|
||||||
|
maximumRouteBoreRadius-routeBoreRadius);
|
||||||
|
return routeBoreRadius+(span==0?0:static_cast<std::int64_t>(
|
||||||
|
LayoutStampSeed(seed,strateIndex,stableId)%(span+1)));
|
||||||
|
};
|
||||||
|
const auto passageBoreRadius=[&](const ResolvedLayoutPassageV1& passage,bool lowerSide)
|
||||||
|
{
|
||||||
|
std::int64_t radius=qToMicrometres(passage.mouthRadiusSubvoxels);
|
||||||
|
if(!passage.controlPoints.empty())
|
||||||
|
{
|
||||||
|
const auto control=lowerSide?std::min(passage.lowerMouthControlIndex,
|
||||||
|
passage.controlPoints.size()-1):std::size_t{0};
|
||||||
|
radius=std::max(radius,qToMicrometres(passage.controlPoints[control].radiusSubvoxels));
|
||||||
|
}
|
||||||
|
return std::clamp<std::int64_t>(std::max(routeBoreRadius,radius),
|
||||||
|
routeBoreRadius,MaximumBoreRadius);
|
||||||
|
};
|
||||||
|
const auto fractional=[](std::int64_t value,std::int64_t q16)
|
||||||
|
{return value*std::clamp<std::int64_t>(q16,0,65536)/65536;};
|
||||||
|
struct FRoomNode
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
FPoint point{};
|
||||||
|
std::string floorProfileStableId;
|
||||||
|
std::int64_t floorProfileDistanceMicrometres=-1;
|
||||||
|
std::array<std::int32_t,2> floorGradientQ16{};
|
||||||
|
std::int64_t radiusMicrometres=0;
|
||||||
|
bool destination=false;
|
||||||
|
};
|
||||||
|
std::vector<FRoomNode> nodes;nodes.reserve(nodeDistances.size());
|
||||||
|
const std::int64_t floorCutMinimum=clampQ16(settings.roomFloorCutMinimumQ16);
|
||||||
|
const std::int64_t floorCutMaximum=std::max(floorCutMinimum,
|
||||||
|
clampQ16(settings.roomFloorCutMaximumQ16));
|
||||||
|
const std::int64_t floorRange=floorCutMaximum-floorCutMinimum;
|
||||||
|
const std::int64_t heightRatio=std::clamp<std::int64_t>(
|
||||||
|
settings.roomHeightRatioQ16,6554,65536);
|
||||||
|
const std::int64_t baseRoomSpan=settings.maximumRoomRadiusMicrometres
|
||||||
|
-settings.minimumRoomRadiusMicrometres;
|
||||||
|
const std::int64_t mainRouteMidpoint=totalRouteLength/2;
|
||||||
|
for(std::size_t nodeIndex=0;nodeIndex<nodeDistances.size();++nodeIndex)
|
||||||
|
{
|
||||||
|
const std::int64_t distance=nodeDistances[nodeIndex];
|
||||||
|
FRoomNode node;node.point=pointAt(distance);
|
||||||
|
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);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
const auto makeCompound=[&](const FRoomNode& node,bool destination,
|
||||||
|
std::vector<ResolvedCavernRoomV1>& outRooms)
|
||||||
|
{
|
||||||
|
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,node.id+"/compound");
|
||||||
|
const std::int64_t variety=clampQ16(settings.roomShapeVarietyQ16);
|
||||||
|
const std::size_t additionalPrimitiveCount=static_cast<std::size_t>(
|
||||||
|
variety*2/65536);
|
||||||
|
const std::size_t primitiveCount=2+static_cast<std::size_t>(
|
||||||
|
hash%(additionalPrimitiveCount+1));
|
||||||
|
std::uint64_t state=hash;
|
||||||
|
const std::int64_t radius=node.radiusMicrometres;
|
||||||
|
const std::int64_t floorCutBase=clampQ16(settings.roomFloorCutMinimumQ16);
|
||||||
|
const std::int64_t componentCount=static_cast<std::int64_t>(primitiveCount);
|
||||||
|
for(std::int64_t component=0;component<componentCount;++component)
|
||||||
|
{
|
||||||
|
const std::string stableId=node.id+"/primitive-"+std::to_string(component);
|
||||||
|
const std::uint64_t componentHash=LayoutStampNext(state);
|
||||||
|
const std::int64_t componentScaleQ16=component==0?65536
|
||||||
|
:42000+static_cast<std::int64_t>(componentHash%22001);
|
||||||
|
const std::int64_t componentRadius=std::max<std::int64_t>(500000,
|
||||||
|
fractional(radius,componentScaleQ16));
|
||||||
|
const std::int64_t axisScaleQ16=component==0?65536
|
||||||
|
:47000+static_cast<std::int64_t>((componentHash>>8)%19001);
|
||||||
|
const bool swapAxes=((componentHash>>24)&1u)!=0;
|
||||||
|
const std::int64_t halfX=swapAxes?fractional(componentRadius,axisScaleQ16)
|
||||||
|
:componentRadius;
|
||||||
|
const std::int64_t halfY=swapAxes?componentRadius
|
||||||
|
:fractional(componentRadius,axisScaleQ16);
|
||||||
|
const std::int64_t halfZ=std::max<std::int64_t>(500000,
|
||||||
|
fractional(componentRadius,heightRatio));
|
||||||
|
const std::int64_t floorCut=floorCutBase
|
||||||
|
+(floorRange==0?0:static_cast<std::int64_t>(componentHash%(
|
||||||
|
static_cast<std::uint64_t>(floorRange+1))));
|
||||||
|
const std::int64_t offsetMagnitude=component==0?0
|
||||||
|
:fractional(radius,12000+static_cast<std::int64_t>(componentHash%11001));
|
||||||
|
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 centerOffset=(component%2==0?1:-1)*offsetMagnitude;
|
||||||
|
ResolvedCavernRoomV1 room;room.stableId=stableId;
|
||||||
|
room.strateIndex=strateIndex;
|
||||||
|
room.floorProfileStableId=node.floorProfileStableId;
|
||||||
|
room.floorProfileDistanceMicrometres=node.floorProfileDistanceMicrometres;
|
||||||
|
room.floorGradientQ16=node.floorGradientQ16;
|
||||||
|
room.halfExtentsMicrometres={halfX,halfY,halfZ};
|
||||||
|
room.centerMicrometres={node.point[0]+(-pathAxisY)*centerOffset/routeLengthXY,
|
||||||
|
node.point[1]+pathAxisX*centerOffset/routeLengthXY,0};
|
||||||
|
// Keep the local floor plane on the backbone profile at the route point,
|
||||||
|
// then place each offset primitive from that plane at its own XY centre.
|
||||||
|
room.floorMicrometres=node.point[2]
|
||||||
|
+(static_cast<std::int64_t>(room.floorGradientQ16[0])
|
||||||
|
*(room.centerMicrometres[0]-node.point[0])
|
||||||
|
+static_cast<std::int64_t>(room.floorGradientQ16[1])
|
||||||
|
*(room.centerMicrometres[1]-node.point[1]))/65536;
|
||||||
|
room.centerMicrometres[2]=room.floorMicrometres+fractional(halfZ,floorCut);
|
||||||
|
if(component>0)
|
||||||
|
{
|
||||||
|
const std::int64_t headroom=std::max<std::int64_t>(0,
|
||||||
|
halfZ-fractional(halfZ,floorCut));
|
||||||
|
const std::int64_t jitterRange=std::min(headroom,halfZ/5);
|
||||||
|
const std::int64_t ceilingJitter=jitterRange>0
|
||||||
|
?static_cast<std::int64_t>((componentHash>>32)
|
||||||
|
%static_cast<std::uint64_t>(jitterRange+1)):0;
|
||||||
|
room.centerMicrometres[2]+=ceilingJitter;
|
||||||
|
}
|
||||||
|
room.smoothWidthMicrometres=smooth;
|
||||||
|
room.irregularityAmplitudeMicrometres=irregularity;
|
||||||
|
room.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
|
||||||
|
outRooms.push_back(std::move(room));
|
||||||
|
}
|
||||||
|
(void)destination;
|
||||||
|
};
|
||||||
|
for(const auto& node:nodes)makeCompound(node,node.destination,rooms);
|
||||||
|
|
||||||
|
const auto componentRoomId=[](const FRoomNode& node)
|
||||||
|
{return node.id+"/primitive-0";};
|
||||||
|
const auto makeEdge=[&](std::string id,std::string startAnchor,
|
||||||
|
const FPoint& start,const FRoomNode& endNode,std::int64_t boreRadius,
|
||||||
|
std::int64_t profileStartDistance,std::int64_t profileEndDistance)
|
||||||
|
{
|
||||||
|
ResolvedCavernEdgeV1 edge;edge.stableId=std::move(id);
|
||||||
|
edge.startAnchorStableId=std::move(startAnchor);
|
||||||
|
edge.endRoomStableId=componentRoomId(endNode);edge.strateIndex=strateIndex;
|
||||||
|
edge.boreRadiusMicrometres=boreRadius;
|
||||||
|
edge.boreHeightMicrometres=std::max(PlannedBoreHeight,MinimumBoreHeight);
|
||||||
|
edge.floorSupportDepthMicrometres=settings.floorSupportDepthMicrometres;
|
||||||
|
if(profileStartDistance>=0&&profileEndDistance>=0)
|
||||||
|
{
|
||||||
|
edge.floorProfileStableId=backboneFloorProfileId;
|
||||||
|
edge.floorProfileStartDistanceMicrometres=profileStartDistance;
|
||||||
|
edge.floorProfileEndDistanceMicrometres=profileEndDistance;
|
||||||
|
}
|
||||||
|
edge.smoothWidthMicrometres=smooth;
|
||||||
|
edge.irregularityAmplitudeMicrometres=irregularity;
|
||||||
|
edge.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
|
||||||
|
const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]};
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
{
|
||||||
|
const std::int64_t delta=end[axis]-start[axis];
|
||||||
|
edge.controlPointsMicrometres[0][axis]=start[axis];
|
||||||
|
edge.controlPointsMicrometres[1][axis]=start[axis]+delta/3;
|
||||||
|
edge.controlPointsMicrometres[2][axis]=start[axis]+(delta*2)/3;
|
||||||
|
edge.controlPointsMicrometres[3][axis]=end[axis];
|
||||||
|
}
|
||||||
|
if(profileStartDistance>=0&&profileEndDistance>=0)
|
||||||
|
{
|
||||||
|
const std::int64_t floorStart=routeFloorAt(profileStartDistance);
|
||||||
|
const std::int64_t floorEnd=routeFloorAt(profileEndDistance);
|
||||||
|
const std::int64_t floorDelta=floorEnd-floorStart;
|
||||||
|
edge.controlPointsMicrometres[0][2]=floorStart;
|
||||||
|
edge.controlPointsMicrometres[1][2]=floorStart+floorDelta/3;
|
||||||
|
edge.controlPointsMicrometres[2][2]=floorStart+(floorDelta*2)/3;
|
||||||
|
edge.controlPointsMicrometres[3][2]=floorEnd;
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
};
|
||||||
|
const auto arrivalPoint=routeStart;
|
||||||
|
FPoint previousPoint=arrivalPoint;
|
||||||
|
std::int64_t previousProfileDistance=0;
|
||||||
|
std::string previousAnchor=arrivalPassage->stableId+"/lower-mouth";
|
||||||
|
for(std::size_t nodeIndex=0;nodeIndex<nodes.size();++nodeIndex)
|
||||||
|
{
|
||||||
|
const auto& node=nodes[nodeIndex];
|
||||||
|
const bool portal=nodeIndex==0;
|
||||||
|
const std::string edgeId="tunnel-network/strate-"+std::to_string(strateIndex)
|
||||||
|
+"/route-edge-"+std::to_string(nodeIndex);
|
||||||
|
edges.push_back(makeEdge(edgeId,previousAnchor,previousPoint,node,
|
||||||
|
portal?passageBoreRadius(*arrivalPassage,true):selectedBoreRadius(edgeId),
|
||||||
|
previousProfileDistance,node.floorProfileDistanceMicrometres));
|
||||||
|
previousPoint={node.point[0],node.point[1],node.point[2]};
|
||||||
|
previousProfileDistance=node.floorProfileDistanceMicrometres;
|
||||||
|
previousAnchor=componentRoomId(node);
|
||||||
|
}
|
||||||
|
if(!nodes.empty())
|
||||||
|
{
|
||||||
|
const std::string departureEdgeId="tunnel-network/strate-"
|
||||||
|
+std::to_string(strateIndex)+"/departure-edge";
|
||||||
|
edges.push_back(makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth",
|
||||||
|
routeEnd,nodes.back(),passageBoreRadius(*departurePassage,false),
|
||||||
|
totalRouteLength,nodes.back().floorProfileDistanceMicrometres));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int64_t roomDensity=clampQ16(settings.roomDensityQ16);
|
||||||
|
const std::int64_t tunnelDensity=clampQ16(settings.tunnelDensityQ16);
|
||||||
|
const std::int64_t branchThreshold=roomDensity*tunnelDensity/65536;
|
||||||
|
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()
|
||||||
|
&&branchCount<branchLimit&&rooms.size()<2048;++nodeIndex)
|
||||||
|
{
|
||||||
|
const auto& parent=nodes[nodeIndex];
|
||||||
|
const std::uint64_t branchHash=LayoutStampSeed(seed,strateIndex,
|
||||||
|
parent.id+"/side-branch");
|
||||||
|
if(static_cast<std::int64_t>(branchHash&0xffffu)>=branchThreshold)continue;
|
||||||
|
const std::int64_t branchLength=std::max<std::int64_t>(
|
||||||
|
routeBoreRadius*3,std::min(spacing/2,settings.maximumTunnelLengthMicrometres/2));
|
||||||
|
const std::int64_t direction=(branchHash&0x10000u)?1:-1;
|
||||||
|
FRoomNode branch;branch.id=parent.id+"/side-"+std::to_string(branchCount);
|
||||||
|
branch.radiusMicrometres=settings.minimumRoomRadiusMicrometres
|
||||||
|
+static_cast<std::int64_t>((branchHash>>17)
|
||||||
|
%static_cast<std::uint64_t>(baseRoomSpan+1));
|
||||||
|
const std::int64_t pathAxisLength=std::max<std::int64_t>(1,
|
||||||
|
static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
|
||||||
|
pathAxisX*pathAxisX+pathAxisY*pathAxisY))));
|
||||||
|
branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction,
|
||||||
|
parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction,
|
||||||
|
parent.point[2]};
|
||||||
|
// 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);
|
||||||
|
ResolvedCavernEdgeV1 branchEdge;
|
||||||
|
if(settings.tunnelsFlowTowardOrigin)
|
||||||
|
branchEdge=makeEdge(branchEdgeId,componentRoomId(branch),
|
||||||
|
branch.point,parent,selectedBoreRadius(branchEdgeId),-1,-1);
|
||||||
|
else
|
||||||
|
branchEdge=makeEdge(branchEdgeId,componentRoomId(parent),
|
||||||
|
parent.point,branch,selectedBoreRadius(branchEdgeId),-1,-1);
|
||||||
|
// Side branches retain their cave floor SDF, but they do not cap the
|
||||||
|
// descending backbone when their tubes overlap it near a junction.
|
||||||
|
branchEdge.floorSupportDepthMicrometres=0;
|
||||||
|
edges.push_back(std::move(branchEdge));
|
||||||
|
++branchCount;
|
||||||
|
}
|
||||||
|
if(nodes.empty()||rooms.empty()||edges.empty())
|
||||||
|
{error="cavern planner emitted an empty arrival-to-departure graph";return false;}
|
||||||
|
|
||||||
|
double maximumGradeDegrees=0.0;
|
||||||
|
for(const auto& edge:edges)
|
||||||
|
{
|
||||||
|
const auto& a=edge.controlPointsMicrometres.front();
|
||||||
|
const auto& b=edge.controlPointsMicrometres.back();
|
||||||
|
const std::int64_t run=static_cast<std::int64_t>(integerSqrt(
|
||||||
|
static_cast<std::uint64_t>((b[0]-a[0])*(b[0]-a[0])
|
||||||
|
+(b[1]-a[1])*(b[1]-a[1]))));
|
||||||
|
const std::int64_t rise=std::abs(b[2]-a[2]);
|
||||||
|
if(run<=0&&rise>0)
|
||||||
|
{error="cavern planner produced a vertical route edge with no walking run";return false;}
|
||||||
|
if(run>0&&rise*MaximumWalkGradeDenominator>run)
|
||||||
|
{error="cavern planner emitted an edge beyond its 1:5 route grade budget";return false;}
|
||||||
|
if(run>0)maximumGradeDegrees=std::max(maximumGradeDegrees,
|
||||||
|
std::atan2(static_cast<double>(rise),static_cast<double>(run))
|
||||||
|
*180.0/3.14159265358979323846);
|
||||||
|
}
|
||||||
|
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 grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.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::abs(dz)/1.0e6,maximumGradeDegrees,routeBoreRadius/1.0e6,PlannedBoreHeight/1.0e6,
|
||||||
|
settings.destinationRadiusMicrometres/1.0e6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
|
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1227,9 +1227,18 @@ public:
|
|||||||
{
|
{
|
||||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||||
if(passage.controlPoints.size()<2)continue;
|
if(passage.controlPoints.size()<2)continue;
|
||||||
if(queryStratum&&queryStratum->Resolved
|
const bool passageTargetsNativeCaveField=queryStratum&&queryStratum->Resolved
|
||||||
&&queryStratum->Resolved->passageCapabilities.hasNativeCaveField
|
&&queryStratum->Resolved->passageCapabilities.hasNativeCaveField
|
||||||
&&passage.lowerStrateIndex==queryStratum->StrateIndex)
|
&&passage.lowerStrateIndex==queryStratum->StrateIndex;
|
||||||
|
const bool passageTargetsPlannedTunnelNetwork=queryStratum
|
||||||
|
&&queryStratum->Resolved
|
||||||
|
&&queryStratum->Resolved->generatorKind=="TunnelNetwork"
|
||||||
|
&&queryStratum->Resolved->recipeId=="Core.Caves.PrimitiveField"
|
||||||
|
&&queryStratum->Resolved->recipeVersion==1;
|
||||||
|
// Legacy native cave fields own the floor and mouth carve on their
|
||||||
|
// lower-strate boundary. The planner's TunnelNetwork field instead
|
||||||
|
// needs the shared passage composer to carry its incoming mouth open.
|
||||||
|
if(passageTargetsNativeCaveField&&!passageTargetsPlannedTunnelNetwork)
|
||||||
continue;
|
continue;
|
||||||
if(passageProfile)++passageProfile->passageIterations;
|
if(passageProfile)++passageProfile->passageIterations;
|
||||||
if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex))
|
if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex))
|
||||||
@@ -1514,13 +1523,13 @@ public:
|
|||||||
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
||||||
{
|
{
|
||||||
float graphDensity=0.0f;
|
float graphDensity=0.0f;
|
||||||
float nativeLatticeFloorSupport=std::numeric_limits<float>::lowest();
|
float nativeGraphFloorSupport=std::numeric_limits<float>::lowest();
|
||||||
{
|
{
|
||||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77GraphPhase(
|
VoxelForgeCore::Linux77Profiler::FScopedPhase l77GraphPhase(
|
||||||
VoxelForgeCore::Linux77Profiler::EPhase::CompositionGraph);
|
VoxelForgeCore::Linux77Profiler::EPhase::CompositionGraph);
|
||||||
FScopedLegacyFieldTermTimer TermTimer(ELegacyFieldTerm::ComposerCavern);
|
FScopedLegacyFieldTermTimer TermTimer(ELegacyFieldTerm::ComposerCavern);
|
||||||
graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z,
|
graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z,
|
||||||
&nativeLatticeFloorSupport);
|
&nativeGraphFloorSupport);
|
||||||
}
|
}
|
||||||
const auto* bound=FindBoundStratumForManagerZ(Z);
|
const auto* bound=FindBoundStratumForManagerZ(Z);
|
||||||
const bool nativeLattice=bound&&bound->Resolved
|
const bool nativeLattice=bound&&bound->Resolved
|
||||||
@@ -1574,7 +1583,7 @@ public:
|
|||||||
const float nativeTunnelFieldDensity=nativeTunnelField
|
const float nativeTunnelFieldDensity=nativeTunnelField
|
||||||
?graphDensity:std::numeric_limits<float>::lowest();
|
?graphDensity:std::numeric_limits<float>::lowest();
|
||||||
stamped=ApplyResolvedStratePassageCarves(stamped,X,Y,Z,
|
stamped=ApplyResolvedStratePassageCarves(stamped,X,Y,Z,
|
||||||
nativeTunnelFieldDensity,nativeLatticeFloorSupport);
|
nativeTunnelFieldDensity,nativeGraphFloorSupport);
|
||||||
}
|
}
|
||||||
if(bTraceTnComposition)
|
if(bTraceTnComposition)
|
||||||
std::printf("[L75TNCompositionTrace] input=%.9g graph=%.9g boundary=%.9g passage=%.9g stamp=%.9g native=1\n",
|
std::printf("[L75TNCompositionTrace] input=%.9g graph=%.9g boundary=%.9g passage=%.9g stamp=%.9g native=1\n",
|
||||||
@@ -3398,6 +3407,10 @@ private:
|
|||||||
double upperMouthPlannedFloor=0.0;
|
double upperMouthPlannedFloor=0.0;
|
||||||
double upperMouthTangentX=0.0,upperMouthTangentY=0.0;
|
double upperMouthTangentX=0.0,upperMouthTangentY=0.0;
|
||||||
std::vector<std::array<double,3>> upperMouthReceivingRoute;
|
std::vector<std::array<double,3>> upperMouthReceivingRoute;
|
||||||
|
double lowerMouthX=0.0,lowerMouthY=0.0;
|
||||||
|
double lowerMouthPlannedFloor=0.0;
|
||||||
|
double lowerMouthTangentX=0.0,lowerMouthTangentY=0.0;
|
||||||
|
std::vector<std::array<double,3>> lowerMouthReceivingRoute;
|
||||||
std::size_t routeEndControlIndex=0;
|
std::size_t routeEndControlIndex=0;
|
||||||
};
|
};
|
||||||
struct FPassageSegmentData
|
struct FPassageSegmentData
|
||||||
@@ -3457,14 +3470,21 @@ private:
|
|||||||
+profileT*profileSegment.authoredFloorZDelta;
|
+profileT*profileSegment.authoredFloorZDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const double mouthBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
|
const double upperBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
|
||||||
constants.upperMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels,
|
constants.upperMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels,
|
||||||
plannedFloor,constants.upperMouthX,constants.upperMouthY,
|
plannedFloor,constants.upperMouthX,constants.upperMouthY,
|
||||||
constants.upperMouthPlannedFloor,
|
constants.upperMouthPlannedFloor,
|
||||||
constants.upperMouthTangentX,constants.upperMouthTangentY,
|
constants.upperMouthTangentX,constants.upperMouthTangentY,
|
||||||
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13,
|
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13,
|
||||||
qPerMetre);
|
qPerMetre);
|
||||||
return mouthBlendFloor+floorDepthSubvoxels;
|
const double lowerBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
|
||||||
|
constants.lowerMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels,
|
||||||
|
upperBlendFloor,constants.lowerMouthX,constants.lowerMouthY,
|
||||||
|
constants.lowerMouthPlannedFloor,
|
||||||
|
-constants.lowerMouthTangentX,-constants.lowerMouthTangentY,
|
||||||
|
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13,
|
||||||
|
qPerMetre);
|
||||||
|
return lowerBlendFloor+floorDepthSubvoxels;
|
||||||
}
|
}
|
||||||
|
|
||||||
static float PassageFlatBottomDistance(float roundDistanceSubvoxels,
|
static float PassageFlatBottomDistance(float roundDistanceSubvoxels,
|
||||||
@@ -3584,6 +3604,7 @@ private:
|
|||||||
double minimumFloorMetres=std::numeric_limits<double>::infinity();
|
double minimumFloorMetres=std::numeric_limits<double>::infinity();
|
||||||
double maximumFloorMetres=-std::numeric_limits<double>::infinity();
|
double maximumFloorMetres=-std::numeric_limits<double>::infinity();
|
||||||
double maximumSegmentGrade=0.0;
|
double maximumSegmentGrade=0.0;
|
||||||
|
std::array<double,2> floorGradient{};
|
||||||
std::array<float,3> centerVoxels{},halfExtentsVoxels{};
|
std::array<float,3> centerVoxels{},halfExtentsVoxels{};
|
||||||
std::array<std::array<float,3>,4> controlPointsVoxels{};
|
std::array<std::array<float,3>,4> controlPointsVoxels{};
|
||||||
float floorVoxels=0.0f;
|
float floorVoxels=0.0f;
|
||||||
@@ -4012,8 +4033,10 @@ private:
|
|||||||
{
|
{
|
||||||
CavernRuntime.clear();CavernSpatialNodes.clear();CavernSpatialOrder.clear();
|
CavernRuntime.clear();CavernSpatialNodes.clear();CavernSpatialOrder.clear();
|
||||||
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
||||||
||ResolvedLayout->cavernGraphVocabularyVersion
|
||(ResolvedLayout->cavernGraphVocabularyVersion
|
||||||
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return;
|
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1
|
||||||
|
&&ResolvedLayout->cavernGraphVocabularyVersion
|
||||||
|
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV2))return;
|
||||||
const auto isNativeGraphStrate=[this](std::int32_t strateIndex)
|
const auto isNativeGraphStrate=[this](std::int32_t strateIndex)
|
||||||
{
|
{
|
||||||
const auto found=std::find_if(ResolvedLayout->strates.begin(),
|
const auto found=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
@@ -4043,6 +4066,8 @@ private:
|
|||||||
}
|
}
|
||||||
runtime.floorVoxels=static_cast<float>(
|
runtime.floorVoxels=static_cast<float>(
|
||||||
static_cast<double>(room.floorMicrometres)/micronsPerVoxel);
|
static_cast<double>(room.floorMicrometres)/micronsPerVoxel);
|
||||||
|
runtime.floorGradient={room.floorGradientQ16[0]/65536.0,
|
||||||
|
room.floorGradientQ16[1]/65536.0};
|
||||||
runtime.smoothWidthVoxels=static_cast<float>(
|
runtime.smoothWidthVoxels=static_cast<float>(
|
||||||
static_cast<double>(room.smoothWidthMicrometres)/micronsPerVoxel);
|
static_cast<double>(room.smoothWidthMicrometres)/micronsPerVoxel);
|
||||||
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
||||||
@@ -4621,7 +4646,8 @@ private:
|
|||||||
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||||
if(upperStratum!=ResolvedLayout->strates.end()
|
if(upperStratum!=ResolvedLayout->strates.end()
|
||||||
&&upperStratum->generatorKind=="FloatingIslands")
|
&&(upperStratum->generatorKind=="FloatingIslands"
|
||||||
|
||upperStratum->generatorKind=="TunnelNetwork"))
|
||||||
{
|
{
|
||||||
std::vector<std::array<double,3>> route;
|
std::vector<std::array<double,3>> route;
|
||||||
for(const auto& anchor:upperStratum->anchors)
|
for(const auto& anchor:upperStratum->anchors)
|
||||||
@@ -4629,16 +4655,77 @@ private:
|
|||||||
route.push_back({static_cast<double>(anchor.values[0]),
|
route.push_back({static_cast<double>(anchor.values[0]),
|
||||||
static_cast<double>(anchor.values[1]),
|
static_cast<double>(anchor.values[1]),
|
||||||
static_cast<double>(anchor.values[2])});
|
static_cast<double>(anchor.values[2])});
|
||||||
|
if(upperStratum->generatorKind=="TunnelNetwork"&&route.size()>2)
|
||||||
|
route.erase(route.begin(),route.end()-2);
|
||||||
constants.upperMouthReceivingRoute=route;
|
constants.upperMouthReceivingRoute=route;
|
||||||
double routeHeight=0.0,distanceSquared=0.0;
|
double routeHeight=0.0,distanceSquared=0.0;
|
||||||
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route,
|
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route,
|
||||||
controls.front()[0],controls.front()[1],routeHeight,distanceSquared)
|
controls.front()[0],controls.front()[1],routeHeight,distanceSquared)
|
||||||
&&distanceSquared<=std::pow(controls.front()[3]+2.0*qPerMetre,2.0))
|
&&distanceSquared<=std::pow(controls.front()[3]+2.0*qPerMetre,2.0))
|
||||||
|
{
|
||||||
upperMouthFloorDepth=std::max(upperMouthFloorDepth,
|
upperMouthFloorDepth=std::max(upperMouthFloorDepth,
|
||||||
std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre+1.13));
|
std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre+1.13));
|
||||||
|
if(upperStratum->generatorKind=="TunnelNetwork")
|
||||||
|
VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute(
|
||||||
|
controls,plannedFloorProfile,0,route,qPerMetre);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
controls.front()[3]=std::max(controls.front()[3],
|
controls.front()[3]=std::max(controls.front()[3],
|
||||||
upperMouthFloorDepth*qPerMetre);
|
upperMouthFloorDepth*qPerMetre);
|
||||||
|
if(upperStratum!=ResolvedLayout->strates.end()
|
||||||
|
&&(upperStratum->generatorKind=="FloatingIslands"
|
||||||
|
||upperStratum->generatorKind=="TunnelNetwork"))
|
||||||
|
for(const auto& anchor:upperStratum->anchors)
|
||||||
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
|
constants.upperMouthReceivingRoute.push_back({
|
||||||
|
static_cast<double>(anchor.values[0]),
|
||||||
|
static_cast<double>(anchor.values[1]),
|
||||||
|
static_cast<double>(anchor.values[2])});
|
||||||
|
if(upperStratum!=ResolvedLayout->strates.end()
|
||||||
|
&&upperStratum->generatorKind=="TunnelNetwork"
|
||||||
|
&&constants.upperMouthReceivingRoute.size()>2)
|
||||||
|
constants.upperMouthReceivingRoute.erase(
|
||||||
|
constants.upperMouthReceivingRoute.begin(),
|
||||||
|
constants.upperMouthReceivingRoute.end()-2);
|
||||||
|
|
||||||
|
const auto lowerStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
|
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||||
|
{return stratum.strateIndex==passage.lowerStrateIndex;});
|
||||||
|
if(lowerStratum!=ResolvedLayout->strates.end()
|
||||||
|
&&lowerStratum->generatorKind=="TunnelNetwork"
|
||||||
|
&&routeEnd<controls.size())
|
||||||
|
{
|
||||||
|
double routeHeight=0.0,distanceSquared=0.0;
|
||||||
|
for(const auto& anchor:lowerStratum->anchors)
|
||||||
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
|
constants.lowerMouthReceivingRoute.push_back({
|
||||||
|
static_cast<double>(anchor.values[0]),
|
||||||
|
static_cast<double>(anchor.values[1]),
|
||||||
|
static_cast<double>(anchor.values[2])});
|
||||||
|
if(constants.lowerMouthReceivingRoute.size()>2)
|
||||||
|
constants.lowerMouthReceivingRoute.resize(2);
|
||||||
|
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(
|
||||||
|
constants.lowerMouthReceivingRoute,controls[routeEnd][0],
|
||||||
|
controls[routeEnd][1],routeHeight,distanceSquared)
|
||||||
|
&&distanceSquared<=std::pow(controls[routeEnd][3]
|
||||||
|
+2.0*qPerMetre,2.0))
|
||||||
|
{
|
||||||
|
VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute(
|
||||||
|
controls,plannedFloorProfile,routeEnd,
|
||||||
|
constants.lowerMouthReceivingRoute,qPerMetre);
|
||||||
|
constants.lowerMouthX=controls[routeEnd][0];
|
||||||
|
constants.lowerMouthY=controls[routeEnd][1];
|
||||||
|
constants.lowerMouthPlannedFloor=plannedFloorProfile[routeEnd];
|
||||||
|
if(routeEnd>0)
|
||||||
|
{
|
||||||
|
constants.lowerMouthTangentX=controls[routeEnd][0]
|
||||||
|
-controls[routeEnd-1][0];
|
||||||
|
constants.lowerMouthTangentY=controls[routeEnd][1]
|
||||||
|
-controls[routeEnd-1][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else constants.lowerMouthReceivingRoute.clear();
|
||||||
|
}
|
||||||
const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments(
|
const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments(
|
||||||
controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre,
|
controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre,
|
||||||
plannedFloorProfile,PassageSegmentChordErrorMetres);
|
plannedFloorProfile,PassageSegmentChordErrorMetres);
|
||||||
@@ -5285,11 +5372,7 @@ public:
|
|||||||
FMetricInterval caveVoid{std::numeric_limits<double>::infinity(),
|
FMetricInterval caveVoid{std::numeric_limits<double>::infinity(),
|
||||||
std::numeric_limits<double>::infinity(),true};
|
std::numeric_limits<double>::infinity(),true};
|
||||||
bool haveFeature=false;
|
bool haveFeature=false;
|
||||||
double latticeFloorSupportLowerBound=std::numeric_limits<double>::infinity();
|
double nativeFloorSupportLowerBound=std::numeric_limits<double>::infinity();
|
||||||
const auto* resolvedStratum=FindResolvedLayoutStratum(strateIndex);
|
|
||||||
const bool nativeLattice=resolvedStratum
|
|
||||||
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*resolvedStratum)
|
|
||||||
&&resolvedStratum->passageCapabilities.hasNativeLatticeField;
|
|
||||||
for(const std::uint32_t index:candidates)
|
for(const std::uint32_t index:candidates)
|
||||||
{
|
{
|
||||||
if(index>=CavernRuntime.size())continue;
|
if(index>=CavernRuntime.size())continue;
|
||||||
@@ -5319,7 +5402,18 @@ public:
|
|||||||
(std::sqrt(normalizedMinimumSquared)-1.0)*meanRadius,
|
(std::sqrt(normalizedMinimumSquared)-1.0)*meanRadius,
|
||||||
(std::sqrt(normalizedMaximumSquared)-1.0)*meanRadius,true};
|
(std::sqrt(normalizedMaximumSquared)-1.0)*meanRadius,true};
|
||||||
const double floorZ=feature.room->floorMicrometres/1000000.0;
|
const double floorZ=feature.room->floorMicrometres/1000000.0;
|
||||||
const FMetricInterval floor={floorZ-hi[2],floorZ-lo[2],true};
|
const double centerX=feature.room->centerMicrometres[0]/1000000.0;
|
||||||
|
const double centerY=feature.room->centerMicrometres[1]/1000000.0;
|
||||||
|
const double gx=feature.room->floorGradientQ16[0]/65536.0;
|
||||||
|
const double gy=feature.room->floorGradientQ16[1]/65536.0;
|
||||||
|
const double x0=lo[0]-centerX,x1=hi[0]-centerX;
|
||||||
|
const double y0=lo[1]-centerY,y1=hi[1]-centerY;
|
||||||
|
const double gradientMinimum=(gx>=0.0?gx*x0:gx*x1)
|
||||||
|
+(gy>=0.0?gy*y0:gy*y1);
|
||||||
|
const double gradientMaximum=(gx>=0.0?gx*x1:gx*x0)
|
||||||
|
+(gy>=0.0?gy*y1:gy*y0);
|
||||||
|
const FMetricInterval floor={floorZ-hi[2]+gradientMinimum,
|
||||||
|
floorZ-lo[2]+gradientMaximum,true};
|
||||||
const double width=feature.room->smoothWidthMicrometres/1000000.0;
|
const double width=feature.room->smoothWidthMicrometres/1000000.0;
|
||||||
featureVoid={SmoothMaxMetres(ellipsoid.minimum,floor.minimum,width),
|
featureVoid={SmoothMaxMetres(ellipsoid.minimum,floor.minimum,width),
|
||||||
SmoothMaxMetres(ellipsoid.maximum,floor.maximum,width),true};
|
SmoothMaxMetres(ellipsoid.maximum,floor.maximum,width),true};
|
||||||
@@ -5441,21 +5535,19 @@ public:
|
|||||||
SmoothMaxMetres(tube.maximum,floor.maximum,width),true};
|
SmoothMaxMetres(tube.maximum,floor.maximum,width),true};
|
||||||
}
|
}
|
||||||
else return {0.0,0.0,false};
|
else return {0.0,0.0,false};
|
||||||
if(nativeLattice)
|
if(feature.edge)
|
||||||
{
|
{
|
||||||
const double supportDepth=feature.edge
|
const double supportDepth=
|
||||||
?feature.edge->floorSupportDepthMicrometres/1000000.0:0.0;
|
feature.edge->floorSupportDepthMicrometres/1000000.0;
|
||||||
const double floorMinimum=feature.edge
|
const double floorMinimum=feature.minimumFloorMetres;
|
||||||
?feature.minimumFloorMetres:std::numeric_limits<double>::infinity();
|
const double floorMaximum=feature.maximumFloorMetres;
|
||||||
const double floorMaximum=feature.edge
|
|
||||||
?feature.maximumFloorMetres:-std::numeric_limits<double>::infinity();
|
|
||||||
const bool overlapsXY=hi[0]>=feature.boundsMinimumMicrometres[0]/1.0e6
|
const bool overlapsXY=hi[0]>=feature.boundsMinimumMicrometres[0]/1.0e6
|
||||||
&&lo[0]<=feature.boundsMaximumMicrometres[0]/1.0e6
|
&&lo[0]<=feature.boundsMaximumMicrometres[0]/1.0e6
|
||||||
&&hi[1]>=feature.boundsMinimumMicrometres[1]/1.0e6
|
&&hi[1]>=feature.boundsMinimumMicrometres[1]/1.0e6
|
||||||
&&lo[1]<=feature.boundsMaximumMicrometres[1]/1.0e6;
|
&&lo[1]<=feature.boundsMaximumMicrometres[1]/1.0e6;
|
||||||
if(supportDepth>0.0&&overlapsXY
|
if(supportDepth>0.0&&overlapsXY
|
||||||
&&hi[2]>=floorMinimum-supportDepth&&lo[2]<=floorMaximum)
|
&&hi[2]>=floorMinimum-supportDepth&&lo[2]<=floorMaximum)
|
||||||
latticeFloorSupportLowerBound=std::min(latticeFloorSupportLowerBound,
|
nativeFloorSupportLowerBound=std::min(nativeFloorSupportLowerBound,
|
||||||
std::max(-supportDepth,lo[2]-floorMaximum));
|
std::max(-supportDepth,lo[2]-floorMaximum));
|
||||||
}
|
}
|
||||||
if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum))
|
if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum))
|
||||||
@@ -5484,11 +5576,11 @@ public:
|
|||||||
result.minimum=std::max(HostRockMetres,-caveVoid.maximum);
|
result.minimum=std::max(HostRockMetres,-caveVoid.maximum);
|
||||||
result.maximum=std::max(HostRockMetres,-caveVoid.minimum);
|
result.maximum=std::max(HostRockMetres,-caveVoid.minimum);
|
||||||
}
|
}
|
||||||
// The final lattice field owns an extra support cap after feature union.
|
// Native graph edges may own a support cap after feature union.
|
||||||
// Lower only the interval minimum when a box could touch that cap; the
|
// Lower only the interval minimum when a box could touch that cap; the
|
||||||
// untouched maximum remains a sound bound when only part of the box does.
|
// untouched maximum remains a sound bound when only part of the box does.
|
||||||
if(std::isfinite(latticeFloorSupportLowerBound))
|
if(std::isfinite(nativeFloorSupportLowerBound))
|
||||||
result.minimum=std::min(result.minimum,latticeFloorSupportLowerBound);
|
result.minimum=std::min(result.minimum,nativeFloorSupportLowerBound);
|
||||||
// Cover floating-point rounding in interval endpoints and the field's fixed-point
|
// Cover floating-point rounding in interval endpoints and the field's fixed-point
|
||||||
// coordinate conversion while keeping the interval tight compared with one micron.
|
// coordinate conversion while keeping the interval tight compared with one micron.
|
||||||
constexpr double OutwardPaddingMetres=1.0e-6;
|
constexpr double OutwardPaddingMetres=1.0e-6;
|
||||||
@@ -7466,10 +7558,11 @@ private:
|
|||||||
candidates=&localCandidates;
|
candidates=&localCandidates;
|
||||||
}
|
}
|
||||||
double caveVoid=std::numeric_limits<double>::infinity();
|
double caveVoid=std::numeric_limits<double>::infinity();
|
||||||
double latticeFloorSupportMinimum=std::numeric_limits<double>::infinity();
|
double nativeFloorSupportMinimum=std::numeric_limits<double>::infinity();
|
||||||
const bool nativeLattice=bound.Resolved
|
double floorSupportScore=std::numeric_limits<double>::infinity();
|
||||||
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound.Resolved)
|
double floorSupportDepth=0.0,floorSupportFloor=0.0,floorSupportGrade=0.0;
|
||||||
&&bound.Resolved->passageCapabilities.hasNativeLatticeField;
|
const std::string* floorSupportEdgeId=nullptr;
|
||||||
|
const std::string* floorSupportProfileId=nullptr;
|
||||||
for(const std::uint32_t index:*candidates)
|
for(const std::uint32_t index:*candidates)
|
||||||
{
|
{
|
||||||
if(index>=CavernRuntime.size())continue;
|
if(index>=CavernRuntime.size())continue;
|
||||||
@@ -7488,8 +7581,19 @@ private:
|
|||||||
const double delta=(point[axis]-feature.centerMetres[axis])/radius;
|
const double delta=(point[axis]-feature.centerMetres[axis])/radius;
|
||||||
normalizedSquared+=delta*delta;meanRadius+=radius/3.0;
|
normalizedSquared+=delta*delta;meanRadius+=radius/3.0;
|
||||||
}
|
}
|
||||||
const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius;
|
double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius;
|
||||||
const double floor=feature.room->floorMicrometres/1000000.0-point[2];
|
if(feature.irregularityAmplitudeVoxels>0.0f)
|
||||||
|
{
|
||||||
|
const float fade=SmoothUnit((Z-feature.floorVoxels)/0.8f);
|
||||||
|
ellipsoid+=StampValueNoise(
|
||||||
|
X-feature.centerVoxels[0],Y-feature.centerVoxels[1],
|
||||||
|
Z-feature.centerVoxels[2],
|
||||||
|
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
||||||
|
*feature.irregularityAmplitudeVoxels*metresPerVoxel*fade;
|
||||||
|
}
|
||||||
|
const double floor=feature.room->floorMicrometres/1000000.0-point[2]
|
||||||
|
+feature.floorGradient[0]*(point[0]-feature.centerMetres[0])
|
||||||
|
+feature.floorGradient[1]*(point[1]-feature.centerMetres[1]);
|
||||||
const double width=feature.room->smoothWidthMicrometres/1000000.0;
|
const double width=feature.room->smoothWidthMicrometres/1000000.0;
|
||||||
featureVoid=SmoothMaxMetres(ellipsoid,floor,width);
|
featureVoid=SmoothMaxMetres(ellipsoid,floor,width);
|
||||||
}
|
}
|
||||||
@@ -7526,7 +7630,17 @@ private:
|
|||||||
}
|
}
|
||||||
const double normalized=std::sqrt(radialSquared/(radius*radius)
|
const double normalized=std::sqrt(radialSquared/(radius*radius)
|
||||||
+vertical*vertical/(height*height));
|
+vertical*vertical/(height*height));
|
||||||
const double distance=(normalized-1.0)*std::min(radius,height);
|
double distance=(normalized-1.0)*std::min(radius,height);
|
||||||
|
if(feature.irregularityAmplitudeVoxels>0.0f)
|
||||||
|
{
|
||||||
|
const float floorVoxels=static_cast<float>(nearestFloor/metresPerVoxel);
|
||||||
|
const float fade=SmoothUnit((Z-floorVoxels)/0.8f);
|
||||||
|
distance+=StampValueNoise(
|
||||||
|
X-feature.controlPointsVoxels[0][0],
|
||||||
|
Y-feature.controlPointsVoxels[0][1],Z-floorVoxels,
|
||||||
|
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
||||||
|
*feature.irregularityAmplitudeVoxels*metresPerVoxel*fade;
|
||||||
|
}
|
||||||
const bool clearlyCloser=distance<tube-SegmentDistanceTieToleranceMetres;
|
const bool clearlyCloser=distance<tube-SegmentDistanceTieToleranceMetres;
|
||||||
const bool stableTie=std::abs(distance-tube)
|
const bool stableTie=std::abs(distance-tube)
|
||||||
<=SegmentDistanceTieToleranceMetres&&segmentIndex<nearestSegment;
|
<=SegmentDistanceTieToleranceMetres&&segmentIndex<nearestSegment;
|
||||||
@@ -7580,11 +7694,48 @@ private:
|
|||||||
}
|
}
|
||||||
else for(std::uint32_t segment=0;segment<feature.segments.size();++segment)
|
else for(std::uint32_t segment=0;segment<feature.segments.size();++segment)
|
||||||
evaluateSegment(segment);
|
evaluateSegment(segment);
|
||||||
const double supportDepth=feature.edge->floorSupportDepthMicrometres/1000000.0;
|
const double candidateSupportDepth=
|
||||||
if(nativeLattice&&supportDepth>0.0&&nearestRadial<radius-0.12
|
feature.edge->floorSupportDepthMicrometres/1000000.0;
|
||||||
&&point[2]<nearestFloor&&nearestFloor-point[2]<=supportDepth)
|
if(candidateSupportDepth>0.0&&nearestRadial<radius-0.12
|
||||||
latticeFloorSupportMinimum=std::min(latticeFloorSupportMinimum,
|
&&std::abs(point[2]-nearestFloor)<=height)
|
||||||
(point[2]-nearestFloor)/std::sqrt(1.0+nearestGrade*nearestGrade));
|
{
|
||||||
|
const double deltaZ=point[2]-nearestFloor;
|
||||||
|
const double candidateScore=nearestRadial*nearestRadial+deltaZ*deltaZ;
|
||||||
|
if(!feature.edge->floorProfileStableId.empty())
|
||||||
|
{
|
||||||
|
const bool sameProfile=floorSupportProfileId
|
||||||
|
&&*floorSupportProfileId==feature.edge->floorProfileStableId;
|
||||||
|
if(!floorSupportProfileId||sameProfile)
|
||||||
|
{
|
||||||
|
// Backbone edges share h(s). At a geometric joint their
|
||||||
|
// cave volumes overlap, so the backing top is the lower
|
||||||
|
// of the local floors, never the closer higher segment.
|
||||||
|
if(!floorSupportProfileId||nearestFloor<floorSupportFloor
|
||||||
|
||(nearestFloor==floorSupportFloor
|
||||||
|
&&feature.edge->stableId<*floorSupportEdgeId))
|
||||||
|
{
|
||||||
|
floorSupportScore=candidateScore;
|
||||||
|
floorSupportDepth=candidateSupportDepth;
|
||||||
|
floorSupportFloor=nearestFloor;
|
||||||
|
floorSupportGrade=nearestGrade;
|
||||||
|
floorSupportEdgeId=&feature.edge->stableId;
|
||||||
|
}
|
||||||
|
floorSupportProfileId=&feature.edge->floorProfileStableId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(!floorSupportProfileId
|
||||||
|
&&(candidateScore<floorSupportScore
|
||||||
|
||(candidateScore==floorSupportScore
|
||||||
|
&&(!floorSupportEdgeId
|
||||||
|
||feature.edge->stableId<*floorSupportEdgeId))))
|
||||||
|
{
|
||||||
|
floorSupportScore=candidateScore;
|
||||||
|
floorSupportDepth=candidateSupportDepth;
|
||||||
|
floorSupportFloor=nearestFloor;
|
||||||
|
floorSupportGrade=nearestGrade;
|
||||||
|
floorSupportEdgeId=&feature.edge->stableId;
|
||||||
|
}
|
||||||
|
}
|
||||||
const double floor=(nearestFloor-point[2])
|
const double floor=(nearestFloor-point[2])
|
||||||
/std::sqrt(1.0+nearestGrade*nearestGrade);
|
/std::sqrt(1.0+nearestGrade*nearestGrade);
|
||||||
const double width=feature.edge->smoothWidthMicrometres/1000000.0;
|
const double width=feature.edge->smoothWidthMicrometres/1000000.0;
|
||||||
@@ -7601,6 +7752,10 @@ private:
|
|||||||
?feature.edge->unionWidthMicrometres
|
?feature.edge->unionWidthMicrometres
|
||||||
:feature.edge->smoothWidthMicrometres)/1000000.0);
|
:feature.edge->smoothWidthMicrometres)/1000000.0);
|
||||||
}
|
}
|
||||||
|
if(std::isfinite(floorSupportScore)&&point[2]<floorSupportFloor
|
||||||
|
&&floorSupportFloor-point[2]<=floorSupportDepth)
|
||||||
|
nativeFloorSupportMinimum=(point[2]-floorSupportFloor)
|
||||||
|
/std::sqrt(1.0+floorSupportGrade*floorSupportGrade);
|
||||||
// A versioned native caves recipe owns a solid host-rock field. The
|
// A versioned native caves recipe owns a solid host-rock field. The
|
||||||
// resolved interval oracle uses the same finite -32 m solid value; using
|
// resolved interval oracle uses the same finite -32 m solid value; using
|
||||||
// the underlying legacy generator density here would leak its unrelated
|
// the underlying legacy generator density here would leak its unrelated
|
||||||
@@ -7609,13 +7764,13 @@ private:
|
|||||||
const double hostDistance=HostRockMetres;
|
const double hostDistance=HostRockMetres;
|
||||||
double composed=std::isfinite(caveVoid)
|
double composed=std::isfinite(caveVoid)
|
||||||
?SmoothMaxMetres(hostDistance,-caveVoid,0.0):hostDistance;
|
?SmoothMaxMetres(hostDistance,-caveVoid,0.0):hostDistance;
|
||||||
// Keep the floor support owned by the lattice route after smooth-unioning
|
// Preserve each native graph edge's planned floor backing after smooth
|
||||||
// neighboring room and corridor voids. This cap is limited to each record's
|
// unions. The record's finite depth keeps independent routes from sealing
|
||||||
// explicit support depth so separated helix turns remain independent.
|
// one another when they pass at different elevations.
|
||||||
if(std::isfinite(latticeFloorSupportMinimum))
|
if(std::isfinite(nativeFloorSupportMinimum))
|
||||||
composed=std::min(composed,latticeFloorSupportMinimum);
|
composed=std::min(composed,nativeFloorSupportMinimum);
|
||||||
if(nativeFloorSupport&&std::isfinite(latticeFloorSupportMinimum))
|
if(nativeFloorSupport&&std::isfinite(nativeFloorSupportMinimum))
|
||||||
*nativeFloorSupport=static_cast<float>(latticeFloorSupportMinimum/metresPerVoxel);
|
*nativeFloorSupport=static_cast<float>(nativeFloorSupportMinimum/metresPerVoxel);
|
||||||
return static_cast<float>(composed/metresPerVoxel);
|
return static_cast<float>(composed/metresPerVoxel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# L133 — native floating islands
|
|
||||||
|
|
||||||
Stopped after the second route design. The native v9 plan binds the owner’s resolved mouths and passes raw route audit, strict layout (0 issues), 3/3 interface audits, shell (0 floor violations), and ripple (3/3). The candidate dispatches FI natively.
|
|
||||||
|
|
||||||
The composed FI walk still fails: 2 capsule collisions, 40 unsupported centers, 0 grade failures; mouth binding passes. The return lane is carved by `passage/0-1/0` near `(19.755, 9.605, -733.553) m` (4.25 m from its 4.5 m tube). At `passage/1-2/0`, the approach has collisions at `(14.402, -10.552, -732.556) m` and `(14.242, -10.712, -732.556) m`; the departure landing probe `(19.166, -14.650, -733.331) m` gets 0/5 support probes. The exact endpoints alone are not enough to place a route clear of both composed passage volumes and retain a supported landing.
|
|
||||||
|
|
||||||
No captures, timing matrix, compiler determinism matrix, baseline update, or UE host build were run after the required composed walk gate failed. The owner’s default import remains on its legacy FI fallback. Logs and candidate fixtures are under `Saved/Linux/l133/`.
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
||||||
index cb2b2dd..5a94ab8 100644
|
|
||||||
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
||||||
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
||||||
@@ -496,8 +496,9 @@ namespace VoxelForgeCore
|
|
||||||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
|
||||||
// Version 226 unifies the passage influence reach in metres and converts it to core
|
|
||||||
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
|
||||||
- // support width/6 when deriving that reach.
|
|
||||||
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
|
|
||||||
+ // support width/6 when deriving that reach. Version 228 permits the native FI route
|
|
||||||
+ // planner to bind its fixed endpoints to the resolved passage mouths.
|
|
||||||
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228;
|
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
|
||||||
{
|
|
||||||
@@ -788,6 +789,15 @@ namespace VoxelForgeCore
|
|
||||||
std::uint64_t contentHash = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
+ // The two resolved inter-strate walk points owned by a FloatingIslands slot.
|
|
||||||
+ // The route planner treats these as fixed player-capsule centers and places
|
|
||||||
+ // the island chain between them.
|
|
||||||
+ struct FloatingIslandRouteEndpointsV1
|
|
||||||
+ {
|
|
||||||
+ std::array<std::int64_t, 3> arrivalSubvoxels{};
|
|
||||||
+ std::array<std::int64_t, 3> departureSubvoxels{};
|
|
||||||
+ };
|
|
||||||
+
|
|
||||||
struct WorldAssemblyFeatureGroupBoundsV1
|
|
||||||
{
|
|
||||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
|
||||||
@@ -1377,6 +1387,9 @@ namespace VoxelForgeCore
|
|
||||||
// Planner output is ordinary reviewed data; clients can load it without re-running the planner.
|
|
||||||
VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
const FloatingIslandRecipeV1& recipe, std::string& error);
|
|
||||||
+ VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
+ const FloatingIslandRecipeV1& recipe,
|
|
||||||
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error);
|
|
||||||
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
|
|
||||||
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
|
|
||||||
std::uint64_t seed = 0x49534c414e444231ull);
|
|
||||||
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
||||||
index 33d97d6..42f855a 100644
|
|
||||||
--- a/Standalone/Core/src/FloatingIslandsInternal.h
|
|
||||||
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
||||||
@@ -285,7 +285,7 @@ namespace floating_island_detail
|
|
||||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
|
||||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
|
||||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
|
||||||
- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
|
||||||
+ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 9
|
|
||||||
|| (recipe.geometryVersion == 1
|
|
||||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
|
||||||
|| recipe.unitsPerVoxel != Q
|
|
||||||
@@ -1473,7 +1473,8 @@ namespace floating_island_detail
|
|
||||||
static_cast<double>(anchorIsland->centerSubvoxels[0]),
|
|
||||||
static_cast<double>(anchorIsland->centerSubvoxels[1]));
|
|
||||||
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
|
|
||||||
- ||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
|
|
||||||
+ ||(recipe.geometryVersion<8
|
|
||||||
+ &&recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels)
|
|
||||||
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
>=anchorIsland->radiusSubvoxels)
|
|
||||||
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
|
|
||||||
@@ -1607,7 +1608,7 @@ namespace floating_island_detail
|
|
||||||
||to.ordinal==recipe.anchorIslandOrdinal);
|
|
||||||
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
|
||||||
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
|
||||||
- const double routeDeckLift=recipe.geometryVersion>=2
|
|
||||||
+ const double routeDeckLift=recipe.geometryVersion>=9?0.0:recipe.geometryVersion>=2
|
|
||||||
?(WorldAssemblyEvaluatorVersion>=178
|
|
||||||
?(westernSupportConnector
|
|
||||||
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
|
||||||
@@ -2264,6 +2265,335 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
return manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
+ const FloatingIslandRecipeV1& inputRecipe,
|
|
||||||
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error)
|
|
||||||
+{
|
|
||||||
+ FloatingIslandManifestV1 manifest;
|
|
||||||
+ manifest.recipe = inputRecipe;
|
|
||||||
+ auto& recipe = manifest.recipe;
|
|
||||||
+ recipe.geometryVersion = 9;
|
|
||||||
+ recipe.primaryIslandCount = 9;
|
|
||||||
+ recipe.anchorIslandOrdinal = recipe.primaryIslandCount - 1;
|
|
||||||
+ constexpr std::int64_t step = 192 * floating_island_detail::Q;
|
|
||||||
+ recipe.spacingSubvoxels = step;
|
|
||||||
+ if (recipe.routeConstraints.version == 0)
|
|
||||||
+ {
|
|
||||||
+ recipe.routeConstraints.version = 1;
|
|
||||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
|
|
||||||
+ recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
|
|
||||||
+ recipe.routeConstraints.maximumGradeQ16 = 17560;
|
|
||||||
+ recipe.routeConstraints.sweepSpacingSubvoxels = 128;
|
|
||||||
+ recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
|
|
||||||
+ recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
|
|
||||||
+ recipe.routeConstraints.minimumClearanceSubvoxels = 128;
|
|
||||||
+ recipe.routeConstraints.minimumSupportProbes = 3;
|
|
||||||
+ }
|
|
||||||
+ for (const auto& value : endpoints.arrivalSubvoxels)
|
|
||||||
+ if (!floating_island_detail::ValidScalar(value))
|
|
||||||
+ { error = "resolved floating-island arrival mouth is outside the fixed-point range"; return std::nullopt; }
|
|
||||||
+ for (const auto& value : endpoints.departureSubvoxels)
|
|
||||||
+ if (!floating_island_detail::ValidScalar(value))
|
|
||||||
+ { error = "resolved floating-island departure mouth is outside the fixed-point range"; return std::nullopt; }
|
|
||||||
+
|
|
||||||
+ const double qPerMetre = static_cast<double>(floating_island_detail::Q)
|
|
||||||
+ * 1000000.0 / recipe.coreVoxelSizeMicrons;
|
|
||||||
+ const double dx = static_cast<double>(endpoints.departureSubvoxels[0]
|
|
||||||
+ - endpoints.arrivalSubvoxels[0]);
|
|
||||||
+ const double dy = static_cast<double>(endpoints.departureSubvoxels[1]
|
|
||||||
+ - endpoints.arrivalSubvoxels[1]);
|
|
||||||
+ const double endpointDistance = std::hypot(dx, dy);
|
|
||||||
+ const std::int64_t maximumMouthOffset = 64 * floating_island_detail::Q
|
|
||||||
+ - recipe.routeConstraints.capsuleRadiusSubvoxels - floating_island_detail::Q;
|
|
||||||
+ const std::int64_t desiredMouthOffset = std::max(recipe.anchorMouthOffsetSubvoxels,
|
|
||||||
+ static_cast<std::int64_t>(std::llround(qPerMetre * 6.6)));
|
|
||||||
+ recipe.anchorMouthOffsetSubvoxels = std::min({desiredMouthOffset,
|
|
||||||
+ maximumMouthOffset, static_cast<std::int64_t>(std::llround(endpointDistance))});
|
|
||||||
+ if (recipe.anchorMouthOffsetSubvoxels <= 0)
|
|
||||||
+ { error = "resolved island mouths cannot provide a bounded departure approach"; return std::nullopt; }
|
|
||||||
+ recipe.anchorRootRadiusSubvoxels = std::max(recipe.anchorRootRadiusSubvoxels,
|
|
||||||
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
+ + floating_island_detail::Q);
|
|
||||||
+ recipe.anchorIslandMinimumRadiusSubvoxels = std::max(recipe.minimumRadiusSubvoxels,
|
|
||||||
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
+ + floating_island_detail::Q + 1);
|
|
||||||
+ // Keep the resolved fixed mouth on a capsule-grade ramp at the anchor island lip.
|
|
||||||
+ recipe.topFlattenQ16 = std::max<std::int64_t>(recipe.topFlattenQ16, 52429);
|
|
||||||
+ recipe.bridgeHalfWidthSubvoxels = std::max(recipe.bridgeHalfWidthSubvoxels,
|
|
||||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels + 2 * floating_island_detail::Q);
|
|
||||||
+ if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt;
|
|
||||||
+
|
|
||||||
+ auto& islands = manifest.plan.islands;
|
|
||||||
+ auto& edges = manifest.plan.clusterEdges;
|
|
||||||
+ auto& route = manifest.plan.route;
|
|
||||||
+ constexpr std::uint32_t count = 9;
|
|
||||||
+ double tangentX = endpointDistance > 1.0
|
|
||||||
+ ? dx / endpointDistance : 1.0;
|
|
||||||
+ double tangentY = endpointDistance > 1.0
|
|
||||||
+ ? dy / endpointDistance : 0.0;
|
|
||||||
+ if (endpointDistance <= 1.0)
|
|
||||||
+ {
|
|
||||||
+ const double angle = static_cast<double>(floating_island_detail::UnitQ16(
|
|
||||||
+ recipe.seed, 0x900000000ull, 0x524f555445ull))
|
|
||||||
+ * (2.0 * floating_island_detail::Pi / 65536.0);
|
|
||||||
+ tangentX = std::cos(angle);
|
|
||||||
+ tangentY = std::sin(angle);
|
|
||||||
+ }
|
|
||||||
+ const double normalX = -tangentY;
|
|
||||||
+ const double normalY = tangentX;
|
|
||||||
+ const auto localPoint = [&](double along, double side)
|
|
||||||
+ {
|
|
||||||
+ return std::array<std::int64_t, 2>{
|
|
||||||
+ floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(endpoints.arrivalSubvoxels[0])
|
|
||||||
+ + tangentX * along + normalX * side),
|
|
||||||
+ floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(endpoints.arrivalSubvoxels[1])
|
|
||||||
+ + tangentY * along + normalY * side)};
|
|
||||||
+ };
|
|
||||||
+ std::array<std::array<std::int64_t, 2>, count> primaryXY{};
|
|
||||||
+ primaryXY[0] = {endpoints.arrivalSubvoxels[0], endpoints.arrivalSubvoxels[1]};
|
|
||||||
+ primaryXY[1] = localPoint(0.0, static_cast<double>(step));
|
|
||||||
+ primaryXY[2] = localPoint(static_cast<double>(step), static_cast<double>(step));
|
|
||||||
+ primaryXY[3] = localPoint(static_cast<double>(step), 0.0);
|
|
||||||
+ primaryXY[4] = localPoint(static_cast<double>(step), -static_cast<double>(step));
|
|
||||||
+ primaryXY[5] = localPoint(0.0, -static_cast<double>(step));
|
|
||||||
+ primaryXY[6] = localPoint(-static_cast<double>(step), -static_cast<double>(step));
|
|
||||||
+ // Keep the return lane outside the arrival passage's spiral and on the negative
|
|
||||||
+ // side of the first connector, so the two bridges cannot cross in plan view.
|
|
||||||
+ // Keep the final approach radial at the passage mouth. This lets the shared
|
|
||||||
+ // passage resolver keep its authored route inside the placement disk.
|
|
||||||
+ primaryXY[7] = localPoint(-0.85 * static_cast<double>(step),
|
|
||||||
+ -0.23 * static_cast<double>(step));
|
|
||||||
+ const double departureRadial = std::hypot(
|
|
||||||
+ static_cast<double>(endpoints.departureSubvoxels[0]),
|
|
||||||
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
|
||||||
+ const double anchorScale = departureRadial > 1.0
|
|
||||||
+ ? std::max(0.0, (departureRadial - recipe.anchorMouthOffsetSubvoxels) / departureRadial)
|
|
||||||
+ : 0.0;
|
|
||||||
+ primaryXY[8] = {
|
|
||||||
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[0] * anchorScale),
|
|
||||||
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[1] * anchorScale)};
|
|
||||||
+
|
|
||||||
+ std::array<double, count> cumulative{};
|
|
||||||
+ for (std::uint32_t i = 1; i < count; ++i)
|
|
||||||
+ {
|
|
||||||
+ const double segmentX = static_cast<double>(primaryXY[i][0] - primaryXY[i - 1][0]);
|
|
||||||
+ const double segmentY = static_cast<double>(primaryXY[i][1] - primaryXY[i - 1][1]);
|
|
||||||
+ const double length = std::hypot(segmentX, segmentY);
|
|
||||||
+ if (!(length > 0.0) || length > static_cast<double>(256 * floating_island_detail::Q))
|
|
||||||
+ { error = "resolved island mouths require an unsupported serpentine segment length"; return std::nullopt; }
|
|
||||||
+ cumulative[i] = cumulative[i - 1] + length;
|
|
||||||
+ }
|
|
||||||
+ const double totalLength = cumulative.back();
|
|
||||||
+ if (!(totalLength > 0.0))
|
|
||||||
+ { error = "resolved floating-island mouths do not define a route"; return std::nullopt; }
|
|
||||||
+ const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
|
|
||||||
+ const std::int64_t routeOffset = capsuleHalf
|
|
||||||
+ + recipe.routeConstraints.minimumClearanceSubvoxels
|
|
||||||
+ + floating_island_detail::Q / 8;
|
|
||||||
+ const std::int64_t startTop = endpoints.arrivalSubvoxels[2] - routeOffset;
|
|
||||||
+ const std::int64_t endTop = endpoints.departureSubvoxels[2] - routeOffset;
|
|
||||||
+ const std::uint64_t anchorSite = 0x700000000ull + recipe.anchorIslandOrdinal;
|
|
||||||
+ auto anchorShape = floating_island_detail::MakeIsland(recipe,
|
|
||||||
+ recipe.anchorIslandOrdinal, primaryXY.back()[0], primaryXY.back()[1], endTop, anchorSite);
|
|
||||||
+ anchorShape.radiusSubvoxels = std::max(anchorShape.radiusSubvoxels,
|
|
||||||
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
||||||
+ anchorShape.topHalfSubvoxels = std::max<std::int64_t>(1,
|
|
||||||
+ anchorShape.radiusSubvoxels / 5);
|
|
||||||
+ anchorShape.centerSubvoxels[2] = endTop - anchorShape.topHalfSubvoxels;
|
|
||||||
+ std::int64_t anchorTop = endTop;
|
|
||||||
+ for (int iteration = 0; iteration < 4; ++iteration)
|
|
||||||
+ {
|
|
||||||
+ anchorShape.topZSubvoxels = anchorTop;
|
|
||||||
+ anchorShape.centerSubvoxels[2] = anchorTop - anchorShape.topHalfSubvoxels;
|
|
||||||
+ const double mouthSurface = floating_island_detail::IslandTopSurface(recipe,
|
|
||||||
+ anchorShape, static_cast<double>(endpoints.departureSubvoxels[0]),
|
|
||||||
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
|
||||||
+ anchorTop = endTop + floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(anchorTop) - mouthSurface);
|
|
||||||
+ }
|
|
||||||
+ std::array<std::int64_t, count> tops{};
|
|
||||||
+ for (std::uint32_t i = 0; i < count; ++i)
|
|
||||||
+ {
|
|
||||||
+ const double t = cumulative[i] / totalLength;
|
|
||||||
+ tops[i] = floating_island_detail::RoundQ(static_cast<double>(startTop)
|
|
||||||
+ + static_cast<double>(anchorTop - startTop) * t);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ islands.reserve(count);
|
|
||||||
+ for (std::uint32_t i = 0; i < count; ++i)
|
|
||||||
+ {
|
|
||||||
+ const std::uint64_t site = 0x700000000ull + i;
|
|
||||||
+ auto island = floating_island_detail::MakeIsland(recipe, i,
|
|
||||||
+ primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
|
||||||
+ if (i == recipe.anchorIslandOrdinal)
|
|
||||||
+ {
|
|
||||||
+ island.radiusSubvoxels = std::max(island.radiusSubvoxels,
|
|
||||||
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
||||||
+ }
|
|
||||||
+ const std::int64_t interiorBottom = recipe.layoutMinimumZSubvoxels
|
|
||||||
+ + recipe.boundarySealSubvoxels;
|
|
||||||
+ while (island.radiusSubvoxels > recipe.minimumRadiusSubvoxels
|
|
||||||
+ && floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
|
||||||
+ < interiorBottom)
|
|
||||||
+ --island.radiusSubvoxels;
|
|
||||||
+ const std::int64_t requiredRadius = i == recipe.anchorIslandOrdinal
|
|
||||||
+ ? recipe.anchorIslandMinimumRadiusSubvoxels : recipe.minimumRadiusSubvoxels;
|
|
||||||
+ if (island.radiusSubvoxels < requiredRadius
|
|
||||||
+ || floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
|
||||||
+ < interiorBottom)
|
|
||||||
+ {
|
|
||||||
+ error = "resolved-mouth island " + std::to_string(i) + " cannot fit above sealed bedrock"
|
|
||||||
+ + " (radius=" + std::to_string(island.radiusSubvoxels)
|
|
||||||
+ + ", required=" + std::to_string(requiredRadius)
|
|
||||||
+ + ", top=" + std::to_string(tops[i])
|
|
||||||
+ + ", bottom=" + std::to_string(floating_island_detail::IslandBottom(
|
|
||||||
+ recipe, tops[i], island.radiusSubvoxels))
|
|
||||||
+ + ", interior_bottom=" + std::to_string(interiorBottom)
|
|
||||||
+ + ", anchor_top=" + std::to_string(anchorTop)
|
|
||||||
+ + ", mouth_offset=" + std::to_string(recipe.anchorMouthOffsetSubvoxels) + ")";
|
|
||||||
+ return std::nullopt;
|
|
||||||
+ }
|
|
||||||
+ island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
|
||||||
+ island.topZSubvoxels = tops[i];
|
|
||||||
+ island.centerSubvoxels[2] = tops[i] - island.topHalfSubvoxels;
|
|
||||||
+ island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
|
||||||
+ island.topZSubvoxels, island.radiusSubvoxels);
|
|
||||||
+ floating_island_detail::SetIslandBounds(recipe, island);
|
|
||||||
+ islands.push_back(std::move(island));
|
|
||||||
+ }
|
|
||||||
+ for (std::uint32_t i = 0; i + 1 < count; ++i)
|
|
||||||
+ edges.push_back(floating_island_detail::MakeConnector(recipe,
|
|
||||||
+ islands[i], islands[i + 1], 0x600000000ull + i));
|
|
||||||
+
|
|
||||||
+ FloatingIslandFieldV1 planningField(recipe, manifest.plan);
|
|
||||||
+ if (!planningField.isValid())
|
|
||||||
+ { error = "resolved-mouth island field did not pass the Core field contract"; return std::nullopt; }
|
|
||||||
+ const std::int64_t routeRadius = 4 * floating_island_detail::Q;
|
|
||||||
+ std::uint32_t ordinal = 0;
|
|
||||||
+ for (std::uint32_t segment = 0; segment + 1 < count; ++segment)
|
|
||||||
+ {
|
|
||||||
+ const double segmentX = static_cast<double>(primaryXY[segment + 1][0]
|
|
||||||
+ - primaryXY[segment][0]);
|
|
||||||
+ const double segmentY = static_cast<double>(primaryXY[segment + 1][1]
|
|
||||||
+ - primaryXY[segment][1]);
|
|
||||||
+ const double length = std::hypot(segmentX, segmentY);
|
|
||||||
+ const std::uint32_t pieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
||||||
+ std::ceil(length / (4.0 * floating_island_detail::Q))));
|
|
||||||
+ for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece)
|
|
||||||
+ {
|
|
||||||
+ const double t = static_cast<double>(piece) / pieces;
|
|
||||||
+ const auto x = floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(primaryXY[segment][0]) + segmentX * t);
|
|
||||||
+ const auto y = floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(primaryXY[segment][1]) + segmentY * t);
|
|
||||||
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
|
||||||
+ recipe, manifest.plan, x, y);
|
|
||||||
+ if (!surface)
|
|
||||||
+ { error = "resolved-mouth island chain has no capsule-width support along a connector"; return std::nullopt; }
|
|
||||||
+ WorldAssemblyRoutePointV1 point;
|
|
||||||
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
||||||
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
|
||||||
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
|
||||||
+ point.radiusSubvoxels = routeRadius;
|
|
||||||
+ if (segment == 0 && piece == 0)
|
|
||||||
+ point.positionSubvoxels = endpoints.arrivalSubvoxels;
|
|
||||||
+ route.push_back(point);
|
|
||||||
+ ++ordinal;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ const double mouthLegX = static_cast<double>(endpoints.departureSubvoxels[0]
|
|
||||||
+ - primaryXY.back()[0]);
|
|
||||||
+ const double mouthLegY = static_cast<double>(endpoints.departureSubvoxels[1]
|
|
||||||
+ - primaryXY.back()[1]);
|
|
||||||
+ const double mouthLegLength = std::hypot(mouthLegX, mouthLegY);
|
|
||||||
+ const std::uint32_t mouthLegPieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
||||||
+ std::ceil(mouthLegLength / (4.0 * floating_island_detail::Q))));
|
|
||||||
+ for (std::uint32_t piece = 1; piece <= mouthLegPieces; ++piece)
|
|
||||||
+ {
|
|
||||||
+ const double t = static_cast<double>(piece) / mouthLegPieces;
|
|
||||||
+ const auto x = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[0])
|
|
||||||
+ + mouthLegX * t);
|
|
||||||
+ const auto y = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[1])
|
|
||||||
+ + mouthLegY * t);
|
|
||||||
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
|
||||||
+ recipe, manifest.plan, x, y);
|
|
||||||
+ if (!surface)
|
|
||||||
+ { error = "resolved island passage mouth has no capsule-width support along its anchor landing at x="
|
|
||||||
+ + std::to_string(x) + ", y=" + std::to_string(y) + ", piece="
|
|
||||||
+ + std::to_string(piece) + "/" + std::to_string(mouthLegPieces)
|
|
||||||
+ + ", anchor_radius=" + std::to_string(anchorShape.radiusSubvoxels); return std::nullopt; }
|
|
||||||
+ WorldAssemblyRoutePointV1 point;
|
|
||||||
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
||||||
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
|
||||||
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
|
||||||
+ point.radiusSubvoxels = routeRadius;
|
|
||||||
+ if (piece == mouthLegPieces)
|
|
||||||
+ point.positionSubvoxels = endpoints.departureSubvoxels;
|
|
||||||
+ route.push_back(point);
|
|
||||||
+ ++ordinal;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ const double maximumGrade = floating_island_detail::Q16ToDouble(
|
|
||||||
+ recipe.routeConstraints.maximumGradeQ16) * 0.95;
|
|
||||||
+ std::vector<std::int64_t> baseHeights;
|
|
||||||
+ std::vector<double> routeDistance(route.size(), 0.0);
|
|
||||||
+ baseHeights.reserve(route.size());
|
|
||||||
+ for (std::size_t i = 0; i < route.size(); ++i)
|
|
||||||
+ {
|
|
||||||
+ baseHeights.push_back(route[i].positionSubvoxels[2]);
|
|
||||||
+ if (i > 0)
|
|
||||||
+ {
|
|
||||||
+ const double dx = static_cast<double>(route[i].positionSubvoxels[0]
|
|
||||||
+ - route[i - 1].positionSubvoxels[0]);
|
|
||||||
+ const double dy = static_cast<double>(route[i].positionSubvoxels[1]
|
|
||||||
+ - route[i - 1].positionSubvoxels[1]);
|
|
||||||
+ routeDistance[i] = routeDistance[i - 1] + std::hypot(dx, dy);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ for (std::size_t i = 0; i < route.size(); ++i)
|
|
||||||
+ {
|
|
||||||
+ double profile = static_cast<double>(baseHeights[i]);
|
|
||||||
+ std::size_t profileSource = i;
|
|
||||||
+ for (std::size_t j = 0; j < route.size(); ++j)
|
|
||||||
+ {
|
|
||||||
+ const double pathDistance = std::abs(routeDistance[i] - routeDistance[j]);
|
|
||||||
+ const double candidate = static_cast<double>(baseHeights[j])
|
|
||||||
+ - maximumGrade * pathDistance;
|
|
||||||
+ if (candidate > profile)
|
|
||||||
+ {
|
|
||||||
+ profile = candidate;
|
|
||||||
+ profileSource = j;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ const std::int64_t plannedHeight = floating_island_detail::RoundQ(profile);
|
|
||||||
+ const bool fixedEndpoint = i == 0 || i + 1 == route.size();
|
|
||||||
+ if (fixedEndpoint && plannedHeight != baseHeights[i])
|
|
||||||
+ {
|
|
||||||
+ error = "resolved fixed island mouth endpoint " + std::to_string(i)
|
|
||||||
+ + " needs " + std::to_string(plannedHeight - baseHeights[i])
|
|
||||||
+ + " extra subvoxels from route sample " + std::to_string(profileSource)
|
|
||||||
+ + " at z=" + std::to_string(baseHeights[profileSource])
|
|
||||||
+ + " and path distance=" + std::to_string(static_cast<std::int64_t>(
|
|
||||||
+ std::abs(routeDistance[i] - routeDistance[profileSource])))
|
|
||||||
+ + " (arrival_z="
|
|
||||||
+ + std::to_string(endpoints.arrivalSubvoxels[2]) + ", departure_z="
|
|
||||||
+ + std::to_string(endpoints.departureSubvoxels[2]) + ", path_length="
|
|
||||||
+ + std::to_string(static_cast<std::int64_t>(routeDistance.back())) + ")";
|
|
||||||
+ return std::nullopt;
|
|
||||||
+ }
|
|
||||||
+ route[i].positionSubvoxels[2] = plannedHeight;
|
|
||||||
+ }
|
|
||||||
+ if (!manifest.finalize(error)) return std::nullopt;
|
|
||||||
+ const std::string encoded = manifest.ToJson();
|
|
||||||
+ const auto roundTrip = FloatingIslandManifestV1::FromJson(encoded, error);
|
|
||||||
+ if (!roundTrip || roundTrip->ToJson() != encoded
|
|
||||||
+ || roundTrip->contentHash != manifest.contentHash)
|
|
||||||
+ { if (error.empty()) error = "resolved-mouth floating-island plan did not canonical-round-trip"; return std::nullopt; }
|
|
||||||
+ return manifest;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
|
|
||||||
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
|
|
||||||
{}
|
|
||||||
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
||||||
index a0cb0a7..468e315 100644
|
|
||||||
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
||||||
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
||||||
@@ -1826,12 +1826,59 @@ int main(int argc,char** argv)
|
|
||||||
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
|
||||||
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
|
||||||
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
|
||||||
- if(((!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
|
||||||
- ||(options.NativeOwnerFloatingIslandsRecipe
|
|
||||||
- &&worldAssembly.floatingIslands.empty()))
|
|
||||||
- &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
||||||
- *ownerWorld,ownerJsonRoot,worldAssembly,error))
|
|
||||||
- {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
||||||
+ const bool attachFloatingIslandPlan=(!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
|
||||||
+ ||(options.NativeOwnerFloatingIslandsRecipe&&worldAssembly.floatingIslands.empty());
|
|
||||||
+ if(attachFloatingIslandPlan)
|
|
||||||
+ {
|
|
||||||
+ const FStrateSlot* islandSlot=nullptr;
|
|
||||||
+ if(ownerWorld->Manager)
|
|
||||||
+ for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
|
||||||
+ if(slot.Definition&&slot.Definition->GeneratorType==ECaveGeneratorType::FloatingIslands)
|
|
||||||
+ {islandSlot=&slot;break;}
|
|
||||||
+ if(!islandSlot)
|
|
||||||
+ {std::fprintf(stderr,"Floating-island endpoint resolution found no FloatingIslands strate.\n");return 2;}
|
|
||||||
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 mouthLayout;
|
|
||||||
+ if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
|
||||||
+ *ownerWorld,worldAssembly,"floating-islands",mouthLayout,error))
|
|
||||||
+ {std::fprintf(stderr,"Pre-plan passage layout failed: %s\n",error.c_str());return 2;}
|
|
||||||
+ VoxelForgeCore::FloatingIslandRouteEndpointsV1 endpoints;
|
|
||||||
+ bool hasArrival=false,hasDeparture=false;
|
|
||||||
+ for(const auto& passage:mouthLayout.passages)
|
|
||||||
+ {
|
|
||||||
+ const std::size_t lastSlash=passage.stableId.rfind('/');
|
|
||||||
+ if(lastSlash==std::string::npos||passage.stableId.substr(lastSlash+1)!="0")continue;
|
|
||||||
+ if(passage.lowerStrateIndex==islandSlot->StrateIndex&&!hasArrival)
|
|
||||||
+ {
|
|
||||||
+ endpoints.arrivalSubvoxels=passage.lowerMouthSubvoxels;
|
|
||||||
+ hasArrival=true;
|
|
||||||
+ }
|
|
||||||
+ if(passage.upperStrateIndex==islandSlot->StrateIndex&&!hasDeparture)
|
|
||||||
+ {
|
|
||||||
+ endpoints.departureSubvoxels=passage.upperMouthSubvoxels;
|
|
||||||
+ hasDeparture=true;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ if(!hasArrival||!hasDeparture)
|
|
||||||
+ {std::fprintf(stderr,"Resolved passage layout does not provide both FI route mouths (arrival=%s departure=%s).\n",
|
|
||||||
+ hasArrival?"yes":"no",hasDeparture?"yes":"no");return 2;}
|
|
||||||
+ std::printf("[FloatingIslandResolvedMouths] strate=%d arrival_m=(%.3f,%.3f,%.3f) departure_m=(%.3f,%.3f,%.3f) status=PASS\n",
|
|
||||||
+ islandSlot->StrateIndex,
|
|
||||||
+ endpoints.arrivalSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.arrivalSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.arrivalSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.departureSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.departureSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.departureSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
||||||
+ if(!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
||||||
+ *ownerWorld,ownerJsonRoot,worldAssembly,endpoints,error))
|
|
||||||
+ {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
||||||
+ }
|
|
||||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
|
||||||
{
|
|
||||||
const std::string manifestText=worldAssembly.ToJson();
|
|
||||||
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
|
|
||||||
index dbe2a7c..9790f2f 100644
|
|
||||||
--- a/Standalone/Source/WorldAssemblyPrototype.h
|
|
||||||
+++ b/Standalone/Source/WorldAssemblyPrototype.h
|
|
||||||
@@ -923,7 +923,8 @@ namespace StandaloneWorldAssemblyPrototype
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
|
|
||||||
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
|
|
||||||
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
|
|
||||||
+ const VoxelForgeCore::FloatingIslandRouteEndpointsV1& endpoints,std::string& error)
|
|
||||||
{
|
|
||||||
if(!owner.Manager)
|
|
||||||
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
|
|
||||||
@@ -996,7 +997,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|
||||||
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
|
|
||||||
+recipe.boundarySealSubvoxels;
|
|
||||||
|
|
||||||
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
|
|
||||||
+ auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,endpoints,error);
|
|
||||||
if(!planned)return false;
|
|
||||||
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
|
|
||||||
planned->recipe,planned->plan);
|
|
||||||
@@ -1217,6 +1218,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|
||||||
:it->manifest.plan.route.size()-1;
|
|
||||||
std::size_t resolvedRoutePointCount=it->manifest.plan.route.size();
|
|
||||||
if(it->manifest.recipe.geometryVersion>=5
|
|
||||||
+ &&it->manifest.recipe.geometryVersion<9
|
|
||||||
&&!it->manifest.plan.route.empty()
|
|
||||||
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
|
|
||||||
{
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Linux134 — Tunnel Network cave planner
|
||||||
|
|
||||||
|
`TunnelNetwork` dispatches native `Core.Caves.PrimitiveField` v1. Seed 0 is strict-valid: 30 compound rooms, 72 room primitives, 31 edges, 3 branches, and a destination chamber. Its 27-point backbone spans 400.620 m horizontally with a 59.021 m floor change and an 8.381° maximum grade.
|
||||||
|
|
||||||
|
Design 4 gives backbone rooms and edges one continuous floor profile `h(s)`. Edge curves follow that profile; each room primitive is placed at its own profiled floor. Side branches keep independent flat floors. Overlap backing is capped at the lower floor. This removes the descending-joint capsule blockage. The incoming passage remains fully open: 21,425/21,425 samples.
|
||||||
|
|
||||||
|
The final-source fast tier passes in 370.8 s, including owner passage, shell, floor, ripple and negative-control audits. The default composed route audit passes: 0 blocked, 0 unsupported, 0 grade failures across 51,975 cave-route capsule samples; blocked-route controls detect their controls. The tile seam audit also passes (4 pairs, 4,900 shared samples, 0 mismatches).
|
||||||
|
|
||||||
|
Seed 2's cave route passes independently (0 blocked, 0 unsupported, 0 grade failures), but its full composed audit still fails at `passage/1-2/0`: 22 unsupported samples, first at `(15.153, 5.830, -754.680) m`. Seed 1's cave route has 24 blocked samples at the arrival mouth, beginning near `(8.380, -12.618, -864.420) m` on `passage/2-3/0`. Explicit seeds 0 and 3 produce no sweepable route candidates. The default seed is the only fully green composed audit among these runs.
|
||||||
|
|
||||||
|
Clang and GCC W1/W8/W12 match all 13 pipeline hash lines at the cave camera; each route audit passes. Seven legacy/planner cutaway pairs cover the arrival room, two backbone rooms, two tunnels, destination chamber, and departure room. All planner views render; several legacy cutaways show sparse geometry. Review them in `Saved/Linux/l134/captures/COMPARE.html`.
|
||||||
|
|
||||||
|
L80 timing used five interleaved samples per binary at W8/W12. Values below are the planner median change versus `assembler/world`; each cell is whole-view ready / LOD0–2 ready.
|
||||||
|
|
||||||
|
| Strate | W8 | W12 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 0 | -0.70% / +1.52% | +0.97% / +3.50% |
|
||||||
|
| 1 | +3.81% / +8.38% | +2.41% / +6.19% |
|
||||||
|
| 2 | +3.38% / +12.90% | +3.98% / +10.26% |
|
||||||
|
| 3 | +7.97% / +13.54% | +1.93% / +7.67% |
|
||||||
|
|
||||||
|
The VFHost Linux Development build succeeds with `-NoUBA -MaxParallelActions=4`. Final Clang/GCC source stamp: `f3b69ea0ce186c855643702a88e0d0732c81c18ec93c17d1824c25210e6408a3`. Canonical owner hashes are pinned at v230.
|
||||||
|
|
||||||
|
Logs and timing data are under `Saved/Linux/l134`; the timing summary is `timing/v230-summary.json`, the final fast log is `verify-fast-final-source.log`, and the host build log is `ubt.log`.
|
||||||
|
|
||||||
|
The separate legacy `--audit-native-caves` signature check still fails its mouth offset (it expects 1.00 m; the route contract is 1.13 m) and ripple span (1.8878 m against its 1.25 m cap). Its center-air, floor-solid, and shell probes pass; the composed owner route audit above is green.
|
||||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 196 KiB |
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":3,"world_identity":"owner-world/seed/0","owner_voxel_size_um":125000,"core_voxel_size_um":250000,"strate_layout_chunk_size_um":8000000,"strate_layout_chunk_width_subvoxels":8192,"strates":[{"stable_id":"owner-world/seed/0/strate/0/generator/SurfaceWorld","order":0,"strate_index":0,"generator_kind":"SurfaceWorld","recipe_id":"SurfaceWorld.Heightfield2D","recipe_version":2,"interior_bottom_um":-504000000,"interior_top_um":8000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":7500000,"top_um":8000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-503500000,"top_um":7500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-504000000,"top_um":-503500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":1,"transition_width_um":8000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":2000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"cap_profile","stable_id":"biome/4097","units":"1/256 Core voxel","values":[-18022,0,64000,1536,0]},{"kind":"route_point","stable_id":"route/0","units":"1/256 Core voxel","values":[8192,24576,-381223,1024]},{"kind":"route_point","stable_id":"route/1","units":"1/256 Core voxel","values":[8192,24320,-381254,1024]},{"kind":"route_point","stable_id":"route/2","units":"1/256 Core voxel","values":[8192,24064,-381284,1024]},{"kind":"route_point","stable_id":"route/3","units":"1/256 Core voxel","values":[8192,23808,-381302,1024]},{"kind":"route_point","stable_id":"route/4","units":"1/256 Core voxel","values":[8192,23552,-381304,1024]},{"kind":"route_point","stable_id":"route/5","units":"1/256 Core voxel","values":[8192,23296,-381291,1024]},{"kind":"route_point","stable_id":"route/6","units":"1/256 Core voxel","values":[8192,23040,-381270,1024]},{"kind":"route_point","stable_id":"route/7","units":"1/256 Core voxel","values":[8192,22784,-381242,1024]},{"kind":"route_point","stable_id":"route/8","units":"1/256 Core voxel","values":[8192,22528,-381218,1024]},{"kind":"route_point","stable_id":"route/9","units":"1/256 Core voxel","values":[8192,22272,-381192,1024]},{"kind":"route_point","stable_id":"route/10","units":"1/256 Core voxel","values":[8192,22016,-381161,1024]},{"kind":"route_point","stable_id":"route/11","units":"1/256 Core voxel","values":[8192,21760,-381118,1024]},{"kind":"route_point","stable_id":"route/12","units":"1/256 Core voxel","values":[8192,21504,-381122,1024]},{"kind":"route_point","stable_id":"route/13","units":"1/256 Core voxel","values":[8192,21248,-381155,1024]},{"kind":"route_point","stable_id":"route/14","units":"1/256 Core voxel","values":[8192,20992,-381183,1024]},{"kind":"route_point","stable_id":"route/15","units":"1/256 Core voxel","values":[8192,20736,-381207,1024]},{"kind":"route_point","stable_id":"route/16","units":"1/256 Core voxel","values":[8192,20480,-381219,1024]},{"kind":"route_point","stable_id":"route/17","units":"1/256 Core voxel","values":[8192,20224,-381220,1024]},{"kind":"route_point","stable_id":"route/18","units":"1/256 Core voxel","values":[8192,19968,-381202,1024]},{"kind":"route_point","stable_id":"route/19","units":"1/256 Core voxel","values":[8192,19712,-381167,1024]},{"kind":"route_point","stable_id":"route/20","units":"1/256 Core voxel","values":[8192,19456,-381130,1024]},{"kind":"route_point","stable_id":"route/21","units":"1/256 Core voxel","values":[8192,19200,-381101,1024]},{"kind":"route_point","stable_id":"route/22","units":"1/256 Core voxel","values":[8192,18944,-381072,1024]},{"kind":"route_point","stable_id":"route/23","units":"1/256 Core voxel","values":[8192,18688,-381092,1024]},{"kind":"route_point","stable_id":"route/24","units":"1/256 Core voxel","values":[8192,18432,-381149,1024]},{"kind":"route_point","stable_id"Line truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":3,"world_identity":"owner-world/seed/0","owner_voxel_size_um":125000,"core_voxel_size_um":250000,"strate_layout_chunk_size_um":8000000,"strate_layout_chunk_width_subvoxels":8192,"strates":[{"stable_id":"owner-world/seed/0/strate/0/generator/SurfaceWorld","order":0,"strate_index":0,"generator_kind":"SurfaceWorld","recipe_id":"SurfaceWorld.Heightfield2D","recipe_version":2,"interior_bottom_um":-504000000,"interior_top_um":8000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":7500000,"top_um":8000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-503500000,"top_um":7500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-504000000,"top_um":-503500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":1,"transition_width_um":8000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":2000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"cap_profile","stable_id":"biome/4097","units":"1/256 Core voxel","values":[-18022,0,64000,1536,0]},{"kind":"route_point","stable_id":"route/0","units":"1/256 Core voxel","values":[8192,24576,-381223,1024]},{"kind":"route_point","stable_id":"route/1","units":"1/256 Core voxel","values":[8192,24320,-381254,1024]},{"kind":"route_point","stable_id":"route/2","units":"1/256 Core voxel","values":[8192,24064,-381284,1024]},{"kind":"route_point","stable_id":"route/3","units":"1/256 Core voxel","values":[8192,23808,-381302,1024]},{"kind":"route_point","stable_id":"route/4","units":"1/256 Core voxel","values":[8192,23552,-381304,1024]},{"kind":"route_point","stable_id":"route/5","units":"1/256 Core voxel","values":[8192,23296,-381291,1024]},{"kind":"route_point","stable_id":"route/6","units":"1/256 Core voxel","values":[8192,23040,-381270,1024]},{"kind":"route_point","stable_id":"route/7","units":"1/256 Core voxel","values":[8192,22784,-381242,1024]},{"kind":"route_point","stable_id":"route/8","units":"1/256 Core voxel","values":[8192,22528,-381218,1024]},{"kind":"route_point","stable_id":"route/9","units":"1/256 Core voxel","values":[8192,22272,-381192,1024]},{"kind":"route_point","stable_id":"route/10","units":"1/256 Core voxel","values":[8192,22016,-381161,1024]},{"kind":"route_point","stable_id":"route/11","units":"1/256 Core voxel","values":[8192,21760,-381118,1024]},{"kind":"route_point","stable_id":"route/12","units":"1/256 Core voxel","values":[8192,21504,-381122,1024]},{"kind":"route_point","stable_id":"route/13","units":"1/256 Core voxel","values":[8192,21248,-381155,1024]},{"kind":"route_point","stable_id":"route/14","units":"1/256 Core voxel","values":[8192,20992,-381183,1024]},{"kind":"route_point","stable_id":"route/15","units":"1/256 Core voxel","values":[8192,20736,-381207,1024]},{"kind":"route_point","stable_id":"route/16","units":"1/256 Core voxel","values":[8192,20480,-381219,1024]},{"kind":"route_point","stable_id":"route/17","units":"1/256 Core voxel","values":[8192,20224,-381220,1024]},{"kind":"route_point","stable_id":"route/18","units":"1/256 Core voxel","values":[8192,19968,-381202,1024]},{"kind":"route_point","stable_id":"route/19","units":"1/256 Core voxel","values":[8192,19712,-381167,1024]},{"kind":"route_point","stable_id":"route/20","units":"1/256 Core voxel","values":[8192,19456,-381130,1024]},{"kind":"route_point","stable_id":"route/21","units":"1/256 Core voxel","values":[8192,19200,-381101,1024]},{"kind":"route_point","stable_id":"route/22","units":"1/256 Core voxel","values":[8192,18944,-381072,1024]},{"kind":"route_point","stable_id":"route/23","units":"1/256 Core voxel","values":[8192,18688,-381092,1024]},{"kind":"route_point","stable_id":"route/24","units":"1/256 Core voxel","values":[8192,18432,-381149,1024]},{"kind":"route_point","stable_id"Line truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema_version":3,"world_identity":"owner-world/seed/0","owner_voxel_size_um":125000,"core_voxel_size_um":250000,"strate_layout_chunk_size_um":8000000,"strate_layout_chunk_width_subvoxels":8192,"strates":[{"stable_id":"owner-world/seed/0/strate/0/generator/SurfaceWorld","order":0,"strate_index":0,"generator_kind":"SurfaceWorld","recipe_id":"SurfaceWorld.Heightfield2D","recipe_version":2,"interior_bottom_um":-504000000,"interior_top_um":8000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":7500000,"top_um":8000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-503500000,"top_um":7500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-504000000,"top_um":-503500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":1,"transition_width_um":8000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":2000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"cap_profile","stable_id":"biome/4097","units":"1/256 Core voxel","values":[-18022,0,64000,1536,0]},{"kind":"route_point","stable_id":"route/0","units":"1/256 Core voxel","values":[8192,24576,-381223,1024]},{"kind":"route_point","stable_id":"route/1","units":"1/256 Core voxel","values":[8192,24320,-381254,1024]},{"kind":"route_point","stable_id":"route/2","units":"1/256 Core voxel","values":[8192,24064,-381284,1024]},{"kind":"route_point","stable_id":"route/3","units":"1/256 Core voxel","values":[8192,23808,-381302,1024]},{"kind":"route_point","stable_id":"route/4","units":"1/256 Core voxel","values":[8192,23552,-381304,1024]},{"kind":"route_point","stable_id":"route/5","units":"1/256 Core voxel","values":[8192,23296,-381291,1024]},{"kind":"route_point","stable_id":"route/6","units":"1/256 Core voxel","values":[8192,23040,-381270,1024]},{"kind":"route_point","stable_id":"route/7","units":"1/256 Core voxel","values":[8192,22784,-381242,1024]},{"kind":"route_point","stable_id":"route/8","units":"1/256 Core voxel","values":[8192,22528,-381218,1024]},{"kind":"route_point","stable_id":"route/9","units":"1/256 Core voxel","values":[8192,22272,-381192,1024]},{"kind":"route_point","stable_id":"route/10","units":"1/256 Core voxel","values":[8192,22016,-381161,1024]},{"kind":"route_point","stable_id":"route/11","units":"1/256 Core voxel","values":[8192,21760,-381118,1024]},{"kind":"route_point","stable_id":"route/12","units":"1/256 Core voxel","values":[8192,21504,-381122,1024]},{"kind":"route_point","stable_id":"route/13","units":"1/256 Core voxel","values":[8192,21248,-381155,1024]},{"kind":"route_point","stable_id":"route/14","units":"1/256 Core voxel","values":[8192,20992,-381183,1024]},{"kind":"route_point","stable_id":"route/15","units":"1/256 Core voxel","values":[8192,20736,-381207,1024]},{"kind":"route_point","stable_id":"route/16","units":"1/256 Core voxel","values":[8192,20480,-381219,1024]},{"kind":"route_point","stable_id":"route/17","units":"1/256 Core voxel","values":[8192,20224,-381220,1024]},{"kind":"route_point","stable_id":"route/18","units":"1/256 Core voxel","values":[8192,19968,-381202,1024]},{"kind":"route_point","stable_id":"route/19","units":"1/256 Core voxel","values":[8192,19712,-381167,1024]},{"kind":"route_point","stable_id":"route/20","units":"1/256 Core voxel","values":[8192,19456,-381130,1024]},{"kind":"route_point","stable_id":"route/21","units":"1/256 Core voxel","values":[8192,19200,-381101,1024]},{"kind":"route_point","stable_id":"route/22","units":"1/256 Core voxel","values":[8192,18944,-381072,1024]},{"kind":"route_point","stable_id":"route/23","units":"1/256 Core voxel","values":[8192,18688,-381092,1024]},{"kind":"route_point","stable_id":"route/24","units":"1/256 Core voxel","values":[8192,18432,-381149,1024]},{"kind":"route_point","stable_id"Line truncated
|
||||||
@@ -74,6 +74,7 @@ namespace
|
|||||||
bool DensityDispatchReport=false;
|
bool DensityDispatchReport=false;
|
||||||
bool CavesRecipe=false;
|
bool CavesRecipe=false;
|
||||||
bool LatticeRecipe=false;
|
bool LatticeRecipe=false;
|
||||||
|
bool LegacyTunnelNetwork=false;
|
||||||
int LatticeStrateIndex=-1;
|
int LatticeStrateIndex=-1;
|
||||||
bool AuditHeightfieldProfile=false;
|
bool AuditHeightfieldProfile=false;
|
||||||
bool LandmarkChamberDemo=false;
|
bool LandmarkChamberDemo=false;
|
||||||
@@ -1434,6 +1435,7 @@ namespace
|
|||||||
" --core-snapshot=FILE (benchmark the exact resolved Core snapshot from Unreal)\n"
|
" --core-snapshot=FILE (benchmark the exact resolved Core snapshot from Unreal)\n"
|
||||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||||
" --seed=N --recipe=surface-world|floating-islands|caves|lattice\n"
|
" --seed=N --recipe=surface-world|floating-islands|caves|lattice\n"
|
||||||
|
" --legacy-tunnel-network (keep TN on its compiled-analytic comparison path)\n"
|
||||||
" --native-owner-fi (build the existing owner FI plan; leave other strates intact)\n"
|
" --native-owner-fi (build the existing owner FI plan; leave other strates intact)\n"
|
||||||
" --density-dispatch-report (print bound path per strate and tile hashes)\n"
|
" --density-dispatch-report (print bound path per strate and tile hashes)\n"
|
||||||
" --lattice-strate=N (opt one Maze or VerticalShafts strate into Core.Lattice)\n"
|
" --lattice-strate=N (opt one Maze or VerticalShafts strate into Core.Lattice)\n"
|
||||||
@@ -1497,6 +1499,7 @@ namespace
|
|||||||
else if(arg=="--native-owner-fi")options.NativeOwnerFloatingIslandsRecipe=true;
|
else if(arg=="--native-owner-fi")options.NativeOwnerFloatingIslandsRecipe=true;
|
||||||
else if(arg=="--density-dispatch-report")
|
else if(arg=="--density-dispatch-report")
|
||||||
{options.DensityDispatchReport=true;options.StreamHash=true;options.StreamHashTiles=true;}
|
{options.DensityDispatchReport=true;options.StreamHash=true;options.StreamHashTiles=true;}
|
||||||
|
else if(arg=="--legacy-tunnel-network")options.LegacyTunnelNetwork=true;
|
||||||
else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true;
|
else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true;
|
||||||
else if(arg=="--audit-native-caves")options.AuditNativeCaves=true;
|
else if(arg=="--audit-native-caves")options.AuditNativeCaves=true;
|
||||||
else if(arg=="--audit-native-lattice")options.AuditNativeLattice=true;
|
else if(arg=="--audit-native-lattice")options.AuditNativeLattice=true;
|
||||||
@@ -1737,7 +1740,8 @@ int main(int argc,char** argv)
|
|||||||
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
|
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
|
||||||
options.MaxLevel,options.StreamHash?"on":"off",
|
options.MaxLevel,options.StreamHash?"on":"off",
|
||||||
options.StreamHashAllowUniformSkips?"on":"off",options.ProfileColumnOverlap?"on":"off",
|
options.StreamHashAllowUniformSkips?"on":"off",options.ProfileColumnOverlap?"on":"off",
|
||||||
options.ForceGenerate?"on":"off",options.CavesRecipe?"caves"
|
options.ForceGenerate?"on":"off",options.LegacyTunnelNetwork?"legacy-tunnel-network"
|
||||||
|
:options.CavesRecipe?"caves"
|
||||||
:options.LatticeRecipe?"lattice"
|
:options.LatticeRecipe?"lattice"
|
||||||
:options.FloatingIslandsRecipe?"floating-islands":"surface-world",
|
:options.FloatingIslandsRecipe?"floating-islands":"surface-world",
|
||||||
options.LatticeStrateIndex,
|
options.LatticeStrateIndex,
|
||||||
@@ -1858,7 +1862,9 @@ int main(int argc,char** argv)
|
|||||||
}
|
}
|
||||||
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
||||||
{
|
{
|
||||||
const std::string variant=options.LatticeRecipe
|
const std::string variant=options.LegacyTunnelNetwork
|
||||||
|
?"legacy-tunnel-network"
|
||||||
|
:options.LatticeRecipe
|
||||||
?"lattice/"+std::to_string(options.LatticeStrateIndex)
|
?"lattice/"+std::to_string(options.LatticeStrateIndex)
|
||||||
:options.CavesRecipe?"caves"
|
:options.CavesRecipe?"caves"
|
||||||
:(options.FloatingIslandsRecipe||options.NativeOwnerFloatingIslandsRecipe)
|
:(options.FloatingIslandsRecipe||options.NativeOwnerFloatingIslandsRecipe)
|
||||||
@@ -1871,6 +1877,11 @@ int main(int argc,char** argv)
|
|||||||
*ownerWorld,worldAssembly,variant,resolvedLayout,error);
|
*ownerWorld,worldAssembly,variant,resolvedLayout,error);
|
||||||
if(!layoutResolved)
|
if(!layoutResolved)
|
||||||
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
||||||
|
if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe
|
||||||
|
&&!options.LegacyTunnelNetwork&&!options.CavesRecipe
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::PlanOwnerTunnelNetworkCaves(
|
||||||
|
*ownerWorld,resolvedLayout,error))
|
||||||
|
{std::fprintf(stderr,"Imported owner Tunnel Network cave planning failed: %s\n",error.c_str());return 2;}
|
||||||
if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe
|
if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe
|
||||||
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
||||||
*ownerWorld,worldAssembly,resolvedLayout,error))
|
*ownerWorld,worldAssembly,resolvedLayout,error))
|
||||||
@@ -1953,8 +1964,11 @@ int main(int argc,char** argv)
|
|||||||
{
|
{
|
||||||
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
||||||
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
||||||
if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error)
|
if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error))
|
||||||
||!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
{std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;}
|
||||||
|
rooms.insert(rooms.end(),resolvedLayout.cavernRooms.begin(),resolvedLayout.cavernRooms.end());
|
||||||
|
edges.insert(edges.end(),resolvedLayout.cavernEdges.begin(),resolvedLayout.cavernEdges.end());
|
||||||
|
if(!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
||||||
resolvedLayout,seed,rooms,edges,error))
|
resolvedLayout,seed,rooms,edges,error))
|
||||||
{std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;}
|
||||||
}
|
}
|
||||||
@@ -1965,8 +1979,11 @@ int main(int argc,char** argv)
|
|||||||
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
||||||
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
||||||
if(!MakeNativeLatticeSpine(resolvedLayout,seed,options.LatticeStrateIndex,
|
if(!MakeNativeLatticeSpine(resolvedLayout,seed,options.LatticeStrateIndex,
|
||||||
options.LatticeOrphanControl,rooms,edges,error)
|
options.LatticeOrphanControl,rooms,edges,error))
|
||||||
||!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
{std::fprintf(stderr,"Native lattice graph resolution failed: %s\n",error.c_str());return 2;}
|
||||||
|
rooms.insert(rooms.end(),resolvedLayout.cavernRooms.begin(),resolvedLayout.cavernRooms.end());
|
||||||
|
edges.insert(edges.end(),resolvedLayout.cavernEdges.begin(),resolvedLayout.cavernEdges.end());
|
||||||
|
if(!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
||||||
resolvedLayout,seed,rooms,edges,error))
|
resolvedLayout,seed,rooms,edges,error))
|
||||||
{std::fprintf(stderr,"Native lattice graph resolution failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Native lattice graph resolution failed: %s\n",error.c_str());return 2;}
|
||||||
}
|
}
|
||||||
@@ -2087,12 +2104,16 @@ int main(int argc,char** argv)
|
|||||||
const bool hasNativeLatticeRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
const bool hasNativeLatticeRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||||
[](const auto& stratum){return VoxelForgeCore::IsNativeLatticeFieldStratum(stratum)
|
[](const auto& stratum){return VoxelForgeCore::IsNativeLatticeFieldStratum(stratum)
|
||||||
&&stratum.passageCapabilities.hasNativeLatticeField;});
|
&&stratum.passageCapabilities.hasNativeLatticeField;});
|
||||||
const auto actualRecipe=hasNativeLatticeRecipe?std::string_view("lattice")
|
const auto actualRecipe=requestedRecipe=="lattice"&&hasNativeLatticeRecipe
|
||||||
:hasNativeCavesRecipe?std::string_view("caves")
|
?std::string_view("lattice")
|
||||||
|
:requestedRecipe=="caves"&&hasNativeCavesRecipe
|
||||||
|
?std::string_view("caves")
|
||||||
:requestedRecipe=="surface-world"&&hasSurfaceWorldPlan
|
:requestedRecipe=="surface-world"&&hasSurfaceWorldPlan
|
||||||
?std::string_view("surface-world")
|
?std::string_view("surface-world")
|
||||||
:requestedRecipe=="floating-islands"&&hasFloatingIslands
|
:requestedRecipe=="floating-islands"&&hasFloatingIslands
|
||||||
?std::string_view("floating-islands")
|
?std::string_view("floating-islands")
|
||||||
|
:hasNativeLatticeRecipe?std::string_view("lattice")
|
||||||
|
:hasNativeCavesRecipe?std::string_view("caves")
|
||||||
:hasFloatingIslands?std::string_view("floating-islands")
|
:hasFloatingIslands?std::string_view("floating-islands")
|
||||||
:hasSurfaceWorldPlan?std::string_view("surface-world")
|
:hasSurfaceWorldPlan?std::string_view("surface-world")
|
||||||
:std::string_view("missing");
|
:std::string_view("missing");
|
||||||
|
|||||||
@@ -1595,6 +1595,208 @@ namespace StandaloneStreamBenchmark
|
|||||||
return passed;
|
return passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool AuditNativeTunnelNetworkField(const VoxelForgeCore::World& shape,
|
||||||
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||||
|
std::uint32_t voxelMicrometres)
|
||||||
|
{
|
||||||
|
using namespace VoxelForgeCore;
|
||||||
|
constexpr double Pi=3.14159265358979323846;
|
||||||
|
const double vm=std::max<std::uint32_t>(1,voxelMicrometres)/1.0e6;
|
||||||
|
const auto density=[&](double x,double y,double z)
|
||||||
|
{return static_cast<double>(shape.densityAt({x/vm,y/vm,z/vm}))*vm;};
|
||||||
|
const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
|
[](const auto& s){return s.generatorKind=="TunnelNetwork"
|
||||||
|
&&IsNativeCaveFieldStratum(s)&&s.passageCapabilities.hasNativeCaveField;});
|
||||||
|
const bool supportedGraphVocabulary=
|
||||||
|
layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV1
|
||||||
|
||layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV2;
|
||||||
|
if(cave==layout.strates.end()||!supportedGraphVocabulary)
|
||||||
|
{std::printf("[TunnelNetworkCavernAudit] recipe=%s graph=%u status=FAIL\n",
|
||||||
|
cave==layout.strates.end()?"missing":"present",
|
||||||
|
layout.cavernGraphVocabularyVersion);return false;}
|
||||||
|
|
||||||
|
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||||||
|
const ResolvedLayoutPassageV1* departure=nullptr;
|
||||||
|
for(const auto& passage:layout.passages)
|
||||||
|
{
|
||||||
|
if(passage.lowerStrateIndex==cave->strateIndex
|
||||||
|
&&passage.upperStrateIndex<cave->strateIndex)arrival=&passage;
|
||||||
|
if(passage.upperStrateIndex==cave->strateIndex
|
||||||
|
&&passage.lowerStrateIndex>cave->strateIndex)departure=&passage;
|
||||||
|
}
|
||||||
|
std::vector<const ResolvedCavernRoomV1*> rooms;
|
||||||
|
std::vector<const ResolvedCavernEdgeV1*> routeEdges;
|
||||||
|
for(const auto& room:layout.cavernRooms)
|
||||||
|
if(room.strateIndex==cave->strateIndex)rooms.push_back(&room);
|
||||||
|
for(const auto& edge:layout.cavernEdges)
|
||||||
|
if(edge.strateIndex==cave->strateIndex
|
||||||
|
&&edge.stableId.find("/route-edge-")!=std::string::npos)
|
||||||
|
routeEdges.push_back(&edge);
|
||||||
|
const auto routeIndex=[](const ResolvedCavernEdgeV1* edge)
|
||||||
|
{
|
||||||
|
const auto marker=edge->stableId.rfind("/route-edge-");
|
||||||
|
return marker==std::string::npos?std::uint64_t{0}
|
||||||
|
:static_cast<std::uint64_t>(std::stoull(edge->stableId.substr(marker+12)));
|
||||||
|
};
|
||||||
|
std::sort(routeEdges.begin(),routeEdges.end(),[&](const auto* a,const auto* b)
|
||||||
|
{return routeIndex(a)<routeIndex(b);});
|
||||||
|
std::unordered_map<std::string,std::size_t> compoundCounts;
|
||||||
|
std::unordered_map<std::string,double> compoundRadii;
|
||||||
|
double largestRoomRadius=0.0,largestOtherRoomRadius=0.0;
|
||||||
|
std::string largestRoomGroup;
|
||||||
|
for(const auto* room:rooms)
|
||||||
|
{
|
||||||
|
const auto marker=room->stableId.rfind("/primitive-");
|
||||||
|
const std::string group=marker==std::string::npos
|
||||||
|
?room->stableId:room->stableId.substr(0,marker);
|
||||||
|
++compoundCounts[group];
|
||||||
|
const double radius=std::max(room->halfExtentsMicrometres[0],
|
||||||
|
room->halfExtentsMicrometres[1])/1.0e6;
|
||||||
|
compoundRadii[group]=std::max(compoundRadii[group],radius);
|
||||||
|
if(radius>largestRoomRadius)
|
||||||
|
{largestRoomRadius=radius;largestRoomGroup=group;}
|
||||||
|
}
|
||||||
|
for(const auto& [group,radius]:compoundRadii)
|
||||||
|
if(group!=largestRoomGroup)
|
||||||
|
largestOtherRoomRadius=std::max(largestOtherRoomRadius,radius);
|
||||||
|
const bool compoundRooms=!compoundCounts.empty()
|
||||||
|
&&std::all_of(compoundCounts.begin(),compoundCounts.end(),
|
||||||
|
[](const auto& row){return row.second>=2&&row.second<=4;});
|
||||||
|
const bool destination=largestOtherRoomRadius>0.0
|
||||||
|
&&largestRoomRadius>largestOtherRoomRadius*1.10;
|
||||||
|
|
||||||
|
std::vector<const ResolvedLayoutAnchorV2*> routeAnchors;
|
||||||
|
for(const auto& anchor:cave->anchors)
|
||||||
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
|
routeAnchors.push_back(&anchor);
|
||||||
|
double maximumGrade=0.0;
|
||||||
|
bool gradeOk=routeAnchors.size()>=2;
|
||||||
|
for(std::size_t i=1;i<routeAnchors.size();++i)
|
||||||
|
{
|
||||||
|
const auto& a=routeAnchors[i-1]->values;
|
||||||
|
const auto& b=routeAnchors[i]->values;
|
||||||
|
const double run=std::hypot(static_cast<double>(b[0]-a[0]),
|
||||||
|
static_cast<double>(b[1]-a[1]));
|
||||||
|
const double rise=std::abs(static_cast<double>(b[2]-a[2]));
|
||||||
|
if(run<=0.0){gradeOk=false;continue;}
|
||||||
|
const double grade=std::atan2(rise,run)*180.0/Pi;
|
||||||
|
maximumGrade=std::max(maximumGrade,grade);
|
||||||
|
gradeOk=gradeOk&&grade<=15.0+1.0e-6;
|
||||||
|
}
|
||||||
|
bool mouthBinding=arrival&&departure&&routeAnchors.size()>=2;
|
||||||
|
if(mouthBinding)
|
||||||
|
{
|
||||||
|
const auto qOffset=static_cast<std::int64_t>(std::llround(
|
||||||
|
1000000.0*WorldAssemblySubvoxelsPerVoxel/layout.coreVoxelSizeMicrometres));
|
||||||
|
const auto& start=routeAnchors.front()->values;
|
||||||
|
const auto& end=routeAnchors.back()->values;
|
||||||
|
mouthBinding=start[0]==arrival->lowerMouthSubvoxels[0]
|
||||||
|
&&start[1]==arrival->lowerMouthSubvoxels[1]
|
||||||
|
&&start[2]==arrival->lowerMouthSubvoxels[2]+qOffset
|
||||||
|
&&end[0]==departure->upperMouthSubvoxels[0]
|
||||||
|
&&end[1]==departure->upperMouthSubvoxels[1]
|
||||||
|
&&end[2]==departure->upperMouthSubvoxels[2]+qOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t airSamples=0,floorSamples=0,shellSamples=0;
|
||||||
|
bool centerAir=true,floorSolid=true,shellSolid=true;
|
||||||
|
for(const auto* edge:routeEdges)
|
||||||
|
{
|
||||||
|
const auto& p=edge->controlPointsMicrometres;
|
||||||
|
const double x=(p[0][0]+3.0*p[1][0]+3.0*p[2][0]+p[3][0])/8.0e6;
|
||||||
|
const double y=(p[0][1]+3.0*p[1][1]+3.0*p[2][1]+p[3][1])/8.0e6;
|
||||||
|
const double floor=(p[0][2]+3.0*p[1][2]+3.0*p[2][2]+p[3][2])/8.0e6;
|
||||||
|
const double bore=edge->boreRadiusMicrometres/1.0e6;
|
||||||
|
if(density(x,y,floor+1.0)<=0.0)centerAir=false;
|
||||||
|
++airSamples;
|
||||||
|
if(density(x,y,floor-0.10)>=0.0)floorSolid=false;
|
||||||
|
++floorSamples;
|
||||||
|
const double dx=(p[3][0]-p[0][0])/1.0e6;
|
||||||
|
const double dy=(p[3][1]-p[0][1])/1.0e6;
|
||||||
|
const double length=std::hypot(dx,dy);
|
||||||
|
if(length<=1.0e-6){shellSolid=false;continue;}
|
||||||
|
const double nx=-dy/length,ny=dx/length;
|
||||||
|
if(density(x+nx*(bore+0.90),y+ny*(bore+0.90),floor+2.0)>=0.0
|
||||||
|
||density(x-nx*(bore+0.90),y-ny*(bore+0.90),floor+2.0)>=0.0)
|
||||||
|
shellSolid=false;
|
||||||
|
shellSamples+=2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResolvedCavernEdgeV1* rippleEdge=nullptr;
|
||||||
|
double longest=0.0;
|
||||||
|
for(const auto* edge:routeEdges)
|
||||||
|
{
|
||||||
|
const auto& p=edge->controlPointsMicrometres;
|
||||||
|
const double length=std::hypot(static_cast<double>(p[3][0]-p[0][0]),
|
||||||
|
static_cast<double>(p[3][1]-p[0][1]));
|
||||||
|
if(length>longest){longest=length;rippleEdge=edge;}
|
||||||
|
}
|
||||||
|
std::vector<double> rippleRadii;
|
||||||
|
if(rippleEdge)
|
||||||
|
{
|
||||||
|
const auto& p=rippleEdge->controlPointsMicrometres;
|
||||||
|
const double radius=rippleEdge->boreRadiusMicrometres/1.0e6;
|
||||||
|
constexpr std::array<double,7> stations{{0.18,0.29,0.40,0.51,0.62,0.73,0.84}};
|
||||||
|
for(double t:stations)
|
||||||
|
{
|
||||||
|
const auto bezier=[&](double u,std::size_t axis)
|
||||||
|
{
|
||||||
|
const double one=1.0-u;
|
||||||
|
return (one*one*one*p[0][axis]+3.0*one*one*u*p[1][axis]
|
||||||
|
+3.0*one*u*u*p[2][axis]+u*u*u*p[3][axis])/1.0e6;
|
||||||
|
};
|
||||||
|
const double x=bezier(t,0),y=bezier(t,1),floor=bezier(t,2);
|
||||||
|
const double tx=bezier(std::min(1.0,t+0.01),0)
|
||||||
|
-bezier(std::max(0.0,t-0.01),0);
|
||||||
|
const double ty=bezier(std::min(1.0,t+0.01),1)
|
||||||
|
-bezier(std::max(0.0,t-0.01),1);
|
||||||
|
const double tangent=std::hypot(tx,ty);
|
||||||
|
if(tangent<=1.0e-6)continue;
|
||||||
|
for(const double side:{-1.0,1.0})
|
||||||
|
{
|
||||||
|
double prior=0.20;
|
||||||
|
double priorDensity=density(x,y,floor+2.0);
|
||||||
|
bool found=false;
|
||||||
|
for(double radial=0.25;radial<=radius+1.8;radial+=0.05)
|
||||||
|
{
|
||||||
|
const double sampleX=x-side*ty/tangent*radial;
|
||||||
|
const double sampleY=y+side*tx/tangent*radial;
|
||||||
|
const double value=density(sampleX,sampleY,floor+2.0);
|
||||||
|
if(priorDensity>=0.0&&value<0.0)
|
||||||
|
{
|
||||||
|
const double fraction=priorDensity/(priorDensity-value);
|
||||||
|
rippleRadii.push_back(prior+(radial-prior)*fraction);
|
||||||
|
found=true;break;
|
||||||
|
}
|
||||||
|
prior=radial;priorDensity=value;
|
||||||
|
}
|
||||||
|
if(!found)continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double ripplePeakToPeak=0.0;
|
||||||
|
if(!rippleRadii.empty())
|
||||||
|
{
|
||||||
|
const auto limits=std::minmax_element(rippleRadii.begin(),rippleRadii.end());
|
||||||
|
ripplePeakToPeak=*limits.second-*limits.first;
|
||||||
|
}
|
||||||
|
const bool ripple=rippleRadii.size()>=8&&ripplePeakToPeak>=0.02
|
||||||
|
&&ripplePeakToPeak<=1.25;
|
||||||
|
std::printf("[TunnelNetworkCavernAudit] strate=%d recipe=%s rooms=%zu compound_groups=%zu destination_radius_m=%.3f route_edges=%zu route_points=%zu mouth_binding=%s max_grade_deg=%.3f grade_ok=%s center_air=%llu/%llu floor_solid=%llu/%llu shell_solid=%llu/%llu ripple_samples=%zu ripple_peak_to_peak_m=%.4f status=%s\n",
|
||||||
|
cave->strateIndex,cave->recipeId.c_str(),rooms.size(),compoundCounts.size(),
|
||||||
|
largestRoomRadius,routeEdges.size(),routeAnchors.size(),mouthBinding?"PASS":"FAIL",
|
||||||
|
maximumGrade,gradeOk?"PASS":"FAIL",
|
||||||
|
static_cast<unsigned long long>(airSamples),static_cast<unsigned long long>(airSamples),
|
||||||
|
static_cast<unsigned long long>(floorSamples),static_cast<unsigned long long>(floorSamples),
|
||||||
|
static_cast<unsigned long long>(shellSamples),static_cast<unsigned long long>(shellSamples),
|
||||||
|
rippleRadii.size(),ripplePeakToPeak,
|
||||||
|
arrival&&departure&&routeEdges.size()>=2&&!rooms.empty()&&compoundRooms
|
||||||
|
&&destination&&mouthBinding&&gradeOk&¢erAir&&floorSolid&&shellSolid
|
||||||
|
&&ripple?"PASS":"FAIL");
|
||||||
|
return arrival&&departure&&routeEdges.size()>=2&&!rooms.empty()&&compoundRooms
|
||||||
|
&&destination&&mouthBinding&&gradeOk&¢erAir&&floorSolid&&shellSolid&&ripple;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool AuditNativeCavesField(const VoxelForgeCore::World& shape,
|
inline bool AuditNativeCavesField(const VoxelForgeCore::World& shape,
|
||||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::uint32_t voxelMicrometres)
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::uint32_t voxelMicrometres)
|
||||||
{
|
{
|
||||||
@@ -1605,9 +1807,13 @@ namespace StandaloneStreamBenchmark
|
|||||||
const auto density=[&](double x,double y,double z)
|
const auto density=[&](double x,double y,double z)
|
||||||
{return static_cast<double>(shape.densityAt({x/vm,y/vm,z/vm}))*vm;};
|
{return static_cast<double>(shape.densityAt({x/vm,y/vm,z/vm}))*vm;};
|
||||||
const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(),
|
const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
[](const auto& s){return s.generatorKind=="Caves"
|
[](const auto& s){return IsNativeCaveFieldStratum(s)
|
||||||
&&s.recipeId=="Core.Caves.PrimitiveField"&&s.recipeVersion==1
|
|
||||||
&&s.passageCapabilities.hasNativeCaveField;});
|
&&s.passageCapabilities.hasNativeCaveField;});
|
||||||
|
if(cave!=layout.strates.end()&&cave->generatorKind=="TunnelNetwork")
|
||||||
|
return AuditNativeTunnelNetworkField(shape,layout,voxelMicrometres);
|
||||||
|
if(cave==layout.strates.end()||cave->generatorKind!="Caves")
|
||||||
|
{std::printf("[CavesAudit] recipe=missing rooms=%zu edges=%zu status=FAIL\n",
|
||||||
|
layout.cavernRooms.size(),layout.cavernEdges.size());return false;}
|
||||||
if(cave==layout.strates.end()||layout.cavernRooms.size()!=1||layout.cavernEdges.size()!=2)
|
if(cave==layout.strates.end()||layout.cavernRooms.size()!=1||layout.cavernEdges.size()!=2)
|
||||||
{std::printf("[CavesAudit] recipe=%s rooms=%zu edges=%zu status=FAIL\n",
|
{std::printf("[CavesAudit] recipe=%s rooms=%zu edges=%zu status=FAIL\n",
|
||||||
cave==layout.strates.end()?"missing":"present",layout.cavernRooms.size(),layout.cavernEdges.size());return false;}
|
cave==layout.strates.end()?"missing":"present",layout.cavernRooms.size(),layout.cavernEdges.size());return false;}
|
||||||
@@ -1925,7 +2131,10 @@ namespace StandaloneStreamBenchmark
|
|||||||
const auto lattice=std::find_if(layout.strates.begin(),layout.strates.end(),
|
const auto lattice=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||||
[](const auto& s){return IsNativeLatticeFieldStratum(s)
|
[](const auto& s){return IsNativeLatticeFieldStratum(s)
|
||||||
&&s.passageCapabilities.hasNativeLatticeField;});
|
&&s.passageCapabilities.hasNativeLatticeField;});
|
||||||
if(lattice==layout.strates.end()||layout.cavernGraphVocabularyVersion!=1)
|
const bool supportedGraphVocabulary=
|
||||||
|
layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV1
|
||||||
|
||layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV2;
|
||||||
|
if(lattice==layout.strates.end()||!supportedGraphVocabulary)
|
||||||
{std::printf("[LatticeAudit] recipe=%s graph=%u status=FAIL\n",
|
{std::printf("[LatticeAudit] recipe=%s graph=%u status=FAIL\n",
|
||||||
lattice==layout.strates.end()?"missing":"present",
|
lattice==layout.strates.end()?"missing":"present",
|
||||||
layout.cavernGraphVocabularyVersion);return false;}
|
layout.cavernGraphVocabularyVersion);return false;}
|
||||||
@@ -2975,13 +3184,16 @@ namespace StandaloneStreamBenchmark
|
|||||||
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||||
if(upperStratum==ResolvedLayout->strates.end()
|
if(upperStratum==ResolvedLayout->strates.end()
|
||||||
||upperStratum->generatorKind!="FloatingIslands")
|
||(upperStratum->generatorKind!="FloatingIslands"
|
||||||
|
&&upperStratum->generatorKind!="TunnelNetwork"))
|
||||||
return authoredMouthDepth;
|
return authoredMouthDepth;
|
||||||
std::vector<std::array<double,3>> route;
|
std::vector<std::array<double,3>> route;
|
||||||
for(const auto& anchor:upperStratum->anchors)
|
for(const auto& anchor:upperStratum->anchors)
|
||||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
route.push_back({qToMetres(anchor.values[0]),
|
route.push_back({qToMetres(anchor.values[0]),
|
||||||
qToMetres(anchor.values[1]),qToMetres(anchor.values[2])});
|
qToMetres(anchor.values[1]),qToMetres(anchor.values[2])});
|
||||||
|
if(upperStratum->generatorKind=="TunnelNetwork"&&route.size()>2)
|
||||||
|
route.erase(route.begin(),route.end()-2);
|
||||||
double routeHeight=0.0,distanceSquared=0.0;
|
double routeHeight=0.0,distanceSquared=0.0;
|
||||||
const double mouthX=qToMetres(passage.controlPoints.front().positionSubvoxels[0]);
|
const double mouthX=qToMetres(passage.controlPoints.front().positionSubvoxels[0]);
|
||||||
const double mouthY=qToMetres(passage.controlPoints.front().positionSubvoxels[1]);
|
const double mouthY=qToMetres(passage.controlPoints.front().positionSubvoxels[1]);
|
||||||
@@ -3016,6 +3228,55 @@ namespace StandaloneStreamBenchmark
|
|||||||
for(const auto& point:passage.controlPoints)
|
for(const auto& point:passage.controlPoints)
|
||||||
profile.push_back(qToMetres(point.positionSubvoxels[2]
|
profile.push_back(qToMetres(point.positionSubvoxels[2]
|
||||||
-point.radiusSubvoxels));
|
-point.radiusSubvoxels));
|
||||||
|
const auto controls=passageSplineControls(passage);
|
||||||
|
const auto routeForStrate=[&](std::int32_t strateIndex)
|
||||||
|
{
|
||||||
|
std::vector<std::array<double,3>> route;
|
||||||
|
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
|
ResolvedLayout->strates.end(),[&](const auto& candidate)
|
||||||
|
{return candidate.strateIndex==strateIndex;});
|
||||||
|
if(stratum!=ResolvedLayout->strates.end()
|
||||||
|
&&(stratum->generatorKind=="TunnelNetwork"
|
||||||
|
||stratum->generatorKind=="FloatingIslands"))
|
||||||
|
for(const auto& anchor:stratum->anchors)
|
||||||
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
|
route.push_back({qToMetres(anchor.values[0]),
|
||||||
|
qToMetres(anchor.values[1]),qToMetres(anchor.values[2])});
|
||||||
|
return route;
|
||||||
|
};
|
||||||
|
const auto upper=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
|
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||||
|
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||||
|
if(upper!=ResolvedLayout->strates.end()
|
||||||
|
&&upper->generatorKind=="TunnelNetwork"&&!controls.empty())
|
||||||
|
{
|
||||||
|
auto route=routeForStrate(passage.upperStrateIndex);
|
||||||
|
if(route.size()>2)route.erase(route.begin(),route.end()-2);
|
||||||
|
double routeHeight=0.0,distanceSquared=0.0;
|
||||||
|
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route,
|
||||||
|
controls.front()[0],controls.front()[1],routeHeight,distanceSquared)
|
||||||
|
&&distanceSquared<=std::pow(controls.front()[3]+2.0,2.0))
|
||||||
|
VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute(
|
||||||
|
controls,profile,0,route,1.0);
|
||||||
|
}
|
||||||
|
const auto lower=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
|
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||||
|
{return stratum.strateIndex==passage.lowerStrateIndex;});
|
||||||
|
const std::size_t routeEnd=std::clamp<std::size_t>(
|
||||||
|
passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1);
|
||||||
|
if(lower!=ResolvedLayout->strates.end()
|
||||||
|
&&lower->generatorKind=="TunnelNetwork"&&routeEnd<controls.size())
|
||||||
|
{
|
||||||
|
auto route=routeForStrate(passage.lowerStrateIndex);
|
||||||
|
if(route.size()>2)route.resize(2);
|
||||||
|
double routeHeight=0.0,distanceSquared=0.0;
|
||||||
|
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route,
|
||||||
|
controls[routeEnd][0],controls[routeEnd][1],
|
||||||
|
routeHeight,distanceSquared)
|
||||||
|
&&distanceSquared<=std::pow(controls[routeEnd][3]+2.0,2.0))
|
||||||
|
VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute(
|
||||||
|
controls,profile,routeEnd,route,1.0);
|
||||||
|
}
|
||||||
return profile;
|
return profile;
|
||||||
};
|
};
|
||||||
const auto passageBodyFloorDepth=[&](
|
const auto passageBodyFloorDepth=[&](
|
||||||
@@ -4796,6 +5057,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::uint64_t routePoints=0,routePasses=0,routeWalkPasses=0,routeBindings=0,routeBindingFailures=0;
|
std::uint64_t routePoints=0,routePasses=0,routeWalkPasses=0,routeBindings=0,routeBindingFailures=0;
|
||||||
|
std::uint64_t tunnelNetworkRoutes=0,tunnelNetworkBlockedControls=0;
|
||||||
for(const auto& stratum:ResolvedLayout->strates)
|
for(const auto& stratum:ResolvedLayout->strates)
|
||||||
{
|
{
|
||||||
std::vector<const VoxelForgeCore::ResolvedLayoutAnchorV2*> anchors;
|
std::vector<const VoxelForgeCore::ResolvedLayoutAnchorV2*> anchors;
|
||||||
@@ -4855,15 +5117,40 @@ namespace StandaloneStreamBenchmark
|
|||||||
supportMaximumMetres=routeConstraints->supportSearchDepthSubvoxels*qMetres;
|
supportMaximumMetres=routeConstraints->supportSearchDepthSubvoxels*qMetres;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(stratum.generatorKind=="TunnelNetwork")
|
||||||
|
{
|
||||||
|
std::int64_t floorSupportDepth=0;
|
||||||
|
for(const auto& edge:ResolvedLayout->cavernEdges)
|
||||||
|
if(edge.strateIndex==stratum.strateIndex)
|
||||||
|
floorSupportDepth=std::max(floorSupportDepth,
|
||||||
|
edge.floorSupportDepthMicrometres);
|
||||||
|
supportMaximumMetres=std::max(supportMaximumMetres,
|
||||||
|
floorSupportDepth/1000000.0+0.25);
|
||||||
|
}
|
||||||
const FWalkAudit routeAudit=auditPlayerPath(Shape,route,supportStartMetres,
|
const FWalkAudit routeAudit=auditPlayerPath(Shape,route,supportStartMetres,
|
||||||
supportStepMetres,supportMaximumMetres,minimumSupportProbes,
|
supportStepMetres,supportMaximumMetres,minimumSupportProbes,
|
||||||
supportProbeRadiusMetres);
|
supportProbeRadiusMetres);
|
||||||
bool binding=true;
|
bool binding=true;
|
||||||
for(const auto& passage:ResolvedLayout->passages)
|
|
||||||
{
|
|
||||||
const auto equalPosition=[](const auto& a,const auto& b)
|
const auto equalPosition=[](const auto& a,const auto& b)
|
||||||
{return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];};
|
{return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];};
|
||||||
|
const auto boundMouthMatches=[&](const auto& authored,const auto& mouth)
|
||||||
|
{
|
||||||
|
if(stratum.generatorKind!="TunnelNetwork")
|
||||||
|
return equalPosition(authored,mouth);
|
||||||
|
auto expected=mouth;
|
||||||
|
expected[2]+=static_cast<std::int64_t>(std::llround(
|
||||||
|
1130000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
||||||
|
/World.CoreVoxelSizeMicrons));
|
||||||
|
return equalPosition(authored,expected);
|
||||||
|
};
|
||||||
|
for(const auto& passage:ResolvedLayout->passages)
|
||||||
|
{
|
||||||
if(passage.upperStrateIndex==stratum.strateIndex)
|
if(passage.upperStrateIndex==stratum.strateIndex)
|
||||||
|
{
|
||||||
|
if(stratum.generatorKind=="TunnelNetwork")
|
||||||
|
binding&=boundMouthMatches(anchors.back()->values,
|
||||||
|
passage.upperMouthSubvoxels);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
const auto rootedMouth=std::find_if(stratum.anchors.begin(),
|
const auto rootedMouth=std::find_if(stratum.anchors.begin(),
|
||||||
stratum.anchors.end(),[](const auto& anchor)
|
stratum.anchors.end(),[](const auto& anchor)
|
||||||
@@ -4874,10 +5161,15 @@ namespace StandaloneStreamBenchmark
|
|||||||
rootedMouth->values[8],rootedMouth->values[9]}}
|
rootedMouth->values[8],rootedMouth->values[9]}}
|
||||||
:std::array<std::int64_t,3>{{anchors.back()->values[0],
|
:std::array<std::int64_t,3>{{anchors.back()->values[0],
|
||||||
anchors.back()->values[1],anchors.back()->values[2]}};
|
anchors.back()->values[1],anchors.back()->values[2]}};
|
||||||
binding&=equalPosition(passage.upperMouthSubvoxels,
|
binding&=equalPosition(passage.upperMouthSubvoxels,departure);
|
||||||
departure);
|
}
|
||||||
}
|
}
|
||||||
if(passage.lowerStrateIndex==stratum.strateIndex)
|
if(passage.lowerStrateIndex==stratum.strateIndex)
|
||||||
|
{
|
||||||
|
if(stratum.generatorKind=="TunnelNetwork")
|
||||||
|
binding&=boundMouthMatches(anchors.front()->values,
|
||||||
|
passage.lowerMouthSubvoxels);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
const auto passageArrival=std::find_if(stratum.anchors.begin(),
|
const auto passageArrival=std::find_if(stratum.anchors.begin(),
|
||||||
stratum.anchors.end(),[](const auto& anchor)
|
stratum.anchors.end(),[](const auto& anchor)
|
||||||
@@ -4887,8 +5179,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
passageArrival->values[1],passageArrival->values[2]}}
|
passageArrival->values[1],passageArrival->values[2]}}
|
||||||
:std::array<std::int64_t,3>{{anchors.front()->values[0],
|
:std::array<std::int64_t,3>{{anchors.front()->values[0],
|
||||||
anchors.front()->values[1],anchors.front()->values[2]}};
|
anchors.front()->values[1],anchors.front()->values[2]}};
|
||||||
binding&=equalPosition(passage.lowerMouthSubvoxels,
|
binding&=equalPosition(passage.lowerMouthSubvoxels,arrival);
|
||||||
arrival);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
++routePasses;routeWalkPasses+=routeAudit.passed;
|
++routePasses;routeWalkPasses+=routeAudit.passed;
|
||||||
@@ -4918,6 +5210,34 @@ namespace StandaloneStreamBenchmark
|
|||||||
stratum.strateIndex,routeAudit.firstUnsupported[i][0],
|
stratum.strateIndex,routeAudit.firstUnsupported[i][0],
|
||||||
routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2],
|
routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2],
|
||||||
routeAudit.firstUnsupported[i][3]);
|
routeAudit.firstUnsupported[i][3]);
|
||||||
|
if(stratum.generatorKind=="TunnelNetwork")
|
||||||
|
{
|
||||||
|
++tunnelNetworkRoutes;
|
||||||
|
auto controlRoute=route;
|
||||||
|
constexpr double OffBoreControlOffsetMetres=12.0;
|
||||||
|
const std::size_t controlIndex=controlRoute.size()/2;
|
||||||
|
const double tangentX=route[controlIndex+1][0]
|
||||||
|
-route[controlIndex-1][0];
|
||||||
|
const double tangentY=route[controlIndex+1][1]
|
||||||
|
-route[controlIndex-1][1];
|
||||||
|
const double tangentLength=std::hypot(tangentX,tangentY);
|
||||||
|
if(tangentLength>1.0e-9)
|
||||||
|
{
|
||||||
|
controlRoute[controlIndex][0]+=-tangentY/tangentLength
|
||||||
|
*OffBoreControlOffsetMetres/coreMetres;
|
||||||
|
controlRoute[controlIndex][1]+=tangentX/tangentLength
|
||||||
|
*OffBoreControlOffsetMetres/coreMetres;
|
||||||
|
}
|
||||||
|
const FWalkAudit control=auditPlayerPath(blockedWorld,controlRoute,
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for(std::size_t passageIndex=0;
|
for(std::size_t passageIndex=0;
|
||||||
passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||||
@@ -4984,13 +5304,16 @@ namespace StandaloneStreamBenchmark
|
|||||||
&&shellAuditPassed
|
&&shellAuditPassed
|
||||||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||||||
&&routeBindingFailures==0
|
&&routeBindingFailures==0
|
||||||
|
&&tunnelNetworkRoutes==tunnelNetworkBlockedControls
|
||||||
&&blockedCompiled&&blockedControls==passageCount;
|
&&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 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 blocked_controls=%llu/%llu status=%s\n",
|
||||||
static_cast<unsigned long long>(passageCount),static_cast<unsigned long long>(passagePasses),
|
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>(naturalPasses),static_cast<unsigned long long>(passageCount),
|
||||||
static_cast<unsigned long long>(routePasses),static_cast<unsigned long long>(routeWalkPasses),
|
static_cast<unsigned long long>(routePasses),static_cast<unsigned long long>(routeWalkPasses),
|
||||||
static_cast<unsigned long long>(routePoints),
|
static_cast<unsigned long long>(routePoints),
|
||||||
static_cast<unsigned long long>(routeBindings),static_cast<unsigned long long>(routeBindingFailures),
|
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>(blockedControls),static_cast<unsigned long long>(passageCount),
|
static_cast<unsigned long long>(blockedControls),static_cast<unsigned long long>(passageCount),
|
||||||
auditPassed?"PASS":"FAIL");
|
auditPassed?"PASS":"FAIL");
|
||||||
if(!auditPassed)
|
if(!auditPassed)
|
||||||
|
|||||||
@@ -1069,6 +1069,214 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool PlanOwnerTunnelNetworkCaves(const FStandaloneOwnerWorld& owner,
|
||||||
|
VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::string& error)
|
||||||
|
{
|
||||||
|
using namespace VoxelForgeCore;
|
||||||
|
if(!owner.Manager)
|
||||||
|
{error="Tunnel Network cave planner requires the initialized owner director";return false;}
|
||||||
|
std::vector<const FStrateSlot*> tunnelSlots;
|
||||||
|
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||||
|
{
|
||||||
|
if(!slot.Definition
|
||||||
|
||owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ))
|
||||||
|
!=ECaveGeneratorType::TunnelNetwork)continue;
|
||||||
|
const bool hasIncomingMouth=std::any_of(layout.passages.begin(),
|
||||||
|
layout.passages.end(),[&](const auto& passage)
|
||||||
|
{return passage.lowerStrateIndex==slot.StrateIndex
|
||||||
|
&&passage.upperStrateIndex<slot.StrateIndex;});
|
||||||
|
const bool hasOutgoingMouth=std::any_of(layout.passages.begin(),
|
||||||
|
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;
|
||||||
|
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->recipeId="Core.Caves.PrimitiveField";
|
||||||
|
stratum->recipeVersion=1;
|
||||||
|
stratum->stableId=layout.worldIdentity+"/strate/"
|
||||||
|
+std::to_string(slot.StrateIndex)+"/generator/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(!layout.finalize(error))return false;
|
||||||
|
const auto toMicrometres=[&](double ownerVoxels)
|
||||||
|
{
|
||||||
|
return static_cast<std::int64_t>(std::llround(ownerVoxels
|
||||||
|
*static_cast<double>(owner.OwnerVoxelSizeMicrons)));
|
||||||
|
};
|
||||||
|
const auto toQ16=[](float value)
|
||||||
|
{
|
||||||
|
return static_cast<std::int32_t>(std::llround(std::clamp(
|
||||||
|
static_cast<double>(value),0.0,1.0)*65536.0));
|
||||||
|
};
|
||||||
|
if(!tunnelSlots.empty())
|
||||||
|
{
|
||||||
|
std::vector<ResolvedCavernRoomV1> plannedRooms=layout.cavernRooms;
|
||||||
|
std::vector<ResolvedCavernEdgeV1> plannedEdges=layout.cavernEdges;
|
||||||
|
for(const FStrateSlot* slot:tunnelSlots)
|
||||||
|
{
|
||||||
|
const FStrateGenerationParams& params=slot->Definition->GenerationParams;
|
||||||
|
ResolvedCavernPlannerSettingsV1 settings;
|
||||||
|
settings.roomSpacingMicrometres=std::max<std::int64_t>(4000000,
|
||||||
|
toMicrometres(params.RoomSpacing));
|
||||||
|
settings.minimumRoomRadiusMicrometres=std::max<std::int64_t>(500000,
|
||||||
|
toMicrometres(params.MinRoomRadius));
|
||||||
|
settings.maximumRoomRadiusMicrometres=std::max(
|
||||||
|
settings.minimumRoomRadiusMicrometres,toMicrometres(params.MaxRoomRadius));
|
||||||
|
settings.destinationRadiusMicrometres=std::max(
|
||||||
|
toMicrometres(params.OriginRoomRadius),
|
||||||
|
settings.maximumRoomRadiusMicrometres*135/100);
|
||||||
|
settings.minimumTunnelRadiusMicrometres=std::max<std::int64_t>(0,
|
||||||
|
toMicrometres(params.TunnelMinRadius));
|
||||||
|
settings.maximumTunnelRadiusMicrometres=std::max(
|
||||||
|
settings.minimumTunnelRadiusMicrometres,toMicrometres(params.TunnelMaxRadius));
|
||||||
|
settings.maximumTunnelLengthMicrometres=std::max<std::int64_t>(4000000,
|
||||||
|
toMicrometres(params.MaxTunnelLength));
|
||||||
|
settings.tunnelWarpMicrometres=std::max<std::int64_t>(0,
|
||||||
|
toMicrometres(params.TunnelWarpStrength));
|
||||||
|
settings.caveWarpMicrometres=std::max<std::int64_t>(0,
|
||||||
|
toMicrometres(params.CaveWarpStrength));
|
||||||
|
settings.smoothWidthMicrometres=std::max<std::int64_t>(1500000,
|
||||||
|
toMicrometres(params.SDFBlendRadius));
|
||||||
|
settings.floorSupportDepthMicrometres=std::clamp<std::int64_t>(
|
||||||
|
settings.smoothWidthMicrometres/2,750000,2000000);
|
||||||
|
settings.irregularityAmplitudeMicrometres=std::max<std::int64_t>(0,
|
||||||
|
toMicrometres(params.SurfaceRoughness));
|
||||||
|
settings.irregularityWavelengthMicrometres=params.RoughnessFrequency>0.0f
|
||||||
|
?static_cast<std::int64_t>(std::llround(owner.OwnerVoxelSizeMicrons
|
||||||
|
/static_cast<double>(params.RoughnessFrequency))):0;
|
||||||
|
settings.roomDensityQ16=toQ16(params.RoomDensity);
|
||||||
|
settings.tunnelDensityQ16=toQ16(params.TunnelDensity);
|
||||||
|
settings.roomShapeVarietyQ16=toQ16(params.RoomShapeVariety);
|
||||||
|
settings.roomHeightRatioQ16=toQ16(params.RoomHeightRatio);
|
||||||
|
settings.roomFloorCutMinimumQ16=toQ16(params.RoomFloorCutMin);
|
||||||
|
settings.roomFloorCutMaximumQ16=toQ16(params.RoomFloorCutMax);
|
||||||
|
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,
|
||||||
|
settings,rooms,edges,error))return false;
|
||||||
|
plannedRooms.insert(plannedRooms.end(),
|
||||||
|
std::make_move_iterator(rooms.begin()),std::make_move_iterator(rooms.end()));
|
||||||
|
plannedEdges.insert(plannedEdges.end(),
|
||||||
|
std::make_move_iterator(edges.begin()),std::make_move_iterator(edges.end()));
|
||||||
|
}
|
||||||
|
if(!ResolveResolvedCavernGraphV1(layout,owner.Seed,
|
||||||
|
plannedRooms,plannedEdges,error))return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto toCoreSubvoxels=[&](std::int64_t micrometres)
|
||||||
|
{
|
||||||
|
return static_cast<std::int64_t>(std::llround(
|
||||||
|
static_cast<double>(micrometres)*WorldAssemblySubvoxelsPerVoxel
|
||||||
|
/layout.coreVoxelSizeMicrometres));
|
||||||
|
};
|
||||||
|
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||||
|
{
|
||||||
|
if(!slot.Definition
|
||||||
|
||owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ))
|
||||||
|
!=ECaveGeneratorType::TunnelNetwork)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 bool hasRoutePoints=std::count_if(stratum->anchors.begin(),
|
||||||
|
stratum->anchors.end(),[](const auto& anchor)
|
||||||
|
{return anchor.kind=="route_point"&&anchor.values.size()>=3;})>=2;
|
||||||
|
if(hasRoutePoints)continue;
|
||||||
|
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||||||
|
const ResolvedLayoutPassageV1* departure=nullptr;
|
||||||
|
for(const auto& passage:layout.passages)
|
||||||
|
{
|
||||||
|
if(passage.lowerStrateIndex==slot.StrateIndex
|
||||||
|
&&passage.upperStrateIndex<slot.StrateIndex)arrival=&passage;
|
||||||
|
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;}
|
||||||
|
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.units="1/256 Core voxel";
|
||||||
|
anchor.values={point[0],point[1],point[2]};
|
||||||
|
stratum->anchors.push_back(std::move(anchor));
|
||||||
|
};
|
||||||
|
const auto sameRoutePoint=[](const auto& a,const auto& b)
|
||||||
|
{
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
if(std::abs(a[axis]-b[axis])>2)return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
constexpr std::int64_t PlayerCenterAboveFloorMicrometres=1130000;
|
||||||
|
auto start=arrival->lowerMouthSubvoxels;
|
||||||
|
start[2]+=toCoreSubvoxels(PlayerCenterAboveFloorMicrometres);
|
||||||
|
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-"
|
||||||
|
+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;
|
||||||
|
if(edgeIndex==0)
|
||||||
|
{
|
||||||
|
auto inner=edge->controlPointsMicrometres.front();
|
||||||
|
inner[2]+=PlayerCenterAboveFloorMicrometres;
|
||||||
|
const std::array<std::int64_t,3> point={
|
||||||
|
toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
|
||||||
|
toCoreSubvoxels(inner[2])};
|
||||||
|
if(!sameRoutePoint(point,start))
|
||||||
|
appendRoutePoint("000-edge-start",point);
|
||||||
|
}
|
||||||
|
auto point=edge->controlPointsMicrometres.back();
|
||||||
|
point[2]+=PlayerCenterAboveFloorMicrometres;
|
||||||
|
appendRoutePoint(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())
|
||||||
|
{
|
||||||
|
auto inner=departureEdge->controlPointsMicrometres.front();
|
||||||
|
inner[2]+=PlayerCenterAboveFloorMicrometres;
|
||||||
|
const std::array<std::int64_t,3> innerPoint={
|
||||||
|
toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
|
||||||
|
toCoreSubvoxels(inner[2])};
|
||||||
|
auto end=departure->upperMouthSubvoxels;
|
||||||
|
end[2]+=toCoreSubvoxels(PlayerCenterAboveFloorMicrometres);
|
||||||
|
if(!sameRoutePoint(innerPoint,end))
|
||||||
|
appendRoutePoint("998-departure-inner",innerPoint);
|
||||||
|
}
|
||||||
|
auto end=departure->upperMouthSubvoxels;
|
||||||
|
end[2]+=toCoreSubvoxels(PlayerCenterAboveFloorMicrometres);
|
||||||
|
appendRoutePoint("999-departure",end);
|
||||||
|
}
|
||||||
|
return layout.finalize(error);
|
||||||
|
}
|
||||||
|
|
||||||
inline bool ImportExportedCoreWorldAssembly(const VFJson::Value& root,
|
inline bool ImportExportedCoreWorldAssembly(const VFJson::Value& root,
|
||||||
VoxelForgeCore::WorldAssemblyManifest& out,std::string& error)
|
VoxelForgeCore::WorldAssemblyManifest& out,std::string& error)
|
||||||
{
|
{
|
||||||
@@ -1184,8 +1392,18 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
if(!slot.Definition)continue;
|
if(!slot.Definition)continue;
|
||||||
const auto type=owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ));
|
const auto type=owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ));
|
||||||
const auto authored=sealAndDensity(*slot.Definition,type);
|
const auto authored=sealAndDensity(*slot.Definition,type);
|
||||||
const bool nativeCaves=recipeVariant=="caves"
|
const bool nativeSurfaceCaves=recipeVariant=="caves"
|
||||||
&&type==ECaveGeneratorType::SurfaceWorld;
|
&&type==ECaveGeneratorType::SurfaceWorld;
|
||||||
|
const bool hasLowerStrate=std::any_of(owner.Manager->GetLayout().begin(),
|
||||||
|
owner.Manager->GetLayout().end(),[&](const FStrateSlot& candidate)
|
||||||
|
{return candidate.StrateIndex<slot.StrateIndex;});
|
||||||
|
const bool hasUpperStrate=std::any_of(owner.Manager->GetLayout().begin(),
|
||||||
|
owner.Manager->GetLayout().end(),[&](const FStrateSlot& candidate)
|
||||||
|
{return candidate.StrateIndex>slot.StrateIndex;});
|
||||||
|
const bool nativeTunnelCaves=type==ECaveGeneratorType::TunnelNetwork
|
||||||
|
&&recipeVariant!="legacy-tunnel-network"&&recipeVariant!="caves"
|
||||||
|
&&hasLowerStrate&&hasUpperStrate;
|
||||||
|
const bool nativeCaves=nativeSurfaceCaves||nativeTunnelCaves;
|
||||||
const bool nativeLattice=recipeVariant==("lattice/"+std::to_string(slot.StrateIndex))
|
const bool nativeLattice=recipeVariant==("lattice/"+std::to_string(slot.StrateIndex))
|
||||||
&&(type==ECaveGeneratorType::Maze||type==ECaveGeneratorType::VerticalShafts);
|
&&(type==ECaveGeneratorType::Maze||type==ECaveGeneratorType::VerticalShafts);
|
||||||
const std::int64_t bottom=static_cast<std::int64_t>(slot.BottomChunkZ)*owner.StrateLayoutChunkSizeMicrometres;
|
const std::int64_t bottom=static_cast<std::int64_t>(slot.BottomChunkZ)*owner.StrateLayoutChunkSizeMicrometres;
|
||||||
@@ -1195,7 +1413,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
if(seal<=0||2*seal>=top-bottom)
|
if(seal<=0||2*seal>=top-bottom)
|
||||||
{error="strate "+std::to_string(slot.StrateIndex)+" has an invalid BoundarySealThickness for two boundary bands";return false;}
|
{error="strate "+std::to_string(slot.StrateIndex)+" has an invalid BoundarySealThickness for two boundary bands";return false;}
|
||||||
ResolvedLayoutStratumV2 s;s.strateIndex=slot.StrateIndex;
|
ResolvedLayoutStratumV2 s;s.strateIndex=slot.StrateIndex;
|
||||||
s.generatorKind=nativeCaves?"Caves":GeneratorKind(type);
|
s.generatorKind=nativeSurfaceCaves?"Caves":GeneratorKind(type);
|
||||||
s.stableId=out.worldIdentity+"/strate/"+std::to_string(slot.StrateIndex)
|
s.stableId=out.worldIdentity+"/strate/"+std::to_string(slot.StrateIndex)
|
||||||
+"/generator/"+s.generatorKind;
|
+"/generator/"+s.generatorKind;
|
||||||
s.recipeId=nativeLattice?"Core.Lattice.PrimitiveField"
|
s.recipeId=nativeLattice?"Core.Lattice.PrimitiveField"
|
||||||
@@ -1407,6 +1625,8 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
passageConfigs.push_back(config);
|
passageConfigs.push_back(config);
|
||||||
}
|
}
|
||||||
if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false;
|
if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false;
|
||||||
|
if(recipeVariant!="legacy-tunnel-network"&&recipeVariant!="caves")
|
||||||
|
return PlanOwnerTunnelNetworkCaves(owner,out,error);
|
||||||
return out.finalize(error);
|
return out.finalize(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173
|
||||||
|
[PipelineWholeViewHash] tiles=2370 density=0x0a735dd766ac8e17 mesh=0x6531939ce3e4df5e payload=0xe79029d3900761fa topology=0x45a4b93eb0997a6b
|
||||||
|
[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=0xc0768e7fec442648 mesh=0x4e8a165606f5b5df payload=0x5949dc76575186d0 topology=0xd2cc03a9bd205f5b
|
||||||
|
[PipelineLODHash] lod=3 tiles=208 density=0xdd07fe8f31ede73a mesh=0xfaa2f90438226625 payload=0x86904d144f5db47c topology=0xcfd10c90d38f3fca
|
||||||
|
[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=0x03331b1721d89092 mesh=0x1fa61520dc6c6d7b payload=0xe5c306e087381056 topology=0xef499f11d87ca569
|
||||||
|
[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=0x8581c27a6966cccd mesh=0x610a6eb3ef75fe11 payload=0xdecaf47d125efd5c topology=0xa5ea883ebeb628e4
|
||||||
|
[PipelineLODHash] lod=3 tiles=208 density=0x5d87341a6428ea0b mesh=0x662096703512f5e2 payload=0x5703acca3ad9c2c7 topology=0xc8fb411f09c46f60
|
||||||
|
[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]:
|
def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]:
|
||||||
if recipe == "surface-world" and style == 1:
|
if recipe == "surface-world" and style == 1:
|
||||||
return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v227.baseline",
|
return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v230.baseline",
|
||||||
"L130-v227-reach-units")
|
"L134-v230-continuous-backbone")
|
||||||
if recipe == "floating-islands" and style == 2:
|
if recipe == "floating-islands" and style == 2:
|
||||||
return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v227.baseline",
|
return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v230.baseline",
|
||||||
"L130-v227-reach-units")
|
"L134-v230-continuous-backbone")
|
||||||
raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")
|
raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")
|
||||||
|
|
||||||
|
|
||||||
@@ -538,6 +538,19 @@ def run_density_dispatch_report(runner: VerificationRunner, binary: Path) -> Non
|
|||||||
voxel_size_um = int(strata[0]["voxel_size_um"])
|
voxel_size_um = int(strata[0]["voxel_size_um"])
|
||||||
if voxel_size_um <= 0:
|
if voxel_size_um <= 0:
|
||||||
raise ValueError(f"invalid voxel_size_um={voxel_size_um}")
|
raise ValueError(f"invalid voxel_size_um={voxel_size_um}")
|
||||||
|
tunnel_network = strata[2]
|
||||||
|
if (tunnel_network.get("generator_kind") != "TunnelNetwork"
|
||||||
|
or tunnel_network.get("recipe_id") != "Core.Caves.PrimitiveField"
|
||||||
|
or int(tunnel_network.get("recipe_version", 0)) != 1):
|
||||||
|
failures.append(f"{name}: Tunnel Network did not bind Core cave recipe v1: "
|
||||||
|
f"generator={tunnel_network.get('generator_kind')} "
|
||||||
|
f"recipe={tunnel_network.get('recipe_id')} "
|
||||||
|
f"version={tunnel_network.get('recipe_version')}")
|
||||||
|
if dispatch.get(2) != ("Caves-native", "TunnelNetwork",
|
||||||
|
"Core.Caves.PrimitiveField", 1,
|
||||||
|
"FCavesFieldAdapter"):
|
||||||
|
failures.append(f"{name}: Tunnel Network dispatch is not native caves: "
|
||||||
|
f"{dispatch.get(2)}")
|
||||||
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error:
|
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error:
|
||||||
failures.append(f"{name}: cannot classify tiles from resolved layout: {error}")
|
failures.append(f"{name}: cannot classify tiles from resolved layout: {error}")
|
||||||
continue
|
continue
|
||||||
@@ -604,7 +617,8 @@ def run_density_dispatch_report(runner: VerificationRunner, binary: Path) -> Non
|
|||||||
failures.append("no tile centers were attributed to any bound path")
|
failures.append("no tile centers were attributed to any bound path")
|
||||||
runner.require("owner-density-dispatch-report", not failures,
|
runner.require("owner-density-dispatch-report", not failures,
|
||||||
"; ".join(failures) if failures else
|
"; ".join(failures) if failures else
|
||||||
"four owner poses reported per-strate bound paths and tile-center path shares")
|
"four owner poses reported per-strate bound paths and tile-center path shares; "
|
||||||
|
"TN binds Core.Caves.PrimitiveField v1 through FCavesFieldAdapter")
|
||||||
|
|
||||||
|
|
||||||
def run_landmark_reachability(runner: VerificationRunner,
|
def run_landmark_reachability(runner: VerificationRunner,
|
||||||
@@ -1064,7 +1078,7 @@ def run_ore_vein_hash_controls(runner: VerificationRunner) -> None:
|
|||||||
def cavern_audit_cameras(layout_path: Path) -> list[tuple[float, float, float, float, float]]:
|
def cavern_audit_cameras(layout_path: Path) -> list[tuple[float, float, float, float, float]]:
|
||||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||||
edges = layout.get("cavern_edges", [])
|
edges = layout.get("cavern_edges", [])
|
||||||
if layout.get("cavern_graph_vocabulary_version") != 1 or len(edges) != 1:
|
if layout.get("cavern_graph_vocabulary_version") not in (1, 2) or len(edges) != 1:
|
||||||
raise VerificationFailure(f"missing one resolved cavern edge in {layout_path}")
|
raise VerificationFailure(f"missing one resolved cavern edge in {layout_path}")
|
||||||
points = [[value / 1_000_000.0 for value in point]
|
points = [[value / 1_000_000.0 for value in point]
|
||||||
for point in edges[0]["control_points_um"]]
|
for point in edges[0]["control_points_um"]]
|
||||||
@@ -1138,7 +1152,7 @@ def run_native_caves_field_audit(runner: VerificationRunner, binary: Path) -> tu
|
|||||||
r"false_all_air=REJECTED false_all_solid=REJECTED .*status=PASS$"),
|
r"false_all_air=REJECTED false_all_solid=REJECTED .*status=PASS$"),
|
||||||
"brute-force interval proofs have zero misses and reject both false-proof controls")
|
"brute-force interval proofs have zero misses and reject both false-proof controls")
|
||||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||||
graph_ok = (layout.get("cavern_graph_vocabulary_version") == 1
|
graph_ok = (layout.get("cavern_graph_vocabulary_version") in (1, 2)
|
||||||
and len(layout.get("cavern_rooms", [])) == 1
|
and len(layout.get("cavern_rooms", [])) == 1
|
||||||
and len(layout.get("cavern_edges", [])) == 2)
|
and len(layout.get("cavern_edges", [])) == 2)
|
||||||
runner.require(f"native-caves-field-{compiler}:resolved-graph", graph_ok,
|
runner.require(f"native-caves-field-{compiler}:resolved-graph", graph_ok,
|
||||||
@@ -1221,7 +1235,7 @@ def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) ->
|
|||||||
rooms = layout.get("cavern_rooms", [])
|
rooms = layout.get("cavern_rooms", [])
|
||||||
lattice_rows = [row for row in layout.get("strates", [])
|
lattice_rows = [row for row in layout.get("strates", [])
|
||||||
if row.get("strate_index") == 1]
|
if row.get("strate_index") == 1]
|
||||||
graph_ok = (layout.get("cavern_graph_vocabulary_version") == 1
|
graph_ok = (layout.get("cavern_graph_vocabulary_version") in (1, 2)
|
||||||
and len(edges) >= 9 and len(rooms) >= 8
|
and len(edges) >= 9 and len(rooms) >= 8
|
||||||
and len(lattice_rows) == 1
|
and len(lattice_rows) == 1
|
||||||
and lattice_rows[0].get("generator_kind") == "Maze"
|
and lattice_rows[0].get("generator_kind") == "Maze"
|
||||||
@@ -1247,7 +1261,7 @@ def lattice_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[fl
|
|||||||
edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"])
|
edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"])
|
||||||
helix = next((edge for edge in edges
|
helix = next((edge for edge in edges
|
||||||
if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None)
|
if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None)
|
||||||
if layout.get("cavern_graph_vocabulary_version") != 1 or helix is None:
|
if layout.get("cavern_graph_vocabulary_version") not in (1, 2) or helix is None:
|
||||||
raise VerificationFailure(f"missing resolved lattice helix in {layout_path}")
|
raise VerificationFailure(f"missing resolved lattice helix in {layout_path}")
|
||||||
helix_points = [[value / 1_000_000.0 for value in point]
|
helix_points = [[value / 1_000_000.0 for value in point]
|
||||||
for point in helix.get("polyline_um", [])]
|
for point in helix.get("polyline_um", [])]
|
||||||
@@ -1400,7 +1414,7 @@ def run_lattice_performance(runner: VerificationRunner, binary: Path,
|
|||||||
def cave_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[float, float, float, float, float]]:
|
def cave_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[float, float, float, float, float]]:
|
||||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||||
edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"])
|
edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"])
|
||||||
if layout.get("cavern_graph_vocabulary_version") != 1 or len(edges) != 2:
|
if layout.get("cavern_graph_vocabulary_version") not in (1, 2) or len(edges) != 2:
|
||||||
raise VerificationFailure(f"missing two resolved cave spine edges in {layout_path}")
|
raise VerificationFailure(f"missing two resolved cave spine edges in {layout_path}")
|
||||||
rng = _SplitMix64(CAVES_CAMERA_SWEEP_SEED)
|
rng = _SplitMix64(CAVES_CAMERA_SWEEP_SEED)
|
||||||
cameras = []
|
cameras = []
|
||||||
|
|||||||