Core.Caves.PrimitiveField v1, opt-in per strate (legacy TunnelNetwork assets stay legacy): solid host rock and one smooth union of two elliptical spine tubes and an ellipsoid room, each intersected with its rising floor by cubic smooth-max (W 2.4 m, measured fillet 0.70 m, no trailing floor cap). Bound in the L40 field table and L42 capabilities, BVH feature candidates, isTileShapeFree aware, exact interval boxes (0 misses). Capsule flood fill arrival -> departure passes (headroom >= 0.3 m, grade <= 15 deg); the generalized whole-surface audit passes with its controls failing. Cold whole view 0.727 s, near ring 0.19 s. Graph ordering/serialization changes are scoped to native caves (the flagged L47 demo is unchanged); verify tiers write captures under --out only. fast/medium/full PASS, Clang and GCC. Research R23 (deepened): per-tile LOD0 kernel speed plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
1773 lines
107 KiB
C++
1773 lines
107 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 CavesRecipe=false;
|
|
bool AuditHeightfieldProfile=false;
|
|
bool LandmarkChamberDemo=false;
|
|
bool LandmarkL45RampControl=false;
|
|
bool OreVeinsDemo=false;
|
|
bool AuditOreVeinYield=false;
|
|
bool AuditLandmarkReachability=false;
|
|
bool AuditNativeCaves=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 WorldAssemblyManifestOutputPath;
|
|
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;
|
|
}
|
|
|
|
const char* SurfaceProfileName(VoxelForgeCore::WorldAssemblySurfaceProfileKindV1 kind)
|
|
{
|
|
using VoxelForgeCore::WorldAssemblySurfaceProfileKindV1;
|
|
switch (kind)
|
|
{
|
|
case WorldAssemblySurfaceProfileKindV1::periodicHills: return "periodicHills";
|
|
case WorldAssemblySurfaceProfileKindV1::mirroredRelief: return "mirroredRelief";
|
|
case WorldAssemblySurfaceProfileKindV1::flatField: return "flatField";
|
|
default: return "natural";
|
|
}
|
|
}
|
|
|
|
bool RunHeightfieldProfileAudit(const VoxelForgeCore::WorldAssemblyManifest& assembly)
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
constexpr std::int64_t QPerMetre = WorldAssemblySubvoxelsPerVoxel * 4;
|
|
constexpr std::int64_t BoxStep = 8 * WorldAssemblySubvoxelsPerVoxel;
|
|
std::uint64_t strataAudited = 0;
|
|
bool allPassed = true;
|
|
for (const auto& stratum : assembly.plan.strates)
|
|
{
|
|
const WorldAssemblyRecipeV1& recipe = stratum.hasRecipeOverride
|
|
? stratum.recipeOverride : assembly.recipe;
|
|
const auto& profile = recipe.surfaceProfile;
|
|
if (profile.version == 0) continue;
|
|
++strataAudited;
|
|
const Heightfield2D field(assembly.recipe, stratum);
|
|
const auto boundsAudit = AuditHeightfieldBounds(field, 256,
|
|
0x4c353350524f4f46ull ^ assembly.contentHash ^ stratum.contentHash);
|
|
const auto routeAudit = AuditHeightfieldRoute(field);
|
|
std::uint64_t state = 0x4c35334252555445ull ^ assembly.contentHash
|
|
^ stratum.contentHash;
|
|
const auto nextRandom = [&state]()
|
|
{
|
|
state += 0x9E3779B97F4A7C15ull;
|
|
std::uint64_t value = state;
|
|
value = (value ^ (value >> 30)) * 0xBF58476D1CE4E5B9ull;
|
|
value = (value ^ (value >> 27)) * 0x94D049BB133111EBull;
|
|
return value ^ (value >> 31);
|
|
};
|
|
std::uint64_t bruteBoxes = 0, bruteSamples = 0;
|
|
std::uint64_t intervalMisses = 0, classifierMismatches = 0;
|
|
std::uint64_t certifiedAir = 0, certifiedSolid = 0;
|
|
for (int boxIndex = 0; boxIndex < 96; ++boxIndex)
|
|
{
|
|
const std::int64_t x = (static_cast<std::int64_t>(nextRandom() % 401) - 200)
|
|
* QPerMetre;
|
|
const std::int64_t y = (static_cast<std::int64_t>(nextRandom() % 401) - 200)
|
|
* QPerMetre;
|
|
const std::int64_t span = (8 + static_cast<std::int64_t>(nextRandom() % 5))
|
|
* BoxStep;
|
|
const std::int64_t minX = x, minY = y;
|
|
const std::int64_t maxX = minX + span, maxY = minY + span;
|
|
const std::int64_t centerX = minX + span / 2;
|
|
const std::int64_t centerY = minY + span / 2;
|
|
const HeightfieldPointSample column = field.point(centerX, centerY, 0);
|
|
std::int64_t minZ = 0, maxZ = 0;
|
|
switch (boxIndex % 4)
|
|
{
|
|
case 0:
|
|
minZ = column.groundHeightSubvoxels - 4 * QPerMetre;
|
|
maxZ = column.groundHeightSubvoxels - 2 * QPerMetre;
|
|
break;
|
|
case 1:
|
|
minZ = column.groundHeightSubvoxels
|
|
+ profile.amplitudeXSubvoxels + profile.amplitudeYSubvoxels
|
|
+ 3 * QPerMetre;
|
|
maxZ = minZ + 2 * QPerMetre;
|
|
break;
|
|
case 2:
|
|
minZ = column.groundHeightSubvoxels - QPerMetre;
|
|
maxZ = column.groundHeightSubvoxels + QPerMetre;
|
|
break;
|
|
default:
|
|
minZ = column.capHeightSubvoxels - 2 * QPerMetre;
|
|
maxZ = column.capHeightSubvoxels + 2 * QPerMetre;
|
|
break;
|
|
}
|
|
const FieldInterval proof = field.interval(minX, minY, minZ,
|
|
maxX, maxY, maxZ);
|
|
++bruteBoxes;
|
|
if (!proof.known) { ++intervalMisses; continue; }
|
|
if (proof.minimumDensitySubvoxels > 0) ++certifiedAir;
|
|
if (proof.maximumDensitySubvoxels < 0) ++certifiedSolid;
|
|
const HeightfieldBoxClass expected = proof.minimumDensitySubvoxels > 0
|
|
? HeightfieldBoxClass::allAir
|
|
: proof.maximumDensitySubvoxels < 0
|
|
? HeightfieldBoxClass::allSolid : HeightfieldBoxClass::mixed;
|
|
if (field.classifyLatticeBox(minX, minY, minZ, maxX, maxY, maxZ, 8)
|
|
!= expected) ++classifierMismatches;
|
|
for (int iz = 0; iz <= 2; ++iz)
|
|
for (int iy = 0; iy <= 8; ++iy)
|
|
for (int ix = 0; ix <= 8; ++ix)
|
|
{
|
|
const std::int64_t px = minX + span * ix / 8;
|
|
const std::int64_t py = minY + span * iy / 8;
|
|
const std::int64_t pz = minZ + (maxZ - minZ) * iz / 2;
|
|
const auto sample = field.point(px, py, pz);
|
|
++bruteSamples;
|
|
if (sample.densitySubvoxels < proof.minimumDensitySubvoxels
|
|
|| sample.densitySubvoxels > proof.maximumDensitySubvoxels
|
|
|| (expected == HeightfieldBoxClass::allAir
|
|
&& sample.densitySubvoxels <= 0)
|
|
|| (expected == HeightfieldBoxClass::allSolid
|
|
&& sample.densitySubvoxels >= 0))
|
|
++intervalMisses;
|
|
}
|
|
}
|
|
const std::int64_t relief = profile.amplitudeXSubvoxels
|
|
+ profile.amplitudeYSubvoxels;
|
|
std::int64_t capSwing = 0;
|
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
|
capSwing = (relief * profile.mirrorLambdaQ16 + 65535) / 65536;
|
|
const double minimumClearanceMetres = static_cast<double>(
|
|
stratum.capHeightSubvoxels - capSwing - profile.h0Subvoxels - relief - 4)
|
|
/ QPerMetre;
|
|
const double requiredClearanceMetres = static_cast<double>(std::max<std::int64_t>(
|
|
2253, profile.minimumArenaClearanceSubvoxels)) / QPerMetre;
|
|
const double gx = profile.kind == WorldAssemblySurfaceProfileKindV1::flatField
|
|
? 0.0 : 2.0 * std::acos(-1.0)
|
|
* profile.amplitudeXSubvoxels / profile.wavelengthXSubvoxels;
|
|
const double gy = profile.kind == WorldAssemblySurfaceProfileKindV1::flatField
|
|
? 0.0 : 2.0 * std::acos(-1.0)
|
|
* profile.amplitudeYSubvoxels / profile.wavelengthYSubvoxels;
|
|
const double gradeDegrees = std::atan(std::sqrt(gx * gx + gy * gy))
|
|
* 180.0 / std::acos(-1.0);
|
|
const bool profilePassed = field.isValid() && boundsAudit.passed && routeAudit.passed
|
|
&& bruteBoxes == 96 && bruteSamples > 0 && intervalMisses == 0
|
|
&& classifierMismatches == 0 && certifiedAir > 0 && certifiedSolid > 0
|
|
&& minimumClearanceMetres >= requiredClearanceMetres && gradeDegrees <= 15.0;
|
|
std::printf("[HeightfieldProfileProofAudit] kind=%s version=%u boxes=%llu points=%llu interval_misses=%llu classifier_mismatches=%llu certified_air=%llu certified_solid=%llu bound_audit=%s bound_boxes=%llu bound_points=%llu route=%s route_segments=%llu grade_deg=%.6f clearance_m=%.6f required_clearance_m=%.6f phase_x_q32=%u phase_y_q32=%u status=%s\n",
|
|
SurfaceProfileName(profile.kind), profile.version,
|
|
static_cast<unsigned long long>(bruteBoxes),
|
|
static_cast<unsigned long long>(bruteSamples),
|
|
static_cast<unsigned long long>(intervalMisses),
|
|
static_cast<unsigned long long>(classifierMismatches),
|
|
static_cast<unsigned long long>(certifiedAir),
|
|
static_cast<unsigned long long>(certifiedSolid),
|
|
boundsAudit.passed ? "PASS" : "FAIL",
|
|
static_cast<unsigned long long>(boundsAudit.boxesTested),
|
|
static_cast<unsigned long long>(boundsAudit.pointSamples),
|
|
routeAudit.passed ? "PASS" : "FAIL",
|
|
static_cast<unsigned long long>(routeAudit.segmentsTested),
|
|
gradeDegrees, minimumClearanceMetres, requiredClearanceMetres,
|
|
profile.phaseXTurnsQ32, profile.phaseYTurnsQ32,
|
|
profilePassed ? "PASS" : "FAIL");
|
|
allPassed = allPassed && profilePassed;
|
|
}
|
|
const bool passed = strataAudited > 0 && allPassed;
|
|
if (strataAudited == 0)
|
|
std::printf("[HeightfieldProfileProofAudit] profiles=0 status=FAIL\n");
|
|
return passed;
|
|
}
|
|
|
|
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 MakeNativeCavesSpine(const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1>& rooms,
|
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1>& edges,
|
|
std::string& error)
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
rooms.clear();edges.clear();error.clear();
|
|
const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(),
|
|
[](const auto& value){return value.generatorKind=="Caves"
|
|
&&value.recipeId=="Core.Caves.PrimitiveField"&&value.recipeVersion==1;});
|
|
if(cave==layout.strates.end())
|
|
{error="native caves recipe has no version-1 cave strate";return false;}
|
|
std::vector<const ResolvedLayoutPassageV1*> mouths;
|
|
for(const auto& passage:layout.passages)
|
|
if(passage.lowerStrateIndex==cave->strateIndex)
|
|
mouths.push_back(&passage);
|
|
std::sort(mouths.begin(),mouths.end(),[](const auto* a,const auto* b)
|
|
{return a->stableId<b->stableId;});
|
|
if(mouths.size()!=2)
|
|
{
|
|
std::fprintf(stderr,"[CavesMouthBindingAudit] cave_strate=%d passages=%zu matching=%zu",
|
|
cave->strateIndex,layout.passages.size(),mouths.size());
|
|
for(const auto& passage:layout.passages)
|
|
std::fprintf(stderr," [%s upper=%d lower=%d]",passage.stableId.c_str(),
|
|
passage.upperStrateIndex,passage.lowerStrateIndex);
|
|
std::fprintf(stderr,"\n");
|
|
error="native caves spine fixture requires exactly two resolved arrival/departure mouths";return false;
|
|
}
|
|
const double micrometresPerSubvoxel=static_cast<double>(layout.coreVoxelSizeMicrometres)
|
|
/WorldAssemblySubvoxelsPerVoxel;
|
|
std::array<std::array<std::int64_t,3>,2> mouthMicrometres{};
|
|
for(std::size_t mouth=0;mouth<2;++mouth)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
mouthMicrometres[mouth][axis]=static_cast<std::int64_t>(std::llround(
|
|
mouths[mouth]->lowerMouthSubvoxels[axis]*micrometresPerSubvoxel));
|
|
const double x0=mouthMicrometres[0][0]/1000000.0;
|
|
const double y0=mouthMicrometres[0][1]/1000000.0;
|
|
const double x1=mouthMicrometres[1][0]/1000000.0;
|
|
const double y1=mouthMicrometres[1][1]/1000000.0;
|
|
const double floorZ=0.5*(mouthMicrometres[0][2]+mouthMicrometres[1][2])/1000000.0;
|
|
const double dx=x1-x0,dy=y1-y0;
|
|
const double length=std::hypot(dx,dy);
|
|
std::printf("[CavesResolvedMouths] arrival=%s xyz_m=(%.3f,%.3f,%.3f) departure=%s xyz_m=(%.3f,%.3f,%.3f) horizontal_separation_m=%.3f\n",
|
|
mouths[0]->stableId.c_str(),x0,y0,mouthMicrometres[0][2]/1e6,
|
|
mouths[1]->stableId.c_str(),x1,y1,mouthMicrometres[1][2]/1e6,length);
|
|
const double mouthZ0=mouthMicrometres[0][2]/1000000.0;
|
|
const double mouthZ1=mouthMicrometres[1][2]/1000000.0;
|
|
const std::array<double,2> startFloorZ={mouthZ0-0.75,mouthZ1-0.75};
|
|
double directionX=0.0,directionY=0.0;
|
|
for(const auto* passage:mouths)
|
|
{
|
|
if(passage->controlPoints.size()<2)
|
|
{error="native caves lower mouth has no resolved passage tangent";return false;}
|
|
const auto last=std::min(passage->lowerMouthControlIndex,
|
|
passage->controlPoints.size()-1);
|
|
const auto previous=last>0?last-1:passage->controlPoints.size()-2;
|
|
const double tx=static_cast<double>(passage->controlPoints[last].positionSubvoxels[0]
|
|
-passage->controlPoints[previous].positionSubvoxels[0]);
|
|
const double ty=static_cast<double>(passage->controlPoints[last].positionSubvoxels[1]
|
|
-passage->controlPoints[previous].positionSubvoxels[1]);
|
|
const double tangentLength=std::hypot(tx,ty);
|
|
if(!(tangentLength>1.0e-9))
|
|
{error="native caves lower mouth has a degenerate passage tangent";return false;}
|
|
directionX+=tx/tangentLength;directionY+=ty/tangentLength;
|
|
}
|
|
const double directionLength=std::hypot(directionX,directionY);
|
|
if(!(directionLength>1.0e-9))
|
|
{error="native caves lower mouths have opposing passage tangents";return false;}
|
|
directionX/=directionLength;directionY/=directionLength;
|
|
if(std::hypot(length,mouthZ1-mouthZ0)<4.0)
|
|
{error="native caves arrival and departure mouths resolve to the same location";return false;}
|
|
|
|
ResolvedCavernRoomV1 room;
|
|
room.stableId="caves/spine/chamber-v1";room.strateIndex=cave->strateIndex;
|
|
const double roomOffset=std::max(32.0,
|
|
std::max(std::abs(floorZ-startFloorZ[0]),
|
|
std::abs(floorZ-startFloorZ[1]))/0.2679491924311227+27.0);
|
|
const double perpendicularX=-directionY;
|
|
const double perpendicularY=directionX;
|
|
room.centerMicrometres={static_cast<std::int64_t>(std::llround(
|
|
((x0+x1)*0.5+directionX*roomOffset)*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround(
|
|
((y0+y1)*0.5+directionY*roomOffset)*1000000.0)),
|
|
static_cast<std::int64_t>(std::llround((floorZ+3.5)*1000000.0))};
|
|
room.halfExtentsMicrometres={7000000,6500000,4500000};
|
|
room.floorMicrometres=static_cast<std::int64_t>(std::llround(floorZ*1000000.0));
|
|
room.smoothWidthMicrometres=2400000;
|
|
rooms.push_back(room);
|
|
|
|
for(std::size_t route=0;route<2;++route)
|
|
{
|
|
const auto& mouth=mouthMicrometres[route];
|
|
const auto* passage=mouths[route];
|
|
const auto tangentLast=std::min(passage->lowerMouthControlIndex,
|
|
passage->controlPoints.size()-1);
|
|
const auto tangentPrevious=tangentLast>0?tangentLast-1:
|
|
passage->controlPoints.size()-2;
|
|
const double tangentX=static_cast<double>(
|
|
passage->controlPoints[tangentLast].positionSubvoxels[0]
|
|
-passage->controlPoints[tangentPrevious].positionSubvoxels[0]);
|
|
const double tangentY=static_cast<double>(
|
|
passage->controlPoints[tangentLast].positionSubvoxels[1]
|
|
-passage->controlPoints[tangentPrevious].positionSubvoxels[1]);
|
|
const double tangentLength=std::hypot(tangentX,tangentY);
|
|
if(!(tangentLength>1.0e-9))
|
|
{error="native caves lower-mouth overlap has a degenerate passage tangent";return false;}
|
|
constexpr double mouthOverlap=3.0;
|
|
const double startX=mouth[0]/1000000.0+tangentX/tangentLength*mouthOverlap;
|
|
const double startY=mouth[1]/1000000.0+tangentY/tangentLength*mouthOverlap;
|
|
const double roomX=room.centerMicrometres[0]/1000000.0;
|
|
const double roomY=room.centerMicrometres[1]/1000000.0;
|
|
const double toRoomX=roomX-startX,toRoomY=roomY-startY;
|
|
const double toRoomLength=std::hypot(toRoomX,toRoomY);
|
|
if(!(toRoomLength>5.0))
|
|
{error="native caves mouth does not clear the chamber portal";return false;}
|
|
const double portalSide=route==0?-3.5:3.5;
|
|
const double portalX=roomX+perpendicularX*portalSide;
|
|
const double portalY=roomY+perpendicularY*portalSide;
|
|
const double endZ=floorZ;
|
|
const double portalRun=std::hypot(portalX-startX,portalY-startY);
|
|
const double edgeStartFloorZ=startFloorZ[route]
|
|
+(endZ-startFloorZ[route])*mouthOverlap/(portalRun+mouthOverlap);
|
|
const double verticalRun=std::abs(endZ-edgeStartFloorZ);
|
|
const double maximumGrade=verticalRun/portalRun;
|
|
if(maximumGrade>0.2679492)
|
|
{error="native caves spine segment exceeds the 15 degree walk grade";return false;}
|
|
const double edgeX=portalX-startX,edgeY=portalY-startY;
|
|
ResolvedCavernEdgeV1 edge;
|
|
edge.stableId=route==0?"caves/spine/arrival-edge-v1"
|
|
:"caves/spine/departure-edge-v1";
|
|
edge.startAnchorStableId=mouths[route]->stableId+"/lower-mouth";
|
|
edge.endRoomStableId=room.stableId;edge.strateIndex=cave->strateIndex;
|
|
// Match the resolved lower-mouth opening so the native spine does not
|
|
// create a sudden shoulder where the legacy passage ends.
|
|
edge.boreRadiusMicrometres=4500000;
|
|
edge.boreHeightMicrometres=4500000;
|
|
edge.floorSupportDepthMicrometres=0;
|
|
edge.smoothWidthMicrometres=2400000;
|
|
const std::array<std::array<double,3>,4> points={{
|
|
{startX,startY,edgeStartFloorZ},
|
|
{startX+edgeX/3.0,startY+edgeY/3.0,
|
|
edgeStartFloorZ+(endZ-edgeStartFloorZ)/3.0},
|
|
{startX+2.0*edgeX/3.0,startY+2.0*edgeY/3.0,
|
|
edgeStartFloorZ+2.0*(endZ-edgeStartFloorZ)/3.0},
|
|
{portalX,portalY,endZ}}};
|
|
for(std::size_t point=0;point<points.size();++point)
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
edge.controlPointsMicrometres[point][axis]=static_cast<std::int64_t>(
|
|
std::llround(points[point][axis]*1000000.0));
|
|
edges.push_back(std::move(edge));
|
|
}
|
|
std::printf("[CavesSpinePlan] recipe=Core.Caves.PrimitiveField version=1 density_sign=negative_solid_positive_air arrival=%s departure=%s room=%s edges=%zu bore_radius_m=4.500 bore_height_m=4.500 mouth_overlap_m=3.000 horizontal_separation_m=%.3f heading=(%.3f,%.3f) room_offset_m=%.3f lower_mouth_floor_offset_m=-0.750 floor_blend_width_m=2.400000 ideal_fillet_radius_m=0.848528 grade_limit_deg=15.000 shared_layer=shared-bedrock status=PASS\n",
|
|
mouths[0]->stableId.c_str(),mouths[1]->stableId.c_str(),room.stableId.c_str(),
|
|
edges.size(),length,directionX,directionY,roomOffset);
|
|
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"
|
|
" --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n"
|
|
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
|
" --seed=N --recipe=surface-world|floating-islands|caves\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-heightfield-profile (brute-force the profiled field intervals and route)\n"
|
|
" --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n"
|
|
" --audit-native-caves (arrival-to-departure capsule flood fill and interval oracle)\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=="--audit-heightfield-profile")options.AuditHeightfieldProfile=true;
|
|
else if(arg.starts_with("--world-assembler-manifest-output="))
|
|
options.WorldAssemblyManifestOutputPath=std::filesystem::path(std::string(
|
|
arg.substr(std::string_view("--world-assembler-manifest-output=").size())));
|
|
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-native-caves")options.AuditNativeCaves=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;options.CavesRecipe=false;}
|
|
else if(recipe=="floating-islands")
|
|
{options.FloatingIslandsRecipe=true;options.CavesRecipe=false;}
|
|
else if(recipe=="caves")
|
|
{options.FloatingIslandsRecipe=false;options.CavesRecipe=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.CavesRecipe?"caves"
|
|
: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,options.WorldAssemblyManifestOutputPath.string(),&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;}
|
|
if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly))
|
|
{
|
|
std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n");
|
|
return 2;
|
|
}
|
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
|
{
|
|
const std::string_view variant=options.CavesRecipe?"caves"
|
|
: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.CavesRecipe)
|
|
{
|
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
|
if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error)
|
|
||!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
|
resolvedLayout,seed,rooms,edges,error))
|
|
{std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;}
|
|
}
|
|
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.CavesRecipe?std::string_view("caves")
|
|
: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 bool hasNativeCavesRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
|
[](const auto& stratum){return stratum.generatorKind=="Caves"
|
|
&&stratum.recipeId=="Core.Caves.PrimitiveField"&&stratum.recipeVersion==1
|
|
&&stratum.passageCapabilities.hasNativeCaveField;});
|
|
const auto actualRecipe=hasNativeCavesRecipe?std::string_view("caves")
|
|
: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,options.AuditNativeCaves);
|
|
}
|