wip(assembler): M1 round 1 -- manifest format, block contract, bound audit, native Heightfield2D
Luna, M1 round 1 of 4 (plan in Standalone/ASSEMBLER_M1_REPORT.md): - Versioned fixed-scale world manifest (the shipped plan format) and the Core block contract: point evaluation, interval-or-unknown, influence bound, detail envelope, channels. - Interval audit: 256 boxes / 6,912 samples, 0 missed, 0 unknown; a deliberately false bound is detected (negative control). - Native Heightfield2D with biome input: 4,096-point hash fae2721c052aec5c identical at W1/W8/W16; point queries 2.059 ms vs 62.115 ms legacy -- but NOT a match yet (mean density error 274 voxels, 2,261 sign mismatches vs the legacy surface). 45 mesh-free Core queries match direct evaluation exactly. Existing cave world unchanged (walkability, 64/64 tiles). - Open: collision readiness, a valid full-view timing (viewer captures were occluded/empty; top-down height maps are the only usable pair). Also carries the design doc's owner decisions (fixed water; player edits unrestricted, route law for automatic changes only) and three plugin files + BuildHost scripts that earlier round commits had missed (also fixed on unreal/core-integration as 2574b56). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
d5ce5d675d
commit
874c796c65
20 files changed
+2880
-12
No files matched your search
@@ -0,0 +1,766 @@
|
||||
#pragma once
|
||||
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
namespace world_assembly_detail
|
||||
{
|
||||
constexpr std::int64_t Q16 = 1ll << 16;
|
||||
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
||||
constexpr std::size_t MaxBiomeCount = 8;
|
||||
constexpr std::size_t MaxStrateCount = 64;
|
||||
constexpr std::size_t MaxFeatureCount = 4096;
|
||||
constexpr std::size_t MaxRoutePointCount = 4096;
|
||||
|
||||
inline void HashWord(std::uint64_t& hash, std::uint64_t word, int bytes)
|
||||
{
|
||||
for (int i = 0; i < bytes; ++i)
|
||||
{
|
||||
hash ^= static_cast<std::uint8_t>(word >> (i * 8));
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::uint64_t StartHash() { return 14695981039346656037ull; }
|
||||
|
||||
inline bool ReadInteger(const VFJson::Value& value, std::int64_t& out)
|
||||
{
|
||||
if (!value.IsNumber()) return false;
|
||||
const double number = value.Number();
|
||||
constexpr double ExactJsonIntegerLimit = 9007199254740991.0;
|
||||
if (!std::isfinite(number) || std::trunc(number) != number
|
||||
|| std::abs(number) > ExactJsonIntegerLimit) return false;
|
||||
out = static_cast<std::int64_t>(number);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadU32(const VFJson::Value& value, std::uint32_t& out)
|
||||
{
|
||||
std::int64_t parsed = 0;
|
||||
if (!ReadInteger(value, parsed) || parsed < 0
|
||||
|| parsed > std::numeric_limits<std::uint32_t>::max()) return false;
|
||||
out = static_cast<std::uint32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadI32(const VFJson::Value& value, std::int32_t& out)
|
||||
{
|
||||
std::int64_t parsed = 0;
|
||||
if (!ReadInteger(value, parsed) || parsed < std::numeric_limits<std::int32_t>::min()
|
||||
|| parsed > std::numeric_limits<std::int32_t>::max()) return false;
|
||||
out = static_cast<std::int32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadArray(const VFJson::Value& value, std::int64_t* out, std::size_t count)
|
||||
{
|
||||
if (!value.IsArray() || value.AsArray().size() != count) return false;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (!ReadInteger(value.AsArray()[i], out[i])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadHash(const VFJson::Value& value, std::uint64_t& out)
|
||||
{
|
||||
if (!value.IsString()) return false;
|
||||
std::string_view text = value.String();
|
||||
if (text.starts_with("0x") || text.starts_with("0X")) text.remove_prefix(2);
|
||||
if (text.empty() || text.size() > 16) return false;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), out, 16);
|
||||
return result.ec == std::errc{} && result.ptr == text.data() + text.size();
|
||||
}
|
||||
|
||||
inline std::string HashText(std::uint64_t hash)
|
||||
{
|
||||
constexpr char Hex[] = "0123456789abcdef";
|
||||
std::string out(16, '0');
|
||||
for (int i = 15; i >= 0; --i)
|
||||
{
|
||||
out[static_cast<std::size_t>(i)] = Hex[hash & 0xfu];
|
||||
hash >>= 4;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool ValidScalar(std::int64_t value)
|
||||
{ return value >= -MaxSerializedMagnitude && value <= MaxSerializedMagnitude; }
|
||||
|
||||
inline bool OutsideRange(std::int64_t value)
|
||||
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
|
||||
|
||||
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||
{
|
||||
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
||||
|| recipe.evaluatorVersion != WorldAssemblyEvaluatorVersion
|
||||
|| recipe.unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel)
|
||||
{ error = "world assembly recipe version, evaluator or fixed scale is unsupported"; return false; }
|
||||
if (recipe.biomes.empty() || recipe.biomes.size() > MaxBiomeCount)
|
||||
{ error = "world assembly recipe must contain one to eight biome profiles"; return false; }
|
||||
if (recipe.biomeCellSizeSubvoxels <= 0 || recipe.biomeCellSizeSubvoxels > 100000000ll
|
||||
|| recipe.biomeWarpSubvoxels < 0 || recipe.biomeWarpSubvoxels > recipe.biomeCellSizeSubvoxels / 2
|
||||
|| recipe.biomeWarpWavelengthSubvoxels <= 0
|
||||
|| recipe.biomeWarpWavelengthSubvoxels > 100000000ll
|
||||
|| recipe.biomeBorderBlendSubvoxels < 0
|
||||
|| recipe.biomeBorderBlendSubvoxels > recipe.biomeCellSizeSubvoxels)
|
||||
{ error = "world assembly biome-map parameters are outside the supported fixed-scale range"; return false; }
|
||||
std::vector<std::uint32_t> ids;
|
||||
ids.reserve(recipe.biomes.size());
|
||||
for (const auto& biome : recipe.biomes)
|
||||
{
|
||||
if (biome.stableId == 0 || std::find(ids.begin(), ids.end(), biome.stableId) != ids.end())
|
||||
{ error = "world assembly biome IDs must be nonzero and unique"; return false; }
|
||||
ids.push_back(biome.stableId);
|
||||
const std::array<std::int64_t, 7> values = {biome.baseHeightSubvoxels,
|
||||
biome.continentAmplitudeSubvoxels, biome.continentWavelengthSubvoxels,
|
||||
biome.mountainAmplitudeSubvoxels, biome.mountainWavelengthSubvoxels,
|
||||
biome.detailAmplitudeSubvoxels, biome.detailWavelengthSubvoxels};
|
||||
if (!std::all_of(values.begin(), values.end(), ValidScalar)
|
||||
|| biome.continentAmplitudeSubvoxels < 0 || biome.mountainAmplitudeSubvoxels < 0
|
||||
|| biome.detailAmplitudeSubvoxels < 0 || biome.continentWavelengthSubvoxels <= 0
|
||||
|| biome.mountainWavelengthSubvoxels <= 0 || biome.detailWavelengthSubvoxels <= 0)
|
||||
{ error = "world assembly biome profile has an invalid fixed-scale value"; return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum, std::string& error)
|
||||
{
|
||||
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
||||
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
||||
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
||||
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
|
||||
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|
||||
|| !ValidScalar(stratum.capWavelengthSubvoxels))
|
||||
{ error = "world assembly cap contains a value outside the fixed-scale range"; return false; }
|
||||
std::vector<std::uint64_t> ids;
|
||||
ids.reserve(stratum.features.size());
|
||||
for (const auto& feature : stratum.features)
|
||||
{
|
||||
if (feature.stableId == 0 || feature.type < 1 || feature.type > 3
|
||||
|| std::find(ids.begin(), ids.end(), feature.stableId) != ids.end())
|
||||
{ error = "world assembly feature IDs and types must be stable, unique and supported"; return false; }
|
||||
ids.push_back(feature.stableId);
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
if (!ValidScalar(feature.minimumSubvoxels[axis])
|
||||
|| !ValidScalar(feature.maximumSubvoxels[axis])
|
||||
|| feature.minimumSubvoxels[axis] > feature.maximumSubvoxels[axis])
|
||||
{ error = "world assembly feature has an invalid influence bound"; return false; }
|
||||
if (!std::all_of(feature.parametersSubvoxels.begin(), feature.parametersSubvoxels.end(), ValidScalar))
|
||||
{ error = "world assembly feature parameter is outside the fixed-scale range"; return false; }
|
||||
}
|
||||
for (const auto& point : stratum.route)
|
||||
{
|
||||
if (point.stableFeatureId == 0 || point.radiusSubvoxels <= 0
|
||||
|| !ValidScalar(point.radiusSubvoxels)
|
||||
|| !std::all_of(point.positionSubvoxels.begin(), point.positionSubvoxels.end(), ValidScalar))
|
||||
{ error = "world assembly route point has an invalid feature ID, position or radius"; return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashRecipe(const WorldAssemblyRecipeV1& recipe)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, recipe.schemaVersion, 4); HashWord(hash, recipe.evaluatorVersion, 4);
|
||||
HashWord(hash, recipe.unitsPerVoxel, 4); HashWord(hash, recipe.seed, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeCellSizeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpWavelengthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeBorderBlendSubvoxels), 8);
|
||||
HashWord(hash, recipe.biomes.size(), 4);
|
||||
for (const auto& biome : recipe.biomes)
|
||||
{
|
||||
HashWord(hash, biome.stableId, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.baseHeightSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.continentAmplitudeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.continentWavelengthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.mountainAmplitudeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.mountainWavelengthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.detailAmplitudeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(biome.detailWavelengthSubvoxels), 8);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashStratum(const WorldAssemblyStratumPlanV1& stratum)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, static_cast<std::uint32_t>(stratum.strateIndex), 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(stratum.topChunkZ), 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(stratum.bottomChunkZ), 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(stratum.capHeightSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(stratum.capUndulationSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(stratum.capWavelengthSubvoxels), 8);
|
||||
HashWord(hash, stratum.features.size(), 4);
|
||||
for (const auto& feature : stratum.features)
|
||||
{
|
||||
HashWord(hash, feature.stableId, 8); HashWord(hash, feature.type, 4);
|
||||
for (const auto value : feature.minimumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
for (const auto value : feature.maximumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
for (const auto value : feature.parametersSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
}
|
||||
HashWord(hash, stratum.route.size(), 4);
|
||||
for (const auto& point : stratum.route)
|
||||
{
|
||||
HashWord(hash, point.stableFeatureId, 8);
|
||||
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashPlan(const WorldAssemblyPlanV1& plan)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, plan.schemaVersion, 4); HashWord(hash, plan.strates.size(), 4);
|
||||
for (const auto& stratum : plan.strates) HashWord(hash, stratum.contentHash, 8);
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline std::uint64_t HashManifest(const WorldAssemblyManifest& manifest)
|
||||
{
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, manifest.schemaVersion, 4);
|
||||
HashWord(hash, manifest.recipe.contentHash, 8);
|
||||
HashWord(hash, manifest.plan.contentHash, 8);
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
||||
std::string& error)
|
||||
{
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), recipe.schemaVersion)
|
||||
|| !ReadU32(value.Get("evaluator_version"), recipe.evaluatorVersion)
|
||||
|| !ReadU32(value.Get("units_per_voxel"), recipe.unitsPerVoxel)
|
||||
|| !ReadU32(value.Get("seed"), recipe.seed)
|
||||
|| !ReadInteger(value.Get("biome_cell_size_q"), recipe.biomeCellSizeSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_warp_q"), recipe.biomeWarpSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_warp_wavelength_q"), recipe.biomeWarpWavelengthSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_border_blend_q"), recipe.biomeBorderBlendSubvoxels)
|
||||
|| !ReadHash(value.Get("content_hash"), recipe.contentHash)
|
||||
|| !value.Get("biomes").IsArray())
|
||||
{ error = "world assembly recipe is missing a required versioned fixed-scale field"; return false; }
|
||||
for (const auto& item : value.Get("biomes").AsArray())
|
||||
{
|
||||
WorldAssemblyBiomeProfileV1 biome;
|
||||
if (!item.IsObject() || !ReadU32(item.Get("id"), biome.stableId)
|
||||
|| !ReadInteger(item.Get("base_height_q"), biome.baseHeightSubvoxels)
|
||||
|| !ReadInteger(item.Get("continent_amplitude_q"), biome.continentAmplitudeSubvoxels)
|
||||
|| !ReadInteger(item.Get("continent_wavelength_q"), biome.continentWavelengthSubvoxels)
|
||||
|| !ReadInteger(item.Get("mountain_amplitude_q"), biome.mountainAmplitudeSubvoxels)
|
||||
|| !ReadInteger(item.Get("mountain_wavelength_q"), biome.mountainWavelengthSubvoxels)
|
||||
|| !ReadInteger(item.Get("detail_amplitude_q"), biome.detailAmplitudeSubvoxels)
|
||||
|| !ReadInteger(item.Get("detail_wavelength_q"), biome.detailWavelengthSubvoxels))
|
||||
{ error = "world assembly biome profile is missing a required fixed-scale field"; return false; }
|
||||
recipe.biomes.push_back(biome);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ParsePlan(const VFJson::Value& value, WorldAssemblyPlanV1& plan,
|
||||
std::string& error)
|
||||
{
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), plan.schemaVersion)
|
||||
|| !ReadHash(value.Get("content_hash"), plan.contentHash)
|
||||
|| !value.Get("strates").IsArray())
|
||||
{ error = "world assembly plan is missing a required versioned field"; return false; }
|
||||
for (const auto& item : value.Get("strates").AsArray())
|
||||
{
|
||||
WorldAssemblyStratumPlanV1 stratum;
|
||||
if (!item.IsObject() || !ReadI32(item.Get("strate_index"), stratum.strateIndex)
|
||||
|| !ReadI32(item.Get("top_chunk_z"), stratum.topChunkZ)
|
||||
|| !ReadI32(item.Get("bottom_chunk_z"), stratum.bottomChunkZ)
|
||||
|| !ReadInteger(item.Get("cap_height_q"), stratum.capHeightSubvoxels)
|
||||
|| !ReadInteger(item.Get("cap_undulation_q"), stratum.capUndulationSubvoxels)
|
||||
|| !ReadInteger(item.Get("cap_wavelength_q"), stratum.capWavelengthSubvoxels)
|
||||
|| !ReadHash(item.Get("content_hash"), stratum.contentHash)
|
||||
|| !item.Get("features").IsArray() || !item.Get("route").IsArray())
|
||||
{ error = "world assembly stratum is missing a required fixed-scale field"; return false; }
|
||||
for (const auto& featureValue : item.Get("features").AsArray())
|
||||
{
|
||||
WorldAssemblyFeatureV1 feature;
|
||||
std::uint32_t type = 0;
|
||||
if (!featureValue.IsObject() || !ReadHash(featureValue.Get("stable_id"), feature.stableId)
|
||||
|| !ReadU32(featureValue.Get("type"), type)
|
||||
|| !ReadArray(featureValue.Get("min_q"), feature.minimumSubvoxels.data(), 3)
|
||||
|| !ReadArray(featureValue.Get("max_q"), feature.maximumSubvoxels.data(), 3)
|
||||
|| !ReadArray(featureValue.Get("parameters_q"), feature.parametersSubvoxels.data(), 4))
|
||||
{ error = "world assembly feature is missing a fixed-scale field"; return false; }
|
||||
feature.type = type;
|
||||
stratum.features.push_back(feature);
|
||||
}
|
||||
for (const auto& pointValue : item.Get("route").AsArray())
|
||||
{
|
||||
WorldAssemblyRoutePointV1 point;
|
||||
if (!pointValue.IsObject() || !ReadHash(pointValue.Get("feature_id"), point.stableFeatureId)
|
||||
|| !ReadArray(pointValue.Get("position_q"), point.positionSubvoxels.data(), 3)
|
||||
|| !ReadInteger(pointValue.Get("radius_q"), point.radiusSubvoxels))
|
||||
{ error = "world assembly route point is missing a fixed-scale field"; return false; }
|
||||
stratum.route.push_back(point);
|
||||
}
|
||||
plan.strates.push_back(std::move(stratum));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::optional<WorldAssemblyManifest> ParseManifestValue(const VFJson::Value& value,
|
||||
std::string& error)
|
||||
{
|
||||
WorldAssemblyManifest manifest;
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), manifest.schemaVersion)
|
||||
|| !ReadHash(value.Get("content_hash"), manifest.contentHash)
|
||||
|| !ParseRecipe(value.Get("recipe"), manifest.recipe, error)
|
||||
|| !ParsePlan(value.Get("plan"), manifest.plan, error))
|
||||
{
|
||||
if (error.empty()) error = "world assembly manifest is missing a required field";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!manifest.validate(error)) return std::nullopt;
|
||||
return manifest;
|
||||
}
|
||||
|
||||
inline std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor)
|
||||
{
|
||||
const std::int64_t quotient = value / divisor, remainder = value % divisor;
|
||||
return quotient - ((remainder != 0 && remainder < 0) ? 1 : 0);
|
||||
}
|
||||
|
||||
inline std::int64_t FloorMod(std::int64_t value, std::int64_t divisor)
|
||||
{ return value - FloorDiv(value, divisor) * divisor; }
|
||||
|
||||
inline std::uint64_t Mix64(std::uint64_t value)
|
||||
{
|
||||
value += 0x9e3779b97f4a7c15ull;
|
||||
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
|
||||
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
|
||||
return value ^ (value >> 31);
|
||||
}
|
||||
|
||||
inline std::uint64_t CoordinateHash(std::uint32_t seed, std::int64_t x,
|
||||
std::int64_t y, std::uint64_t salt)
|
||||
{
|
||||
std::uint64_t hash = Mix64(static_cast<std::uint64_t>(seed) ^ salt);
|
||||
hash ^= Mix64(static_cast<std::uint64_t>(x) + 0x517cc1b727220a95ull);
|
||||
hash ^= Mix64(static_cast<std::uint64_t>(y) + 0x6eed0e9da4d94a4full);
|
||||
return Mix64(hash);
|
||||
}
|
||||
|
||||
inline std::int32_t LatticeValue(std::uint32_t seed, std::int64_t x,
|
||||
std::int64_t y, std::uint64_t salt)
|
||||
{
|
||||
const std::uint32_t bits = static_cast<std::uint32_t>(CoordinateHash(seed, x, y, salt) >> 48) & 0x1ffffu;
|
||||
return static_cast<std::int32_t>(bits) - 65536;
|
||||
}
|
||||
|
||||
inline std::int32_t LerpQ16(std::int32_t a, std::int32_t b, std::uint32_t t)
|
||||
{ return static_cast<std::int32_t>(a + (static_cast<std::int64_t>(b - a) * t) / Q16); }
|
||||
|
||||
inline std::uint32_t SmoothQ16(std::uint32_t t)
|
||||
{
|
||||
const std::int64_t t2 = (static_cast<std::int64_t>(t) * t) / Q16;
|
||||
return static_cast<std::uint32_t>((t2 * (3 * Q16 - 2 * static_cast<std::int64_t>(t))) / Q16);
|
||||
}
|
||||
|
||||
inline std::int32_t Noise2D(std::int64_t x, std::int64_t y, std::int64_t wavelength,
|
||||
std::uint32_t seed, std::uint64_t salt)
|
||||
{
|
||||
const std::int64_t cellX = FloorDiv(x, wavelength), cellY = FloorDiv(y, wavelength);
|
||||
const std::uint32_t fractionX = static_cast<std::uint32_t>((FloorMod(x, wavelength) * Q16) / wavelength);
|
||||
const std::uint32_t fractionY = static_cast<std::uint32_t>((FloorMod(y, wavelength) * Q16) / wavelength);
|
||||
const std::uint32_t smoothX = SmoothQ16(fractionX), smoothY = SmoothQ16(fractionY);
|
||||
const std::int32_t low = LerpQ16(LatticeValue(seed, cellX, cellY, salt),
|
||||
LatticeValue(seed, cellX + 1, cellY, salt), smoothX);
|
||||
const std::int32_t high = LerpQ16(LatticeValue(seed, cellX, cellY + 1, salt),
|
||||
LatticeValue(seed, cellX + 1, cellY + 1, salt), smoothX);
|
||||
return LerpQ16(low, high, smoothY);
|
||||
}
|
||||
|
||||
inline std::uint32_t RatioQ16(std::uint64_t numerator, std::uint64_t denominator)
|
||||
{
|
||||
if (denominator == 0 || numerator >= denominator) return 65536;
|
||||
std::uint32_t result = 0;
|
||||
std::uint64_t remainder = numerator;
|
||||
for (int bit = 15; bit >= 0; --bit)
|
||||
{
|
||||
remainder *= 2;
|
||||
if (remainder >= denominator)
|
||||
{ remainder -= denominator; result |= (1u << bit); }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::int64_t MulQ16(std::int64_t value, std::int64_t q16)
|
||||
{ return (value * q16) / Q16; }
|
||||
|
||||
inline std::int64_t ProfileHeight(const WorldAssemblyBiomeProfileV1& biome,
|
||||
std::int64_t x, std::int64_t y, std::uint32_t seed)
|
||||
{
|
||||
const std::int64_t continent = MulQ16(biome.continentAmplitudeSubvoxels,
|
||||
Noise2D(x, y, biome.continentWavelengthSubvoxels, seed, 0x434f4e54494e454eull));
|
||||
const std::int64_t mountain = MulQ16(biome.mountainAmplitudeSubvoxels,
|
||||
Noise2D(x, y, biome.mountainWavelengthSubvoxels, seed, 0x4d4f554e5441494eull));
|
||||
const std::int64_t detail = MulQ16(biome.detailAmplitudeSubvoxels,
|
||||
Noise2D(x, y, biome.detailWavelengthSubvoxels, seed, 0x44455441494c3031ull));
|
||||
return biome.baseHeightSubvoxels + continent + mountain + detail;
|
||||
}
|
||||
|
||||
inline bool InInterval(const FieldInterval& interval, std::int64_t density)
|
||||
{ return interval.known && density >= interval.minimumDensitySubvoxels
|
||||
&& density <= interval.maximumDensitySubvoxels; }
|
||||
}
|
||||
|
||||
namespace detail_world_assembly
|
||||
{
|
||||
inline std::optional<WorldAssemblyManifest> ParseNestedManifest(const VFJson::Value& root,
|
||||
std::string& error)
|
||||
{
|
||||
const VFJson::Value& payload = root.Get("world_assembly");
|
||||
if (!payload.IsObject()) return std::nullopt;
|
||||
return world_assembly_detail::ParseManifestValue(payload, error);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
|
||||
std::string_view json, std::string& error)
|
||||
{
|
||||
VFJson::Value root;
|
||||
if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt;
|
||||
return world_assembly_detail::ParseManifestValue(root, error);
|
||||
}
|
||||
|
||||
bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
{
|
||||
if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion
|
||||
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
||||
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
||||
if (plan.strates.empty() || plan.strates.size() > world_assembly_detail::MaxStrateCount)
|
||||
{ error = "world assembly plan must contain one to sixty-four finite strates"; return false; }
|
||||
std::vector<std::int32_t> strateIds;
|
||||
for (auto& stratum : plan.strates)
|
||||
{
|
||||
if (!world_assembly_detail::ValidateStratum(stratum, error)) return false;
|
||||
if (std::find(strateIds.begin(), strateIds.end(), stratum.strateIndex) != strateIds.end())
|
||||
{ error = "world assembly plan contains a duplicate strate index"; return false; }
|
||||
strateIds.push_back(stratum.strateIndex);
|
||||
stratum.contentHash = world_assembly_detail::HashStratum(stratum);
|
||||
}
|
||||
recipe.contentHash = world_assembly_detail::HashRecipe(recipe);
|
||||
plan.contentHash = world_assembly_detail::HashPlan(plan);
|
||||
contentHash = world_assembly_detail::HashManifest(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldAssemblyManifest::validate(std::string& error) const
|
||||
{
|
||||
WorldAssemblyManifest copy = *this;
|
||||
const std::uint64_t expectedRecipe = recipe.contentHash;
|
||||
const std::uint64_t expectedPlan = plan.contentHash;
|
||||
const std::uint64_t expectedManifest = contentHash;
|
||||
std::vector<std::uint64_t> expectedStrates;
|
||||
for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.contentHash);
|
||||
if (!copy.finalize(error)) return false;
|
||||
if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash
|
||||
|| expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size())
|
||||
{ error = "world assembly recipe, plan or manifest content hash does not match its fields"; return false; }
|
||||
for (std::size_t i = 0; i < expectedStrates.size(); ++i)
|
||||
if (expectedStrates[i] != copy.plan.strates[i].contentHash)
|
||||
{ error = "world assembly stratum content hash does not match its fields"; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string WorldAssemblyManifest::ToJson() const
|
||||
{
|
||||
std::string error;
|
||||
WorldAssemblyManifest value = *this;
|
||||
if (!value.finalize(error)) return {};
|
||||
std::ostringstream out;
|
||||
out << "{\"schema_version\":" << value.schemaVersion
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.contentHash)
|
||||
<< "\",\"recipe\":{\"schema_version\":" << value.recipe.schemaVersion
|
||||
<< ",\"evaluator_version\":" << value.recipe.evaluatorVersion
|
||||
<< ",\"units_per_voxel\":" << value.recipe.unitsPerVoxel
|
||||
<< ",\"seed\":" << value.recipe.seed
|
||||
<< ",\"biome_cell_size_q\":" << value.recipe.biomeCellSizeSubvoxels
|
||||
<< ",\"biome_warp_q\":" << value.recipe.biomeWarpSubvoxels
|
||||
<< ",\"biome_warp_wavelength_q\":" << value.recipe.biomeWarpWavelengthSubvoxels
|
||||
<< ",\"biome_border_blend_q\":" << value.recipe.biomeBorderBlendSubvoxels
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.recipe.contentHash)
|
||||
<< "\",\"biomes\":[";
|
||||
for (std::size_t i = 0; i < value.recipe.biomes.size(); ++i)
|
||||
{
|
||||
const auto& biome = value.recipe.biomes[i];
|
||||
if (i) out << ',';
|
||||
out << "{\"id\":" << biome.stableId << ",\"base_height_q\":" << biome.baseHeightSubvoxels
|
||||
<< ",\"continent_amplitude_q\":" << biome.continentAmplitudeSubvoxels
|
||||
<< ",\"continent_wavelength_q\":" << biome.continentWavelengthSubvoxels
|
||||
<< ",\"mountain_amplitude_q\":" << biome.mountainAmplitudeSubvoxels
|
||||
<< ",\"mountain_wavelength_q\":" << biome.mountainWavelengthSubvoxels
|
||||
<< ",\"detail_amplitude_q\":" << biome.detailAmplitudeSubvoxels
|
||||
<< ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels << '}';
|
||||
}
|
||||
out << "]},\"plan\":{\"schema_version\":" << value.plan.schemaVersion
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash)
|
||||
<< "\",\"strates\":[";
|
||||
for (std::size_t i = 0; i < value.plan.strates.size(); ++i)
|
||||
{
|
||||
const auto& stratum = value.plan.strates[i];
|
||||
if (i) out << ',';
|
||||
out << "{\"strate_index\":" << stratum.strateIndex << ",\"top_chunk_z\":" << stratum.topChunkZ
|
||||
<< ",\"bottom_chunk_z\":" << stratum.bottomChunkZ
|
||||
<< ",\"cap_height_q\":" << stratum.capHeightSubvoxels
|
||||
<< ",\"cap_undulation_q\":" << stratum.capUndulationSubvoxels
|
||||
<< ",\"cap_wavelength_q\":" << stratum.capWavelengthSubvoxels
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
||||
<< "\",\"features\":[";
|
||||
for (std::size_t f = 0; f < stratum.features.size(); ++f)
|
||||
{
|
||||
const auto& feature = stratum.features[f];
|
||||
if (f) out << ',';
|
||||
out << "{\"stable_id\":\"" << world_assembly_detail::HashText(feature.stableId)
|
||||
<< "\",\"type\":" << feature.type << ",\"min_q\":["
|
||||
<< feature.minimumSubvoxels[0] << ',' << feature.minimumSubvoxels[1] << ',' << feature.minimumSubvoxels[2]
|
||||
<< "],\"max_q\":[" << feature.maximumSubvoxels[0] << ',' << feature.maximumSubvoxels[1]
|
||||
<< ',' << feature.maximumSubvoxels[2] << "],\"parameters_q\":["
|
||||
<< feature.parametersSubvoxels[0] << ',' << feature.parametersSubvoxels[1] << ','
|
||||
<< feature.parametersSubvoxels[2] << ',' << feature.parametersSubvoxels[3] << "]}";
|
||||
}
|
||||
out << "],\"route\":[";
|
||||
for (std::size_t p = 0; p < stratum.route.size(); ++p)
|
||||
{
|
||||
const auto& point = stratum.route[p];
|
||||
if (p) out << ',';
|
||||
out << "{\"feature_id\":\"" << world_assembly_detail::HashText(point.stableFeatureId)
|
||||
<< "\",\"position_q\":[" << point.positionSubvoxels[0] << ','
|
||||
<< point.positionSubvoxels[1] << ',' << point.positionSubvoxels[2]
|
||||
<< "],\"radius_q\":" << point.radiusSubvoxels << '}';
|
||||
}
|
||||
out << "]}";
|
||||
}
|
||||
out << "]}}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe,
|
||||
const WorldAssemblyStratumPlanV1& plan) noexcept
|
||||
: recipe_(&recipe), plan_(&plan)
|
||||
{}
|
||||
|
||||
bool Heightfield2D::isValid() const noexcept
|
||||
{
|
||||
if (!recipe_ || !plan_ || recipe_->schemaVersion != WorldAssemblySchemaVersion
|
||||
|| recipe_->evaluatorVersion != WorldAssemblyEvaluatorVersion
|
||||
|| recipe_->unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel
|
||||
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
||||
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
||||
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
||||
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
||||
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
||||
return false;
|
||||
return std::all_of(recipe_->biomes.begin(), recipe_->biomes.end(), [](const auto& biome)
|
||||
{
|
||||
return biome.stableId != 0 && biome.continentAmplitudeSubvoxels >= 0
|
||||
&& biome.mountainAmplitudeSubvoxels >= 0 && biome.detailAmplitudeSubvoxels >= 0
|
||||
&& biome.continentWavelengthSubvoxels > 0 && biome.mountainWavelengthSubvoxels > 0
|
||||
&& biome.detailWavelengthSubvoxels > 0;
|
||||
});
|
||||
}
|
||||
|
||||
HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y,
|
||||
std::int64_t z) const noexcept
|
||||
{
|
||||
HeightfieldPointSample out;
|
||||
if (!isValid() || world_assembly_detail::OutsideRange(x)
|
||||
|| world_assembly_detail::OutsideRange(y)
|
||||
|| world_assembly_detail::OutsideRange(z)) return out;
|
||||
using namespace world_assembly_detail;
|
||||
const std::int64_t warpX = MulQ16(recipe_->biomeWarpSubvoxels,
|
||||
Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f585f31ull));
|
||||
const std::int64_t warpY = MulQ16(recipe_->biomeWarpSubvoxels,
|
||||
Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f595f31ull));
|
||||
const std::int64_t warpedX = x + warpX, warpedY = y + warpY;
|
||||
const std::int64_t cellX = FloorDiv(warpedX, recipe_->biomeCellSizeSubvoxels);
|
||||
const std::int64_t cellY = FloorDiv(warpedY, recipe_->biomeCellSizeSubvoxels);
|
||||
std::uint64_t nearestDistance = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t secondDistance = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint32_t nearestId = 0, secondId = 0;
|
||||
const std::uint32_t jitterSeed = recipe_->seed ^ 0xa341316cu;
|
||||
const std::int64_t cellSize = recipe_->biomeCellSizeSubvoxels;
|
||||
for (std::int64_t cy = cellY - 2; cy <= cellY + 2; ++cy)
|
||||
for (std::int64_t cx = cellX - 2; cx <= cellX + 2; ++cx)
|
||||
{
|
||||
const std::uint64_t h = CoordinateHash(jitterSeed, cx, cy, 0x42494f4d455f5349ull);
|
||||
const std::int64_t jitterX = static_cast<std::int64_t>((h & 0xffffu)) * (cellSize / 2) / 65535 - cellSize / 4;
|
||||
const std::int64_t jitterY = static_cast<std::int64_t>(((h >> 16) & 0xffffu)) * (cellSize / 2) / 65535 - cellSize / 4;
|
||||
const std::int64_t siteX = cx * cellSize + cellSize / 2 + jitterX;
|
||||
const std::int64_t siteY = cy * cellSize + cellSize / 2 + jitterY;
|
||||
const std::int64_t dx = warpedX - siteX, dy = warpedY - siteY;
|
||||
const std::uint64_t distance = static_cast<std::uint64_t>(dx * dx + dy * dy);
|
||||
const std::uint32_t biomeId = recipe_->biomes[CoordinateHash(jitterSeed, cx, cy,
|
||||
0x42494f4d455f4944ull) % recipe_->biomes.size()].stableId;
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
secondDistance = nearestDistance; secondId = nearestId;
|
||||
nearestDistance = distance; nearestId = biomeId;
|
||||
}
|
||||
else if (distance < secondDistance && biomeId != nearestId)
|
||||
{ secondDistance = distance; secondId = biomeId; }
|
||||
}
|
||||
const auto findBiome = [&](std::uint32_t id) -> const WorldAssemblyBiomeProfileV1*
|
||||
{
|
||||
const auto found = std::find_if(recipe_->biomes.begin(), recipe_->biomes.end(),
|
||||
[id](const auto& biome) { return biome.stableId == id; });
|
||||
return found == recipe_->biomes.end() ? nullptr : &*found;
|
||||
};
|
||||
const auto* primary = findBiome(nearestId);
|
||||
const auto* secondary = findBiome(secondId ? secondId : nearestId);
|
||||
if (!primary || !secondary) return out;
|
||||
std::uint32_t primaryWeight = 65536;
|
||||
if (recipe_->biomeBorderBlendSubvoxels > 0 && nearestDistance < secondDistance)
|
||||
{
|
||||
const std::uint64_t difference = secondDistance - nearestDistance;
|
||||
const std::uint64_t denominator = static_cast<std::uint64_t>(2 * cellSize)
|
||||
* static_cast<std::uint64_t>(recipe_->biomeBorderBlendSubvoxels);
|
||||
primaryWeight = SmoothQ16(RatioQ16(difference, denominator));
|
||||
}
|
||||
const std::int64_t primaryHeight = ProfileHeight(*primary, warpedX, warpedY, recipe_->seed);
|
||||
const std::int64_t secondaryHeight = ProfileHeight(*secondary, warpedX, warpedY, recipe_->seed);
|
||||
out.groundHeightSubvoxels = (primaryHeight * primaryWeight
|
||||
+ secondaryHeight * (65536 - primaryWeight)) / 65536;
|
||||
out.biomeId = primary->stableId;
|
||||
out.neighborBiomeId = secondary->stableId;
|
||||
out.biomeWeightQ16 = primaryWeight;
|
||||
out.capHeightSubvoxels = plan_->capHeightSubvoxels + MulQ16(plan_->capUndulationSubvoxels,
|
||||
Noise2D(x, y, plan_->capWavelengthSubvoxels, recipe_->seed, 0x4341505f554e4431ull));
|
||||
out.densitySubvoxels = std::min(z - out.groundHeightSubvoxels,
|
||||
out.capHeightSubvoxels - z);
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY,
|
||||
std::int64_t minZ, std::int64_t maxX, std::int64_t maxY, std::int64_t maxZ) const noexcept
|
||||
{
|
||||
FieldInterval out;
|
||||
if (!isValid() || minX > maxX || minY > maxY || minZ > maxZ
|
||||
|| world_assembly_detail::OutsideRange(minX)
|
||||
|| world_assembly_detail::OutsideRange(minY)
|
||||
|| world_assembly_detail::OutsideRange(minZ)
|
||||
|| world_assembly_detail::OutsideRange(maxX)
|
||||
|| world_assembly_detail::OutsideRange(maxY)
|
||||
|| world_assembly_detail::OutsideRange(maxZ)) return out;
|
||||
std::int64_t groundMin = std::numeric_limits<std::int64_t>::max();
|
||||
std::int64_t groundMax = std::numeric_limits<std::int64_t>::min();
|
||||
for (const auto& biome : recipe_->biomes)
|
||||
{
|
||||
const std::int64_t amplitude = biome.continentAmplitudeSubvoxels
|
||||
+ biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels;
|
||||
groundMin = std::min(groundMin, biome.baseHeightSubvoxels - amplitude);
|
||||
groundMax = std::max(groundMax, biome.baseHeightSubvoxels + amplitude);
|
||||
}
|
||||
const std::int64_t capMin = plan_->capHeightSubvoxels - plan_->capUndulationSubvoxels;
|
||||
const std::int64_t capMax = plan_->capHeightSubvoxels + plan_->capUndulationSubvoxels;
|
||||
const std::int64_t groundLower = minZ - groundMax;
|
||||
const std::int64_t groundUpper = maxZ - groundMin;
|
||||
const std::int64_t capLower = capMin - maxZ;
|
||||
const std::int64_t capUpper = capMax - minZ;
|
||||
out.known = true;
|
||||
out.minimumDensitySubvoxels = std::min(groundLower, capLower);
|
||||
out.maximumDensitySubvoxels = std::min(groundUpper, capUpper);
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldInfluenceBounds Heightfield2D::influenceBounds() const noexcept
|
||||
{
|
||||
FieldInfluenceBounds out;
|
||||
if (!isValid()) return out;
|
||||
out.minimumZSubvoxels = static_cast<std::int64_t>(plan_->bottomChunkZ) * 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
out.maximumZSubvoxels = (static_cast<std::int64_t>(plan_->topChunkZ) + 1) * 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
|
||||
{
|
||||
FieldDetailEnvelope out;
|
||||
if (!isValid()) return out;
|
||||
std::int64_t maximum = plan_->capUndulationSubvoxels;
|
||||
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
||||
for (const auto& biome : recipe_->biomes)
|
||||
{
|
||||
maximum = std::max(maximum, biome.continentAmplitudeSubvoxels
|
||||
+ biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels);
|
||||
minimumFeature = std::min({minimumFeature, biome.continentWavelengthSubvoxels,
|
||||
biome.mountainWavelengthSubvoxels, biome.detailWavelengthSubvoxels});
|
||||
}
|
||||
out.maximumAbsoluteDisplacementSubvoxels = maximum;
|
||||
out.minimumFeatureSizeSubvoxels = std::max<std::int64_t>(1,
|
||||
std::min(minimumFeature, recipe_->biomeBorderBlendSubvoxels > 0
|
||||
? recipe_->biomeBorderBlendSubvoxels : minimumFeature));
|
||||
return out;
|
||||
}
|
||||
|
||||
FieldBlockContract Heightfield2D::contract() const noexcept
|
||||
{
|
||||
return {ChannelBit(FieldChannel::biome),
|
||||
ChannelBit(FieldChannel::height) | ChannelBit(FieldChannel::rock),
|
||||
WorldAssemblyEvaluatorVersion};
|
||||
}
|
||||
|
||||
WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block,
|
||||
std::uint32_t boxCount, std::uint64_t seed)
|
||||
{
|
||||
WorldAssemblyBoundsAuditResult result;
|
||||
if (!block.isValid()) return result;
|
||||
boxCount = std::clamp<std::uint32_t>(boxCount, 1, 100000);
|
||||
const auto random = [&seed]()
|
||||
{
|
||||
seed = world_assembly_detail::Mix64(seed);
|
||||
return seed;
|
||||
};
|
||||
const auto signedVoxel = [&random]()
|
||||
{ return static_cast<std::int64_t>(random() % 12001) - 6000; };
|
||||
for (std::uint32_t i = 0; i < boxCount; ++i)
|
||||
{
|
||||
const std::int64_t x = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t y = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
|
||||
const auto column = block.point(x, y, 0);
|
||||
const std::int64_t z = column.groundHeightSubvoxels
|
||||
+ (static_cast<std::int64_t>(random() % 257) - 128) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfX = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfY = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfZ = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
|
||||
const FieldInterval bounds = block.interval(x - halfX, y - halfY, z - halfZ,
|
||||
x + halfX, y + halfY, z + halfZ);
|
||||
++result.boxesTested;
|
||||
if (!bounds.known) { ++result.unknownBounds; continue; }
|
||||
for (int iz = 0; iz <= 2; ++iz)
|
||||
for (int iy = 0; iy <= 2; ++iy)
|
||||
for (int ix = 0; ix <= 2; ++ix)
|
||||
{
|
||||
const std::int64_t px = x - halfX + (2 * halfX * ix) / 2;
|
||||
const std::int64_t py = y - halfY + (2 * halfY * iy) / 2;
|
||||
const std::int64_t pz = z - halfZ + (2 * halfZ * iz) / 2;
|
||||
const auto sample = block.point(px, py, pz);
|
||||
++result.pointSamples;
|
||||
if (!world_assembly_detail::InInterval(bounds, sample.densitySubvoxels))
|
||||
++result.missedBounds;
|
||||
}
|
||||
}
|
||||
const auto ground = block.point(0, 0, 0).groundHeightSubvoxels;
|
||||
const auto controlPoint = block.point(0, 0, ground + 17).densitySubvoxels;
|
||||
// Negative control: this fabricated one-sided range excludes its known sample. The same
|
||||
// containment check used by the brute-force audit must reject the deliberate lie.
|
||||
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
||||
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
||||
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
||||
result.passed = result.boxesTested == boxCount && result.missedBounds == 0
|
||||
&& result.unknownBounds == 0 && result.negativeControlDetected;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user