S3's far cap sat up to 8.2 m above the near roof and read as floating pieces. The far render cap now projects onto the owning strate's upper boundary (density unchanged), on a global XY lattice, LOD4 16-voxel grid + LOD5-8 128-voxel. Cap agreement 0.05 cm (100 cm control detected), 2520/2520 upward rays hit roof on the owner walk. Far recipe v2; LOD0-3 hashes equal S2/S3. Tile p95 flat; cold clipmap +4.4%. Far-cliff fluting left open (coarse MC step). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2501 lines
135 KiB
C++
2501 lines
135 KiB
C++
#pragma once
|
|
|
|
#include "Json.h"
|
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
|
|
#include <algorithm>
|
|
#include <bit>
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
|
|
namespace VoxelForgeCore
|
|
{
|
|
namespace
|
|
{
|
|
thread_local const ScopedWorldAssemblyFeatureQueriesV1* ActiveAssemblyQueries = nullptr;
|
|
}
|
|
|
|
ScopedWorldAssemblyFeatureQueriesV1::ScopedWorldAssemblyFeatureQueriesV1(
|
|
std::span<const WorldAssemblyFeatureQueryV1> queries, bool forceCandidatesForValidation) noexcept
|
|
: queries_(queries), forceCandidates_(forceCandidatesForValidation), previous_(ActiveAssemblyQueries)
|
|
{
|
|
// Resolve cost hints once per domain, never rediscover them in the sample loop.
|
|
// Resoudre les indications de cout une fois par domaine, hors de la boucle d'echantillons.
|
|
ActiveAssemblyQueries = nullptr;
|
|
for (const auto& query : queries_)
|
|
if (forceCandidates_ || query.features.candidateEvaluationUseful)
|
|
{ ActiveAssemblyQueries = this; break; }
|
|
}
|
|
|
|
ScopedWorldAssemblyFeatureQueriesV1::~ScopedWorldAssemblyFeatureQueriesV1()
|
|
{ ActiveAssemblyQueries = previous_; }
|
|
|
|
const WorldAssemblyFeatureTileListV1* ScopedWorldAssemblyFeatureQueriesV1::find(
|
|
const void* recipe, const void* plan, std::int64_t x, std::int64_t y,
|
|
std::int64_t z, bool columnOnly) noexcept
|
|
{
|
|
const std::array<std::int64_t, 3> point{x,y,z};
|
|
if (ActiveAssemblyQueries)
|
|
for (const auto& query : ActiveAssemblyQueries->queries_)
|
|
{
|
|
if (query.recipe != recipe || query.plan != plan
|
|
|| (!ActiveAssemblyQueries->forceCandidates_ && !query.features.candidateEvaluationUseful)) continue;
|
|
bool inside = true;
|
|
for (std::size_t axis=0; axis<(columnOnly ? 2u : 3u); ++axis)
|
|
inside = inside && point[axis]>=query.features.minimumSubvoxels[axis]
|
|
&& point[axis]<=query.features.maximumSubvoxels[axis];
|
|
if (inside) return &query.features;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
|
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
|
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
|
const std::array<std::int64_t, 3>& tileMaximumSubvoxels)
|
|
{
|
|
if (features.size() > std::numeric_limits<std::uint32_t>::max())
|
|
throw std::length_error("world-assembly feature list exceeds 32-bit indexing");
|
|
|
|
const auto allFeatures = [&]()
|
|
{
|
|
std::vector<std::uint32_t> result;
|
|
result.reserve(features.size());
|
|
for (std::size_t index = 0; index < features.size(); ++index)
|
|
result.push_back(static_cast<std::uint32_t>(index));
|
|
return result;
|
|
};
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (tileMinimumSubvoxels[axis] > tileMaximumSubvoxels[axis]) return allFeatures();
|
|
for (const auto& feature : features)
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (feature.minimumSubvoxels[axis] > feature.maximumSubvoxels[axis]) return allFeatures();
|
|
|
|
std::vector<std::uint32_t> result;
|
|
result.reserve(features.size());
|
|
for (std::size_t index = 0; index < features.size(); ++index)
|
|
{
|
|
const auto& feature = features[index];
|
|
bool intersects = true;
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
intersects = intersects
|
|
&& feature.minimumSubvoxels[axis] <= tileMaximumSubvoxels[axis]
|
|
&& feature.maximumSubvoxels[axis] >= tileMinimumSubvoxels[axis];
|
|
if (intersects) result.push_back(static_cast<std::uint32_t>(index));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1(
|
|
std::span<const WorldAssemblyFeatureBoundsV1> bounds, std::int64_t x, std::int64_t y)
|
|
{
|
|
WorldAssemblyFeatureColumnRangeV1 result;
|
|
result.known=true;
|
|
result.empty=true;
|
|
for(const auto& bound:bounds)
|
|
{
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
if(bound.minimumSubvoxels[axis]>bound.maximumSubvoxels[axis]) return {};
|
|
if(x<bound.minimumSubvoxels[0]||x>bound.maximumSubvoxels[0]
|
|
||y<bound.minimumSubvoxels[1]||y>bound.maximumSubvoxels[1])continue;
|
|
if(result.empty)
|
|
{
|
|
result.minimumZSubvoxels=bound.minimumSubvoxels[2];
|
|
result.maximumZSubvoxels=bound.maximumSubvoxels[2];
|
|
result.empty=false;
|
|
}
|
|
else
|
|
{
|
|
result.minimumZSubvoxels=std::min(result.minimumZSubvoxels,bound.minimumSubvoxels[2]);
|
|
result.maximumZSubvoxels=std::max(result.maximumZSubvoxels,bound.maximumSubvoxels[2]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
namespace world_assembly_detail
|
|
{
|
|
// Preserve recipe order without allocating or looking up metadata in the point loop.
|
|
// Garder l'ordre de recette sans allocation ni recherche de metadonnees par point.
|
|
struct FeatureRange
|
|
{
|
|
const std::vector<WorldAssemblyFeatureV1>& features;
|
|
const WorldAssemblyFeatureTileListV1* list;
|
|
struct Iterator
|
|
{
|
|
const FeatureRange* range;
|
|
std::size_t index;
|
|
const WorldAssemblyFeatureV1& operator*() const
|
|
{ return range->features[range->list ? range->list->featureIndices[index] : index]; }
|
|
Iterator& operator++() { ++index; return *this; }
|
|
bool operator!=(const Iterator& other) const { return index!=other.index; }
|
|
};
|
|
Iterator begin() const { return {this,0}; }
|
|
Iterator end() const { return {this,list ? list->featureIndices.size() : features.size()}; }
|
|
};
|
|
|
|
constexpr std::int64_t Q16 = 1ll << 16;
|
|
constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
|
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 == 0
|
|
|| 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, 31> values = {biome.baseHeightSubvoxels,
|
|
biome.continentAmplitudeSubvoxels, biome.continentWavelengthSubvoxels,
|
|
biome.mountainAmplitudeSubvoxels, biome.mountainWavelengthSubvoxels,
|
|
biome.detailAmplitudeSubvoxels, biome.detailWavelengthSubvoxels,
|
|
biome.reliefMinimumQ16, biome.reliefMaximumQ16, biome.moistureMinimumQ16,
|
|
biome.moistureMaximumQ16, biome.reliefStrengthQ16, biome.reliefContrastQ16,
|
|
biome.reliefWavelengthSubvoxels, biome.heightWarpStrengthSubvoxels,
|
|
biome.heightWarpWavelengthSubvoxels, biome.beachWidthSubvoxels,
|
|
biome.cliffStrengthQ16, biome.cliffSlopeThresholdQ16, biome.cliffSharpnessQ16,
|
|
biome.cliffSampleDistanceSubvoxels, biome.cliffMaximumDisplacementSubvoxels,
|
|
biome.capHeightSubvoxels, biome.capWarpStrengthSubvoxels,
|
|
biome.capWarpWavelengthSubvoxels, biome.capUndulationSubvoxels,
|
|
biome.capWavelengthSubvoxels, biome.capDetailAmplitudeSubvoxels,
|
|
biome.capDetailWavelengthSubvoxels, biome.capRidgeAmplitudeSubvoxels,
|
|
biome.capRidgeWavelengthSubvoxels};
|
|
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
|
|
|| biome.reliefMinimumQ16 < 0 || biome.reliefMaximumQ16 > Q16
|
|
|| biome.reliefMinimumQ16 > biome.reliefMaximumQ16
|
|
|| biome.moistureMinimumQ16 < 0 || biome.moistureMaximumQ16 > Q16
|
|
|| biome.moistureMinimumQ16 > biome.moistureMaximumQ16
|
|
|| biome.reliefStrengthQ16 < 0 || biome.reliefStrengthQ16 > Q16
|
|
|| biome.reliefContrastQ16 < 0 || biome.reliefWavelengthSubvoxels <= 0
|
|
|| biome.heightWarpStrengthSubvoxels < 0 || biome.heightWarpWavelengthSubvoxels <= 0
|
|
|| biome.beachWidthSubvoxels < 0 || biome.cliffStrengthQ16 < 0
|
|
|| biome.cliffSlopeThresholdQ16 <= 0 || biome.cliffSharpnessQ16 < 0
|
|
|| biome.cliffSampleDistanceSubvoxels <= 0
|
|
|| biome.cliffMaximumDisplacementSubvoxels < 0)
|
|
{ error = "world assembly biome profile has an invalid fixed-scale value"; return false; }
|
|
if (biome.capWarpStrengthSubvoxels < 0 || biome.capWarpWavelengthSubvoxels <= 0
|
|
|| biome.capUndulationSubvoxels < 0 || biome.capWavelengthSubvoxels <= 0
|
|
|| biome.capDetailAmplitudeSubvoxels < 0 || biome.capDetailWavelengthSubvoxels <= 0
|
|
|| biome.capRidgeAmplitudeSubvoxels < 0 || biome.capRidgeWavelengthSubvoxels <= 0)
|
|
{ error = "world assembly biome cap profile has an invalid fixed-scale value"; return false; }
|
|
}
|
|
if (recipe.evaluatorVersion >= 2
|
|
&& (recipe.biomeClimateReliefWavelengthSubvoxels <= 0
|
|
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
|
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
|
{ error = "world assembly climate parameters are outside the supported fixed-scale range"; 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)
|
|
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
|
|| !ValidScalar(stratum.capDetailWavelengthSubvoxels)
|
|
|| !ValidScalar(stratum.capRidgeAmplitudeSubvoxels)
|
|
|| !ValidScalar(stratum.capRidgeWavelengthSubvoxels)
|
|
|| !ValidScalar(stratum.waterSurfaceSubvoxels)
|
|
|| stratum.capDetailAmplitudeSubvoxels < 0 || stratum.capDetailWavelengthSubvoxels <= 0
|
|
|| stratum.capRidgeAmplitudeSubvoxels < 0 || stratum.capRidgeWavelengthSubvoxels <= 0)
|
|
{ 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 > 4
|
|
|| 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; }
|
|
if (feature.type == 2 || feature.type == 4)
|
|
{
|
|
const std::int64_t stratumMinZ = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
|
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t stratumMaxZ = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
|
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
|
constexpr std::int64_t maximumExceptionExtent = 1024
|
|
* WorldAssemblySubvoxelsPerVoxel;
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (feature.maximumSubvoxels[axis] <= feature.minimumSubvoxels[axis]
|
|
|| feature.maximumSubvoxels[axis] - feature.minimumSubvoxels[axis]
|
|
> maximumExceptionExtent)
|
|
{ error = "world assembly overhang exception is not a bounded positive-volume box"; return false; }
|
|
if (feature.minimumSubvoxels[2] < stratumMinZ
|
|
|| feature.maximumSubvoxels[2] > stratumMaxZ)
|
|
{ error = "world assembly overhang exception escapes its finite stratum Z bounds"; return false; }
|
|
if (feature.type == 4
|
|
&& (std::abs(feature.parametersSubvoxels[0])
|
|
+ std::abs(feature.parametersSubvoxels[1]) != 256
|
|
|| feature.parametersSubvoxels[2] <= 0
|
|
|| feature.parametersSubvoxels[3] <= 0))
|
|
{ error = "world assembly cliff shelf requires a unit horizontal axis and positive height/reach"; 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; }
|
|
}
|
|
std::vector<std::uint64_t> routeIds;
|
|
routeIds.reserve(stratum.route.size());
|
|
for (const auto& point : stratum.route)
|
|
{
|
|
if (std::find(routeIds.begin(), routeIds.end(), point.stableFeatureId) != routeIds.end())
|
|
{ error = "world assembly route point IDs must be unique"; return false; }
|
|
routeIds.push_back(point.stableFeatureId);
|
|
}
|
|
for (std::size_t i = 1; i < stratum.route.size(); ++i)
|
|
{
|
|
std::uint64_t squared = 0;
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
const std::int64_t delta = stratum.route[i].positionSubvoxels[axis]
|
|
- stratum.route[i - 1].positionSubvoxels[axis];
|
|
if (std::abs(delta) > 4096 * WorldAssemblySubvoxelsPerVoxel)
|
|
{ error = "world assembly route segment exceeds its bounded sweep length"; return false; }
|
|
squared += static_cast<std::uint64_t>(delta * delta);
|
|
}
|
|
if (squared == 0)
|
|
{ error = "world assembly route contains a zero-length segment"; return false; }
|
|
}
|
|
const auto& constraints = stratum.routeConstraints;
|
|
if (constraints.version == 0)
|
|
{
|
|
if (constraints.capsuleRadiusSubvoxels != 0 || constraints.capsuleHeightSubvoxels != 0
|
|
|| constraints.maximumGradeQ16 != 0 || constraints.sweepSpacingSubvoxels != 0
|
|
|| constraints.supportProbeRadiusSubvoxels != 0
|
|
|| constraints.supportSearchDepthSubvoxels != 0
|
|
|| constraints.minimumClearanceSubvoxels != 0 || constraints.minimumSupportProbes != 0)
|
|
{ error = "world assembly route constraints need a supported version"; return false; }
|
|
}
|
|
else if (constraints.version != 1
|
|
|| !ValidScalar(constraints.capsuleRadiusSubvoxels)
|
|
|| !ValidScalar(constraints.capsuleHeightSubvoxels)
|
|
|| !ValidScalar(constraints.maximumGradeQ16)
|
|
|| !ValidScalar(constraints.sweepSpacingSubvoxels)
|
|
|| !ValidScalar(constraints.supportProbeRadiusSubvoxels)
|
|
|| !ValidScalar(constraints.supportSearchDepthSubvoxels)
|
|
|| !ValidScalar(constraints.minimumClearanceSubvoxels)
|
|
|| constraints.capsuleRadiusSubvoxels <= 0
|
|
|| constraints.capsuleHeightSubvoxels < 2 * constraints.capsuleRadiusSubvoxels
|
|
|| constraints.maximumGradeQ16 <= 0 || constraints.maximumGradeQ16 > Q16
|
|
|| constraints.sweepSpacingSubvoxels <= 0
|
|
|| constraints.sweepSpacingSubvoxels > WorldAssemblySubvoxelsPerVoxel / 2
|
|
|| constraints.supportProbeRadiusSubvoxels <= 0
|
|
|| constraints.supportSearchDepthSubvoxels <= 0
|
|
|| constraints.minimumClearanceSubvoxels < 0
|
|
|| constraints.minimumSupportProbes == 0 || constraints.minimumSupportProbes > 5)
|
|
{ error = "world assembly route support constraints are outside the supported range"; return false; }
|
|
if (constraints.version == 1)
|
|
for (const auto& point : stratum.route)
|
|
if (point.radiusSubvoxels < constraints.capsuleRadiusSubvoxels)
|
|
{ error = "world assembly route corridor is narrower than its player capsule"; 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);
|
|
if (recipe.evaluatorVersion >= 2)
|
|
{
|
|
HashWord(hash, 0x535552465632ull, 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateReliefWavelengthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateMoistureWavelengthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateReliefContrastQ16), 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);
|
|
if (recipe.evaluatorVersion >= 2)
|
|
{
|
|
for (const auto value : {biome.reliefMinimumQ16, biome.reliefMaximumQ16,
|
|
biome.moistureMinimumQ16, biome.moistureMaximumQ16, biome.reliefStrengthQ16,
|
|
biome.reliefContrastQ16, biome.reliefWavelengthSubvoxels,
|
|
biome.heightWarpStrengthSubvoxels, biome.heightWarpWavelengthSubvoxels,
|
|
biome.beachWidthSubvoxels, biome.cliffStrengthQ16,
|
|
biome.cliffSlopeThresholdQ16, biome.cliffSharpnessQ16,
|
|
biome.cliffSampleDistanceSubvoxels, biome.cliffMaximumDisplacementSubvoxels})
|
|
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
HashWord(hash, biome.hasCapProfile ? 1u : 0u, 1);
|
|
for (const auto value : {biome.capHeightSubvoxels,
|
|
biome.capWarpStrengthSubvoxels, biome.capWarpWavelengthSubvoxels,
|
|
biome.capUndulationSubvoxels, biome.capWavelengthSubvoxels,
|
|
biome.capDetailAmplitudeSubvoxels, biome.capDetailWavelengthSubvoxels,
|
|
biome.capRidgeAmplitudeSubvoxels, biome.capRidgeWavelengthSubvoxels})
|
|
HashWord(hash, static_cast<std::uint64_t>(value), 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);
|
|
if (stratum.hasRecipeOverride)
|
|
{
|
|
HashWord(hash, 0x5354524154455250ull, 8);
|
|
HashWord(hash, HashRecipe(stratum.recipeOverride), 8);
|
|
}
|
|
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);
|
|
const bool hasExtendedCap = stratum.capDetailAmplitudeSubvoxels != 0
|
|
|| stratum.capRidgeAmplitudeSubvoxels != 0 || stratum.waterEnabled
|
|
|| stratum.waterSurfaceSubvoxels != 0;
|
|
if (hasExtendedCap)
|
|
{
|
|
HashWord(hash, 0x4341505632ull, 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(stratum.capDetailAmplitudeSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(stratum.capDetailWavelengthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(stratum.capRidgeAmplitudeSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(stratum.capRidgeWavelengthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(stratum.waterSurfaceSubvoxels), 8);
|
|
HashWord(hash, stratum.waterEnabled ? 1u : 0u, 1);
|
|
}
|
|
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);
|
|
}
|
|
if (stratum.routeConstraints.version != 0)
|
|
{
|
|
const auto& constraints = stratum.routeConstraints;
|
|
HashWord(hash, 0x524f5554455631ull, 8);
|
|
HashWord(hash, constraints.version, 4);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.capsuleRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.capsuleHeightSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.maximumGradeQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.sweepSpacingSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.supportProbeRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.supportSearchDepthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(constraints.minimumClearanceSubvoxels), 8);
|
|
HashWord(hash, constraints.minimumSupportProbes, 4);
|
|
}
|
|
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);
|
|
// 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)
|
|
{
|
|
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; }
|
|
if (recipe.evaluatorVersion >= 2
|
|
&& (!ReadInteger(value.Get("climate_relief_wavelength_q"), recipe.biomeClimateReliefWavelengthSubvoxels)
|
|
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
|
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
|
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; 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; }
|
|
if (recipe.evaluatorVersion >= 2
|
|
&& (!ReadInteger(item.Get("relief_min_q16"), biome.reliefMinimumQ16)
|
|
|| !ReadInteger(item.Get("relief_max_q16"), biome.reliefMaximumQ16)
|
|
|| !ReadInteger(item.Get("moisture_min_q16"), biome.moistureMinimumQ16)
|
|
|| !ReadInteger(item.Get("moisture_max_q16"), biome.moistureMaximumQ16)
|
|
|| !ReadInteger(item.Get("relief_strength_q16"), biome.reliefStrengthQ16)
|
|
|| !ReadInteger(item.Get("relief_contrast_q16"), biome.reliefContrastQ16)
|
|
|| !ReadInteger(item.Get("relief_wavelength_q"), biome.reliefWavelengthSubvoxels)
|
|
|| !ReadInteger(item.Get("height_warp_strength_q"), biome.heightWarpStrengthSubvoxels)
|
|
|| !ReadInteger(item.Get("height_warp_wavelength_q"), biome.heightWarpWavelengthSubvoxels)
|
|
|| !ReadInteger(item.Get("beach_width_q"), biome.beachWidthSubvoxels)
|
|
|| !ReadInteger(item.Get("cliff_strength_q16"), biome.cliffStrengthQ16)
|
|
|| !ReadInteger(item.Get("cliff_slope_threshold_q16"), biome.cliffSlopeThresholdQ16)
|
|
|| !ReadInteger(item.Get("cliff_sharpness_q16"), biome.cliffSharpnessQ16)
|
|
|| !ReadInteger(item.Get("cliff_sample_distance_q"), biome.cliffSampleDistanceSubvoxels)
|
|
|| !ReadInteger(item.Get("cliff_max_displacement_q"), biome.cliffMaximumDisplacementSubvoxels)
|
|
|| !std::holds_alternative<bool>(item.Get("cap_enabled").Data)
|
|
|| !ReadInteger(item.Get("cap_height_q"), biome.capHeightSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_warp_strength_q"), biome.capWarpStrengthSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_warp_wavelength_q"), biome.capWarpWavelengthSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_undulation_q"), biome.capUndulationSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_wavelength_q"), biome.capWavelengthSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_detail_amplitude_q"), biome.capDetailAmplitudeSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_detail_wavelength_q"), biome.capDetailWavelengthSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_ridge_amplitude_q"), biome.capRidgeAmplitudeSubvoxels)
|
|
|| !ReadInteger(item.Get("cap_ridge_wavelength_q"), biome.capRidgeWavelengthSubvoxels)))
|
|
{ error = "world assembly biome profile is missing evaluator-2 surface parameters"; return false; }
|
|
if (recipe.evaluatorVersion >= 2)
|
|
biome.hasCapProfile = std::get<bool>(item.Get("cap_enabled").Data);
|
|
recipe.biomes.push_back(biome);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
inline std::string RecipeJson(const WorldAssemblyRecipeV1& recipe)
|
|
{
|
|
std::ostringstream out;
|
|
out << "{\"schema_version\":" << recipe.schemaVersion
|
|
<< ",\"evaluator_version\":" << recipe.evaluatorVersion
|
|
<< ",\"units_per_voxel\":" << recipe.unitsPerVoxel
|
|
<< ",\"seed\":" << recipe.seed
|
|
<< ",\"biome_cell_size_q\":" << recipe.biomeCellSizeSubvoxels
|
|
<< ",\"biome_warp_q\":" << recipe.biomeWarpSubvoxels
|
|
<< ",\"biome_warp_wavelength_q\":" << recipe.biomeWarpWavelengthSubvoxels
|
|
<< ",\"biome_border_blend_q\":" << recipe.biomeBorderBlendSubvoxels;
|
|
if (recipe.evaluatorVersion >= 2)
|
|
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
|
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
|
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
|
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
|
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
|
{
|
|
const auto& biome = 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;
|
|
if (recipe.evaluatorVersion >= 2)
|
|
out << ",\"relief_min_q16\":" << biome.reliefMinimumQ16
|
|
<< ",\"relief_max_q16\":" << biome.reliefMaximumQ16
|
|
<< ",\"moisture_min_q16\":" << biome.moistureMinimumQ16
|
|
<< ",\"moisture_max_q16\":" << biome.moistureMaximumQ16
|
|
<< ",\"relief_strength_q16\":" << biome.reliefStrengthQ16
|
|
<< ",\"relief_contrast_q16\":" << biome.reliefContrastQ16
|
|
<< ",\"relief_wavelength_q\":" << biome.reliefWavelengthSubvoxels
|
|
<< ",\"height_warp_strength_q\":" << biome.heightWarpStrengthSubvoxels
|
|
<< ",\"height_warp_wavelength_q\":" << biome.heightWarpWavelengthSubvoxels
|
|
<< ",\"beach_width_q\":" << biome.beachWidthSubvoxels
|
|
<< ",\"cliff_strength_q16\":" << biome.cliffStrengthQ16
|
|
<< ",\"cliff_slope_threshold_q16\":" << biome.cliffSlopeThresholdQ16
|
|
<< ",\"cliff_sharpness_q16\":" << biome.cliffSharpnessQ16
|
|
<< ",\"cliff_sample_distance_q\":" << biome.cliffSampleDistanceSubvoxels
|
|
<< ",\"cliff_max_displacement_q\":" << biome.cliffMaximumDisplacementSubvoxels
|
|
<< ",\"cap_enabled\":" << (biome.hasCapProfile ? "true" : "false")
|
|
<< ",\"cap_height_q\":" << biome.capHeightSubvoxels
|
|
<< ",\"cap_warp_strength_q\":" << biome.capWarpStrengthSubvoxels
|
|
<< ",\"cap_warp_wavelength_q\":" << biome.capWarpWavelengthSubvoxels
|
|
<< ",\"cap_undulation_q\":" << biome.capUndulationSubvoxels
|
|
<< ",\"cap_wavelength_q\":" << biome.capWavelengthSubvoxels
|
|
<< ",\"cap_detail_amplitude_q\":" << biome.capDetailAmplitudeSubvoxels
|
|
<< ",\"cap_detail_wavelength_q\":" << biome.capDetailWavelengthSubvoxels
|
|
<< ",\"cap_ridge_amplitude_q\":" << biome.capRidgeAmplitudeSubvoxels
|
|
<< ",\"cap_ridge_wavelength_q\":" << biome.capRidgeWavelengthSubvoxels;
|
|
out << '}';
|
|
}
|
|
out << "]}";
|
|
return out.str();
|
|
}
|
|
|
|
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; }
|
|
const VFJson::Value& recipeOverrideValue = item.Get("recipe_override");
|
|
if (!std::holds_alternative<std::nullptr_t>(recipeOverrideValue.Data))
|
|
{
|
|
if (!ParseRecipe(recipeOverrideValue, stratum.recipeOverride, error)) return false;
|
|
stratum.hasRecipeOverride = true;
|
|
}
|
|
const auto readOptional = [&item](const char* name, std::int64_t& output)
|
|
{
|
|
const VFJson::Value& field = item.Get(name);
|
|
if (std::holds_alternative<std::nullptr_t>(field.Data)) return true;
|
|
return field.IsNumber() && ReadInteger(field, output);
|
|
};
|
|
const VFJson::Value& waterEnabled = item.Get("water_enabled");
|
|
const bool hasWaterBoolean=std::holds_alternative<bool>(waterEnabled.Data);
|
|
const bool hasWaterNumber=waterEnabled.IsNumber();
|
|
const bool hasWaterNull=std::holds_alternative<std::nullptr_t>(waterEnabled.Data);
|
|
if (!readOptional("cap_detail_amplitude_q", stratum.capDetailAmplitudeSubvoxels)
|
|
|| !readOptional("cap_detail_wavelength_q", stratum.capDetailWavelengthSubvoxels)
|
|
|| !readOptional("cap_ridge_amplitude_q", stratum.capRidgeAmplitudeSubvoxels)
|
|
|| !readOptional("cap_ridge_wavelength_q", stratum.capRidgeWavelengthSubvoxels)
|
|
|| !readOptional("water_surface_q", stratum.waterSurfaceSubvoxels)
|
|
|| (!hasWaterBoolean&&!hasWaterNumber&&!hasWaterNull))
|
|
{ error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; }
|
|
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
|
if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0;
|
|
const VFJson::Value& routeConstraintsValue = item.Get("route_constraints");
|
|
if (!std::holds_alternative<std::nullptr_t>(routeConstraintsValue.Data))
|
|
{
|
|
auto& constraints = stratum.routeConstraints;
|
|
std::uint32_t minimumSupportProbes = 0;
|
|
if (!routeConstraintsValue.IsObject()
|
|
|| !ReadU32(routeConstraintsValue.Get("version"), constraints.version)
|
|
|| !ReadInteger(routeConstraintsValue.Get("capsule_radius_q"), constraints.capsuleRadiusSubvoxels)
|
|
|| !ReadInteger(routeConstraintsValue.Get("capsule_height_q"), constraints.capsuleHeightSubvoxels)
|
|
|| !ReadInteger(routeConstraintsValue.Get("maximum_grade_q16"), constraints.maximumGradeQ16)
|
|
|| !ReadInteger(routeConstraintsValue.Get("sweep_spacing_q"), constraints.sweepSpacingSubvoxels)
|
|
|| !ReadInteger(routeConstraintsValue.Get("support_probe_radius_q"), constraints.supportProbeRadiusSubvoxels)
|
|
|| !ReadInteger(routeConstraintsValue.Get("support_search_depth_q"), constraints.supportSearchDepthSubvoxels)
|
|
|| !ReadInteger(routeConstraintsValue.Get("minimum_clearance_q"), constraints.minimumClearanceSubvoxels)
|
|
|| !ReadU32(routeConstraintsValue.Get("minimum_support_probes"), minimumSupportProbes))
|
|
{ error = "world assembly route constraints are missing a fixed-scale field"; return false; }
|
|
constraints.minimumSupportProbes = minimumSupportProbes;
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
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 std::uint32_t Mix32(std::uint32_t value)
|
|
{
|
|
value ^= value >> 16; value *= 0x45d9f3bu;
|
|
value ^= value >> 16; value *= 0x45d9f3bu;
|
|
value ^= value >> 16;
|
|
return value;
|
|
}
|
|
|
|
inline std::int64_t SeedOffsetQ16(std::uint32_t seed, std::uint32_t siteHundredths)
|
|
{
|
|
const std::uint32_t site = siteHundredths * 2654435761u;
|
|
return static_cast<std::int64_t>(Mix32(seed ^ site) & 0x3fffu) * Q16;
|
|
}
|
|
|
|
inline std::uint32_t HashCorner(std::int64_t x, std::int64_t y, std::int64_t z)
|
|
{
|
|
std::uint32_t hash = static_cast<std::uint32_t>(x) * 0x9E3779B1u
|
|
^ static_cast<std::uint32_t>(y) * 0x85EBCA77u
|
|
^ static_cast<std::uint32_t>(z) * 0xC2B2AE3Du;
|
|
hash ^= hash >> 15; hash *= 0x2C1B3C6Du;
|
|
hash ^= hash >> 12; hash *= 0x297A2D39u;
|
|
hash ^= hash >> 15;
|
|
return hash;
|
|
}
|
|
|
|
inline std::int32_t FadeQ16(std::int32_t value)
|
|
{
|
|
const std::int64_t t2 = (static_cast<std::int64_t>(value) * value) / Q16;
|
|
const std::int64_t t3 = (t2 * value) / Q16;
|
|
const std::int64_t t4 = (t3 * value) / Q16;
|
|
const std::int64_t t5 = (t4 * value) / Q16;
|
|
return static_cast<std::int32_t>(6 * t5 - 15 * t4 + 10 * t3);
|
|
}
|
|
|
|
inline std::int32_t GradientDotQ16(std::uint32_t hash, std::int32_t x,
|
|
std::int32_t y, std::int32_t z)
|
|
{
|
|
const std::uint32_t h = hash & 15u;
|
|
const std::int32_t u = (h & 8u) == 0u ? x : y;
|
|
const std::int32_t v = (h & 12u) == 0u ? y : ((h & 13u) == 12u ? x : z);
|
|
return ((h & 1u) == 0u ? u : -u) + ((h & 2u) == 0u ? v : -v);
|
|
}
|
|
|
|
inline std::int32_t Perlin3DQ16(std::int64_t xQ16, std::int64_t yQ16,
|
|
std::int64_t zQ16)
|
|
{
|
|
const std::int64_t cellX = FloorDiv(xQ16, Q16);
|
|
const std::int64_t cellY = FloorDiv(yQ16, Q16);
|
|
const std::int64_t cellZ = FloorDiv(zQ16, Q16);
|
|
const std::int32_t fx = static_cast<std::int32_t>(FloorMod(xQ16, Q16));
|
|
const std::int32_t fy = static_cast<std::int32_t>(FloorMod(yQ16, Q16));
|
|
const std::int32_t fz = static_cast<std::int32_t>(FloorMod(zQ16, Q16));
|
|
const std::int32_t sx = FadeQ16(fx), sy = FadeQ16(fy), sz = FadeQ16(fz);
|
|
const std::int32_t fx1 = fx - static_cast<std::int32_t>(Q16);
|
|
const std::int32_t fy1 = fy - static_cast<std::int32_t>(Q16);
|
|
const std::int32_t fz1 = fz - static_cast<std::int32_t>(Q16);
|
|
const auto n = [&](std::int64_t dx, std::int64_t dy, std::int64_t dz,
|
|
std::int32_t px, std::int32_t py, std::int32_t pz)
|
|
{ return GradientDotQ16(HashCorner(cellX + dx, cellY + dy, cellZ + dz), px, py, pz); };
|
|
const std::int32_t n000=n(0,0,0,fx,fy,fz), n100=n(1,0,0,fx1,fy,fz);
|
|
const std::int32_t n010=n(0,1,0,fx,fy1,fz), n110=n(1,1,0,fx1,fy1,fz);
|
|
const std::int32_t n001=n(0,0,1,fx,fy,fz1), n101=n(1,0,1,fx1,fy,fz1);
|
|
const std::int32_t n011=n(0,1,1,fx,fy1,fz1), n111=n(1,1,1,fx1,fy1,fz1);
|
|
const std::int32_t x00=LerpQ16(n000,n100,static_cast<std::uint32_t>(sx));
|
|
const std::int32_t x10=LerpQ16(n010,n110,static_cast<std::uint32_t>(sx));
|
|
const std::int32_t x01=LerpQ16(n001,n101,static_cast<std::uint32_t>(sx));
|
|
const std::int32_t x11=LerpQ16(n011,n111,static_cast<std::uint32_t>(sx));
|
|
const std::int32_t y0=LerpQ16(x00,x10,static_cast<std::uint32_t>(sy));
|
|
const std::int32_t y1=LerpQ16(x01,x11,static_cast<std::uint32_t>(sy));
|
|
return LerpQ16(y0,y1,static_cast<std::uint32_t>(sz));
|
|
}
|
|
|
|
inline std::int64_t NoiseCoordinateQ16(std::int64_t positionSubvoxels,
|
|
std::int64_t wavelengthSubvoxels)
|
|
{ return FloorDiv(positionSubvoxels * Q16, wavelengthSubvoxels); }
|
|
|
|
inline std::int32_t FractalNoiseQ16(std::int64_t xQ16, std::int64_t yQ16,
|
|
std::int64_t zQ16, std::uint32_t octaves, bool ridged = false)
|
|
{
|
|
std::int64_t total = 0, weightSum = 0, weight = Q16, ridgeWeight = Q16;
|
|
for (std::uint32_t octave = 0; octave < octaves; ++octave)
|
|
{
|
|
const std::int64_t scale = 1ll << octave;
|
|
const std::int64_t noise = Perlin3DQ16(xQ16 * scale,yQ16 * scale,zQ16 * scale);
|
|
std::int64_t value = noise;
|
|
if (ridged)
|
|
{
|
|
const std::int64_t scaled = noise * 5 / 4;
|
|
const std::int64_t folded = Q16 - std::abs(scaled);
|
|
value = MulQ16(MulQ16(folded,folded),ridgeWeight);
|
|
ridgeWeight = std::clamp<std::int64_t>(value * 2,0,Q16);
|
|
}
|
|
total += MulQ16(value,weight);
|
|
weightSum += weight;
|
|
weight /= 2;
|
|
}
|
|
if (weightSum == 0) return 0;
|
|
const std::int64_t average = (total * Q16) / weightSum;
|
|
return static_cast<std::int32_t>(ridged ? average * 2 - Q16 : average);
|
|
}
|
|
|
|
inline std::int32_t ReliefQ16(std::uint32_t seed, std::int64_t x,
|
|
std::int64_t y, std::int64_t wavelength, std::int64_t contrastQ16)
|
|
{
|
|
const std::int32_t noise = FractalNoiseQ16(
|
|
NoiseCoordinateQ16(x,wavelength)+SeedOffsetQ16(seed,730),
|
|
NoiseCoordinateQ16(y,wavelength)+SeedOffsetQ16(seed,210),
|
|
SeedOffsetQ16(seed,50),2);
|
|
const std::int64_t raw = Q16/2 + noise/2;
|
|
const std::int64_t contrasted = Q16/2 + MulQ16(raw-Q16/2,contrastQ16);
|
|
return static_cast<std::int32_t>(SmoothQ16(static_cast<std::uint32_t>(
|
|
std::clamp<std::int64_t>(contrasted,0,Q16))));
|
|
}
|
|
|
|
inline std::int32_t MoistureQ16(std::uint32_t seed, std::int64_t x,
|
|
std::int64_t y, std::int64_t wavelength)
|
|
{
|
|
const std::int32_t noise = FractalNoiseQ16(
|
|
NoiseCoordinateQ16(x,wavelength)+SeedOffsetQ16(seed,470),
|
|
NoiseCoordinateQ16(y,wavelength)+SeedOffsetQ16(seed,890),
|
|
SeedOffsetQ16(seed,130),2);
|
|
return static_cast<std::int32_t>(std::clamp<std::int64_t>(Q16/2+noise/2,0,Q16));
|
|
}
|
|
|
|
inline std::int64_t WarpDisplacement(std::int32_t noiseQ16,
|
|
std::int64_t amplitudeSubvoxels)
|
|
{ return (static_cast<std::int64_t>(noiseQ16)*amplitudeSubvoxels*5)/(4*Q16); }
|
|
|
|
inline std::int64_t StructuralHeightV2(const WorldAssemblyRecipeV1& recipe,
|
|
const WorldAssemblyBiomeProfileV1& biome, std::int64_t x, std::int64_t y,
|
|
std::int32_t* outRelief = nullptr)
|
|
{
|
|
const std::uint32_t seed=recipe.seed;
|
|
std::int64_t qx=x,qy=y;
|
|
if(biome.heightWarpStrengthSubvoxels>0)
|
|
{
|
|
const std::int64_t xw=NoiseCoordinateQ16(x,biome.heightWarpWavelengthSubvoxels);
|
|
const std::int64_t yw=NoiseCoordinateQ16(y,biome.heightWarpWavelengthSubvoxels);
|
|
const std::int32_t wx=Perlin3DQ16(xw+SeedOffsetQ16(seed,31),yw+4*Q16+13*Q16/64,SeedOffsetQ16(seed,170));
|
|
const std::int32_t wy=Perlin3DQ16(xw+8*Q16+3*Q16/5,yw+SeedOffsetQ16(seed,53),SeedOffsetQ16(seed,290));
|
|
qx+=WarpDisplacement(wx,biome.heightWarpStrengthSubvoxels);
|
|
qy+=WarpDisplacement(wy,biome.heightWarpStrengthSubvoxels);
|
|
}
|
|
const std::int32_t relief=ReliefQ16(seed,x,y,biome.reliefWavelengthSubvoxels,
|
|
biome.reliefContrastQ16);
|
|
const std::int32_t mountainous=static_cast<std::int32_t>(Q16
|
|
+MulQ16(relief-Q16,biome.reliefStrengthQ16));
|
|
if(outRelief)*outRelief=mountainous;
|
|
const std::int64_t contX=NoiseCoordinateQ16(qx,biome.continentWavelengthSubvoxels)+SeedOffsetQ16(seed,310);
|
|
const std::int64_t contY=NoiseCoordinateQ16(qy,biome.continentWavelengthSubvoxels)+SeedOffsetQ16(seed,570);
|
|
const std::int64_t contZ=SeedOffsetQ16(seed,70);
|
|
const std::int32_t continent=FractalNoiseQ16(contX,contY,contZ,4);
|
|
const std::int64_t continentScale=Q16*45/100+MulQ16(mountainous,Q16*55/100);
|
|
std::int64_t height=biome.baseHeightSubvoxels
|
|
+MulQ16(MulQ16(continent,biome.continentAmplitudeSubvoxels),continentScale)/2;
|
|
if(biome.mountainAmplitudeSubvoxels>0)
|
|
{
|
|
const std::int64_t mountainX=NoiseCoordinateQ16(qx,biome.mountainWavelengthSubvoxels)+SeedOffsetQ16(seed,990);
|
|
const std::int64_t mountainY=NoiseCoordinateQ16(qy,biome.mountainWavelengthSubvoxels)+SeedOffsetQ16(seed,770);
|
|
const std::int64_t mountainZ=SeedOffsetQ16(seed,90);
|
|
const std::int32_t ridge=FractalNoiseQ16(mountainX,mountainY,mountainZ,4,true);
|
|
const std::int64_t ridgeHeight=std::clamp<std::int64_t>((ridge+Q16)/2,0,Q16);
|
|
height+=MulQ16(MulQ16(ridgeHeight,biome.mountainAmplitudeSubvoxels),mountainous);
|
|
}
|
|
const std::int64_t detailX=NoiseCoordinateQ16(x,biome.detailWavelengthSubvoxels)+11*Q16;
|
|
const std::int64_t detailY=NoiseCoordinateQ16(y,biome.detailWavelengthSubvoxels)+22*Q16;
|
|
const std::int64_t detailZ=SeedOffsetQ16(seed,130);
|
|
height+=MulQ16(FractalNoiseQ16(detailX,detailY,detailZ,3),biome.detailAmplitudeSubvoxels);
|
|
return height;
|
|
}
|
|
|
|
inline std::int64_t SurfaceHeightV2(const WorldAssemblyRecipeV1& recipe,
|
|
const WorldAssemblyStratumPlanV1& plan, const WorldAssemblyBiomeProfileV1& biome,
|
|
std::int64_t x,std::int64_t y)
|
|
{
|
|
std::int64_t terrain=StructuralHeightV2(recipe,biome,x,y);
|
|
if(biome.cliffStrengthQ16>0 && biome.cliffMaximumDisplacementSubvoxels>0)
|
|
{
|
|
const std::int64_t d=biome.cliffSampleDistanceSubvoxels;
|
|
const std::int64_t xp=StructuralHeightV2(recipe,biome,x+d,y);
|
|
const std::int64_t xm=StructuralHeightV2(recipe,biome,x-d,y);
|
|
const std::int64_t yp=StructuralHeightV2(recipe,biome,x,y+d);
|
|
const std::int64_t ym=StructuralHeightV2(recipe,biome,x,y-d);
|
|
const std::int64_t gx=((xp-xm)*Q16)/(2*d),gy=((yp-ym)*Q16)/(2*d);
|
|
const std::uint64_t square=static_cast<std::uint64_t>(gx*gx+gy*gy);
|
|
std::uint64_t remainder=square,root=0,bit=1ull<<62;
|
|
while(bit>remainder)bit>>=2;
|
|
while(bit)
|
|
{
|
|
if(remainder>=root+bit)
|
|
{remainder-=root+bit;root=(root>>1)+bit;}
|
|
else root>>=1;
|
|
bit>>=2;
|
|
}
|
|
const std::int64_t slope=static_cast<std::int64_t>(root);
|
|
const std::int64_t threshold=std::max<std::int64_t>(1,biome.cliffSlopeThresholdQ16);
|
|
const std::int64_t gate=std::clamp<std::int64_t>(
|
|
(slope-threshold)*Q16/threshold,0,Q16);
|
|
if(gate>0)
|
|
{
|
|
const std::int64_t reference=(xp+xm+yp+ym)/4;
|
|
const std::int64_t gain=MulQ16(MulQ16(biome.cliffStrengthQ16,gate),
|
|
biome.cliffSharpnessQ16);
|
|
const std::int64_t displacement=std::clamp<std::int64_t>(
|
|
MulQ16(terrain-reference,gain),-biome.cliffMaximumDisplacementSubvoxels,
|
|
biome.cliffMaximumDisplacementSubvoxels);
|
|
terrain+=displacement;
|
|
}
|
|
}
|
|
if(biome.beachWidthSubvoxels>0 && plan.waterEnabled)
|
|
{
|
|
const std::int64_t distance=std::abs(terrain-plan.waterSurfaceSubvoxels);
|
|
if(distance<biome.beachWidthSubvoxels)
|
|
{
|
|
const std::uint32_t t=static_cast<std::uint32_t>(std::clamp<std::int64_t>(
|
|
distance*Q16/biome.beachWidthSubvoxels,0,Q16));
|
|
const std::uint32_t smooth=SmoothQ16(t);
|
|
terrain=plan.waterSurfaceSubvoxels+MulQ16(terrain-plan.waterSurfaceSubvoxels,smooth);
|
|
}
|
|
}
|
|
return terrain;
|
|
}
|
|
|
|
inline std::uint64_t IntegerSqrt(std::uint64_t value)
|
|
{
|
|
std::uint64_t remainder=value,root=0,bit=1ull<<62;
|
|
while(bit>remainder)bit>>=2;
|
|
while(bit)
|
|
{
|
|
if(remainder>=root+bit){remainder-=root+bit;root=(root>>1)+bit;}
|
|
else root>>=1;
|
|
bit>>=2;
|
|
}
|
|
return root;
|
|
}
|
|
|
|
struct BiomeSelectionV2
|
|
{
|
|
std::size_t primary=0,neighbor=0;
|
|
std::uint32_t primaryWeightQ16=static_cast<std::uint32_t>(Q16);
|
|
std::int64_t warpedX=0,warpedY=0;
|
|
};
|
|
|
|
inline std::size_t ClassifyBiomeSiteV2(const WorldAssemblyRecipeV1& recipe,
|
|
std::int64_t siteX,std::int64_t siteY,std::uint32_t siteHash)
|
|
{
|
|
const std::int32_t relief=ReliefQ16(recipe.seed,siteX,siteY,
|
|
recipe.biomeClimateReliefWavelengthSubvoxels,recipe.biomeClimateReliefContrastQ16);
|
|
const std::int32_t moisture=MoistureQ16(recipe.seed,siteX,siteY,
|
|
recipe.biomeClimateMoistureWavelengthSubvoxels);
|
|
std::size_t best=0;
|
|
std::uint64_t bestScore=std::numeric_limits<std::uint64_t>::max();
|
|
for(std::size_t i=0;i<recipe.biomes.size();++i)
|
|
{
|
|
const auto& biome=recipe.biomes[i];
|
|
const std::int64_t dr=relief<biome.reliefMinimumQ16
|
|
?biome.reliefMinimumQ16-relief:(relief>biome.reliefMaximumQ16
|
|
?relief-biome.reliefMaximumQ16:0);
|
|
const std::int64_t dm=moisture<biome.moistureMinimumQ16
|
|
?biome.moistureMinimumQ16-moisture:(moisture>biome.moistureMaximumQ16
|
|
?moisture-biome.moistureMaximumQ16:0);
|
|
const std::uint32_t jitterHash=Mix32(siteHash^(0x9e3779b9u*static_cast<std::uint32_t>(i+1)));
|
|
const std::uint64_t jitter=(static_cast<std::uint64_t>(jitterHash>>8)*429497u)/0x1000000u;
|
|
const std::uint64_t score=static_cast<std::uint64_t>(dr*dr+dm*dm)+jitter;
|
|
if(score<bestScore){bestScore=score;best=i;}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
inline BiomeSelectionV2 SelectBiomeV2(const WorldAssemblyRecipeV1& recipe,
|
|
std::int64_t x,std::int64_t y)
|
|
{
|
|
BiomeSelectionV2 out;
|
|
out.warpedX=x;out.warpedY=y;
|
|
if(recipe.biomes.size()<2)return out;
|
|
const std::int64_t xq=NoiseCoordinateQ16(x,recipe.biomeWarpWavelengthSubvoxels);
|
|
const std::int64_t yq=NoiseCoordinateQ16(y,recipe.biomeWarpWavelengthSubvoxels);
|
|
const std::int32_t wx=Perlin3DQ16(xq+SeedOffsetQ16(recipe.seed,27),
|
|
yq+3*Q16+Q16/10,SeedOffsetQ16(recipe.seed,110));
|
|
const std::int32_t wy=Perlin3DQ16(xq+7*Q16+7*Q16/10,
|
|
yq+SeedOffsetQ16(recipe.seed,61),SeedOffsetQ16(recipe.seed,230));
|
|
out.warpedX+=WarpDisplacement(wx,recipe.biomeWarpSubvoxels);
|
|
out.warpedY+=WarpDisplacement(wy,recipe.biomeWarpSubvoxels);
|
|
const std::int64_t cellSize=std::max<std::int64_t>(1,recipe.biomeCellSizeSubvoxels);
|
|
const std::int64_t cellX=FloorDiv(out.warpedX,cellSize);
|
|
const std::int64_t cellY=FloorDiv(out.warpedY,cellSize);
|
|
const std::uint32_t biomeSeed=recipe.seed^0x42494f4du;
|
|
std::uint64_t nearest=std::numeric_limits<std::uint64_t>::max();
|
|
std::uint64_t second=nearest;
|
|
for(std::int64_t dy=-1;dy<=1;++dy)
|
|
for(std::int64_t dx=-1;dx<=1;++dx)
|
|
{
|
|
const std::int64_t cx=cellX+dx,cy=cellY+dy;
|
|
std::uint32_t hash=biomeSeed;
|
|
hash^=Mix32(static_cast<std::uint32_t>(cx)+0x7fffffffu);
|
|
hash^=Mix32((static_cast<std::uint32_t>(cy)+0x7fffffffu)*2654435761u);
|
|
hash=Mix32(hash);
|
|
const std::uint32_t jx=hash&0xffffffu;
|
|
const std::uint32_t jy=Mix32(hash^0x68bc21ebu)&0xffffffu;
|
|
const std::int64_t siteX=cx*cellSize+(static_cast<std::int64_t>(jx)*cellSize)/0x1000000ll;
|
|
const std::int64_t siteY=cy*cellSize+(static_cast<std::int64_t>(jy)*cellSize)/0x1000000ll;
|
|
const std::int64_t dxq=siteX-out.warpedX,dyq=siteY-out.warpedY;
|
|
const std::uint64_t distance=static_cast<std::uint64_t>(dxq*dxq+dyq*dyq);
|
|
if(distance<nearest)
|
|
{
|
|
second=nearest;out.neighbor=out.primary;nearest=distance;
|
|
out.primary=ClassifyBiomeSiteV2(recipe,siteX,siteY,hash);
|
|
}
|
|
else if(distance<second)
|
|
{
|
|
second=distance;out.neighbor=ClassifyBiomeSiteV2(recipe,siteX,siteY,hash);
|
|
}
|
|
}
|
|
if(out.primary!=out.neighbor && recipe.biomeBorderBlendSubvoxels>0
|
|
&&second!=std::numeric_limits<std::uint64_t>::max())
|
|
{
|
|
const std::int64_t d1=static_cast<std::int64_t>(IntegerSqrt(nearest));
|
|
const std::int64_t d2=static_cast<std::int64_t>(IntegerSqrt(second));
|
|
const std::int64_t t=std::clamp<std::int64_t>((d2-d1)*Q16
|
|
/std::max<std::int64_t>(recipe.biomeBorderBlendSubvoxels,1),0,Q16);
|
|
const std::uint32_t neighborWeight=static_cast<std::uint32_t>(
|
|
(Q16-SmoothQ16(static_cast<std::uint32_t>(t)))/2);
|
|
out.primaryWeightQ16=static_cast<std::uint32_t>(Q16)-neighborWeight;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
inline std::int64_t CapHeightV2(const WorldAssemblyRecipeV1& recipe,
|
|
const WorldAssemblyStratumPlanV1& plan,const WorldAssemblyBiomeProfileV1& biome,
|
|
std::int64_t x,std::int64_t y)
|
|
{
|
|
const std::int64_t base=biome.hasCapProfile?biome.capHeightSubvoxels:plan.capHeightSubvoxels;
|
|
const std::int64_t warpStrength=biome.hasCapProfile
|
|
?biome.capWarpStrengthSubvoxels:0;
|
|
const std::int64_t warpWave=biome.hasCapProfile
|
|
?biome.capWarpWavelengthSubvoxels:plan.capWavelengthSubvoxels;
|
|
const std::int64_t undulation=biome.hasCapProfile
|
|
?biome.capUndulationSubvoxels:plan.capUndulationSubvoxels;
|
|
const std::int64_t swellWave=biome.hasCapProfile
|
|
?biome.capWavelengthSubvoxels:plan.capWavelengthSubvoxels;
|
|
const std::int64_t detail=biome.hasCapProfile
|
|
?biome.capDetailAmplitudeSubvoxels:plan.capDetailAmplitudeSubvoxels;
|
|
const std::int64_t detailWave=biome.hasCapProfile
|
|
?biome.capDetailWavelengthSubvoxels:plan.capDetailWavelengthSubvoxels;
|
|
const std::int64_t ridge=biome.hasCapProfile
|
|
?biome.capRidgeAmplitudeSubvoxels:plan.capRidgeAmplitudeSubvoxels;
|
|
const std::int64_t ridgeWave=biome.hasCapProfile
|
|
?biome.capRidgeWavelengthSubvoxels:plan.capRidgeWavelengthSubvoxels;
|
|
std::int64_t qx=x,qy=y;
|
|
if(warpStrength>0)
|
|
{
|
|
const std::int64_t nx=NoiseCoordinateQ16(x,warpWave);
|
|
const std::int64_t ny=NoiseCoordinateQ16(y,warpWave);
|
|
const std::int32_t wx=Perlin3DQ16(nx+SeedOffsetQ16(recipe.seed,71),ny+2*Q16+Q16*3/10,
|
|
SeedOffsetQ16(recipe.seed,330));
|
|
const std::int32_t wy=Perlin3DQ16(nx+6*Q16+Q16/10,ny+SeedOffsetQ16(recipe.seed,19),
|
|
SeedOffsetQ16(recipe.seed,470));
|
|
qx+=WarpDisplacement(wx,warpStrength);qy+=WarpDisplacement(wy,warpStrength);
|
|
}
|
|
std::int64_t cap=base;
|
|
if(undulation!=0)
|
|
{
|
|
const std::int64_t nx=NoiseCoordinateQ16(qx,swellWave)+SeedOffsetQ16(recipe.seed,190);
|
|
const std::int64_t ny=NoiseCoordinateQ16(qy,swellWave)+13*Q16;
|
|
const std::int32_t swell=FractalNoiseQ16(nx,ny,SeedOffsetQ16(recipe.seed,50),3);
|
|
cap+=WarpDisplacement(swell,undulation);
|
|
}
|
|
if(detail>0)
|
|
{
|
|
const std::int64_t nx=NoiseCoordinateQ16(x,detailWave)+5*Q16;
|
|
const std::int64_t ny=NoiseCoordinateQ16(y,detailWave)+6*Q16;
|
|
const std::int32_t noise=FractalNoiseQ16(nx,ny,SeedOffsetQ16(recipe.seed,210),3);
|
|
cap-=WarpDisplacement(std::abs(noise),detail);
|
|
}
|
|
if(ridge>0)
|
|
{
|
|
const std::int64_t nx=NoiseCoordinateQ16(qx,ridgeWave)+31*Q16;
|
|
const std::int64_t ny=NoiseCoordinateQ16(qy,ridgeWave)+47*Q16;
|
|
const std::int32_t ridgeNoise=FractalNoiseQ16(nx,ny,SeedOffsetQ16(recipe.seed,110),4,true);
|
|
const std::int64_t hang=std::clamp<std::int64_t>((ridgeNoise+Q16)/2,0,Q16);
|
|
cap-=MulQ16(hang,ridge);
|
|
}
|
|
return cap;
|
|
}
|
|
|
|
inline std::uint32_t FeatureFadeQ16(const WorldAssemblyFeatureV1& feature,
|
|
std::int64_t x,std::int64_t y,std::int64_t feather)
|
|
{
|
|
const auto& lo=feature.minimumSubvoxels;const auto& hi=feature.maximumSubvoxels;
|
|
if(x<lo[0]||x>hi[0]||y<lo[1]||y>hi[1])return 0;
|
|
if(feather<=0)return static_cast<std::uint32_t>(Q16);
|
|
const std::int64_t edge=std::min({x-lo[0],hi[0]-x,y-lo[1],hi[1]-y});
|
|
return SmoothQ16(static_cast<std::uint32_t>(std::clamp<std::int64_t>(edge*Q16/feather,0,Q16)));
|
|
}
|
|
|
|
struct HeightfieldColumnCacheEntry
|
|
{
|
|
const WorldAssemblyRecipeV1* recipe = nullptr;
|
|
const WorldAssemblyStratumPlanV1* plan = nullptr;
|
|
std::uint64_t recipeHash = 0, planHash = 0;
|
|
std::int64_t x = 0, y = 0;
|
|
HeightfieldPointSample column{};
|
|
};
|
|
// Coarse LOD tiles revisit a wider set of gradient-sample columns than the full-resolution
|
|
// band. Keep those columns resident for a complete worker tile so the 3D mesher does not
|
|
// repeat the procedural biome evaluation for every Z sample.
|
|
inline thread_local std::array<HeightfieldColumnCacheEntry, 65536> HeightfieldColumnCache{};
|
|
inline thread_local HeightfieldColumnCacheStats HeightfieldColumnStats{};
|
|
|
|
inline HeightfieldPointSample EvaluateColumn(const WorldAssemblyRecipeV1& recipe,
|
|
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y)
|
|
{
|
|
HeightfieldPointSample out;
|
|
if(recipe.evaluatorVersion>=2)
|
|
{
|
|
const BiomeSelectionV2 selection=SelectBiomeV2(recipe,x,y);
|
|
const auto& primary=recipe.biomes[selection.primary];
|
|
const auto& neighbor=recipe.biomes[selection.neighbor];
|
|
const std::int64_t primaryHeight=SurfaceHeightV2(recipe,plan,primary,x,y);
|
|
const std::int64_t neighborHeight=selection.primary==selection.neighbor?primaryHeight
|
|
:SurfaceHeightV2(recipe,plan,neighbor,x,y);
|
|
std::int64_t ground=primaryHeight+((neighborHeight-primaryHeight)
|
|
*(static_cast<std::int64_t>(Q16)-selection.primaryWeightQ16))/Q16;
|
|
const std::int64_t primaryCap=CapHeightV2(recipe,plan,primary,x,y);
|
|
const std::int64_t neighborCap=selection.primary==selection.neighbor?primaryCap
|
|
:CapHeightV2(recipe,plan,neighbor,x,y);
|
|
std::int64_t cap=primaryCap+((neighborCap-primaryCap)
|
|
*(static_cast<std::int64_t>(Q16)-selection.primaryWeightQ16))/Q16;
|
|
const auto* list=ScopedWorldAssemblyFeatureQueriesV1::find(&recipe,&plan,x,y,0,true);
|
|
for(const auto& feature:FeatureRange{plan.features,list})
|
|
{
|
|
if(feature.type==1)
|
|
{
|
|
const std::uint32_t fade=FeatureFadeQ16(feature,x,y,feature.parametersSubvoxels[1]);
|
|
if(fade)ground+=MulQ16(feature.parametersSubvoxels[0],fade);
|
|
}
|
|
else if(feature.type==3)
|
|
{
|
|
const std::uint32_t fade=FeatureFadeQ16(feature,x,y,feature.parametersSubvoxels[2]);
|
|
const std::int64_t wave=std::max<std::int64_t>(1,feature.parametersSubvoxels[1]);
|
|
if(fade)
|
|
{
|
|
const std::uint32_t featureSeed=static_cast<std::uint32_t>(feature.stableId);
|
|
const std::int64_t nx=NoiseCoordinateQ16(x,wave)+SeedOffsetQ16(featureSeed,1700);
|
|
const std::int64_t ny=NoiseCoordinateQ16(y,wave)+SeedOffsetQ16(featureSeed,2900);
|
|
const std::int32_t noise=FractalNoiseQ16(nx,ny,
|
|
SeedOffsetQ16(featureSeed,4100),3);
|
|
cap+=MulQ16(MulQ16(noise,feature.parametersSubvoxels[0]),fade);
|
|
}
|
|
}
|
|
}
|
|
out.groundHeightSubvoxels=ground;
|
|
out.capHeightSubvoxels=cap;
|
|
out.biomeId=primary.stableId;
|
|
out.neighborBiomeId=neighbor.stableId;
|
|
out.biomeWeightQ16=selection.primaryWeightQ16;
|
|
out.waterSurfaceSubvoxels=plan.waterSurfaceSubvoxels;
|
|
return out;
|
|
}
|
|
|
|
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;
|
|
std::int64_t cap=plan.capHeightSubvoxels+MulQ16(plan.capUndulationSubvoxels,
|
|
Noise2D(x,y,plan.capWavelengthSubvoxels,recipe.seed,0x4341505f554e4431ull));
|
|
out.capHeightSubvoxels=cap;
|
|
out.waterSurfaceSubvoxels=plan.waterSurfaceSubvoxels;
|
|
return out;
|
|
}
|
|
|
|
inline HeightfieldPointSample CachedColumn(const WorldAssemblyRecipeV1& recipe,
|
|
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y)
|
|
{
|
|
const std::uint64_t mixed=(static_cast<std::uint64_t>(x)*0x9E3779B185EBCA87ull)
|
|
^std::rotl(static_cast<std::uint64_t>(y)*0xC2B2AE3D27D4EB4Full,23)
|
|
^static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe))
|
|
^std::rotl(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),41);
|
|
auto& entry=HeightfieldColumnCache[static_cast<std::size_t>(mixed)&
|
|
(HeightfieldColumnCache.size()-1)];
|
|
if(entry.recipe==&recipe&&entry.plan==&plan&&entry.recipeHash==recipe.contentHash
|
|
&&entry.planHash==plan.contentHash&&entry.x==x&&entry.y==y)
|
|
{++HeightfieldColumnStats.columnHits;return entry.column;}
|
|
const auto start=std::chrono::steady_clock::now();
|
|
HeightfieldPointSample column=EvaluateColumn(recipe,plan,x,y);
|
|
HeightfieldColumnStats.columnComputeMilliseconds+=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-start).count();
|
|
++HeightfieldColumnStats.columnMisses;
|
|
entry={&recipe,&plan,recipe.contentHash,plan.contentHash,x,y,column};
|
|
return column;
|
|
}
|
|
|
|
inline std::int64_t CliffShelfDensity(const WorldAssemblyFeatureV1& feature,
|
|
const WorldAssemblyRecipeV1& recipe,const WorldAssemblyStratumPlanV1& plan,
|
|
std::int64_t x,std::int64_t y,std::int64_t z,
|
|
const HeightfieldPointSample& localColumn,bool enforceMinimumThickness)
|
|
{
|
|
const auto& lo=feature.minimumSubvoxels;const auto& hi=feature.maximumSubvoxels;
|
|
if(x<lo[0]||x>hi[0]||y<lo[1]||y>hi[1]||z<lo[2]||z>hi[2])
|
|
return std::numeric_limits<std::int64_t>::max()/4;
|
|
constexpr std::int64_t Feather=8*WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t dirX=feature.parametersSubvoxels[0]/WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t dirY=feature.parametersSubvoxels[1]/WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t height=feature.parametersSubvoxels[2];
|
|
const std::int64_t reach=feature.parametersSubvoxels[3];
|
|
if((dirX==0&&dirY==0)||height<=0||reach<=0)return std::numeric_limits<std::int64_t>::max()/4;
|
|
const std::int64_t centerX=(lo[0]+hi[0])/2,centerY=(lo[1]+hi[1])/2;
|
|
const std::int64_t edgeX=dirX>0?lo[0]+Feather:dirX<0?hi[0]-Feather:centerX;
|
|
const std::int64_t edgeY=dirY>0?lo[1]+Feather:dirY<0?hi[1]-Feather:centerY;
|
|
const std::int64_t dx=x-edgeX,dy=y-edgeY;
|
|
const std::int64_t along=dx*dirX+dy*dirY;
|
|
const std::int64_t lateral=-dx*dirY+dy*dirX;
|
|
const std::int64_t halfWidth=std::max<std::int64_t>(0,
|
|
(dirX!=0?(hi[1]-lo[1]):(hi[0]-lo[0]))/2-Feather);
|
|
const std::int64_t aboveGround=z-localColumn.groundHeightSubvoxels;
|
|
if(aboveGround<=0||aboveGround>=height||along<0||along>reach
|
|
||std::abs(lateral)>halfWidth)
|
|
return std::numeric_limits<std::int64_t>::max()/4;
|
|
const std::uint32_t vertical=SmoothQ16(static_cast<std::uint32_t>(
|
|
std::clamp<std::int64_t>(aboveGround*Q16/height,0,Q16)));
|
|
// Evaluator v4 samples the source column on the shelf's root line. The offset
|
|
// therefore follows horizontal progress through the block and stays fixed for
|
|
// every z slice. Advancing it with z can make the shelf switch on at its top;
|
|
// sampling the far end everywhere instead can pull a distant cliff cap across
|
|
// the whole shelf. Earlier evaluators retain their height-dependent taper.
|
|
const std::int64_t sourceOffset=enforceMinimumThickness
|
|
?std::clamp<std::int64_t>(along,0,reach):MulQ16(reach,vertical);
|
|
const std::int64_t sourceX=x-dirX*sourceOffset;
|
|
const std::int64_t sourceY=y-dirY*sourceOffset;
|
|
const HeightfieldPointSample source=CachedColumn(recipe,plan,sourceX,sourceY);
|
|
if(source.groundHeightSubvoxels<=localColumn.groundHeightSubvoxels)
|
|
return std::numeric_limits<std::int64_t>::max()/4;
|
|
const std::uint32_t edgeFade=FeatureFadeQ16(feature,x,y,Feather);
|
|
const std::int64_t edgeInset=static_cast<std::int64_t>(Q16-edgeFade)*Feather/Q16;
|
|
const std::int64_t sourceTop=enforceMinimumThickness
|
|
?std::max(source.groundHeightSubvoxels,
|
|
localColumn.groundHeightSubvoxels+MinimumCliffShelfThicknessSubvoxels)
|
|
:source.groundHeightSubvoxels;
|
|
std::int64_t candidate=std::max({localColumn.groundHeightSubvoxels-z,
|
|
z-sourceTop,-aboveGround,aboveGround-height});
|
|
if(enforceMinimumThickness)
|
|
{
|
|
// The feather narrows the shelf footprint. It must not offset the top and
|
|
// bottom surfaces, or their vertical separation collapses into MC sheets.
|
|
candidate=std::max({candidate,-along+edgeInset,along-reach+edgeInset,
|
|
std::abs(lateral)-halfWidth+edgeInset});
|
|
}
|
|
else
|
|
candidate=std::max({candidate,-along,along-reach,
|
|
std::abs(lateral)-halfWidth})+edgeInset;
|
|
return candidate;
|
|
}
|
|
|
|
inline std::int64_t SmoothMinSubvoxels(std::int64_t a,std::int64_t b,
|
|
std::int64_t width)
|
|
{
|
|
if(width<=0)return std::min(a,b);
|
|
const std::int64_t h=std::max<std::int64_t>(width-std::abs(a-b),0);
|
|
const std::int64_t smoothQ16=h*Q16/width;
|
|
return std::min(a,b)-MulQ16(MulQ16(smoothQ16,smoothQ16),width/4);
|
|
}
|
|
|
|
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() && 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)
|
|
{
|
|
if (stratum.hasRecipeOverride)
|
|
{
|
|
if (!world_assembly_detail::ValidateRecipe(stratum.recipeOverride, error)) return false;
|
|
if (stratum.recipeOverride.seed != recipe.seed)
|
|
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
|
|
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
|
|
}
|
|
if (!world_assembly_detail::ValidateStratum(stratum, error)) return false;
|
|
if (recipe.evaluatorVersion >= 4)
|
|
{
|
|
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
|
{ error = "world assembly evaluator 4 requires a versioned entrance-to-exit route"; return false; }
|
|
const std::int64_t bottomZ = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
|
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t topZ = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
|
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t halfHeight = stratum.routeConstraints.capsuleHeightSubvoxels / 2;
|
|
for (const auto& point : stratum.route)
|
|
{
|
|
if (point.positionSubvoxels[2] - halfHeight < bottomZ
|
|
|| point.positionSubvoxels[2] + halfHeight > topZ)
|
|
{ error = "world assembly route capsule escapes its finite stratum Z bounds"; return false; }
|
|
}
|
|
for (const auto& feature : stratum.features)
|
|
if (feature.type == 4 && feature.parametersSubvoxels[2]
|
|
< 2 * WorldAssemblySubvoxelsPerVoxel)
|
|
{ error = "world assembly evaluator 4 cliff shelf is thinner than its two-voxel minimum"; 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);
|
|
}
|
|
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; }
|
|
const auto& Scale = islands.manifest.recipe;
|
|
if (Scale.ownerToCoreScaleNumerator == 0 || Scale.ownerToCoreScaleDenominator == 0)
|
|
{ error = "floating-island owner-to-Core scale is invalid"; return false; }
|
|
const double CoreVoxelsPerOwnerChunk = static_cast<double>(CoreTileVoxelCount)
|
|
* Scale.ownerToCoreScaleNumerator / Scale.ownerToCoreScaleDenominator;
|
|
const std::int64_t bottomQ = static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(islands.bottomChunkZ) * CoreVoxelsPerOwnerChunk
|
|
* WorldAssemblySubvoxelsPerVoxel));
|
|
const std::int64_t topQ = static_cast<std::int64_t>(std::llround(
|
|
static_cast<double>(static_cast<std::int64_t>(islands.topChunkZ) + 1)
|
|
* CoreVoxelsPerOwnerChunk * WorldAssemblySubvoxelsPerVoxel));
|
|
if (islands.manifest.recipe.layoutMinimumZSubvoxels < bottomQ
|
|
|| islands.manifest.recipe.layoutMaximumZSubvoxels > topQ)
|
|
{ error = "floating-island recipe layout escapes the resolved owner 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);
|
|
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;
|
|
std::vector<std::uint64_t> expectedStratumRecipes;
|
|
for (const auto& stratum : plan.strates)
|
|
{
|
|
expectedStrates.push_back(stratum.contentHash);
|
|
expectedStratumRecipes.push_back(stratum.hasRecipeOverride
|
|
? stratum.recipeOverride.contentHash : 0);
|
|
}
|
|
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()
|
|
|| expectedStratumRecipes.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
|
|
|| expectedStratumRecipes[i] != (copy.plan.strates[i].hasRecipeOverride
|
|
? copy.plan.strates[i].recipeOverride.contentHash : 0))
|
|
{ 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;
|
|
}
|
|
|
|
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\":" << world_assembly_detail::RecipeJson(value.recipe)
|
|
<< ",\"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
|
|
;
|
|
const bool hasExtendedCap = stratum.capDetailAmplitudeSubvoxels != 0
|
|
|| stratum.capRidgeAmplitudeSubvoxels != 0 || stratum.waterEnabled
|
|
|| stratum.waterSurfaceSubvoxels != 0;
|
|
if (hasExtendedCap)
|
|
out << ",\"cap_detail_amplitude_q\":" << stratum.capDetailAmplitudeSubvoxels
|
|
<< ",\"cap_detail_wavelength_q\":" << stratum.capDetailWavelengthSubvoxels
|
|
<< ",\"cap_ridge_amplitude_q\":" << stratum.capRidgeAmplitudeSubvoxels
|
|
<< ",\"cap_ridge_wavelength_q\":" << stratum.capRidgeWavelengthSubvoxels
|
|
<< ",\"water_surface_q\":" << stratum.waterSurfaceSubvoxels
|
|
<< ",\"water_enabled\":" << (stratum.waterEnabled ? "true" : "false");
|
|
if (stratum.hasRecipeOverride)
|
|
out << ",\"recipe_override\":"
|
|
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
|
out
|
|
<< ",\"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 << ']';
|
|
if (stratum.routeConstraints.version != 0)
|
|
{
|
|
const auto& constraints = stratum.routeConstraints;
|
|
out << ",\"route_constraints\":{\"version\":" << constraints.version
|
|
<< ",\"capsule_radius_q\":" << constraints.capsuleRadiusSubvoxels
|
|
<< ",\"capsule_height_q\":" << constraints.capsuleHeightSubvoxels
|
|
<< ",\"maximum_grade_q16\":" << constraints.maximumGradeQ16
|
|
<< ",\"sweep_spacing_q\":" << constraints.sweepSpacingSubvoxels
|
|
<< ",\"support_probe_radius_q\":" << constraints.supportProbeRadiusSubvoxels
|
|
<< ",\"support_search_depth_q\":" << constraints.supportSearchDepthSubvoxels
|
|
<< ",\"minimum_clearance_q\":" << constraints.minimumClearanceSubvoxels
|
|
<< ",\"minimum_support_probes\":" << constraints.minimumSupportProbes << '}';
|
|
}
|
|
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 << "]}";
|
|
}
|
|
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();
|
|
}
|
|
|
|
Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe,
|
|
const WorldAssemblyStratumPlanV1& plan) noexcept
|
|
: recipe_(plan.hasRecipeOverride ? &plan.recipeOverride : &recipe), plan_(&plan)
|
|
{}
|
|
|
|
bool Heightfield2D::isValid() const noexcept
|
|
{
|
|
if (!recipe_ || !plan_ || recipe_->schemaVersion != WorldAssemblySchemaVersion
|
|
|| recipe_->evaluatorVersion == 0
|
|
|| 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 && biome.reliefWavelengthSubvoxels > 0
|
|
&& biome.heightWarpWavelengthSubvoxels > 0 && biome.cliffSampleDistanceSubvoxels > 0;
|
|
});
|
|
}
|
|
|
|
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
|
{
|
|
if(!isValid())return {0,0};
|
|
long double Minimum=std::numeric_limits<long double>::infinity();
|
|
long double Maximum=-std::numeric_limits<long double>::infinity();
|
|
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
|
{
|
|
const long double Base=Biome.hasCapProfile
|
|
?static_cast<long double>(Biome.capHeightSubvoxels)
|
|
:static_cast<long double>(plan_->capHeightSubvoxels);
|
|
const long double Undulation=Biome.hasCapProfile
|
|
?static_cast<long double>(Biome.capUndulationSubvoxels)
|
|
:static_cast<long double>(plan_->capUndulationSubvoxels);
|
|
const long double Detail=Biome.hasCapProfile
|
|
?static_cast<long double>(Biome.capDetailAmplitudeSubvoxels)
|
|
:static_cast<long double>(plan_->capDetailAmplitudeSubvoxels);
|
|
const long double Ridge=Biome.hasCapProfile
|
|
?static_cast<long double>(Biome.capRidgeAmplitudeSubvoxels)
|
|
:static_cast<long double>(plan_->capRidgeAmplitudeSubvoxels);
|
|
Minimum=std::min(Minimum,Base-Undulation-Detail-Ridge);
|
|
Maximum=std::max(Maximum,Base+Undulation);
|
|
};
|
|
if(recipe_->biomes.empty())
|
|
{
|
|
const WorldAssemblyBiomeProfileV1 Fallback{};
|
|
IncludeBiome(Fallback);
|
|
}
|
|
else for(const auto& Biome:recipe_->biomes)IncludeBiome(Biome);
|
|
long double FeatureAmplitude=0.0L;
|
|
for(const auto& Feature:plan_->features)
|
|
if(Feature.type==3)
|
|
FeatureAmplitude+=std::abs(static_cast<long double>(Feature.parametersSubvoxels[0]));
|
|
Minimum-=FeatureAmplitude;Maximum+=FeatureAmplitude;
|
|
if(recipe_->evaluatorVersion>=5)
|
|
{
|
|
const long double TopSubvoxels=(static_cast<long double>(plan_->topChunkZ)+1.0L)
|
|
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
|
|
Minimum=std::min(Minimum,TopSubvoxels);
|
|
Maximum=std::min(Maximum,TopSubvoxels);
|
|
}
|
|
const long double Int64Minimum=static_cast<long double>(std::numeric_limits<std::int64_t>::min());
|
|
const long double Int64Maximum=static_cast<long double>(std::numeric_limits<std::int64_t>::max());
|
|
return {static_cast<std::int64_t>(std::clamp(Minimum,Int64Minimum,Int64Maximum)),
|
|
static_cast<std::int64_t>(std::clamp(Maximum,Int64Minimum,Int64Maximum))};
|
|
}
|
|
|
|
std::int64_t Heightfield2D::farSurfaceCapHeightSubvoxels(std::int64_t x,
|
|
std::int64_t y) const noexcept
|
|
{
|
|
if(!isValid())return 0;
|
|
const HeightfieldPointSample column=point(x,y,0);
|
|
if(recipe_->evaluatorVersion<5)return column.capHeightSubvoxels;
|
|
// Clip only the far render surface. Keep the near density evaluator bit-identical.
|
|
// Borner uniquement le plafond lointain. L'evaluateur de densite proche reste identique.
|
|
const std::int64_t ownedUpperBoundary=(static_cast<std::int64_t>(plan_->topChunkZ)+1)
|
|
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
|
|
return std::min(column.capHeightSubvoxels,ownedUpperBoundary);
|
|
}
|
|
|
|
WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList(
|
|
const std::array<std::int64_t,3>& minimum,
|
|
const std::array<std::int64_t,3>& maximum) const
|
|
{
|
|
WorldAssemblyFeatureTileListV1 result;
|
|
// One shelf in the staged owner plan has no useful scan to shorten. Keep its full evaluator.
|
|
// Une seule corniche dans le plan auteur ne justifie pas la selection par echantillon.
|
|
result.candidateEvaluationUseful=plan_->features.size()>1;
|
|
result.minimumSubvoxels=minimum;
|
|
result.maximumSubvoxels=maximum;
|
|
std::vector<WorldAssemblyFeatureBoundsV1> bounds;
|
|
bounds.reserve(plan_->features.size());
|
|
for(const auto& feature:plan_->features)
|
|
{
|
|
WorldAssemblyFeatureBoundsV1 bound{feature.stableId,
|
|
feature.minimumSubvoxels,feature.maximumSubvoxels};
|
|
// Column writers affect every Z; finite volumes have an exact no-op outside their box.
|
|
// Les modificateurs de colonne agissent sur tout Z; les volumes sont neutres hors boite.
|
|
if(feature.type==1||feature.type==3)
|
|
{
|
|
bound.minimumSubvoxels[2]=std::numeric_limits<std::int64_t>::min();
|
|
bound.maximumSubvoxels[2]=std::numeric_limits<std::int64_t>::max();
|
|
}
|
|
else if(feature.type!=2&&feature.type!=4)
|
|
{
|
|
// Unknown future operations must opt into a proof before they can be culled.
|
|
// Une operation inconnue reste active tant que sa preuve manque.
|
|
bound.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
|
bound.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
|
}
|
|
bounds.push_back(bound);
|
|
}
|
|
result.featureIndices=BuildWorldAssemblyFeatureTileListV1(bounds,minimum,maximum);
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
++HeightfieldColumnStats.pointCalls;
|
|
out=CachedColumn(*recipe_,*plan_,x,y);
|
|
out.densitySubvoxels=std::min(z-out.groundHeightSubvoxels,
|
|
out.capHeightSubvoxels-z);
|
|
if(recipe_->evaluatorVersion>=2)
|
|
{
|
|
const auto* list=ScopedWorldAssemblyFeatureQueriesV1::find(recipe_,plan_,x,y,z);
|
|
for(const auto& feature:FeatureRange{plan_->features,list})
|
|
{
|
|
if(feature.type==2)
|
|
{
|
|
const auto& lo=feature.minimumSubvoxels;const auto& hi=feature.maximumSubvoxels;
|
|
if(x<lo[0]||x>hi[0]||y<lo[1]||y>hi[1]||z<lo[2]||z>hi[2])continue;
|
|
const std::int64_t depth=std::min({x-lo[0],hi[0]-x,y-lo[1],hi[1]-y,
|
|
z-lo[2],hi[2]-z});
|
|
out.densitySubvoxels=std::min(out.densitySubvoxels,-depth);
|
|
}
|
|
else if(feature.type==4&&recipe_->evaluatorVersion>=3)
|
|
{
|
|
const std::int64_t shelf=CliffShelfDensity(feature,*recipe_,*plan_,x,y,z,out,
|
|
recipe_->evaluatorVersion>=4);
|
|
out.densitySubvoxels=SmoothMinSubvoxels(out.densitySubvoxels,shelf,
|
|
4*WorldAssemblySubvoxelsPerVoxel);
|
|
}
|
|
}
|
|
}
|
|
if(recipe_->evaluatorVersion>=2&&plan_->waterEnabled&&out.densitySubvoxels>=0
|
|
&&z>out.groundHeightSubvoxels&&z<plan_->waterSurfaceSubvoxels
|
|
&&out.groundHeightSubvoxels<plan_->waterSurfaceSubvoxels)
|
|
out.waterFillSubvoxels=std::min(z-out.groundHeightSubvoxels,
|
|
plan_->waterSurfaceSubvoxels-z);
|
|
return out;
|
|
}
|
|
|
|
void Heightfield2D::resetThreadColumnCacheStats() noexcept
|
|
{ world_assembly_detail::HeightfieldColumnStats={}; }
|
|
|
|
HeightfieldColumnCacheStats Heightfield2D::threadColumnCacheStats() noexcept
|
|
{ return world_assembly_detail::HeightfieldColumnStats; }
|
|
|
|
HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX,
|
|
std::int64_t minimumY,std::int64_t minimumZ,std::int64_t maximumX,
|
|
std::int64_t maximumY,std::int64_t maximumZ,std::int32_t stepVoxels) const noexcept
|
|
{
|
|
using namespace world_assembly_detail;
|
|
if(!isValid()||stepVoxels<=0||minimumX>maximumX||minimumY>maximumY||minimumZ>maximumZ
|
|
||OutsideRange(minimumX)||OutsideRange(minimumY)||OutsideRange(minimumZ)
|
|
||OutsideRange(maximumX)||OutsideRange(maximumY)||OutsideRange(maximumZ))
|
|
return HeightfieldBoxClass::unknown;
|
|
for(const auto& feature:plan_->features)
|
|
{
|
|
if(feature.type!=2&&feature.type!=4)continue;
|
|
if(feature.minimumSubvoxels[0]<=maximumX&&feature.maximumSubvoxels[0]>=minimumX
|
|
&&feature.minimumSubvoxels[1]<=maximumY&&feature.maximumSubvoxels[1]>=minimumY
|
|
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
|
return HeightfieldBoxClass::mixed;
|
|
}
|
|
const std::int64_t stride=static_cast<std::int64_t>(stepVoxels)
|
|
*WorldAssemblySubvoxelsPerVoxel;
|
|
if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0
|
|
||(maximumX-minimumX)/stride>2048||(maximumY-minimumY)/stride>2048)
|
|
return HeightfieldBoxClass::unknown;
|
|
std::int64_t groundMin=std::numeric_limits<std::int64_t>::max();
|
|
std::int64_t groundMax=std::numeric_limits<std::int64_t>::min();
|
|
std::int64_t capMin=std::numeric_limits<std::int64_t>::max();
|
|
std::int64_t capMax=std::numeric_limits<std::int64_t>::min();
|
|
for(std::int64_t y=minimumY;;y+=stride)
|
|
{
|
|
for(std::int64_t x=minimumX;;x+=stride)
|
|
{
|
|
const auto column=point(x,y,0);
|
|
groundMin=std::min(groundMin,column.groundHeightSubvoxels);
|
|
groundMax=std::max(groundMax,column.groundHeightSubvoxels);
|
|
capMin=std::min(capMin,column.capHeightSubvoxels);
|
|
capMax=std::max(capMax,column.capHeightSubvoxels);
|
|
if(x==maximumX)break;
|
|
}
|
|
if(y==maximumY)break;
|
|
}
|
|
if(minimumZ>groundMax&&maximumZ<capMin)return HeightfieldBoxClass::allAir;
|
|
if(maximumZ<groundMin||minimumZ>capMax)return HeightfieldBoxClass::allSolid;
|
|
return HeightfieldBoxClass::mixed;
|
|
}
|
|
|
|
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();
|
|
std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
|
std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels;
|
|
if(recipe_->evaluatorVersion>=2)
|
|
{
|
|
for(const auto& biome:recipe_->biomes)
|
|
{
|
|
const std::int64_t displacement=biome.continentAmplitudeSubvoxels
|
|
+biome.mountainAmplitudeSubvoxels+2*biome.detailAmplitudeSubvoxels
|
|
+biome.cliffMaximumDisplacementSubvoxels;
|
|
groundMin=std::min(groundMin,biome.baseHeightSubvoxels-displacement);
|
|
groundMax=std::max(groundMax,biome.baseHeightSubvoxels+displacement);
|
|
if(plan_->waterEnabled&&biome.beachWidthSubvoxels>0)
|
|
{
|
|
groundMin=std::min(groundMin,plan_->waterSurfaceSubvoxels-biome.beachWidthSubvoxels);
|
|
groundMax=std::max(groundMax,plan_->waterSurfaceSubvoxels+biome.beachWidthSubvoxels);
|
|
}
|
|
if(biome.hasCapProfile)
|
|
{
|
|
const std::int64_t profileCapDown=biome.capUndulationSubvoxels*5/2
|
|
+biome.capDetailAmplitudeSubvoxels*5/2+biome.capRidgeAmplitudeSubvoxels;
|
|
const std::int64_t profileCapUp=biome.capUndulationSubvoxels*5/2;
|
|
capMin=std::min(capMin,biome.capHeightSubvoxels-profileCapDown);
|
|
capMax=std::max(capMax,biome.capHeightSubvoxels+profileCapUp);
|
|
}
|
|
}
|
|
std::int64_t localCliffMagnitude=0,localCapMagnitude=0;
|
|
for(const auto& feature:plan_->features)
|
|
{
|
|
if(feature.type==1)localCliffMagnitude=std::max(localCliffMagnitude,
|
|
std::abs(feature.parametersSubvoxels[0]));
|
|
if(feature.type==3)localCapMagnitude+=std::abs(feature.parametersSubvoxels[0]);
|
|
}
|
|
groundMin-=localCliffMagnitude;groundMax+=localCliffMagnitude;
|
|
const std::int64_t capDown=plan_->capUndulationSubvoxels*5/2
|
|
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
|
const std::int64_t capUp=plan_->capUndulationSubvoxels*5/2+localCapMagnitude;
|
|
capMin=std::min(capMin,plan_->capHeightSubvoxels-capDown-localCapMagnitude);
|
|
capMax=std::max(capMax,plan_->capHeightSubvoxels+capUp);
|
|
}
|
|
for (const auto& biome : recipe_->biomes)
|
|
{
|
|
if(recipe_->evaluatorVersion<2)
|
|
{
|
|
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 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);
|
|
if(recipe_->evaluatorVersion>=2)
|
|
{
|
|
for(const auto& feature:plan_->features)
|
|
{
|
|
if(feature.type!=2&&feature.type!=4)continue;
|
|
bool overlap=true;
|
|
for(std::size_t axis=0;axis<3;++axis)
|
|
overlap=overlap&&feature.minimumSubvoxels[axis]<=std::array<std::int64_t,3>{maxX,maxY,maxZ}[axis]
|
|
&&feature.maximumSubvoxels[axis]>=std::array<std::int64_t,3>{minX,minY,minZ}[axis];
|
|
if(!overlap)continue;
|
|
if(feature.type==2)
|
|
{
|
|
const std::int64_t depth=std::min({
|
|
(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2,
|
|
(feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2,
|
|
(feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2});
|
|
out.minimumDensitySubvoxels=std::min(out.minimumDensitySubvoxels,-depth);
|
|
}
|
|
else
|
|
out.minimumDensitySubvoxels=std::min(out.minimumDensitySubvoxels,
|
|
-feature.parametersSubvoxels[2]-WorldAssemblySubvoxelsPerVoxel);
|
|
}
|
|
}
|
|
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*5/2
|
|
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
|
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
|
for (const auto& biome : recipe_->biomes)
|
|
{
|
|
const std::int64_t surfaceAmplitude=biome.continentAmplitudeSubvoxels
|
|
+biome.mountainAmplitudeSubvoxels+biome.detailAmplitudeSubvoxels
|
|
+biome.cliffMaximumDisplacementSubvoxels+biome.beachWidthSubvoxels;
|
|
maximum = std::max(maximum,surfaceAmplitude);
|
|
if(biome.hasCapProfile)
|
|
maximum=std::max(maximum,biome.capUndulationSubvoxels*5/2
|
|
+biome.capDetailAmplitudeSubvoxels*5/2+biome.capRidgeAmplitudeSubvoxels);
|
|
minimumFeature = std::min({minimumFeature, biome.continentWavelengthSubvoxels/8,
|
|
biome.mountainWavelengthSubvoxels/8, biome.detailWavelengthSubvoxels/4,
|
|
biome.reliefWavelengthSubvoxels/2, biome.heightWarpWavelengthSubvoxels,
|
|
biome.cliffSampleDistanceSubvoxels});
|
|
}
|
|
minimumFeature=std::min({minimumFeature,plan_->capWavelengthSubvoxels/4,
|
|
plan_->capDetailWavelengthSubvoxels/4,plan_->capRidgeWavelengthSubvoxels/8});
|
|
for(const auto& feature:plan_->features)
|
|
{
|
|
if(feature.type==1)maximum=std::max(maximum,std::abs(feature.parametersSubvoxels[0]));
|
|
if(feature.type==2)
|
|
maximum=std::max(maximum,std::min({
|
|
(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2,
|
|
(feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2,
|
|
(feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2}));
|
|
if(feature.type==4)maximum=std::max(maximum,feature.parametersSubvoxels[2]
|
|
+WorldAssemblySubvoxelsPerVoxel);
|
|
if(feature.type==4&&recipe_->evaluatorVersion>=4)
|
|
minimumFeature=std::min(minimumFeature,
|
|
world_assembly_detail::MinimumCliffShelfThicknessSubvoxels);
|
|
if(feature.type==3)maximum=std::max(maximum,std::abs(feature.parametersSubvoxels[0]));
|
|
}
|
|
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)
|
|
| ChannelBit(FieldChannel::water) | ChannelBit(FieldChannel::waterSurface),
|
|
recipe_?recipe_->evaluatorVersion:WorldAssemblyEvaluatorVersion};
|
|
}
|
|
|
|
WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block,
|
|
std::uint32_t boxCount, std::uint64_t seed)
|
|
{
|
|
WorldAssemblyBoundsAuditResult result;
|
|
const auto* recipe=block.recipe();
|
|
const auto* plan=block.plan();
|
|
if (!block.isValid() || !recipe || !plan) 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; };
|
|
const auto testBox=[&](std::int64_t x,std::int64_t y,std::int64_t z,
|
|
std::int64_t halfX,std::int64_t halfY,std::int64_t halfZ,
|
|
std::uint64_t* category,bool countExceptionPoints,const char* tag)
|
|
{
|
|
const FieldInterval bounds=block.interval(x-halfX,y-halfY,z-halfZ,
|
|
x+halfX,y+halfY,z+halfZ);
|
|
++result.boxesTested;
|
|
if(category)++*category;
|
|
if(x<0||y<0)++result.negativeCoordinateBoxes;
|
|
if(!bounds.known){++result.unknownBounds;return;}
|
|
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+halfX*ix;
|
|
const std::int64_t py=y-halfY+halfY*iy;
|
|
const std::int64_t pz=z-halfZ+halfZ*iz;
|
|
const auto sample=block.point(px,py,pz);
|
|
++result.pointSamples;
|
|
if(countExceptionPoints)++result.exceptionPointSamples;
|
|
if(!world_assembly_detail::InInterval(bounds,sample.densitySubvoxels))
|
|
{
|
|
++result.missedBounds;
|
|
if(result.missedBounds<=6)
|
|
std::fprintf(stderr,"[WorldAssemblerBoundMiss] tag=%s box=(%lld,%lld,%lld) half=(%lld,%lld,%lld) bounds=[%lld,%lld] point=(%lld,%lld,%lld) density=%lld\n",
|
|
tag,static_cast<long long>(x),static_cast<long long>(y),static_cast<long long>(z),
|
|
static_cast<long long>(halfX),static_cast<long long>(halfY),static_cast<long long>(halfZ),
|
|
static_cast<long long>(bounds.minimumDensitySubvoxels),
|
|
static_cast<long long>(bounds.maximumDensitySubvoxels),
|
|
static_cast<long long>(px),static_cast<long long>(py),static_cast<long long>(pz),
|
|
static_cast<long long>(sample.densitySubvoxels));
|
|
}
|
|
}
|
|
};
|
|
for (std::uint32_t i = 0; i < boxCount; ++i)
|
|
{
|
|
std::int64_t x = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
|
|
std::int64_t y = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
|
|
if(i%4==0){x=-std::abs(x);y=-std::abs(y);}
|
|
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;
|
|
testBox(x,y,z,halfX,halfY,halfZ,nullptr,false,"random");
|
|
}
|
|
const std::int64_t cell=recipe->biomeCellSizeSubvoxels;
|
|
const std::int64_t searchExtent=2*cell;
|
|
const std::int64_t borderStep=std::max<std::int64_t>(cell/8,
|
|
32*WorldAssemblySubvoxelsPerVoxel);
|
|
for(std::int64_t y=-searchExtent;y<=searchExtent&&result.biomeBorderBoxes<16;y+=borderStep)
|
|
for(std::int64_t x=-searchExtent;x<=searchExtent&&result.biomeBorderBoxes<16;x+=borderStep)
|
|
{
|
|
const auto column=block.point(x,y,0);
|
|
if(column.biomeId==column.neighborBiomeId||column.biomeWeightQ16>=65536)continue;
|
|
testBox(x,y,column.groundHeightSubvoxels,16*WorldAssemblySubvoxelsPerVoxel,
|
|
16*WorldAssemblySubvoxelsPerVoxel,8*WorldAssemblySubvoxelsPerVoxel,
|
|
&result.biomeBorderBoxes,false,"border");
|
|
}
|
|
const std::int64_t capStep=std::max<std::int64_t>(cell/2,
|
|
64*WorldAssemblySubvoxelsPerVoxel);
|
|
for(std::int64_t y=-capStep;y<=capStep;y+=capStep)
|
|
for(std::int64_t x=-capStep;x<=capStep;x+=capStep)
|
|
{
|
|
const auto column=block.point(x,y,0);
|
|
testBox(x,y,column.capHeightSubvoxels,8*WorldAssemblySubvoxelsPerVoxel,
|
|
8*WorldAssemblySubvoxelsPerVoxel,16*WorldAssemblySubvoxelsPerVoxel,
|
|
&result.capBoxes,false,"cap");
|
|
}
|
|
bool hasCliffProfile=false;
|
|
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
|
if(hasCliffProfile)
|
|
{
|
|
const std::int64_t d=16*WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t step=std::max<std::int64_t>(cell/10,
|
|
64*WorldAssemblySubvoxelsPerVoxel);
|
|
for(std::int64_t y=-2*cell;y<=2*cell&&result.cliffBoxes<16;y+=step)
|
|
for(std::int64_t x=-2*cell;x<=2*cell&&result.cliffBoxes<16;x+=step)
|
|
{
|
|
const auto center=block.point(x,y,0);
|
|
const auto xp=block.point(x+d,y,0),xm=block.point(x-d,y,0);
|
|
const auto yp=block.point(x,y+d,0),ym=block.point(x,y-d,0);
|
|
const std::int64_t gradient=std::max(std::abs(xp.groundHeightSubvoxels-xm.groundHeightSubvoxels),
|
|
std::abs(yp.groundHeightSubvoxels-ym.groundHeightSubvoxels));
|
|
if(gradient<d/2)continue;
|
|
testBox(x,y,center.groundHeightSubvoxels,d,d,8*WorldAssemblySubvoxelsPerVoxel,
|
|
&result.cliffBoxes,false,"cliff");
|
|
}
|
|
}
|
|
for(const auto& feature:plan->features)
|
|
{
|
|
if(feature.type!=2&&feature.type!=4)continue;
|
|
const std::int64_t x=feature.minimumSubvoxels[0]
|
|
+(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2;
|
|
const std::int64_t y=feature.minimumSubvoxels[1]
|
|
+(feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2;
|
|
const std::int64_t z=feature.minimumSubvoxels[2]
|
|
+(feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2;
|
|
testBox(x,y,z,(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2,
|
|
(feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2,
|
|
(feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2,
|
|
&result.exceptionBoxes,true,"exception");
|
|
}
|
|
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);
|
|
const bool borderCoverage=recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
|
||result.biomeBorderBoxes>0;
|
|
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
|
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
|
[](const auto& feature){return feature.type==2||feature.type==4;})||result.exceptionBoxes>0;
|
|
result.passed = result.boxesTested >= boxCount && result.negativeCoordinateBoxes>0
|
|
&& borderCoverage&&cliffCoverage&&result.capBoxes>0&&exceptionCoverage
|
|
&& result.missedBounds == 0
|
|
&& result.unknownBounds == 0 && result.negativeControlDetected;
|
|
return result;
|
|
}
|
|
|
|
WorldAssemblyShelfThicknessAuditResult AuditHeightfieldShelfThickness(const Heightfield2D& block)
|
|
{
|
|
WorldAssemblyShelfThicknessAuditResult result;
|
|
const auto* recipe=block.recipe();
|
|
const auto* plan=block.plan();
|
|
if(!block.isValid()||!recipe||!plan||recipe->evaluatorVersion<4)return result;
|
|
constexpr std::int64_t sampleStep=WorldAssemblySubvoxelsPerVoxel/4;
|
|
constexpr std::int64_t xyStep=WorldAssemblySubvoxelsPerVoxel/2;
|
|
std::int64_t minimumObserved=std::numeric_limits<std::int64_t>::max();
|
|
for(std::size_t featureIndex=0;featureIndex<plan->features.size();++featureIndex)
|
|
{
|
|
const auto& feature=plan->features[featureIndex];
|
|
if(feature.type!=4)continue;
|
|
++result.featuresTested;
|
|
auto baselinePlan=*plan;
|
|
baselinePlan.features.erase(baselinePlan.features.begin()+featureIndex);
|
|
const Heightfield2D baseline(*recipe,baselinePlan);
|
|
const std::int64_t firstX=feature.minimumSubvoxels[0]+WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t lastX=feature.maximumSubvoxels[0]-WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t firstY=feature.minimumSubvoxels[1]+WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t lastY=feature.maximumSubvoxels[1]-WorldAssemblySubvoxelsPerVoxel;
|
|
for(std::int64_t y=firstY;y<=lastY;y+=xyStep)
|
|
for(std::int64_t x=firstX;x<=lastX;x+=xyStep)
|
|
{
|
|
++result.columnsTested;
|
|
const std::int64_t ground=baseline.point(x,y,0).groundHeightSubvoxels;
|
|
const std::int64_t range=std::max<std::int64_t>(feature.parametersSubvoxels[2],
|
|
world_assembly_detail::MinimumCliffShelfThicknessSubvoxels)+sampleStep;
|
|
std::int64_t currentRun=0,legacyCurrentRun=0;
|
|
std::int64_t currentStart=0,legacyCurrentStart=0;
|
|
bool columnActive=false,columnSubVoxel=false,legacyColumnSubVoxel=false;
|
|
const auto recordRun=[&](std::int64_t run,std::int64_t start,bool legacy)
|
|
{
|
|
if(run<=0)return;
|
|
if(legacy)
|
|
{
|
|
if(run<WorldAssemblySubvoxelsPerVoxel)
|
|
{++result.legacySubVoxelRuns;legacyColumnSubVoxel=true;}
|
|
return;
|
|
}
|
|
columnActive=true;
|
|
++result.negativeRuns;
|
|
if(run<minimumObserved)
|
|
{
|
|
minimumObserved=run;
|
|
result.minimumThicknessColumnXSubvoxels=x;
|
|
result.minimumThicknessColumnYSubvoxels=y;
|
|
result.minimumThicknessGroundSubvoxels=ground;
|
|
result.minimumThicknessBottomSubvoxels=start;
|
|
result.minimumThicknessTopSubvoxels=start+run-sampleStep;
|
|
}
|
|
if(run<WorldAssemblySubvoxelsPerVoxel)
|
|
{++result.subVoxelRuns;columnSubVoxel=true;}
|
|
};
|
|
for(std::int64_t dz=sampleStep;dz<=range;dz+=sampleStep)
|
|
{
|
|
const std::int64_t z=ground+dz;
|
|
const auto baselineSample=baseline.point(x,y,z);
|
|
if(baselineSample.densitySubvoxels<=0)
|
|
{
|
|
recordRun(currentRun,currentStart,false);currentRun=0;
|
|
recordRun(legacyCurrentRun,legacyCurrentStart,true);legacyCurrentRun=0;
|
|
continue;
|
|
}
|
|
const auto composedSample=block.point(x,y,z);
|
|
if(composedSample.densitySubvoxels<0)
|
|
{
|
|
if(currentRun==0)currentStart=z;
|
|
currentRun+=sampleStep;
|
|
}
|
|
else {recordRun(currentRun,currentStart,false);currentRun=0;}
|
|
|
|
const std::int64_t oldShelf=world_assembly_detail::CliffShelfDensity(feature,*recipe,baselinePlan,
|
|
x,y,z,baselineSample,false);
|
|
const std::int64_t legacyDensity=world_assembly_detail::SmoothMinSubvoxels(
|
|
baselineSample.densitySubvoxels,oldShelf,4*WorldAssemblySubvoxelsPerVoxel);
|
|
if(legacyDensity<0)
|
|
{if(legacyCurrentRun==0)legacyCurrentStart=z;legacyCurrentRun+=sampleStep;}
|
|
else {recordRun(legacyCurrentRun,legacyCurrentStart,true);legacyCurrentRun=0;}
|
|
}
|
|
recordRun(currentRun,currentStart,false);
|
|
recordRun(legacyCurrentRun,legacyCurrentStart,true);
|
|
if(columnActive)++result.activeColumns;
|
|
if(columnSubVoxel)++result.subVoxelColumns;
|
|
if(legacyColumnSubVoxel)++result.legacySubVoxelColumns;
|
|
}
|
|
}
|
|
if(minimumObserved!=std::numeric_limits<std::int64_t>::max())
|
|
result.minimumObservedThicknessSubvoxels=minimumObserved;
|
|
result.negativeControlDetected=result.legacySubVoxelColumns>0;
|
|
result.passed=result.featuresTested>0&&result.activeColumns>0
|
|
&&result.minimumObservedThicknessSubvoxels>=world_assembly_detail::MinimumCliffShelfThicknessSubvoxels
|
|
&&result.subVoxelRuns==0&&result.negativeControlDetected;
|
|
return result;
|
|
}
|
|
|
|
WorldAssemblyRouteAuditResult AuditHeightfieldRoute(const Heightfield2D& block)
|
|
{
|
|
WorldAssemblyRouteAuditResult result;
|
|
const auto* plan=block.plan();
|
|
if(!block.isValid()||!plan||plan->routeConstraints.version!=1||plan->route.size()<2)
|
|
return result;
|
|
using namespace world_assembly_detail;
|
|
const auto& constraints=plan->routeConstraints;
|
|
const std::int64_t radius=constraints.capsuleRadiusSubvoxels;
|
|
const std::int64_t halfHeight=constraints.capsuleHeightSubvoxels/2;
|
|
const std::int64_t axisHalf=std::max<std::int64_t>(0,halfHeight-radius);
|
|
const std::int64_t spacing=std::min<std::int64_t>(constraints.sweepSpacingSubvoxels,
|
|
WorldAssemblySubvoxelsPerVoxel/2);
|
|
const std::int64_t supportSpacing=std::min<std::int64_t>(spacing,
|
|
WorldAssemblySubvoxelsPerVoxel/4);
|
|
std::int64_t minimumClearance=std::numeric_limits<std::int64_t>::max();
|
|
std::int64_t maximumSupportDepth=0;
|
|
const std::array<std::array<std::int64_t,2>,5> supportOffsets{{
|
|
{{0,0}},{{constraints.supportProbeRadiusSubvoxels,0}},
|
|
{{-constraints.supportProbeRadiusSubvoxels,0}},
|
|
{{0,constraints.supportProbeRadiusSubvoxels}},
|
|
{{0,-constraints.supportProbeRadiusSubvoxels}}}};
|
|
|
|
const auto sweepCapsule=[&](const std::array<std::int64_t,3>& center)
|
|
{
|
|
++result.centerSamples;
|
|
const std::int64_t footZ=center[2]-halfHeight;
|
|
const std::int64_t firstDz=-halfHeight+constraints.minimumClearanceSubvoxels;
|
|
for(std::int64_t dz=firstDz;dz<=halfHeight;dz+=spacing)
|
|
{
|
|
const std::int64_t capOffset=std::max<std::int64_t>(0,std::abs(dz)-axisHalf);
|
|
const std::int64_t radialSquared=radius*radius-capOffset*capOffset;
|
|
if(radialSquared<0)continue;
|
|
for(std::int64_t oy=-radius;oy<=radius;oy+=spacing)
|
|
for(std::int64_t ox=-radius;ox<=radius;ox+=spacing)
|
|
{
|
|
if(ox*ox+oy*oy>radialSquared)continue;
|
|
const auto sample=block.point(center[0]+ox,center[1]+oy,center[2]+dz);
|
|
++result.capsuleFieldSamples;
|
|
minimumClearance=std::min(minimumClearance,sample.densitySubvoxels);
|
|
if(sample.densitySubvoxels<constraints.minimumClearanceSubvoxels)
|
|
++result.blockedCapsuleSamples;
|
|
}
|
|
}
|
|
std::uint32_t supportedProbes=0;
|
|
for(const auto& offset:supportOffsets)
|
|
{
|
|
++result.supportProbeSamples;
|
|
bool supported=false;
|
|
for(std::int64_t depth=supportSpacing;
|
|
depth<=constraints.supportSearchDepthSubvoxels;depth+=supportSpacing)
|
|
{
|
|
const auto sample=block.point(center[0]+offset[0],center[1]+offset[1],footZ-depth);
|
|
if(sample.densitySubvoxels<0)
|
|
{
|
|
supported=true;
|
|
maximumSupportDepth=std::max(maximumSupportDepth,depth);
|
|
break;
|
|
}
|
|
}
|
|
if(supported)++supportedProbes;
|
|
}
|
|
if(supportedProbes<constraints.minimumSupportProbes)
|
|
++result.unsupportedCenterSamples;
|
|
};
|
|
|
|
for(std::size_t segment=1;segment<plan->route.size();++segment)
|
|
{
|
|
++result.segmentsTested;
|
|
const auto& from=plan->route[segment-1].positionSubvoxels;
|
|
const auto& to=plan->route[segment].positionSubvoxels;
|
|
const std::int64_t dx=to[0]-from[0],dy=to[1]-from[1],dz=to[2]-from[2];
|
|
const std::uint64_t horizontalSquared=static_cast<std::uint64_t>(dx*dx+dy*dy);
|
|
const std::uint64_t horizontalLength=IntegerSqrt(horizontalSquared);
|
|
const std::uint64_t rise=static_cast<std::uint64_t>(std::abs(dz));
|
|
if(horizontalLength==0||rise*static_cast<std::uint64_t>(Q16)
|
|
>horizontalLength*static_cast<std::uint64_t>(constraints.maximumGradeQ16))
|
|
++result.gradeFailures;
|
|
const std::uint64_t length=IntegerSqrt(horizontalSquared
|
|
+static_cast<std::uint64_t>(dz*dz));
|
|
const std::uint64_t sampleCount=std::max<std::uint64_t>(1,
|
|
(length+static_cast<std::uint64_t>(spacing)-1)/static_cast<std::uint64_t>(spacing));
|
|
const std::uint64_t first=segment==1?0:1;
|
|
for(std::uint64_t i=first;i<=sampleCount;++i)
|
|
{
|
|
const auto interpolate=[&](std::size_t axis)
|
|
{
|
|
const std::int64_t delta=to[axis]-from[axis];
|
|
return from[axis]+delta*static_cast<std::int64_t>(i)
|
|
/static_cast<std::int64_t>(sampleCount);
|
|
};
|
|
sweepCapsule({interpolate(0),interpolate(1),interpolate(2)});
|
|
}
|
|
}
|
|
result.minimumClearanceSubvoxels=minimumClearance==std::numeric_limits<std::int64_t>::max()
|
|
?0:minimumClearance;
|
|
result.maximumSupportDepthSubvoxels=maximumSupportDepth;
|
|
result.passed=result.segmentsTested>0&&result.centerSamples>0
|
|
&&result.blockedCapsuleSamples==0&&result.unsupportedCenterSamples==0
|
|
&&result.gradeFailures==0;
|
|
return result;
|
|
}
|
|
}
|