- Export commandlet writes the live Core adapter's world_assembly + resolved layout pair; the headless owner path imports that pair (manifest/layout equal, layout_chunk_width_q validated). owner_world.json re-exported from the owner's staged assets on the Linux host. - Adapter SW route: Core clearance/radius floors, 0.5 m support search, outward-facing tie-break; passage mouth/mid radius floors match Core (36/24 owner voxels). - FI in Unreal now uses the legacy (compiled analytic) generator unless a plan is imported: the auto native-FI plan failed the passage audit on the owner plan (1843 blocked samples, 945 open-air violations). Owner decides whether native FI returns. - In-engine spawn traces per strate (3 each): SW GenerateTile 42.7 worker-s vs 20.1 headless for equal tile counts; LOD0 p95 45-169 ms in UE. - Fast tier green; all three passages pass on the shared plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2219 lines
135 KiB
C++
2219 lines
135 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 InstrumentTileDrain=false;
|
|
bool ExactMidpointDensityMemo=false;
|
|
bool ActiveLaneBatching=false;
|
|
bool ParallelSignEdgeRepair=false;
|
|
bool HasSignEdgeRepairOverride=false;
|
|
int ProofBlockCells=8;
|
|
int ShapeHelperLimit=8;
|
|
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 LatticeRecipe=false;
|
|
int LatticeStrateIndex=-1;
|
|
bool AuditHeightfieldProfile=false;
|
|
bool LandmarkChamberDemo=false;
|
|
bool LandmarkL45RampControl=false;
|
|
bool OreVeinsDemo=false;
|
|
bool AuditOreVeinYield=false;
|
|
bool AuditLandmarkReachability=false;
|
|
bool AuditNativeCaves=false;
|
|
bool AuditNativeLattice=false;
|
|
bool LatticeOrphanControl=false;
|
|
bool AuditMotifIntervals=false;
|
|
bool TlsProbeRequested=false;
|
|
int TlsProbeWorkers=0;
|
|
std::string TlsProbeTouch="none";
|
|
bool HasCamera=false;
|
|
bool HasCameraStep=false;
|
|
bool AutoSurfaceCamera=false;
|
|
bool CaptureSkipWalkability=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 MakeNativeLatticeSpine(const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
|
std::int32_t seed,std::int32_t latticeStrateIndex,bool orphanControl,
|
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1>& rooms,
|
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1>& edges,std::string& error)
|
|
{
|
|
using namespace VoxelForgeCore;
|
|
rooms.clear();edges.clear();error.clear();
|
|
const auto lattice=std::find_if(layout.strates.begin(),layout.strates.end(),
|
|
[latticeStrateIndex](const auto& value)
|
|
{return value.strateIndex==latticeStrateIndex
|
|
&&IsNativeLatticeFieldStratum(value);});
|
|
if(lattice==layout.strates.end())
|
|
{error="native lattice recipe does not name an opt-in Maze or VerticalShafts strate";return false;}
|
|
|
|
std::vector<const ResolvedLayoutPassageV1*> arrivals,departures;
|
|
for(const auto& passage:layout.passages)
|
|
{
|
|
if(passage.lowerStrateIndex==latticeStrateIndex)arrivals.push_back(&passage);
|
|
if(passage.upperStrateIndex==latticeStrateIndex)departures.push_back(&passage);
|
|
}
|
|
const auto byStableId=[](const auto* left,const auto* right)
|
|
{return left->stableId<right->stableId;};
|
|
std::sort(arrivals.begin(),arrivals.end(),byStableId);
|
|
std::sort(departures.begin(),departures.end(),byStableId);
|
|
if(arrivals.size()!=1||departures.size()!=1)
|
|
{error="native lattice spine requires exactly one resolved arrival and one departure mouth";return false;}
|
|
if(layout.coreVoxelSizeMicrometres==0)
|
|
{error="native lattice spine requires a resolved Core voxel size";return false;}
|
|
|
|
const double micrometresPerSubvoxel=static_cast<double>(layout.coreVoxelSizeMicrometres)
|
|
/WorldAssemblySubvoxelsPerVoxel;
|
|
const auto toMicrometres=[&](const std::array<std::int64_t,3>& point)
|
|
{
|
|
std::array<std::int64_t,3> result{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
result[axis]=static_cast<std::int64_t>(std::llround(
|
|
point[axis]*micrometresPerSubvoxel));
|
|
return result;
|
|
};
|
|
const auto toMetres=[](const std::array<std::int64_t,3>& point)
|
|
{return std::array<double,3>{point[0]/1.0e6,point[1]/1.0e6,point[2]/1.0e6};};
|
|
const auto micronPoint=[](const std::array<double,3>& point)
|
|
{
|
|
std::array<std::int64_t,3> result{};
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
result[axis]=static_cast<std::int64_t>(std::llround(point[axis]*1.0e6));
|
|
return result;
|
|
};
|
|
const auto tangentAt=[&](const ResolvedLayoutPassageV1& passage,bool lower,
|
|
std::array<double,2>& direction)
|
|
{
|
|
if(passage.controlPoints.size()<2)return false;
|
|
const std::size_t index=lower?std::min(passage.lowerMouthControlIndex,
|
|
passage.controlPoints.size()-1):0;
|
|
const std::size_t other=lower?(index>0?index-1:passage.controlPoints.size()-2)
|
|
:std::min<std::size_t>(1,passage.controlPoints.size()-1);
|
|
const auto& a=passage.controlPoints[lower?other:index].positionSubvoxels;
|
|
const auto& b=passage.controlPoints[lower?index:other].positionSubvoxels;
|
|
const double dx=static_cast<double>(b[0]-a[0]);
|
|
const double dy=static_cast<double>(b[1]-a[1]);
|
|
const double length=std::hypot(dx,dy);
|
|
if(!(length>1.0e-9))return false;
|
|
direction={dx/length,dy/length};
|
|
return true;
|
|
};
|
|
std::array<double,2> arrivalTangent{},departureTangent{};
|
|
if(!tangentAt(*arrivals.front(),true,arrivalTangent)
|
|
||!tangentAt(*departures.front(),false,departureTangent))
|
|
{error="native lattice mouth has no finite resolved passage tangent";return false;}
|
|
const auto arrival=toMetres(toMicrometres(arrivals.front()->lowerMouthSubvoxels));
|
|
const auto departure=toMetres(toMicrometres(departures.front()->upperMouthSubvoxels));
|
|
constexpr double MouthPlayerCenterHeight=1.13;
|
|
const double arrivalFloor=arrival[2]-MouthPlayerCenterHeight;
|
|
const double departureFloor=departure[2]-MouthPlayerCenterHeight;
|
|
const std::array<double,3> helixStart={arrival[0]+arrivalTangent[0]*3.0,
|
|
arrival[1]+arrivalTangent[1]*3.0,arrivalFloor};
|
|
constexpr double Pi=3.1415926535897932384626433832795;
|
|
constexpr double CenterlineRadius=6.0;
|
|
constexpr double ClearWidth=3.0;
|
|
constexpr double FloorToSoffit=2.6;
|
|
constexpr double Pitch=5.0;
|
|
constexpr int Turns=4;
|
|
constexpr double FloorBlend=1.9;
|
|
constexpr double FeatureUnionBlend=0.05;
|
|
constexpr double CorridorSpacing=14.0;
|
|
constexpr double MazeCellSpan=16.0;
|
|
constexpr double RouteGradeLimit=13.0;
|
|
const std::array<double,2> lateral={-arrivalTangent[1],arrivalTangent[0]};
|
|
const std::array<double,2> helixCenter={
|
|
helixStart[0]+lateral[0]*CenterlineRadius,
|
|
helixStart[1]+lateral[1]*CenterlineRadius};
|
|
const double helixEndFloor=arrivalFloor-Pitch*Turns;
|
|
const std::array<double,3> helixEnd={helixStart[0],helixStart[1],helixEndFloor};
|
|
const double mouthEndX=departure[0]-departureTangent[0]*3.0;
|
|
const double mouthEndY=departure[1]-departureTangent[1]*3.0;
|
|
|
|
const auto featureRoll=[&](std::string_view stableId)
|
|
{
|
|
std::uint64_t hash=14695981039346656037ull;
|
|
const auto addWord=[&](std::uint64_t word)
|
|
{for(int byte=0;byte<8;++byte){hash^=static_cast<std::uint8_t>(word>>(byte*8));hash*=1099511628211ull;}};
|
|
addWord(static_cast<std::uint32_t>(seed));
|
|
addWord(static_cast<std::uint32_t>(latticeStrateIndex));
|
|
addWord(1);
|
|
for(unsigned char value:stableId){hash^=value;hash*=1099511628211ull;}
|
|
hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull;
|
|
hash^=hash>>27;hash*=0x94d049bb133111ebull;
|
|
return hash^(hash>>31);
|
|
};
|
|
const auto addRoom=[&](std::string id,const std::array<double,3>& floorCenter)
|
|
{
|
|
ResolvedCavernRoomV1 room;room.stableId=std::move(id);
|
|
room.strateIndex=latticeStrateIndex;
|
|
room.centerMicrometres=micronPoint({floorCenter[0],floorCenter[1],
|
|
floorCenter[2]+FloorToSoffit*0.5});
|
|
room.halfExtentsMicrometres={2000000,2000000,
|
|
static_cast<std::int64_t>(std::llround(FloorToSoffit*500000.0))};
|
|
room.floorMicrometres=static_cast<std::int64_t>(std::llround(floorCenter[2]*1.0e6));
|
|
room.smoothWidthMicrometres=static_cast<std::int64_t>(std::llround(FloorBlend*1.0e6));
|
|
room.unionWidthMicrometres=static_cast<std::int64_t>(
|
|
std::llround(FeatureUnionBlend*1.0e6));
|
|
room.irregularitySeed=featureRoll(room.stableId);
|
|
rooms.push_back(std::move(room));
|
|
};
|
|
const auto addEdge=[&](std::string id,std::string startAnchor,std::string endRoom,
|
|
std::vector<std::array<double,3>> path)
|
|
{
|
|
ResolvedCavernEdgeV1 edge;edge.stableId=std::move(id);
|
|
edge.startAnchorStableId=std::move(startAnchor);
|
|
edge.endRoomStableId=std::move(endRoom);edge.strateIndex=latticeStrateIndex;
|
|
edge.boreRadiusMicrometres=1500000;
|
|
edge.boreHeightMicrometres=static_cast<std::int64_t>(
|
|
std::llround(FloorToSoffit*1.0e6));
|
|
edge.floorSupportDepthMicrometres=1000000;
|
|
edge.smoothWidthMicrometres=static_cast<std::int64_t>(std::llround(FloorBlend*1.0e6));
|
|
edge.unionWidthMicrometres=static_cast<std::int64_t>(
|
|
std::llround(FeatureUnionBlend*1.0e6));
|
|
edge.irregularitySeed=featureRoll(edge.stableId);
|
|
if(path.size()<2)return edge;
|
|
for(const auto& point:path)edge.polylineMicrometres.push_back(micronPoint(point));
|
|
const auto& first=path.front();const auto& last=path.back();
|
|
for(std::size_t control=0;control<4;++control)
|
|
{
|
|
const double t=static_cast<double>(control)/3.0;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
edge.controlPointsMicrometres[control][axis]=static_cast<std::int64_t>(
|
|
std::llround(std::lerp(first[axis],last[axis],t)*1.0e6));
|
|
}
|
|
return edge;
|
|
};
|
|
|
|
const std::string arrivalRoomId="lattice/spine/arrival-room-v1";
|
|
const std::string mazeCellZeroId="lattice/spine/maze-cell-00-v1";
|
|
const std::string helixEntryRoomId=orphanControl
|
|
?"lattice/control/orphan-shaft-entry-room-v1"
|
|
:arrivalRoomId;
|
|
const std::string helixEndRoomId=orphanControl
|
|
?"lattice/control/orphan-shaft-end-room-v1":mazeCellZeroId;
|
|
const std::array<double,3> orphanOffset=orphanControl
|
|
?std::array<double,3>{lateral[0]*20.0,lateral[1]*20.0,0.0}
|
|
:std::array<double,3>{0.0,0.0,0.0};
|
|
addRoom(arrivalRoomId,helixStart);
|
|
if(orphanControl)
|
|
addRoom(helixEntryRoomId,{helixStart[0]+orphanOffset[0],
|
|
helixStart[1]+orphanOffset[1],helixStart[2]});
|
|
addRoom(mazeCellZeroId,helixEnd);
|
|
if(orphanControl)
|
|
addRoom(helixEndRoomId,{helixEnd[0]+orphanOffset[0],
|
|
helixEnd[1]+orphanOffset[1],helixEnd[2]});
|
|
const std::string departureRoomId="lattice/spine/departure-room-v1";
|
|
addRoom(departureRoomId,{departure[0],departure[1],departureFloor});
|
|
edges.push_back(addEdge("lattice/spine/00-arrival-portal-v1",
|
|
arrivals.front()->stableId+"/lower-mouth",arrivalRoomId,
|
|
{{arrival[0],arrival[1],arrivalFloor},helixStart}));
|
|
std::vector<std::array<double,3>> helixPath;
|
|
constexpr double HelixSampleArcLength=0.5;
|
|
const double helixLength=std::hypot(2.0*Pi*CenterlineRadius*Turns,
|
|
Pitch*Turns);
|
|
const int helixSteps=std::max(64,static_cast<int>(std::ceil(
|
|
helixLength/HelixSampleArcLength)));
|
|
helixPath.reserve(static_cast<std::size_t>(helixSteps)+1);
|
|
for(int sample=0;sample<=helixSteps;++sample)
|
|
{
|
|
const double u=static_cast<double>(sample)/helixSteps;
|
|
const double angle=2.0*Pi*Turns*u;
|
|
helixPath.push_back({
|
|
helixCenter[0]-lateral[0]*CenterlineRadius*std::cos(angle)
|
|
+arrivalTangent[0]*CenterlineRadius*std::sin(angle)+orphanOffset[0],
|
|
helixCenter[1]-lateral[1]*CenterlineRadius*std::cos(angle)
|
|
+arrivalTangent[1]*CenterlineRadius*std::sin(angle)+orphanOffset[1],
|
|
arrivalFloor-Pitch*Turns*u});
|
|
}
|
|
edges.push_back(addEdge("lattice/spine/01-helix-ramp-v1",
|
|
helixEntryRoomId,helixEndRoomId,std::move(helixPath)));
|
|
|
|
double dx=mouthEndX-helixEnd[0],dy=mouthEndY-helixEnd[1];
|
|
double horizontal=std::hypot(dx,dy);
|
|
double routeX=horizontal>1.0e-9?dx/horizontal:arrivalTangent[0];
|
|
double routeY=horizontal>1.0e-9?dy/horizontal:arrivalTangent[1];
|
|
double routeSideX=-routeY,routeSideY=routeX;
|
|
const double vertical=departureFloor-helixEndFloor;
|
|
const int routeEdges=4;
|
|
const double requiredRun=std::abs(vertical)/std::tan(RouteGradeLimit*Pi/180.0);
|
|
const double sideSpan=std::max(MazeCellSpan,
|
|
(requiredRun-horizontal)/3.0);
|
|
if(sideSpan>24.0)
|
|
{error="native lattice maze spine requires more than 24 m cells to preserve its grade";return false;}
|
|
std::vector<std::array<double,2>> mazeXY={
|
|
{helixEnd[0],helixEnd[1]},
|
|
{helixEnd[0]+routeX*sideSpan,helixEnd[1]+routeY*sideSpan},
|
|
{helixEnd[0]+routeX*sideSpan+routeSideX*sideSpan,
|
|
helixEnd[1]+routeY*sideSpan+routeSideY*sideSpan},
|
|
{mouthEndX+routeSideX*sideSpan,mouthEndY+routeSideY*sideSpan},
|
|
{mouthEndX,mouthEndY}};
|
|
std::array<double,5> routeDistance{};
|
|
for(std::size_t index=1;index<mazeXY.size();++index)
|
|
routeDistance[index]=routeDistance[index-1]
|
|
+std::hypot(mazeXY[index][0]-mazeXY[index-1][0],
|
|
mazeXY[index][1]-mazeXY[index-1][1]);
|
|
const double mazeRun=routeDistance.back();
|
|
if(!(mazeRun>=requiredRun))
|
|
{error="native lattice maze spine cannot satisfy the 13 degree grade limit";return false;}
|
|
std::vector<std::array<double,3>> cells(mazeXY.size());
|
|
for(std::size_t index=0;index<mazeXY.size();++index)
|
|
cells[index]={mazeXY[index][0],mazeXY[index][1],
|
|
helixEndFloor+vertical*(routeDistance[index]/mazeRun)};
|
|
std::vector<std::string> cellIds(static_cast<std::size_t>(routeEdges)+1);
|
|
cellIds[0]=mazeCellZeroId;
|
|
for(int index=1;index<=routeEdges;++index)
|
|
{
|
|
char suffix[16]{};std::snprintf(suffix,sizeof(suffix),"%02d",index);
|
|
cellIds[static_cast<std::size_t>(index)]=std::string("lattice/spine/maze-cell-")
|
|
+suffix+"-v1";
|
|
addRoom(cellIds[static_cast<std::size_t>(index)],cells[static_cast<std::size_t>(index)]);
|
|
}
|
|
for(int index=0;index<routeEdges;++index)
|
|
{
|
|
char suffix[16]{};std::snprintf(suffix,sizeof(suffix),"%02d",index);
|
|
edges.push_back(addEdge("lattice/spine/maze-edge-"+std::string(suffix)+"-v1",
|
|
cellIds[static_cast<std::size_t>(index)],
|
|
cellIds[static_cast<std::size_t>(index+1)],
|
|
{cells[static_cast<std::size_t>(index)],cells[static_cast<std::size_t>(index+1)]}));
|
|
}
|
|
|
|
const int branchIndices[2]={1,routeEdges-1};
|
|
for(int branch=0;branch<2;++branch)
|
|
{
|
|
const int index=branchIndices[branch];
|
|
const auto& base=cells[static_cast<std::size_t>(index)];
|
|
const std::string roomId=branch==0?"lattice/branch/dead-end-a-room-v1"
|
|
:"lattice/branch/dead-end-b-room-v1";
|
|
const std::string edgeId=branch==0?"lattice/branch/dead-end-a-edge-v1"
|
|
:"lattice/branch/dead-end-b-edge-v1";
|
|
const int sign=branch==0?-1:1;
|
|
const std::array<double,3> branchEnd={base[0]+routeSideX*sign*CorridorSpacing,
|
|
base[1]+routeSideY*sign*CorridorSpacing,base[2]};
|
|
addRoom(roomId,branchEnd);
|
|
edges.push_back(addEdge(edgeId,cellIds[static_cast<std::size_t>(index)],
|
|
roomId,{base,branchEnd}));
|
|
}
|
|
edges.push_back(addEdge("lattice/spine/99-departure-portal-v1",
|
|
departures.front()->stableId+"/upper-mouth",departureRoomId,
|
|
{cells.back(),{departure[0],departure[1],departureFloor}}));
|
|
|
|
const double innerEdgeGrade=std::atan(Pitch/(2.0*Pi*(CenterlineRadius-ClearWidth*0.5)))
|
|
*180.0/Pi;
|
|
const double centerlineGrade=std::atan(Pitch/(2.0*Pi*CenterlineRadius))*180.0/Pi;
|
|
std::printf("[LatticeResolvedMouths] arrival=%s side=lower xyz_m=(%.3f,%.3f,%.3f) departure=%s side=upper xyz_m=(%.3f,%.3f,%.3f) binding=actual-resolved status=PASS\n",
|
|
arrivals.front()->stableId.c_str(),arrival[0],arrival[1],arrival[2],
|
|
departures.front()->stableId.c_str(),departure[0],departure[1],departure[2]);
|
|
std::printf("[LatticeSpinePlan] recipe=Core.Lattice.PrimitiveField version=1 strate=%d stable_key=seed,strate,recipe_version,stable_id spine_first=true arrival=%s departure=%s helix_id=lattice/spine/01-helix-ramp-v1 centerline_radius_m=%.3f clear_width_m=%.3f pitch_m=%.3f turns=%d floor_to_soffit_m=%.3f centerline_grade_deg=%.3f inner_edge_grade_deg=%.3f floor_blend_m=%.3f union_envelope_m=%.3f maze_cells=%d dead_ends=2 orphan_control=%s status=PASS\n",
|
|
latticeStrateIndex,arrivals.front()->stableId.c_str(),departures.front()->stableId.c_str(),
|
|
CenterlineRadius,ClearWidth,Pitch,Turns,FloorToSoffit,centerlineGrade,innerEdgeGrade,
|
|
FloorBlend,FeatureUnionBlend,routeEdges+1,orphanControl?"enabled":"disabled");
|
|
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"
|
|
" --capture-skip-walkability (mesh export only; defer the owner-wide passage gate)\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|lattice\n"
|
|
" --lattice-strate=N (opt one Maze or VerticalShafts strate into Core.Lattice)\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-motif-intervals (brute-force motif bounds, BVH and shape-free guards)\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-native-lattice (mouth-to-mouth capsule, grade, support and interval proof)\n"
|
|
" --lattice-orphan-control (disconnect a required edge for the negative control)\n"
|
|
" --audit-surface-clearance (sample biome terrain/caps against the resolved ceiling)\n"
|
|
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
|
" --mesh-coverage (report non-empty tiles and indexed triangles)\n"
|
|
" --screen-space-gate (measure display error and historical negative controls)\n"
|
|
" --audit-uniform-block-proofs (fill and check every interval-certified block)\n"
|
|
" --profile-lod-work (report per-LOD density sample counts)\n"
|
|
" --instrument-tile-drain (opt-in CpuTileTimeline and per-tile stages/counters)\n"
|
|
" --exact-midpoint-density-memo (exact-input midpoint query memo A/B)\n"
|
|
" --active-lane-batching --proof-block-cells=N (proof-masked density fill A/B)\n"
|
|
" --parallel-sign-edge-repair --serial-sign-edge-repair\n"
|
|
" --shape-helper-limit=8|12 (default 8; shape build A/B)\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=="--audit-motif-intervals")options.AuditMotifIntervals=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-native-lattice")options.AuditNativeLattice=true;
|
|
else if(arg=="--lattice-orphan-control")options.LatticeOrphanControl=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=="--instrument-tile-drain")options.InstrumentTileDrain=true;
|
|
else if(arg=="--exact-midpoint-density-memo")options.ExactMidpointDensityMemo=true;
|
|
else if(arg=="--active-lane-batching")options.ActiveLaneBatching=true;
|
|
else if(arg=="--parallel-sign-edge-repair")
|
|
{options.ParallelSignEdgeRepair=true;options.HasSignEdgeRepairOverride=true;}
|
|
else if(arg=="--serial-sign-edge-repair")
|
|
{options.ParallelSignEdgeRepair=false;options.HasSignEdgeRepairOverride=true;}
|
|
else if(arg.starts_with("--proof-block-cells="))
|
|
{
|
|
if(!readInt("--proof-block-cells=",options.ProofBlockCells)
|
|
||options.ProofBlockCells<1||options.ProofBlockCells>32)
|
|
{std::fprintf(stderr,"--proof-block-cells must be in 1..32\n");return false;}
|
|
}
|
|
else if(arg.starts_with("--shape-helper-limit="))
|
|
{
|
|
if(!readInt("--shape-helper-limit=",options.ShapeHelperLimit)
|
|
||(options.ShapeHelperLimit!=8&&options.ShapeHelperLimit!=12))
|
|
{std::fprintf(stderr,"--shape-helper-limit must be 8 or 12\n");return false;}
|
|
}
|
|
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;options.LatticeRecipe=false;}
|
|
else if(recipe=="floating-islands")
|
|
{options.FloatingIslandsRecipe=true;options.CavesRecipe=false;options.LatticeRecipe=false;}
|
|
else if(recipe=="caves")
|
|
{options.FloatingIslandsRecipe=false;options.CavesRecipe=true;options.LatticeRecipe=false;}
|
|
else if(recipe=="lattice")
|
|
{options.FloatingIslandsRecipe=false;options.CavesRecipe=false;options.LatticeRecipe=true;}
|
|
else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;}
|
|
}
|
|
else if(arg.starts_with("--lattice-strate="))
|
|
{
|
|
if(!readInt("--lattice-strate=",options.LatticeStrateIndex)
|
|
||options.LatticeStrateIndex<0)
|
|
{std::fprintf(stderr,"--lattice-strate must be a non-negative strate index\n");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=="--capture-skip-walkability")options.CaptureSkipWalkability=true;
|
|
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.LatticeRecipe&&options.LatticeStrateIndex<0)
|
|
{std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return false;}
|
|
if(options.CaptureSkipWalkability&&options.MeshExportPath.empty())
|
|
{std::fprintf(stderr,"--capture-skip-walkability requires --mesh-export=FILE\n");return false;}
|
|
if((options.AuditNativeLattice||options.LatticeOrphanControl)&&!options.LatticeRecipe)
|
|
{std::fprintf(stderr,"native lattice audits require --recipe=lattice\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 lattice_strate=%d shared_column_cache=%s camera_step_m=(%.3f,%.3f,%.3f) camera_override=%s camera_m=(%.6f,%.6f,%.6f) camera_rotation_deg=(%.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.LatticeRecipe?"lattice"
|
|
:options.FloatingIslandsRecipe?"floating-islands":"surface-world",
|
|
options.LatticeStrateIndex,
|
|
options.UseSharedColumnCache?"on":"off",options.CameraStepMeters.X,options.CameraStepMeters.Y,
|
|
options.CameraStepMeters.Z,options.HasCamera?"yes":"no",options.CameraMeters.X,
|
|
options.CameraMeters.Y,options.CameraMeters.Z,options.CameraYawDegrees,
|
|
options.CameraPitchDegrees,
|
|
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;}
|
|
const bool hasExportedAssembly=ownerJsonRoot.Get("core_world_assembly_json").IsString();
|
|
const bool hasExportedLayout=ownerJsonRoot.Get("resolved_layout_json").IsString();
|
|
if(hasExportedAssembly!=hasExportedLayout)
|
|
{std::fprintf(stderr,"Owner export has only half of its Unreal-resolved Core plan.\n");return 2;}
|
|
const bool useExportedCorePlan=hasExportedAssembly
|
|
&&!options.HasSeedOverride
|
|
&&!options.FloatingIslandsRecipe
|
|
&&!options.CavesRecipe
|
|
&&!options.LatticeRecipe
|
|
&&options.TransitionModeOverride<0
|
|
&&options.TransitionWidthChunksOverride<0;
|
|
ownerWorld->bHasUnrealResolvedCorePlan=useExportedCorePlan;
|
|
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(useExportedCorePlan)
|
|
{
|
|
if(!StandaloneWorldAssemblyPrototype::ImportExportedCoreWorldAssembly(
|
|
ownerJsonRoot,worldAssembly,error))
|
|
{std::fprintf(stderr,"Unreal-resolved Core plan import failed: %s\n",error.c_str());return 2;}
|
|
std::printf("[WorldAssemblerPlanSource] source=owner_export/unreal_core_adapter manifest_hash=%016llx layout_chunk_width_q=%lld surface_strates=%zu floating_islands=%zu\n",
|
|
static_cast<unsigned long long>(worldAssembly.contentHash),
|
|
static_cast<long long>(worldAssembly.recipe.layoutChunkWidthSubvoxels),
|
|
worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size());
|
|
}
|
|
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
|
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
|
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
|
if(!useExportedCorePlan&&options.FloatingIslandsRecipe
|
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
|
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
if(!options.WorldAssemblyManifestOutputPath.empty())
|
|
{
|
|
const std::string manifestText=worldAssembly.ToJson();
|
|
const auto& outputPath=options.WorldAssemblyManifestOutputPath;
|
|
std::error_code directoryError;
|
|
if(!outputPath.parent_path().empty())
|
|
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
|
if(directoryError)
|
|
{std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;}
|
|
std::ofstream output(outputPath,std::ios::binary);
|
|
if(!output)
|
|
{std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;}
|
|
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
|
if(!output)
|
|
{std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;}
|
|
std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n",
|
|
manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(),
|
|
outputPath.string().c_str());
|
|
}
|
|
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 variant=options.LatticeRecipe
|
|
?"lattice/"+std::to_string(options.LatticeStrateIndex)
|
|
:options.CavesRecipe?"caves"
|
|
:options.FloatingIslandsRecipe?"floating-islands":"surface-world";
|
|
const bool layoutResolved=useExportedCorePlan
|
|
?StandaloneWorldAssemblyPrototype::ImportExportedResolvedLayout(
|
|
ownerJsonRoot,resolvedLayout,error)
|
|
:StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
|
*ownerWorld,worldAssembly,variant,resolvedLayout,error);
|
|
if(!layoutResolved)
|
|
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
|
if(useExportedCorePlan
|
|
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
|
*ownerWorld,worldAssembly,resolvedLayout,error))
|
|
{std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;}
|
|
if(!std::holds_alternative<std::nullptr_t>(ownerJsonRoot.Get("motif_family_override").Data))
|
|
{
|
|
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
|
if(!StandaloneWorldAssemblyPrototype::BuildMotifFamilyRequests(ownerJsonRoot,
|
|
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
|
||!VoxelForgeCore::ResolveResolvedLayoutStampsV3(
|
|
resolvedLayout,seed,requests,error))
|
|
{std::fprintf(stderr,"Native motif family resolution failed: %s\n",error.c_str());return 2;}
|
|
std::printf("[ResolvedMotifFamily] vocabulary=%u records=%zu placement=seed,strate,stable_id route_keepout=PASS band_clearance=PASS pairwise_spacing=PASS status=PASS\n",
|
|
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size());
|
|
for(const auto& motif:resolvedLayout.stamps)
|
|
{
|
|
const char* shape=motif.kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillMonolith
|
|
?"rounded_capped_cylinder":"rounded_box_platform";
|
|
std::printf("[ResolvedMotif] id=%s strate=%d shape=%s position_m=(%.3f,%.3f,%.3f) bounds_m=[%.3f,%.3f]x[%.3f,%.3f]x[%.3f,%.3f] half_extents_m=(%.3f,%.3f,%.3f) fillet_radius_m=%.3f blend_width_m=%.3f minimum_spacing_m=%.3f floor_m=%.3f seed=%llu\n",
|
|
motif.stableId.c_str(),motif.strateIndex,shape,
|
|
motif.positionMicrometres[0]/1.0e6,motif.positionMicrometres[1]/1.0e6,
|
|
motif.positionMicrometres[2]/1.0e6,
|
|
motif.boundsMinimumMicrometres[0]/1.0e6,motif.boundsMaximumMicrometres[0]/1.0e6,
|
|
motif.boundsMinimumMicrometres[1]/1.0e6,motif.boundsMaximumMicrometres[1]/1.0e6,
|
|
motif.boundsMinimumMicrometres[2]/1.0e6,motif.boundsMaximumMicrometres[2]/1.0e6,
|
|
motif.halfExtentsMicrometres[0]/1.0e6,motif.halfExtentsMicrometres[1]/1.0e6,
|
|
motif.halfExtentsMicrometres[2]/1.0e6,
|
|
motif.roundingRadiusMicrometres/1.0e6,motif.smoothWidthMicrometres/1.0e6,
|
|
motif.minimumSpacingMicrometres/1.0e6,motif.floorMicrometres/1.0e6,
|
|
static_cast<unsigned long long>(motif.irregularitySeed));
|
|
}
|
|
}
|
|
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.LatticeRecipe)
|
|
{
|
|
if(options.LatticeStrateIndex<0)
|
|
{std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return 2;}
|
|
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
|
|
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
|
|
if(!MakeNativeLatticeSpine(resolvedLayout,seed,options.LatticeStrateIndex,
|
|
options.LatticeOrphanControl,rooms,edges,error)
|
|
||!VoxelForgeCore::ResolveResolvedCavernGraphV1(
|
|
resolvedLayout,seed,rooms,edges,error))
|
|
{std::fprintf(stderr,"Native lattice graph 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.LatticeRecipe?std::string_view("lattice")
|
|
: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 VoxelForgeCore::IsNativeCaveFieldStratum(stratum)
|
|
&&stratum.passageCapabilities.hasNativeCaveField;});
|
|
const bool hasNativeLatticeRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
|
[](const auto& stratum){return VoxelForgeCore::IsNativeLatticeFieldStratum(stratum)
|
|
&&stratum.passageCapabilities.hasNativeLatticeField;});
|
|
const auto actualRecipe=hasNativeLatticeRecipe?std::string_view("lattice")
|
|
:hasNativeCavesRecipe?std::string_view("caves")
|
|
:requestedRecipe=="surface-world"&&hasSurfaceWorldPlan
|
|
?std::string_view("surface-world")
|
|
:requestedRecipe=="floating-islands"&&hasFloatingIslands
|
|
?std::string_view("floating-islands")
|
|
: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(static_cast<std::size_t>(options.ShapeHelperLimit));
|
|
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,options.InstrumentTileDrain,
|
|
options.AuditMotifIntervals,
|
|
options.ExactMidpointDensityMemo,options.ActiveLaneBatching,
|
|
options.ProofBlockCells,options.HasSignEdgeRepairOverride
|
|
?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe,
|
|
options.AuditNativeLattice,options.LatticeOrphanControl,
|
|
options.CaptureSkipWalkability);
|
|
}
|