core: cave planner holds for 16 seeds; mouth joints planned from the resolved mouth (L135, v236)
Mouth floor/profile carried into the first/last edges (fixes seeds 1 and 2); fixed a Q16/Q32 scale bug in the mouth clearance scorer; shortest grade-safe handles; bounded cave AABBs and hoisted edge-BVH normalisation. Seeds 0-15 pass all composed audits and negative controls; fast tier, Clang/GCC W1/W8/W12 green. Near-ready still +3.9..+13.7% vs assembler/world (Surface World +3.9..+8.2% with no caves). Not for merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
a3c8e3687d
commit
2d89023933
25 files changed
+2516
-235
No files matched your search
@@ -3176,13 +3176,15 @@ namespace StandaloneStreamBenchmark
|
||||
const auto passageUpperMouthFloorDepth=[&](
|
||||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
||||
{
|
||||
const double controlZ=qToMetres(
|
||||
passage.controlPoints.front().positionSubvoxels[2]);
|
||||
const double authoredMouthDepth=std::max(0.0,controlZ
|
||||
-qToMetres(passage.upperMouthSubvoxels[2])+1.13);
|
||||
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||
const double mouthCenterHeight=upperStratum!=ResolvedLayout->strates.end()
|
||||
&&upperStratum->generatorKind=="SurfaceWorld"?0.98:1.13;
|
||||
const double controlZ=qToMetres(
|
||||
passage.controlPoints.front().positionSubvoxels[2]);
|
||||
const double authoredMouthDepth=std::max(0.0,controlZ
|
||||
-qToMetres(passage.upperMouthSubvoxels[2])+mouthCenterHeight);
|
||||
if(upperStratum==ResolvedLayout->strates.end()
|
||||
||(upperStratum->generatorKind!="FloatingIslands"
|
||||
&&upperStratum->generatorKind!="TunnelNetwork"))
|
||||
@@ -3202,7 +3204,8 @@ namespace StandaloneStreamBenchmark
|
||||
||distanceSquared>std::pow(
|
||||
qToMetres(passage.controlPoints.front().radiusSubvoxels)+2.0,2.0))
|
||||
return authoredMouthDepth;
|
||||
const double routeAlignedDepth=std::max(0.0,controlZ-routeHeight+1.13);
|
||||
const double routeAlignedDepth=std::max(0.0,
|
||||
controlZ-routeHeight+mouthCenterHeight);
|
||||
return std::max(authoredMouthDepth,routeAlignedDepth);
|
||||
};
|
||||
const auto passageSplineControls=[&](
|
||||
@@ -3349,61 +3352,124 @@ namespace StandaloneStreamBenchmark
|
||||
};
|
||||
std::vector<double> allPassageBodyFloorDepths;
|
||||
std::vector<std::vector<FPassageAuditRoutePoint>> allPassageSmoothRoutes;
|
||||
struct FUpperMouthFloorBlend
|
||||
struct FMouthFloorBlend
|
||||
{
|
||||
std::vector<std::array<double,3>> receivingRoute;
|
||||
double mouthX=0.0,mouthY=0.0,mouthFloor=0.0;
|
||||
double tangentX=0.0,tangentY=0.0;
|
||||
};
|
||||
std::vector<FUpperMouthFloorBlend> allUpperMouthFloorBlends;
|
||||
struct FPassageFloorBlends
|
||||
{
|
||||
FMouthFloorBlend upper,lower;
|
||||
};
|
||||
std::vector<FPassageFloorBlends> allPassageFloorBlends;
|
||||
allPassageBodyFloorDepths.reserve(ResolvedLayout->passages.size());
|
||||
allPassageSmoothRoutes.reserve(ResolvedLayout->passages.size());
|
||||
allUpperMouthFloorBlends.reserve(ResolvedLayout->passages.size());
|
||||
allPassageFloorBlends.reserve(ResolvedLayout->passages.size());
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
const double bodyDepth=passageBodyFloorDepth(passage);
|
||||
allPassageBodyFloorDepths.push_back(bodyDepth);
|
||||
allPassageSmoothRoutes.push_back(passageSmoothRoute(passage,bodyDepth));
|
||||
FUpperMouthFloorBlend blend;
|
||||
FPassageFloorBlends blends;
|
||||
const auto controls=passageSplineControls(passage);
|
||||
const auto floorProfile=passageSplineFloorProfile(passage);
|
||||
const std::size_t routeEnd=std::clamp<std::size_t>(
|
||||
passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1);
|
||||
const auto segments=VoxelForgeCore::PassageSpline::BuildSegments(
|
||||
controls,routeEnd,8.0,1.0,floorProfile,0.100);
|
||||
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||
if(upperStratum!=ResolvedLayout->strates.end()
|
||||
&&upperStratum->generatorKind=="FloatingIslands")
|
||||
&&(upperStratum->generatorKind=="FloatingIslands"
|
||||
||upperStratum->generatorKind=="TunnelNetwork"))
|
||||
{
|
||||
for(const auto& anchor:upperStratum->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||
blend.receivingRoute.push_back({
|
||||
blends.upper.receivingRoute.push_back({
|
||||
qToMetres(anchor.values[0]),qToMetres(anchor.values[1]),
|
||||
qToMetres(anchor.values[2])});
|
||||
const auto controls=passageSplineControls(passage);
|
||||
const auto floorProfile=passageSplineFloorProfile(passage);
|
||||
const std::size_t routeEnd=std::clamp<std::size_t>(
|
||||
passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1);
|
||||
const auto segments=VoxelForgeCore::PassageSpline::BuildSegments(
|
||||
controls,routeEnd,8.0,1.0,floorProfile,0.100);
|
||||
if(upperStratum->generatorKind=="TunnelNetwork"
|
||||
&&blends.upper.receivingRoute.size()>2)
|
||||
blends.upper.receivingRoute.erase(
|
||||
blends.upper.receivingRoute.begin(),
|
||||
blends.upper.receivingRoute.end()-2);
|
||||
if(!segments.empty())
|
||||
{
|
||||
blend.mouthX=segments.front().a[0];
|
||||
blend.mouthY=segments.front().a[1];
|
||||
blend.mouthFloor=floorProfile.front();
|
||||
blend.tangentX=segments.front().b[0]-segments.front().a[0];
|
||||
blend.tangentY=segments.front().b[1]-segments.front().a[1];
|
||||
blends.upper.mouthX=controls.front()[0];
|
||||
blends.upper.mouthY=controls.front()[1];
|
||||
blends.upper.mouthFloor=floorProfile.front();
|
||||
blends.upper.tangentX=segments.front().b[0]-segments.front().a[0];
|
||||
blends.upper.tangentY=segments.front().b[1]-segments.front().a[1];
|
||||
}
|
||||
if(upperStratum->generatorKind=="TunnelNetwork")
|
||||
for(const auto& edge:ResolvedLayout->cavernEdges)
|
||||
if(edge.startAnchorStableId==passage.stableId+"/upper-mouth"
|
||||
&&edge.controlPointsMicrometres.size()>=2)
|
||||
{
|
||||
blends.upper.tangentX=
|
||||
(edge.controlPointsMicrometres[0][0]
|
||||
-edge.controlPointsMicrometres[1][0])/1000000.0;
|
||||
blends.upper.tangentY=
|
||||
(edge.controlPointsMicrometres[0][1]
|
||||
-edge.controlPointsMicrometres[1][1])/1000000.0;
|
||||
}
|
||||
}
|
||||
allUpperMouthFloorBlends.push_back(std::move(blend));
|
||||
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"
|
||||
&&!controls.empty()&&routeEnd<controls.size())
|
||||
{
|
||||
for(const auto& anchor:lowerStratum->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||
blends.lower.receivingRoute.push_back({
|
||||
qToMetres(anchor.values[0]),qToMetres(anchor.values[1]),
|
||||
qToMetres(anchor.values[2])});
|
||||
if(blends.lower.receivingRoute.size()>2)
|
||||
blends.lower.receivingRoute.resize(2);
|
||||
blends.lower.mouthX=controls[routeEnd][0];
|
||||
blends.lower.mouthY=controls[routeEnd][1];
|
||||
blends.lower.mouthFloor=floorProfile[routeEnd];
|
||||
if(!segments.empty())
|
||||
{
|
||||
blends.lower.tangentX=controls[routeEnd][0]
|
||||
-controls[routeEnd-1][0];
|
||||
blends.lower.tangentY=controls[routeEnd][1]
|
||||
-controls[routeEnd-1][1];
|
||||
}
|
||||
for(const auto& edge:ResolvedLayout->cavernEdges)
|
||||
if(edge.startAnchorStableId==passage.stableId+"/lower-mouth"
|
||||
&&edge.controlPointsMicrometres.size()>=2)
|
||||
{
|
||||
blends.lower.tangentX=
|
||||
(edge.controlPointsMicrometres[1][0]
|
||||
-edge.controlPointsMicrometres[0][0])/1000000.0;
|
||||
blends.lower.tangentY=
|
||||
(edge.controlPointsMicrometres[1][1]
|
||||
-edge.controlPointsMicrometres[0][1])/1000000.0;
|
||||
}
|
||||
}
|
||||
allPassageFloorBlends.push_back(std::move(blends));
|
||||
}
|
||||
const auto passageFloorAtXY=[&](std::size_t passageIndex,double x,double y,
|
||||
double plannedFloor)
|
||||
{
|
||||
if(passageIndex>=allUpperMouthFloorBlends.size())return plannedFloor;
|
||||
const auto& blend=allUpperMouthFloorBlends[passageIndex];
|
||||
if(passageIndex>=allPassageFloorBlends.size())return plannedFloor;
|
||||
const auto& blends=allPassageFloorBlends[passageIndex];
|
||||
double floor=plannedFloor;
|
||||
floor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
|
||||
blends.upper.receivingRoute,x,y,floor,blends.upper.mouthX,
|
||||
blends.upper.mouthY,blends.upper.mouthFloor,
|
||||
blends.upper.tangentX,blends.upper.tangentY,
|
||||
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13);
|
||||
return VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
|
||||
blend.receivingRoute,x,y,plannedFloor,blend.mouthX,blend.mouthY,
|
||||
blend.mouthFloor,
|
||||
blend.tangentX,blend.tangentY,
|
||||
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,
|
||||
1.13);
|
||||
blends.lower.receivingRoute,x,y,floor,blends.lower.mouthX,
|
||||
blends.lower.mouthY,blends.lower.mouthFloor,
|
||||
-blends.lower.tangentX,-blends.lower.tangentY,
|
||||
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13);
|
||||
};
|
||||
const auto interpolatePassageRoute=[](
|
||||
const std::vector<FPassageAuditRoutePoint>& route,double distance)
|
||||
@@ -4361,7 +4427,8 @@ namespace StandaloneStreamBenchmark
|
||||
// The probes sit 0.18 m inside the authored radius. Add that
|
||||
// radius back to the signed wall distance so radius-carried ribs
|
||||
// remain visible; the cubic detrend removes the smooth mouth flare.
|
||||
radii.push_back(sampleRadius+0.5*(left+right));
|
||||
const double measuredRadius=sampleRadius+0.5*(left+right);
|
||||
radii.push_back(measuredRadius);
|
||||
++audit.samples;
|
||||
}
|
||||
if(radii.size()<static_cast<std::size_t>(WindowLengthMetres/
|
||||
@@ -4494,6 +4561,18 @@ namespace StandaloneStreamBenchmark
|
||||
constexpr double PassageShellAuditSpacingMetres=0.25;
|
||||
constexpr std::array<double,2> PassageShellAuditDepthsMetres{{0.125,0.375}};
|
||||
constexpr float PassageShellDensityTolerance=1.0e-6f;
|
||||
std::vector<bool> surfaceLandingPassages(ResolvedLayout->passages.size(),false);
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
||||
++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
const auto upper=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||
surfaceLandingPassages[passageIndex]=upper!=ResolvedLayout->strates.end()
|
||||
&&upper->generatorKind=="SurfaceWorld"
|
||||
&&upper->lowerInterfaceCapabilities.surfaceToTunnelLandingSupport;
|
||||
}
|
||||
const auto expectedPassageShellAt=[&](double xMetres,double yMetres,
|
||||
double zMetres)
|
||||
{
|
||||
@@ -4639,7 +4718,17 @@ namespace StandaloneStreamBenchmark
|
||||
const double floorBlend=std::clamp(floorDistance/0.25,0.0,1.0);
|
||||
const double smoothFloorBlend=floorBlend*floorBlend
|
||||
*(3.0-2.0*floorBlend);
|
||||
const double thickness=0.75*smoothFade*smoothFloorBlend;
|
||||
double thickness=0.75*smoothFade*smoothFloorBlend;
|
||||
if(candidatePassage<surfaceLandingPassages.size()
|
||||
&&surfaceLandingPassages[candidatePassage])
|
||||
{
|
||||
const double landingFade=std::clamp(1.0-along/8.0,
|
||||
0.0,1.0);
|
||||
const double smoothLandingFade=landingFade*landingFade
|
||||
*(3.0-2.0*landingFade);
|
||||
thickness=std::max(thickness,
|
||||
0.50*smoothLandingFade*smoothFloorBlend);
|
||||
}
|
||||
const double shellDistance=std::max(-flatDistance,
|
||||
flatDistance-thickness);
|
||||
minimumShellDistance=std::min(minimumShellDistance,
|
||||
@@ -5137,11 +5226,9 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
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);
|
||||
// Passage mouths already encode the player center 1.13 m above
|
||||
// their floor, matching the route-point convention directly.
|
||||
return equalPosition(authored,mouth);
|
||||
};
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
|
||||
@@ -1580,10 +1580,13 @@ private:
|
||||
static_cast<unsigned long long>(D.islandSharedCacheConflictReplacements),
|
||||
static_cast<double>(D.islandSharedCacheLockWaitNanoseconds)/1000.0);
|
||||
if(D.passageComposerProfiled)
|
||||
std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu native_field_calls=%llu features=%llu route_candidates=%llu boundary_band_queries=%llu queries=%llu passage_iterations=%llu carve_segment_visits=%llu relief_field_calls=%llu sweep_segments=%llu sweep_pairs=%llu sweep_prepare_ms=%.3f sweep_outside_pairs=%llu sweep_shell_support_skips=%llu sweep_min_updates=%llu sweep_base_samples=%llu sweep_detail_samples=%llu sweep_grid_samples=%llu sweep_no_influence_samples=%llu sweep_lookups=%llu sweep_cache_hits=%llu sweep_base_hits=%llu sweep_outside_hits=%llu sweep_missing=%llu\n",
|
||||
std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu field_calls=%llu native_cave_samples=%llu room_candidates=%llu edge_candidates=%llu features=%llu route_candidates=%llu boundary_band_queries=%llu queries=%llu passage_iterations=%llu carve_segment_visits=%llu relief_field_calls=%llu sweep_segments=%llu sweep_pairs=%llu sweep_prepare_ms=%.3f sweep_outside_pairs=%llu sweep_shell_support_skips=%llu sweep_min_updates=%llu sweep_base_samples=%llu sweep_detail_samples=%llu sweep_grid_samples=%llu sweep_no_influence_samples=%llu sweep_lookups=%llu sweep_cache_hits=%llu sweep_base_hits=%llu sweep_outside_hits=%llu sweep_missing=%llu\n",
|
||||
Event.Key.Coord.X,Event.Key.Coord.Y,Event.Key.Coord.Z,Event.Key.Level,
|
||||
Event.WorkerMilliseconds,static_cast<unsigned long long>(Event.DensitySamples),
|
||||
static_cast<unsigned long long>(D.fieldCalls),
|
||||
static_cast<unsigned long long>(D.nativeCaveFieldSamples),
|
||||
static_cast<unsigned long long>(D.cavernRoomCandidates),
|
||||
static_cast<unsigned long long>(D.cavernEdgeCandidates),
|
||||
static_cast<unsigned long long>(D.passageTileFeatureCount),
|
||||
static_cast<unsigned long long>(D.passageTileRouteSegmentCandidates),
|
||||
static_cast<unsigned long long>(D.resolvedBoundaryBandQueries),
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -283,6 +285,330 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
}
|
||||
}
|
||||
}
|
||||
std::uint64_t terrainPathCount=0,terrainGridSamples=0;
|
||||
std::int32_t terrainGridStepQ=0;
|
||||
const auto appendTerrainFallback=[&](std::int32_t stepQ)
|
||||
{
|
||||
if(!fallbackSearch
|
||||
||!((arrivalRadiusSubvoxels>0)!=(departureRadiusSubvoxels>0)))
|
||||
return;
|
||||
{
|
||||
// A bounded one-mouth world can have no walkable straight chord even
|
||||
// when a contour route exists. Link real ground columns only when their
|
||||
// grade meets the same limit, then use the ordinary capsule and support
|
||||
// sweep to accept or reject each resulting path.
|
||||
struct FGroundNode
|
||||
{
|
||||
std::int64_t ground=0;
|
||||
bool supportReachable=false;
|
||||
};
|
||||
struct FQueueEntry
|
||||
{
|
||||
std::uint64_t cost=0;
|
||||
std::size_t index=0;
|
||||
bool operator>(const FQueueEntry& other) const
|
||||
{return cost>other.cost||(cost==other.cost&&index>other.index);}
|
||||
};
|
||||
constexpr std::array<std::array<std::int64_t,2>,5> supportOffsets{{
|
||||
{{0,0}},{{ProbeRadiusQ,0}},{{-ProbeRadiusQ,0}},
|
||||
{{0,ProbeRadiusQ}},{{0,-ProbeRadiusQ}}}};
|
||||
constexpr std::int64_t SupportStepQ=WorldAssemblySubvoxelsPerVoxel/4;
|
||||
const std::int64_t routeBaseLiftQ=constraints.minimumClearanceSubvoxels/4;
|
||||
const auto hasRouteSupportAt=[&](std::int64_t centerX,std::int64_t centerY,
|
||||
std::int64_t footZ)
|
||||
{
|
||||
std::uint32_t reachable=0;
|
||||
for(const auto& offset:supportOffsets)
|
||||
{
|
||||
for(std::int64_t depth=SupportStepQ;depth<=SupportDepthQ;
|
||||
depth+=SupportStepQ)
|
||||
{
|
||||
if(finalField.point(centerX+offset[0],centerY+offset[1],
|
||||
footZ-depth).densitySubvoxels<0)
|
||||
{++reachable;break;}
|
||||
}
|
||||
}
|
||||
return reachable>=constraints.minimumSupportProbes;
|
||||
};
|
||||
const auto tryTerrainPaths=[&](std::int32_t terrainStepQ)
|
||||
{
|
||||
terrainPathCount=0;terrainGridSamples=0;terrainGridStepQ=0;
|
||||
const std::int64_t boundedRadius=std::max(arrivalRadiusSubvoxels,
|
||||
departureRadiusSubvoxels);
|
||||
const std::int32_t boundRadiusVoxels=static_cast<std::int32_t>(
|
||||
(boundedRadius+WorldAssemblySubvoxelsPerVoxel-1)
|
||||
/WorldAssemblySubvoxelsPerVoxel);
|
||||
const std::int32_t searchRadiusVoxels=std::clamp(
|
||||
boundRadiusVoxels+64,64,256);
|
||||
const std::int64_t searchRadiusQ=static_cast<std::int64_t>(
|
||||
searchRadiusVoxels)*WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int32_t radiusSteps=static_cast<std::int32_t>(
|
||||
searchRadiusQ/terrainStepQ);
|
||||
const std::int32_t dimension=radiusSteps*2+1;
|
||||
const std::size_t nodeCount=static_cast<std::size_t>(dimension)*dimension;
|
||||
const auto indexOf=[dimension](std::int32_t x,std::int32_t y)
|
||||
{return static_cast<std::size_t>(y)*dimension+x;};
|
||||
std::vector<FGroundNode> grid(nodeCount);
|
||||
const auto safeGroundAt=[&](std::int64_t centerX,std::int64_t centerY)
|
||||
{
|
||||
constexpr std::int64_t FootprintStepQ=WorldAssemblySubvoxelsPerVoxel/2;
|
||||
std::int64_t highestGround=std::numeric_limits<std::int64_t>::min();
|
||||
for(std::int64_t oy=-PlayerRadiusQ;oy<=PlayerRadiusQ;
|
||||
oy+=FootprintStepQ)
|
||||
for(std::int64_t ox=-PlayerRadiusQ;ox<=PlayerRadiusQ;
|
||||
ox+=FootprintStepQ)
|
||||
{
|
||||
if(ox*ox+oy*oy>PlayerRadiusQ*PlayerRadiusQ)continue;
|
||||
highestGround=std::max(highestGround,finalField.point(
|
||||
centerX+ox,centerY+oy,0).groundHeightSubvoxels);
|
||||
}
|
||||
return FGroundNode{highestGround,
|
||||
hasRouteSupportAt(centerX,centerY,
|
||||
highestGround+routeBaseLiftQ)};
|
||||
};
|
||||
for(std::int32_t gy=0;gy<dimension;++gy)
|
||||
for(std::int32_t gx=0;gx<dimension;++gx)
|
||||
{
|
||||
const std::int64_t xQ=static_cast<std::int64_t>(gx-radiusSteps)
|
||||
*terrainStepQ;
|
||||
const std::int64_t yQ=static_cast<std::int64_t>(gy-radiusSteps)
|
||||
*terrainStepQ;
|
||||
grid[indexOf(gx,gy)]=safeGroundAt(xQ,yQ);
|
||||
++terrainGridSamples;
|
||||
}
|
||||
for(std::size_t directionIndex=0;directionIndex<1;++directionIndex)
|
||||
{
|
||||
std::vector<std::uint64_t> costs(nodeCount,
|
||||
std::numeric_limits<std::uint64_t>::max());
|
||||
std::vector<std::size_t> parent(nodeCount,nodeCount);
|
||||
std::priority_queue<FQueueEntry,std::vector<FQueueEntry>,
|
||||
std::greater<FQueueEntry>> open;
|
||||
std::vector<std::int8_t> edgeSupportCache(nodeCount*4,-1);
|
||||
for(std::int32_t gy=0;gy<dimension;++gy)
|
||||
for(std::int32_t gx=0;gx<dimension;++gx)
|
||||
{
|
||||
if(gx!=0&&gy!=0&&gx!=dimension-1&&gy!=dimension-1)continue;
|
||||
const std::size_t start=indexOf(gx,gy);
|
||||
if(!grid[start].supportReachable)continue;
|
||||
costs[start]=0;
|
||||
open.push({0,start});
|
||||
}
|
||||
if(open.empty())continue;
|
||||
std::size_t goal=nodeCount;
|
||||
std::uint64_t goalCost=std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t goalDistanceSquared=std::numeric_limits<std::uint64_t>::max();
|
||||
while(!open.empty())
|
||||
{
|
||||
const FQueueEntry current=open.top();open.pop();
|
||||
if(current.cost!=costs[current.index])continue;
|
||||
if(goalCost!=std::numeric_limits<std::uint64_t>::max()
|
||||
&¤t.cost>goalCost)break;
|
||||
const std::int32_t gx=static_cast<std::int32_t>(current.index%dimension);
|
||||
const std::int32_t gy=static_cast<std::int32_t>(current.index/dimension);
|
||||
if(parent[current.index]!=nodeCount)
|
||||
{
|
||||
const std::int64_t xQ=static_cast<std::int64_t>(gx-radiusSteps)
|
||||
*terrainStepQ;
|
||||
const std::int64_t yQ=static_cast<std::int64_t>(gy-radiusSteps)
|
||||
*terrainStepQ;
|
||||
const std::uint64_t distanceSquared=static_cast<std::uint64_t>(
|
||||
xQ*xQ+yQ*yQ);
|
||||
if(distanceSquared<=static_cast<std::uint64_t>(
|
||||
boundedRadius*boundedRadius)
|
||||
&&(current.cost<goalCost||(current.cost==goalCost
|
||||
&&distanceSquared<goalDistanceSquared)))
|
||||
{goal=current.index;goalCost=current.cost;
|
||||
goalDistanceSquared=distanceSquared;}
|
||||
}
|
||||
for(std::int32_t oy=-1;oy<=1;++oy)
|
||||
for(std::int32_t ox=-1;ox<=1;++ox)
|
||||
{
|
||||
if(ox==0&&oy==0)continue;
|
||||
const std::int32_t nx=gx+ox,ny=gy+oy;
|
||||
if(nx<0||ny<0||nx>=dimension||ny>=dimension)continue;
|
||||
const std::size_t next=indexOf(nx,ny);
|
||||
if(!grid[next].supportReachable)continue;
|
||||
const std::int64_t horizontalQ=static_cast<std::int64_t>(
|
||||
std::llround(terrainStepQ*std::hypot(static_cast<double>(ox),
|
||||
static_cast<double>(oy))));
|
||||
const std::int64_t riseQ=std::abs(grid[next].ground
|
||||
-grid[current.index].ground);
|
||||
if(horizontalQ<=0||riseQ*65536>horizontalQ*GradeQ16)continue;
|
||||
const std::int32_t edgeDx=nx-gx,edgeDy=ny-gy;
|
||||
std::int32_t baseX=gx,baseY=gy,edgeDirection=0;
|
||||
if(edgeDx==0)
|
||||
{baseY=std::min(gy,ny);edgeDirection=1;}
|
||||
else if(edgeDy==0)
|
||||
{baseX=std::min(gx,nx);edgeDirection=0;}
|
||||
else if(edgeDx==edgeDy)
|
||||
{baseX=std::min(gx,nx);baseY=std::min(gy,ny);edgeDirection=2;}
|
||||
else
|
||||
{baseX=std::max(gx,nx);baseY=std::min(gy,ny);edgeDirection=3;}
|
||||
const std::size_t edgeCacheIndex=indexOf(baseX,baseY)*4
|
||||
+static_cast<std::size_t>(edgeDirection);
|
||||
if(edgeSupportCache[edgeCacheIndex]<0)
|
||||
{
|
||||
const std::uint64_t intervals=std::max<std::uint64_t>(2,
|
||||
(static_cast<std::uint64_t>(horizontalQ)
|
||||
+WorldAssemblySubvoxelsPerVoxel/8-1)
|
||||
/(WorldAssemblySubvoxelsPerVoxel/8));
|
||||
bool supported=true;
|
||||
const std::int64_t fromX=static_cast<std::int64_t>(
|
||||
gx-radiusSteps)*terrainStepQ;
|
||||
const std::int64_t fromY=static_cast<std::int64_t>(
|
||||
gy-radiusSteps)*terrainStepQ;
|
||||
const std::int64_t toX=static_cast<std::int64_t>(
|
||||
nx-radiusSteps)*terrainStepQ;
|
||||
const std::int64_t toY=static_cast<std::int64_t>(
|
||||
ny-radiusSteps)*terrainStepQ;
|
||||
for(std::uint64_t sample=1;sample<intervals;++sample)
|
||||
{
|
||||
const std::int64_t x=fromX+(toX-fromX)
|
||||
*static_cast<std::int64_t>(sample)
|
||||
/static_cast<std::int64_t>(intervals);
|
||||
const std::int64_t y=fromY+(toY-fromY)
|
||||
*static_cast<std::int64_t>(sample)
|
||||
/static_cast<std::int64_t>(intervals);
|
||||
const std::int64_t footZ=grid[current.index].ground
|
||||
+(grid[next].ground-grid[current.index].ground)
|
||||
*static_cast<std::int64_t>(sample)
|
||||
/static_cast<std::int64_t>(intervals)
|
||||
+routeBaseLiftQ;
|
||||
if(!hasRouteSupportAt(x,y,footZ))
|
||||
{supported=false;break;}
|
||||
}
|
||||
edgeSupportCache[edgeCacheIndex]=supported?1:0;
|
||||
}
|
||||
if(edgeSupportCache[edgeCacheIndex]==0)continue;
|
||||
const std::uint64_t edgeCost=static_cast<std::uint64_t>(
|
||||
horizontalQ+riseQ*4);
|
||||
if(current.cost>std::numeric_limits<std::uint64_t>::max()-edgeCost)
|
||||
continue;
|
||||
const std::uint64_t nextCost=current.cost+edgeCost;
|
||||
if(nextCost>=costs[next])continue;
|
||||
costs[next]=nextCost;parent[next]=current.index;
|
||||
open.push({nextCost,next});
|
||||
}
|
||||
}
|
||||
if(goal==nodeCount)continue;
|
||||
std::vector<std::size_t> path;
|
||||
for(std::size_t cursor=goal;cursor!=nodeCount;cursor=parent[cursor])
|
||||
{
|
||||
path.push_back(cursor);
|
||||
if(parent[cursor]==nodeCount)break;
|
||||
if(path.size()>nodeCount)break;
|
||||
}
|
||||
if(path.size()<2||parent[path.back()]!=nodeCount)continue;
|
||||
std::reverse(path.begin(),path.end());
|
||||
if(arrivalRadiusSubvoxels>0)
|
||||
std::reverse(path.begin(),path.end());
|
||||
Candidate candidate;
|
||||
candidate.centerX=static_cast<std::int32_t>(
|
||||
(static_cast<std::int32_t>(path.front()%dimension)-radiusSteps)
|
||||
*terrainStepQ);
|
||||
candidate.centerY=static_cast<std::int32_t>(
|
||||
(static_cast<std::int32_t>(path.front()/dimension)-radiusSteps)
|
||||
*terrainStepQ);
|
||||
candidate.direction=static_cast<std::int32_t>(directionIndex);
|
||||
candidate.score=static_cast<std::int64_t>(goalCost/path.size());
|
||||
candidate.endpointProximityCost=goalDistanceSquared
|
||||
*(arrivalRadiusSubvoxels>0?16ull:1ull);
|
||||
candidate.points.reserve(path.size());
|
||||
bool gradePass=true;
|
||||
for(std::size_t pointIndex=0;pointIndex<path.size();++pointIndex)
|
||||
{
|
||||
const std::size_t index=path[pointIndex];
|
||||
const std::int32_t gx=static_cast<std::int32_t>(index%dimension);
|
||||
const std::int32_t gy=static_cast<std::int32_t>(index/dimension);
|
||||
const std::int64_t xQ=static_cast<std::int64_t>(gx-radiusSteps)
|
||||
*terrainStepQ;
|
||||
const std::int64_t yQ=static_cast<std::int64_t>(gy-radiusSteps)
|
||||
*terrainStepQ;
|
||||
WorldAssemblyRoutePointV1 point;
|
||||
point.positionSubvoxels={xQ,yQ,
|
||||
grid[index].ground+routeBaseLiftQ+HalfHeightQ};
|
||||
point.radiusSubvoxels=RouteRadiusQ;
|
||||
point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed,
|
||||
xQ,yQ,
|
||||
0x52550000u+static_cast<std::uint32_t>(pointIndex));
|
||||
if(pointIndex>0)
|
||||
{
|
||||
const auto& previous=candidate.points.back().positionSubvoxels;
|
||||
const std::int64_t dx=point.positionSubvoxels[0]-previous[0];
|
||||
const std::int64_t dy=point.positionSubvoxels[1]-previous[1];
|
||||
const std::int64_t dz=std::abs(point.positionSubvoxels[2]-previous[2]);
|
||||
const std::int64_t run=static_cast<std::int64_t>(std::llround(
|
||||
std::hypot(static_cast<double>(dx),static_cast<double>(dy))));
|
||||
if(run<=0||dz*65536>run*GradeQ16)gradePass=false;
|
||||
}
|
||||
candidate.points.push_back(point);
|
||||
}
|
||||
if(!gradePass)continue;
|
||||
++terrainPathCount;
|
||||
const std::array<std::array<std::int64_t,2>,8> lateralNudges{{
|
||||
{{-ProbeRadiusQ,-ProbeRadiusQ}},{{-ProbeRadiusQ,0}},
|
||||
{{0,-ProbeRadiusQ}},{{ProbeRadiusQ,-ProbeRadiusQ}},
|
||||
{{-ProbeRadiusQ,ProbeRadiusQ}},{{0,ProbeRadiusQ}},
|
||||
{{ProbeRadiusQ,0}},{{ProbeRadiusQ,ProbeRadiusQ}}}};
|
||||
for(std::size_t nudgeIndex=0;nudgeIndex<lateralNudges.size();++nudgeIndex)
|
||||
{
|
||||
Candidate shifted=candidate;
|
||||
shifted.direction=candidate.direction*9
|
||||
+static_cast<std::int32_t>(nudgeIndex+1);
|
||||
shifted.score+=static_cast<std::int64_t>(nudgeIndex+1);
|
||||
bool shiftedGradePass=true;
|
||||
for(std::size_t pointIndex=0;pointIndex<shifted.points.size();++pointIndex)
|
||||
{
|
||||
const std::int64_t tQ16=shifted.points.size()>1
|
||||
?static_cast<std::int64_t>(pointIndex)*65536
|
||||
/static_cast<std::int64_t>(shifted.points.size()-1):0;
|
||||
const std::int64_t t2Q16=tQ16*tQ16/65536;
|
||||
const std::int64_t oneMinusQ16=65536-tQ16;
|
||||
const std::int64_t oneMinus2Q16=
|
||||
oneMinusQ16*oneMinusQ16/65536;
|
||||
const std::int64_t weightQ16=std::min<std::int64_t>(65536,
|
||||
16*t2Q16*oneMinus2Q16/65536);
|
||||
auto& point=shifted.points[pointIndex];
|
||||
point.positionSubvoxels[0]+=lateralNudges[nudgeIndex][0]
|
||||
*weightQ16/65536;
|
||||
point.positionSubvoxels[1]+=lateralNudges[nudgeIndex][1]
|
||||
*weightQ16/65536;
|
||||
const auto ground=safeGroundAt(point.positionSubvoxels[0],
|
||||
point.positionSubvoxels[1]);
|
||||
if(!ground.supportReachable)shiftedGradePass=false;
|
||||
point.positionSubvoxels[2]=ground.ground+routeBaseLiftQ
|
||||
+HalfHeightQ;
|
||||
point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed,
|
||||
point.positionSubvoxels[0],point.positionSubvoxels[1],
|
||||
0x52550000u+static_cast<std::uint32_t>(
|
||||
(nudgeIndex+1)*0x10000u+pointIndex));
|
||||
if(pointIndex==0)continue;
|
||||
const auto& previous=shifted.points[pointIndex-1]
|
||||
.positionSubvoxels;
|
||||
const std::int64_t dx=point.positionSubvoxels[0]-previous[0];
|
||||
const std::int64_t dy=point.positionSubvoxels[1]-previous[1];
|
||||
const std::int64_t dz=std::abs(point.positionSubvoxels[2]-previous[2]);
|
||||
const std::int64_t run=static_cast<std::int64_t>(std::llround(
|
||||
std::hypot(static_cast<double>(dx),static_cast<double>(dy))));
|
||||
if(run<=0||dz*65536>run*GradeQ16)shiftedGradePass=false;
|
||||
}
|
||||
if(!shiftedGradePass)continue;
|
||||
candidates.push_back(std::move(shifted));
|
||||
++terrainPathCount;
|
||||
}
|
||||
candidates.push_back(std::move(candidate));
|
||||
}
|
||||
if(terrainPathCount>0)terrainGridStepQ=terrainStepQ;
|
||||
};
|
||||
tryTerrainPaths(stepQ);
|
||||
tested+=terrainPathCount;
|
||||
std::printf("[WorldAssemblerRouteTerrainSearch] grid_step_m=%.3f grid_samples=%llu candidate_paths=%llu grade_limit_deg=15.000 status=%s\n",
|
||||
terrainGridStepQ*coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<unsigned long long>(terrainGridSamples),
|
||||
static_cast<unsigned long long>(terrainPathCount),
|
||||
terrainPathCount>0?"FOUND":"EMPTY");
|
||||
}
|
||||
};
|
||||
if(gradeRejected>0||placementRejected>0)
|
||||
{
|
||||
const double metresPerSubvoxel=static_cast<double>(coreVoxelSizeMicrons)
|
||||
@@ -318,8 +644,33 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
});
|
||||
std::uint64_t sweepAttempt=0,repairAttempt=0;
|
||||
VoxelForgeCore::WorldAssemblyRouteAuditResult lastAudit{};
|
||||
for(auto& candidate:candidates)
|
||||
std::size_t candidateCursor=0;
|
||||
constexpr std::array<std::int32_t,4> TerrainFallbackSteps{{1024,512,256,128}};
|
||||
std::size_t terrainFallbackStep=0;
|
||||
for(;;)
|
||||
{
|
||||
if(candidateCursor>=candidates.size())
|
||||
{
|
||||
if(terrainFallbackStep<TerrainFallbackSteps.size()&&fallbackSearch
|
||||
&&((arrivalRadiusSubvoxels>0)!=(departureRadiusSubvoxels>0)))
|
||||
{
|
||||
const std::size_t firstTerrainCandidate=candidates.size();
|
||||
appendTerrainFallback(TerrainFallbackSteps[terrainFallbackStep++]);
|
||||
std::stable_sort(candidates.begin()+firstTerrainCandidate,
|
||||
candidates.end(),[](const Candidate& a,const Candidate& b)
|
||||
{
|
||||
if(a.endpointProximityCost!=b.endpointProximityCost)
|
||||
return a.endpointProximityCost<b.endpointProximityCost;
|
||||
if(a.score!=b.score)return a.score<b.score;
|
||||
if(a.centerY!=b.centerY)return a.centerY<b.centerY;
|
||||
if(a.centerX!=b.centerX)return a.centerX<b.centerX;
|
||||
return a.direction<b.direction;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
auto& candidate=candidates[candidateCursor++];
|
||||
stratum.route=candidate.points;
|
||||
std::int64_t liftQ=0;
|
||||
constexpr std::int64_t RepairStepQ=64;
|
||||
@@ -349,6 +700,42 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
static_cast<long long>(candidate.score));
|
||||
return true;
|
||||
}
|
||||
if(liftQ==0&&audit.blockedCapsuleSamples==0
|
||||
&&audit.unsupportedCenterSamples>0)
|
||||
{
|
||||
constexpr std::int64_t SupportAlignmentStepQ=16;
|
||||
const std::int64_t lowerLimit=std::max<std::int64_t>(0,
|
||||
audit.minimumClearanceSubvoxels
|
||||
-constraints.minimumClearanceSubvoxels);
|
||||
for(std::int64_t lowerQ=SupportAlignmentStepQ;
|
||||
lowerQ<=lowerLimit;lowerQ+=SupportAlignmentStepQ)
|
||||
{
|
||||
for(std::size_t pointIndex=0;pointIndex<stratum.route.size();++pointIndex)
|
||||
stratum.route[pointIndex].positionSubvoxels[2]
|
||||
=candidate.points[pointIndex].positionSubvoxels[2]-lowerQ;
|
||||
const auto loweredAudit=AuditHeightfieldRoute(candidateField);
|
||||
lastAudit=loweredAudit;++repairAttempt;
|
||||
if(loweredAudit.passed)
|
||||
{
|
||||
std::printf("[WorldAssemblerRouteRepaired] uniform_lower_q=%lld support_limit_q=%lld\n",
|
||||
static_cast<long long>(lowerQ),
|
||||
static_cast<long long>(lowerLimit));
|
||||
const double metresPerSubvoxel=coreVoxelMetres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
std::printf("[WorldAssemblerRouteAccepted] arrival_xy_m=(%.3f,%.3f) departure_xy_m=(%.3f,%.3f) endpoint_proximity_cost=%llu average_grade_q16=%lld\n",
|
||||
candidate.points.front().positionSubvoxels[0]*metresPerSubvoxel,
|
||||
candidate.points.front().positionSubvoxels[1]*metresPerSubvoxel,
|
||||
candidate.points.back().positionSubvoxels[0]*metresPerSubvoxel,
|
||||
candidate.points.back().positionSubvoxels[1]*metresPerSubvoxel,
|
||||
static_cast<unsigned long long>(candidate.endpointProximityCost),
|
||||
static_cast<long long>(candidate.score));
|
||||
return true;
|
||||
}
|
||||
if(loweredAudit.blockedCapsuleSamples>0
|
||||
||loweredAudit.minimumClearanceSubvoxels
|
||||
<constraints.minimumClearanceSubvoxels)break;
|
||||
}
|
||||
}
|
||||
if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break;
|
||||
const std::int64_t needed=std::max<std::int64_t>(1,
|
||||
constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels);
|
||||
@@ -358,6 +745,62 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
liftQ=nextLift;
|
||||
++repairAttempt;
|
||||
}
|
||||
if(lastAudit.unsupportedCenterSamples>0&&sweepAttempt<5)
|
||||
{
|
||||
const Heightfield2D diagnosticField(recipe,stratum);
|
||||
const std::int64_t sampleSpacing=std::min<std::int64_t>(
|
||||
constraints.sweepSpacingSubvoxels,
|
||||
WorldAssemblySubvoxelsPerVoxel/2);
|
||||
const std::int64_t supportSpacing=std::min<std::int64_t>(sampleSpacing,
|
||||
WorldAssemblySubvoxelsPerVoxel/4);
|
||||
constexpr std::array<std::array<std::int64_t,2>,5> supportOffsets{{
|
||||
{{0,0}},{{ProbeRadiusQ,0}},{{-ProbeRadiusQ,0}},
|
||||
{{0,ProbeRadiusQ}},{{0,-ProbeRadiusQ}}}};
|
||||
for(std::size_t segment=1;segment<stratum.route.size();++segment)
|
||||
{
|
||||
const auto& from=stratum.route[segment-1].positionSubvoxels;
|
||||
const auto& to=stratum.route[segment].positionSubvoxels;
|
||||
const std::int64_t dx=to[0]-from[0],dy=to[1]-from[1],dz=to[2]-from[2];
|
||||
const std::uint64_t length=std::max<std::uint64_t>(1,
|
||||
static_cast<std::uint64_t>(std::llround(std::sqrt(
|
||||
static_cast<double>(dx*dx+dy*dy+dz*dz)))));
|
||||
const std::uint64_t intervals=std::max<std::uint64_t>(1,
|
||||
(length+sampleSpacing-1)/sampleSpacing);
|
||||
for(std::uint64_t step=segment==1?0:1;step<=intervals;++step)
|
||||
{
|
||||
const auto interpolate=[&](std::size_t axis)
|
||||
{return from[axis]+(to[axis]-from[axis])
|
||||
*static_cast<std::int64_t>(step)/static_cast<std::int64_t>(intervals);};
|
||||
const std::int64_t x=interpolate(0),y=interpolate(1),z=interpolate(2);
|
||||
const std::int64_t footZ=z-PlayerHeightQ/2;
|
||||
std::array<std::int64_t,5> reachedDepth{};
|
||||
for(std::size_t probe=0;probe<supportOffsets.size();++probe)
|
||||
for(std::int64_t depth=supportSpacing;
|
||||
depth<=constraints.supportSearchDepthSubvoxels;
|
||||
depth+=supportSpacing)
|
||||
{
|
||||
if(diagnosticField.point(x+supportOffsets[probe][0],
|
||||
y+supportOffsets[probe][1],footZ-depth)
|
||||
.densitySubvoxels<0)
|
||||
{reachedDepth[probe]=depth;break;}
|
||||
}
|
||||
const auto supported=static_cast<std::uint32_t>(std::count_if(
|
||||
reachedDepth.begin(),reachedDepth.end(),[](auto depth){return depth>0;}));
|
||||
if(supported>=constraints.minimumSupportProbes)continue;
|
||||
const double metresPerSubvoxel=coreVoxelMetres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
std::printf("[WorldAssemblerRouteSupportSample] ordinal=%llu x_m=%.3f y_m=%.3f center_z_m=%.3f supported=%u/5 depths_q=(%lld,%lld,%lld,%lld,%lld)\n",
|
||||
static_cast<unsigned long long>(sweepAttempt),x*metresPerSubvoxel,
|
||||
y*metresPerSubvoxel,z*metresPerSubvoxel,supported,
|
||||
static_cast<long long>(reachedDepth[0]),
|
||||
static_cast<long long>(reachedDepth[1]),
|
||||
static_cast<long long>(reachedDepth[2]),
|
||||
static_cast<long long>(reachedDepth[3]),
|
||||
static_cast<long long>(reachedDepth[4]));
|
||||
segment=stratum.route.size();break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(sweepAttempt<3)
|
||||
std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n",
|
||||
static_cast<unsigned long long>(sweepAttempt),
|
||||
@@ -1183,6 +1626,7 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
static_cast<double>(micrometres)*WorldAssemblySubvoxelsPerVoxel
|
||||
/layout.coreVoxelSizeMicrometres));
|
||||
};
|
||||
constexpr std::int64_t PlayerCenterAboveFloorMicrometres=1130000;
|
||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition
|
||||
@@ -1192,10 +1636,24 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
[&](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(),
|
||||
const std::string routePrefix="tunnel-network/strate-"
|
||||
+std::to_string(slot.StrateIndex)+"/";
|
||||
const bool hasPlannedArrival=std::any_of(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& edge)
|
||||
{return edge.strateIndex==slot.StrateIndex
|
||||
&&edge.stableId==routePrefix+"route-edge-0";});
|
||||
const bool hasPlannedDeparture=std::any_of(layout.cavernEdges.begin(),
|
||||
layout.cavernEdges.end(),[&](const auto& edge)
|
||||
{return edge.strateIndex==slot.StrateIndex
|
||||
&&edge.stableId==routePrefix+"departure-edge";});
|
||||
if(!hasPlannedArrival||!hasPlannedDeparture)continue;
|
||||
// Route-point anchors also drive the passage-mouth floor blend. The
|
||||
// native cave planner replaces the legacy finite-strate route, so
|
||||
// retaining its old anchors would join the passage floor to a route
|
||||
// that is no longer present in the composed cave field.
|
||||
stratum->anchors.erase(std::remove_if(stratum->anchors.begin(),
|
||||
stratum->anchors.end(),[](const auto& anchor)
|
||||
{return anchor.kind=="route_point"&&anchor.values.size()>=3;})>=2;
|
||||
if(hasRoutePoints)continue;
|
||||
{return anchor.kind=="route_point";}),stratum->anchors.end());
|
||||
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||||
const ResolvedLayoutPassageV1* departure=nullptr;
|
||||
for(const auto& passage:layout.passages)
|
||||
@@ -1222,9 +1680,7 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
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)
|
||||
{
|
||||
@@ -1239,6 +1695,10 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
if(edgeIndex==0)
|
||||
{
|
||||
auto inner=edge->controlPointsMicrometres.front();
|
||||
// Keep the exact authored mouth anchor and use the planner
|
||||
// edge's floor height for the first cave-side walking point.
|
||||
// Passage composition uses these same two points to blend the
|
||||
// floor profile into the native cave route.
|
||||
inner[2]+=PlayerCenterAboveFloorMicrometres;
|
||||
const std::array<std::int64_t,3> point={
|
||||
toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]),
|
||||
@@ -1266,12 +1726,10 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user