Versioned capsule vein records (stable ID, resource ID, endpoints, radius, seeded irregularity) resolved into the snapshot only when requested, placed beside the route and away from passages, landings and landmarks (forced-passage control rejected). The bound field table gains materialAt (host + two resource IDs + Q16 weights) behind a per-tile BVH; mesh export v2 carries per-vertex materials; a measurement-only yield computes removed resource volume (0 on already-air space). With veins on, every density, mesh, payload and topology hash is unchanged; demo off: owner hashes identical to L47. fast/medium/full PASS. Research R18 (cavern-graph planner) and R19 (why FloatingIslands costs ~10x SurfaceWorld). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
1424 lines
86 KiB
C++
1424 lines
86 KiB
C++
#include "SourceStamp.h"
|
|
#include "OwnerWorldLoader.h"
|
|
#include "StandaloneCoreScheduler.h"
|
|
#include "StreamBenchmarkPipeline.h"
|
|
#include "WorldAssemblyPrototype.h"
|
|
#include "StandaloneTlsProbe.h"
|
|
#include "../Core/src/StandaloneSnapshotAdapter.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <tuple>
|
|
|
|
namespace
|
|
{
|
|
struct FOptions
|
|
{
|
|
std::filesystem::path DataPath;
|
|
int WorkerCount=8;
|
|
int ClipRadius=3;
|
|
int MaxClipLevel=8;
|
|
int MaxLevel=-1;
|
|
int FarHorizonLOD8InteriorStepVoxels=0;
|
|
bool WorldAssemblerPrototype=false;
|
|
bool StreamBenchmark=false;
|
|
bool StreamPipeline=false;
|
|
bool StreamHash=false;
|
|
bool StreamHashTiles=false;
|
|
bool ProfileColumnOverlap=false;
|
|
bool UseSharedColumnCache=true;
|
|
bool ForceGenerate=false;
|
|
bool MeshCoverage=false;
|
|
bool ScreenSpaceGate=false;
|
|
bool AuditUniformBlockProofs=false;
|
|
bool ProfileLodWork=false;
|
|
bool AuditBasePageEquivalence=false;
|
|
bool AuditRouteParity=false;
|
|
bool RegionCrossing=false;
|
|
bool AuditTileShapeReadiness=false;
|
|
int DiagnosticSlowShapeCompileMs=0;
|
|
bool HasSeedOverride=false;
|
|
int SeedOverride=0;
|
|
bool HasRenderDistanceOverride=false;
|
|
int RenderDistanceOverride=0;
|
|
bool FloatingIslandsRecipe=false;
|
|
bool LandmarkChamberDemo=false;
|
|
bool LandmarkL45RampControl=false;
|
|
bool OreVeinsDemo=false;
|
|
bool AuditOreVeinYield=false;
|
|
bool AuditLandmarkReachability=false;
|
|
bool TlsProbeRequested=false;
|
|
int TlsProbeWorkers=0;
|
|
std::string TlsProbeTouch="none";
|
|
bool HasCamera=false;
|
|
bool HasCameraStep=false;
|
|
bool AutoSurfaceCamera=false;
|
|
FVector CameraMeters=FVector::ZeroVector;
|
|
FVector CameraStepMeters=FVector::ZeroVector;
|
|
float CameraYawDegrees=0.0f;
|
|
float CameraPitchDegrees=0.0f;
|
|
std::filesystem::path MeshExportPath;
|
|
std::filesystem::path ResolvedLayoutOutputPath;
|
|
bool ResolvedLayoutOnly=false;
|
|
bool AuditResolvedLayoutBoundaries=false;
|
|
bool AuditResolvedLayoutPassages=false;
|
|
bool AuditSurfaceClearance=false;
|
|
bool AuditStrateTransitions=false;
|
|
int TransitionModeOverride=-1;
|
|
int TransitionWidthChunksOverride=-1;
|
|
};
|
|
|
|
bool ParseInteger(std::string_view text,int& output)
|
|
{
|
|
if(text.empty())return false;
|
|
char* end=nullptr;
|
|
const std::string value(text);
|
|
const long parsed=std::strtol(value.c_str(),&end,10);
|
|
if(end==value.c_str()||*end!='\0'||parsed<INT32_MIN||parsed>INT32_MAX)return false;
|
|
output=static_cast<int>(parsed);
|
|
return true;
|
|
}
|
|
|
|
bool ParseCamera(std::string_view text,FVector& meters,float& yawDegrees,float& pitchDegrees)
|
|
{
|
|
const std::string values(text);
|
|
float x=0.0f,y=0.0f,z=0.0f,yaw=0.0f,pitch=0.0f;
|
|
if(std::sscanf(values.c_str(),"%f,%f,%f,%f,%f",&x,&y,&z,&yaw,&pitch)!=5)
|
|
return false;
|
|
meters={x,y,z};
|
|
yawDegrees=yaw;
|
|
pitchDegrees=pitch;
|
|
return true;
|
|
}
|
|
|
|
bool ParseCameraStep(std::string_view text,FVector& meters)
|
|
{
|
|
const std::string values(text);
|
|
float x=0.0f,y=0.0f,z=0.0f;
|
|
if(std::sscanf(values.c_str(),"%f,%f,%f",&x,&y,&z)!=3)return false;
|
|
meters={x,y,z};
|
|
return true;
|
|
}
|
|
|
|
bool MakeLandmarkChamberDemoRequests(
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
|
const VoxelForgeCore::WorldAssemblyManifest& worldAssembly,
|
|
std::int32_t seed,
|
|
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1>& cavernRooms,
|
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1>& cavernEdges,
|
|
bool legacyRampControl,
|
|
std::string& error)
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
requests.clear();
|
|
cavernRooms.clear();cavernEdges.clear();
|
|
const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(),
|
|
[](const auto& value){return value.generatorKind=="SurfaceWorld";});
|
|
if(surface==layout.strates.end())
|
|
{error="landmark chamber demo needs a SurfaceWorld strate";return false;}
|
|
const auto passage=std::find_if(layout.passages.begin(),layout.passages.end(),
|
|
[&](const auto& value){return value.lowerStrateIndex==surface->strateIndex
|
|
&&value.controlPoints.size()>=2;});
|
|
if(passage==layout.passages.end())
|
|
{error="landmark chamber demo needs a passage arrival mouth in SurfaceWorld";return false;}
|
|
const std::size_t mouthIndex=std::min(passage->lowerMouthControlIndex,
|
|
passage->controlPoints.size()-1);
|
|
const auto& mouth=passage->controlPoints[mouthIndex].positionSubvoxels;
|
|
const std::size_t priorIndex=mouthIndex>0?mouthIndex-1:mouthIndex;
|
|
const auto& prior=passage->controlPoints[priorIndex].positionSubvoxels;
|
|
const double dx=static_cast<double>(mouth[0])-prior[0];
|
|
const double dy=static_cast<double>(mouth[1])-prior[1];
|
|
const double length=std::hypot(dx,dy);
|
|
if(!(length>0.0))
|
|
{error="landmark chamber demo passage arrival has no horizontal tangent";return false;}
|
|
const double sideX=-dy/length,sideY=dx/length;
|
|
const double micrometresPerSubvoxel=static_cast<double>(layout.coreVoxelSizeMicrometres)
|
|
/WorldAssemblySubvoxelsPerVoxel;
|
|
std::array<std::int64_t,3> arrivalMicrometres{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
arrivalMicrometres[axis]=static_cast<std::int64_t>(std::llround(
|
|
mouth[axis]*micrometresPerSubvoxel));
|
|
ResolvedLayoutStampRequestV1 chamber;
|
|
chamber.stableId="landmark/sw-arrival-chamber-v1";
|
|
chamber.strateIndex=surface->strateIndex;
|
|
chamber.kind=ResolvedSdfStampKindV1::carveEllipsoid;
|
|
// Anchor the chamber horizontally beside the arrival passage. Its height is
|
|
// set from the local SurfaceWorld ground sample below so the carve keeps a
|
|
// real rock roof instead of breaking through a low patch of terrain.
|
|
chamber.anchorPositionMicrometres={
|
|
arrivalMicrometres[0]+static_cast<std::int64_t>(std::llround(sideX*28000000.0)),
|
|
arrivalMicrometres[1]+static_cast<std::int64_t>(std::llround(sideY*28000000.0)),
|
|
arrivalMicrometres[2]-13000000};
|
|
chamber.placementRadiusMicrometres=0;
|
|
chamber.halfExtentsMicrometres={8500000,7500000,3000000};
|
|
chamber.smoothWidthMicrometres=600000;
|
|
chamber.irregularityAmplitudeMicrometres=600000;
|
|
chamber.irregularityWavelengthMicrometres=3800000;
|
|
|
|
ResolvedLayoutStampRequestV1 dais;
|
|
// Resolved stamps compose in stable-ID order. Keep the raised fills after
|
|
// chamber-v1 so that the enclosing carve cannot cut them away.
|
|
dais.stableId="landmark/sw-arrival-chamber-v1-dais";
|
|
dais.strateIndex=surface->strateIndex;
|
|
// The version-2 ellipsoid fill rounds the platform into the chamber floor
|
|
// instead of leaving the long, straight wall of a flat-topped pillar.
|
|
dais.kind=ResolvedSdfStampKindV1::fillDais;
|
|
dais.anchorPositionMicrometres=chamber.anchorPositionMicrometres;
|
|
// Keep the raised platform on the broad filled floor rather than merging
|
|
// it into the chamber's outer slope. Leave a capsule-width landing around
|
|
// the ramp's centerline at the chamber entrance.
|
|
dais.anchorPositionMicrometres[0]+=static_cast<std::int64_t>(std::llround(sideX*2600000.0));
|
|
dais.anchorPositionMicrometres[1]+=static_cast<std::int64_t>(std::llround(sideY*2600000.0));
|
|
dais.anchorPositionMicrometres[2]-=400000;
|
|
dais.placementRadiusMicrometres=0;
|
|
dais.halfExtentsMicrometres={2100000,1800000,1300000};
|
|
dais.smoothWidthMicrometres=450000;
|
|
dais.irregularityAmplitudeMicrometres=180000;
|
|
dais.irregularityWavelengthMicrometres=1800000;
|
|
|
|
ResolvedLayoutStampRequestV1 chamberFloor;
|
|
chamberFloor.stableId="landmark/sw-arrival-chamber-v1-floor";
|
|
chamberFloor.strateIndex=surface->strateIndex;
|
|
chamberFloor.kind=ResolvedSdfStampKindV1::fillDais;
|
|
chamberFloor.anchorPositionMicrometres=chamber.anchorPositionMicrometres;
|
|
chamberFloor.anchorPositionMicrometres[2]-=2500000;
|
|
chamberFloor.placementRadiusMicrometres=0;
|
|
chamberFloor.halfExtentsMicrometres={5800000,5200000,2000000};
|
|
chamberFloor.smoothWidthMicrometres=250000;
|
|
chamberFloor.irregularityAmplitudeMicrometres=80000;
|
|
chamberFloor.irregularityWavelengthMicrometres=2200000;
|
|
|
|
const auto surfacePlan=std::find_if(worldAssembly.plan.strates.begin(),
|
|
worldAssembly.plan.strates.end(),[&](const auto& value)
|
|
{return value.strateIndex==surface->strateIndex;});
|
|
if(surfacePlan==worldAssembly.plan.strates.end())
|
|
{error="landmark chamber demo needs its SurfaceWorld heightfield plan";return false;}
|
|
const Heightfield2D field(worldAssembly.recipe,*surfacePlan);
|
|
if(!field.isValid())
|
|
{error="landmark chamber demo SurfaceWorld heightfield is invalid";return false;}
|
|
|
|
const double coreMicrometres=std::max<std::uint32_t>(1,layout.coreVoxelSizeMicrometres);
|
|
const double qToMicrometres=coreMicrometres/WorldAssemblySubvoxelsPerVoxel;
|
|
const auto qToMetres=[&](std::int64_t value)
|
|
{return static_cast<double>(value)*qToMicrometres/1000000.0;};
|
|
const auto metresToQ=[&](double value)
|
|
{return static_cast<std::int64_t>(std::llround(value*1000000.0/qToMicrometres));};
|
|
const double startX=qToMetres(mouth[0])+sideX*20.0;
|
|
const double startY=qToMetres(mouth[1])+sideY*20.0;
|
|
const auto surfaceSample=field.point(metresToQ(startX),metresToQ(startY),0);
|
|
const std::int64_t surfaceHeightQ=surfaceSample.groundHeightSubvoxels;
|
|
const double surfaceHeight=qToMetres(surfaceHeightQ);
|
|
|
|
// Search beside the arrival corridor for a chamber site whose actual terrain
|
|
// height lets the player descend from this surface entrance at a walkable grade.
|
|
// The earlier fixed XY site lay over a deep valley, leaving no room for a roof
|
|
// and asking the ramp to drop 37 m in 71 m.
|
|
double chamberSideOffset=0.0,chamberTangentOffset=0.0;
|
|
double chamberGroundHeight=0.0,bestChamberSiteScore=std::numeric_limits<double>::infinity();
|
|
for(double sideOffset=28.0;sideOffset<=52.0;sideOffset+=2.0)
|
|
for(double tangentOffset=-32.0;tangentOffset<=32.0;tangentOffset+=2.0)
|
|
{
|
|
const double x=qToMetres(mouth[0])+sideX*sideOffset+dx/length*tangentOffset;
|
|
const double y=qToMetres(mouth[1])+sideY*sideOffset+dy/length*tangentOffset;
|
|
const double distanceFromEntrance=std::hypot(x-startX,y-startY);
|
|
if(distanceFromEntrance<16.0)continue;
|
|
const auto sample=field.point(metresToQ(x),metresToQ(y),0);
|
|
const double ground=qToMetres(sample.groundHeightSubvoxels);
|
|
if(ground<surfaceHeight-3.5||ground>surfaceHeight+4.0)continue;
|
|
const double sideDelta=sideOffset-42.0;
|
|
const double tangentDelta=tangentOffset-2.0;
|
|
const double score=sideDelta*sideDelta+tangentDelta*tangentDelta
|
|
+4.0*(ground-surfaceHeight)*(ground-surfaceHeight);
|
|
if(score>=bestChamberSiteScore)continue;
|
|
bestChamberSiteScore=score;chamberSideOffset=sideOffset;
|
|
chamberTangentOffset=tangentOffset;chamberGroundHeight=ground;
|
|
chamber.anchorPositionMicrometres[0]=static_cast<std::int64_t>(
|
|
std::llround(x*1000000.0));
|
|
chamber.anchorPositionMicrometres[1]=static_cast<std::int64_t>(
|
|
std::llround(y*1000000.0));
|
|
}
|
|
if(!std::isfinite(bestChamberSiteScore))
|
|
{error="landmark chamber has no nearby surface site with a walkable descent grade";return false;}
|
|
constexpr double ChamberRoofCoverMetres=0.5;
|
|
const double chamberCarveTopReach=
|
|
static_cast<double>(chamber.halfExtentsMicrometres[2]
|
|
+chamber.smoothWidthMicrometres
|
|
+chamber.irregularityAmplitudeMicrometres)/1000000.0;
|
|
const double chamberCenterZ=chamberGroundHeight-chamberCarveTopReach
|
|
-ChamberRoofCoverMetres;
|
|
chamber.anchorPositionMicrometres[2]=static_cast<std::int64_t>(
|
|
std::llround(chamberCenterZ*1000000.0));
|
|
dais.anchorPositionMicrometres[0]=chamber.anchorPositionMicrometres[0]
|
|
+static_cast<std::int64_t>(std::llround(sideX*2600000.0));
|
|
dais.anchorPositionMicrometres[1]=chamber.anchorPositionMicrometres[1]
|
|
+static_cast<std::int64_t>(std::llround(sideY*2600000.0));
|
|
dais.anchorPositionMicrometres[2]=chamber.anchorPositionMicrometres[2]-400000;
|
|
chamberFloor.anchorPositionMicrometres[0]=chamber.anchorPositionMicrometres[0];
|
|
chamberFloor.anchorPositionMicrometres[1]=chamber.anchorPositionMicrometres[1];
|
|
chamberFloor.anchorPositionMicrometres[2]=chamber.anchorPositionMicrometres[2]-2500000;
|
|
|
|
struct FPathPoint { double x=0.0,y=0.0; };
|
|
const FPathPoint start{startX,startY};
|
|
const FPathPoint firstTurn{
|
|
qToMetres(mouth[0])+sideX*34000000.0/1000000.0+dx/length*14000000.0/1000000.0,
|
|
qToMetres(mouth[1])+sideY*34000000.0/1000000.0+dy/length*14000000.0/1000000.0};
|
|
const FPathPoint end{
|
|
chamber.anchorPositionMicrometres[0]/1000000.0,
|
|
chamber.anchorPositionMicrometres[1]/1000000.0};
|
|
const std::array<FPathPoint,3> waypoints={start,firstTurn,end};
|
|
std::vector<FPathPoint> densePath;
|
|
const auto appendLine=[&](FPathPoint from,FPathPoint to)
|
|
{
|
|
const double distance=std::hypot(to.x-from.x,to.y-from.y);
|
|
const int steps=std::max(1,static_cast<int>(std::ceil(distance/0.25)));
|
|
for(int i=0;i<=steps;++i)
|
|
{
|
|
const double t=static_cast<double>(i)/steps;
|
|
const FPathPoint point{from.x+(to.x-from.x)*t,from.y+(to.y-from.y)*t};
|
|
if(densePath.empty()||std::hypot(point.x-densePath.back().x,
|
|
point.y-densePath.back().y)>1.0e-6)densePath.push_back(point);
|
|
}
|
|
};
|
|
constexpr double BendRadius=6.0;
|
|
FPathPoint current=waypoints.front();
|
|
for(std::size_t i=1;i+1<waypoints.size();++i)
|
|
{
|
|
const auto& previous=waypoints[i-1];const auto& corner=waypoints[i];
|
|
const auto& next=waypoints[i+1];
|
|
const double inLength=std::hypot(corner.x-previous.x,corner.y-previous.y);
|
|
const double outLength=std::hypot(next.x-corner.x,next.y-corner.y);
|
|
if(inLength<=2.0*BendRadius||outLength<=2.0*BendRadius)
|
|
{error="landmark cave ramp has a segment too short to round";return false;}
|
|
const FPathPoint inDirection{(corner.x-previous.x)/inLength,
|
|
(corner.y-previous.y)/inLength};
|
|
const FPathPoint outDirection{(next.x-corner.x)/outLength,
|
|
(next.y-corner.y)/outLength};
|
|
const FPathPoint before{corner.x-inDirection.x*BendRadius,
|
|
corner.y-inDirection.y*BendRadius};
|
|
const FPathPoint after{corner.x+outDirection.x*BendRadius,
|
|
corner.y+outDirection.y*BendRadius};
|
|
appendLine(current,before);
|
|
constexpr int ArcSteps=24;
|
|
for(int step=0;step<=ArcSteps;++step)
|
|
{
|
|
const double t=static_cast<double>(step)/ArcSteps,oneMinus=1.0-t;
|
|
const FPathPoint point{
|
|
oneMinus*oneMinus*before.x+2.0*oneMinus*t*corner.x+t*t*after.x,
|
|
oneMinus*oneMinus*before.y+2.0*oneMinus*t*corner.y+t*t*after.y};
|
|
if(densePath.empty()||std::hypot(point.x-densePath.back().x,
|
|
point.y-densePath.back().y)>1.0e-6)densePath.push_back(point);
|
|
}
|
|
current=after;
|
|
}
|
|
appendLine(current,waypoints.back());
|
|
if(densePath.size()<2)
|
|
{error="landmark cave ramp path is degenerate";return false;}
|
|
std::vector<double> arcLength(densePath.size(),0.0);
|
|
for(std::size_t i=1;i<densePath.size();++i)
|
|
arcLength[i]=arcLength[i-1]+std::hypot(densePath[i].x-densePath[i-1].x,
|
|
densePath[i].y-densePath[i-1].y);
|
|
const double totalLength=arcLength.back();
|
|
double firstTurnDistance=0.0;
|
|
double firstTurnDistanceError=std::numeric_limits<double>::infinity();
|
|
for(std::size_t i=0;i<densePath.size();++i)
|
|
{
|
|
const double distance=std::hypot(densePath[i].x-firstTurn.x,
|
|
densePath[i].y-firstTurn.y);
|
|
if(distance<firstTurnDistanceError)
|
|
{firstTurnDistanceError=distance;firstTurnDistance=arcLength[i];}
|
|
}
|
|
// Put the first tunnel center one floor radius above grade so its lower boundary
|
|
// meets the outdoor ground cleanly, while keeping a walkable cave-sized headroom.
|
|
constexpr double RampRadiusMetres=1.6;
|
|
const double startZ=surfaceHeight+RampRadiusMetres;
|
|
// Join the chamber's filled floor at its top surface.
|
|
const double endZ=chamberCenterZ+1.1;
|
|
const double descent=endZ-startZ;
|
|
constexpr double EntryLandingLength=5.0;
|
|
constexpr double TurnLandingHalfLength=1.5;
|
|
const double firstTurnLandingStart=std::max(EntryLandingLength,
|
|
firstTurnDistance-TurnLandingHalfLength);
|
|
const double firstTurnLandingEnd=std::min(totalLength,
|
|
firstTurnDistance+TurnLandingHalfLength);
|
|
const double firstTurnLandingLength=firstTurnLandingEnd-firstTurnLandingStart;
|
|
const double slopedLength=std::max(0.0,totalLength-EntryLandingLength
|
|
-firstTurnLandingLength);
|
|
ResolvedLayoutStampRequestV1 mouthStamp;
|
|
mouthStamp.stableId="landmark/sw-arrival-chamber-mouth-v1";
|
|
mouthStamp.strateIndex=surface->strateIndex;
|
|
mouthStamp.kind=ResolvedSdfStampKindV1::carveEllipsoid;
|
|
mouthStamp.anchorPositionMicrometres={
|
|
static_cast<std::int64_t>(std::llround(startX*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(startY*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround((startZ+1.0)*1000000.0))};
|
|
mouthStamp.placementRadiusMicrometres=0;
|
|
mouthStamp.halfExtentsMicrometres={4200000,3700000,2800000};
|
|
mouthStamp.smoothWidthMicrometres=450000;
|
|
// A broad, rounded upper flare blends the side-facing portal into the slope.
|
|
mouthStamp.irregularityAmplitudeMicrometres=250000;
|
|
mouthStamp.irregularityWavelengthMicrometres=2400000;
|
|
std::printf("[LandmarkChamberPlan] center_m=(%.3f,%.3f,%.3f) local_ground_m=%.3f carve_top_reach_m=%.3f roof_cover_m=%.3f\n",
|
|
end.x,end.y,chamberCenterZ,chamberGroundHeight,chamberCarveTopReach,
|
|
chamberGroundHeight-(chamberCenterZ+chamberCarveTopReach));
|
|
std::printf("[LandmarkChamberSite] side_offset_m=%.3f tangent_offset_m=%.3f entrance_ground_m=%.3f chamber_ground_m=%.3f\n",
|
|
chamberSideOffset,chamberTangentOffset,surfaceHeight,chamberGroundHeight);
|
|
std::printf("[LandmarkRampPlan] entry_m=(%.3f,%.3f,%.3f) ground_m=%.3f cap_m=%.3f end_m=(%.3f,%.3f,%.3f) horizontal_m=%.3f entry_landing_m=%.3f turn_landing_m=%.3f turn_distance_m=%.3f sloped_m=%.3f descent_m=%.3f grade_deg=%.3f required_length_m=%.3f\n",
|
|
startX,startY,startZ,surfaceHeight,
|
|
qToMetres(surfaceSample.capHeightSubvoxels),end.x,end.y,endZ,totalLength,
|
|
EntryLandingLength,firstTurnLandingLength,firstTurnDistance,
|
|
slopedLength,descent,
|
|
std::atan2(std::abs(descent),slopedLength)*180.0/3.14159265358979323846,
|
|
std::abs(descent)/std::tan(15.0*3.14159265358979323846/180.0));
|
|
if(!(slopedLength>0.0)||std::atan2(std::abs(descent),slopedLength)
|
|
>15.0*3.14159265358979323846/180.0)
|
|
{error="landmark cave ramp cannot meet the 15 degree walk grade";return false;}
|
|
|
|
const double entryPathLength=std::hypot(firstTurn.x-start.x,
|
|
firstTurn.y-start.y);
|
|
const double entryPathX=(firstTurn.x-start.x)/entryPathLength;
|
|
const double entryPathY=(firstTurn.y-start.y)/entryPathLength;
|
|
const double mouthNormalX=-entryPathY,mouthNormalY=entryPathX;
|
|
constexpr double MouthPortalDepth=3.8;
|
|
constexpr double MouthPortalHalfWidth=2.8;
|
|
constexpr double MouthPortalCenterOffset=2.6;
|
|
const double mouthPortalX=startX+mouthNormalX*MouthPortalCenterOffset;
|
|
const double mouthPortalY=startY+mouthNormalY*MouthPortalCenterOffset;
|
|
ResolvedLayoutStampRequestV1 mouthPortal;
|
|
mouthPortal.stableId="landmark/sw-arrival-chamber-mouth-approach-v1";
|
|
mouthPortal.strateIndex=surface->strateIndex;
|
|
mouthPortal.kind=ResolvedSdfStampKindV1::carveEllipsoid;
|
|
mouthPortal.anchorPositionMicrometres={
|
|
static_cast<std::int64_t>(std::llround(mouthPortalX*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(mouthPortalY*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(startZ*1000000.0))};
|
|
mouthPortal.placementRadiusMicrometres=0;
|
|
mouthPortal.halfExtentsMicrometres={3800000,2800000,2000000};
|
|
mouthPortal.smoothWidthMicrometres=450000;
|
|
mouthPortal.irregularityAmplitudeMicrometres=180000;
|
|
mouthPortal.irregularityWavelengthMicrometres=2000000;
|
|
|
|
ResolvedLayoutStampRequestV1 mouthLanding;
|
|
mouthLanding.stableId="landmark/sw-arrival-z-mouth-landing-v1";
|
|
mouthLanding.strateIndex=surface->strateIndex;
|
|
mouthLanding.kind=ResolvedSdfStampKindV1::fillPillar;
|
|
mouthLanding.anchorPositionMicrometres={
|
|
static_cast<std::int64_t>(std::llround(mouthPortalX*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(mouthPortalY*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround((startZ-RampRadiusMetres-1.5)
|
|
*1000000.0))};
|
|
mouthLanding.placementRadiusMicrometres=0;
|
|
mouthLanding.halfExtentsMicrometres={3800000,2800000,1500000};
|
|
mouthLanding.smoothWidthMicrometres=180000;
|
|
mouthLanding.irregularityAmplitudeMicrometres=0;
|
|
mouthLanding.irregularityWavelengthMicrometres=2000000;
|
|
std::printf("[LandmarkMouthPortal] center_m=(%.3f,%.3f,%.3f) normal=(%.3f,%.3f) depth_m=%.2f half_width_m=%.2f landing_top_m=%.3f\n",
|
|
mouthPortalX,mouthPortalY,startZ,mouthNormalX,mouthNormalY,
|
|
MouthPortalDepth,MouthPortalHalfWidth,surfaceHeight);
|
|
|
|
const auto atDistance=[&](double distance)
|
|
{
|
|
distance=std::clamp(distance,0.0,totalLength);
|
|
const auto upper=std::lower_bound(arcLength.begin(),arcLength.end(),distance);
|
|
const std::size_t hi=std::min<std::size_t>(densePath.size()-1,
|
|
static_cast<std::size_t>(upper-arcLength.begin()));
|
|
const std::size_t lo=hi?hi-1:0;
|
|
const double span=arcLength[hi]-arcLength[lo];
|
|
const double t=span>0.0?(distance-arcLength[lo])/span:0.0;
|
|
return FPathPoint{densePath[lo].x+(densePath[hi].x-densePath[lo].x)*t,
|
|
densePath[lo].y+(densePath[hi].y-densePath[lo].y)*t};
|
|
};
|
|
const auto centerlineHeight=[&](double distance)
|
|
{
|
|
const double beforeFirstTurn=std::clamp(distance-EntryLandingLength,0.0,
|
|
firstTurnLandingStart-EntryLandingLength);
|
|
const double afterFirstTurn=distance>firstTurnLandingEnd
|
|
?std::clamp(distance-firstTurnLandingEnd,0.0,
|
|
totalLength-firstTurnLandingEnd):0.0;
|
|
return startZ+descent*std::clamp((beforeFirstTurn+afterFirstTurn)
|
|
/slopedLength,0.0,1.0);
|
|
};
|
|
// The resolved edge is one cubic sweep. Its floor controls are independent
|
|
// of the curved XY centerline, so the floor remains an explicit profile.
|
|
const double startFloor=surfaceHeight;
|
|
const double endFloor=chamberCenterZ-0.5;
|
|
const double firstLength=std::hypot(firstTurn.x-start.x,firstTurn.y-start.y);
|
|
const double lastLength=std::hypot(end.x-firstTurn.x,end.y-firstTurn.y);
|
|
const double estimatedLength=firstLength+lastLength;
|
|
const double handleLength=estimatedLength/3.0;
|
|
const double firstDirX=(firstTurn.x-start.x)/firstLength;
|
|
const double firstDirY=(firstTurn.y-start.y)/firstLength;
|
|
const double lastDirX=(end.x-firstTurn.x)/lastLength;
|
|
const double lastDirY=(end.y-firstTurn.y)/lastLength;
|
|
std::array<std::array<double,3>,4> curve={{
|
|
{start.x,start.y,startFloor},
|
|
{start.x+firstDirX*handleLength,start.y+firstDirY*handleLength,
|
|
startFloor+(endFloor-startFloor)/3.0},
|
|
{end.x-lastDirX*handleLength,end.y-lastDirY*handleLength,
|
|
startFloor+2.0*(endFloor-startFloor)/3.0},
|
|
{end.x,end.y,endFloor}}};
|
|
ResolvedCavernRoomV1 room;
|
|
room.stableId=chamber.stableId;room.strateIndex=surface->strateIndex;
|
|
room.centerMicrometres=chamber.anchorPositionMicrometres;
|
|
room.halfExtentsMicrometres=chamber.halfExtentsMicrometres;
|
|
room.floorMicrometres=static_cast<std::int64_t>(std::llround(endFloor*1000000.0));
|
|
room.smoothWidthMicrometres=chamber.smoothWidthMicrometres;
|
|
room.irregularityAmplitudeMicrometres=chamber.irregularityAmplitudeMicrometres;
|
|
room.irregularityWavelengthMicrometres=chamber.irregularityWavelengthMicrometres;
|
|
if(!legacyRampControl)cavernRooms.push_back(room);
|
|
ResolvedCavernEdgeV1 edge;
|
|
edge.stableId="landmark/sw-arrival-chamber-tunnel-v1";
|
|
edge.startAnchorStableId=passage->stableId+"/surface-ground-approach";
|
|
edge.endRoomStableId=room.stableId;edge.strateIndex=surface->strateIndex;
|
|
edge.boreRadiusMicrometres=2600000;
|
|
edge.boreHeightMicrometres=4800000;
|
|
edge.floorSupportDepthMicrometres=450000;
|
|
edge.smoothWidthMicrometres=300000;
|
|
edge.irregularityAmplitudeMicrometres=20000;
|
|
edge.irregularityWavelengthMicrometres=3200000;
|
|
double measuredCurveLength=0.0,arcAtThird=0.0,arcAtTwoThirds=0.0;
|
|
std::array<double,2> previousCurvePoint={curve[0][0],curve[0][1]};
|
|
for(int sample=1;sample<=256;++sample)
|
|
{
|
|
const double t=static_cast<double>(sample)/256.0,one=1.0-t;
|
|
std::array<double,3> point{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
point[axis]=one*one*one*curve[0][axis]
|
|
+3.0*one*one*t*curve[1][axis]+3.0*one*t*t*curve[2][axis]
|
|
+t*t*t*curve[3][axis];
|
|
const double dx=point[0]-previousCurvePoint[0];
|
|
const double dy=point[1]-previousCurvePoint[1];
|
|
const double horizontal=std::hypot(dx,dy);
|
|
measuredCurveLength+=horizontal;
|
|
if(sample==85)arcAtThird=measuredCurveLength;
|
|
if(sample==171)arcAtTwoThirds=measuredCurveLength;
|
|
previousCurvePoint={point[0],point[1]};
|
|
}
|
|
const double targetFloorThird=startFloor+(endFloor-startFloor)
|
|
*(arcAtThird/measuredCurveLength);
|
|
const double targetFloorTwoThirds=startFloor+(endFloor-startFloor)
|
|
*(arcAtTwoThirds/measuredCurveLength);
|
|
const double firstEquation=targetFloorThird
|
|
-(8.0*startFloor+endFloor)/27.0;
|
|
const double secondEquation=targetFloorTwoThirds
|
|
-(startFloor+8.0*endFloor)/27.0;
|
|
curve[1][2]=1.5*(2.0*firstEquation-secondEquation);
|
|
curve[2][2]=1.5*(2.0*secondEquation-firstEquation);
|
|
double maximumCurveGrade=0.0;
|
|
std::array<double,3> previousCurvePosition=curve[0];
|
|
for(int sample=1;sample<=256;++sample)
|
|
{
|
|
const double t=static_cast<double>(sample)/256.0,one=1.0-t;
|
|
std::array<double,3> point{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
point[axis]=one*one*one*curve[0][axis]
|
|
+3.0*one*one*t*curve[1][axis]+3.0*one*t*t*curve[2][axis]
|
|
+t*t*t*curve[3][axis];
|
|
const double horizontal=std::hypot(point[0]-previousCurvePosition[0],
|
|
point[1]-previousCurvePosition[1]);
|
|
maximumCurveGrade=std::max(maximumCurveGrade,std::atan2(
|
|
std::abs(point[2]-previousCurvePosition[2]),
|
|
std::max(1.0e-6,horizontal))*180.0/3.14159265358979323846);
|
|
previousCurvePosition=point;
|
|
}
|
|
for(std::size_t i=0;i<curve.size();++i)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
edge.controlPointsMicrometres[i][axis]=static_cast<std::int64_t>(
|
|
std::llround(curve[i][axis]*1000000.0));
|
|
if(!legacyRampControl)cavernEdges.push_back(edge);
|
|
std::printf("[CavernGraphPlan] rooms=1 edges=1 room=%s edge=%s curve=cubic-bezier floor=cubic-profile length_m=%.3f max_grade_deg=%.3f bore_radius_m=2.600 bore_height_m=4.800 floor_um=%lld..%lld\n",
|
|
room.stableId.c_str(),edge.stableId.c_str(),measuredCurveLength,maximumCurveGrade,
|
|
static_cast<long long>(room.floorMicrometres),
|
|
static_cast<long long>(edge.controlPointsMicrometres[0][2]));
|
|
if(maximumCurveGrade>15.0)
|
|
{error="landmark cavern edge exceeds the 15 degree walk grade";return false;}
|
|
const double pointSpacing=0.6;
|
|
const std::size_t rampCount=static_cast<std::size_t>(std::ceil(totalLength/pointSpacing));
|
|
std::vector<ResolvedLayoutStampRequestV1> rampCarves;
|
|
std::vector<ResolvedLayoutStampRequestV1> rampSupports;
|
|
rampCarves.reserve(rampCount+1);
|
|
// Footfall supports are denser and narrower than the tunnel stamps. This
|
|
// keeps their overlapping flat caps from forming broad downhill shelves.
|
|
const double supportSpacing=0.3;
|
|
const std::size_t supportCount=static_cast<std::size_t>(std::ceil(
|
|
totalLength/supportSpacing));
|
|
rampSupports.reserve(supportCount+1);
|
|
for(std::size_t i=0;i<=rampCount;++i)
|
|
{
|
|
const double distance=std::min(totalLength,static_cast<double>(i)*pointSpacing);
|
|
const FPathPoint point=atDistance(distance);
|
|
ResolvedLayoutStampRequestV1 ramp;
|
|
const std::size_t number=i;
|
|
ramp.stableId=std::string("landmark/sw-arrival-chamber-ramp-")+
|
|
(number<10?"00":number<100?"0":"")+std::to_string(number);
|
|
ramp.strateIndex=surface->strateIndex;
|
|
ramp.kind=ResolvedSdfStampKindV1::carveEllipsoid;
|
|
const double centerZ=centerlineHeight(distance);
|
|
ramp.anchorPositionMicrometres={
|
|
static_cast<std::int64_t>(std::llround(point.x*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(point.y*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(centerZ*1000000.0))};
|
|
ramp.placementRadiusMicrometres=0;
|
|
ramp.halfExtentsMicrometres={2600000,2600000,2000000};
|
|
ramp.smoothWidthMicrometres=650000;
|
|
// Keep the footfall surface smooth enough for the capsule grade limit;
|
|
// the chamber and entrance carry the larger natural wall variation.
|
|
ramp.irregularityAmplitudeMicrometres=250000;
|
|
ramp.irregularityWavelengthMicrometres=2400000;
|
|
rampCarves.push_back(std::move(ramp));
|
|
}
|
|
for(std::size_t i=0;i<=supportCount;++i)
|
|
{
|
|
const double distance=std::min(totalLength,static_cast<double>(i)*supportSpacing);
|
|
const FPathPoint point=atDistance(distance);
|
|
const double floorZ=centerlineHeight(distance)-RampRadiusMetres;
|
|
ResolvedLayoutStampRequestV1 support;
|
|
support.stableId=std::string("landmark/sw-arrival-ramp-support-")+
|
|
(i<10?"00":i<100?"0":"")+std::to_string(i);
|
|
support.strateIndex=surface->strateIndex;
|
|
support.kind=ResolvedSdfStampKindV1::fillPillar;
|
|
support.anchorPositionMicrometres={
|
|
static_cast<std::int64_t>(std::llround(point.x*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(point.y*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround((floorZ-1.5)*1000000.0))};
|
|
support.placementRadiusMicrometres=0;
|
|
// The first support widens into a buried entry landing under the open
|
|
// mouth, so the approach surface stays supported at its carved rim.
|
|
support.halfExtentsMicrometres=i==0
|
|
?std::array<std::int64_t,3>{3500000,3000000,1500000}
|
|
:std::array<std::int64_t,3>{550000,550000,1500000};
|
|
support.smoothWidthMicrometres=180000;
|
|
support.irregularityAmplitudeMicrometres=0;
|
|
support.irregularityWavelengthMicrometres=2200000;
|
|
rampSupports.push_back(std::move(support));
|
|
}
|
|
requests.push_back(std::move(mouthStamp));
|
|
requests.push_back(std::move(mouthPortal));
|
|
if(legacyRampControl)
|
|
{
|
|
requests.insert(requests.end(),std::make_move_iterator(rampCarves.begin()),
|
|
std::make_move_iterator(rampCarves.end()));
|
|
requests.push_back(std::move(chamber));
|
|
requests.insert(requests.end(),std::make_move_iterator(rampSupports.begin()),
|
|
std::make_move_iterator(rampSupports.end()));
|
|
}
|
|
if(legacyRampControl)requests.push_back(std::move(mouthLanding));
|
|
requests.push_back(std::move(chamberFloor));
|
|
requests.push_back(std::move(dais));
|
|
(void)seed;
|
|
return true;
|
|
}
|
|
|
|
bool MakeOreVeinDemoRequests(
|
|
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
|
std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1>& requests,
|
|
std::string& error)
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
requests.clear();
|
|
const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(),
|
|
[](const auto& value){return value.generatorKind=="SurfaceWorld";});
|
|
if(surface==layout.strates.end()||layout.coreVoxelSizeMicrometres==0)
|
|
{error="ore-vein demo needs a SurfaceWorld strate";return false;}
|
|
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
|
const double coreMicrometres=layout.coreVoxelSizeMicrometres;
|
|
const auto addCapsule=[&](std::string id,std::uint16_t resourceId,
|
|
std::array<double,3> base,std::array<double,3> tangent,
|
|
std::array<double,3> side,double routeClearRadius,
|
|
std::int64_t radius,std::int64_t irregularity,std::int64_t blend,
|
|
std::int64_t length)
|
|
{
|
|
const auto normalize=[](std::array<double,3>& value)
|
|
{
|
|
const double length=std::sqrt(value[0]*value[0]+value[1]*value[1]
|
|
+value[2]*value[2]);
|
|
if(length<=1.0e-9)return false;
|
|
for(double& component:value)component/=length;
|
|
return true;
|
|
};
|
|
if(!normalize(tangent))return false;
|
|
const double sideLength=std::sqrt(side[0]*side[0]+side[1]*side[1]);
|
|
if(sideLength<=1.0e-9)return false;
|
|
side[0]/=sideLength;side[1]/=sideLength;
|
|
const double offset=routeClearRadius+radius+irregularity+200000.0;
|
|
base[0]+=side[0]*offset;base[1]+=side[1]*offset;
|
|
ResolvedLayoutOreVeinRequestV1 request;
|
|
request.stableId=std::move(id);request.strateIndex=surface->strateIndex;
|
|
request.resourceMaterialId=resourceId;request.placementRadiusMicrometres=1500000;
|
|
request.radiusMicrometres=radius;
|
|
request.irregularityAmplitudeMicrometres=irregularity;
|
|
request.irregularityWavelengthMicrometres=1400000;
|
|
request.blendWidthMicrometres=blend;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double half=static_cast<double>(length)*0.5*tangent[axis];
|
|
request.anchorEndpointsMicrometres[0][axis]=static_cast<std::int64_t>(
|
|
std::llround(base[axis]-half));
|
|
request.anchorEndpointsMicrometres[1][axis]=static_cast<std::int64_t>(
|
|
std::llround(base[axis]+half));
|
|
}
|
|
requests.push_back(std::move(request));return true;
|
|
};
|
|
std::vector<const ResolvedLayoutAnchorV2*> route;
|
|
for(const auto& anchor:surface->anchors)
|
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)route.push_back(&anchor);
|
|
if(route.size()<12)
|
|
{error="ore-vein demo needs at least twelve resolved SurfaceWorld route points";return false;}
|
|
const auto routePoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
|
{
|
|
return std::array<double,3>{
|
|
anchor.values[0]*coreMicrometres/Q,
|
|
anchor.values[1]*coreMicrometres/Q,
|
|
anchor.values[2]*coreMicrometres/Q};
|
|
};
|
|
const auto routeRadius=[&](const ResolvedLayoutAnchorV2& anchor)
|
|
{
|
|
return anchor.values.size()>=4?anchor.values[3]*coreMicrometres/Q:0.0;
|
|
};
|
|
for(const auto [pointIndex,id,resourceId,sideSign]:{
|
|
std::tuple<std::size_t,const char*,std::uint16_t,double>{7,
|
|
"ore/sw-arrival-route-copper-v1",1,1.0},
|
|
std::tuple<std::size_t,const char*,std::uint16_t,double>{10,
|
|
"ore/sw-arrival-route-cobalt-v1",2,-1.0}})
|
|
{
|
|
const auto a=routePoint(*route[pointIndex]);
|
|
const auto b=routePoint(*route[pointIndex+1]);
|
|
std::array<double,3> tangent={b[0]-a[0],b[1]-a[1],b[2]-a[2]};
|
|
std::array<double,3> side={-tangent[1]*sideSign,tangent[0]*sideSign,0.0};
|
|
std::array<double,3> center={(a[0]+b[0])*0.5,(a[1]+b[1])*0.5,
|
|
(a[2]+b[2])*0.5};
|
|
const double clearance=std::max(routeRadius(*route[pointIndex]),
|
|
routeRadius(*route[pointIndex+1]));
|
|
if(!addCapsule(id,resourceId,center,tangent,side,clearance,
|
|
resourceId==1?650000:750000,100000,600000,2500000))
|
|
{error="ore-vein route capsule has a degenerate tangent";return false;}
|
|
}
|
|
const auto edge=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
|
[&](const auto& value){return value.strateIndex==surface->strateIndex;});
|
|
if(edge!=layout.cavernEdges.end())
|
|
{
|
|
constexpr double t=0.12,one=1.0-t;
|
|
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
|
3.0*one*t*t,t*t*t};
|
|
const std::array<double,4> derivative={-3.0*one*one,
|
|
3.0*one*one-6.0*one*t,6.0*one*t-3.0*t*t,3.0*t*t};
|
|
std::array<double,3> center{},tangent{};
|
|
for(std::size_t control=0;control<4;++control)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double point=edge->controlPointsMicrometres[control][axis];
|
|
center[axis]+=weights[control]*point;
|
|
tangent[axis]+=derivative[control]*point;
|
|
}
|
|
center[2]+=edge->boreHeightMicrometres*0.5;
|
|
// The room lies west of the entry tangent; use the east wall for the
|
|
// approach outcrop so its conservative bounds stay outside the room.
|
|
std::array<double,3> side={tangent[1],-tangent[0],0.0};
|
|
const double wallOffset=edge->boreRadiusMicrometres+800000.0+100000.0+200000.0;
|
|
const double sideLength=std::sqrt(side[0]*side[0]+side[1]*side[1]);
|
|
if(sideLength>1.0e-9)
|
|
{side[0]*=wallOffset/sideLength;side[1]*=wallOffset/sideLength;}
|
|
ResolvedLayoutOreVeinRequestV1 request;
|
|
request.stableId="ore/sw-chamber-approach-quartz-v1";
|
|
request.strateIndex=surface->strateIndex;request.resourceMaterialId=3;
|
|
request.placementRadiusMicrometres=1000000;
|
|
request.radiusMicrometres=800000;
|
|
request.irregularityAmplitudeMicrometres=100000;
|
|
request.irregularityWavelengthMicrometres=1800000;
|
|
request.blendWidthMicrometres=600000;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
{
|
|
const double half=1250000.0*tangent[axis]
|
|
/std::sqrt(tangent[0]*tangent[0]+tangent[1]*tangent[1]
|
|
+tangent[2]*tangent[2]);
|
|
request.anchorEndpointsMicrometres[0][axis]=static_cast<std::int64_t>(
|
|
std::llround(center[axis]+side[axis]-half));
|
|
request.anchorEndpointsMicrometres[1][axis]=static_cast<std::int64_t>(
|
|
std::llround(center[axis]+side[axis]+half));
|
|
}
|
|
requests.push_back(std::move(request));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OverrideTransitions(VFJson::Value& root,int mode,int widthChunks,std::string& error)
|
|
{
|
|
auto* object=std::get_if<VFJson::Value::Object>(&root.Data);
|
|
if(!object){error="transition fixture override requires an owner JSON object";return false;}
|
|
const auto stratesIt=object->find("strates");
|
|
if(stratesIt==object->end())
|
|
{error="transition fixture override cannot find the resolved strate array";return false;}
|
|
auto* strates=std::get_if<VFJson::Value::Array>(&stratesIt->second.Data);
|
|
if(!strates||strates->empty())
|
|
{error="transition fixture override found an empty or invalid strate array";return false;}
|
|
for(auto& strate:*strates)
|
|
{
|
|
auto* strateObject=std::get_if<VFJson::Value::Object>(&strate.Data);
|
|
if(!strateObject){error="transition fixture contains a malformed strate";return false;}
|
|
const auto assetIt=strateObject->find("asset");
|
|
if(assetIt==strateObject->end())continue;
|
|
auto* asset=std::get_if<VFJson::Value::Object>(&assetIt->second.Data);
|
|
if(!asset)continue;
|
|
if(mode>=0)
|
|
{
|
|
VFJson::Value value;value.Data=static_cast<double>(mode);
|
|
(*asset)["TransitionType"]=std::move(value);
|
|
}
|
|
if(widthChunks>=0)
|
|
{
|
|
VFJson::Value value;value.Data=static_cast<double>(widthChunks);
|
|
(*asset)["TransitionBlendChunks"]=std::move(value);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseOptions(int argc,char** argv,FOptions& options)
|
|
{
|
|
const std::filesystem::path executable=std::filesystem::absolute(argv[0]);
|
|
options.DataPath=executable.parent_path().parent_path()/"data/owner_world.json";
|
|
for(int index=1;index<argc;++index)
|
|
{
|
|
const std::string_view arg(argv[index]);
|
|
const auto readInt=[&](std::string_view prefix,int& value)
|
|
{return arg.starts_with(prefix)&&ParseInteger(arg.substr(prefix.size()),value);};
|
|
if(arg=="--help"||arg=="-h")
|
|
{
|
|
std::printf("Usage: %s [owner_world.json] --world-assembler-prototype --stream-benchmark --stream-pipeline [options]\n"
|
|
" --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n"
|
|
" --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n"
|
|
" --stream-hash-tiles (print per-tile hashes; implies --stream-hash)\n"
|
|
" --camera-step=X,Y,Z (optional second view step in metres)\n"
|
|
" --region-crossing (compile entering Core regions at the camera step)\n"
|
|
" --column-cache=shared|thread-local (default: shared)\n"
|
|
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
|
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
|
" --seed=N --recipe=surface-world|floating-islands\n"
|
|
" --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n"
|
|
" --ore-veins-demo (opt-in material-only SW capsule veins)\n"
|
|
" --audit-ore-vein-yield (measure one deterministic sphere carve)\n"
|
|
" --landmark-l45-ramp-control (use the buried L45 stamp geometry as an audit control)\n"
|
|
" --resolved-layout-output=FILE (canonical plan-time layout snapshot)\n"
|
|
" --resolved-layout-only (exit after plan and snapshot validation)\n"
|
|
" --audit-resolved-layout-boundaries (query Core at each recorded endpoint)\n"
|
|
" --audit-strate-transitions (measure interface crossing, band thickness and holes)\n"
|
|
" --transition-mode=hard|gradient|interleaved --transition-width-chunks=N (fixture override)\n"
|
|
" --audit-resolved-layout-passages (capsule, support, route-binding and blocked-control audit)\n"
|
|
" --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n"
|
|
" --audit-surface-clearance (sample biome terrain/caps against the resolved ceiling)\n"
|
|
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
|
" --mesh-coverage (report non-empty tiles and indexed triangles)\n"
|
|
" --screen-space-gate (measure display error and historical negative controls)\n"
|
|
" --audit-uniform-block-proofs (fill and check every interval-certified block)\n"
|
|
" --profile-lod-work (report per-LOD density sample counts)\n"
|
|
" --audit-base-page-equivalence (hash one tile through both shape routes)\n"
|
|
" --audit-route-parity (compare page, native-only, exact-query and mesh routes)\n"
|
|
" --audit-tile-shape-readiness (run a deliberately mis-marked tile against the native-only guard)\n"
|
|
" --diagnostic-slow-shape-compile-ms=N (diagnostic overlap control; default 0)\n"
|
|
" --render-distance-chunks=N (0 disables the extra far ring)\n"
|
|
" --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n"
|
|
" --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n"
|
|
" --process-affinity=MASK --process-priority=Normal --frame-limit=FPS (viewer compatibility only)\n",
|
|
argv[0]);
|
|
return false;
|
|
}
|
|
if(arg=="--world-assembler-prototype")options.WorldAssemblerPrototype=true;
|
|
else if(arg=="--resolved-layout-only")options.ResolvedLayoutOnly=true;
|
|
else if(arg=="--audit-resolved-layout-boundaries")options.AuditResolvedLayoutBoundaries=true;
|
|
else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true;
|
|
else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true;
|
|
else if(arg=="--audit-surface-clearance")options.AuditSurfaceClearance=true;
|
|
else if(arg=="--audit-strate-transitions")options.AuditStrateTransitions=true;
|
|
else if(arg.starts_with("--transition-mode="))
|
|
{
|
|
const std::string_view mode=arg.substr(std::string_view("--transition-mode=").size());
|
|
if(mode=="gradient")options.TransitionModeOverride=0;
|
|
else if(mode=="hard")options.TransitionModeOverride=1;
|
|
else if(mode=="interleaved")options.TransitionModeOverride=2;
|
|
else {std::fprintf(stderr,"Invalid transition mode: %.*s\n",static_cast<int>(mode.size()),mode.data());return false;}
|
|
}
|
|
else if(arg.starts_with("--transition-width-chunks="))
|
|
{
|
|
if(!readInt("--transition-width-chunks=",options.TransitionWidthChunksOverride)
|
|
||options.TransitionWidthChunksOverride<0||options.TransitionWidthChunksOverride>64)
|
|
{std::fprintf(stderr,"transition width chunks must be in 0..64\n");return false;}
|
|
}
|
|
else if(arg=="--stream-benchmark")options.StreamBenchmark=true;
|
|
else if(arg=="--stream-pipeline")options.StreamPipeline=true;
|
|
else if(arg=="--stream-hash")options.StreamHash=true;
|
|
else if(arg=="--stream-hash-tiles")options.StreamHash=true,options.StreamHashTiles=true;
|
|
else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true;
|
|
else if(arg=="--force-generate")options.ForceGenerate=true;
|
|
else if(arg=="--mesh-coverage")options.MeshCoverage=true;
|
|
else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true;
|
|
else if(arg=="--audit-uniform-block-proofs")options.AuditUniformBlockProofs=true;
|
|
else if(arg=="--profile-lod-work")options.ProfileLodWork=true;
|
|
else if(arg=="--audit-base-page-equivalence")options.AuditBasePageEquivalence=true;
|
|
else if(arg=="--audit-route-parity")options.AuditRouteParity=true;
|
|
else if(arg=="--region-crossing")options.RegionCrossing=true;
|
|
else if(arg=="--audit-tile-shape-readiness")options.AuditTileShapeReadiness=true;
|
|
else if(arg=="--auto-surface-camera")options.AutoSurfaceCamera=true;
|
|
else if(arg.starts_with("--recipe="))
|
|
{
|
|
const std::string_view recipe=arg.substr(std::string_view("--recipe=").size());
|
|
if(recipe=="surface-world")options.FloatingIslandsRecipe=false;
|
|
else if(recipe=="floating-islands")options.FloatingIslandsRecipe=true;
|
|
else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;}
|
|
}
|
|
else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true;
|
|
else if(arg=="--landmark-l45-ramp-control")options.LandmarkL45RampControl=true;
|
|
else if(arg=="--ore-veins-demo")options.OreVeinsDemo=true;
|
|
else if(arg=="--audit-ore-vein-yield")options.AuditOreVeinYield=true;
|
|
else if(arg.starts_with("--resolved-layout-output="))
|
|
options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size()));
|
|
else if(arg.starts_with("--seed="))
|
|
{if(!readInt("--seed=",options.SeedOverride))return false;options.HasSeedOverride=true;}
|
|
else if(arg.starts_with("--render-distance-chunks="))
|
|
{
|
|
if(!readInt("--render-distance-chunks=",options.RenderDistanceOverride))return false;
|
|
options.RenderDistanceOverride=std::max(0,options.RenderDistanceOverride);
|
|
options.HasRenderDistanceOverride=true;
|
|
}
|
|
else if(arg.starts_with("--tls-probe="))
|
|
{
|
|
if(!readInt("--tls-probe=",options.TlsProbeWorkers))return false;
|
|
if(options.TlsProbeWorkers<0||options.TlsProbeWorkers>12)
|
|
{std::fprintf(stderr,"--tls-probe must be in 0..12\n");return false;}
|
|
options.TlsProbeRequested=true;
|
|
}
|
|
else if(arg.starts_with("--tls-probe-touch="))
|
|
{
|
|
const std::string_view touch=arg.substr(std::string_view("--tls-probe-touch=").size());
|
|
if(touch!="none"&&touch!="surface-world"&&touch!="floating-islands")
|
|
{std::fprintf(stderr,"Invalid --tls-probe-touch value: %.*s\n",static_cast<int>(touch.size()),touch.data());return false;}
|
|
options.TlsProbeTouch=touch;
|
|
}
|
|
else if(arg.starts_with("--camera="))
|
|
{
|
|
if(!ParseCamera(arg.substr(9),options.CameraMeters,
|
|
options.CameraYawDegrees,options.CameraPitchDegrees))
|
|
{std::fprintf(stderr,"Invalid --camera value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
|
options.HasCamera=true;
|
|
}
|
|
else if(arg.starts_with("--camera-step="))
|
|
{
|
|
if(!ParseCameraStep(arg.substr(std::string_view("--camera-step=").size()),
|
|
options.CameraStepMeters))
|
|
{std::fprintf(stderr,"Invalid --camera-step value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
|
options.HasCameraStep=true;
|
|
}
|
|
else if(arg.starts_with("--column-cache="))
|
|
{
|
|
const std::string_view mode=arg.substr(std::string_view("--column-cache=").size());
|
|
if(mode=="shared")options.UseSharedColumnCache=true;
|
|
else if(mode=="thread-local")options.UseSharedColumnCache=false;
|
|
else {std::fprintf(stderr,"Invalid --column-cache value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
|
}
|
|
else if(arg.starts_with("--mesh-export="))
|
|
{
|
|
options.MeshExportPath=std::filesystem::path(arg.substr(14));
|
|
if(options.MeshExportPath.empty())
|
|
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
|
}
|
|
else if(arg.starts_with("--workers="))
|
|
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
|
else if(arg.starts_with("--clip-radius="))
|
|
{if(!readInt("--clip-radius=",options.ClipRadius))return false;}
|
|
else if(arg.starts_with("--max-clip-level="))
|
|
{if(!readInt("--max-clip-level=",options.MaxClipLevel))return false;}
|
|
else if(arg.starts_with("--max-level="))
|
|
{if(!readInt("--max-level=",options.MaxLevel))return false;}
|
|
else if(arg.starts_with("--far-horizon-lod8-step="))
|
|
{if(!readInt("--far-horizon-lod8-step=",options.FarHorizonLOD8InteriorStepVoxels))return false;}
|
|
else if(arg.starts_with("--diagnostic-slow-shape-compile-ms="))
|
|
{if(!readInt("--diagnostic-slow-shape-compile-ms=",options.DiagnosticSlowShapeCompileMs))return false;}
|
|
else if(arg.starts_with("--process-affinity=")||arg.starts_with("--process-priority=")
|
|
||arg.starts_with("--frame-limit="))
|
|
{
|
|
// These viewer switches are accepted for command-line parity. The caller pins
|
|
// this headless process with taskset, and a headless run has no frame limiter.
|
|
}
|
|
else if(!arg.empty()&&arg.front()!='-')options.DataPath=std::filesystem::path(arg);
|
|
else
|
|
{std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
|
}
|
|
options.WorkerCount=std::clamp(options.WorkerCount,1,16);
|
|
options.ClipRadius=std::clamp(options.ClipRadius,1,64);
|
|
options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8);
|
|
options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels);
|
|
options.DiagnosticSlowShapeCompileMs=std::clamp(options.DiagnosticSlowShapeCompileMs,0,30000);
|
|
if(!options.TlsProbeRequested&&options.TlsProbeTouch!="none")
|
|
{std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;}
|
|
if(options.RegionCrossing&&!options.HasCameraStep)
|
|
{std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;}
|
|
if(options.LandmarkL45RampControl&&!options.LandmarkChamberDemo)
|
|
{std::fprintf(stderr,"--landmark-l45-ramp-control requires --landmark-chamber-demo\n");return false;}
|
|
if(options.AuditOreVeinYield&&!options.OreVeinsDemo)
|
|
{std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;}
|
|
if(options.TlsProbeRequested)return true;
|
|
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
|
|
{
|
|
std::fprintf(stderr,"This headless target runs the world-assembler streaming pipeline; pass --world-assembler-prototype --stream-benchmark --stream-pipeline.\n");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
int main(int argc,char** argv)
|
|
{
|
|
VoxelForgePrintSourceStamp();
|
|
if(argc==2&&std::string_view(argv[1])=="--print-source-stamp")return 0;
|
|
using Clock=std::chrono::steady_clock;
|
|
const auto coldStart=Clock::now();
|
|
FOptions options;
|
|
if(!ParseOptions(argc,argv,options))return 2;
|
|
if(options.TlsProbeRequested)
|
|
return StandaloneTlsProbe::Run(options.TlsProbeWorkers,options.TlsProbeTouch);
|
|
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s recipe=%s shared_column_cache=%s camera_step_m=(%.3f,%.3f,%.3f) seed_override=%s render_distance_override=%s\n",
|
|
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
|
|
options.MaxLevel,options.StreamHash?"on":"off",options.ProfileColumnOverlap?"on":"off",
|
|
options.ForceGenerate?"on":"off",options.FloatingIslandsRecipe?"floating-islands":"surface-world",
|
|
options.UseSharedColumnCache?"on":"off",options.CameraStepMeters.X,options.CameraStepMeters.Y,
|
|
options.CameraStepMeters.Z,
|
|
options.HasSeedOverride?"yes":"no",options.HasRenderDistanceOverride?"yes":"no");
|
|
std::printf("[ColdTimeline] stage=owner_data_load begin_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(Clock::now()-coldStart).count());
|
|
|
|
std::string error;
|
|
auto ownerWorld=std::make_shared<FStandaloneOwnerWorld>();
|
|
VFJson::Value ownerJsonRoot;
|
|
{
|
|
std::ifstream jsonFile(options.DataPath,std::ios::binary);
|
|
if(!jsonFile)
|
|
{std::fprintf(stderr,"Owner data load failed: cannot open %s\n",options.DataPath.string().c_str());return 2;}
|
|
std::string ownerJsonText{std::istreambuf_iterator<char>(jsonFile),std::istreambuf_iterator<char>{}};
|
|
if(!jsonFile&&!jsonFile.eof())
|
|
{std::fprintf(stderr,"Owner data load failed: read error for %s\n",options.DataPath.string().c_str());return 2;}
|
|
if(!VFJson::Parser(std::move(ownerJsonText)).Parse(ownerJsonRoot,error))
|
|
{std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;}
|
|
}
|
|
if(options.HasSeedOverride
|
|
&&!StandaloneWorldAssemblyPrototype::OverrideStandaloneSeed(ownerJsonRoot,options.SeedOverride,error))
|
|
{std::fprintf(stderr,"Seed override failed: %s\n",error.c_str());return 2;}
|
|
if(options.HasRenderDistanceOverride
|
|
&&!StandaloneWorldAssemblyPrototype::OverrideStandaloneRenderDistance(
|
|
ownerJsonRoot,options.RenderDistanceOverride,error))
|
|
{std::fprintf(stderr,"Render-distance override failed: %s\n",error.c_str());return 2;}
|
|
if(options.FloatingIslandsRecipe
|
|
&&!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(ownerJsonRoot,error))
|
|
{std::fprintf(stderr,"Floating-island recipe selection failed: %s\n",error.c_str());return 2;}
|
|
if((options.TransitionModeOverride>=0||options.TransitionWidthChunksOverride>=0)
|
|
&&!OverrideTransitions(ownerJsonRoot,options.TransitionModeOverride,
|
|
options.TransitionWidthChunksOverride,error))
|
|
{std::fprintf(stderr,"Transition fixture override failed: %s\n",error.c_str());return 2;}
|
|
if(!StandaloneOwnerLoader::Resolve(ownerJsonRoot,*ownerWorld,error))
|
|
{std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;}
|
|
std::printf("[ColdTimeline] stage=owner_data_load end_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(Clock::now()-coldStart).count());
|
|
|
|
const int seed=ownerWorld->Seed;
|
|
const int exportedMaxClipLevel=ownerWorld->MaxClipLevel;
|
|
ownerWorld->ClipRadius=options.ClipRadius;
|
|
ownerWorld->Settings->ClipRadius=options.ClipRadius;
|
|
ownerWorld->MaxClipLevel=options.MaxClipLevel;
|
|
ownerWorld->Settings->MaxClipLevel=options.MaxClipLevel;
|
|
if(exportedMaxClipLevel!=options.MaxClipLevel)
|
|
std::printf("Standalone max clip level override: exported=%d -> max_level=%d.\n",
|
|
exportedMaxClipLevel,options.MaxClipLevel);
|
|
|
|
FVector cameraVoxels=ownerWorld->ViewerStartVoxels;
|
|
if(options.HasCamera)
|
|
{
|
|
const float VoxelsPerMetre=1000000.0f/std::max<std::uint32_t>(1,ownerWorld->CoreVoxelSizeMicrons);
|
|
cameraVoxels=FVector(options.CameraMeters.X*VoxelsPerMetre,
|
|
options.CameraMeters.Y*VoxelsPerMetre,options.CameraMeters.Z*VoxelsPerMetre);
|
|
}
|
|
|
|
const auto planStart=Clock::now();
|
|
VoxelForgeCore::WorldAssemblyManifest worldAssembly;
|
|
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
|
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
|
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
|
if(options.FloatingIslandsRecipe
|
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
|
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
|
{
|
|
const std::string_view variant=options.FloatingIslandsRecipe?"floating-islands":"surface-world";
|
|
if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
|
*ownerWorld,worldAssembly,variant,resolvedLayout,error))
|
|
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
|
if(options.LandmarkChamberDemo)
|
|
{
|
|
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
|
if(!MakeLandmarkChamberDemoRequests(resolvedLayout,worldAssembly,seed,
|
|
requests,rooms,edges,options.LandmarkL45RampControl,error)
|
|
||!VoxelForgeCore::ResolveResolvedLayoutStampsV2(
|
|
resolvedLayout,seed,requests,error)
|
|
||!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
|
resolvedLayout,seed,rooms,edges,error))
|
|
{std::fprintf(stderr,"Landmark cavern graph resolution failed: %s\n",error.c_str());return 2;}
|
|
const auto audit=VoxelForgeCore::AuditResolvedLayoutStampPlacementV1(resolvedLayout);
|
|
std::printf("[ResolvedStampPlacementAudit] vocabulary=%u records=%zu route_control=rejected band_control=rejected controls=%u/%u status=%s\n",
|
|
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size(),
|
|
audit.negativeControlsRejected,audit.negativeControlsRun,
|
|
audit.passed?"PASS":"FAIL");
|
|
if(!audit.passed)
|
|
{std::fprintf(stderr,"Resolved stamp placement negative control failed.\n");return 2;}
|
|
for(const auto& stamp:resolvedLayout.stamps)
|
|
std::printf("[ResolvedSdfStamp] id=%s strate=%d kind=%u position_m=(%.3f,%.3f,%.3f) bounds_um=[%lld,%lld)x[%lld,%lld)x[%lld,%lld) extents_m=(%.2f,%.2f,%.2f) irregularity_m=%.2f/%.2f seed=%llu\n",
|
|
stamp.stableId.c_str(),stamp.strateIndex,static_cast<unsigned>(stamp.kind),
|
|
stamp.positionMicrometres[0]/1000000.0,
|
|
stamp.positionMicrometres[1]/1000000.0,
|
|
stamp.positionMicrometres[2]/1000000.0,
|
|
static_cast<long long>(stamp.boundsMinimumMicrometres[0]),
|
|
static_cast<long long>(stamp.boundsMaximumMicrometres[0]),
|
|
static_cast<long long>(stamp.boundsMinimumMicrometres[1]),
|
|
static_cast<long long>(stamp.boundsMaximumMicrometres[1]),
|
|
static_cast<long long>(stamp.boundsMinimumMicrometres[2]),
|
|
static_cast<long long>(stamp.boundsMaximumMicrometres[2]),
|
|
stamp.halfExtentsMicrometres[0]/1000000.0,
|
|
stamp.halfExtentsMicrometres[1]/1000000.0,
|
|
stamp.halfExtentsMicrometres[2]/1000000.0,
|
|
stamp.irregularityAmplitudeMicrometres/1000000.0,
|
|
stamp.irregularityWavelengthMicrometres/1000000.0,
|
|
static_cast<unsigned long long>(stamp.irregularitySeed));
|
|
if(options.LandmarkL45RampControl)
|
|
std::printf("[LandmarkL45RampControl] enabled=true stamps=%zu graph_records=0 expected=segmented status=PASS\n",
|
|
resolvedLayout.stamps.size());
|
|
else
|
|
std::printf("[LandmarkCavernGraphDemo] enabled=true seed=%d layout_id=%s rooms=%zu edges=%zu support_stamps=%zu passage_safe=true deterministic_hash=seed,strate,stable_id version=%u status=PASS\n",
|
|
seed,resolvedLayout.stableIdentity.c_str(),
|
|
resolvedLayout.cavernRooms.size(),resolvedLayout.cavernEdges.size(),
|
|
resolvedLayout.stamps.size(),resolvedLayout.cavernGraphVocabularyVersion);
|
|
}
|
|
if(options.OreVeinsDemo)
|
|
{
|
|
std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1> requests;
|
|
if(!MakeOreVeinDemoRequests(resolvedLayout,requests,error)
|
|
||!VoxelForgeCore::ResolveResolvedLayoutOreVeinsV1(
|
|
resolvedLayout,seed,requests,error))
|
|
{std::fprintf(stderr,"Ore-vein demo resolution failed: %s\n",error.c_str());return 2;}
|
|
const auto audit=VoxelForgeCore::AuditResolvedLayoutOreVeinPlacementV1(resolvedLayout);
|
|
std::printf("[ResolvedOreVeinPlacementAudit] vocabulary=%u records=%zu forced_clear_passage=rejected controls=%u/%u status=%s\n",
|
|
resolvedLayout.oreVeinVocabularyVersion,resolvedLayout.oreVeins.size(),
|
|
audit.negativeControlsRejected,audit.negativeControlsRun,
|
|
audit.passed?"PASS":"FAIL");
|
|
if(!audit.passed)
|
|
{std::fprintf(stderr,"Resolved ore-vein placement negative control failed.\n");return 2;}
|
|
for(const auto& vein:resolvedLayout.oreVeins)
|
|
std::printf("[ResolvedOreVein] id=%s strate=%d resource=%u shape=capsule endpoints_m=(%.3f,%.3f,%.3f)->(%.3f,%.3f,%.3f) radius_m=%.2f irregularity_m=%.2f/%.2f blend_m=%.2f bounds_um=[%lld,%lld)x[%lld,%lld)x[%lld,%lld) seed=%llu\n",
|
|
vein.stableId.c_str(),vein.strateIndex,vein.resourceMaterialId,
|
|
vein.endpointsMicrometres[0][0]/1000000.0,
|
|
vein.endpointsMicrometres[0][1]/1000000.0,
|
|
vein.endpointsMicrometres[0][2]/1000000.0,
|
|
vein.endpointsMicrometres[1][0]/1000000.0,
|
|
vein.endpointsMicrometres[1][1]/1000000.0,
|
|
vein.endpointsMicrometres[1][2]/1000000.0,
|
|
vein.radiusMicrometres/1000000.0,
|
|
vein.irregularityAmplitudeMicrometres/1000000.0,
|
|
vein.irregularityWavelengthMicrometres/1000000.0,
|
|
vein.blendWidthMicrometres/1000000.0,
|
|
static_cast<long long>(vein.boundsMinimumMicrometres[0]),
|
|
static_cast<long long>(vein.boundsMaximumMicrometres[0]),
|
|
static_cast<long long>(vein.boundsMinimumMicrometres[1]),
|
|
static_cast<long long>(vein.boundsMaximumMicrometres[1]),
|
|
static_cast<long long>(vein.boundsMinimumMicrometres[2]),
|
|
static_cast<long long>(vein.boundsMaximumMicrometres[2]),
|
|
static_cast<unsigned long long>(vein.irregularitySeed));
|
|
std::printf("[ResolvedOreVeinDemo] enabled=true seed=%d records=%zu stable_seed=seed,strate,stable_id placement=side-pocket-and-chamber-approach geometry_changes=0 status=PASS\n",
|
|
seed,resolvedLayout.oreVeins.size());
|
|
}
|
|
const std::string layoutJson=resolvedLayout.ToJson();
|
|
const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(layoutJson,error);
|
|
if(!roundTrip||roundTrip->ToJson()!=layoutJson||roundTrip->stableIdentity!=resolvedLayout.stableIdentity)
|
|
{if(error.empty())error="resolved-layout canonical serialization did not round-trip";
|
|
std::fprintf(stderr,"Resolved-layout round trip failed: %s\n",error.c_str());return 2;}
|
|
std::string resolvedStrateStyles="{";
|
|
bool firstResolvedStrateStyle=true;
|
|
for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
|
{
|
|
if(!slot.Definition)continue;
|
|
const auto resolvedStrate=std::find_if(resolvedLayout.strates.begin(),
|
|
resolvedLayout.strates.end(),[&](const auto& value)
|
|
{return value.strateIndex==slot.StrateIndex;});
|
|
if(resolvedStrate==resolvedLayout.strates.end())continue;
|
|
const auto style=static_cast<std::uint32_t>(slot.Definition->PassageConfig.Style);
|
|
if(!firstResolvedStrateStyle)resolvedStrateStyles+=',';
|
|
firstResolvedStrateStyle=false;
|
|
resolvedStrateStyles.push_back('"');
|
|
resolvedStrateStyles+=std::to_string(slot.StrateIndex);
|
|
resolvedStrateStyles+="\":";
|
|
resolvedStrateStyles+=std::to_string(style);
|
|
}
|
|
resolvedStrateStyles+='}';
|
|
const std::string layoutOutputJson=layoutJson.substr(0,layoutJson.size()-1)
|
|
+",\"resolved_strate_passage_styles\":"+resolvedStrateStyles+"}";
|
|
if(!options.ResolvedLayoutOutputPath.empty())
|
|
{
|
|
if(!options.ResolvedLayoutOutputPath.parent_path().empty())
|
|
std::filesystem::create_directories(options.ResolvedLayoutOutputPath.parent_path());
|
|
std::ofstream output(options.ResolvedLayoutOutputPath,std::ios::binary);
|
|
if(!output){std::fprintf(stderr,"Could not write resolved-layout snapshot: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;}
|
|
output.write(layoutOutputJson.data(),static_cast<std::streamsize>(layoutOutputJson.size()));
|
|
if(!output){std::fprintf(stderr,"Resolved-layout snapshot write failed: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;}
|
|
}
|
|
const auto controls=VoxelForgeCore::AuditResolvedLayoutValidatorV3();
|
|
std::vector<VoxelForgeCore::ResolvedLayoutIssueV2> compatibilityIssues;
|
|
std::string compatibilityError;
|
|
const bool compatibilityAccepted=resolvedLayout.validate(
|
|
VoxelForgeCore::ResolvedLayoutValidationModeV3::compatibility,
|
|
compatibilityIssues,compatibilityError);
|
|
std::vector<VoxelForgeCore::ResolvedLayoutIssueV2> strictIssues;
|
|
std::string strictError;
|
|
const bool strictAccepted=resolvedLayout.validate(
|
|
VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,strictIssues,strictError);
|
|
std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n",
|
|
resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size());
|
|
for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
|
{
|
|
if(!slot.Definition)continue;
|
|
const auto resolvedStrate=std::find_if(resolvedLayout.strates.begin(),
|
|
resolvedLayout.strates.end(),[&](const auto& value)
|
|
{return value.strateIndex==slot.StrateIndex;});
|
|
if(resolvedStrate!=resolvedLayout.strates.end())
|
|
std::printf("[ResolvedStratePassageStyle] strate_index=%d generator_kind=%s style=%u\n",
|
|
slot.StrateIndex,resolvedStrate->generatorKind.c_str(),
|
|
static_cast<std::uint32_t>(slot.Definition->PassageConfig.Style));
|
|
}
|
|
const auto requestedRecipe=options.FloatingIslandsRecipe
|
|
?std::string_view("floating-islands"):std::string_view("surface-world");
|
|
const auto topStratum=std::max_element(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
|
[](const auto& a,const auto& b)
|
|
{return a.interiorTopMicrometres<b.interiorTopMicrometres;});
|
|
const bool hasFloatingIslands=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
|
[](const auto& stratum){return stratum.generatorKind=="FloatingIslands";});
|
|
const bool hasSurfaceWorldPlan=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
|
[&](const auto& stratum)
|
|
{
|
|
return stratum.generatorKind=="SurfaceWorld"
|
|
&&stratum.recipeId=="SurfaceWorld.Heightfield2D"
|
|
&&std::any_of(worldAssembly.plan.strates.begin(),worldAssembly.plan.strates.end(),
|
|
[&](const auto& plan){return plan.strateIndex==stratum.strateIndex;});
|
|
});
|
|
const auto actualRecipe=hasFloatingIslands?std::string_view("floating-islands")
|
|
:hasSurfaceWorldPlan?std::string_view("surface-world")
|
|
:std::string_view("missing");
|
|
const char* topGenerator=topStratum==resolvedLayout.strates.end()
|
|
?"missing":topStratum->generatorKind.c_str();
|
|
std::printf("[WorldAssemblerRecipeAudit] requested=%.*s actual=%.*s top_generator=%s surface_world_plan=%s floating_islands_stratum=%s status=%s\n",
|
|
static_cast<int>(requestedRecipe.size()),requestedRecipe.data(),
|
|
static_cast<int>(actualRecipe.size()),actualRecipe.data(),
|
|
topGenerator,hasSurfaceWorldPlan?"present":"absent",hasFloatingIslands?"present":"absent",
|
|
actualRecipe==requestedRecipe?"PASS":"FAIL");
|
|
if(actualRecipe!=requestedRecipe)
|
|
{std::fprintf(stderr,"Requested recipe %.*s resolved to %.*s\n",
|
|
static_cast<int>(requestedRecipe.size()),requestedRecipe.data(),
|
|
static_cast<int>(actualRecipe.size()),actualRecipe.data());return 2;}
|
|
std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n",
|
|
compatibilityAccepted?"true":"false",compatibilityIssues.size(),compatibilityError.c_str());
|
|
for(const auto& issue:compatibilityIssues)
|
|
std::printf("[ResolvedLayoutCompatibilityIssue] code=%s detail=%s\n",
|
|
issue.code.c_str(),issue.detail.c_str());
|
|
if(!compatibilityAccepted)
|
|
{std::fprintf(stderr,"Resolved-layout compatibility validation failed: %s\n",compatibilityError.c_str());return 2;}
|
|
std::printf("[ResolvedLayoutStrict] accepted=%s issues=%zu error=%s\n",
|
|
strictAccepted?"true":"false",strictIssues.size(),strictError.c_str());
|
|
for(const auto& issue:strictIssues)
|
|
std::printf("[ResolvedLayoutStrictIssue] code=%s detail=%s\n",issue.code.c_str(),issue.detail.c_str());
|
|
if(!strictAccepted)
|
|
{std::fprintf(stderr,"Resolved-layout strict validation failed: %s\n",strictError.c_str());return 2;}
|
|
std::printf("[ResolvedLayoutValidatorControls] rejected=%u/%u status=%s\n",
|
|
controls.controlsRejectedAsExpected,controls.controlsRun,controls.passed?"PASS":"FAIL");
|
|
if(!controls.passed)
|
|
{for(const auto& failure:controls.failures)std::printf("[ResolvedLayoutValidatorControlFailure] %s %s\n",failure.code.c_str(),failure.detail.c_str());return 2;}
|
|
if(!options.ResolvedLayoutOutputPath.empty())
|
|
std::printf("[ResolvedLayoutSnapshotOutput] path=%s\n",options.ResolvedLayoutOutputPath.c_str());
|
|
if(auto* root=std::get_if<VFJson::Value::Object>(&ownerJsonRoot.Data))
|
|
{VFJson::Value layoutText;layoutText.Data=layoutJson;(*root)["resolved_layout_json"]=std::move(layoutText);}
|
|
else
|
|
{std::fprintf(stderr,"Owner snapshot root cannot carry its resolved layout.\n");return 2;}
|
|
if(options.AuditSurfaceClearance)
|
|
{
|
|
if(options.FloatingIslandsRecipe)
|
|
{std::fprintf(stderr,"--audit-surface-clearance requires --recipe=surface-world\n");return 2;}
|
|
const auto surface=std::find_if(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
|
[](const auto& Stratum){return Stratum.generatorKind=="SurfaceWorld";});
|
|
const auto plan=surface==resolvedLayout.strates.end()?worldAssembly.plan.strates.end()
|
|
:std::find_if(worldAssembly.plan.strates.begin(),worldAssembly.plan.strates.end(),
|
|
[&](const auto& Stratum){return Stratum.strateIndex==surface->strateIndex;});
|
|
if(surface==resolvedLayout.strates.end()||plan==worldAssembly.plan.strates.end()
|
|
||!surface->ceiling.presentInCurrentField)
|
|
{std::fprintf(stderr,"SurfaceWorld clearance audit could not resolve its plan/ceiling\n");return 2;}
|
|
const VoxelForgeCore::Heightfield2D Field(worldAssembly.recipe,*plan);
|
|
if(!Field.isValid())
|
|
{std::fprintf(stderr,"SurfaceWorld clearance audit field is invalid\n");return 2;}
|
|
constexpr std::int64_t HalfExtentMetres=2048,StepMetres=1;
|
|
const double CoreMicrometres=std::max<std::uint32_t>(1,ownerWorld->CoreVoxelSizeMicrons);
|
|
const double MetresPerSubvoxel=CoreMicrometres
|
|
/(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
|
double CenterX=0.0,CenterY=0.0;
|
|
if(!plan->route.empty())
|
|
{CenterX=plan->route.front().positionSubvoxels[0]*MetresPerSubvoxel;
|
|
CenterY=plan->route.front().positionSubvoxels[1]*MetresPerSubvoxel;}
|
|
const std::int64_t CenterXQ=static_cast<std::int64_t>(std::llround(
|
|
CenterX/MetresPerSubvoxel));
|
|
const std::int64_t CenterYQ=static_cast<std::int64_t>(std::llround(
|
|
CenterY/MetresPerSubvoxel));
|
|
const std::int64_t StepQ=static_cast<std::int64_t>(std::llround(
|
|
StepMetres/MetresPerSubvoxel));
|
|
struct FBiomeClearance
|
|
{
|
|
std::uint64_t Samples=0;
|
|
double MaximumGround=-std::numeric_limits<double>::infinity();
|
|
double MaximumCap=-std::numeric_limits<double>::infinity();
|
|
double MaximumSurface=-std::numeric_limits<double>::infinity();
|
|
double MinimumClearance=std::numeric_limits<double>::infinity();
|
|
double AtX=0.0,AtY=0.0,AtHeight=0.0;
|
|
std::int64_t MaximumCapQ=std::numeric_limits<std::int64_t>::min();
|
|
};
|
|
std::map<std::uint32_t,FBiomeClearance> ByBiome;
|
|
const double CeilingFace=static_cast<double>(surface->ceiling.bottomMicrometres)/1000000.0;
|
|
for(std::int64_t Y=-HalfExtentMetres;Y<=HalfExtentMetres;Y+=StepMetres)
|
|
for(std::int64_t X=-HalfExtentMetres;X<=HalfExtentMetres;X+=StepMetres)
|
|
{
|
|
const auto Sample=Field.point(CenterXQ+(X/StepMetres)*StepQ,
|
|
CenterYQ+(Y/StepMetres)*StepQ,0);
|
|
const double Ground=Sample.groundHeightSubvoxels*MetresPerSubvoxel;
|
|
const double Cap=Sample.capHeightSubvoxels*MetresPerSubvoxel;
|
|
const double SurfaceHeight=std::max(Ground,Cap);
|
|
auto& Stats=ByBiome[Sample.biomeId];
|
|
++Stats.Samples;
|
|
Stats.MaximumGround=std::max(Stats.MaximumGround,Ground);
|
|
Stats.MaximumCap=std::max(Stats.MaximumCap,Cap);
|
|
Stats.MaximumCapQ=std::max(Stats.MaximumCapQ,Sample.capHeightSubvoxels);
|
|
Stats.MaximumSurface=std::max(Stats.MaximumSurface,SurfaceHeight);
|
|
const double Clearance=CeilingFace-SurfaceHeight;
|
|
if(Clearance<Stats.MinimumClearance)
|
|
{Stats.MinimumClearance=Clearance;Stats.AtX=CenterX+X;Stats.AtY=CenterY+Y;
|
|
Stats.AtHeight=SurfaceHeight;}
|
|
}
|
|
double MaximumRouteZ=-std::numeric_limits<double>::infinity();
|
|
for(const auto& Point:plan->route)
|
|
MaximumRouteZ=std::max(MaximumRouteZ,Point.positionSubvoxels[2]*MetresPerSubvoxel);
|
|
const auto CapBounds=Field.capHeightBounds();
|
|
const auto RouteSample=plan->route.empty()?VoxelForgeCore::HeightfieldPointSample{}
|
|
:Field.point(plan->route.front().positionSubvoxels[0],
|
|
plan->route.front().positionSubvoxels[1],0);
|
|
std::printf("[SurfaceClearancePlan] has_recipe_override=%s layout_chunk_q=%lld plan_cap_q=%lld cap_bounds_q=(%lld,%lld)\n",
|
|
plan->hasRecipeOverride?"true":"false",
|
|
static_cast<long long>(worldAssembly.recipe.layoutChunkWidthSubvoxels),
|
|
static_cast<long long>(plan->capHeightSubvoxels),
|
|
static_cast<long long>(CapBounds[0]),static_cast<long long>(CapBounds[1]));
|
|
for(const auto& Profile:worldAssembly.recipe.biomes)
|
|
std::printf("[SurfaceClearanceProfile] biome_id=%u has_cap=%s cap_q=%lld undulation_q=%lld detail_q=%lld ridge_q=%lld\n",
|
|
Profile.stableId,Profile.hasCapProfile?"true":"false",
|
|
static_cast<long long>(Profile.capHeightSubvoxels),
|
|
static_cast<long long>(Profile.capUndulationSubvoxels),
|
|
static_cast<long long>(Profile.capDetailAmplitudeSubvoxels),
|
|
static_cast<long long>(Profile.capRidgeAmplitudeSubvoxels));
|
|
std::printf("[SurfaceClearanceAudit] box_m=4096x4096 step_m=%lld samples=%lld ceiling_inner_face_m=%.3f unmodified_field=true\n",
|
|
static_cast<long long>(StepMetres),
|
|
static_cast<long long>((2*HalfExtentMetres/StepMetres+1)*(2*HalfExtentMetres/StepMetres+1)),
|
|
CeilingFace);
|
|
std::printf("[SurfaceClearanceCapRange] plan_min_m=%.3f plan_max_m=%.3f spawn_cap_m=%.3f spawn_ground_m=%.3f\n",
|
|
CapBounds[0]*MetresPerSubvoxel,CapBounds[1]*MetresPerSubvoxel,
|
|
RouteSample.capHeightSubvoxels*MetresPerSubvoxel,
|
|
RouteSample.groundHeightSubvoxels*MetresPerSubvoxel);
|
|
for(const auto& Feature:plan->features)
|
|
std::printf("[SurfaceClearanceFeature] type=%u params_q=(%lld,%lld,%lld,%lld)\n",
|
|
Feature.type,static_cast<long long>(Feature.parametersSubvoxels[0]),
|
|
static_cast<long long>(Feature.parametersSubvoxels[1]),
|
|
static_cast<long long>(Feature.parametersSubvoxels[2]),
|
|
static_cast<long long>(Feature.parametersSubvoxels[3]));
|
|
for(const auto& [BiomeId,Stats]:ByBiome)
|
|
std::printf("[SurfaceClearanceBiome] biome_id=%u samples=%llu max_ground_m=%.3f max_cap_m=%.3f max_cap_q=%lld max_surface_m=%.3f min_clearance_m=%.3f at_m=(%.1f,%.1f,%.3f)\n",
|
|
BiomeId,static_cast<unsigned long long>(Stats.Samples),Stats.MaximumGround,
|
|
Stats.MaximumCap,static_cast<long long>(Stats.MaximumCapQ),Stats.MaximumSurface,Stats.MinimumClearance,
|
|
Stats.AtX,Stats.AtY,Stats.AtHeight);
|
|
std::printf("[SurfaceClearanceRoute] route_points=%zu max_route_z_m=%.3f clearance_m=%.3f status=%s\n",
|
|
plan->route.size(),MaximumRouteZ,CeilingFace-MaximumRouteZ,
|
|
MaximumRouteZ<CeilingFace?"PASS":"COLLISION");
|
|
}
|
|
if(options.ResolvedLayoutOnly)return 0;
|
|
}
|
|
if(options.AutoSurfaceCamera)
|
|
{
|
|
constexpr std::int64_t UnitsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
if(options.FloatingIslandsRecipe)
|
|
{
|
|
const VoxelForgeCore::FloatingIslandAnchorV1* chosen=nullptr;
|
|
for(const auto& islandSet:worldAssembly.floatingIslands)
|
|
for(const auto& island:islandSet.manifest.plan.islands)
|
|
if(!chosen||island.radiusSubvoxels>chosen->radiusSubvoxels)chosen=&island;
|
|
if(!chosen)
|
|
{std::fprintf(stderr,"[SeedSurfacePlacement] FAIL: island plan has no anchors\n");return 2;}
|
|
cameraVoxels={
|
|
static_cast<float>(chosen->centerSubvoxels[0])/UnitsPerVoxel,
|
|
static_cast<float>(chosen->centerSubvoxels[1])/UnitsPerVoxel,
|
|
static_cast<float>(chosen->centerSubvoxels[2])/UnitsPerVoxel};
|
|
std::printf("[SeedSurfacePlacement] recipe=floating-islands seed=%d island_radius_vox=%.3f camera_voxel=(%.6f,%.6f,%.6f) status=PASS\n",
|
|
seed,static_cast<double>(chosen->radiusSubvoxels)/UnitsPerVoxel,
|
|
cameraVoxels.X,cameraVoxels.Y,cameraVoxels.Z);
|
|
}
|
|
else
|
|
{
|
|
if(worldAssembly.plan.strates.empty()||worldAssembly.plan.strates.front().route.empty())
|
|
{std::fprintf(stderr,"[SeedSurfacePlacement] FAIL: route-derived spawn is missing\n");return 2;}
|
|
const auto& spawn=worldAssembly.plan.strates.front().route.front();
|
|
cameraVoxels={static_cast<float>(static_cast<double>(spawn.positionSubvoxels[0])/UnitsPerVoxel),
|
|
static_cast<float>(static_cast<double>(spawn.positionSubvoxels[1])/UnitsPerVoxel),
|
|
static_cast<float>(static_cast<double>(spawn.positionSubvoxels[2])/UnitsPerVoxel)};
|
|
std::printf("[SeedSurfacePlacement] recipe=surface-world seed=%d source=route.front position_voxel=(%.6f,%.6f,%.6f) position_m=(%.3f,%.3f,%.3f) status=PASS\n",
|
|
seed,cameraVoxels.X,cameraVoxels.Y,cameraVoxels.Z,
|
|
cameraVoxels.X*ownerWorld->CoreVoxelSizeMicrons/1000000.0f,
|
|
cameraVoxels.Y*ownerWorld->CoreVoxelSizeMicrons/1000000.0f,
|
|
cameraVoxels.Z*ownerWorld->CoreVoxelSizeMicrons/1000000.0f);
|
|
}
|
|
}
|
|
std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=PASS\n",
|
|
std::chrono::duration<double,std::milli>(Clock::now()-planStart).count());
|
|
|
|
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(ownerJsonRoot,seed,
|
|
options.ClipRadius,options.MaxClipLevel,false,false,error);
|
|
if(!snapshot)
|
|
{std::fprintf(stderr,"Shape interval build failed: %s\n",error.c_str());return 2;}
|
|
|
|
FStandaloneCoreScheduler scheduler;
|
|
const FVector cameraStepVoxels=options.HasCameraStep
|
|
?FVector(options.CameraStepMeters.X*1000000.0f/std::max<std::uint32_t>(1,ownerWorld->CoreVoxelSizeMicrons),
|
|
options.CameraStepMeters.Y*1000000.0f/std::max<std::uint32_t>(1,ownerWorld->CoreVoxelSizeMicrons),
|
|
options.CameraStepMeters.Z*1000000.0f/std::max<std::uint32_t>(1,ownerWorld->CoreVoxelSizeMicrons)):FVector::ZeroVector;
|
|
return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(ownerWorld,snapshot,
|
|
cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent,
|
|
scheduler,options.StreamHash,false,false,false,false,coldStart,
|
|
options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels,
|
|
options.MeshCoverage,options.MeshExportPath,options.CameraYawDegrees,
|
|
options.CameraPitchDegrees,options.ScreenSpaceGate,options.UseSharedColumnCache,
|
|
cameraStepVoxels,options.AuditUniformBlockProofs,options.ProfileLodWork,
|
|
options.RegionCrossing,options.AuditTileShapeReadiness,
|
|
options.DiagnosticSlowShapeCompileMs,options.AuditBasePageEquivalence,
|
|
options.AuditRouteParity,options.AuditResolvedLayoutBoundaries,
|
|
options.AuditResolvedLayoutPassages,options.AuditStrateTransitions,
|
|
&resolvedLayout,options.StreamHashTiles,options.AuditLandmarkReachability,
|
|
options.AuditOreVeinYield);
|
|
}
|