载入中...
搜索中...
未找到
TerrainSpawnPlanCodec.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <bit>
5#include <cmath>
6#include <limits>
7#include <type_traits>
8
9#include "common/Value.h"
10
11namespace eve::procgen {
12namespace {
13constexpr std::size_t kMaximumSnapshotBytes = 128U * 1024U * 1024U;
14constexpr std::uint32_t kMaximumRules = 4096;
15constexpr std::uint32_t kMaximumInstances = 65536;
16
17struct Writer {
18 std::string bytes;
19 template <class T>
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));
30 } else {
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));
35 }
36 }
37 void text(const std::string& value) {
38 scalar(static_cast<std::uint32_t>(value.size()));
39 bytes.append(value);
40 }
41};
42
43struct Reader {
44 std::string_view bytes;
45 std::size_t offset = 0;
46 template <class T>
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);
52 return true;
53 } else if constexpr (std::is_same_v<T, bool>) {
54 std::uint8_t raw{};
55 if (!scalar(raw) || raw > 1) return false;
56 value = raw != 0;
57 return true;
58 } else if constexpr (std::is_same_v<T, float>) {
59 std::uint32_t raw{};
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>) {
64 std::uint64_t raw{};
65 if (!scalar(raw)) return false;
66 value = std::bit_cast<double>(raw);
67 return std::isfinite(value);
68 } else {
69 if (bytes.size() - offset < sizeof(T)) return false;
70 using Unsigned = std::make_unsigned_t<T>;
71 Unsigned bits = 0;
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);
75 return true;
76 }
77 }
78 bool text(std::string& value) {
79 std::uint32_t size{};
80 if (!scalar(size) || size > bytes.size() - offset) return false;
81 value.assign(bytes.substr(offset, size));
82 offset += size;
83 return true;
84 }
85};
86
87std::string hexEncode(std::string_view bytes) {
88 static constexpr char digits[] = "0123456789abcdef";
89 std::string result;
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]);
94 }
95 return result;
96}
97
98Result<std::string> hexDecode(std::string_view text) {
99 if ((text.size() & 1U) != 0 || text.size() / 2 > kMaximumSnapshotBytes)
101 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.spawnPlan.restore: invalid payload size"));
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;
105 return -1;
106 };
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)
112 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.spawnPlan.restore: invalid payload hex"));
113 result[i] = static_cast<char>((high << 4) | low);
114 }
115 return Result<std::string>::success(std::move(result));
116}
117
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)
124
125#define WRITE_MEMBER(name) writer.scalar(value.name);
126#define READ_MEMBER(name) if (!reader.scalar(value.name)) return false;
127
128void writeStamp(Writer& writer, const TerrainStampSettings& value) { STAMP_FIELDS(WRITE_MEMBER) }
129bool readStamp(Reader& reader, TerrainStampSettings& value) { STAMP_FIELDS(READ_MEMBER) return true; }
130void writeDetail(Writer& writer, const TerrainDetailSettings& value) { DETAIL_FIELDS(WRITE_MEMBER) }
131bool readDetail(Reader& reader, TerrainDetailSettings& value) { DETAIL_FIELDS(READ_MEMBER) return true; }
132void writeTree(Writer& writer, const TerrainTreePlacementSettings& value) {
134 writer.text(value.asset);
135}
136bool readTree(Reader& reader, TerrainTreePlacementSettings& value) {
138 return reader.text(value.asset);
139}
140void writeInstance(Writer& writer, const TerrainObjectInstanceSettings& value) {
141 writer.text(value.asset);
143}
144bool readInstance(Reader& reader, TerrainObjectInstanceSettings& value) {
145 if (!reader.text(value.asset)) return false;
147 return true;
148}
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);
154}
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;
160 value.instances.resize(count);
161 for (auto& instance : value.instances)
162 if (!readInstance(reader, instance)) return false;
163 return true;
164}
165void writeProbe(Writer& writer, const TerrainProbePlacementSettings& value) {
166 writer.text(value.name);
167 writer.scalar(value.type);
169}
170bool readProbe(Reader& reader, TerrainProbePlacementSettings& value) {
171 if (!reader.text(value.name) || !reader.scalar(value.type)) return false;
173 return true;
174}
175void writeHeightmap(Writer& writer, const Heightmap& value) {
176 writer.scalar(value.getWidth());
177 writer.scalar(value.getHeight());
178 for (float sample : value.data()) writer.scalar(sample);
179}
180bool readHeightmap(Reader& reader, Heightmap& value) {
181 int width{}, height{};
182 if (!reader.scalar(width) || !reader.scalar(height) || width <= 0 || height <= 0 || width > INT_MAX / height)
183 return false;
184 Heightmap candidate(width, height);
185 for (float& sample : candidate.data())
186 if (!reader.scalar(sample)) return false;
187 value = std::move(candidate);
188 return true;
189}
190bool validEnums(const TerrainDetailSpawnRule& rule) {
191 return rule.mode >= TerrainDetailMode::Replace && rule.mode <= TerrainDetailMode::Remove &&
192 rule.settings.mode >= TerrainDetailMode::Replace && rule.settings.mode <= TerrainDetailMode::Remove;
193}
194bool validEnums(const TerrainTreeSpawnRule& rule) {
195 return rule.mode >= TerrainTreeOperationMode::Add && rule.mode <= TerrainTreeOperationMode::Remove &&
196 rule.settings.yOffsetMode >= TerrainTreeYOffsetMode::TerrainHeight &&
197 rule.settings.yOffsetMode <= TerrainTreeYOffsetMode::Custom &&
198 rule.settings.scaleMode >= TerrainTreeScaleMode::Fixed &&
199 rule.settings.scaleMode <= TerrainTreeScaleMode::FitnessRandomized;
200}
201bool validEnums(const TerrainObjectSpawnRule& rule) {
202 if (rule.mode < TerrainObjectOperationMode::Add || rule.mode > TerrainObjectOperationMode::Remove) return false;
203 for (const auto& instance : rule.settings.instances)
204 if (instance.yOffsetMode < TerrainObjectYOffsetMode::TerrainHeight ||
205 instance.yOffsetMode > TerrainObjectYOffsetMode::Custom ||
206 instance.scaleMode < TerrainObjectScaleMode::Fixed ||
207 instance.scaleMode > TerrainObjectScaleMode::FitnessRandomized)
208 return false;
209 return true;
210}
211bool validEnums(const TerrainProbeSpawnRule& rule) {
212 return rule.mode >= TerrainProbeOperationMode::Add && rule.mode <= TerrainProbeOperationMode::Remove &&
213 rule.settings.type >= TerrainProbeType::Reflection && rule.settings.type <= TerrainProbeType::Light;
214}
215bool validEnums(const TerrainSplatSpawnRule& rule) { return rule.targetLayer >= 0; }
216bool validEnums(const TerrainModifierStampSpawnRule& rule) {
217 return rule.operation.operation >= TerrainStampOperation::Raise &&
218 rule.operation.operation <= TerrainStampOperation::Subtract;
219}
220bool stringsFit(const TerrainSpawnRule& variant) {
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>) {
227 return true;
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();
232 } else {
233 if (rule.settings.prototype.size() > std::numeric_limits<std::uint32_t>::max() ||
234 rule.settings.instances.size() > kMaximumInstances)
235 return false;
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();
238 });
239 }
240 }, variant);
241}
242
243void writeRule(Writer& writer, const TerrainSpawnRule& variant) {
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);
260 } else {
261 writeHeightmap(writer, rule.fitness);
262 }
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);
279 }
280 }, variant);
281}
282
283bool readRule(Reader& reader, int version, TerrainSpawnRule& output) {
284 std::uint8_t kind{};
285 std::string id;
286 bool enabled = true;
287 Heightmap fitness;
288 TerrainStampSettings operation;
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))
292 return false;
293 Heightmap localMask, globalMask;
294 if (kind == 4 && (!readHeightmap(reader, localMask) || !readHeightmap(reader, globalMask))) return false;
295 if (!readStamp(reader, operation)) return false;
296 if (kind == 3) {
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;
300 output = std::move(rule);
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;
305 output = std::move(rule);
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;
310 output = std::move(rule);
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;
315 output = std::move(rule);
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;
321 output = std::move(rule);
322 } else {
323 TerrainProbeSpawnRule rule;
324 rule.ruleId = std::move(id); rule.enabled = enabled; rule.fitness = std::move(fitness);
325 rule.operation = operation;
326 if (!reader.scalar(rule.mode) || !readProbe(reader, rule.settings) || !validEnums(rule)) return false;
327 output = std::move(rule);
328 }
329 return true;
330}
331
332#undef WRITE_MEMBER
333#undef READ_MEMBER
334#undef STAMP_FIELDS
335#undef DETAIL_FIELDS
336#undef TREE_FIELDS
337#undef OBJECT_FIELDS
338#undef PROBE_FIELDS
339#undef INSTANCE_FIELDS
340} // namespace
341
342Result<std::string> TerrainSpawnPlan::snapshotJson() const {
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))
347 return Result<std::string>::failure(Diagnostic::error(
348 DiagnosticCode::InvalidArgument, "terrain.spawnPlan.snapshot: string or instance count exceeds limit"));
349 Writer writer;
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)
353 return Result<std::string>::failure(Diagnostic::error(
354 DiagnosticCode::InvalidArgument, "terrain.spawnPlan.snapshot: payload exceeds size limit"));
355 Value::Object root{{"payload", hexEncode(writer.bytes)}, {"schema", "eve.procgen.terrain-spawn-plan"},
356 {"version", 3}};
357 return Value(std::move(root)).toJson();
358}
359
360Result<void> TerrainSpawnPlan::restoreJson(const std::string& json) {
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);
365 if (!decoded.ok()) return Result<void>::failure(decoded.status());
366 const auto* root = decoded.value().getIf<Value::Object>();
367 if (!root || root->size() != 3 || !root->contains("schema") || !root->contains("version") ||
368 !root->contains("payload"))
369 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
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) ||
376 !payload)
377 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
378 "terrain.spawnPlan.restore: unsupported schema or version"));
379 auto bytes = hexDecode(*payload);
380 if (!bytes.ok()) return Result<void>::failure(bytes.status());
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"));
386 TerrainSpawnPlan candidate;
387 candidate.rules_.reserve(count);
388 for (std::uint32_t i = 0; i < count; ++i) {
389 TerrainSpawnRule rule;
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));
398 }
399 if (reader.offset != reader.bytes.size())
401 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.spawnPlan.restore: trailing payload data"));
402 rules_.swap(candidate.rules_);
403 return Result<void>::success();
404}
405
406} // namespace eve::procgen
ActionParameterOperation operation
double value
Value::Object payload
int root
Definition AnimSmr.cpp:119
std::string output
std::string variant
std::vector< BuildingInstanceSnapshot > instances
std::int32_t c
std::uint32_t height
std::uint32_t width
std::string text
TokenKind kind
size_t offset
std::uint64_t bytes
std::string id
Definition PlayHost.cpp:108
std::uint32_t count
TerrainThermalSettings settings
#define INSTANCE_FIELDS(X)
#define PROBE_FIELDS(X)
#define OBJECT_FIELDS(X)
#define STAMP_FIELDS(X)
#define DETAIL_FIELDS(X)
#define READ_MEMBER(name)
#define TREE_FIELDS(X)
#define WRITE_MEMBER(name)
std::string bytes
float size
Definition TreeMesh.cpp:156
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
std::map< std::string, Value > Object
Definition Value.h:34
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
bool enabled