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 arrivalSubvoxels{}; + std::array departureSubvoxels{}; + }; + struct WorldAssemblyFeatureGroupBoundsV1 { std::array 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 PlanFloatingIslandCluster( const FloatingIslandRecipeV1& recipe, std::string& error); + VFCORE_API std::optional 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(anchorIsland->centerSubvoxels[0]), static_cast(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 PlanFloatingIslandCluster( return manifest; } +std::optional 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(floating_island_detail::Q) + * 1000000.0 / recipe.coreVoxelSizeMicrons; + const double dx = static_cast(endpoints.departureSubvoxels[0] + - endpoints.arrivalSubvoxels[0]); + const double dy = static_cast(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::llround(qPerMetre * 6.6))); + recipe.anchorMouthOffsetSubvoxels = std::min({desiredMouthOffset, + maximumMouthOffset, static_cast(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(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(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{ + floating_island_detail::RoundQ( + static_cast(endpoints.arrivalSubvoxels[0]) + + tangentX * along + normalX * side), + floating_island_detail::RoundQ( + static_cast(endpoints.arrivalSubvoxels[1]) + + tangentY * along + normalY * side)}; + }; + std::array, count> primaryXY{}; + primaryXY[0] = {endpoints.arrivalSubvoxels[0], endpoints.arrivalSubvoxels[1]}; + primaryXY[1] = localPoint(0.0, static_cast(step)); + primaryXY[2] = localPoint(static_cast(step), static_cast(step)); + primaryXY[3] = localPoint(static_cast(step), 0.0); + primaryXY[4] = localPoint(static_cast(step), -static_cast(step)); + primaryXY[5] = localPoint(0.0, -static_cast(step)); + primaryXY[6] = localPoint(-static_cast(step), -static_cast(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(step), + -0.23 * static_cast(step)); + const double departureRadial = std::hypot( + static_cast(endpoints.departureSubvoxels[0]), + static_cast(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 cumulative{}; + for (std::uint32_t i = 1; i < count; ++i) + { + const double segmentX = static_cast(primaryXY[i][0] - primaryXY[i - 1][0]); + const double segmentY = static_cast(primaryXY[i][1] - primaryXY[i - 1][1]); + const double length = std::hypot(segmentX, segmentY); + if (!(length > 0.0) || length > static_cast(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(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(endpoints.departureSubvoxels[0]), + static_cast(endpoints.departureSubvoxels[1])); + anchorTop = endTop + floating_island_detail::RoundQ( + static_cast(anchorTop) - mouthSurface); + } + std::array tops{}; + for (std::uint32_t i = 0; i < count; ++i) + { + const double t = cumulative[i] / totalLength; + tops[i] = floating_island_detail::RoundQ(static_cast(startTop) + + static_cast(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(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(primaryXY[segment + 1][0] + - primaryXY[segment][0]); + const double segmentY = static_cast(primaryXY[segment + 1][1] + - primaryXY[segment][1]); + const double length = std::hypot(segmentX, segmentY); + const std::uint32_t pieces = static_cast(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(piece) / pieces; + const auto x = floating_island_detail::RoundQ( + static_cast(primaryXY[segment][0]) + segmentX * t); + const auto y = floating_island_detail::RoundQ( + static_cast(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(endpoints.departureSubvoxels[0] + - primaryXY.back()[0]); + const double mouthLegY = static_cast(endpoints.departureSubvoxels[1] + - primaryXY.back()[1]); + const double mouthLegLength = std::hypot(mouthLegX, mouthLegY); + const std::uint32_t mouthLegPieces = static_cast(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(piece) / mouthLegPieces; + const auto x = floating_island_detail::RoundQ(static_cast(primaryXY.back()[0]) + + mouthLegX * t); + const auto y = floating_island_detail::RoundQ(static_cast(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 baseHeights; + std::vector 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(route[i].positionSubvoxels[0] + - route[i - 1].positionSubvoxels[0]); + const double dy = static_cast(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(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(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::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(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.anchorIslandOrdinalmanifest.plan.islands.size()) {