diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index cb2b2dd..416ed76 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 composes deterministic cave + // graphs from fixed passage mouths and the owner Tunnel Network settings. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228; enum class FieldChannel : std::uint32_t { @@ -991,6 +992,34 @@ namespace VoxelForgeCore std::array 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; + 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 // micrometres; bounds include irregularity and the blend envelope. struct ResolvedLayoutOreVeinRequestV1 @@ -1095,7 +1124,7 @@ namespace VoxelForgeCore inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept { - return stratum.generatorKind=="Caves" + return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork") &&stratum.recipeId=="Core.Caves.PrimitiveField" &&stratum.recipeVersion==1; } @@ -1256,6 +1285,17 @@ namespace VoxelForgeCore std::span rooms, std::span edges, 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& rooms, + std::vector& edges, + std::string& error); VFCORE_API bool ResolveResolvedLayoutOreVeinsV1( ResolvedLayoutSnapshotV3& layout, std::int32_t seed, std::span requests, diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 5fcb719..a56112e 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -2031,6 +2031,458 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t layout=std::move(candidate);return true; } +bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + std::int32_t seed,std::int32_t strateIndex, + const ResolvedCavernPlannerSettingsV1& settings, + std::vector& rooms, + std::vector& 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.maximumRoomRadiusMicrometres100000000 + ||settings.destinationRadiusMicrometres100000000 + ||settings.maximumTunnelLengthMicrometres<=0 + ||settings.minimumTunnelRadiusMicrometres<0 + ||settings.maximumTunnelRadiusMicrometres10000000 + ||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.upperStrateIndexstableId)) + arrivalPassage=&passage; + if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex + &&(!departurePassage||passage.stableIdstableId)) + 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; + 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 MouthFloorOffset=750000; + 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(dx*dx+dy*dy); + return static_cast(integerSqrt(squared)); + }; + const auto qToMicrometres=[&](std::int64_t q) + {return q*static_cast(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]); + point[2]-=MouthFloorOffset; + 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(integerSqrt( + static_cast(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( + 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(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 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; + points[2][0]-=perpX;points[2][1]-=perpY; + return points; + }; + const auto routeLength=[&](const std::array& points) + { + std::int64_t length=0; + for(std::size_t i=1;i0)lateral=std::min(lateral*2,1000000000ll); + else lateral=std::max(lateral*2,-1000000000ll); + routePoints=makeRoute(lateral); + } + const std::int64_t totalRouteLength=routeLength(routePoints); + if(totalRouteLength<=0||totalRouteLength cumulative{}; + for(std::size_t i=1;i(distance,0,totalRouteLength); + std::size_t segment=1; + while(segmentcumulative[segment])++segment; + const std::int64_t startDistance=cumulative[segment-1]; + const std::int64_t segmentLength=std::max(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]=routeStart[2]+dz*distance/totalRouteLength; + return point; + }; + std::int64_t spacing=std::min(std::max(settings.roomSpacingMicrometres, + 4000000),settings.maximumTunnelLengthMicrometres); + spacing=std::max(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 nodeDistances; + for(std::size_t leg=1;leg(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&&distance256) + {error="cavern planner would exceed the 256-node bounded backbone";return false;} + + const auto clampQ16=[](std::int32_t value) + {return std::clamp(value,0,65536);}; + const std::int64_t smooth=std::clamp( + std::max(1500000,settings.smoothWidthMicrometres), + 50000,10000000); + const std::int64_t irregularity=std::clamp( + settings.irregularityAmplitudeMicrometres,0,500000); + const std::int64_t routeBoreRadius=std::clamp( + std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres), + MinimumBoreRadius,MaximumBoreRadius); + const std::int64_t maximumRouteBoreRadius=std::clamp( + std::max(routeBoreRadius,settings.maximumTunnelRadiusMicrometres), + routeBoreRadius,MaximumBoreRadius); + const auto selectedBoreRadius=[&](const std::string& stableId) + { + const std::uint64_t span=static_cast( + maximumRouteBoreRadius-routeBoreRadius); + return routeBoreRadius+(span==0?0:static_cast( + 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::max(routeBoreRadius,radius), + routeBoreRadius,MaximumBoreRadius); + }; + const auto fractional=[](std::int64_t value,std::int64_t q16) + {return value*std::clamp(q16,0,65536)/65536;}; + struct FRoomNode + { + std::string id; + FPoint point{}; + std::int64_t radiusMicrometres=0; + bool destination=false; + }; + std::vector 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( + 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(hash%static_cast(baseRoomSpan+1)); + nodes.push_back(std::move(node)); + } + const auto makeCompound=[&](const FRoomNode& node,bool destination, + std::vector& 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( + variety*2/65536); + const std::size_t primitiveCount=2+static_cast( + 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(primitiveCount); + for(std::int64_t component=0;component(componentHash%22001); + const std::int64_t componentRadius=std::max(500000, + fractional(radius,componentScaleQ16)); + const std::int64_t axisScaleQ16=component==0?65536 + :47000+static_cast((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(500000, + fractional(componentRadius,heightRatio)); + const std::int64_t floorCut=floorCutBase + +(floorRange==0?0:static_cast(componentHash%( + static_cast(floorRange+1)))); + const std::int64_t offsetMagnitude=component==0?0 + :fractional(radius,12000+static_cast(componentHash%11001)); + const std::int64_t routeLengthXY=std::max(1, + static_cast(integerSqrt(static_cast( + pathAxisX*pathAxisX+pathAxisY*pathAxisY)))); + const std::int64_t centerOffset=(component%2==0?1:-1)*offsetMagnitude; + ResolvedCavernRoomV1 room;room.stableId=stableId; + room.strateIndex=strateIndex;room.floorMicrometres=node.point[2]; + room.halfExtentsMicrometres={halfX,halfY,halfZ}; + room.centerMicrometres={node.point[0]+(-pathAxisY)*centerOffset/routeLengthXY, + node.point[1]+pathAxisX*centerOffset/routeLengthXY, + node.point[2]+fractional(halfZ,floorCut)}; + if(component>0) + { + const std::int64_t headroom=std::max(0, + halfZ-fractional(halfZ,floorCut)); + const std::int64_t jitterRange=std::min(headroom,halfZ/5); + const std::int64_t ceilingJitter=jitterRange>0 + ?static_cast((componentHash>>32) + %static_cast(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) + { + 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=0; + 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]; + } + return edge; + }; + const auto arrivalPoint=routeStart; + FPoint previousPoint=arrivalPoint; + std::string previousAnchor=arrivalPassage->stableId+"/lower-mouth"; + for(std::size_t nodeIndex=0;nodeIndexstableId+"/upper-mouth", + routeEnd,nodes.back(),passageBoreRadius(*departurePassage,false))); + } + + 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::clamp(settings.originRoomMaximumConnections,0,32)); + std::size_t branchCount=0; + for(std::size_t nodeIndex=1;nodeIndex+1(branchHash&0xffffu)>=branchThreshold)continue; + const std::int64_t branchLength=std::max( + 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((branchHash>>17) + %static_cast(baseRoomSpan+1)); + const std::int64_t pathAxisLength=std::max(1, + static_cast(integerSqrt(static_cast( + pathAxisX*pathAxisX+pathAxisY*pathAxisY)))); + branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction, + parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction, + parent.point[2]}; + makeCompound(branch,false,rooms); + const std::string branchEdgeId="tunnel-network/strate-" + +std::to_string(strateIndex)+"/side-edge-"+std::to_string(branchCount); + if(settings.tunnelsFlowTowardOrigin) + edges.push_back(makeEdge(branchEdgeId,componentRoomId(branch), + branch.point,parent,selectedBoreRadius(branchEdgeId))); + else + edges.push_back(makeEdge(branchEdgeId,componentRoomId(parent), + parent.point,branch,selectedBoreRadius(branchEdgeId))); + ++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(integerSqrt( + static_cast((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(rise),static_cast(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, std::span requests,std::string& error) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 37bad31..6651169 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -1221,16 +1221,15 @@ public: double unionDistance=std::numeric_limits::infinity(); double unionShellDistance=std::numeric_limits::infinity(); const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); - const auto* queryStratum=FindBoundStratumForManagerZ(Z); for(std::size_t passageIndex=0;passageIndexpassages.size(); ++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; - if(queryStratum&&queryStratum->Resolved - &&queryStratum->Resolved->passageCapabilities.hasNativeCaveField - &&passage.lowerStrateIndex==queryStratum->StrateIndex) - continue; + // Native graph fields own their local rooms and tunnels, not the + // inter-strate connection above their mouth. Union the shared passage + // field here as well so the connector remains open through the boundary + // band for both native caves and future native lattice layers. if(passageProfile)++passageProfile->passageIterations; if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex)) continue; @@ -7488,7 +7487,16 @@ private: const double delta=(point[axis]-feature.centerMetres[axis])/radius; normalizedSquared+=delta*delta;meanRadius+=radius/3.0; } - const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius; + double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius; + 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]; const double width=feature.room->smoothWidthMicrometres/1000000.0; featureVoid=SmoothMaxMetres(ellipsoid,floor,width); @@ -7526,7 +7534,17 @@ private: } const double normalized=std::sqrt(radialSquared/(radius*radius) +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(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 rooms; std::vector edges; - if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error) - ||!VoxelForgeCore::ResolveResolvedCavernGraphV1( + if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error)) + {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)) {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 rooms; std::vector edges; if(!MakeNativeLatticeSpine(resolvedLayout,seed,options.LatticeStrateIndex, - options.LatticeOrphanControl,rooms,edges,error) - ||!VoxelForgeCore::ResolveResolvedCavernGraphV1( + options.LatticeOrphanControl,rooms,edges,error)) + {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)) {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 auto& stratum){return VoxelForgeCore::IsNativeLatticeFieldStratum(stratum) &&stratum.passageCapabilities.hasNativeLatticeField;}); - const auto actualRecipe=hasNativeLatticeRecipe?std::string_view("lattice") - :hasNativeCavesRecipe?std::string_view("caves") + const auto actualRecipe=requestedRecipe=="lattice"&&hasNativeLatticeRecipe + ?std::string_view("lattice") + :requestedRecipe=="caves"&&hasNativeCavesRecipe + ?std::string_view("caves") :requestedRecipe=="surface-world"&&hasSurfaceWorldPlan ?std::string_view("surface-world") :requestedRecipe=="floating-islands"&&hasFloatingIslands ?std::string_view("floating-islands") + :hasNativeLatticeRecipe?std::string_view("lattice") + :hasNativeCavesRecipe?std::string_view("caves") :hasFloatingIslands?std::string_view("floating-islands") :hasSurfaceWorldPlan?std::string_view("surface-world") :std::string_view("missing"); diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 06626c9..d152a19 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1595,6 +1595,205 @@ namespace StandaloneStreamBenchmark 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(1,voxelMicrometres)/1.0e6; + const auto density=[&](double x,double y,double z) + {return static_cast(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;}); + if(cave==layout.strates.end()||layout.cavernGraphVocabularyVersion!=1) + {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.upperStrateIndexstrateIndex)arrival=&passage; + if(passage.upperStrateIndex==cave->strateIndex + &&passage.lowerStrateIndex>cave->strateIndex)departure=&passage; + } + std::vector rooms; + std::vector 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::stoull(edge->stableId.substr(marker+12))); + }; + std::sort(routeEdges.begin(),routeEdges.end(),[&](const auto* a,const auto* b) + {return routeIndex(a) compoundCounts; + std::unordered_map 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 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;ivalues; + const auto& b=routeAnchors[i]->values; + const double run=std::hypot(static_cast(b[0]-a[0]), + static_cast(b[1]-a[1])); + const double rise=std::abs(static_cast(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::llround( + 250000.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(p[3][0]-p[0][0]), + static_cast(p[3][1]-p[0][1])); + if(length>longest){longest=length;rippleEdge=edge;} + } + std::vector rippleRadii; + if(rippleEdge) + { + const auto& p=rippleEdge->controlPointsMicrometres; + const double radius=rippleEdge->boreRadiusMicrometres/1.0e6; + constexpr std::array 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(airSamples),static_cast(airSamples), + static_cast(floorSamples),static_cast(floorSamples), + static_cast(shellSamples),static_cast(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, const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::uint32_t voxelMicrometres) { @@ -1605,9 +1804,13 @@ namespace StandaloneStreamBenchmark const auto density=[&](double x,double y,double z) {return static_cast(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=="Caves" - &&s.recipeId=="Core.Caves.PrimitiveField"&&s.recipeVersion==1 + [](const auto& s){return IsNativeCaveFieldStratum(s) &&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) {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;} @@ -4796,6 +4999,7 @@ namespace StandaloneStreamBenchmark } } 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) { std::vector anchors; @@ -4859,36 +5063,56 @@ namespace StandaloneStreamBenchmark supportStepMetres,supportMaximumMetres,minimumSupportProbes, supportProbeRadiusMetres); bool binding=true; + const auto equalPosition=[](const auto& a,const auto& b) + {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::llround( + 250000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + /World.CoreVoxelSizeMicrons)); + return equalPosition(authored,expected); + }; for(const auto& passage:ResolvedLayout->passages) { - const auto equalPosition=[](const auto& a,const auto& b) - {return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];}; if(passage.upperStrateIndex==stratum.strateIndex) { - const auto rootedMouth=std::find_if(stratum.anchors.begin(), - stratum.anchors.end(),[](const auto& anchor) - {return anchor.kind=="passage_anchor"&&anchor.values.size()>=10;}); - const std::array departure= - rootedMouth!=stratum.anchors.end() - ?std::array{{rootedMouth->values[7], - rootedMouth->values[8],rootedMouth->values[9]}} - :std::array{{anchors.back()->values[0], - anchors.back()->values[1],anchors.back()->values[2]}}; - binding&=equalPosition(passage.upperMouthSubvoxels, - departure); + if(stratum.generatorKind=="TunnelNetwork") + binding&=boundMouthMatches(anchors.back()->values, + passage.upperMouthSubvoxels); + else + { + const auto rootedMouth=std::find_if(stratum.anchors.begin(), + stratum.anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_anchor"&&anchor.values.size()>=10;}); + const std::array departure= + rootedMouth!=stratum.anchors.end() + ?std::array{{rootedMouth->values[7], + rootedMouth->values[8],rootedMouth->values[9]}} + :std::array{{anchors.back()->values[0], + anchors.back()->values[1],anchors.back()->values[2]}}; + binding&=equalPosition(passage.upperMouthSubvoxels,departure); + } } if(passage.lowerStrateIndex==stratum.strateIndex) { - const auto passageArrival=std::find_if(stratum.anchors.begin(), - stratum.anchors.end(),[](const auto& anchor) - {return anchor.kind=="passage_arrival"&&anchor.values.size()>=3;}); - const std::array arrival=passageArrival!=stratum.anchors.end() - ?std::array{{passageArrival->values[0], - passageArrival->values[1],passageArrival->values[2]}} - :std::array{{anchors.front()->values[0], - anchors.front()->values[1],anchors.front()->values[2]}}; - binding&=equalPosition(passage.lowerMouthSubvoxels, - arrival); + if(stratum.generatorKind=="TunnelNetwork") + binding&=boundMouthMatches(anchors.front()->values, + passage.lowerMouthSubvoxels); + else + { + const auto passageArrival=std::find_if(stratum.anchors.begin(), + stratum.anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_arrival"&&anchor.values.size()>=3;}); + const std::array arrival=passageArrival!=stratum.anchors.end() + ?std::array{{passageArrival->values[0], + passageArrival->values[1],passageArrival->values[2]}} + :std::array{{anchors.front()->values[0], + anchors.front()->values[1],anchors.front()->values[2]}}; + binding&=equalPosition(passage.lowerMouthSubvoxels,arrival); + } } } ++routePasses;routeWalkPasses+=routeAudit.passed; @@ -4918,6 +5142,18 @@ namespace StandaloneStreamBenchmark stratum.strateIndex,routeAudit.firstUnsupported[i][0], routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2], routeAudit.firstUnsupported[i][3]); + if(stratum.generatorKind=="TunnelNetwork") + { + ++tunnelNetworkRoutes; + const FWalkAudit control=auditPlayerPath(blockedWorld,route, + supportStartMetres,supportStepMetres,supportMaximumMetres, + minimumSupportProbes,supportProbeRadiusMetres); + const bool detected=control.blocked>0&&!control.passed; + tunnelNetworkBlockedControls+=detected; + std::printf("[TunnelNetworkBlockedRouteControl] strate=%d arrival_to_departure=true blocked_capsule_samples=%llu result=%s expected=FAIL status=%s\n", + stratum.strateIndex,static_cast(control.blocked), + control.passed?"PASS":"FAIL",detected?"PASS":"FAIL"); + } } for(std::size_t passageIndex=0; passageIndexpassages.size();++passageIndex) @@ -4984,13 +5220,16 @@ namespace StandaloneStreamBenchmark &&shellAuditPassed &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings &&routeBindingFailures==0 + &&tunnelNetworkRoutes==tunnelNetworkBlockedControls &&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(passageCount),static_cast(passagePasses), static_cast(naturalPasses),static_cast(passageCount), static_cast(routePasses),static_cast(routeWalkPasses), static_cast(routePoints), static_cast(routeBindings),static_cast(routeBindingFailures), + static_cast(tunnelNetworkBlockedControls), + static_cast(tunnelNetworkRoutes), static_cast(blockedControls),static_cast(passageCount), auditPassed?"PASS":"FAIL"); if(!auditPassed) diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index dbe2a7c..ad093d5 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -1069,6 +1069,166 @@ namespace StandaloneWorldAssemblyPrototype 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 tunnelSlots; + 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()) + {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::llround(ownerVoxels + *static_cast(owner.OwnerVoxelSizeMicrons))); + }; + const auto toQ16=[](float value) + { + return static_cast(std::llround(std::clamp( + static_cast(value),0.0,1.0)*65536.0)); + }; + if(!tunnelSlots.empty()) + { + std::vector plannedRooms=layout.cavernRooms; + std::vector plannedEdges=layout.cavernEdges; + for(const FStrateSlot* slot:tunnelSlots) + { + const FStrateGenerationParams& params=slot->Definition->GenerationParams; + ResolvedCavernPlannerSettingsV1 settings; + settings.roomSpacingMicrometres=std::max(4000000, + toMicrometres(params.RoomSpacing)); + settings.minimumRoomRadiusMicrometres=std::max(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(0, + toMicrometres(params.TunnelMinRadius)); + settings.maximumTunnelRadiusMicrometres=std::max( + settings.minimumTunnelRadiusMicrometres,toMicrometres(params.TunnelMaxRadius)); + settings.maximumTunnelLengthMicrometres=std::max(4000000, + toMicrometres(params.MaxTunnelLength)); + settings.tunnelWarpMicrometres=std::max(0, + toMicrometres(params.TunnelWarpStrength)); + settings.caveWarpMicrometres=std::max(0, + toMicrometres(params.CaveWarpStrength)); + settings.smoothWidthMicrometres=std::max(1500000, + toMicrometres(params.SDFBlendRadius)); + settings.irregularityAmplitudeMicrometres=std::max(0, + toMicrometres(params.SurfaceRoughness)); + settings.irregularityWavelengthMicrometres=params.RoughnessFrequency>0.0f + ?static_cast(std::llround(owner.OwnerVoxelSizeMicrons + /static_cast(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 rooms; + std::vector 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::llround( + static_cast(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())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.upperStrateIndexslot.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& 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)); + }; + constexpr std::int64_t PlayerCenterAboveMouthFloorMicrometres=250000; + auto start=arrival->lowerMouthSubvoxels; + start[2]+=toCoreSubvoxels(PlayerCenterAboveMouthFloorMicrometres); + 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; + auto point=edge->controlPointsMicrometres.back(); + point[2]+=1000000; + appendRoutePoint(std::to_string(edgeIndex+1), + {toCoreSubvoxels(point[0]),toCoreSubvoxels(point[1]), + toCoreSubvoxels(point[2])}); + } + auto end=departure->upperMouthSubvoxels; + end[2]+=toCoreSubvoxels(PlayerCenterAboveMouthFloorMicrometres); + appendRoutePoint("999-departure",end); + } + return layout.finalize(error); + } + inline bool ImportExportedCoreWorldAssembly(const VFJson::Value& root, VoxelForgeCore::WorldAssemblyManifest& out,std::string& error) { @@ -1184,8 +1344,11 @@ namespace StandaloneWorldAssemblyPrototype if(!slot.Definition)continue; const auto type=owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ)); const auto authored=sealAndDensity(*slot.Definition,type); - const bool nativeCaves=recipeVariant=="caves" + const bool nativeSurfaceCaves=recipeVariant=="caves" &&type==ECaveGeneratorType::SurfaceWorld; + const bool nativeTunnelCaves=type==ECaveGeneratorType::TunnelNetwork + &&recipeVariant!="legacy-tunnel-network"&&recipeVariant!="caves"; + const bool nativeCaves=nativeSurfaceCaves||nativeTunnelCaves; const bool nativeLattice=recipeVariant==("lattice/"+std::to_string(slot.StrateIndex)) &&(type==ECaveGeneratorType::Maze||type==ECaveGeneratorType::VerticalShafts); const std::int64_t bottom=static_cast(slot.BottomChunkZ)*owner.StrateLayoutChunkSizeMicrometres; @@ -1195,7 +1358,7 @@ namespace StandaloneWorldAssemblyPrototype if(seal<=0||2*seal>=top-bottom) {error="strate "+std::to_string(slot.StrateIndex)+" has an invalid BoundarySealThickness for two boundary bands";return false;} 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) +"/generator/"+s.generatorKind; s.recipeId=nativeLattice?"Core.Lattice.PrimitiveField" @@ -1407,6 +1570,8 @@ namespace StandaloneWorldAssemblyPrototype passageConfigs.push_back(config); } 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); } diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 167da39..9db1b1d 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -538,6 +538,19 @@ def run_density_dispatch_report(runner: VerificationRunner, binary: Path) -> Non voxel_size_um = int(strata[0]["voxel_size_um"]) if voxel_size_um <= 0: 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: failures.append(f"{name}: cannot classify tiles from resolved layout: {error}") 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") runner.require("owner-density-dispatch-report", not failures, "; ".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,