wip(assembler): M3 round 3 -- separated islands + Unreal island route (owner build 18:46)
Islands read as separate (91.89% open sky at mid-height), underside cavity closed, rim teeth were capture shading. Island manifest import + BuildHost proof passed on the pre-cache tree; M1 SurfaceWorld and M2 V1/V2 gates passed. Known open: final XY-column cache compiled in the owner build but is unverified; island LOD0 p95 ~155 ms misses the 133 ms sprint deadline (legacy ~43 ms); the island manifest only fits the BuildHost fixture layout (owner's world: Core init fails, empty world) -> round 4 builds plans inside Unreal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
4afea7c802
commit
48fa028086
37 files changed
+3788
-107
No files matched your search
@@ -278,23 +278,6 @@ namespace VoxelForgeCore
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
struct VFCORE_API WorldAssemblyManifest
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
WorldAssemblyRecipeV1 recipe;
|
||||
WorldAssemblyPlanV1 plan;
|
||||
std::uint64_t contentHash = 0;
|
||||
|
||||
// JSON is the reviewed season-manifest payload. Hashes use canonical ordered fields,
|
||||
// little-endian integers and FNV-1a 64; textual key order and whitespace do not matter.
|
||||
// Le JSON est le payload de saison valide; les entiers little-endian sont haches en FNV-1a 64, sans dependance a la mise en forme.
|
||||
static std::optional<WorldAssemblyManifest> FromJson(
|
||||
std::string_view json, std::string& error);
|
||||
bool finalize(std::string& error);
|
||||
bool validate(std::string& error) const;
|
||||
std::string ToJson() const;
|
||||
};
|
||||
|
||||
// Reusable finite 3D island features. The recipe owns fixed-scale shape ranges; the plan
|
||||
// serializes the seeded anchors, connected cluster graph, route and sharp-ready connectors.
|
||||
// Reutilisable dans toute recette : le plan porte les ancres, le graphe, la route et les plans nets.
|
||||
@@ -305,6 +288,12 @@ namespace VoxelForgeCore
|
||||
// bounded lobed roots and arched spans without changing the feature-family schema.
|
||||
std::uint32_t geometryVersion = 2;
|
||||
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
|
||||
// Import scale is explicit: legacy owner voxels are 12.5 cm and Core voxels are 25 cm.
|
||||
// L'echelle d'import est explicite : voxel owner de 12,5 cm et voxel Core de 25 cm.
|
||||
std::uint32_t ownerVoxelSizeMicrons = 125000;
|
||||
std::uint32_t coreVoxelSizeMicrons = 250000;
|
||||
std::uint32_t ownerToCoreScaleNumerator = 1;
|
||||
std::uint32_t ownerToCoreScaleDenominator = 2;
|
||||
std::uint32_t seed = 0;
|
||||
std::int64_t layoutOriginXSubvoxels = 0;
|
||||
std::int64_t layoutOriginYSubvoxels = 0;
|
||||
@@ -327,9 +316,9 @@ namespace VoxelForgeCore
|
||||
std::int64_t rootDepthRatioQ16 = 49152; // 0.75 radius, profile 2 only
|
||||
std::int64_t blendRadiusSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t contourWarpSubvoxels = 6 * WorldAssemblySubvoxelsPerVoxel + 77; // 6.3
|
||||
std::int64_t roughnessSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel / 2;
|
||||
std::int64_t roughnessSubvoxels = WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t boundarySealSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t bridgeHalfWidthSubvoxels = 6 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t bridgeHalfWidthSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t bridgeThicknessSubvoxels = 4 * WorldAssemblySubvoxelsPerVoxel;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::uint64_t contentHash = 0;
|
||||
@@ -384,6 +373,32 @@ namespace VoxelForgeCore
|
||||
std::string ToJson() const;
|
||||
};
|
||||
|
||||
struct WorldAssemblyFloatingIslandPlanV1
|
||||
{
|
||||
std::int32_t strateIndex = -1;
|
||||
std::int32_t topChunkZ = 0;
|
||||
std::int32_t bottomChunkZ = 0;
|
||||
FloatingIslandManifestV1 manifest;
|
||||
};
|
||||
|
||||
struct VFCORE_API WorldAssemblyManifest
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
WorldAssemblyRecipeV1 recipe;
|
||||
WorldAssemblyPlanV1 plan;
|
||||
std::vector<WorldAssemblyFloatingIslandPlanV1> floatingIslands;
|
||||
std::uint64_t contentHash = 0;
|
||||
|
||||
// JSON is the reviewed season-manifest payload. Hashes use canonical ordered fields,
|
||||
// little-endian integers and FNV-1a 64; textual key order and whitespace do not matter.
|
||||
// Le JSON est le payload de saison valide; les entiers little-endian sont haches en FNV-1a 64, sans dependance a la mise en forme.
|
||||
static std::optional<WorldAssemblyManifest> FromJson(
|
||||
std::string_view json, std::string& error);
|
||||
bool finalize(std::string& error);
|
||||
bool validate(std::string& error) const;
|
||||
std::string ToJson() const;
|
||||
};
|
||||
|
||||
struct FloatingIslandBoundsAuditResult
|
||||
{
|
||||
std::uint64_t boxesTested = 0;
|
||||
|
||||
@@ -158,6 +158,10 @@ namespace floating_island_detail
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, recipe.schemaVersion, 4);
|
||||
HashWord(hash, recipe.unitsPerVoxel, 4);
|
||||
HashWord(hash, recipe.ownerVoxelSizeMicrons, 4);
|
||||
HashWord(hash, recipe.coreVoxelSizeMicrons, 4);
|
||||
HashWord(hash, recipe.ownerToCoreScaleNumerator, 4);
|
||||
HashWord(hash, recipe.ownerToCoreScaleDenominator, 4);
|
||||
HashWord(hash, recipe.seed, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutOriginXSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutOriginYSubvoxels), 8);
|
||||
@@ -258,6 +262,10 @@ namespace floating_island_detail
|
||||
|| (recipe.geometryVersion == 1
|
||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||
|| recipe.unitsPerVoxel != Q
|
||||
|| recipe.ownerVoxelSizeMicrons != 125000
|
||||
|| recipe.coreVoxelSizeMicrons != 250000
|
||||
|| recipe.ownerToCoreScaleNumerator != 1
|
||||
|| recipe.ownerToCoreScaleDenominator != 2
|
||||
|| recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9
|
||||
|| recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels
|
||||
|| recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q
|
||||
@@ -339,6 +347,30 @@ namespace floating_island_detail
|
||||
return static_cast<double>(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c);
|
||||
}
|
||||
|
||||
inline double LobePhase(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandAnchorV1& island)
|
||||
{
|
||||
return static_cast<double>(Roll(recipe.seed, island.stableId, 0x4c4f4245ull)
|
||||
& 0xffffffffull) * (2.0 * Pi / 4294967296.0);
|
||||
}
|
||||
|
||||
inline double LobeOffsetWithPhase(const FloatingIslandRecipeV1& recipe,
|
||||
double angle,double zSubvoxels,double phase)
|
||||
{
|
||||
const double z = zSubvoxels / static_cast<double>(Q);
|
||||
const double a = std::sin(3.0 * angle + phase + z * 0.017);
|
||||
const double b = std::sin(5.0 * angle - phase * 0.73 - z * 0.023);
|
||||
const double c = std::sin(8.0 * angle + phase * 1.41 + z * 0.011);
|
||||
return static_cast<double>(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c);
|
||||
}
|
||||
|
||||
inline double LobeOffsetAtAngle(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandAnchorV1& island,
|
||||
double angle,double zSubvoxels)
|
||||
{
|
||||
return LobeOffsetWithPhase(recipe,angle,zSubvoxels,LobePhase(recipe,island));
|
||||
}
|
||||
|
||||
inline double Roughness(const FloatingIslandRecipeV1& recipe,
|
||||
std::uint64_t featureId, double x, double y, double z)
|
||||
{
|
||||
@@ -382,6 +414,83 @@ namespace floating_island_detail
|
||||
return static_cast<double>(island.topZSubvoxels) - drop;
|
||||
}
|
||||
|
||||
struct FIslandColumnCacheEntry
|
||||
{
|
||||
bool bValid=false;
|
||||
std::uint32_t Seed=0;
|
||||
std::uint64_t StableId=0;
|
||||
std::int64_t SampleX=0,SampleY=0,CenterX=0,CenterY=0,TopZ=0,Radius=0,TopHalf=0;
|
||||
std::int64_t TopFlattenQ16=0,ContourWarp=0;
|
||||
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
||||
};
|
||||
|
||||
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island,
|
||||
double x,double y)
|
||||
{
|
||||
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
||||
// across Z samples without changing the field value or touching shared world state.
|
||||
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
||||
constexpr std::size_t Ways=4;
|
||||
constexpr std::size_t SetCount=2048;
|
||||
static_assert((SetCount&(SetCount-1))==0);
|
||||
static thread_local std::array<FIslandColumnCacheEntry,SetCount*Ways> Cache{};
|
||||
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
||||
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
||||
std::uint64_t IndexKey=island.stableId
|
||||
^(static_cast<std::uint64_t>(SampleX)*0x9e3779b97f4a7c15ull)
|
||||
^(static_cast<std::uint64_t>(SampleY)*0xc2b2ae3d27d4eb4full)
|
||||
^(static_cast<std::uint64_t>(recipe.seed)<<23);
|
||||
IndexKey^=IndexKey>>33;
|
||||
IndexKey*=0xff51afd7ed558ccdull;
|
||||
IndexKey^=IndexKey>>33;
|
||||
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
||||
FIslandColumnCacheEntry* Replacement=&Cache[Base+((IndexKey>>11)&(Ways-1))];
|
||||
for(std::size_t Way=0;Way<Ways;++Way)
|
||||
{
|
||||
FIslandColumnCacheEntry& Entry=Cache[Base+Way];
|
||||
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
|
||||
&&Entry.StableId==island.stableId
|
||||
&&Entry.SampleX==SampleX&&Entry.SampleY==SampleY
|
||||
&&Entry.CenterX==island.centerSubvoxels[0]
|
||||
&&Entry.CenterY==island.centerSubvoxels[1]
|
||||
&&Entry.TopZ==island.topZSubvoxels
|
||||
&&Entry.Radius==island.radiusSubvoxels
|
||||
&&Entry.TopHalf==island.topHalfSubvoxels
|
||||
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
||||
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
||||
if(bKeyMatches)return Entry;
|
||||
if(!Entry.bValid)Replacement=&Entry;
|
||||
}
|
||||
|
||||
FIslandColumnCacheEntry& Entry=*Replacement;
|
||||
Entry.bValid=true;
|
||||
Entry.Seed=recipe.seed;
|
||||
Entry.StableId=island.stableId;
|
||||
Entry.SampleX=SampleX;
|
||||
Entry.SampleY=SampleY;
|
||||
Entry.CenterX=island.centerSubvoxels[0];
|
||||
Entry.CenterY=island.centerSubvoxels[1];
|
||||
Entry.TopZ=island.topZSubvoxels;
|
||||
Entry.Radius=island.radiusSubvoxels;
|
||||
Entry.TopHalf=island.topHalfSubvoxels;
|
||||
Entry.TopFlattenQ16=recipe.topFlattenQ16;
|
||||
Entry.ContourWarp=recipe.contourWarpSubvoxels;
|
||||
Entry.DeltaX=x-static_cast<double>(island.centerSubvoxels[0]);
|
||||
Entry.DeltaY=y-static_cast<double>(island.centerSubvoxels[1]);
|
||||
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
||||
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
||||
Entry.LobePhase=LobePhase(recipe,island);
|
||||
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
||||
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
|
||||
Entry.LobePhase));
|
||||
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
||||
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
||||
Entry.TopSurface=static_cast<double>(island.topZSubvoxels)-Drop;
|
||||
return Entry;
|
||||
}
|
||||
|
||||
inline std::int64_t ScaledQ16(std::int64_t value, std::int64_t scaleQ16)
|
||||
{ return (value * scaleQ16 + Q16 / 2) / Q16; }
|
||||
|
||||
@@ -406,42 +515,104 @@ namespace floating_island_detail
|
||||
inline std::int32_t RootCount(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandAnchorV1& island)
|
||||
{
|
||||
return 3 + static_cast<std::int32_t>(Roll(recipe.seed, island.stableId,
|
||||
0x524f4f54434e54ull) & 1ull);
|
||||
// Keep four separate side lobes and a full-depth central buttress to close the measured
|
||||
// underside cavity without driving the outer roots to one point.
|
||||
// Garder quatre lobes lateraux et un renfort central profond pour fermer la cavite mesuree.
|
||||
return 5;
|
||||
}
|
||||
|
||||
inline double UnitHash(std::uint64_t hash)
|
||||
{ return static_cast<double>(hash & 0xffffull) / 65535.0; }
|
||||
|
||||
struct FRootLobeParameters
|
||||
{
|
||||
double RootX=0.0,RootY=0.0,CenterZ=0.0;
|
||||
double RadiusXSquared=1.0,RadiusYSquared=1.0,VerticalRadiusSquared=1.0;
|
||||
double MinimumRadius=1.0;
|
||||
};
|
||||
|
||||
struct FRootLobeCacheEntry
|
||||
{
|
||||
bool bValid=false;
|
||||
std::uint32_t Seed=0;
|
||||
std::uint64_t StableId=0;
|
||||
std::int32_t RootIndex=0,GeometryVersion=0;
|
||||
std::array<std::int64_t,3> Center{};
|
||||
std::int64_t TopZ=0,Radius=0,TaperEndQ16=0;
|
||||
std::int64_t ThicknessRatioQ16=0,CrustDepthBonusQ16=0,RootDepthRatioQ16=0;
|
||||
FRootLobeParameters Parameters{};
|
||||
};
|
||||
|
||||
inline double RootLobeDensity(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandAnchorV1& island,
|
||||
std::int32_t rootIndex, double x, double y, double z)
|
||||
{
|
||||
constexpr std::array<std::array<double, 2>, 4> pattern{{
|
||||
{{-0.92, -0.12}}, {{0.28, 0.98}}, {{0.83, -0.42}}, {{-0.16, -0.94}}}};
|
||||
const std::uint64_t salt = 0x524f4f540000ull + static_cast<std::uint64_t>(rootIndex);
|
||||
const std::uint64_t roll = Roll(recipe.seed, island.stableId, salt);
|
||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||
const double rootDepth = radius * Q16ToDouble(recipe.rootDepthRatioQ16);
|
||||
const double crustBottom = static_cast<double>(IslandCrustBottom(recipe, island));
|
||||
const double orbit = radius * (0.17 + 0.07 * UnitHash(roll));
|
||||
const double rootX = static_cast<double>(island.centerSubvoxels[0])
|
||||
+ pattern[static_cast<std::size_t>(rootIndex)][0] * orbit;
|
||||
const double rootY = static_cast<double>(island.centerSubvoxels[1])
|
||||
+ pattern[static_cast<std::size_t>(rootIndex)][1] * orbit;
|
||||
const double shortRootVariation = UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x4c454eull));
|
||||
const double lengthRatio = rootIndex == 0 ? 1.0
|
||||
: Q16ToDouble(island.taperEndQ16) * (0.82 + 0.18 * shortRootVariation);
|
||||
const double lowerDepth = rootDepth * lengthRatio;
|
||||
const double embedDepth = rootDepth * (rootIndex == 0 ? 0.30 : 0.22);
|
||||
const double verticalRadius = 0.5 * (lowerDepth + embedDepth);
|
||||
const double centerZ = crustBottom + 0.5 * (embedDepth - lowerDepth);
|
||||
const double rx = radius * (0.15 + 0.055 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5258ull)));
|
||||
const double ry = radius * (0.15 + 0.065 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5259ull)));
|
||||
const double q = (x - rootX) * (x - rootX) / (rx * rx)
|
||||
+ (y - rootY) * (y - rootY) / (ry * ry)
|
||||
+ (z - centerZ) * (z - centerZ) / (verticalRadius * verticalRadius);
|
||||
return (std::sqrt(std::max(q, 0.0)) - 1.0) * std::min({rx, ry, verticalRadius});
|
||||
// Cache derived root constants per worker. The complete immutable input key keeps this
|
||||
// value-only cache safe across worlds, epochs and worker schedules.
|
||||
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
||||
constexpr std::size_t CacheSize=1024;
|
||||
static_assert((CacheSize&(CacheSize-1))==0);
|
||||
static thread_local std::array<FRootLobeCacheEntry,CacheSize> Cache{};
|
||||
const std::uint64_t IndexKey=island.stableId^(island.stableId>>33)
|
||||
^(static_cast<std::uint64_t>(recipe.seed)<<17)
|
||||
^(static_cast<std::uint64_t>(static_cast<std::uint32_t>(rootIndex))*0x9e3779b97f4a7c15ull);
|
||||
FRootLobeCacheEntry& Entry=Cache[static_cast<std::size_t>(IndexKey)&(CacheSize-1)];
|
||||
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
|
||||
&&Entry.StableId==island.stableId&&Entry.RootIndex==rootIndex
|
||||
&&Entry.GeometryVersion==recipe.geometryVersion
|
||||
&&Entry.Center==island.centerSubvoxels
|
||||
&&Entry.TopZ==island.topZSubvoxels&&Entry.Radius==island.radiusSubvoxels
|
||||
&&Entry.TaperEndQ16==island.taperEndQ16
|
||||
&&Entry.ThicknessRatioQ16==recipe.thicknessRatioQ16
|
||||
&&Entry.CrustDepthBonusQ16==recipe.crustDepthBonusQ16
|
||||
&&Entry.RootDepthRatioQ16==recipe.rootDepthRatioQ16;
|
||||
if(!bKeyMatches)
|
||||
{
|
||||
constexpr std::array<std::array<double, 2>, 4> pattern{{
|
||||
{{-0.92, -0.12}}, {{0.28, 0.98}}, {{0.83, -0.42}}, {{-0.16, -0.94}}}};
|
||||
const std::uint64_t salt = 0x524f4f540000ull + static_cast<std::uint64_t>(rootIndex);
|
||||
const std::uint64_t roll = Roll(recipe.seed, island.stableId, salt);
|
||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||
const double rootDepth = radius * Q16ToDouble(recipe.rootDepthRatioQ16);
|
||||
const double crustBottom = static_cast<double>(IslandCrustBottom(recipe, island));
|
||||
const bool centralButtress = rootIndex == 4;
|
||||
const double orbit = centralButtress ? 0.0
|
||||
: radius * (0.17 + 0.07 * UnitHash(roll));
|
||||
const double rootX = static_cast<double>(island.centerSubvoxels[0])
|
||||
+ (centralButtress ? 0.0 : pattern[static_cast<std::size_t>(rootIndex)][0] * orbit);
|
||||
const double rootY = static_cast<double>(island.centerSubvoxels[1])
|
||||
+ (centralButtress ? 0.0 : pattern[static_cast<std::size_t>(rootIndex)][1] * orbit);
|
||||
const double shortRootVariation = UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x4c454eull));
|
||||
const double lengthRatio = centralButtress ? 1.0 : (rootIndex == 0 ? 1.0
|
||||
: Q16ToDouble(island.taperEndQ16) * (0.82 + 0.18 * shortRootVariation));
|
||||
const double lowerDepth = rootDepth * lengthRatio;
|
||||
const double embedDepth = rootDepth * (centralButtress ? 0.22 : (rootIndex == 0 ? 0.30 : 0.22));
|
||||
const double verticalRadius = 0.5 * (lowerDepth + embedDepth);
|
||||
const double centerZ = crustBottom + 0.5 * (embedDepth - lowerDepth);
|
||||
const double rx = radius * (centralButtress ? 0.25
|
||||
: 0.15 + 0.055 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5258ull)));
|
||||
const double ry = radius * (centralButtress ? 0.25
|
||||
: 0.15 + 0.065 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5259ull)));
|
||||
Entry.bValid=true;
|
||||
Entry.Seed=recipe.seed;
|
||||
Entry.StableId=island.stableId;
|
||||
Entry.RootIndex=rootIndex;
|
||||
Entry.GeometryVersion=recipe.geometryVersion;
|
||||
Entry.Center=island.centerSubvoxels;
|
||||
Entry.TopZ=island.topZSubvoxels;
|
||||
Entry.Radius=island.radiusSubvoxels;
|
||||
Entry.TaperEndQ16=island.taperEndQ16;
|
||||
Entry.ThicknessRatioQ16=recipe.thicknessRatioQ16;
|
||||
Entry.CrustDepthBonusQ16=recipe.crustDepthBonusQ16;
|
||||
Entry.RootDepthRatioQ16=recipe.rootDepthRatioQ16;
|
||||
Entry.Parameters={rootX,rootY,centerZ,rx*rx,ry*ry,
|
||||
verticalRadius*verticalRadius,std::min({rx,ry,verticalRadius})};
|
||||
}
|
||||
const FRootLobeParameters& Parameters=Entry.Parameters;
|
||||
const double q = (x - Parameters.RootX) * (x - Parameters.RootX) / Parameters.RadiusXSquared
|
||||
+ (y - Parameters.RootY) * (y - Parameters.RootY) / Parameters.RadiusYSquared
|
||||
+ (z - Parameters.CenterZ) * (z - Parameters.CenterZ) / Parameters.VerticalRadiusSquared;
|
||||
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
||||
}
|
||||
|
||||
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
||||
@@ -482,19 +653,18 @@ namespace floating_island_detail
|
||||
std::max({z - top, radial - envelope, bottomZ - z}), x, y, z);
|
||||
}
|
||||
|
||||
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
||||
const double dy = y - static_cast<double>(island.centerSubvoxels[1]);
|
||||
const double radial = std::hypot(dx, dy);
|
||||
const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,island,x,y);
|
||||
const double dx=Column.DeltaX;
|
||||
const double dy=Column.DeltaY;
|
||||
const double radial=Column.Radial;
|
||||
const double topZ = static_cast<double>(island.topZSubvoxels);
|
||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||
const double effectiveRadius = std::max(0.0,
|
||||
radius + LobeOffset(recipe, island, dx, dy, z));
|
||||
radius + LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase));
|
||||
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
||||
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
||||
+ radius * 0.16 * edge * edge;
|
||||
const double top = IslandTopSurface(recipe, island,
|
||||
static_cast<double>(island.centerSubvoxels[0]) + dx,
|
||||
static_cast<double>(island.centerSubvoxels[1]) + dy);
|
||||
const double top = Column.TopSurface;
|
||||
const double topDistance = z - top;
|
||||
const double sideDistance = radial - effectiveRadius;
|
||||
const double undersideDistance = lowerSurface - z;
|
||||
@@ -774,7 +944,9 @@ namespace floating_island_detail
|
||||
// Land each connector well inside the broad top so the full capsule footprint remains
|
||||
// supported across the bridge-to-island transition, including bounded surface roughness.
|
||||
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
||||
const double landingFraction = recipe.geometryVersion >= 2 ? 0.38 : 0.62;
|
||||
// Keep the narrow span exposed to open sky; a smaller fraction buries most of it in the island tops.
|
||||
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
||||
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
||||
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
||||
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
||||
const double sx = from.centerSubvoxels[0] + ux * fromReach;
|
||||
@@ -912,6 +1084,10 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
||||
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("units_per_voxel"), manifest.recipe.unitsPerVoxel)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("owner_voxel_size_microns"), manifest.recipe.ownerVoxelSizeMicrons)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("core_voxel_size_microns"), manifest.recipe.coreVoxelSizeMicrons)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("owner_to_core_scale_numerator"), manifest.recipe.ownerToCoreScaleNumerator)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("owner_to_core_scale_denominator"), manifest.recipe.ownerToCoreScaleDenominator)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("seed"), manifest.recipe.seed)
|
||||
|| !floating_island_detail::ReadInteger(recipeValue.Get("origin_x_q"), manifest.recipe.layoutOriginXSubvoxels)
|
||||
|| !floating_island_detail::ReadInteger(recipeValue.Get("origin_y_q"), manifest.recipe.layoutOriginYSubvoxels)
|
||||
@@ -1026,6 +1202,10 @@ std::string FloatingIslandManifestV1::ToJson() const
|
||||
<< floating_island_detail::HashText(value.contentHash) << "\",\"recipe\":{"
|
||||
<< "\"schema_version\":" << recipe.schemaVersion
|
||||
<< ",\"units_per_voxel\":" << recipe.unitsPerVoxel
|
||||
<< ",\"owner_voxel_size_microns\":" << recipe.ownerVoxelSizeMicrons
|
||||
<< ",\"core_voxel_size_microns\":" << recipe.coreVoxelSizeMicrons
|
||||
<< ",\"owner_to_core_scale_numerator\":" << recipe.ownerToCoreScaleNumerator
|
||||
<< ",\"owner_to_core_scale_denominator\":" << recipe.ownerToCoreScaleDenominator
|
||||
<< ",\"seed\":" << recipe.seed
|
||||
<< ",\"origin_x_q\":" << recipe.layoutOriginXSubvoxels
|
||||
<< ",\"origin_y_q\":" << recipe.layoutOriginYSubvoxels
|
||||
@@ -1190,6 +1370,17 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
static_cast<std::int64_t>(std::ceil(riseLimit(static_cast<std::uint32_t>(i),
|
||||
static_cast<std::uint32_t>(i + 1)))));
|
||||
}
|
||||
if (recipe.geometryVersion >= 2)
|
||||
{
|
||||
// Step the outer route islands around the middle island so the connected path still reads as separate floating masses.
|
||||
// Decaler les iles externes autour de l'ile centrale pour distinguer les masses tout en gardant la route continue.
|
||||
constexpr std::int64_t VerticalStagger = 3 * floating_island_detail::Q;
|
||||
for (std::uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (i < middle) tops[i] -= VerticalStagger;
|
||||
else if (i > middle) tops[i] += VerticalStagger;
|
||||
}
|
||||
}
|
||||
for (std::uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const std::uint64_t site = 0x100000000ull + i;
|
||||
|
||||
@@ -936,6 +936,12 @@ public:
|
||||
// edge interpolation and normals to the existing mesher.
|
||||
float DensityAt(float X,float Y,float Z) const
|
||||
{
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeFloatFieldCalls;
|
||||
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||
}
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{
|
||||
@@ -954,6 +960,13 @@ public:
|
||||
|
||||
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),IslandDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
||||
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||
}
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),NativeDensity))
|
||||
@@ -975,6 +988,12 @@ public:
|
||||
|
||||
float DensityAtUnclamped(float X,float Y,float Z) const
|
||||
{
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
||||
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||
}
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{
|
||||
@@ -1007,11 +1026,18 @@ public:
|
||||
Count=std::clamp(Count,0,8);
|
||||
if(Count==0)return;
|
||||
bool bHasLegacySurfaceSample=false;
|
||||
bool bHasNativeIslandSample=false;
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
bHasLegacySurfaceSample|=UsesLegacySurfaceField(
|
||||
static_cast<float>(XStart+Lane*XStep),static_cast<float>(Y),
|
||||
static_cast<float>(Z));
|
||||
if(bHasLegacySurfaceSample)
|
||||
{
|
||||
const float SampleX=static_cast<float>(XStart+Lane*XStep);
|
||||
const float SampleY=static_cast<float>(Y);
|
||||
const float SampleZ=static_cast<float>(Z);
|
||||
bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ);
|
||||
float IslandDensity=0.0f;
|
||||
bHasNativeIslandSample|=NativeFloatingIslandDensityAt(
|
||||
SampleX,SampleY,SampleZ,IslandDensity);
|
||||
}
|
||||
if(bHasLegacySurfaceSample||bHasNativeIslandSample)
|
||||
{
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
||||
@@ -1100,6 +1126,9 @@ private:
|
||||
if(OutDetailDisplacement)*OutDetailDisplacement=0.0f;
|
||||
if(OutCaveInfluence)*OutCaveInfluence=0.0f;
|
||||
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||
return bApplyEdits?ApplyEditBrushes(IslandDensity,X,Y,Z):IslandDensity;
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
{
|
||||
float NativeDensity=0.0f;
|
||||
@@ -1191,17 +1220,22 @@ public:
|
||||
int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
|
||||
int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step};
|
||||
const VoxelForgeCore::WorldAssemblyStratumPlanV1* SurfacePlan=nullptr;
|
||||
bool bSurfaceOverlap=false,bOtherStrateOverlap=false,bBadPlan=false;
|
||||
const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* IslandPlan=nullptr;
|
||||
bool bSurfaceOverlap=false,bIslandOverlap=false,bOtherStrateOverlap=false;
|
||||
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
|
||||
if(Owner&&Owner->Manager)
|
||||
{
|
||||
// Keep the delivered SurfaceWorld chunk mapping unchanged.
|
||||
// Conserver le mapping de chunks SurfaceWorld deja livre.
|
||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
||||
/static_cast<float>(CHUNK_SIZE));
|
||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
|
||||
/static_cast<float>(CHUNK_SIZE));
|
||||
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
||||
{
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
==ECaveGeneratorType::SurfaceWorld)
|
||||
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0,0,ChunkZ));
|
||||
if(Type==ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
bSurfaceOverlap=true;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
@@ -1211,12 +1245,39 @@ public:
|
||||
bBadPlan=true;
|
||||
SurfacePlan=Candidate;
|
||||
}
|
||||
else bOtherStrateOverlap=true;
|
||||
else if(Type!=ECaveGeneratorType::FloatingIslands)
|
||||
bOtherStrateOverlap=true;
|
||||
}
|
||||
|
||||
// Floating islands use the explicit 12.5 cm owner to 25 cm Core mapping:
|
||||
// one legacy 4 m chunk occupies 16 Core voxels.
|
||||
// Les iles flottantes suivent l'echelle explicite proprietaire/Core: 16 voxels Core.
|
||||
constexpr int32 CoreVoxelsPerIslandChunk=CHUNK_SIZE/2;
|
||||
const int32 FirstIslandChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
||||
/static_cast<float>(CoreVoxelsPerIslandChunk));
|
||||
const int32 LastIslandChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
|
||||
/static_cast<float>(CoreVoxelsPerIslandChunk));
|
||||
for(int32 ChunkZ=FirstIslandChunk;ChunkZ<=LastIslandChunk;++ChunkZ)
|
||||
{
|
||||
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0,0,ChunkZ));
|
||||
if(Type==ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
bIslandOverlap=true;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
|
||||
if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true;
|
||||
IslandPlan=Candidate;
|
||||
}
|
||||
else bIslandOtherStrateOverlap=true;
|
||||
}
|
||||
}
|
||||
const auto Finish=[&](EVoxelTileClass Verdict,bool bUnknown=false)
|
||||
{
|
||||
FBoxClassification Result{Verdict,bSurfaceOverlap&&!bOtherStrateOverlap,bUnknown};
|
||||
const bool bNative=(bSurfaceOverlap&&!bOtherStrateOverlap)
|
||||
||(bIslandOverlap&&!bIslandOtherStrateOverlap);
|
||||
FBoxClassification Result{Verdict,bNative,bUnknown};
|
||||
auto& Counts=ThreadClassificationStats();
|
||||
Counts.Milliseconds+=std::chrono::duration<double,std::milli>(Clock::now()-Start).count();
|
||||
if(!Result.bNativeSurface||bUnknown)++Counts.UnknownBoxes;
|
||||
@@ -1227,7 +1288,9 @@ public:
|
||||
};
|
||||
// A box crossing recipes cannot use one field interval; unknown composition stays Mixed.
|
||||
// Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas.
|
||||
if(bBadPlan|| (bSurfaceOverlap&&bOtherStrateOverlap))
|
||||
if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap))
|
||||
||(bIslandOverlap&&(bIslandOtherStrateOverlap
|
||||
||(!bSurfaceOverlap&&bOtherStrateOverlap))))
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
||||
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
|
||||
@@ -1273,6 +1336,38 @@ public:
|
||||
default:return Finish(EVoxelTileClass::Mixed,true);
|
||||
}
|
||||
}
|
||||
if(bIslandOverlap)
|
||||
{
|
||||
if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true);
|
||||
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t CoreVoxelsPerLegacyChunk=CHUNK_SIZE/2;
|
||||
const std::int64_t Bottom=static_cast<std::int64_t>(IslandPlan->bottomChunkZ)
|
||||
*CoreVoxelsPerLegacyChunk*Scale;
|
||||
const std::int64_t Top=(static_cast<std::int64_t>(IslandPlan->topChunkZ)+1)
|
||||
*CoreVoxelsPerLegacyChunk*Scale;
|
||||
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
|
||||
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
|
||||
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
|
||||
const bool bBottomSealOverlap=BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
|
||||
const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal;
|
||||
if(bBottomSealOverlap)
|
||||
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
|
||||
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||
if(bTopSealOverlap)
|
||||
return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top
|
||||
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Block(
|
||||
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
|
||||
if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true);
|
||||
const auto Interval=Block.interval(static_cast<int64>(BMin[0])*Scale,
|
||||
static_cast<int64>(BMin[1])*Scale,static_cast<int64>(BMin[2])*Scale,
|
||||
static_cast<int64>(BMax[0])*Scale,static_cast<int64>(BMax[1])*Scale,
|
||||
static_cast<int64>(BMax[2])*Scale);
|
||||
if(!Interval.known)return Finish(EVoxelTileClass::Mixed,true);
|
||||
if(Interval.minimumDensitySubvoxels>=0)return Finish(EVoxelTileClass::AllAir);
|
||||
if(Interval.maximumDensitySubvoxels<0)return Finish(EVoxelTileClass::AllSolid);
|
||||
return Finish(EVoxelTileClass::Mixed);
|
||||
}
|
||||
const bool bRock=EvaluateFast((BMin[0]+BMax[0])*0.5,
|
||||
(BMin[1]+BMax[1])*0.5,(BMin[2]+BMax[2])*0.5);
|
||||
return Finish(bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir);
|
||||
@@ -1753,6 +1848,16 @@ private:
|
||||
return It==WorldAssembly->plan.strates.end()?nullptr:&*It;
|
||||
}
|
||||
|
||||
const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* FindWorldAssemblyFloatingIslandStratum(
|
||||
int32 StrateIndex,int32 ChunkZ) const
|
||||
{
|
||||
if(!WorldAssembly||StrateIndex<0)return nullptr;
|
||||
const auto It=std::find_if(WorldAssembly->floatingIslands.begin(),
|
||||
WorldAssembly->floatingIslands.end(),[StrateIndex,ChunkZ](const auto& Entry)
|
||||
{return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;});
|
||||
return It==WorldAssembly->floatingIslands.end()?nullptr:&*It;
|
||||
}
|
||||
|
||||
bool NativeSurfaceDensityAt(float X,float Y,float Z,float& OutDensity) const
|
||||
{
|
||||
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
@@ -1778,6 +1883,45 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NativeFloatingIslandDensityAt(float X,float Y,float Z,float& OutDensity) const
|
||||
{
|
||||
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
||||
// Convert Core query Z back to the owner's legacy chunk coordinate.
|
||||
// Convertir Z du Core vers la coordonnee de chunk legacy du proprietaire.
|
||||
constexpr int32 CoreVoxelsPerLegacyChunk=CHUNK_SIZE/2;
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CoreVoxelsPerLegacyChunk));
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::FloatingIslands)return false;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return false;
|
||||
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr double Maximum=1000000000000.0/Scale;
|
||||
if(std::abs(static_cast<double>(X))>Maximum||std::abs(static_cast<double>(Y))>Maximum
|
||||
||std::abs(static_cast<double>(Z))>Maximum)return false;
|
||||
const auto ToFixed=[](float Value)
|
||||
{return static_cast<int64>(std::llround(static_cast<double>(Value)
|
||||
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));};
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||
Plan->manifest.recipe,Plan->manifest.plan);
|
||||
if(!Field.isValid())return false;
|
||||
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
// A legacy 4 m chunk is 16 Core voxels at the published 12.5 cm to 25 cm scale.
|
||||
// Un chunk legacy de 4 m represente 16 voxels Core avec l'echelle publiee.
|
||||
const std::int64_t Bottom=static_cast<std::int64_t>(Plan->bottomChunkZ)
|
||||
*CoreVoxelsPerLegacyChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t Top=(static_cast<std::int64_t>(Plan->topChunkZ)+1)
|
||||
*CoreVoxelsPerLegacyChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||
const std::int64_t Zq=ToFixed(Z);
|
||||
if(Zq>=Bottom&&Zq<Bottom+Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
|
||||
if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Top-Zq));
|
||||
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
||||
return std::isfinite(OutDensity);
|
||||
}
|
||||
|
||||
bool UsesLegacySurfaceField(float X,float Y,float Z) const
|
||||
{
|
||||
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
@@ -403,9 +404,91 @@ namespace world_assembly_detail
|
||||
HashWord(hash, manifest.schemaVersion, 4);
|
||||
HashWord(hash, manifest.recipe.contentHash, 8);
|
||||
HashWord(hash, manifest.plan.contentHash, 8);
|
||||
// Preserve the historical SurfaceWorld manifest digest when no island plan is attached.
|
||||
if (!manifest.floatingIslands.empty())
|
||||
{
|
||||
HashWord(hash, 0x464c4f415449534cull, 8);
|
||||
HashWord(hash, manifest.floatingIslands.size(), 4);
|
||||
for (const auto& islands : manifest.floatingIslands)
|
||||
{
|
||||
HashWord(hash, static_cast<std::uint32_t>(islands.strateIndex), 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(islands.topChunkZ), 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(islands.bottomChunkZ), 4);
|
||||
HashWord(hash, islands.manifest.contentHash, 8);
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline void AppendJsonString(std::ostringstream& out, std::string_view value)
|
||||
{
|
||||
out << '"';
|
||||
for (const unsigned char ch : value)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '"': out << "\\\""; break;
|
||||
case '\\': out << "\\\\"; break;
|
||||
case '\b': out << "\\b"; break;
|
||||
case '\f': out << "\\f"; break;
|
||||
case '\n': out << "\\n"; break;
|
||||
case '\r': out << "\\r"; break;
|
||||
case '\t': out << "\\t"; break;
|
||||
default:
|
||||
if (ch < 0x20) out << "\\u00" << std::hex << std::setw(2)
|
||||
<< std::setfill('0') << static_cast<unsigned int>(ch) << std::dec;
|
||||
else out << static_cast<char>(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
out << '"';
|
||||
}
|
||||
|
||||
inline void AppendJsonValue(std::ostringstream& out, const VFJson::Value& value)
|
||||
{
|
||||
if (std::holds_alternative<std::nullptr_t>(value.Data)) out << "null";
|
||||
else if (const auto* boolean = std::get_if<bool>(&value.Data)) out << (*boolean ? "true" : "false");
|
||||
else if (const auto* number = std::get_if<double>(&value.Data))
|
||||
{
|
||||
char buffer[64];
|
||||
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), *number,
|
||||
std::chars_format::general, std::numeric_limits<double>::max_digits10);
|
||||
if (result.ec == std::errc{}) out.write(buffer, result.ptr - buffer);
|
||||
else out << '0';
|
||||
}
|
||||
else if (const auto* string = std::get_if<std::string>(&value.Data)) AppendJsonString(out, *string);
|
||||
else if (const auto* array = std::get_if<VFJson::Value::Array>(&value.Data))
|
||||
{
|
||||
out << '[';
|
||||
for (std::size_t i = 0; i < array->size(); ++i)
|
||||
{ if (i) out << ','; AppendJsonValue(out, (*array)[i]); }
|
||||
out << ']';
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto& object = std::get<VFJson::Value::Object>(value.Data);
|
||||
std::vector<std::string_view> keys;
|
||||
keys.reserve(object.size());
|
||||
for (const auto& entry : object) keys.push_back(entry.first);
|
||||
std::sort(keys.begin(), keys.end());
|
||||
out << '{';
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
{
|
||||
if (i) out << ',';
|
||||
AppendJsonString(out, keys[i]); out << ':';
|
||||
AppendJsonValue(out, object.at(std::string(keys[i])));
|
||||
}
|
||||
out << '}';
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string JsonText(const VFJson::Value& value)
|
||||
{
|
||||
std::ostringstream out;
|
||||
AppendJsonValue(out, value);
|
||||
return out.str();
|
||||
}
|
||||
|
||||
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
||||
std::string& error)
|
||||
{
|
||||
@@ -566,6 +649,26 @@ namespace world_assembly_detail
|
||||
if (error.empty()) error = "world assembly manifest is missing a required field";
|
||||
return std::nullopt;
|
||||
}
|
||||
const VFJson::Value& islandPlans = value.Get("floating_islands");
|
||||
if (!std::holds_alternative<std::nullptr_t>(islandPlans.Data))
|
||||
{
|
||||
if (!islandPlans.IsArray() || islandPlans.AsArray().size() > MaxStrateCount)
|
||||
{ error = "world assembly floating-island plans must be an array of at most sixty-four entries"; return std::nullopt; }
|
||||
for (const VFJson::Value& islandValue : islandPlans.AsArray())
|
||||
{
|
||||
WorldAssemblyFloatingIslandPlanV1 entry;
|
||||
if (!islandValue.IsObject()
|
||||
|| !ReadI32(islandValue.Get("strate_index"), entry.strateIndex)
|
||||
|| !ReadI32(islandValue.Get("top_chunk_z"), entry.topChunkZ)
|
||||
|| !ReadI32(islandValue.Get("bottom_chunk_z"), entry.bottomChunkZ))
|
||||
{ error = "world assembly floating-island slot is missing its resolved strate bounds"; return std::nullopt; }
|
||||
const std::string nestedJson = JsonText(islandValue.Get("manifest"));
|
||||
auto parsedIsland = FloatingIslandManifestV1::FromJson(nestedJson, error);
|
||||
if (!parsedIsland) return std::nullopt;
|
||||
entry.manifest = std::move(*parsedIsland);
|
||||
manifest.floatingIslands.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
if (!manifest.validate(error)) return std::nullopt;
|
||||
return manifest;
|
||||
}
|
||||
@@ -1292,8 +1395,10 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion
|
||||
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
||||
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
||||
if (plan.strates.empty() || plan.strates.size() > world_assembly_detail::MaxStrateCount)
|
||||
{ error = "world assembly plan must contain one to sixty-four finite strates"; return false; }
|
||||
if ((plan.strates.empty() && floatingIslands.empty())
|
||||
|| plan.strates.size() > world_assembly_detail::MaxStrateCount
|
||||
|| floatingIslands.size() > world_assembly_detail::MaxStrateCount)
|
||||
{ error = "world assembly must contain SurfaceWorld or floating-island plans within the sixty-four-strate limit"; return false; }
|
||||
std::vector<std::int32_t> strateIds;
|
||||
for (auto& stratum : plan.strates)
|
||||
{
|
||||
@@ -1323,6 +1428,25 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
strateIds.push_back(stratum.strateIndex);
|
||||
stratum.contentHash = world_assembly_detail::HashStratum(stratum);
|
||||
}
|
||||
std::vector<std::int32_t> islandStrateIds;
|
||||
for (auto& islands : floatingIslands)
|
||||
{
|
||||
if (islands.strateIndex < 0 || islands.topChunkZ < islands.bottomChunkZ
|
||||
|| std::find(islandStrateIds.begin(), islandStrateIds.end(), islands.strateIndex) != islandStrateIds.end())
|
||||
{ error = "world assembly contains invalid or duplicate floating-island strate bounds"; return false; }
|
||||
if (!islands.manifest.validate(error)) return false;
|
||||
if (islands.manifest.recipe.seed != recipe.seed)
|
||||
{ error = "floating-island seed does not match the world assembly season seed"; return false; }
|
||||
constexpr std::int64_t CoreVoxelsPerLegacyChunk = 16;
|
||||
const std::int64_t bottomQ = static_cast<std::int64_t>(islands.bottomChunkZ)
|
||||
* CoreVoxelsPerLegacyChunk * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t topQ = (static_cast<std::int64_t>(islands.topChunkZ) + 1)
|
||||
* CoreVoxelsPerLegacyChunk * WorldAssemblySubvoxelsPerVoxel;
|
||||
if (islands.manifest.recipe.layoutMinimumZSubvoxels < bottomQ
|
||||
|| islands.manifest.recipe.layoutMaximumZSubvoxels > topQ)
|
||||
{ error = "floating-island recipe layout escapes the resolved 12.5 cm legacy strate bounds"; return false; }
|
||||
islandStrateIds.push_back(islands.strateIndex);
|
||||
}
|
||||
recipe.contentHash = world_assembly_detail::HashRecipe(recipe);
|
||||
plan.contentHash = world_assembly_detail::HashPlan(plan);
|
||||
contentHash = world_assembly_detail::HashManifest(*this);
|
||||
@@ -1337,13 +1461,19 @@ bool WorldAssemblyManifest::validate(std::string& error) const
|
||||
const std::uint64_t expectedManifest = contentHash;
|
||||
std::vector<std::uint64_t> expectedStrates;
|
||||
for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.contentHash);
|
||||
std::vector<std::uint64_t> expectedIslandManifests;
|
||||
for (const auto& islands : floatingIslands) expectedIslandManifests.push_back(islands.manifest.contentHash);
|
||||
if (!copy.finalize(error)) return false;
|
||||
if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash
|
||||
|| expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size())
|
||||
|| expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size()
|
||||
|| expectedIslandManifests.size() != copy.floatingIslands.size())
|
||||
{ error = "world assembly recipe, plan or manifest content hash does not match its fields"; return false; }
|
||||
for (std::size_t i = 0; i < expectedStrates.size(); ++i)
|
||||
if (expectedStrates[i] != copy.plan.strates[i].contentHash)
|
||||
{ error = "world assembly stratum content hash does not match its fields"; return false; }
|
||||
for (std::size_t i = 0; i < expectedIslandManifests.size(); ++i)
|
||||
if (expectedIslandManifests[i] != copy.floatingIslands[i].manifest.contentHash)
|
||||
{ error = "world assembly floating-island content hash does not match its fields"; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1474,7 +1604,23 @@ std::string WorldAssemblyManifest::ToJson() const
|
||||
}
|
||||
out << "]}";
|
||||
}
|
||||
out << "]}}";
|
||||
if (value.floatingIslands.empty()) out << "]}}";
|
||||
else
|
||||
{
|
||||
out << "]},\"floating_islands\":[";
|
||||
for (std::size_t i = 0; i < value.floatingIslands.size(); ++i)
|
||||
{
|
||||
const auto& islands = value.floatingIslands[i];
|
||||
if (i) out << ',';
|
||||
const std::string nested = islands.manifest.ToJson();
|
||||
if (nested.empty()) return {};
|
||||
out << "{\"strate_index\":" << islands.strateIndex
|
||||
<< ",\"top_chunk_z\":" << islands.topChunkZ
|
||||
<< ",\"bottom_chunk_z\":" << islands.bottomChunkZ
|
||||
<< ",\"manifest\":" << nested << '}';
|
||||
}
|
||||
out << "]}";
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user