Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/l140-fi-around-tunnels.patch
T
Fr0zkaandClaude Opus 5.5 4f93f3fffb research: Floating Islands planned around fixed tunnels (L140, 0/16; kept as patch)
Native FI binds and its own route/clearance/mouth-support checks pass on seeds 0 and 10, but 14/16 seeds have no
island arrangement that clears the fixed passage tubes and reaches both mouth heights; seed 10 has one composed
collision; ripple 6-7 cm vs the 3.5 cm limit on both upper passages. Third failure of the 'islands adapt to fixed
tunnels' premise (L133 x2, L140).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-01 08:08:40 +01:00

1280 lines
72 KiB
Diff

diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp
index 5e66412..8d21685 100644
--- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp
+++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp
@@ -892,6 +892,7 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest,
+ const VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout,
double CoreVoxelSizeCm, std::int64_t LayoutChunkWidthSubvoxels, FString& OutError)
{
using namespace VoxelForgeCore;
@@ -903,7 +904,8 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
IslandPlan.topChunkZ = Slot.TopChunkZ;
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
auto& Recipe = IslandPlan.manifest.recipe;
- Recipe.geometryVersion = 8;
+ Recipe.geometryVersion = 10;
+ Recipe.primaryIslandCount = 9;
Recipe.terrainFollowingRoute = static_cast<std::uint32_t>(Passage.Style) == 0;
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
@@ -929,13 +931,24 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
/ Recipe.ownerToCoreScaleDenominator;
Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale);
Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale);
- Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale);
+ Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(28.0 * OwnerToCoreScale);
Recipe.thicknessRatioQ16 = WorldAssemblyQ16(Params.ThicknessRatio);
Recipe.verticalJitterQ16 = WorldAssemblyQ16(Params.VerticalJitter);
Recipe.topFlattenQ16 = WorldAssemblyQ16(Params.TopFlatten);
- Recipe.roughnessSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness * OwnerToCoreScale);
- Recipe.blendRadiusSubvoxels = WorldAssemblyFixed(Params.SDFBlendRadius * OwnerToCoreScale);
+ Recipe.roughnessSubvoxels = std::min(WorldAssemblyFixed(
+ Params.SurfaceRoughness * OwnerToCoreScale), WorldAssemblySubvoxelsPerVoxel / 2);
+ Recipe.blendRadiusSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
+ Recipe.contourWarpSubvoxels = WorldAssemblySubvoxelsPerVoxel;
Recipe.boundarySealSubvoxels = WorldAssemblyFixed(Params.BoundarySealThickness * OwnerToCoreScale);
+ Recipe.routeConstraints.version = 1;
+ Recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
+ Recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
+ Recipe.routeConstraints.maximumGradeQ16 = 17560;
+ Recipe.routeConstraints.sweepSpacingSubvoxels = 128;
+ Recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
+ Recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
+ Recipe.routeConstraints.minimumClearanceSubvoxels = 128;
+ Recipe.routeConstraints.minimumSupportProbes = 3;
const auto OwnerVoxelsToCoreQ=[&](double Value)
{
return static_cast<std::int64_t>(std::llround(Value*Recipe.ownerVoxelSizeMicrons
@@ -944,40 +957,21 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1;
Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels(
Slot.PassageDistanceMaximumMicrometres, Recipe.coreVoxelSizeMicrons);
- const std::int64_t spiralRadius=WorldAssemblyMicrometresToSubvoxels(
- Slot.PassageSpiralRadiusMicrometres, Recipe.coreVoxelSizeMicrons);
- // Reserve at least a 1.5 m mid-bore through the anchor root.
- constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0;
- const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons;
- const std::int64_t mouthBore=std::max(
- WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres,
- Recipe.coreVoxelSizeMicrons),
- WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres,
- Recipe.coreVoxelSizeMicrons));
- const std::int64_t bore=std::max(
- WorldAssemblyMicrometresToSubvoxels(Slot.PassageMidRadiusMicrometres,
- Recipe.coreVoxelSizeMicrons),
- WorldAssemblyMicrometresToSubvoxels(
- static_cast<std::int64_t>(AnchorPassageMinimumBoreOwnerVoxels * OwnerVoxelMicrometres),
- Recipe.coreVoxelSizeMicrons));
- const std::int64_t wallReserve=WorldAssemblyMicrometresToSubvoxels(
- 12 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
- const std::int64_t FourOwnerVoxelsSubvoxels=WorldAssemblyMicrometresToSubvoxels(
- 4 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
- Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+FourOwnerVoxelsSubvoxels;
- const std::int64_t excursion=std::max(
- WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres,
- Recipe.coreVoxelSizeMicrons), spiralRadius);
- Recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve;
- Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels,
- Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels);
+ Recipe.anchorMouthOffsetSubvoxels = OwnerVoxelsToCoreQ(4.0);
+ Recipe.anchorRootRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(16.0),
+ Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels
+ + WorldAssemblySubvoxelsPerVoxel);
+ Recipe.anchorIslandMinimumRadiusSubvoxels = std::max(Recipe.maximumRadiusSubvoxels,
+ Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels
+ + WorldAssemblySubvoxelsPerVoxel);
Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels
+Recipe.boundarySealSubvoxels;
std::string PlanError;
- const std::optional<FloatingIslandManifestV1> Planned = PlanFloatingIslandCluster(Recipe, PlanError);
+ const std::optional<FloatingIslandManifestV1> Planned =
+ PlanFloatingIslandClusterAroundPassages(Recipe, Layout, Slot.StrateIndex, PlanError);
if (!Planned)
{
- OutError = FString::Printf(TEXT("FloatingIslands automatic plan for strate %d failed: %s"),
+ OutError = FString::Printf(TEXT("FloatingIslands passage-aware plan for strate %d failed: %s"),
Slot.StrateIndex, UTF8_TO_TCHAR(PlanError.c_str()));
return false;
}
@@ -1671,18 +1665,6 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Generated.plan.strates.push_back(std::move(Stratum));
++BuiltSurfaceCount;
}
- // Native floating-island evaluation remains an explicit imported-plan opt-in.
- // The default owner path uses the legacy generator for this slot; emitting an
- // automatic FI plan here silently switches density dispatch and changes the
- // shared passage mouth before the owner has opted into that field.
- else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride)
- {
- if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
- CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
- {
- return false;
- }
- }
}
if (BuiltSurfaceCount > 1)
{
@@ -2031,6 +2013,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
{
return false;
}
+ bool bAddedFloatingIslandPlan = false;
+ for (const FStrateSlot& Slot : StrateManager->GetLayout())
+ {
+ if (Slot.Definition == nullptr
+ || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ))
+ != ECaveGeneratorType::FloatingIslands)
+ {
+ continue;
+ }
+ const auto Existing = std::find_if(Manifest->floatingIslands.begin(),
+ Manifest->floatingIslands.end(), [&Slot](const auto& Plan)
+ { return Plan.strateIndex == Slot.StrateIndex; });
+ if (Existing != Manifest->floatingIslands.end()) continue;
+ if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, *Manifest, Layout,
+ CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
+ {
+ return false;
+ }
+ bAddedFloatingIslandPlan = true;
+ }
+ if (bAddedFloatingIslandPlan)
+ {
+ if (!Manifest->finalize(CoreError))
+ {
+ OutError = FString::Printf(TEXT("Core passage-aware FloatingIslands planning failed: %s"),
+ UTF8_TO_TCHAR(CoreError.c_str()));
+ return false;
+ }
+ ManifestHash = FString::Printf(TEXT("%016llx"),
+ static_cast<unsigned long long>(Manifest->contentHash));
+ for (const auto& Plan : Manifest->floatingIslands)
+ {
+ auto Stratum = std::find_if(Layout.strates.begin(), Layout.strates.end(),
+ [&Plan](const auto& Item) { return Item.strateIndex == Plan.strateIndex; });
+ if (Stratum == Layout.strates.end())
+ {
+ OutError = FString::Printf(TEXT("FloatingIslands strate %d disappeared before anchor export."),
+ Plan.strateIndex);
+ return false;
+ }
+ Stratum->recipeVersion = Plan.manifest.recipe.geometryVersion;
+ std::erase_if(Stratum->anchors, [](const auto& Anchor)
+ {
+ return Anchor.kind == "island_anchor" || Anchor.kind == "passage_anchor"
+ || Anchor.kind == "route_point";
+ });
+ const auto& NativePlan = Plan.manifest.plan;
+ const auto& NativeRecipe = Plan.manifest.recipe;
+ for (std::size_t IslandIndex = 0; IslandIndex < NativePlan.islands.size(); ++IslandIndex)
+ {
+ const auto& Island = NativePlan.islands[IslandIndex];
+ VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor;
+ Anchor.kind = "island_anchor";
+ Anchor.stableId = "island/" + std::to_string(IslandIndex);
+ Anchor.units = "1/256 Core voxel";
+ Anchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1],
+ Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels };
+ Stratum->anchors.push_back(std::move(Anchor));
+ if (IslandIndex == NativeRecipe.anchorIslandOrdinal)
+ {
+ VoxelForgeCore::ResolvedLayoutAnchorV2 PassageAnchor;
+ PassageAnchor.kind = "passage_anchor";
+ PassageAnchor.stableId = "passage-anchor/island/"
+ + std::to_string(IslandIndex);
+ PassageAnchor.units = "1/256 Core voxel";
+ PassageAnchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1],
+ NativeRecipe.anchorRootRadiusSubvoxels,
+ NativeRecipe.anchorMouthOffsetSubvoxels,
+ NativeRecipe.anchorPlacementRadiusSubvoxels,
+ NativeRecipe.anchorRootBottomZSubvoxels, Island.radiusSubvoxels };
+ if (!NativePlan.route.empty())
+ {
+ const auto& Mouth = NativePlan.route.back().positionSubvoxels;
+ PassageAnchor.values.insert(PassageAnchor.values.end(),
+ { Mouth[0], Mouth[1], Mouth[2] });
+ }
+ Stratum->anchors.push_back(std::move(PassageAnchor));
+ }
+ }
+ for (std::size_t RouteIndex = 0; RouteIndex < NativePlan.route.size(); ++RouteIndex)
+ {
+ const auto& Point = NativePlan.route[RouteIndex];
+ VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor;
+ Anchor.kind = "route_point";
+ Anchor.stableId = "route/" + std::to_string(RouteIndex);
+ Anchor.units = "1/256 Core voxel";
+ Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1],
+ Point.positionSubvoxels[2], Point.radiusSubvoxels };
+ Stratum->anchors.push_back(std::move(Anchor));
+ }
+ }
+ }
std::string LayoutError;
if (!Layout.finalize(LayoutError))
{
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
index 8c44461..ec7cfe3 100644
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
@@ -511,7 +511,7 @@ namespace VoxelForgeCore
// the planned graph route, preserves its walk corridor through shell overlaps, and keeps
// boundary-blended host rock from widening the authored passage wall.
// v237 adds the shared Core cave-graph shaft primitive and basal terminal route.
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237;
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239;
enum class FieldChannel : std::uint32_t
{
@@ -1417,6 +1417,19 @@ namespace VoxelForgeCore
bool passed = false;
};
+ struct FloatingIslandPassageClearanceAuditResult
+ {
+ std::uint64_t passageSamples = 0;
+ std::uint64_t blockedIslandSamples = 0;
+ std::uint64_t routeSamples = 0;
+ std::uint64_t blockedRouteSamples = 0;
+ std::uint32_t arrivalMouthSupportProbes = 0;
+ std::uint32_t departureMouthSupportProbes = 0;
+ std::int64_t minimumExcessClearanceSubvoxels = 0;
+ double portalExclusionMetres = 0.0;
+ bool passed = false;
+ };
+
struct FloatingIslandInfluenceBounds
{
bool finite = false;
@@ -1461,11 +1474,17 @@ namespace VoxelForgeCore
// Planner output is ordinary reviewed data; clients can load it without re-running the planner.
VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
const FloatingIslandRecipeV1& recipe, std::string& error);
+ VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandClusterAroundPassages(
+ const FloatingIslandRecipeV1& recipe, const ResolvedLayoutSnapshotV3& layout,
+ std::int32_t strateIndex, std::string& error);
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
std::uint64_t seed = 0x49534c414e444231ull);
VFCORE_API FloatingIslandRouteAuditResult AuditFloatingIslandRoute(
const FloatingIslandFieldV1& field, std::int64_t routeOffsetSubvoxels = 0);
+ VFCORE_API FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance(
+ const FloatingIslandFieldV1& field, const ResolvedLayoutSnapshotV3& layout,
+ std::int32_t strateIndex, double portalExclusionMetres = 10.0);
VFCORE_API FloatingIslandThicknessAuditResult AuditFloatingIslandThickness(
const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio = 0.70);
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
index 33d97d6..58f0959 100644
--- a/Standalone/Core/src/FloatingIslandsInternal.h
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
@@ -4,6 +4,7 @@
#include "VoxelForgeCore/CpuCycleCounter.h"
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
#include "VoxelForgeCore/SharedColumnCache.h"
+#include "VoxelForgeCore/PassageSpline.h"
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
@@ -285,7 +286,7 @@ namespace floating_island_detail
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
+ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 10
|| (recipe.geometryVersion == 1
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|| recipe.unitsPerVoxel != Q
@@ -1988,6 +1989,229 @@ std::string FloatingIslandManifestV1::ToJson() const
return out.str();
}
+namespace floating_island_detail
+{
+ struct FIslandPassageSpan
+ {
+ PassageSpline::FSegment segment;
+ double alongMetres = 0.0;
+ double totalMetres = 0.0;
+ double radiusSubvoxels = 0.0;
+ bool portalAtStart = false;
+ };
+
+ inline double SegmentDistanceSquared3D(const PassageSpline::FPoint& p1,
+ const PassageSpline::FPoint& q1, const PassageSpline::FPoint& p2,
+ const PassageSpline::FPoint& q2)
+ {
+ const std::array<double,3> d1={q1[0]-p1[0],q1[1]-p1[1],q1[2]-p1[2]};
+ const std::array<double,3> d2={q2[0]-p2[0],q2[1]-p2[1],q2[2]-p2[2]};
+ const std::array<double,3> r={p1[0]-p2[0],p1[1]-p2[1],p1[2]-p2[2]};
+ const auto dot=[](const auto& a,const auto& b)
+ {return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];};
+ const double a=dot(d1,d1),e=dot(d2,d2),f=dot(d2,r);
+ double s=0.0,t=0.0;
+ if(a<=1.0e-18&&e<=1.0e-18)return dot(r,r);
+ if(a<=1.0e-18)t=std::clamp(f/e,0.0,1.0);
+ else
+ {
+ const double c=dot(d1,r);
+ if(e<=1.0e-18)s=std::clamp(-c/a,0.0,1.0);
+ else
+ {
+ const double b=dot(d1,d2);
+ const double denominator=a*e-b*b;
+ if(std::abs(denominator)>1.0e-18)
+ s=std::clamp((b*f-c*e)/denominator,0.0,1.0);
+ t=(b*s+f)/e;
+ if(t<0.0){t=0.0;s=std::clamp(-c/a,0.0,1.0);}
+ else if(t>1.0){t=1.0;s=std::clamp((b-c)/a,0.0,1.0);}
+ }
+ }
+ std::array<double,3> delta{};
+ for(std::size_t axis=0;axis<3;++axis)
+ delta[axis]=r[axis]+s*d1[axis]-t*d2[axis];
+ return dot(delta,delta);
+ }
+
+ inline std::vector<FIslandPassageSpan> BuildIslandPassageSpans(
+ const ResolvedLayoutSnapshotV3& layout,std::int32_t strateIndex,
+ const FloatingIslandRecipeV1& recipe,double portalExclusionMetres)
+ {
+ std::vector<FIslandPassageSpan> spans;
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
+ for(const auto& passage:layout.passages)
+ {
+ const bool upper=passage.upperStrateIndex==strateIndex;
+ const bool lower=passage.lowerStrateIndex==strateIndex;
+ if((!upper&&!lower)||passage.controlPoints.size()<2)continue;
+ std::vector<PassageSpline::FPoint> controls;
+ controls.reserve(passage.controlPoints.size());
+ for(const auto& point:passage.controlPoints)
+ controls.push_back({static_cast<double>(point.positionSubvoxels[0]),
+ static_cast<double>(point.positionSubvoxels[1]),
+ static_cast<double>(point.positionSubvoxels[2]),
+ static_cast<double>(point.radiusSubvoxels)});
+ const auto built=PassageSpline::BuildSegments(controls,controls.size()-1,
+ 0.25,qPerMetre,{},0.04);
+ if(built.empty())continue;
+ const double total=built.back().alongMetres+built.back().lengthMetres;
+ const auto& first=controls.front();
+ const auto& last=controls.back();
+ const auto distanceSquaredTo=[&](const PassageSpline::FPoint& point,
+ const std::array<std::int64_t,3>& mouth)
+ {
+ double result=0.0;
+ for(std::size_t axis=0;axis<3;++axis)
+ {const double delta=point[axis]-mouth[axis];result+=delta*delta;}
+ return result;
+ };
+ const bool upperAtStart=distanceSquaredTo(first,passage.upperMouthSubvoxels)
+ <=distanceSquaredTo(last,passage.upperMouthSubvoxels);
+ const bool portalAtStart=upper?upperAtStart:!upperAtStart;
+ for(const auto& segment:built)
+ {
+ const double along=segment.alongMetres+0.5*segment.lengthMetres;
+ const double portalDistance=portalAtStart?along:total-along;
+ if(portalDistance<=portalExclusionMetres)continue;
+ const PassageSpline::FPoint midpoint={
+ 0.5*(segment.a[0]+segment.b[0]),
+ 0.5*(segment.a[1]+segment.b[1]),
+ 0.5*(segment.a[2]+segment.b[2]),
+ 0.5*(segment.a[3]+segment.b[3])};
+ const double minZ=std::min(segment.a[2],segment.b[2]);
+ const double maxZ=std::max(segment.a[2],segment.b[2]);
+ if(maxZ<static_cast<double>(recipe.layoutMinimumZSubvoxels)
+ ||minZ>static_cast<double>(recipe.layoutMaximumZSubvoxels))continue;
+ spans.push_back({segment,along,total,midpoint[3],portalAtStart});
+ }
+ }
+ return spans;
+ }
+
+ inline std::array<double,4> EvaluateIslandRouteBezier(
+ const std::array<double,3>& a,const std::array<double,3>& b,
+ const std::array<double,3>& waypoint,double t)
+ {
+ const double u=1.0-t;
+ const std::array<double,4> result={
+ u*u*u*a[0]+3.0*u*u*t*waypoint[0]+3.0*u*t*t*waypoint[0]+t*t*t*b[0],
+ u*u*u*a[1]+3.0*u*u*t*waypoint[1]+3.0*u*t*t*waypoint[1]+t*t*t*b[1],
+ std::lerp(a[2],b[2],t),t};
+ return result;
+ }
+
+ struct FIslandDetourCandidate
+ {
+ std::array<std::array<std::int64_t,3>,9> centers{};
+ double horizontalLengthMetres=0.0;
+ double conservativeClearanceMetres=-std::numeric_limits<double>::infinity();
+ double waypointAngle=0.0;
+ double waypointRadiusMetres=0.0;
+ };
+
+ inline std::vector<FIslandDetourCandidate> BuildIslandDetourCandidates(
+ const FloatingIslandRecipeV1& recipe,const ResolvedLayoutPassageV1& arrival,
+ const ResolvedLayoutPassageV1& departure,
+ const std::vector<FIslandPassageSpan>& spans)
+ {
+ std::vector<FIslandDetourCandidate> candidates;
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
+ const std::array<double,3> a={static_cast<double>(arrival.lowerMouthSubvoxels[0]),
+ static_cast<double>(arrival.lowerMouthSubvoxels[1]),
+ static_cast<double>(arrival.lowerMouthSubvoxels[2])};
+ const std::array<double,3> b={static_cast<double>(departure.upperMouthSubvoxels[0]),
+ static_cast<double>(departure.upperMouthSubvoxels[1]),
+ static_cast<double>(departure.upperMouthSubvoxels[2])};
+ for(int angleIndex=0;angleIndex<16;++angleIndex)
+ for(const double radiusMetres:{60.0,72.0,84.0})
+ {
+ FIslandDetourCandidate candidate;
+ candidate.waypointAngle=2.0*Pi*angleIndex/16.0;
+ candidate.waypointRadiusMetres=radiusMetres;
+ const std::array<double,3> waypoint={
+ static_cast<double>(recipe.layoutOriginXSubvoxels)
+ +std::cos(candidate.waypointAngle)*radiusMetres*qPerMetre,
+ static_cast<double>(recipe.layoutOriginYSubvoxels)
+ +std::sin(candidate.waypointAngle)*radiusMetres*qPerMetre,
+ 0.5*(a[2]+b[2])};
+ constexpr std::size_t CurveSteps=512;
+ std::array<double,CurveSteps+1> arc{};
+ std::array<std::array<double,3>,CurveSteps+1> curve{};
+ auto prior=EvaluateIslandRouteBezier(a,b,waypoint,0.0);
+ curve[0]={prior[0],prior[1],prior[2]};
+ for(std::size_t n=1;n<=CurveSteps;++n)
+ {
+ const auto value=EvaluateIslandRouteBezier(a,b,waypoint,
+ static_cast<double>(n)/CurveSteps);
+ curve[n]={value[0],value[1],value[2]};
+ const double dx=(value[0]-prior[0])/qPerMetre;
+ const double dy=(value[1]-prior[1])/qPerMetre;
+ arc[n]=arc[n-1]+std::hypot(dx,dy);
+ prior=value;
+ }
+ candidate.horizontalLengthMetres=arc.back();
+ if(!(candidate.horizontalLengthMetres>1.0))continue;
+ for(std::size_t island=0;island<candidate.centers.size();++island)
+ {
+ const double target=arc.back()*island/(candidate.centers.size()-1);
+ const auto upper=std::lower_bound(arc.begin(),arc.end(),target);
+ const std::size_t upperIndex=std::min<std::size_t>(
+ CurveSteps,static_cast<std::size_t>(upper-arc.begin()));
+ const std::size_t lowerIndex=upperIndex?upperIndex-1:0;
+ const double span=arc[upperIndex]-arc[lowerIndex];
+ const double t=span>1.0e-9?(target-arc[lowerIndex])/span:0.0;
+ auto point=std::array<double,3>{
+ std::lerp(curve[lowerIndex][0],curve[upperIndex][0],t),
+ std::lerp(curve[lowerIndex][1],curve[upperIndex][1],t),
+ std::lerp(a[2],b[2],target/arc.back())};
+ if(island==0)point=a;
+ if(island+1==candidate.centers.size())point=b;
+ for(std::size_t axis=0;axis<3;++axis)
+ candidate.centers[island][axis]=RoundQ(point[axis]);
+ }
+ double minimumClearance=std::numeric_limits<double>::infinity();
+ for(std::size_t routeIndex=1;routeIndex<candidate.centers.size();++routeIndex)
+ {
+ PassageSpline::FPoint routeA{},routeB{};
+ for(std::size_t axis=0;axis<3;++axis)
+ {
+ routeA[axis]=static_cast<double>(candidate.centers[routeIndex-1][axis]);
+ routeB[axis]=static_cast<double>(candidate.centers[routeIndex][axis]);
+ }
+ for(const auto& span:spans)
+ {
+ const double distance=std::sqrt(SegmentDistanceSquared3D(
+ routeA,routeB,span.segment.a,span.segment.b))/qPerMetre;
+ const double passageRadius=span.radiusSubvoxels/qPerMetre;
+ const double capsuleRadius=0.5*recipe.routeConstraints.capsuleHeightSubvoxels/qPerMetre;
+ const double featureReach=recipe.maximumRadiusSubvoxels/qPerMetre
+ +(recipe.contourWarpSubvoxels+IslandBlendMargin(recipe)
+ +recipe.roughnessSubvoxels)/qPerMetre;
+ minimumClearance=std::min(minimumClearance,distance-passageRadius
+ -1.0533-capsuleRadius-featureReach-0.15);
+ }
+ }
+ candidate.conservativeClearanceMetres=std::isfinite(minimumClearance)
+ ?minimumClearance:1.0e6;
+ candidates.push_back(candidate);
+ }
+ std::stable_sort(candidates.begin(),candidates.end(),
+ [](const auto& left,const auto& right)
+ {
+ const bool leftSafe=left.conservativeClearanceMetres>=0.0;
+ const bool rightSafe=right.conservativeClearanceMetres>=0.0;
+ if(leftSafe!=rightSafe)return leftSafe;
+ if(left.horizontalLengthMetres!=right.horizontalLengthMetres)
+ return left.horizontalLengthMetres<right.horizontalLengthMetres;
+ return left.conservativeClearanceMetres>right.conservativeClearanceMetres;
+ });
+ return candidates;
+ }
+}
+
std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
const FloatingIslandRecipeV1& inputRecipe, std::string& error)
{
@@ -2264,6 +2488,210 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
return manifest;
}
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandClusterAroundPassages(
+ const FloatingIslandRecipeV1& inputRecipe,const ResolvedLayoutSnapshotV3& layout,
+ std::int32_t strateIndex,std::string& error)
+{
+ using namespace floating_island_detail;
+ FloatingIslandRecipeV1 recipe=inputRecipe;
+ if(recipe.routeConstraints.version==0)
+ {
+ recipe.routeConstraints.version=1;
+ recipe.routeConstraints.capsuleRadiusSubvoxels=348;
+ recipe.routeConstraints.capsuleHeightSubvoxels=1802;
+ recipe.routeConstraints.maximumGradeQ16=17560;
+ recipe.routeConstraints.sweepSpacingSubvoxels=128;
+ recipe.routeConstraints.supportProbeRadiusSubvoxels=230;
+ recipe.routeConstraints.supportSearchDepthSubvoxels=512;
+ recipe.routeConstraints.minimumClearanceSubvoxels=128;
+ recipe.routeConstraints.minimumSupportProbes=3;
+ }
+ if(recipe.geometryVersion<10||recipe.primaryIslandCount!=9)
+ {error="corridor-aware FloatingIslands requires geometry version 10 and nine route islands";return std::nullopt;}
+ if(!ValidateRecipe(recipe,error))return std::nullopt;
+
+ const ResolvedLayoutPassageV1* arrival=nullptr;
+ const ResolvedLayoutPassageV1* departure=nullptr;
+ for(const auto& passage:layout.passages)
+ {
+ if(passage.lowerStrateIndex==strateIndex
+ &&passage.upperStrateIndex<strateIndex)arrival=&passage;
+ if(passage.upperStrateIndex==strateIndex
+ &&passage.lowerStrateIndex>strateIndex)departure=&passage;
+ }
+ if(!arrival||!departure)
+ {error="corridor-aware FloatingIslands needs resolved arrival and departure mouths";return std::nullopt;}
+
+ constexpr double PortalExclusionMetres=13.0;
+ const auto passageSpans=BuildIslandPassageSpans(layout,strateIndex,recipe,
+ PortalExclusionMetres);
+ if(passageSpans.empty())
+ {error="corridor-aware FloatingIslands found no resolved passage spans inside its strate";return std::nullopt;}
+ auto candidates=BuildIslandDetourCandidates(recipe,*arrival,*departure,passageSpans);
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
+ const double capsuleHalf=0.5*recipe.routeConstraints.capsuleHeightSubvoxels;
+ const double routeLift=capsuleHalf+recipe.routeConstraints.minimumClearanceSubvoxels+Q/8.0;
+ std::uint32_t tried=0;
+ std::string lastFailure="no outer detour has conservative tube clearance";
+ for(const auto& candidate:candidates)
+ {
+ if(candidate.conservativeClearanceMetres<0.0)break;
+ ++tried;
+ FloatingIslandManifestV1 manifest;
+ manifest.recipe=recipe;
+ auto& plan=manifest.plan;
+ plan.islands.reserve(recipe.primaryIslandCount);
+ bool candidateValid=true;
+ for(std::uint32_t ordinal=0;ordinal<recipe.primaryIslandCount;++ordinal)
+ {
+ const auto& center=candidate.centers[ordinal];
+ const double t=static_cast<double>(ordinal)/(recipe.primaryIslandCount-1);
+ const std::int64_t topZ=RoundQ(std::lerp(
+ static_cast<double>(arrival->lowerMouthSubvoxels[2])-routeLift,
+ static_cast<double>(departure->upperMouthSubvoxels[2])-routeLift,t));
+ auto island=MakeIsland(recipe,ordinal,center[0],center[1],topZ,
+ 0x100000000ull+ordinal);
+ if(ordinal==recipe.anchorIslandOrdinal)
+ island.radiusSubvoxels=std::max(island.radiusSubvoxels,
+ recipe.anchorIslandMinimumRadiusSubvoxels);
+ island.topHalfSubvoxels=std::max<std::int64_t>(1,island.radiusSubvoxels/5);
+ island.centerSubvoxels[2]=island.topZSubvoxels-island.topHalfSubvoxels;
+ island.bottomZSubvoxels=IslandBottom(recipe,island.topZSubvoxels,
+ island.radiusSubvoxels);
+ SetIslandBounds(recipe,island);
+ if(island.bottomZSubvoxels<recipe.layoutMinimumZSubvoxels
+ +recipe.boundarySealSubvoxels
+ ||island.topZSubvoxels>recipe.layoutMaximumZSubvoxels
+ -recipe.boundarySealSubvoxels)
+ {candidateValid=false;lastFailure="detour island escaped the sealed vertical layout";break;}
+ plan.islands.push_back(std::move(island));
+ }
+ if(!candidateValid)continue;
+ for(std::uint32_t ordinal=0;ordinal+1<recipe.primaryIslandCount;++ordinal)
+ plan.clusterEdges.push_back(MakeConnector(recipe,plan.islands[ordinal],
+ plan.islands[ordinal+1],0x200000000ull+ordinal));
+ FloatingIslandFieldV1 planningField(recipe,plan);
+ if(!planningField.isValid())
+ {lastFailure="detour island features did not pass the Core field contract";continue;}
+
+ std::uint32_t routeOrdinal=0;
+ const double routeStep=0.75*qPerMetre;
+ for(std::size_t segment=0;segment+1<plan.islands.size();++segment)
+ {
+ const auto& a=plan.islands[segment].centerSubvoxels;
+ const auto& b=plan.islands[segment+1].centerSubvoxels;
+ const double dx=static_cast<double>(b[0]-a[0]);
+ const double dy=static_cast<double>(b[1]-a[1]);
+ const std::uint32_t pieces=static_cast<std::uint32_t>(std::max(1.0,
+ std::ceil(std::hypot(dx,dy)/routeStep)));
+ for(std::uint32_t piece=segment==0?0:1;piece<=pieces;++piece)
+ {
+ const double t=static_cast<double>(piece)/pieces;
+ const std::int64_t x=RoundQ(static_cast<double>(a[0])+dx*t);
+ const std::int64_t y=RoundQ(static_cast<double>(a[1])+dy*t);
+ const auto surface=FindCapsuleFootingTopZ(recipe,plan,x,y);
+ if(!surface)
+ {candidateValid=false;lastFailure="detour route has no full capsule-width island support";break;}
+ WorldAssemblyRoutePointV1 point;
+ point.stableFeatureId=StableId(recipe.seed,0x500000000ull+routeOrdinal,
+ 0x524f555445ull);
+ point.positionSubvoxels={x,y,*surface+static_cast<std::int64_t>(std::llround(routeLift))};
+ point.radiusSubvoxels=4*Q;
+ if(segment==0&&piece==0)
+ point.positionSubvoxels=arrival->lowerMouthSubvoxels;
+ if(segment+2==plan.islands.size()&&piece==pieces)
+ point.positionSubvoxels=departure->upperMouthSubvoxels;
+ if(!plan.route.empty()&&plan.route.back().positionSubvoxels==point.positionSubvoxels)
+ continue;
+ plan.route.push_back(point);
+ ++routeOrdinal;
+ }
+ if(!candidateValid)break;
+ }
+ if(!candidateValid||plan.route.size()<2)continue;
+
+ const double maximumGrade=Q16ToDouble(recipe.routeConstraints.maximumGradeQ16)*0.95;
+ std::vector<std::int64_t> baseHeights;
+ baseHeights.reserve(plan.route.size());
+ for(const auto& point:plan.route)baseHeights.push_back(point.positionSubvoxels[2]);
+ std::vector<std::int64_t> profile=baseHeights;
+ for(std::size_t i=0;i<plan.route.size();++i)
+ {
+ double adjusted=static_cast<double>(baseHeights[i]);
+ for(std::size_t j=0;j<plan.route.size();++j)
+ {
+ const double dx=static_cast<double>(plan.route[i].positionSubvoxels[0]
+ -plan.route[j].positionSubvoxels[0]);
+ const double dy=static_cast<double>(plan.route[i].positionSubvoxels[1]
+ -plan.route[j].positionSubvoxels[1]);
+ adjusted=std::max(adjusted,static_cast<double>(baseHeights[j])
+ -maximumGrade*std::hypot(dx,dy));
+ }
+ profile[i]=RoundQ(adjusted);
+ }
+ const auto arrivalZ=arrival->lowerMouthSubvoxels[2];
+ const auto departureZ=departure->upperMouthSubvoxels[2];
+ if(profile.front()>arrivalZ+Q/2||profile.back()>departureZ+Q/2)
+ {lastFailure="island support profile cannot meet both fixed mouth elevations";continue;}
+ for(std::size_t i=1;i+1<plan.route.size();++i)
+ plan.route[i].positionSubvoxels[2]=profile[i];
+ plan.route.front().positionSubvoxels=arrival->lowerMouthSubvoxels;
+ plan.route.back().positionSubvoxels=departure->upperMouthSubvoxels;
+
+ if(!manifest.finalize(error))
+ {lastFailure=error;continue;}
+ const FloatingIslandFieldV1 field(manifest.recipe,manifest.plan);
+ const auto routeAudit=AuditFloatingIslandRoute(field);
+ if(!routeAudit.passed)
+ {
+ lastFailure="detour route sweep failed: blocked="
+ +std::to_string(routeAudit.blockedCapsuleSamples)+" unsupported="
+ +std::to_string(routeAudit.unsupportedCenterSamples)+" grade="
+ +std::to_string(routeAudit.gradeFailures)+" detour_angle_deg="
+ +std::to_string(candidate.waypointAngle*180.0/Pi)+" detour_radius_m="
+ +std::to_string(candidate.waypointRadiusMetres)+" first_unsupported_q=("
+ +std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[0])+","+
+ std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[1])+","+
+ std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[2])+") segment="
+ +std::to_string(routeAudit.firstUnsupportedSegment);
+ continue;
+ }
+ const auto clearance=AuditFloatingIslandPassageClearance(field,layout,
+ strateIndex,PortalExclusionMetres);
+ if(!clearance.passed)
+ {
+ lastFailure="passage clearance failed: island="
+ +std::to_string(clearance.blockedIslandSamples)+" route="
+ +std::to_string(clearance.blockedRouteSamples)+" mouth_support="
+ +std::to_string(clearance.arrivalMouthSupportProbes)+"/"
+ +std::to_string(clearance.departureMouthSupportProbes);
+ continue;
+ }
+ const std::string encoded=manifest.ToJson();
+ const auto roundTrip=FloatingIslandManifestV1::FromJson(encoded,error);
+ if(!roundTrip||roundTrip->ToJson()!=encoded
+ ||roundTrip->contentHash!=manifest.contentHash)
+ {if(error.empty())error="corridor-aware floating-island plan did not canonical-round-trip";return std::nullopt;}
+ std::printf("[FloatingIslandCorridorPlan] strate=%d detours_tried=%u waypoint_angle_deg=%.1f waypoint_radius_m=%.1f route_m=%.1f conservative_clearance_m=%.2f portal_exclusion_m=%.1f islands=%zu arrival_binding=PASS departure_binding=PASS status=PASS\n",
+ strateIndex,tried,candidate.waypointAngle*180.0/Pi,
+ candidate.waypointRadiusMetres,candidate.horizontalLengthMetres,
+ candidate.conservativeClearanceMetres,PortalExclusionMetres,
+ manifest.plan.islands.size());
+ error.clear();
+ return manifest;
+ }
+ const auto bestCandidate=std::max_element(candidates.begin(),candidates.end(),
+ [](const auto& left,const auto& right)
+ {return left.conservativeClearanceMetres<right.conservativeClearanceMetres;});
+ error="no fixed-mouth island detour passed route support and passage clearance after "
+ +std::to_string(tried)+" candidates; best_clearance_m="
+ +(bestCandidate==candidates.end()?std::string("none")
+ :std::to_string(bestCandidate->conservativeClearanceMetres))
+ +" last="+lastFailure;
+ return std::nullopt;
+}
+
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
{}
@@ -2848,6 +3276,107 @@ FloatingIslandRouteAuditResult AuditFloatingIslandRoute(const FloatingIslandFiel
return result;
}
+FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance(
+ const FloatingIslandFieldV1& field,const ResolvedLayoutSnapshotV3& layout,
+ std::int32_t strateIndex,double portalExclusionMetres)
+{
+ using namespace floating_island_detail;
+ FloatingIslandPassageClearanceAuditResult result;
+ result.portalExclusionMetres=portalExclusionMetres;
+ if(!field.isValid()||!field.recipe()||!field.plan()
+ ||field.plan()->route.size()<2||!std::isfinite(portalExclusionMetres)
+ ||portalExclusionMetres<0.0)return result;
+ const auto& recipe=*field.recipe();
+ const double qPerMetre=static_cast<double>(WorldAssemblySubvoxelsPerVoxel)
+ *1000000.0/std::max<std::uint32_t>(1,recipe.coreVoxelSizeMicrons);
+ const std::int64_t influenceReach=RoundQ(1.0533*qPerMetre);
+ const std::int64_t capsuleReach=recipe.routeConstraints.capsuleHeightSubvoxels/2;
+ const std::int64_t sampleGuard=RoundQ(0.15*qPerMetre);
+ const auto spans=BuildIslandPassageSpans(layout,strateIndex,recipe,
+ portalExclusionMetres);
+ bool haveClearance=false;
+ std::int64_t minimumExcess=std::numeric_limits<std::int64_t>::max();
+ for(const auto& span:spans)
+ {
+ const PassageSpline::FPoint center={
+ 0.5*(span.segment.a[0]+span.segment.b[0]),
+ 0.5*(span.segment.a[1]+span.segment.b[1]),
+ 0.5*(span.segment.a[2]+span.segment.b[2]),
+ 0.5*(span.segment.a[3]+span.segment.b[3])};
+ const std::int64_t density=field.densitySubvoxels(RoundQ(center[0]),
+ RoundQ(center[1]),RoundQ(center[2]));
+ const std::int64_t required=RoundQ(center[3])+influenceReach
+ +capsuleReach+sampleGuard;
+ const std::int64_t excess=density-required;
+ minimumExcess=std::min(minimumExcess,excess);
+ haveClearance=true;
+ ++result.passageSamples;
+ if(excess<0)++result.blockedIslandSamples;
+
+ for(std::size_t routeSegment=0;routeSegment+1<field.plan()->route.size();++routeSegment)
+ {
+ const auto& routeA=field.plan()->route[routeSegment].positionSubvoxels;
+ const auto& routeB=field.plan()->route[routeSegment+1].positionSubvoxels;
+ PassageSpline::FPoint a{},b{};
+ for(std::size_t axis=0;axis<3;++axis)
+ {a[axis]=static_cast<double>(routeA[axis]);b[axis]=static_cast<double>(routeB[axis]);}
+ const double distance=std::sqrt(SegmentDistanceSquared3D(a,b,
+ span.segment.a,span.segment.b));
+ const double requiredDistance=center[3]+influenceReach
+ +capsuleReach+sampleGuard;
+ ++result.routeSamples;
+ if(distance<requiredDistance)++result.blockedRouteSamples;
+ }
+ }
+ result.minimumExcessClearanceSubvoxels=haveClearance?minimumExcess:0;
+
+ const ResolvedLayoutPassageV1* arrival=nullptr;
+ const ResolvedLayoutPassageV1* departure=nullptr;
+ for(const auto& passage:layout.passages)
+ {
+ if(passage.lowerStrateIndex==strateIndex&&passage.upperStrateIndex<strateIndex)
+ arrival=&passage;
+ if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex)
+ departure=&passage;
+ }
+ const auto mouthSupport=[&](const std::array<std::int64_t,3>& mouth)
+ {
+ constexpr std::array<std::array<double,2>,5> probes={{{0.0,0.0},
+ {1.0,0.0},{-1.0,0.0},{0.0,1.0},{0.0,-1.0}}};
+ const double radius=static_cast<double>(recipe.routeConstraints.supportProbeRadiusSubvoxels);
+ const std::int64_t foot=mouth[2]-recipe.routeConstraints.capsuleHeightSubvoxels/2;
+ const std::int64_t sampleZ=foot-recipe.routeConstraints.minimumClearanceSubvoxels-Q/2;
+ std::uint32_t supported=0;
+ for(const auto& probe:probes)
+ {
+ const auto x=RoundQ(static_cast<double>(mouth[0])+probe[0]*radius);
+ const auto y=RoundQ(static_cast<double>(mouth[1])+probe[1]*radius);
+ if(field.densitySubvoxels(x,y,sampleZ)<0)++supported;
+ }
+ return supported;
+ };
+ bool bound=false;
+ if(arrival&&departure)
+ {
+ const auto& route=field.plan()->route;
+ const auto close=[](const auto& a,const auto& b)
+ {
+ for(std::size_t axis=0;axis<3;++axis)
+ if(std::abs(a[axis]-b[axis])>2)return false;
+ return true;
+ };
+ bound=close(route.front().positionSubvoxels,arrival->lowerMouthSubvoxels)
+ &&close(route.back().positionSubvoxels,departure->upperMouthSubvoxels);
+ result.arrivalMouthSupportProbes=mouthSupport(arrival->lowerMouthSubvoxels);
+ result.departureMouthSupportProbes=mouthSupport(departure->upperMouthSubvoxels);
+ }
+ result.passed=bound&&haveClearance&&result.blockedIslandSamples==0
+ &&result.blockedRouteSamples==0
+ &&result.arrivalMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes
+ &&result.departureMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes;
+ return result;
+}
+
FloatingIslandThicknessAuditResult AuditFloatingIslandThickness(
const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio)
{
diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h
index 1a57f85..98dabf5 100644
--- a/Standalone/Core/src/ShapeWorldInternal.h
+++ b/Standalone/Core/src/ShapeWorldInternal.h
@@ -1754,9 +1754,81 @@ public:
?passageAirDistance:density);
if(nativeTunnelFieldDensity!=std::numeric_limits<float>::lowest())
return std::max(density,passageAirDistance);
+ const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* landingPlan=nullptr;
+ if(WorldAssembly&&queryStratum&&queryStratum->Resolved
+ &&queryStratum->Resolved->generatorKind=="FloatingIslands")
+ {
+ const auto island=std::find_if(WorldAssembly->floatingIslands.begin(),
+ WorldAssembly->floatingIslands.end(),[&](const auto& candidate)
+ {return candidate.strateIndex==queryStratum->StrateIndex;});
+ if(island!=WorldAssembly->floatingIslands.end())landingPlan=&*island;
+ }
+ const auto preserveFloatingIslandLandingSupport=[&](float composedDensity)
+ {
+ if(!landingPlan||landingPlan->manifest.recipe.geometryVersion<10)
+ return composedDensity;
+ const auto& constraints=landingPlan->manifest.recipe.routeConstraints;
+ if(constraints.version!=1)return composedDensity;
+ const auto* routeField=std::get_if<FFloatingIslandFieldAdapter>(
+ &queryStratum->Evaluator);
+ const auto& route=landingPlan->manifest.plan.route;
+ if(!routeField||route.size()<2)return composedDensity;
+
+ constexpr std::size_t LandingRouteSegments=18;
+ const double supportRadiusMetres=std::min(0.45,
+ constraints.supportProbeRadiusSubvoxels/qPerMetre+0.10);
+ const double queryX=qx,queryY=qy,queryZ=qz;
+ for(std::size_t segment=0;segment+1<route.size();++segment)
+ {
+ if(segment>=LandingRouteSegments
+ &&segment+LandingRouteSegments<route.size())continue;
+ const auto& a=route[segment].positionSubvoxels;
+ const auto& b=route[segment+1].positionSubvoxels;
+ const double dx=static_cast<double>(b[0]-a[0]);
+ const double dy=static_cast<double>(b[1]-a[1]);
+ const double dz=static_cast<double>(b[2]-a[2]);
+ const double horizontalSquared=dx*dx+dy*dy;
+ if(horizontalSquared<=1.0e-12)continue;
+ const double t=std::clamp(((queryX-a[0])*dx+(queryY-a[1])*dy)
+ /horizontalSquared,0.0,1.0);
+ const double routeX=a[0]+t*dx,routeY=a[1]+t*dy;
+ if(std::hypot(queryX-routeX,queryY-routeY)
+ >supportRadiusMetres*qPerMetre)
+ continue;
+ const double routeCenterZ=a[2]+t*dz;
+ const double footZ=routeCenterZ
+ -0.5*constraints.capsuleHeightSubvoxels;
+ const double shallowDepth=constraints.minimumClearanceSubvoxels+Q/2.0;
+ const double deepDepth=constraints.minimumClearanceSubvoxels
+ +constraints.supportSearchDepthSubvoxels;
+ if(queryZ<=footZ-shallowDepth&&queryZ>=footZ-deepDepth)
+ {
+ const std::int64_t nativeDensity=routeField->densitySubvoxelsAt(
+ static_cast<std::int64_t>(std::llround(queryX)),
+ static_cast<std::int64_t>(std::llround(queryY)),
+ static_cast<std::int64_t>(std::llround(queryZ)));
+ if(nativeDensity<0)
+ return std::min(composedDensity,
+ static_cast<float>(static_cast<double>(nativeDensity)/Q));
+ }
+ }
+ return composedDensity;
+ };
if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f)
return static_cast<float>(unionShellDistance/Q);
- return std::max(density,passageAirDistance);
+ float composedDensity=std::max(density,passageAirDistance);
+ if(landingPlan&&landingPlan->manifest.recipe.geometryVersion>=10
+ &&density>=0.0f&&unionDistance<0.0&&unionShellDistance>0.0)
+ {
+ // In open-air parts of the island stratum, the boolean air union saturates
+ // the passage's positive interior distance to the farther island-air
+ // distance. Keep the nearest positive bore/shell distance so the explicit
+ // passage shell remains measurable without changing any field signs.
+ composedDensity=std::min(composedDensity,
+ std::min(passageAirDistance,
+ static_cast<float>(unionShellDistance/Q)));
+ }
+ return preserveFloatingIslandLandingSupport(composedDensity);
}
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
@@ -7217,9 +7289,11 @@ private:
Density=filteredDensity;
if(routeNativeBlend>0.0&&NativeDensity<0)
{
+ const double routePreservation=Plan->manifest.recipe.geometryVersion>=10
+ ?routeNativeBlend:routeNativeBlend*0.60;
const double routePreserved=std::lerp(static_cast<double>(Density),
static_cast<double>(std::min(Density,NativeDensity)),
- routeNativeBlend*0.60);
+ routePreservation);
Density=static_cast<std::int64_t>(std::llround(routePreserved));
}
// Marching Cubes can retain an isolated solid with no two-metre axis
diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp
index 1c0ed4c..80ad688 100644
--- a/Standalone/Source/StandaloneViewer.cpp
+++ b/Standalone/Source/StandaloneViewer.cpp
@@ -9208,11 +9208,15 @@ int main(int Argc,char** Argv)
{
const auto PlanStart=std::chrono::steady_clock::now();
VoxelForgeCore::WorldAssemblyManifest WorldAssembly;
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 ResolvedLayout;
const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,
Error,WorldAssemblyManifestOutputPath,&WorldAssembly)
&&(!bFloatingIslandsRecipe
||StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
- *App.World,App.OwnerJsonRoot,WorldAssembly,Error));
+ *App.World,App.OwnerJsonRoot,WorldAssembly,Error))
+ &&(!bFloatingIslandsRecipe
+ ||StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
+ *App.World,WorldAssembly,"floating-islands",ResolvedLayout,Error));
const double PlanMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-PlanStart).count();
std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=%s\n",
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
index 1afcff5..f22afe9 100644
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
@@ -1830,31 +1830,11 @@ int main(int argc,char** argv)
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)
- ||(options.NativeOwnerFloatingIslandsRecipe
- &&worldAssembly.floatingIslands.empty()))
+ if(!useExportedCorePlan&&options.FloatingIslandsRecipe
+ &&!options.NativeOwnerFloatingIslandsRecipe
&&!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");
@@ -1886,6 +1866,31 @@ int main(int argc,char** argv)
&&!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(options.NativeOwnerFloatingIslandsRecipe
+ &&worldAssembly.floatingIslands.empty()
+ &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
+ *ownerWorld,ownerJsonRoot,worldAssembly,resolvedLayout,error,
+ true))
+ {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(!std::holds_alternative<std::nullptr_t>(ownerJsonRoot.Get("motif_family_override").Data))
{
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
index 4e20068..70c4c61 100644
--- a/Standalone/Source/WorldAssemblyPrototype.h
+++ b/Standalone/Source/WorldAssemblyPrototype.h
@@ -1366,7 +1366,9 @@ namespace StandaloneWorldAssemblyPrototype
}
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
+ VoxelForgeCore::ResolvedLayoutSnapshotV3& resolvedLayout,
+ std::string& error,bool planAroundPassages=false)
{
if(!owner.Manager)
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
@@ -1386,7 +1388,8 @@ namespace StandaloneWorldAssemblyPrototype
FIntVector(0,0,islandSlot->TopChunkZ));
const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig;
recipe.seed=static_cast<std::uint32_t>(owner.Seed);
- recipe.geometryVersion=8;
+ recipe.geometryVersion=planAroundPassages?10:8;
+ if(planAroundPassages)recipe.primaryIslandCount=9;
recipe.terrainFollowingRoute=static_cast<std::uint32_t>(passage.Style)==0;
recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons;
recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons;
@@ -1406,43 +1409,79 @@ namespace StandaloneWorldAssemblyPrototype
recipe.islandDensityQ16=WorldAssemblyQ16(std::clamp<double>(params.IslandDensity,0.0,1.0));
recipe.spacingSubvoxels=WorldAssemblyFixed(params.IslandSpacing*ownerToCoreScale);
recipe.minimumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMinRadius*ownerToCoreScale);
- recipe.maximumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMaxRadius*ownerToCoreScale);
+ recipe.maximumRadiusSubvoxels=WorldAssemblyFixed((planAroundPassages
+ ?28.0:params.IslandMaxRadius)*ownerToCoreScale);
recipe.thicknessRatioQ16=WorldAssemblyQ16(params.ThicknessRatio);
recipe.verticalJitterQ16=WorldAssemblyQ16(params.VerticalJitter);
recipe.topFlattenQ16=WorldAssemblyQ16(params.TopFlatten);
- recipe.blendRadiusSubvoxels=WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale);
+ recipe.blendRadiusSubvoxels=planAroundPassages
+ ?2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
+ :WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale);
+ if(planAroundPassages)
+ recipe.contourWarpSubvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
recipe.roughnessSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness*ownerToCoreScale);
+ if(planAroundPassages)
+ recipe.roughnessSubvoxels=std::min(recipe.roughnessSubvoxels,
+ VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/2);
recipe.boundarySealSubvoxels=WorldAssemblyFixed(params.BoundarySealThickness*ownerToCoreScale);
+ if(planAroundPassages)
+ {
+ recipe.routeConstraints.version=1;
+ recipe.routeConstraints.capsuleRadiusSubvoxels=348;
+ recipe.routeConstraints.capsuleHeightSubvoxels=1802;
+ recipe.routeConstraints.maximumGradeQ16=17560;
+ recipe.routeConstraints.sweepSpacingSubvoxels=128;
+ recipe.routeConstraints.supportProbeRadiusSubvoxels=230;
+ recipe.routeConstraints.supportSearchDepthSubvoxels=512;
+ recipe.routeConstraints.minimumClearanceSubvoxels=128;
+ recipe.routeConstraints.minimumSupportProbes=3;
+ }
const auto ownerVoxelsToCoreQ=[&](double value)
{return static_cast<std::int64_t>(std::llround(value*owner.OwnerVoxelSizeMicrons
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));};
recipe.anchorIslandOrdinal=recipe.primaryIslandCount-1;
recipe.anchorPlacementRadiusSubvoxels=ownerVoxelsToCoreQ(passage.DistanceMax);
- const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius);
- const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius),
- ownerVoxelsToCoreQ(36.0));
- const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius),
- ownerVoxelsToCoreQ(24.0));
- const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0);
- // Retain a 4.5-metre mouth for capsule clearance while keeping the long
- // ramp at three metres to leave more rock around the first coil.
- recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0);
- const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0);
- const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0);
- const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore;
- const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang;
- recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope)
- +wallReserve+rootTaperReserve;
- recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels,
- std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0),
- recipe.anchorRootRadiusSubvoxels+wallReserve));
+ if(planAroundPassages)
+ {
+ recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(4.0);
+ recipe.anchorRootRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(16.0),
+ recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
+ +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
+ recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.maximumRadiusSubvoxels,
+ recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
+ +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
+ }
+ else
+ {
+ const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius);
+ const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius),
+ ownerVoxelsToCoreQ(36.0));
+ const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius),
+ ownerVoxelsToCoreQ(24.0));
+ const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0);
+ recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0);
+ const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0);
+ const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0);
+ const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore;
+ const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang;
+ recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope)
+ +wallReserve+rootTaperReserve;
+ recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels,
+ std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0),
+ recipe.anchorRootRadiusSubvoxels+wallReserve));
+ }
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
+recipe.boundarySealSubvoxels;
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
+ auto planned=planAroundPassages
+ ?VoxelForgeCore::PlanFloatingIslandClusterAroundPassages(
+ recipe,resolvedLayout,islandSlot->StrateIndex,error)
+ :VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
if(!planned)return false;
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
planned->recipe,planned->plan);
+ const double metresPerSubvoxel=static_cast<double>(owner.CoreVoxelSizeMicrons)
+ /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const auto routeAudit=VoxelForgeCore::AuditFloatingIslandRoute(routeField);
std::printf("[FloatingIslandRouteAudit] points=%zu centers=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n",
planned->plan.route.size(),
@@ -1453,8 +1492,27 @@ namespace StandaloneWorldAssemblyPrototype
static_cast<unsigned long long>(routeAudit.unsupportedCenterSamples),
static_cast<unsigned long long>(routeAudit.gradeFailures),
routeAudit.passed?"PASS":"FAIL");
- const double metresPerSubvoxel=static_cast<double>(owner.CoreVoxelSizeMicrons)
- /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
+ VoxelForgeCore::FloatingIslandPassageClearanceAuditResult passageAudit;
+ if(planAroundPassages)
+ {
+ constexpr double PortalExclusionMetres=13.0;
+ passageAudit=VoxelForgeCore::AuditFloatingIslandPassageClearance(
+ routeField,resolvedLayout,islandSlot->StrateIndex,PortalExclusionMetres);
+ std::printf("[FloatingIslandPassageClearance] strate=%d tube_samples=%llu island_blocked=%llu route_checks=%llu route_blocked=%llu arrival_support=%u/5 departure_support=%u/5 clearance_excess_m=%.3f portal_exclusion_m=%.1f influence_reach_m=1.053 capsule_bound_m=%.3f status=%s\n",
+ islandSlot->StrateIndex,
+ static_cast<unsigned long long>(passageAudit.passageSamples),
+ static_cast<unsigned long long>(passageAudit.blockedIslandSamples),
+ static_cast<unsigned long long>(passageAudit.routeSamples),
+ static_cast<unsigned long long>(passageAudit.blockedRouteSamples),
+ passageAudit.arrivalMouthSupportProbes,
+ passageAudit.departureMouthSupportProbes,
+ passageAudit.minimumExcessClearanceSubvoxels*metresPerSubvoxel,
+ passageAudit.portalExclusionMetres,
+ planned->recipe.routeConstraints.capsuleHeightSubvoxels*metresPerSubvoxel*0.5,
+ passageAudit.passed?"PASS":"FAIL");
+ if(!routeAudit.passed||!passageAudit.passed)
+ {error="corridor-aware floating-island route or passage audit failed";return false;}
+ }
const double bedrockTopMetres=(minimumZ+recipe.boundarySealSubvoxels)*metresPerSubvoxel;
const double ceilingBottomMetres=(maximumZ-recipe.boundarySealSubvoxels)*metresPerSubvoxel;
for(const auto& island:planned->plan.islands)
@@ -1473,18 +1531,82 @@ namespace StandaloneWorldAssemblyPrototype
assembly.floatingIslands.push_back(std::move(islands));
if(!assembly.finalize(error))return false;
+ if(planAroundPassages)
+ {
+ auto resolvedStratum=std::find_if(resolvedLayout.strates.begin(),
+ resolvedLayout.strates.end(),[&](const auto& item)
+ {return item.strateIndex==islandSlot->StrateIndex;});
+ if(resolvedStratum==resolvedLayout.strates.end())
+ {error="resolved FloatingIslands stratum disappeared before native anchor attachment";return false;}
+ resolvedStratum->recipeVersion=assembly.floatingIslands.back().manifest.recipe.geometryVersion;
+ resolvedStratum->anchors.erase(std::remove_if(resolvedStratum->anchors.begin(),
+ resolvedStratum->anchors.end(),[](const auto& anchor)
+ {return anchor.kind=="island_anchor"||anchor.kind=="passage_anchor"
+ ||anchor.kind=="route_point";}),resolvedStratum->anchors.end());
+ const auto& nativePlan=assembly.floatingIslands.back().manifest.plan;
+ const auto& nativeRecipe=assembly.floatingIslands.back().manifest.recipe;
+ for(std::size_t n=0;n<nativePlan.islands.size();++n)
+ {
+ const auto& island=nativePlan.islands[n];
+ VoxelForgeCore::ResolvedLayoutAnchorV2 anchor;
+ anchor.kind="island_anchor";anchor.stableId="island/"+std::to_string(n);
+ anchor.units="1/256 Core voxel";
+ anchor.values={island.centerSubvoxels[0],island.centerSubvoxels[1],
+ island.centerSubvoxels[2],island.radiusSubvoxels,island.topHalfSubvoxels};
+ resolvedStratum->anchors.push_back(std::move(anchor));
+ if(n==nativeRecipe.anchorIslandOrdinal)
+ {
+ VoxelForgeCore::ResolvedLayoutAnchorV2 passageAnchor;
+ passageAnchor.kind="passage_anchor";
+ passageAnchor.stableId="passage-anchor/island/"+std::to_string(n);
+ passageAnchor.units="1/256 Core voxel";
+ passageAnchor.values={island.centerSubvoxels[0],island.centerSubvoxels[1],
+ nativeRecipe.anchorRootRadiusSubvoxels,
+ nativeRecipe.anchorMouthOffsetSubvoxels,
+ nativeRecipe.anchorPlacementRadiusSubvoxels,
+ nativeRecipe.anchorRootBottomZSubvoxels,island.radiusSubvoxels};
+ if(!nativePlan.route.empty())
+ {
+ const auto& mouth=nativePlan.route.back().positionSubvoxels;
+ passageAnchor.values.insert(passageAnchor.values.end(),
+ {mouth[0],mouth[1],mouth[2]});
+ }
+ resolvedStratum->anchors.push_back(std::move(passageAnchor));
+ }
+ }
+ for(std::size_t n=0;n<nativePlan.route.size();++n)
+ {
+ const auto& point=nativePlan.route[n];
+ VoxelForgeCore::ResolvedLayoutAnchorV2 anchor;
+ anchor.kind="route_point";anchor.stableId="route/"+std::to_string(n);
+ anchor.units="1/256 Core voxel";
+ anchor.values={point.positionSubvoxels[0],point.positionSubvoxels[1],
+ point.positionSubvoxels[2],point.radiusSubvoxels};
+ resolvedStratum->anchors.push_back(std::move(anchor));
+ }
+ if(!resolvedLayout.finalize(error))return false;
+ }
+
VFJson::Value assemblyJson;
if(!VFJson::Parser(assembly.ToJson()).Parse(assemblyJson,error))return false;
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
if(!rootObject){error="owner snapshot root is not a JSON object";return false;}
(*rootObject)["world_assembly"]=std::move(assemblyJson);
- std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu recipe_hash=%016llx status=PASS\n",
+ std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu dispatch=FloatingIslands-native recipe_hash=%016llx status=PASS\n",
owner.Seed,islandSlot->StrateIndex,islandSlot->TopChunkZ,islandSlot->BottomChunkZ,
assembly.floatingIslands.back().manifest.plan.islands.size(),
static_cast<unsigned long long>(assembly.floatingIslands.back().manifest.recipe.contentHash));
return true;
}
+ inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
+ std::string& error)
+ {
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 unusedLayout;
+ return AttachFloatingIslandWorldAssembly(owner,root,assembly,unusedLayout,error,false);
+ }
+
inline const char* GeneratorKind(ECaveGeneratorType type)
{
switch(type)
@@ -1964,6 +2086,7 @@ namespace StandaloneWorldAssemblyPrototype
:it->manifest.plan.route.size()-1;
std::size_t resolvedRoutePointCount=it->manifest.plan.route.size();
if(it->manifest.recipe.geometryVersion>=5
+ &&it->manifest.recipe.geometryVersion<10
&&!it->manifest.plan.route.empty()
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
{