wip(assembler): M1 round 1 -- manifest format, block contract, bound audit, native Heightfield2D
Luna, M1 round 1 of 4 (plan in Standalone/ASSEMBLER_M1_REPORT.md): - Versioned fixed-scale world manifest (the shipped plan format) and the Core block contract: point evaluation, interval-or-unknown, influence bound, detail envelope, channels. - Interval audit: 256 boxes / 6,912 samples, 0 missed, 0 unknown; a deliberately false bound is detected (negative control). - Native Heightfield2D with biome input: 4,096-point hash fae2721c052aec5c identical at W1/W8/W16; point queries 2.059 ms vs 62.115 ms legacy -- but NOT a match yet (mean density error 274 voxels, 2,261 sign mismatches vs the legacy surface). 45 mesh-free Core queries match direct evaluation exactly. Existing cave world unchanged (walkability, 64/64 tiles). - Open: collision readiness, a valid full-view timing (viewer captures were occluded/empty; top-down height maps are the only usable pair). Also carries the design doc's owner decisions (fixed water; player edits unrestricted, route law for automatic changes only) and three plugin files + BuildHost scripts that earlier round commits had missed (also fixed on unreal/core-integration as 2574b56). 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
d5ce5d675d
commit
874c796c65
20 files changed
+2880
-12
No files matched your search
@@ -79,6 +79,45 @@ Pass `analyticOnly = false` to retain the reference packed-page compiler. Packed
|
||||
parent-cell coordinates and an 8-voxel apron; each covers a 128-voxel cube or one larger coarse
|
||||
tile. This path remains available for parity checks and workloads that need the packed lattice.
|
||||
|
||||
## World assembler manifest v1 (prototype)
|
||||
|
||||
`ResolvedWorldSnapshot::FromJson` accepts an optional `world_assembly` object alongside the owner
|
||||
snapshot. The object is the shipped-plan payload shape: a fixed-scale recipe and finite-strate plan
|
||||
with hashes, no generated mesh or voxel samples. The reviewed sample payload is
|
||||
[`round1-manifest.json`](../Research/WorldAssemblerM1/round1-manifest.json). Round 4 still needs to
|
||||
connect this payload to the season asset/hash path.
|
||||
|
||||
Every coordinate and scalar uses signed integer units of 1/256 voxel (`units_per_voxel: 256`),
|
||||
independent of the plugin's 25 cm Core voxel and of tile size. Schema and evaluator versions are
|
||||
both 1. JSON integer fields are canonicalized in declared field order and hashed as 4- or 8-byte
|
||||
little-endian words with FNV-1a 64. Hashes are emitted as 16 lowercase hexadecimal characters;
|
||||
JSON key order and whitespace do not affect them. The root, recipe, plan and each stratum carry a
|
||||
content hash. The recipe stores its seed, warped biome-map settings and one to eight biome profiles;
|
||||
the plan stores finite-strate extents, cap settings, stable-ID features and route points. Bounds,
|
||||
feature count and route count are validated before a manifest is accepted.
|
||||
|
||||
```cpp
|
||||
std::string error;
|
||||
auto manifest = VoxelForgeCore::WorldAssemblyManifest::FromJson(payload, error);
|
||||
if (!manifest || !manifest->validate(error)) { /* reject the payload */ }
|
||||
const auto* loaded = snapshot.worldAssembly(); // null when the owner snapshot has no plan
|
||||
```
|
||||
|
||||
`Heightfield2D` is the first field block. `point(xQ, yQ, zQ)` evaluates the biome map and blended
|
||||
height profiles at fixed-scale coordinates, returning the ground and cap heights, primary and
|
||||
neighbor biome IDs, Q16 blend weight, and signed density. Negative density is solid. Its block
|
||||
contract reads `biome` and writes `height` plus `rock`; `water`, `waterSurface`, `route` and
|
||||
`surfaceTags` are declared channels for later blocks. The detail envelope reports maximum absolute
|
||||
vertical displacement and minimum feature size. Influence is global in XY and limited to the
|
||||
finite stratum's Z extent.
|
||||
|
||||
`interval(minXQ, minYQ, minZQ, maxXQ, maxYQ, maxZQ)` returns a conservative density range or
|
||||
`known == false`; callers must keep evaluating when it is unknown. The current heightfield range
|
||||
uses the maximum recipe-profile amplitude and cap undulation over the whole biome set. The public
|
||||
`AuditHeightfieldBounds` samples points inside randomized boxes and includes a deliberately false
|
||||
one-sided range as a negative control. A pass means no sampled point escaped and the negative
|
||||
control was rejected; it is an audit of this block implementation, not a proof for future blocks.
|
||||
|
||||
## Queries and tiles
|
||||
|
||||
`densityAt(position)` returns signed density; negative is solid and nonnegative is air.
|
||||
|
||||
@@ -98,6 +98,182 @@ namespace VoxelForgeCore
|
||||
Vec3d viewerStartVoxels{};
|
||||
};
|
||||
|
||||
// World assembler v1 serializes every coordinate and scalar in 1/256 voxel units.
|
||||
// This is independent of tile size and is the fixed input scale for Core field blocks.
|
||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1;
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 1;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
rock = 1u << 0, height = 1u << 1, biome = 1u << 2, water = 1u << 3,
|
||||
waterSurface = 1u << 4, route = 1u << 5, surfaceTags = 1u << 6
|
||||
};
|
||||
|
||||
constexpr std::uint32_t ChannelBit(FieldChannel channel) noexcept
|
||||
{ return static_cast<std::uint32_t>(channel); }
|
||||
|
||||
struct WorldAssemblyBiomeProfileV1
|
||||
{
|
||||
std::uint32_t stableId = 0;
|
||||
std::int64_t baseHeightSubvoxels = 0;
|
||||
std::int64_t continentAmplitudeSubvoxels = 0;
|
||||
std::int64_t continentWavelengthSubvoxels = 256 * 256;
|
||||
std::int64_t mountainAmplitudeSubvoxels = 0;
|
||||
std::int64_t mountainWavelengthSubvoxels = 256 * 128;
|
||||
std::int64_t detailAmplitudeSubvoxels = 0;
|
||||
std::int64_t detailWavelengthSubvoxels = 256 * 16;
|
||||
};
|
||||
|
||||
struct WorldAssemblyRecipeV1
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
std::uint32_t evaluatorVersion = WorldAssemblyEvaluatorVersion;
|
||||
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
|
||||
std::uint32_t seed = 0;
|
||||
std::int64_t biomeCellSizeSubvoxels = 256 * 800;
|
||||
std::int64_t biomeWarpSubvoxels = 256 * 250;
|
||||
std::int64_t biomeWarpWavelengthSubvoxels = 256 * 556;
|
||||
std::int64_t biomeBorderBlendSubvoxels = 256 * 120;
|
||||
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
struct WorldAssemblyRoutePointV1
|
||||
{
|
||||
std::uint64_t stableFeatureId = 0;
|
||||
std::array<std::int64_t, 3> positionSubvoxels{};
|
||||
std::int64_t radiusSubvoxels = 0;
|
||||
};
|
||||
|
||||
struct WorldAssemblyFeatureV1
|
||||
{
|
||||
// Stable numeric type IDs are part of the plan contract; v1 reserves 1 for cliff planes,
|
||||
// 2 for bounded overhang shelves and 3 for authored cap features.
|
||||
// Les types numeriques stables font partie du plan; v1 reserve 1 aux falaises, 2 aux surplombs bornes et 3 aux caps.
|
||||
std::uint64_t stableId = 0;
|
||||
std::uint32_t type = 0;
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
||||
std::array<std::int64_t, 4> parametersSubvoxels{};
|
||||
};
|
||||
|
||||
struct WorldAssemblyStratumPlanV1
|
||||
{
|
||||
std::int32_t strateIndex = -1;
|
||||
std::int32_t topChunkZ = 0;
|
||||
std::int32_t bottomChunkZ = 0;
|
||||
std::int64_t capHeightSubvoxels = 0;
|
||||
std::int64_t capUndulationSubvoxels = 0;
|
||||
std::int64_t capWavelengthSubvoxels = 256 * 512;
|
||||
std::vector<WorldAssemblyFeatureV1> features;
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
struct WorldAssemblyPlanV1
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
std::vector<WorldAssemblyStratumPlanV1> strates;
|
||||
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;
|
||||
};
|
||||
|
||||
struct HeightfieldPointSample
|
||||
{
|
||||
std::int64_t groundHeightSubvoxels = 0;
|
||||
std::int64_t capHeightSubvoxels = 0;
|
||||
std::uint32_t biomeId = 0;
|
||||
std::uint32_t neighborBiomeId = 0;
|
||||
std::uint32_t biomeWeightQ16 = 0;
|
||||
std::int64_t densitySubvoxels = 0;
|
||||
};
|
||||
|
||||
struct FieldInterval
|
||||
{
|
||||
bool known = false;
|
||||
std::int64_t minimumDensitySubvoxels = 0;
|
||||
std::int64_t maximumDensitySubvoxels = 0;
|
||||
};
|
||||
|
||||
struct FieldInfluenceBounds
|
||||
{
|
||||
bool unboundedXY = true;
|
||||
std::int64_t minimumXSubvoxels = 0, maximumXSubvoxels = 0;
|
||||
std::int64_t minimumYSubvoxels = 0, maximumYSubvoxels = 0;
|
||||
std::int64_t minimumZSubvoxels = 0, maximumZSubvoxels = 0;
|
||||
};
|
||||
|
||||
struct FieldDetailEnvelope
|
||||
{
|
||||
std::int64_t maximumAbsoluteDisplacementSubvoxels = 0;
|
||||
std::int64_t minimumFeatureSizeSubvoxels = 1;
|
||||
};
|
||||
|
||||
struct FieldBlockContract
|
||||
{
|
||||
std::uint32_t readsChannels = 0;
|
||||
std::uint32_t writesChannels = 0;
|
||||
std::uint32_t evaluatorVersion = WorldAssemblyEvaluatorVersion;
|
||||
};
|
||||
|
||||
// Point evaluation is canonical at 1/256 voxel. Bounds fail closed when the block or input
|
||||
// extent is invalid; callers must evaluate when interval() returns unknown.
|
||||
// L'evaluation est canonique au 1/256 de voxel. Toute borne invalide echoue prudemment; une borne inconnue impose l'evaluation.
|
||||
class VFCORE_API Heightfield2D
|
||||
{
|
||||
public:
|
||||
Heightfield2D(const WorldAssemblyRecipeV1& recipe,
|
||||
const WorldAssemblyStratumPlanV1& plan) noexcept;
|
||||
bool isValid() const noexcept;
|
||||
HeightfieldPointSample point(std::int64_t xSubvoxels, std::int64_t ySubvoxels,
|
||||
std::int64_t zSubvoxels) const noexcept;
|
||||
FieldInterval interval(std::int64_t minimumXSubvoxels,
|
||||
std::int64_t minimumYSubvoxels,
|
||||
std::int64_t minimumZSubvoxels,
|
||||
std::int64_t maximumXSubvoxels,
|
||||
std::int64_t maximumYSubvoxels,
|
||||
std::int64_t maximumZSubvoxels) const noexcept;
|
||||
FieldInfluenceBounds influenceBounds() const noexcept;
|
||||
FieldDetailEnvelope detailEnvelope() const noexcept;
|
||||
FieldBlockContract contract() const noexcept;
|
||||
|
||||
private:
|
||||
const WorldAssemblyRecipeV1* recipe_ = nullptr;
|
||||
const WorldAssemblyStratumPlanV1* plan_ = nullptr;
|
||||
};
|
||||
|
||||
struct WorldAssemblyBoundsAuditResult
|
||||
{
|
||||
std::uint64_t boxesTested = 0;
|
||||
std::uint64_t pointSamples = 0;
|
||||
std::uint64_t missedBounds = 0;
|
||||
std::uint64_t unknownBounds = 0;
|
||||
bool negativeControlDetected = false;
|
||||
bool passed = false;
|
||||
};
|
||||
|
||||
VFCORE_API WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(
|
||||
const Heightfield2D& block, std::uint32_t boxCount = 256,
|
||||
std::uint64_t seed = 0x574f524c44415331ull);
|
||||
|
||||
class VFCORE_API ResolvedWorldSnapshot
|
||||
{
|
||||
public:
|
||||
@@ -110,6 +286,7 @@ namespace VoxelForgeCore
|
||||
|
||||
explicit operator bool() const noexcept { return static_cast<bool>(data_); }
|
||||
const WorldConfiguration& configuration() const;
|
||||
const WorldAssemblyManifest* worldAssembly() const noexcept;
|
||||
|
||||
private:
|
||||
struct Data;
|
||||
|
||||
@@ -52,3 +52,4 @@
|
||||
// VFJson is a small, stateless standalone input parser with no Unreal-shaped names. Keeping its
|
||||
// value tree shared lets the viewer resolve its reference world and Core snapshot from one parse.
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
@@ -281,11 +281,13 @@ public:
|
||||
bool bInRoundTunnels=false,bool bInProfileDetailBand=false,
|
||||
bool bValidatePassageRoutes=true,
|
||||
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InGpuVisualDetailTiles={},
|
||||
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={})
|
||||
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={},
|
||||
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const auto TotalStart=Clock::now();
|
||||
Owner=&World;WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
|
||||
Owner=&World;WorldAssembly=InWorldAssembly;
|
||||
WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
|
||||
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
|
||||
bAnalyticOnly=false;
|
||||
bProfileDetailBand=bInProfileDetailBand;
|
||||
@@ -919,6 +921,9 @@ public:
|
||||
// edge interpolation and normals to the existing mesher.
|
||||
float DensityAt(float X,float Y,float Z) const
|
||||
{
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
return LegacySurfaceDensityAt(X,Y,Z);
|
||||
int32 IX=0,IY=0,IZ=0;
|
||||
@@ -931,6 +936,10 @@ public:
|
||||
|
||||
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),NativeDensity))
|
||||
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z)))
|
||||
return LegacySurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
@@ -945,6 +954,9 @@ public:
|
||||
|
||||
float DensityAtUnclamped(float X,float Y,float Z) const
|
||||
{
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
return LegacySurfaceDensityAt(X,Y,Z);
|
||||
return AnalyticDensityAt(X,Y,Z,false);
|
||||
@@ -1066,6 +1078,9 @@ private:
|
||||
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
{
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
|
||||
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
|
||||
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
|
||||
}
|
||||
@@ -1119,10 +1134,8 @@ public:
|
||||
const int32 Pad=std::max(Step,1);
|
||||
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};
|
||||
// A primitive-only proof cannot classify the biome heightfield. Keep overlapping
|
||||
// SurfaceWorld boxes mixed so generation samples the exact copied evaluator.
|
||||
// Une preuve fondee sur les primitives ne classe pas la hauteur par biomes; garder les
|
||||
// boites SurfaceWorld en Mixed force l'evaluation exacte lors du maillage.
|
||||
const VoxelForgeCore::WorldAssemblyStratumPlanV1* SurfacePlan=nullptr;
|
||||
bool bSurfaceOverlap=false,bOtherStrateOverlap=false;
|
||||
if(Owner&&Owner->Manager)
|
||||
{
|
||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
||||
@@ -1130,10 +1143,24 @@ public:
|
||||
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)
|
||||
return EVoxelTileClass::Mixed;
|
||||
{
|
||||
bSurfaceOverlap=true;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
||||
if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))
|
||||
return EVoxelTileClass::Mixed;
|
||||
SurfacePlan=Candidate;
|
||||
}
|
||||
else bOtherStrateOverlap=true;
|
||||
}
|
||||
}
|
||||
// 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(bSurfaceOverlap&&bOtherStrateOverlap)return EVoxelTileClass::Mixed;
|
||||
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
||||
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
|
||||
&&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2])
|
||||
@@ -1155,6 +1182,22 @@ public:
|
||||
if(R.Lo<=BMax[2]&&R.Hi>=BMin[2]
|
||||
&&(R.Lo>=BMin[2]||R.Hi<=BMax[2]))
|
||||
return EVoxelTileClass::Mixed;
|
||||
if(bSurfaceOverlap)
|
||||
{
|
||||
if(!WorldAssembly||!SurfacePlan)return EVoxelTileClass::Mixed;
|
||||
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*SurfacePlan);
|
||||
const auto Bounds=Block.interval(
|
||||
static_cast<int64>(BMin[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<int64>(BMin[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<int64>(BMin[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<int64>(BMax[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<int64>(BMax[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<int64>(BMax[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
if(!Bounds.known)return EVoxelTileClass::Mixed;
|
||||
if(Bounds.minimumDensitySubvoxels>0)return EVoxelTileClass::AllAir;
|
||||
if(Bounds.maximumDensitySubvoxels<0)return EVoxelTileClass::AllSolid;
|
||||
return 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 bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir;
|
||||
@@ -1619,6 +1662,41 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
const VoxelForgeCore::WorldAssemblyStratumPlanV1* FindWorldAssemblyStratum(
|
||||
int32 StrateIndex,int32 ChunkZ) const
|
||||
{
|
||||
if(!WorldAssembly||StrateIndex<0)return nullptr;
|
||||
const auto It=std::find_if(WorldAssembly->plan.strates.begin(),WorldAssembly->plan.strates.end(),
|
||||
[StrateIndex,ChunkZ](const auto& Entry)
|
||||
{return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;});
|
||||
return It==WorldAssembly->plan.strates.end()?nullptr:&*It;
|
||||
}
|
||||
|
||||
bool NativeSurfaceDensityAt(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;
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE));
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)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=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return false;
|
||||
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double MaxVoxel=static_cast<double>(std::numeric_limits<int64>::max())/Scale;
|
||||
if(std::abs(static_cast<double>(X))>MaxVoxel||std::abs(static_cast<double>(Y))>MaxVoxel
|
||||
||std::abs(static_cast<double>(Z))>MaxVoxel)return false;
|
||||
const auto ToFixed=[](float Value)
|
||||
{return static_cast<int64>(std::llround(static_cast<double>(Value)
|
||||
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));};
|
||||
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
|
||||
if(!Block.isValid())return false;
|
||||
OutDensity=static_cast<float>(Block.point(ToFixed(X),ToFixed(Y),ToFixed(Z)).densitySubvoxels
|
||||
/Scale);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UsesLegacySurfaceField(float X,float Y,float Z) const
|
||||
{
|
||||
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
|
||||
@@ -4850,6 +4928,7 @@ private:
|
||||
}
|
||||
|
||||
FStandaloneOwnerWorld* Owner=nullptr;
|
||||
const VoxelForgeCore::WorldAssemblyManifest* WorldAssembly=nullptr;
|
||||
int32 WorkerCount=6;
|
||||
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
|
||||
bool RoundTunnelsEnabled=false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "CoreNamespacePreamble.h"
|
||||
#include "StandaloneSnapshotAdapter.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
#include "WorldAssemblerInternal.h"
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <mutex>
|
||||
@@ -23,6 +24,7 @@ namespace VoxelForgeCore
|
||||
struct ResolvedWorldSnapshot::Data
|
||||
{
|
||||
std::shared_ptr<FStandaloneOwnerWorld> owner;
|
||||
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
|
||||
WorldConfiguration configuration;
|
||||
};
|
||||
|
||||
@@ -216,12 +218,14 @@ namespace VoxelForgeCore
|
||||
|
||||
struct detail_StandaloneSnapshotAdapter
|
||||
{
|
||||
static ResolvedWorldSnapshot FromOwner(std::shared_ptr<FStandaloneOwnerWorld> owner)
|
||||
static ResolvedWorldSnapshot FromOwner(std::shared_ptr<FStandaloneOwnerWorld> owner,
|
||||
std::shared_ptr<const WorldAssemblyManifest> worldAssembly = {})
|
||||
{
|
||||
if(!owner||!owner->Settings||!owner->Manager||!owner->Generator)return {};
|
||||
auto data=std::make_shared<ResolvedWorldSnapshot::Data>();
|
||||
data->configuration=MakeConfiguration(*owner);
|
||||
data->owner=std::move(owner);
|
||||
data->worldAssembly=std::move(worldAssembly);
|
||||
return ResolvedWorldSnapshot(std::move(data));
|
||||
}
|
||||
};
|
||||
@@ -232,6 +236,13 @@ namespace VoxelForgeCore
|
||||
{
|
||||
auto owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||
if(!StandaloneOwnerLoader::Resolve(jsonRoot,*owner,error))return {};
|
||||
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
|
||||
if(jsonRoot.Get("world_assembly").IsObject())
|
||||
{
|
||||
auto parsed=detail_world_assembly::ParseNestedManifest(jsonRoot,error);
|
||||
if(!parsed)return {};
|
||||
worldAssembly=std::make_shared<WorldAssemblyManifest>(std::move(*parsed));
|
||||
}
|
||||
|
||||
const bool seedChanged=owner->Seed!=seed;
|
||||
owner->Settings->Seed=seed;
|
||||
@@ -254,7 +265,7 @@ namespace VoxelForgeCore
|
||||
owner->MaxClipLevel=maxLodOverride;
|
||||
owner->Settings->MaxClipLevel=maxLodOverride;
|
||||
}
|
||||
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner));
|
||||
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner),std::move(worldAssembly));
|
||||
}
|
||||
|
||||
std::optional<ResolvedWorldSnapshot> ResolvedWorldSnapshot::FromJson(
|
||||
@@ -262,9 +273,21 @@ namespace VoxelForgeCore
|
||||
{
|
||||
auto owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||
if(!StandaloneOwnerLoader::LoadJson(json,*owner,error))return std::nullopt;
|
||||
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner));
|
||||
VFJson::Value root;
|
||||
if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt;
|
||||
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
|
||||
if(root.Get("world_assembly").IsObject())
|
||||
{
|
||||
auto parsed=detail_world_assembly::ParseNestedManifest(root,error);
|
||||
if(!parsed)return std::nullopt;
|
||||
worldAssembly=std::make_shared<WorldAssemblyManifest>(std::move(*parsed));
|
||||
}
|
||||
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner),std::move(worldAssembly));
|
||||
}
|
||||
|
||||
const WorldAssemblyManifest* ResolvedWorldSnapshot::worldAssembly() const noexcept
|
||||
{ return data_?data_->worldAssembly.get():nullptr; }
|
||||
|
||||
const WorldConfiguration& ResolvedWorldSnapshot::configuration() const
|
||||
{
|
||||
static const WorldConfiguration empty{};
|
||||
@@ -274,6 +297,7 @@ namespace VoxelForgeCore
|
||||
struct World::Impl
|
||||
{
|
||||
std::shared_ptr<FStandaloneOwnerWorld> owner;
|
||||
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
|
||||
WorldConfiguration configuration;
|
||||
std::unique_ptr<FStandaloneShapeWorld> shape;
|
||||
ShapeBuildRequest buildRequest;
|
||||
@@ -291,6 +315,7 @@ namespace VoxelForgeCore
|
||||
if(snapshot.data_)
|
||||
{
|
||||
impl_->owner=snapshot.data_->owner;
|
||||
impl_->worldAssembly=snapshot.data_->worldAssembly;
|
||||
impl_->configuration=snapshot.data_->configuration;
|
||||
}
|
||||
}
|
||||
@@ -307,7 +332,7 @@ namespace VoxelForgeCore
|
||||
static_cast<int32>(std::clamp<std::uint32_t>(request.workerLimit,1,16)),ToNative(request.detail),
|
||||
request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand,
|
||||
request.validatePassageRoutes,request.gpuVisualDetailTiles,
|
||||
request.cpuVisualDetailTiles);
|
||||
request.cpuVisualDetailTiles,impl_->worldAssembly.get());
|
||||
if(ok)
|
||||
{
|
||||
impl_->shape=std::move(shape);
|
||||
@@ -443,7 +468,7 @@ namespace VoxelForgeCore
|
||||
auto region=std::make_shared<FStandaloneShapeWorld>();
|
||||
if(!region->Build(*impl_->owner,fineMin,fineMax,regionMin,regionMax,
|
||||
static_cast<int32>(clampedWorkers),ToNative(base.detail),false,error,&scheduler,
|
||||
base.roundTunnels,base.profileDetailBand,false))return false;
|
||||
base.roundTunnels,base.profileDetailBand,false,{}, {},impl_->worldAssembly.get()))return false;
|
||||
if(!impl_->editLog.empty())region->SetEditTransaction(impl_->editLog);
|
||||
const auto& build=region->GetStats();
|
||||
++stats.regionsBuilt;
|
||||
|
||||
@@ -0,0 +1,766 @@
|
||||
#pragma once
|
||||
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
namespace world_assembly_detail
|
||||
{
|
||||
constexpr std::int64_t Q16 = 1ll << 16;
|
||||
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
||||
constexpr std::size_t MaxBiomeCount = 8;
|
||||
constexpr std::size_t MaxStrateCount = 64;
|
||||
constexpr std::size_t MaxFeatureCount = 4096;
|
||||
constexpr std::size_t MaxRoutePointCount = 4096;
|
||||
|
||||
inline void HashWord(std::uint64_t& hash, std::uint64_t word, int bytes)
|
||||
{
|
||||
for (int i = 0; i < bytes; ++i)
|
||||
{
|
||||
hash ^= static_cast<std::uint8_t>(word >> (i * 8));
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::uint64_t StartHash() { return 14695981039346656037ull; }
|
||||
|
||||
inline bool ReadInteger(const VFJson::Value& value, std::int64_t& out)
|
||||
{
|
||||
if (!value.IsNumber()) return false;
|
||||
const double number = value.Number();
|
||||
constexpr double ExactJsonIntegerLimit = 9007199254740991.0;
|
||||
if (!std::isfinite(number) || std::trunc(number) != number
|
||||
|| std::abs(number) > ExactJsonIntegerLimit) return false;
|
||||
out = static_cast<std::int64_t>(number);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadU32(const VFJson::Value& value, std::uint32_t& out)
|
||||
{
|
||||
std::int64_t parsed = 0;
|
||||
if (!ReadInteger(value, parsed) || parsed < 0
|
||||
|| parsed > std::numeric_limits<std::uint32_t>::max()) return false;
|
||||
out = static_cast<std::uint32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadI32(const VFJson::Value& value, std::int32_t& out)
|
||||
{
|
||||
std::int64_t parsed = 0;
|
||||
if (!ReadInteger(value, parsed) || parsed < std::numeric_limits<std::int32_t>::min()
|
||||
|| parsed > std::numeric_limits<std::int32_t>::max()) return false;
|
||||
out = static_cast<std::int32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadArray(const VFJson::Value& value, std::int64_t* out, std::size_t count)
|
||||
{
|
||||
if (!value.IsArray() || value.AsArray().size() != count) return false;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (!ReadInteger(value.AsArray()[i], out[i])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadHash(const VFJson::Value& value, std::uint64_t& out)
|
||||
{
|
||||
if (!value.IsString()) return false;
|
||||
std::string_view text = value.String();
|
||||
if (text.starts_with("0x") || text.starts_with("0X")) text.remove_prefix(2);
|
||||
if (text.empty() || text.size() > 16) return false;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), out, 16);
|
||||
return result.ec == std::errc{} && result.ptr == text.data() + text.size();
|
||||
}
|
||||
|
||||
inline std::string HashText(std::uint64_t hash)
|
||||
{
|
||||
constexpr char Hex[] = "0123456789abcdef";
|
||||
std::string out(16, '0');
|
||||
for (int i = 15; i >= 0; --i)
|
||||
{
|
||||
out[static_cast<std::size_t>(i)] = Hex[hash & 0xfu];
|
||||
hash >>= 4;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool ValidScalar(std::int64_t value)
|
||||
{ return value >= -MaxSerializedMagnitude && value <= MaxSerializedMagnitude; }
|
||||
|
||||
inline bool OutsideRange(std::int64_t value)
|
||||
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
|
||||
|
||||
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||
{
|
||||
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
||||
|| recipe.evaluatorVersion != WorldAssemblyEvaluatorVersion
|
||||
|| recipe.unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel)
|
||||
{ error = "world assembly recipe version, evaluator or fixed scale is unsupported"; return false; }
|
||||
if (recipe.biomes.empty() || recipe.biomes.size() > MaxBiomeCount)
|
||||
{ error = "world assembly recipe must contain one to eight biome profiles"; return false; }
|
||||
if (recipe.biomeCellSizeSubvoxels <= 0 || recipe.biomeCellSizeSubvoxels > 100000000ll
|
||||
|| recipe.biomeWarpSubvoxels < 0 || recipe.biomeWarpSubvoxels > recipe.biomeCellSizeSubvoxels / 2
|
||||
|| recipe.biomeWarpWavelengthSubvoxels <= 0
|
||||
|| recipe.biomeWarpWavelengthSubvoxels > 100000000ll
|
||||
|| recipe.biomeBorderBlendSubvoxels < 0
|
||||
|| recipe.biomeBorderBlendSubvoxels > recipe.biomeCellSizeSubvoxels)
|
||||
{ error = "world assembly biome-map parameters are outside the supported fixed-scale range"; return false; }
|
||||
std::vector<std::uint32_t> ids;
|
||||
ids.reserve(recipe.biomes.size());
|
||||
for (const auto& biome : recipe.biomes)
|
||||
{
|
||||
if (biome.stableId == 0 || std::find(ids.begin(), ids.end(), biome.stableId) != ids.end())
|
||||
{ error = "world assembly biome IDs must be nonzero and unique"; return false; }
|
||||
ids.push_back(biome.stableId);
|
||||
const std::array<std::int64_t, 7> values = {biome.baseHeightSubvoxels,
|
||||
biome.continentAmplitudeSubvoxels, biome.continentWavelengthSubvoxels,
|
||||
biome.mountainAmplitudeSubvoxels, biome.mountainWavelengthSubvoxels,
|
||||
biome.detailAmplitudeSubvoxels, biome.detailWavelengthSubvoxels};
|
||||
if (!std::all_of(values.begin(), values.end(), ValidScalar)
|
||||
|| biome.continentAmplitudeSubvoxels < 0 || biome.mountainAmplitudeSubvoxels < 0
|
||||
|| biome.detailAmplitudeSubvoxels < 0 || biome.continentWavelengthSubvoxels <= 0
|
||||
|| biome.mountainWavelengthSubvoxels <= 0 || biome.detailWavelengthSubvoxels <= 0)
|
||||
{ error = "world assembly biome profile has an invalid fixed-scale value"; return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum, std::string& error)
|
||||
{
|
||||
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
||||
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
||||
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
||||
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
|
||||
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|
||||
|| !ValidScalar(stratum.capWavelengthSubvoxels))
|
||||
{ error = "world assembly cap contains a value outside the fixed-scale range"; return false; }
|
||||
std::vector<std::uint64_t> ids;
|
||||
ids.reserve(stratum.features.size());
|
||||
for (const auto& feature : stratum.features)
|
||||
{
|
||||
if (feature.stableId == 0 || feature.type < 1 || feature.type > 3
|
||||
|| std::find(ids.begin(), ids.end(), feature.stableId) != ids.end())
|
||||
{ error = "world assembly feature IDs and types must be stable, unique and supported"; return false; }
|
||||
ids.push_back(feature.stableId);
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
if (!ValidScalar(feature.minimumSubvoxels[axis])
|
||||
|| !ValidScalar(feature.maximumSubvoxels[axis])
|
||||
|| feature.minimumSubvoxels[axis] > feature.maximumSubvoxels[axis])
|
||||
{ error = "world assembly feature has an invalid influence bound"; return false; }
|
||||
if (!std::all_of(feature.parametersSubvoxels.begin(), feature.parametersSubvoxels.end(), ValidScalar))
|
||||
{ error = "world assembly feature parameter is outside the fixed-scale range"; return false; }
|
||||
}
|
||||
for (const auto& point : stratum.route)
|
||||
{
|
||||
if (point.stableFeatureId == 0 || point.radiusSubvoxels <= 0
|
||||
|| !ValidScalar(point.radiusSubvoxels)
|
||||
|| !std::all_of(point.positionSubvoxels.begin(), point.positionSubvoxels.end(), ValidScalar))
|
||||
{ error = "world assembly route point has an invalid feature ID, position or radius"; return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashRecipe(const WorldAssemblyRecipeV1& recipe)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, recipe.schemaVersion, 4); HashWord(hash, recipe.evaluatorVersion, 4);
|
||||
HashWord(hash, recipe.unitsPerVoxel, 4); HashWord(hash, recipe.seed, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeCellSizeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpWavelengthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeBorderBlendSubvoxels), 8);
|
||||
HashWord(hash, recipe.biomes.size(), 4);
|
||||
for (const auto& biome : recipe.biomes)
|
||||
{
|
||||
HashWord(hash, biome.stableId, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.baseHeightSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.continentAmplitudeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.continentWavelengthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.mountainAmplitudeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.mountainWavelengthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.detailAmplitudeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.detailWavelengthSubvoxels), 8);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashStratum(const WorldAssemblyStratumPlanV1& stratum)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, static_cast<std::uint32_t>(stratum.strateIndex), 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(stratum.topChunkZ), 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(stratum.bottomChunkZ), 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(stratum.capHeightSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(stratum.capUndulationSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(stratum.capWavelengthSubvoxels), 8);
|
||||
HashWord(hash, stratum.features.size(), 4);
|
||||
for (const auto& feature : stratum.features)
|
||||
{
|
||||
HashWord(hash, feature.stableId, 8); HashWord(hash, feature.type, 4);
|
||||
for (const auto value : feature.minimumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
for (const auto value : feature.maximumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
for (const auto value : feature.parametersSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
}
|
||||
HashWord(hash, stratum.route.size(), 4);
|
||||
for (const auto& point : stratum.route)
|
||||
{
|
||||
HashWord(hash, point.stableFeatureId, 8);
|
||||
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashPlan(const WorldAssemblyPlanV1& plan)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, plan.schemaVersion, 4); HashWord(hash, plan.strates.size(), 4);
|
||||
for (const auto& stratum : plan.strates) HashWord(hash, stratum.contentHash, 8);
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashManifest(const WorldAssemblyManifest& manifest)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, manifest.schemaVersion, 4);
|
||||
HashWord(hash, manifest.recipe.contentHash, 8);
|
||||
HashWord(hash, manifest.plan.contentHash, 8);
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
||||
std::string& error)
|
||||
{
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), recipe.schemaVersion)
|
||||
|| !ReadU32(value.Get("evaluator_version"), recipe.evaluatorVersion)
|
||||
|| !ReadU32(value.Get("units_per_voxel"), recipe.unitsPerVoxel)
|
||||
|| !ReadU32(value.Get("seed"), recipe.seed)
|
||||
|| !ReadInteger(value.Get("biome_cell_size_q"), recipe.biomeCellSizeSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_warp_q"), recipe.biomeWarpSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_warp_wavelength_q"), recipe.biomeWarpWavelengthSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_border_blend_q"), recipe.biomeBorderBlendSubvoxels)
|
||||
|| !ReadHash(value.Get("content_hash"), recipe.contentHash)
|
||||
|| !value.Get("biomes").IsArray())
|
||||
{ error = "world assembly recipe is missing a required versioned fixed-scale field"; return false; }
|
||||
for (const auto& item : value.Get("biomes").AsArray())
|
||||
{
|
||||
WorldAssemblyBiomeProfileV1 biome;
|
||||
if (!item.IsObject() || !ReadU32(item.Get("id"), biome.stableId)
|
||||
|| !ReadInteger(item.Get("base_height_q"), biome.baseHeightSubvoxels)
|
||||
|| !ReadInteger(item.Get("continent_amplitude_q"), biome.continentAmplitudeSubvoxels)
|
||||
|| !ReadInteger(item.Get("continent_wavelength_q"), biome.continentWavelengthSubvoxels)
|
||||
|| !ReadInteger(item.Get("mountain_amplitude_q"), biome.mountainAmplitudeSubvoxels)
|
||||
|| !ReadInteger(item.Get("mountain_wavelength_q"), biome.mountainWavelengthSubvoxels)
|
||||
|| !ReadInteger(item.Get("detail_amplitude_q"), biome.detailAmplitudeSubvoxels)
|
||||
|| !ReadInteger(item.Get("detail_wavelength_q"), biome.detailWavelengthSubvoxels))
|
||||
{ error = "world assembly biome profile is missing a required fixed-scale field"; return false; }
|
||||
recipe.biomes.push_back(biome);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ParsePlan(const VFJson::Value& value, WorldAssemblyPlanV1& plan,
|
||||
std::string& error)
|
||||
{
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), plan.schemaVersion)
|
||||
|| !ReadHash(value.Get("content_hash"), plan.contentHash)
|
||||
|| !value.Get("strates").IsArray())
|
||||
{ error = "world assembly plan is missing a required versioned field"; return false; }
|
||||
for (const auto& item : value.Get("strates").AsArray())
|
||||
{
|
||||
WorldAssemblyStratumPlanV1 stratum;
|
||||
if (!item.IsObject() || !ReadI32(item.Get("strate_index"), stratum.strateIndex)
|
||||
|| !ReadI32(item.Get("top_chunk_z"), stratum.topChunkZ)
|
||||
|| !ReadI32(item.Get("bottom_chunk_z"), stratum.bottomChunkZ)
|
||||
|| !ReadInteger(item.Get("cap_height_q"), stratum.capHeightSubvoxels)
|
||||
|| !ReadInteger(item.Get("cap_undulation_q"), stratum.capUndulationSubvoxels)
|
||||
|| !ReadInteger(item.Get("cap_wavelength_q"), stratum.capWavelengthSubvoxels)
|
||||
|| !ReadHash(item.Get("content_hash"), stratum.contentHash)
|
||||
|| !item.Get("features").IsArray() || !item.Get("route").IsArray())
|
||||
{ error = "world assembly stratum is missing a required fixed-scale field"; return false; }
|
||||
for (const auto& featureValue : item.Get("features").AsArray())
|
||||
{
|
||||
WorldAssemblyFeatureV1 feature;
|
||||
std::uint32_t type = 0;
|
||||
if (!featureValue.IsObject() || !ReadHash(featureValue.Get("stable_id"), feature.stableId)
|
||||
|| !ReadU32(featureValue.Get("type"), type)
|
||||
|| !ReadArray(featureValue.Get("min_q"), feature.minimumSubvoxels.data(), 3)
|
||||
|| !ReadArray(featureValue.Get("max_q"), feature.maximumSubvoxels.data(), 3)
|
||||
|| !ReadArray(featureValue.Get("parameters_q"), feature.parametersSubvoxels.data(), 4))
|
||||
{ error = "world assembly feature is missing a fixed-scale field"; return false; }
|
||||
feature.type = type;
|
||||
stratum.features.push_back(feature);
|
||||
}
|
||||
for (const auto& pointValue : item.Get("route").AsArray())
|
||||
{
|
||||
WorldAssemblyRoutePointV1 point;
|
||||
if (!pointValue.IsObject() || !ReadHash(pointValue.Get("feature_id"), point.stableFeatureId)
|
||||
|| !ReadArray(pointValue.Get("position_q"), point.positionSubvoxels.data(), 3)
|
||||
|| !ReadInteger(pointValue.Get("radius_q"), point.radiusSubvoxels))
|
||||
{ error = "world assembly route point is missing a fixed-scale field"; return false; }
|
||||
stratum.route.push_back(point);
|
||||
}
|
||||
plan.strates.push_back(std::move(stratum));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::optional<WorldAssemblyManifest> ParseManifestValue(const VFJson::Value& value,
|
||||
std::string& error)
|
||||
{
|
||||
WorldAssemblyManifest manifest;
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), manifest.schemaVersion)
|
||||
|| !ReadHash(value.Get("content_hash"), manifest.contentHash)
|
||||
|| !ParseRecipe(value.Get("recipe"), manifest.recipe, error)
|
||||
|| !ParsePlan(value.Get("plan"), manifest.plan, error))
|
||||
{
|
||||
if (error.empty()) error = "world assembly manifest is missing a required field";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!manifest.validate(error)) return std::nullopt;
|
||||
return manifest;
|
||||
}
|
||||
|
||||
inline std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor)
|
||||
{
|
||||
const std::int64_t quotient = value / divisor, remainder = value % divisor;
|
||||
return quotient - ((remainder != 0 && remainder < 0) ? 1 : 0);
|
||||
}
|
||||
|
||||
inline std::int64_t FloorMod(std::int64_t value, std::int64_t divisor)
|
||||
{ return value - FloorDiv(value, divisor) * divisor; }
|
||||
|
||||
inline std::uint64_t Mix64(std::uint64_t value)
|
||||
{
|
||||
value += 0x9e3779b97f4a7c15ull;
|
||||
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
|
||||
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
|
||||
return value ^ (value >> 31);
|
||||
}
|
||||
|
||||
inline std::uint64_t CoordinateHash(std::uint32_t seed, std::int64_t x,
|
||||
std::int64_t y, std::uint64_t salt)
|
||||
{
|
||||
std::uint64_t hash = Mix64(static_cast<std::uint64_t>(seed) ^ salt);
|
||||
hash ^= Mix64(static_cast<std::uint64_t>(x) + 0x517cc1b727220a95ull);
|
||||
hash ^= Mix64(static_cast<std::uint64_t>(y) + 0x6eed0e9da4d94a4full);
|
||||
return Mix64(hash);
|
||||
}
|
||||
|
||||
inline std::int32_t LatticeValue(std::uint32_t seed, std::int64_t x,
|
||||
std::int64_t y, std::uint64_t salt)
|
||||
{
|
||||
const std::uint32_t bits = static_cast<std::uint32_t>(CoordinateHash(seed, x, y, salt) >> 48) & 0x1ffffu;
|
||||
return static_cast<std::int32_t>(bits) - 65536;
|
||||
}
|
||||
|
||||
inline std::int32_t LerpQ16(std::int32_t a, std::int32_t b, std::uint32_t t)
|
||||
{ return static_cast<std::int32_t>(a + (static_cast<std::int64_t>(b - a) * t) / Q16); }
|
||||
|
||||
inline std::uint32_t SmoothQ16(std::uint32_t t)
|
||||
{
|
||||
const std::int64_t t2 = (static_cast<std::int64_t>(t) * t) / Q16;
|
||||
return static_cast<std::uint32_t>((t2 * (3 * Q16 - 2 * static_cast<std::int64_t>(t))) / Q16);
|
||||
}
|
||||
|
||||
inline std::int32_t Noise2D(std::int64_t x, std::int64_t y, std::int64_t wavelength,
|
||||
std::uint32_t seed, std::uint64_t salt)
|
||||
{
|
||||
const std::int64_t cellX = FloorDiv(x, wavelength), cellY = FloorDiv(y, wavelength);
|
||||
const std::uint32_t fractionX = static_cast<std::uint32_t>((FloorMod(x, wavelength) * Q16) / wavelength);
|
||||
const std::uint32_t fractionY = static_cast<std::uint32_t>((FloorMod(y, wavelength) * Q16) / wavelength);
|
||||
const std::uint32_t smoothX = SmoothQ16(fractionX), smoothY = SmoothQ16(fractionY);
|
||||
const std::int32_t low = LerpQ16(LatticeValue(seed, cellX, cellY, salt),
|
||||
LatticeValue(seed, cellX + 1, cellY, salt), smoothX);
|
||||
const std::int32_t high = LerpQ16(LatticeValue(seed, cellX, cellY + 1, salt),
|
||||
LatticeValue(seed, cellX + 1, cellY + 1, salt), smoothX);
|
||||
return LerpQ16(low, high, smoothY);
|
||||
}
|
||||
|
||||
inline std::uint32_t RatioQ16(std::uint64_t numerator, std::uint64_t denominator)
|
||||
{
|
||||
if (denominator == 0 || numerator >= denominator) return 65536;
|
||||
std::uint32_t result = 0;
|
||||
std::uint64_t remainder = numerator;
|
||||
for (int bit = 15; bit >= 0; --bit)
|
||||
{
|
||||
remainder *= 2;
|
||||
if (remainder >= denominator)
|
||||
{ remainder -= denominator; result |= (1u << bit); }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::int64_t MulQ16(std::int64_t value, std::int64_t q16)
|
||||
{ return (value * q16) / Q16; }
|
||||
|
||||
inline std::int64_t ProfileHeight(const WorldAssemblyBiomeProfileV1& biome,
|
||||
std::int64_t x, std::int64_t y, std::uint32_t seed)
|
||||
{
|
||||
const std::int64_t continent = MulQ16(biome.continentAmplitudeSubvoxels,
|
||||
Noise2D(x, y, biome.continentWavelengthSubvoxels, seed, 0x434f4e54494e454eull));
|
||||
const std::int64_t mountain = MulQ16(biome.mountainAmplitudeSubvoxels,
|
||||
Noise2D(x, y, biome.mountainWavelengthSubvoxels, seed, 0x4d4f554e5441494eull));
|
||||
const std::int64_t detail = MulQ16(biome.detailAmplitudeSubvoxels,
|
||||
Noise2D(x, y, biome.detailWavelengthSubvoxels, seed, 0x44455441494c3031ull));
|
||||
return biome.baseHeightSubvoxels + continent + mountain + detail;
|
||||
}
|
||||
|
||||
inline bool InInterval(const FieldInterval& interval, std::int64_t density)
|
||||
{ return interval.known && density >= interval.minimumDensitySubvoxels
|
||||
&& density <= interval.maximumDensitySubvoxels; }
|
||||
}
|
||||
|
||||
namespace detail_world_assembly
|
||||
{
|
||||
inline std::optional<WorldAssemblyManifest> ParseNestedManifest(const VFJson::Value& root,
|
||||
std::string& error)
|
||||
{
|
||||
const VFJson::Value& payload = root.Get("world_assembly");
|
||||
if (!payload.IsObject()) return std::nullopt;
|
||||
return world_assembly_detail::ParseManifestValue(payload, error);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
|
||||
std::string_view json, std::string& error)
|
||||
{
|
||||
VFJson::Value root;
|
||||
if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt;
|
||||
return world_assembly_detail::ParseManifestValue(root, error);
|
||||
}
|
||||
|
||||
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; }
|
||||
std::vector<std::int32_t> strateIds;
|
||||
for (auto& stratum : plan.strates)
|
||||
{
|
||||
if (!world_assembly_detail::ValidateStratum(stratum, error)) return false;
|
||||
if (std::find(strateIds.begin(), strateIds.end(), stratum.strateIndex) != strateIds.end())
|
||||
{ error = "world assembly plan contains a duplicate strate index"; return false; }
|
||||
strateIds.push_back(stratum.strateIndex);
|
||||
stratum.contentHash = world_assembly_detail::HashStratum(stratum);
|
||||
}
|
||||
recipe.contentHash = world_assembly_detail::HashRecipe(recipe);
|
||||
plan.contentHash = world_assembly_detail::HashPlan(plan);
|
||||
contentHash = world_assembly_detail::HashManifest(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldAssemblyManifest::validate(std::string& error) const
|
||||
{
|
||||
WorldAssemblyManifest copy = *this;
|
||||
const std::uint64_t expectedRecipe = recipe.contentHash;
|
||||
const std::uint64_t expectedPlan = plan.contentHash;
|
||||
const std::uint64_t expectedManifest = contentHash;
|
||||
std::vector<std::uint64_t> expectedStrates;
|
||||
for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.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())
|
||||
{ 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; }
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string WorldAssemblyManifest::ToJson() const
|
||||
{
|
||||
std::string error;
|
||||
WorldAssemblyManifest value = *this;
|
||||
if (!value.finalize(error)) return {};
|
||||
std::ostringstream out;
|
||||
out << "{\"schema_version\":" << value.schemaVersion
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.contentHash)
|
||||
<< "\",\"recipe\":{\"schema_version\":" << value.recipe.schemaVersion
|
||||
<< ",\"evaluator_version\":" << value.recipe.evaluatorVersion
|
||||
<< ",\"units_per_voxel\":" << value.recipe.unitsPerVoxel
|
||||
<< ",\"seed\":" << value.recipe.seed
|
||||
<< ",\"biome_cell_size_q\":" << value.recipe.biomeCellSizeSubvoxels
|
||||
<< ",\"biome_warp_q\":" << value.recipe.biomeWarpSubvoxels
|
||||
<< ",\"biome_warp_wavelength_q\":" << value.recipe.biomeWarpWavelengthSubvoxels
|
||||
<< ",\"biome_border_blend_q\":" << value.recipe.biomeBorderBlendSubvoxels
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.recipe.contentHash)
|
||||
<< "\",\"biomes\":[";
|
||||
for (std::size_t i = 0; i < value.recipe.biomes.size(); ++i)
|
||||
{
|
||||
const auto& biome = value.recipe.biomes[i];
|
||||
if (i) out << ',';
|
||||
out << "{\"id\":" << biome.stableId << ",\"base_height_q\":" << biome.baseHeightSubvoxels
|
||||
<< ",\"continent_amplitude_q\":" << biome.continentAmplitudeSubvoxels
|
||||
<< ",\"continent_wavelength_q\":" << biome.continentWavelengthSubvoxels
|
||||
<< ",\"mountain_amplitude_q\":" << biome.mountainAmplitudeSubvoxels
|
||||
<< ",\"mountain_wavelength_q\":" << biome.mountainWavelengthSubvoxels
|
||||
<< ",\"detail_amplitude_q\":" << biome.detailAmplitudeSubvoxels
|
||||
<< ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels << '}';
|
||||
}
|
||||
out << "]},\"plan\":{\"schema_version\":" << value.plan.schemaVersion
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash)
|
||||
<< "\",\"strates\":[";
|
||||
for (std::size_t i = 0; i < value.plan.strates.size(); ++i)
|
||||
{
|
||||
const auto& stratum = value.plan.strates[i];
|
||||
if (i) out << ',';
|
||||
out << "{\"strate_index\":" << stratum.strateIndex << ",\"top_chunk_z\":" << stratum.topChunkZ
|
||||
<< ",\"bottom_chunk_z\":" << stratum.bottomChunkZ
|
||||
<< ",\"cap_height_q\":" << stratum.capHeightSubvoxels
|
||||
<< ",\"cap_undulation_q\":" << stratum.capUndulationSubvoxels
|
||||
<< ",\"cap_wavelength_q\":" << stratum.capWavelengthSubvoxels
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
||||
<< "\",\"features\":[";
|
||||
for (std::size_t f = 0; f < stratum.features.size(); ++f)
|
||||
{
|
||||
const auto& feature = stratum.features[f];
|
||||
if (f) out << ',';
|
||||
out << "{\"stable_id\":\"" << world_assembly_detail::HashText(feature.stableId)
|
||||
<< "\",\"type\":" << feature.type << ",\"min_q\":["
|
||||
<< feature.minimumSubvoxels[0] << ',' << feature.minimumSubvoxels[1] << ',' << feature.minimumSubvoxels[2]
|
||||
<< "],\"max_q\":[" << feature.maximumSubvoxels[0] << ',' << feature.maximumSubvoxels[1]
|
||||
<< ',' << feature.maximumSubvoxels[2] << "],\"parameters_q\":["
|
||||
<< feature.parametersSubvoxels[0] << ',' << feature.parametersSubvoxels[1] << ','
|
||||
<< feature.parametersSubvoxels[2] << ',' << feature.parametersSubvoxels[3] << "]}";
|
||||
}
|
||||
out << "],\"route\":[";
|
||||
for (std::size_t p = 0; p < stratum.route.size(); ++p)
|
||||
{
|
||||
const auto& point = stratum.route[p];
|
||||
if (p) out << ',';
|
||||
out << "{\"feature_id\":\"" << world_assembly_detail::HashText(point.stableFeatureId)
|
||||
<< "\",\"position_q\":[" << point.positionSubvoxels[0] << ','
|
||||
<< point.positionSubvoxels[1] << ',' << point.positionSubvoxels[2]
|
||||
<< "],\"radius_q\":" << point.radiusSubvoxels << '}';
|
||||
}
|
||||
out << "]}";
|
||||
}
|
||||
out << "]}}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe,
|
||||
const WorldAssemblyStratumPlanV1& plan) noexcept
|
||||
: recipe_(&recipe), plan_(&plan)
|
||||
{}
|
||||
|
||||
bool Heightfield2D::isValid() const noexcept
|
||||
{
|
||||
if (!recipe_ || !plan_ || recipe_->schemaVersion != WorldAssemblySchemaVersion
|
||||
|| recipe_->evaluatorVersion != WorldAssemblyEvaluatorVersion
|
||||
|| recipe_->unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel
|
||||
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
||||
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
||||
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
||||
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
||||
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
||||
return false;
|
||||
return std::all_of(recipe_->biomes.begin(), recipe_->biomes.end(), [](const auto& biome)
|
||||
{
|
||||
return biome.stableId != 0 && biome.continentAmplitudeSubvoxels >= 0
|
||||
&& biome.mountainAmplitudeSubvoxels >= 0 && biome.detailAmplitudeSubvoxels >= 0
|
||||
&& biome.continentWavelengthSubvoxels > 0 && biome.mountainWavelengthSubvoxels > 0
|
||||
&& biome.detailWavelengthSubvoxels > 0;
|
||||
});
|
||||
}
|
||||
|
||||
HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y,
|
||||
std::int64_t z) const noexcept
|
||||
{
|
||||
HeightfieldPointSample out;
|
||||
if (!isValid() || world_assembly_detail::OutsideRange(x)
|
||||
|| world_assembly_detail::OutsideRange(y)
|
||||
|| world_assembly_detail::OutsideRange(z)) return out;
|
||||
using namespace world_assembly_detail;
|
||||
const std::int64_t warpX = MulQ16(recipe_->biomeWarpSubvoxels,
|
||||
Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f585f31ull));
|
||||
const std::int64_t warpY = MulQ16(recipe_->biomeWarpSubvoxels,
|
||||
Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f595f31ull));
|
||||
const std::int64_t warpedX = x + warpX, warpedY = y + warpY;
|
||||
const std::int64_t cellX = FloorDiv(warpedX, recipe_->biomeCellSizeSubvoxels);
|
||||
const std::int64_t cellY = FloorDiv(warpedY, recipe_->biomeCellSizeSubvoxels);
|
||||
std::uint64_t nearestDistance = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t secondDistance = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint32_t nearestId = 0, secondId = 0;
|
||||
const std::uint32_t jitterSeed = recipe_->seed ^ 0xa341316cu;
|
||||
const std::int64_t cellSize = recipe_->biomeCellSizeSubvoxels;
|
||||
for (std::int64_t cy = cellY - 2; cy <= cellY + 2; ++cy)
|
||||
for (std::int64_t cx = cellX - 2; cx <= cellX + 2; ++cx)
|
||||
{
|
||||
const std::uint64_t h = CoordinateHash(jitterSeed, cx, cy, 0x42494f4d455f5349ull);
|
||||
const std::int64_t jitterX = static_cast<std::int64_t>((h & 0xffffu)) * (cellSize / 2) / 65535 - cellSize / 4;
|
||||
const std::int64_t jitterY = static_cast<std::int64_t>(((h >> 16) & 0xffffu)) * (cellSize / 2) / 65535 - cellSize / 4;
|
||||
const std::int64_t siteX = cx * cellSize + cellSize / 2 + jitterX;
|
||||
const std::int64_t siteY = cy * cellSize + cellSize / 2 + jitterY;
|
||||
const std::int64_t dx = warpedX - siteX, dy = warpedY - siteY;
|
||||
const std::uint64_t distance = static_cast<std::uint64_t>(dx * dx + dy * dy);
|
||||
const std::uint32_t biomeId = recipe_->biomes[CoordinateHash(jitterSeed, cx, cy,
|
||||
0x42494f4d455f4944ull) % recipe_->biomes.size()].stableId;
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
secondDistance = nearestDistance; secondId = nearestId;
|
||||
nearestDistance = distance; nearestId = biomeId;
|
||||
}
|
||||
else if (distance < secondDistance && biomeId != nearestId)
|
||||
{ secondDistance = distance; secondId = biomeId; }
|
||||
}
|
||||
const auto findBiome = [&](std::uint32_t id) -> const WorldAssemblyBiomeProfileV1*
|
||||
{
|
||||
const auto found = std::find_if(recipe_->biomes.begin(), recipe_->biomes.end(),
|
||||
[id](const auto& biome) { return biome.stableId == id; });
|
||||
return found == recipe_->biomes.end() ? nullptr : &*found;
|
||||
};
|
||||
const auto* primary = findBiome(nearestId);
|
||||
const auto* secondary = findBiome(secondId ? secondId : nearestId);
|
||||
if (!primary || !secondary) return out;
|
||||
std::uint32_t primaryWeight = 65536;
|
||||
if (recipe_->biomeBorderBlendSubvoxels > 0 && nearestDistance < secondDistance)
|
||||
{
|
||||
const std::uint64_t difference = secondDistance - nearestDistance;
|
||||
const std::uint64_t denominator = static_cast<std::uint64_t>(2 * cellSize)
|
||||
* static_cast<std::uint64_t>(recipe_->biomeBorderBlendSubvoxels);
|
||||
primaryWeight = SmoothQ16(RatioQ16(difference, denominator));
|
||||
}
|
||||
const std::int64_t primaryHeight = ProfileHeight(*primary, warpedX, warpedY, recipe_->seed);
|
||||
const std::int64_t secondaryHeight = ProfileHeight(*secondary, warpedX, warpedY, recipe_->seed);
|
||||
out.groundHeightSubvoxels = (primaryHeight * primaryWeight
|
||||
+ secondaryHeight * (65536 - primaryWeight)) / 65536;
|
||||
out.biomeId = primary->stableId;
|
||||
out.neighborBiomeId = secondary->stableId;
|
||||
out.biomeWeightQ16 = primaryWeight;
|
||||
out.capHeightSubvoxels = plan_->capHeightSubvoxels + MulQ16(plan_->capUndulationSubvoxels,
|
||||
Noise2D(x, y, plan_->capWavelengthSubvoxels, recipe_->seed, 0x4341505f554e4431ull));
|
||||
out.densitySubvoxels = std::min(z - out.groundHeightSubvoxels,
|
||||
out.capHeightSubvoxels - z);
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY,
|
||||
std::int64_t minZ, std::int64_t maxX, std::int64_t maxY, std::int64_t maxZ) const noexcept
|
||||
{
|
||||
FieldInterval out;
|
||||
if (!isValid() || minX > maxX || minY > maxY || minZ > maxZ
|
||||
|| world_assembly_detail::OutsideRange(minX)
|
||||
|| world_assembly_detail::OutsideRange(minY)
|
||||
|| world_assembly_detail::OutsideRange(minZ)
|
||||
|| world_assembly_detail::OutsideRange(maxX)
|
||||
|| world_assembly_detail::OutsideRange(maxY)
|
||||
|| world_assembly_detail::OutsideRange(maxZ)) return out;
|
||||
std::int64_t groundMin = std::numeric_limits<std::int64_t>::max();
|
||||
std::int64_t groundMax = std::numeric_limits<std::int64_t>::min();
|
||||
for (const auto& biome : recipe_->biomes)
|
||||
{
|
||||
const std::int64_t amplitude = biome.continentAmplitudeSubvoxels
|
||||
+ biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels;
|
||||
groundMin = std::min(groundMin, biome.baseHeightSubvoxels - amplitude);
|
||||
groundMax = std::max(groundMax, biome.baseHeightSubvoxels + amplitude);
|
||||
}
|
||||
const std::int64_t capMin = plan_->capHeightSubvoxels - plan_->capUndulationSubvoxels;
|
||||
const std::int64_t capMax = plan_->capHeightSubvoxels + plan_->capUndulationSubvoxels;
|
||||
const std::int64_t groundLower = minZ - groundMax;
|
||||
const std::int64_t groundUpper = maxZ - groundMin;
|
||||
const std::int64_t capLower = capMin - maxZ;
|
||||
const std::int64_t capUpper = capMax - minZ;
|
||||
out.known = true;
|
||||
out.minimumDensitySubvoxels = std::min(groundLower, capLower);
|
||||
out.maximumDensitySubvoxels = std::min(groundUpper, capUpper);
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldInfluenceBounds Heightfield2D::influenceBounds() const noexcept
|
||||
{
|
||||
FieldInfluenceBounds out;
|
||||
if (!isValid()) return out;
|
||||
out.minimumZSubvoxels = static_cast<std::int64_t>(plan_->bottomChunkZ) * 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
out.maximumZSubvoxels = (static_cast<std::int64_t>(plan_->topChunkZ) + 1) * 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
|
||||
{
|
||||
FieldDetailEnvelope out;
|
||||
if (!isValid()) return out;
|
||||
std::int64_t maximum = plan_->capUndulationSubvoxels;
|
||||
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
||||
for (const auto& biome : recipe_->biomes)
|
||||
{
|
||||
maximum = std::max(maximum, biome.continentAmplitudeSubvoxels
|
||||
+ biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels);
|
||||
minimumFeature = std::min({minimumFeature, biome.continentWavelengthSubvoxels,
|
||||
biome.mountainWavelengthSubvoxels, biome.detailWavelengthSubvoxels});
|
||||
}
|
||||
out.maximumAbsoluteDisplacementSubvoxels = maximum;
|
||||
out.minimumFeatureSizeSubvoxels = std::max<std::int64_t>(1,
|
||||
std::min(minimumFeature, recipe_->biomeBorderBlendSubvoxels > 0
|
||||
? recipe_->biomeBorderBlendSubvoxels : minimumFeature));
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldBlockContract Heightfield2D::contract() const noexcept
|
||||
{
|
||||
return {ChannelBit(FieldChannel::biome),
|
||||
ChannelBit(FieldChannel::height) | ChannelBit(FieldChannel::rock),
|
||||
WorldAssemblyEvaluatorVersion};
|
||||
}
|
||||
|
||||
WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block,
|
||||
std::uint32_t boxCount, std::uint64_t seed)
|
||||
{
|
||||
WorldAssemblyBoundsAuditResult result;
|
||||
if (!block.isValid()) return result;
|
||||
boxCount = std::clamp<std::uint32_t>(boxCount, 1, 100000);
|
||||
const auto random = [&seed]()
|
||||
{
|
||||
seed = world_assembly_detail::Mix64(seed);
|
||||
return seed;
|
||||
};
|
||||
const auto signedVoxel = [&random]()
|
||||
{ return static_cast<std::int64_t>(random() % 12001) - 6000; };
|
||||
for (std::uint32_t i = 0; i < boxCount; ++i)
|
||||
{
|
||||
const std::int64_t x = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t y = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
|
||||
const auto column = block.point(x, y, 0);
|
||||
const std::int64_t z = column.groundHeightSubvoxels
|
||||
+ (static_cast<std::int64_t>(random() % 257) - 128) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfX = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfY = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfZ = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const FieldInterval bounds = block.interval(x - halfX, y - halfY, z - halfZ,
|
||||
x + halfX, y + halfY, z + halfZ);
|
||||
++result.boxesTested;
|
||||
if (!bounds.known) { ++result.unknownBounds; continue; }
|
||||
for (int iz = 0; iz <= 2; ++iz)
|
||||
for (int iy = 0; iy <= 2; ++iy)
|
||||
for (int ix = 0; ix <= 2; ++ix)
|
||||
{
|
||||
const std::int64_t px = x - halfX + (2 * halfX * ix) / 2;
|
||||
const std::int64_t py = y - halfY + (2 * halfY * iy) / 2;
|
||||
const std::int64_t pz = z - halfZ + (2 * halfZ * iz) / 2;
|
||||
const auto sample = block.point(px, py, pz);
|
||||
++result.pointSamples;
|
||||
if (!world_assembly_detail::InInterval(bounds, sample.densitySubvoxels))
|
||||
++result.missedBounds;
|
||||
}
|
||||
}
|
||||
const auto ground = block.point(0, 0, 0).groundHeightSubvoxels;
|
||||
const auto controlPoint = block.point(0, 0, ground + 17).densitySubvoxels;
|
||||
// Negative control: this fabricated one-sided range excludes its known sample. The same
|
||||
// containment check used by the brute-force audit must reject the deliberate lie.
|
||||
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
||||
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
||||
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
||||
result.passed = result.boxesTested == boxCount && result.missedBounds == 0
|
||||
&& result.unknownBounds == 0 && result.negativeControlDetected;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ExecutableName = 'VoxelForgeUnrealSurfaceNative.exe',
|
||||
[ValidateRange(1, 16)][int]$Workers = 8,
|
||||
[ValidateRange(30, 900)][int]$TimeoutSeconds = 120
|
||||
)
|
||||
|
||||
# Rejoue les probes de strates sauvegardées et fait échouer l'audit si un passage redevient solide.
|
||||
# Replays the saved strate probes and fails the audit if any passage becomes solid again.
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$StandaloneRoot = Join-Path $PluginRoot 'Standalone'
|
||||
$ExecutablePath = Join-Path $StandaloneRoot ('bin\' + $ExecutableName)
|
||||
$ProbeRoot = Join-Path $StandaloneRoot 'Research\OwnerShapeWorld\core_strate_coverage\probes'
|
||||
$LogRoot = Join-Path $ProbeRoot ('round5_walkability_w' + $Workers)
|
||||
|
||||
if ([IO.Path]::GetFileName($ExecutableName) -ne $ExecutableName -or
|
||||
[IO.Path]::GetExtension($ExecutableName) -ne '.exe') {
|
||||
throw 'ExecutableName must be an executable filename.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf)) {
|
||||
throw "Viewer not found: $ExecutablePath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ProbeRoot -PathType Container)) {
|
||||
throw "Passage probe directory not found: $ProbeRoot"
|
||||
}
|
||||
[IO.Directory]::CreateDirectory($LogRoot) | Out-Null
|
||||
|
||||
$HostProcess = Get-Process -Id $PID
|
||||
$HostProcess.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$HostProcess.ProcessorAffinity = [IntPtr]0xFFFF
|
||||
|
||||
$Probes = @(Get-ChildItem -LiteralPath $ProbeRoot -Filter '*.json' -File | Sort-Object Name)
|
||||
if ($Probes.Count -ne 15) {
|
||||
throw "Expected all 15 saved strate probes, found $($Probes.Count)."
|
||||
}
|
||||
|
||||
$Passed = 0
|
||||
foreach ($Probe in $Probes) {
|
||||
$Stem = [IO.Path]::GetFileNameWithoutExtension($Probe.Name)
|
||||
$StdoutPath = Join-Path $LogRoot ($Stem + '.stdout.log')
|
||||
$StderrPath = Join-Path $LogRoot ($Stem + '.stderr.log')
|
||||
# Start-Process flattens this array into one command-line string on Windows.
|
||||
# Quote the JSON path because the workspace's parent directory contains a space.
|
||||
$Arguments = @(('"' + $Probe.FullName + '"'), '--walkability-audit', "--workers=$Workers")
|
||||
$Succeeded = $false
|
||||
|
||||
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
|
||||
if ($Attempt -eq 2) {
|
||||
Write-Warning "$Stem exited with 0xC0000142; retrying once after 60 seconds."
|
||||
Start-Sleep -Seconds 60
|
||||
}
|
||||
Remove-Item -LiteralPath $StdoutPath, $StderrPath -Force -ErrorAction SilentlyContinue
|
||||
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $Arguments `
|
||||
-WorkingDirectory $StandaloneRoot -WindowStyle Hidden -PassThru `
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
|
||||
$StartedUtc = [datetime]::UtcNow
|
||||
$PeakPrivateBytes = [int64]0
|
||||
try {
|
||||
while (-not $Process.HasExited) {
|
||||
Start-Sleep -Milliseconds 100
|
||||
$Process.Refresh()
|
||||
if ($Process.HasExited) { break }
|
||||
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$Process.ProcessorAffinity = [IntPtr]0xFFFF
|
||||
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes, [int64]$Process.PrivateMemorySize64)
|
||||
if (([datetime]::UtcNow - $StartedUtc).TotalSeconds -gt $TimeoutSeconds) {
|
||||
$Process.Kill()
|
||||
$Process.WaitForExit()
|
||||
throw "$Stem exceeded the $TimeoutSeconds-second per-probe time limit."
|
||||
}
|
||||
if ($PeakPrivateBytes -ge 8GB) {
|
||||
$Process.Kill()
|
||||
$Process.WaitForExit()
|
||||
throw "$Stem exceeded the 8 GiB private-memory limit."
|
||||
}
|
||||
}
|
||||
$Process.WaitForExit()
|
||||
$Process.Refresh()
|
||||
$ExitCode = [int]$Process.ExitCode
|
||||
} finally {
|
||||
if (-not $Process.HasExited) {
|
||||
$Process.Kill()
|
||||
$Process.WaitForExit()
|
||||
}
|
||||
}
|
||||
|
||||
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { continue }
|
||||
if ($ExitCode -eq -1073741502) {
|
||||
throw "$Stem repeated 0xC0000142 after its single retry."
|
||||
}
|
||||
$LogText = Get-Content -LiteralPath $StdoutPath -Raw
|
||||
$Walkability = [regex]::Match($LogText, '(?m)^\[Walkability\] PASS (\d+)/(\d+)')
|
||||
if ($LogText -match '(?m)^\[Walkability\] FAIL ' -or -not $Walkability.Success) {
|
||||
throw "$Stem failed the walkability regression gate; see $StdoutPath (exit $ExitCode)."
|
||||
}
|
||||
if ([int64]$Walkability.Groups[1].Value -ne [int64]$Walkability.Groups[2].Value -or
|
||||
[int64]$Walkability.Groups[2].Value -le 0) {
|
||||
throw "$Stem reported an incomplete or empty walkability sample set: $($Walkability.Value)."
|
||||
}
|
||||
if ($ExitCode -ne 0) {
|
||||
throw "$Stem passed route samples but the shape audit exited $ExitCode; see $StdoutPath."
|
||||
}
|
||||
$Succeeded = $true
|
||||
++$Passed
|
||||
Write-Output "PASS probe=$Stem workers=$Workers samples=$($Walkability.Groups[1].Value)/$($Walkability.Groups[2].Value) peak_private_gb=$([math]::Round($PeakPrivateBytes / 1GB, 2))"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "PASS saved_strate_probes=$Passed/$($Probes.Count) workers=$Workers"
|
||||
Reference in new issue
Block a user