13constexpr std::size_t kMaximumSnapshotBytes = 128U * 1024U * 1024U;
14constexpr std::uint32_t kMaximumRules = 4096;
15constexpr std::uint32_t kMaximumInstances = 65536;
20 void scalar(T
value) {
21 if constexpr (std::is_enum_v<T>) {
22 using Raw = std::underlying_type_t<T>;
23 scalar(
static_cast<Raw
>(
value));
24 }
else if constexpr (std::is_same_v<T, bool>) {
25 scalar(
static_cast<std::uint8_t
>(
value));
26 }
else if constexpr (std::is_same_v<T, float>) {
27 scalar(std::bit_cast<std::uint32_t>(
value));
28 }
else if constexpr (std::is_same_v<T, double>) {
29 scalar(std::bit_cast<std::uint64_t>(
value));
31 using Unsigned = std::make_unsigned_t<T>;
32 Unsigned bits =
static_cast<Unsigned
>(
value);
33 for (std::size_t i = 0; i <
sizeof(T); ++i)
34 bytes.push_back(
static_cast<char>((bits >> (i * 8)) & 0xffU));
38 scalar(
static_cast<std::uint32_t
>(
value.size()));
44 std::string_view
bytes;
47 bool scalar(T&
value) {
48 if constexpr (std::is_enum_v<T>) {
49 std::underlying_type_t<T> raw{};
50 if (!scalar(raw))
return false;
51 value =
static_cast<T
>(raw);
53 }
else if constexpr (std::is_same_v<T, bool>) {
55 if (!scalar(raw) || raw > 1)
return false;
58 }
else if constexpr (std::is_same_v<T, float>) {
60 if (!scalar(raw))
return false;
61 value = std::bit_cast<float>(raw);
62 return std::isfinite(
value);
63 }
else if constexpr (std::is_same_v<T, double>) {
65 if (!scalar(raw))
return false;
66 value = std::bit_cast<double>(raw);
67 return std::isfinite(
value);
69 if (
bytes.size() -
offset <
sizeof(T))
return false;
70 using Unsigned = std::make_unsigned_t<T>;
72 for (std::size_t i = 0; i <
sizeof(T); ++i)
73 bits |=
static_cast<Unsigned
>(
static_cast<unsigned char>(
bytes[
offset++])) << (i * 8);
74 value =
static_cast<T
>(bits);
80 if (!scalar(
size) ||
size >
bytes.size() - offset)
return false;
87std::string hexEncode(std::string_view
bytes) {
88 static constexpr char digits[] =
"0123456789abcdef";
90 result.reserve(
bytes.size() * 2);
91 for (
unsigned char byte :
bytes) {
92 result.push_back(digits[
byte >> 4]);
93 result.push_back(digits[
byte & 15]);
98Result<std::string> hexDecode(std::string_view
text) {
99 if ((
text.size() & 1U) != 0 ||
text.size() / 2 > kMaximumSnapshotBytes)
102 auto nibble = [](
char c) ->
int {
103 if (
c >=
'0' &&
c <=
'9')
return c -
'0';
104 if (
c >=
'a' &&
c <=
'f')
return c -
'a' + 10;
107 std::string result(
text.size() / 2,
'\0');
108 for (std::size_t i = 0; i < result.size(); ++i) {
109 const int high = nibble(
text[i * 2]), low = nibble(
text[i * 2 + 1]);
110 if (high < 0 || low < 0)
113 result[i] =
static_cast<char>((high << 4) | low);
118#define STAMP_FIELDS(X) X(originX) X(originZ) X(spacingX) X(spacingZ) X(centerX) X(centerZ) X(width) X(depth) X(rotation) X(amplitude) X(baseHeight) X(blendStrength) X(operation)
119#define DETAIL_FIELDS(X) X(minimumFitness) X(fadeStart) X(density) X(namespaceId)
120#define TREE_FIELDS(X) X(originX) X(originZ) X(width) X(depth) X(heightScale) X(spacing) X(spawnDensity) X(jitterPercent) X(failureRate) X(minimumFitness) X(snapToTerrain) X(yOffsetMode) X(seaLevel) X(customOffset) X(minimumYOffset) X(maximumYOffset) X(scaleMode) X(minimumWidth) X(maximumWidth) X(minimumHeight) X(maximumHeight) X(widthRandomPercentage) X(heightRandomPercentage) X(healthyR) X(healthyG) X(healthyB) X(healthyA) X(dryR) X(dryG) X(dryB) X(dryA) X(bendFactor) X(boundsRadius) X(seed) X(namespaceId) X(maxPoints)
121#define OBJECT_FIELDS(X) X(originX) X(originZ) X(width) X(depth) X(heightScale) X(spacing) X(spawnDensity) X(jitterPercent) X(startOffsetX) X(startOffsetZ) X(failureRate) X(minimumFitness) X(minimumInstanceFitness) X(minimumDirection) X(maximumDirection) X(boundsRadius) X(boundsCheckQuality) X(prototypeScale) X(boundsCollisionCheck) X(seaLevel) X(seed) X(namespaceId) X(maxPoints)
122#define PROBE_FIELDS(X) X(originX) X(originZ) X(width) X(depth) X(heightScale) X(spacing) X(jitterPercent) X(minimumFitness) X(seaLevelActive) X(seaLevel) X(reflectionOffset) X(lightOffset) X(reflectionResolution) X(reflectionClipDistance) X(reflectionShadowDistance) X(seed) X(namespaceId) X(maxPoints)
123#define INSTANCE_FIELDS(X) X(minimumInstances) X(maximumInstances) X(failureRate) X(minimumOffsetX) X(maximumOffsetX) X(minimumOffsetY) X(maximumOffsetY) X(minimumOffsetZ) X(maximumOffsetZ) X(yOffsetMode) X(customOffset) X(scaleMode) X(commonScale) X(minimumScale) X(maximumScale) X(minimumScaleX) X(maximumScaleX) X(minimumScaleY) X(maximumScaleY) X(minimumScaleZ) X(maximumScaleZ) X(scaleRandomPercentage) X(scaleRandomPercentageX) X(scaleRandomPercentageY) X(scaleRandomPercentageZ) X(minimumRotationX) X(maximumRotationX) X(minimumRotationY) X(maximumRotationY) X(minimumRotationZ) X(maximumRotationZ) X(yOffsetAlongSlope) X(alignForwardToSlope) X(rotateToSlope)
125#define WRITE_MEMBER(name) writer.scalar(value.name);
126#define READ_MEMBER(name) if (!reader.scalar(value.name)) return false;
132void writeTree(Writer& writer,
const TerrainTreePlacementSettings&
value) {
134 writer.text(
value.asset);
136bool readTree(Reader& reader, TerrainTreePlacementSettings&
value) {
138 return reader.text(
value.asset);
140void writeInstance(Writer& writer,
const TerrainObjectInstanceSettings&
value) {
141 writer.text(
value.asset);
144bool readInstance(Reader& reader, TerrainObjectInstanceSettings&
value) {
145 if (!reader.text(
value.asset))
return false;
149void writeObject(Writer& writer,
const TerrainObjectPlacementSettings&
value) {
151 writer.text(
value.prototype);
152 writer.scalar(
static_cast<std::uint32_t
>(
value.instances.size()));
153 for (
const auto& instance :
value.
instances) writeInstance(writer, instance);
155bool readObject(Reader& reader, TerrainObjectPlacementSettings&
value) {
157 if (!reader.text(
value.prototype))
return false;
158 std::uint32_t
count{};
159 if (!reader.scalar(
count) ||
count > kMaximumInstances)
return false;
162 if (!readInstance(reader, instance)) return false;
165void writeProbe(Writer& writer,
const TerrainProbePlacementSettings&
value) {
166 writer.text(
value.name);
167 writer.scalar(
value.type);
170bool readProbe(Reader& reader, TerrainProbePlacementSettings&
value) {
171 if (!reader.text(
value.name) || !reader.scalar(
value.type))
return false;
175void writeHeightmap(Writer& writer,
const Heightmap&
value) {
176 writer.scalar(
value.getWidth());
177 writer.scalar(
value.getHeight());
180bool readHeightmap(Reader& reader, Heightmap&
value) {
182 if (!reader.scalar(
width) || !reader.scalar(
height) ||
width <= 0 || height <= 0 || width > INT_MAX /
height)
185 for (
float&
sample : candidate.data())
186 if (!reader.scalar(
sample)) return false;
187 value = std::move(candidate);
190bool validEnums(
const TerrainDetailSpawnRule& rule) {
194bool validEnums(
const TerrainTreeSpawnRule& rule) {
201bool validEnums(
const TerrainObjectSpawnRule& rule) {
211bool validEnums(
const TerrainProbeSpawnRule& rule) {
215bool validEnums(
const TerrainSplatSpawnRule& rule) {
return rule.targetLayer >= 0; }
216bool validEnums(
const TerrainModifierStampSpawnRule& rule) {
221 return std::visit([](
const auto& rule) {
222 if (rule.ruleId.size() > std::numeric_limits<std::uint32_t>::max())
return false;
223 using Rule = std::decay_t<
decltype(rule)>;
224 if constexpr (std::is_same_v<Rule, TerrainSplatSpawnRule> ||
225 std::is_same_v<Rule, TerrainDetailSpawnRule> ||
226 std::is_same_v<Rule, TerrainModifierStampSpawnRule>) {
228 }
else if constexpr (std::is_same_v<Rule, TerrainTreeSpawnRule>) {
229 return rule.settings.asset.size() <= std::numeric_limits<std::uint32_t>::max();
230 }
else if constexpr (std::is_same_v<Rule, TerrainProbeSpawnRule>) {
231 return rule.settings.name.size() <= std::numeric_limits<std::uint32_t>::max();
233 if (rule.settings.prototype.size() > std::numeric_limits<std::uint32_t>::max() ||
234 rule.settings.instances.size() > kMaximumInstances)
236 return std::all_of(rule.settings.instances.begin(), rule.settings.instances.end(), [](
const auto& item) {
237 return item.asset.size() <= std::numeric_limits<std::uint32_t>::max();
244 std::visit([&](
const auto& rule) {
245 using Rule = std::decay_t<
decltype(rule)>;
246 if constexpr (std::is_same_v<Rule, TerrainDetailSpawnRule>) writer.scalar(std::uint8_t{0});
247 else if constexpr (std::is_same_v<Rule, TerrainTreeSpawnRule>) writer.scalar(std::uint8_t{1});
248 else if constexpr (std::is_same_v<Rule, TerrainObjectSpawnRule>) writer.scalar(std::uint8_t{2});
249 else if constexpr (std::is_same_v<Rule, TerrainSplatSpawnRule>) writer.scalar(std::uint8_t{3});
250 else if constexpr (std::is_same_v<Rule, TerrainModifierStampSpawnRule>) writer.scalar(std::uint8_t{4});
251 else writer.scalar(std::uint8_t{5});
252 writer.text(rule.ruleId);
253 writer.scalar(rule.enabled);
254 if constexpr (std::is_same_v<Rule, TerrainSplatSpawnRule>) {
255 writeHeightmap(writer, rule.paint);
256 }
else if constexpr (std::is_same_v<Rule, TerrainModifierStampSpawnRule>) {
257 writeHeightmap(writer, rule.stamp);
258 writeHeightmap(writer, rule.localMask);
259 writeHeightmap(writer, rule.globalMask);
261 writeHeightmap(writer, rule.fitness);
263 writeStamp(writer, rule.operation);
264 if constexpr (std::is_same_v<Rule, TerrainSplatSpawnRule>) {
265 writer.scalar(rule.targetLayer);
266 }
else if constexpr (std::is_same_v<Rule, TerrainDetailSpawnRule>) {
267 writer.scalar(rule.mode);
268 writeDetail(writer, rule.settings);
269 writer.scalar(rule.seed);
270 }
else if constexpr (std::is_same_v<Rule, TerrainTreeSpawnRule>) {
271 writer.scalar(rule.mode);
272 writeTree(writer, rule.settings);
273 }
else if constexpr (std::is_same_v<Rule, TerrainObjectSpawnRule>) {
274 writer.scalar(rule.mode);
275 writeObject(writer, rule.settings);
276 }
else if constexpr (std::is_same_v<Rule, TerrainProbeSpawnRule>) {
277 writer.scalar(rule.mode);
278 writeProbe(writer, rule.settings);
283bool readRule(Reader& reader,
int version, TerrainSpawnRule&
output) {
289 const std::uint8_t maximumKind = version >= 3 ? 5 : (version >= 2 ? 4 : 3);
290 if (!reader.scalar(
kind) ||
kind > maximumKind || !reader.text(
id) ||
id.empty() ||
291 (version >= 1 && !reader.scalar(
enabled)) || !readHeightmap(reader, fitness))
293 Heightmap localMask, globalMask;
294 if (
kind == 4 && (!readHeightmap(reader, localMask) || !readHeightmap(reader, globalMask)))
return false;
295 if (!readStamp(reader,
operation))
return false;
297 TerrainSplatSpawnRule rule;
298 rule.ruleId = std::move(
id); rule.enabled =
enabled; rule.paint = std::move(fitness); rule.operation =
operation;
299 if (!reader.scalar(rule.targetLayer) || !validEnums(rule))
return false;
301 }
else if (
kind == 0) {
302 TerrainDetailSpawnRule rule;
303 rule.ruleId = std::move(
id); rule.enabled =
enabled; rule.fitness = std::move(fitness); rule.operation =
operation;
304 if (!reader.scalar(rule.mode) || !readDetail(reader, rule.settings) || !reader.scalar(rule.seed) || !validEnums(rule))
return false;
306 }
else if (
kind == 1) {
307 TerrainTreeSpawnRule rule;
308 rule.ruleId = std::move(
id); rule.enabled =
enabled; rule.fitness = std::move(fitness); rule.operation =
operation;
309 if (!reader.scalar(rule.mode) || !readTree(reader, rule.settings) || !validEnums(rule))
return false;
311 }
else if (
kind == 2) {
312 TerrainObjectSpawnRule rule;
313 rule.ruleId = std::move(
id); rule.enabled =
enabled; rule.fitness = std::move(fitness); rule.operation =
operation;
314 if (!reader.scalar(rule.mode) || !readObject(reader, rule.settings) || !validEnums(rule))
return false;
316 }
else if (
kind == 4) {
317 TerrainModifierStampSpawnRule rule;
318 rule.ruleId = std::move(
id); rule.enabled =
enabled; rule.stamp = std::move(fitness);
319 rule.localMask = std::move(localMask); rule.globalMask = std::move(globalMask); rule.operation =
operation;
320 if (!validEnums(rule))
return false;
323 TerrainProbeSpawnRule rule;
324 rule.ruleId = std::move(
id); rule.enabled =
enabled; rule.fitness = std::move(fitness);
326 if (!reader.scalar(rule.mode) || !readProbe(reader, rule.settings) || !validEnums(rule))
return false;
339#undef INSTANCE_FIELDS
343 if (rules_.size() > kMaximumRules)
345 Diagnostic::error(DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.snapshot: too many rules"));
346 if (!std::all_of(rules_.begin(), rules_.end(), stringsFit))
348 DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.snapshot: string or instance count exceeds limit"));
350 writer.scalar(
static_cast<std::uint32_t
>(rules_.size()));
351 for (
const auto& rule : rules_) writeRule(writer, rule);
352 if (writer.bytes.size() > kMaximumSnapshotBytes)
354 DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.snapshot: payload exceeds size limit"));
355 Value::Object root{{
"payload", hexEncode(writer.bytes)}, {
"schema",
"eve.procgen.terrain-spawn-plan"},
361 if (json.size() > kMaximumSnapshotBytes * 2 + 1024)
363 Diagnostic::error(DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.restore: JSON exceeds size limit"));
364 auto decoded = Value::fromJson(json);
367 if (!
root ||
root->size() != 3 || !
root->contains(
"schema") || !
root->contains(
"version") ||
368 !
root->contains(
"payload"))
370 "terrain.spawnPlan.restore: missing or unknown root fields"));
371 const auto* schema =
root->at(
"schema").getIf<std::string>();
372 const auto* version =
root->at(
"version").getIf<std::int64_t>();
373 const auto*
payload =
root->at(
"payload").getIf<std::string>();
374 if (!schema || *schema !=
"eve.procgen.terrain-spawn-plan" || !version ||
375 (*version != 0 && *version != 1 && *version != 2 && *version != 3) ||
378 "terrain.spawnPlan.restore: unsupported schema or version"));
381 Reader reader{
bytes.value()};
382 std::uint32_t
count{};
383 if (!reader.scalar(
count) ||
count > kMaximumRules)
385 Diagnostic::error(DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.restore: invalid rule count"));
387 candidate.rules_.reserve(
count);
388 for (std::uint32_t i = 0; i <
count; ++i) {
390 if (!readRule(reader,
static_cast<int>(*version), rule))
392 Diagnostic::error(DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.restore: invalid rule payload"));
393 const auto id = std::visit([](
const auto&
value) {
return value.ruleId; }, rule);
394 if (candidate.contains(
id))
396 Diagnostic::error(DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.restore: duplicate rule ID"));
397 candidate.rules_.push_back(std::move(rule));
399 if (reader.offset != reader.bytes.size())
401 Diagnostic::error(DiagnosticCode::InvalidArgument,
"terrain.spawnPlan.restore: trailing payload data"));
402 rules_.swap(candidate.rules_);
ActionParameterOperation operation
std::vector< BuildingInstanceSnapshot > instances
TerrainThermalSettings settings
#define INSTANCE_FIELDS(X)
#define READ_MEMBER(name)
#define WRITE_MEMBER(name)
Owning, renderer-independent dynamic values.
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
The canonical owning dynamic value used by data-facing protocols.
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
std::map< std::string, Value > Object
Own an ordered mixed-domain Pcg-style spawn recipe. Rule IDs are stable and unique....
double sample(const Heightmap &map, double u, double v)
Sample.
TerrainObjectScaleMode
Pcg-compatible scale policy for one terrain object instance resource.
TerrainObjectYOffsetMode
Height reference used by one terrain object instance resource.
std::variant< TerrainSplatSpawnRule, TerrainDetailSpawnRule, TerrainTreeSpawnRule, TerrainObjectSpawnRule, TerrainProbeSpawnRule, TerrainModifierStampSpawnRule > TerrainSpawnRule