v9 plan binds the resolved mouths and passes layout, 3/3 interfaces, shell, ripple; the composed FI walk still fails (2 capsule collisions where passage/0-1 and 1-2 tubes carve the island route, 40 unsupported centers, a departure landing with 0/5 support). Owner stays on the compiled-analytic FI path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
512 lines
29 KiB
Diff
512 lines
29 KiB
Diff
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
index cb2b2dd..5a94ab8 100644
|
|
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
@@ -496,8 +496,9 @@ namespace VoxelForgeCore
|
|
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
|
// Version 226 unifies the passage influence reach in metres and converts it to core
|
|
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
|
- // support width/6 when deriving that reach.
|
|
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
|
|
+ // support width/6 when deriving that reach. Version 228 permits the native FI route
|
|
+ // planner to bind its fixed endpoints to the resolved passage mouths.
|
|
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228;
|
|
|
|
enum class FieldChannel : std::uint32_t
|
|
{
|
|
@@ -788,6 +789,15 @@ namespace VoxelForgeCore
|
|
std::uint64_t contentHash = 0;
|
|
};
|
|
|
|
+ // The two resolved inter-strate walk points owned by a FloatingIslands slot.
|
|
+ // The route planner treats these as fixed player-capsule centers and places
|
|
+ // the island chain between them.
|
|
+ struct FloatingIslandRouteEndpointsV1
|
|
+ {
|
|
+ std::array<std::int64_t, 3> arrivalSubvoxels{};
|
|
+ std::array<std::int64_t, 3> departureSubvoxels{};
|
|
+ };
|
|
+
|
|
struct WorldAssemblyFeatureGroupBoundsV1
|
|
{
|
|
std::array<std::int64_t, 3> minimumSubvoxels{};
|
|
@@ -1377,6 +1387,9 @@ 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> PlanFloatingIslandCluster(
|
|
+ const FloatingIslandRecipeV1& recipe,
|
|
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error);
|
|
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
|
|
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
|
|
std::uint64_t seed = 0x49534c414e444231ull);
|
|
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
index 33d97d6..42f855a 100644
|
|
--- a/Standalone/Core/src/FloatingIslandsInternal.h
|
|
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
@@ -285,7 +285,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 > 9
|
|
|| (recipe.geometryVersion == 1
|
|
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
|
|| recipe.unitsPerVoxel != Q
|
|
@@ -1473,7 +1473,8 @@ namespace floating_island_detail
|
|
static_cast<double>(anchorIsland->centerSubvoxels[0]),
|
|
static_cast<double>(anchorIsland->centerSubvoxels[1]));
|
|
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
|
|
- ||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
|
|
+ ||(recipe.geometryVersion<8
|
|
+ &&recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels)
|
|
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
>=anchorIsland->radiusSubvoxels)
|
|
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
|
|
@@ -1607,7 +1608,7 @@ namespace floating_island_detail
|
|
||to.ordinal==recipe.anchorIslandOrdinal);
|
|
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
|
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
|
- const double routeDeckLift=recipe.geometryVersion>=2
|
|
+ const double routeDeckLift=recipe.geometryVersion>=9?0.0:recipe.geometryVersion>=2
|
|
?(WorldAssemblyEvaluatorVersion>=178
|
|
?(westernSupportConnector
|
|
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
|
@@ -2264,6 +2265,335 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
return manifest;
|
|
}
|
|
|
|
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
+ const FloatingIslandRecipeV1& inputRecipe,
|
|
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error)
|
|
+{
|
|
+ FloatingIslandManifestV1 manifest;
|
|
+ manifest.recipe = inputRecipe;
|
|
+ auto& recipe = manifest.recipe;
|
|
+ recipe.geometryVersion = 9;
|
|
+ recipe.primaryIslandCount = 9;
|
|
+ recipe.anchorIslandOrdinal = recipe.primaryIslandCount - 1;
|
|
+ constexpr std::int64_t step = 192 * floating_island_detail::Q;
|
|
+ recipe.spacingSubvoxels = step;
|
|
+ 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;
|
|
+ }
|
|
+ for (const auto& value : endpoints.arrivalSubvoxels)
|
|
+ if (!floating_island_detail::ValidScalar(value))
|
|
+ { error = "resolved floating-island arrival mouth is outside the fixed-point range"; return std::nullopt; }
|
|
+ for (const auto& value : endpoints.departureSubvoxels)
|
|
+ if (!floating_island_detail::ValidScalar(value))
|
|
+ { error = "resolved floating-island departure mouth is outside the fixed-point range"; return std::nullopt; }
|
|
+
|
|
+ const double qPerMetre = static_cast<double>(floating_island_detail::Q)
|
|
+ * 1000000.0 / recipe.coreVoxelSizeMicrons;
|
|
+ const double dx = static_cast<double>(endpoints.departureSubvoxels[0]
|
|
+ - endpoints.arrivalSubvoxels[0]);
|
|
+ const double dy = static_cast<double>(endpoints.departureSubvoxels[1]
|
|
+ - endpoints.arrivalSubvoxels[1]);
|
|
+ const double endpointDistance = std::hypot(dx, dy);
|
|
+ const std::int64_t maximumMouthOffset = 64 * floating_island_detail::Q
|
|
+ - recipe.routeConstraints.capsuleRadiusSubvoxels - floating_island_detail::Q;
|
|
+ const std::int64_t desiredMouthOffset = std::max(recipe.anchorMouthOffsetSubvoxels,
|
|
+ static_cast<std::int64_t>(std::llround(qPerMetre * 6.6)));
|
|
+ recipe.anchorMouthOffsetSubvoxels = std::min({desiredMouthOffset,
|
|
+ maximumMouthOffset, static_cast<std::int64_t>(std::llround(endpointDistance))});
|
|
+ if (recipe.anchorMouthOffsetSubvoxels <= 0)
|
|
+ { error = "resolved island mouths cannot provide a bounded departure approach"; return std::nullopt; }
|
|
+ recipe.anchorRootRadiusSubvoxels = std::max(recipe.anchorRootRadiusSubvoxels,
|
|
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
+ + floating_island_detail::Q);
|
|
+ recipe.anchorIslandMinimumRadiusSubvoxels = std::max(recipe.minimumRadiusSubvoxels,
|
|
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
+ + floating_island_detail::Q + 1);
|
|
+ // Keep the resolved fixed mouth on a capsule-grade ramp at the anchor island lip.
|
|
+ recipe.topFlattenQ16 = std::max<std::int64_t>(recipe.topFlattenQ16, 52429);
|
|
+ recipe.bridgeHalfWidthSubvoxels = std::max(recipe.bridgeHalfWidthSubvoxels,
|
|
+ recipe.routeConstraints.capsuleRadiusSubvoxels + 2 * floating_island_detail::Q);
|
|
+ if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt;
|
|
+
|
|
+ auto& islands = manifest.plan.islands;
|
|
+ auto& edges = manifest.plan.clusterEdges;
|
|
+ auto& route = manifest.plan.route;
|
|
+ constexpr std::uint32_t count = 9;
|
|
+ double tangentX = endpointDistance > 1.0
|
|
+ ? dx / endpointDistance : 1.0;
|
|
+ double tangentY = endpointDistance > 1.0
|
|
+ ? dy / endpointDistance : 0.0;
|
|
+ if (endpointDistance <= 1.0)
|
|
+ {
|
|
+ const double angle = static_cast<double>(floating_island_detail::UnitQ16(
|
|
+ recipe.seed, 0x900000000ull, 0x524f555445ull))
|
|
+ * (2.0 * floating_island_detail::Pi / 65536.0);
|
|
+ tangentX = std::cos(angle);
|
|
+ tangentY = std::sin(angle);
|
|
+ }
|
|
+ const double normalX = -tangentY;
|
|
+ const double normalY = tangentX;
|
|
+ const auto localPoint = [&](double along, double side)
|
|
+ {
|
|
+ return std::array<std::int64_t, 2>{
|
|
+ floating_island_detail::RoundQ(
|
|
+ static_cast<double>(endpoints.arrivalSubvoxels[0])
|
|
+ + tangentX * along + normalX * side),
|
|
+ floating_island_detail::RoundQ(
|
|
+ static_cast<double>(endpoints.arrivalSubvoxels[1])
|
|
+ + tangentY * along + normalY * side)};
|
|
+ };
|
|
+ std::array<std::array<std::int64_t, 2>, count> primaryXY{};
|
|
+ primaryXY[0] = {endpoints.arrivalSubvoxels[0], endpoints.arrivalSubvoxels[1]};
|
|
+ primaryXY[1] = localPoint(0.0, static_cast<double>(step));
|
|
+ primaryXY[2] = localPoint(static_cast<double>(step), static_cast<double>(step));
|
|
+ primaryXY[3] = localPoint(static_cast<double>(step), 0.0);
|
|
+ primaryXY[4] = localPoint(static_cast<double>(step), -static_cast<double>(step));
|
|
+ primaryXY[5] = localPoint(0.0, -static_cast<double>(step));
|
|
+ primaryXY[6] = localPoint(-static_cast<double>(step), -static_cast<double>(step));
|
|
+ // Keep the return lane outside the arrival passage's spiral and on the negative
|
|
+ // side of the first connector, so the two bridges cannot cross in plan view.
|
|
+ // Keep the final approach radial at the passage mouth. This lets the shared
|
|
+ // passage resolver keep its authored route inside the placement disk.
|
|
+ primaryXY[7] = localPoint(-0.85 * static_cast<double>(step),
|
|
+ -0.23 * static_cast<double>(step));
|
|
+ const double departureRadial = std::hypot(
|
|
+ static_cast<double>(endpoints.departureSubvoxels[0]),
|
|
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
|
+ const double anchorScale = departureRadial > 1.0
|
|
+ ? std::max(0.0, (departureRadial - recipe.anchorMouthOffsetSubvoxels) / departureRadial)
|
|
+ : 0.0;
|
|
+ primaryXY[8] = {
|
|
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[0] * anchorScale),
|
|
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[1] * anchorScale)};
|
|
+
|
|
+ std::array<double, count> cumulative{};
|
|
+ for (std::uint32_t i = 1; i < count; ++i)
|
|
+ {
|
|
+ const double segmentX = static_cast<double>(primaryXY[i][0] - primaryXY[i - 1][0]);
|
|
+ const double segmentY = static_cast<double>(primaryXY[i][1] - primaryXY[i - 1][1]);
|
|
+ const double length = std::hypot(segmentX, segmentY);
|
|
+ if (!(length > 0.0) || length > static_cast<double>(256 * floating_island_detail::Q))
|
|
+ { error = "resolved island mouths require an unsupported serpentine segment length"; return std::nullopt; }
|
|
+ cumulative[i] = cumulative[i - 1] + length;
|
|
+ }
|
|
+ const double totalLength = cumulative.back();
|
|
+ if (!(totalLength > 0.0))
|
|
+ { error = "resolved floating-island mouths do not define a route"; return std::nullopt; }
|
|
+ const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
|
|
+ const std::int64_t routeOffset = capsuleHalf
|
|
+ + recipe.routeConstraints.minimumClearanceSubvoxels
|
|
+ + floating_island_detail::Q / 8;
|
|
+ const std::int64_t startTop = endpoints.arrivalSubvoxels[2] - routeOffset;
|
|
+ const std::int64_t endTop = endpoints.departureSubvoxels[2] - routeOffset;
|
|
+ const std::uint64_t anchorSite = 0x700000000ull + recipe.anchorIslandOrdinal;
|
|
+ auto anchorShape = floating_island_detail::MakeIsland(recipe,
|
|
+ recipe.anchorIslandOrdinal, primaryXY.back()[0], primaryXY.back()[1], endTop, anchorSite);
|
|
+ anchorShape.radiusSubvoxels = std::max(anchorShape.radiusSubvoxels,
|
|
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
+ anchorShape.topHalfSubvoxels = std::max<std::int64_t>(1,
|
|
+ anchorShape.radiusSubvoxels / 5);
|
|
+ anchorShape.centerSubvoxels[2] = endTop - anchorShape.topHalfSubvoxels;
|
|
+ std::int64_t anchorTop = endTop;
|
|
+ for (int iteration = 0; iteration < 4; ++iteration)
|
|
+ {
|
|
+ anchorShape.topZSubvoxels = anchorTop;
|
|
+ anchorShape.centerSubvoxels[2] = anchorTop - anchorShape.topHalfSubvoxels;
|
|
+ const double mouthSurface = floating_island_detail::IslandTopSurface(recipe,
|
|
+ anchorShape, static_cast<double>(endpoints.departureSubvoxels[0]),
|
|
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
|
+ anchorTop = endTop + floating_island_detail::RoundQ(
|
|
+ static_cast<double>(anchorTop) - mouthSurface);
|
|
+ }
|
|
+ std::array<std::int64_t, count> tops{};
|
|
+ for (std::uint32_t i = 0; i < count; ++i)
|
|
+ {
|
|
+ const double t = cumulative[i] / totalLength;
|
|
+ tops[i] = floating_island_detail::RoundQ(static_cast<double>(startTop)
|
|
+ + static_cast<double>(anchorTop - startTop) * t);
|
|
+ }
|
|
+
|
|
+ islands.reserve(count);
|
|
+ for (std::uint32_t i = 0; i < count; ++i)
|
|
+ {
|
|
+ const std::uint64_t site = 0x700000000ull + i;
|
|
+ auto island = floating_island_detail::MakeIsland(recipe, i,
|
|
+ primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
|
+ if (i == recipe.anchorIslandOrdinal)
|
|
+ {
|
|
+ island.radiusSubvoxels = std::max(island.radiusSubvoxels,
|
|
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
+ }
|
|
+ const std::int64_t interiorBottom = recipe.layoutMinimumZSubvoxels
|
|
+ + recipe.boundarySealSubvoxels;
|
|
+ while (island.radiusSubvoxels > recipe.minimumRadiusSubvoxels
|
|
+ && floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
|
+ < interiorBottom)
|
|
+ --island.radiusSubvoxels;
|
|
+ const std::int64_t requiredRadius = i == recipe.anchorIslandOrdinal
|
|
+ ? recipe.anchorIslandMinimumRadiusSubvoxels : recipe.minimumRadiusSubvoxels;
|
|
+ if (island.radiusSubvoxels < requiredRadius
|
|
+ || floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
|
+ < interiorBottom)
|
|
+ {
|
|
+ error = "resolved-mouth island " + std::to_string(i) + " cannot fit above sealed bedrock"
|
|
+ + " (radius=" + std::to_string(island.radiusSubvoxels)
|
|
+ + ", required=" + std::to_string(requiredRadius)
|
|
+ + ", top=" + std::to_string(tops[i])
|
|
+ + ", bottom=" + std::to_string(floating_island_detail::IslandBottom(
|
|
+ recipe, tops[i], island.radiusSubvoxels))
|
|
+ + ", interior_bottom=" + std::to_string(interiorBottom)
|
|
+ + ", anchor_top=" + std::to_string(anchorTop)
|
|
+ + ", mouth_offset=" + std::to_string(recipe.anchorMouthOffsetSubvoxels) + ")";
|
|
+ return std::nullopt;
|
|
+ }
|
|
+ island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
|
+ island.topZSubvoxels = tops[i];
|
|
+ island.centerSubvoxels[2] = tops[i] - island.topHalfSubvoxels;
|
|
+ island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
|
+ island.topZSubvoxels, island.radiusSubvoxels);
|
|
+ floating_island_detail::SetIslandBounds(recipe, island);
|
|
+ islands.push_back(std::move(island));
|
|
+ }
|
|
+ for (std::uint32_t i = 0; i + 1 < count; ++i)
|
|
+ edges.push_back(floating_island_detail::MakeConnector(recipe,
|
|
+ islands[i], islands[i + 1], 0x600000000ull + i));
|
|
+
|
|
+ FloatingIslandFieldV1 planningField(recipe, manifest.plan);
|
|
+ if (!planningField.isValid())
|
|
+ { error = "resolved-mouth island field did not pass the Core field contract"; return std::nullopt; }
|
|
+ const std::int64_t routeRadius = 4 * floating_island_detail::Q;
|
|
+ std::uint32_t ordinal = 0;
|
|
+ for (std::uint32_t segment = 0; segment + 1 < count; ++segment)
|
|
+ {
|
|
+ const double segmentX = static_cast<double>(primaryXY[segment + 1][0]
|
|
+ - primaryXY[segment][0]);
|
|
+ const double segmentY = static_cast<double>(primaryXY[segment + 1][1]
|
|
+ - primaryXY[segment][1]);
|
|
+ const double length = std::hypot(segmentX, segmentY);
|
|
+ const std::uint32_t pieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
+ std::ceil(length / (4.0 * floating_island_detail::Q))));
|
|
+ for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece)
|
|
+ {
|
|
+ const double t = static_cast<double>(piece) / pieces;
|
|
+ const auto x = floating_island_detail::RoundQ(
|
|
+ static_cast<double>(primaryXY[segment][0]) + segmentX * t);
|
|
+ const auto y = floating_island_detail::RoundQ(
|
|
+ static_cast<double>(primaryXY[segment][1]) + segmentY * t);
|
|
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
|
+ recipe, manifest.plan, x, y);
|
|
+ if (!surface)
|
|
+ { error = "resolved-mouth island chain has no capsule-width support along a connector"; return std::nullopt; }
|
|
+ WorldAssemblyRoutePointV1 point;
|
|
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
|
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
|
+ point.radiusSubvoxels = routeRadius;
|
|
+ if (segment == 0 && piece == 0)
|
|
+ point.positionSubvoxels = endpoints.arrivalSubvoxels;
|
|
+ route.push_back(point);
|
|
+ ++ordinal;
|
|
+ }
|
|
+ }
|
|
+ const double mouthLegX = static_cast<double>(endpoints.departureSubvoxels[0]
|
|
+ - primaryXY.back()[0]);
|
|
+ const double mouthLegY = static_cast<double>(endpoints.departureSubvoxels[1]
|
|
+ - primaryXY.back()[1]);
|
|
+ const double mouthLegLength = std::hypot(mouthLegX, mouthLegY);
|
|
+ const std::uint32_t mouthLegPieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
+ std::ceil(mouthLegLength / (4.0 * floating_island_detail::Q))));
|
|
+ for (std::uint32_t piece = 1; piece <= mouthLegPieces; ++piece)
|
|
+ {
|
|
+ const double t = static_cast<double>(piece) / mouthLegPieces;
|
|
+ const auto x = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[0])
|
|
+ + mouthLegX * t);
|
|
+ const auto y = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[1])
|
|
+ + mouthLegY * t);
|
|
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
|
+ recipe, manifest.plan, x, y);
|
|
+ if (!surface)
|
|
+ { error = "resolved island passage mouth has no capsule-width support along its anchor landing at x="
|
|
+ + std::to_string(x) + ", y=" + std::to_string(y) + ", piece="
|
|
+ + std::to_string(piece) + "/" + std::to_string(mouthLegPieces)
|
|
+ + ", anchor_radius=" + std::to_string(anchorShape.radiusSubvoxels); return std::nullopt; }
|
|
+ WorldAssemblyRoutePointV1 point;
|
|
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
|
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
|
+ point.radiusSubvoxels = routeRadius;
|
|
+ if (piece == mouthLegPieces)
|
|
+ point.positionSubvoxels = endpoints.departureSubvoxels;
|
|
+ route.push_back(point);
|
|
+ ++ordinal;
|
|
+ }
|
|
+
|
|
+ const double maximumGrade = floating_island_detail::Q16ToDouble(
|
|
+ recipe.routeConstraints.maximumGradeQ16) * 0.95;
|
|
+ std::vector<std::int64_t> baseHeights;
|
|
+ std::vector<double> routeDistance(route.size(), 0.0);
|
|
+ baseHeights.reserve(route.size());
|
|
+ for (std::size_t i = 0; i < route.size(); ++i)
|
|
+ {
|
|
+ baseHeights.push_back(route[i].positionSubvoxels[2]);
|
|
+ if (i > 0)
|
|
+ {
|
|
+ const double dx = static_cast<double>(route[i].positionSubvoxels[0]
|
|
+ - route[i - 1].positionSubvoxels[0]);
|
|
+ const double dy = static_cast<double>(route[i].positionSubvoxels[1]
|
|
+ - route[i - 1].positionSubvoxels[1]);
|
|
+ routeDistance[i] = routeDistance[i - 1] + std::hypot(dx, dy);
|
|
+ }
|
|
+ }
|
|
+ for (std::size_t i = 0; i < route.size(); ++i)
|
|
+ {
|
|
+ double profile = static_cast<double>(baseHeights[i]);
|
|
+ std::size_t profileSource = i;
|
|
+ for (std::size_t j = 0; j < route.size(); ++j)
|
|
+ {
|
|
+ const double pathDistance = std::abs(routeDistance[i] - routeDistance[j]);
|
|
+ const double candidate = static_cast<double>(baseHeights[j])
|
|
+ - maximumGrade * pathDistance;
|
|
+ if (candidate > profile)
|
|
+ {
|
|
+ profile = candidate;
|
|
+ profileSource = j;
|
|
+ }
|
|
+ }
|
|
+ const std::int64_t plannedHeight = floating_island_detail::RoundQ(profile);
|
|
+ const bool fixedEndpoint = i == 0 || i + 1 == route.size();
|
|
+ if (fixedEndpoint && plannedHeight != baseHeights[i])
|
|
+ {
|
|
+ error = "resolved fixed island mouth endpoint " + std::to_string(i)
|
|
+ + " needs " + std::to_string(plannedHeight - baseHeights[i])
|
|
+ + " extra subvoxels from route sample " + std::to_string(profileSource)
|
|
+ + " at z=" + std::to_string(baseHeights[profileSource])
|
|
+ + " and path distance=" + std::to_string(static_cast<std::int64_t>(
|
|
+ std::abs(routeDistance[i] - routeDistance[profileSource])))
|
|
+ + " (arrival_z="
|
|
+ + std::to_string(endpoints.arrivalSubvoxels[2]) + ", departure_z="
|
|
+ + std::to_string(endpoints.departureSubvoxels[2]) + ", path_length="
|
|
+ + std::to_string(static_cast<std::int64_t>(routeDistance.back())) + ")";
|
|
+ return std::nullopt;
|
|
+ }
|
|
+ route[i].positionSubvoxels[2] = plannedHeight;
|
|
+ }
|
|
+ if (!manifest.finalize(error)) return std::nullopt;
|
|
+ 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 = "resolved-mouth floating-island plan did not canonical-round-trip"; return std::nullopt; }
|
|
+ return manifest;
|
|
+}
|
|
+
|
|
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
|
|
{}
|
|
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
index a0cb0a7..468e315 100644
|
|
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
@@ -1826,12 +1826,59 @@ 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()))
|
|
- &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
- *ownerWorld,ownerJsonRoot,worldAssembly,error))
|
|
- {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
+ const bool attachFloatingIslandPlan=(!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
|
+ ||(options.NativeOwnerFloatingIslandsRecipe&&worldAssembly.floatingIslands.empty());
|
|
+ if(attachFloatingIslandPlan)
|
|
+ {
|
|
+ const FStrateSlot* islandSlot=nullptr;
|
|
+ if(ownerWorld->Manager)
|
|
+ for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
|
+ if(slot.Definition&&slot.Definition->GeneratorType==ECaveGeneratorType::FloatingIslands)
|
|
+ {islandSlot=&slot;break;}
|
|
+ if(!islandSlot)
|
|
+ {std::fprintf(stderr,"Floating-island endpoint resolution found no FloatingIslands strate.\n");return 2;}
|
|
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 mouthLayout;
|
|
+ if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
|
+ *ownerWorld,worldAssembly,"floating-islands",mouthLayout,error))
|
|
+ {std::fprintf(stderr,"Pre-plan passage layout failed: %s\n",error.c_str());return 2;}
|
|
+ VoxelForgeCore::FloatingIslandRouteEndpointsV1 endpoints;
|
|
+ bool hasArrival=false,hasDeparture=false;
|
|
+ for(const auto& passage:mouthLayout.passages)
|
|
+ {
|
|
+ const std::size_t lastSlash=passage.stableId.rfind('/');
|
|
+ if(lastSlash==std::string::npos||passage.stableId.substr(lastSlash+1)!="0")continue;
|
|
+ if(passage.lowerStrateIndex==islandSlot->StrateIndex&&!hasArrival)
|
|
+ {
|
|
+ endpoints.arrivalSubvoxels=passage.lowerMouthSubvoxels;
|
|
+ hasArrival=true;
|
|
+ }
|
|
+ if(passage.upperStrateIndex==islandSlot->StrateIndex&&!hasDeparture)
|
|
+ {
|
|
+ endpoints.departureSubvoxels=passage.upperMouthSubvoxels;
|
|
+ hasDeparture=true;
|
|
+ }
|
|
+ }
|
|
+ if(!hasArrival||!hasDeparture)
|
|
+ {std::fprintf(stderr,"Resolved passage layout does not provide both FI route mouths (arrival=%s departure=%s).\n",
|
|
+ hasArrival?"yes":"no",hasDeparture?"yes":"no");return 2;}
|
|
+ std::printf("[FloatingIslandResolvedMouths] strate=%d arrival_m=(%.3f,%.3f,%.3f) departure_m=(%.3f,%.3f,%.3f) status=PASS\n",
|
|
+ islandSlot->StrateIndex,
|
|
+ endpoints.arrivalSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
|
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
+ endpoints.arrivalSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
|
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
+ endpoints.arrivalSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
|
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
+ endpoints.departureSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
|
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
+ endpoints.departureSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
|
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
+ endpoints.departureSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
|
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
+ if(!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
+ *ownerWorld,ownerJsonRoot,worldAssembly,endpoints,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();
|
|
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
|
|
index dbe2a7c..9790f2f 100644
|
|
--- a/Standalone/Source/WorldAssemblyPrototype.h
|
|
+++ b/Standalone/Source/WorldAssemblyPrototype.h
|
|
@@ -923,7 +923,8 @@ namespace StandaloneWorldAssemblyPrototype
|
|
}
|
|
|
|
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
|
|
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
|
|
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
|
|
+ const VoxelForgeCore::FloatingIslandRouteEndpointsV1& endpoints,std::string& error)
|
|
{
|
|
if(!owner.Manager)
|
|
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
|
|
@@ -996,7 +997,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
|
|
+recipe.boundarySealSubvoxels;
|
|
|
|
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
|
|
+ auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,endpoints,error);
|
|
if(!planned)return false;
|
|
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
|
|
planned->recipe,planned->plan);
|
|
@@ -1217,6 +1218,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<9
|
|
&&!it->manifest.plan.route.empty()
|
|
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
|
|
{
|