载入中...
搜索中...
未找到
GeneratedArtifact.cpp
浏览该文件的文档.
2
6
7#include <algorithm>
8#include <array>
9#include <cctype>
10#include <charconv>
11#include <cmath>
12#include <iomanip>
13#include <limits>
14#include <memory>
15#include <sstream>
16#include <type_traits>
17#include <unordered_map>
18#include <unordered_set>
19
20namespace eve::procgen {
21namespace {
22
25 eve::Diagnostic::error(code, message, "generatedArtifact"));
26}
27
30 eve::Diagnostic::error(code, message, "generatedArtifact.part"));
31}
32
33std::uint64_t fnv1a(std::string_view text) noexcept {
34 std::uint64_t value = 14695981039346656037ull;
35 for (const unsigned char byte : text) {
36 value ^= byte;
37 value *= 1099511628211ull;
38 }
39 return value;
40}
41
42bool hasControl(std::string_view text) noexcept {
43 for (const unsigned char byte : text) {
44 if (std::iscntrl(byte) != 0) return true;
45 }
46 return false;
47}
48
49bool typeMatches(ArtifactType type, const ArtifactLeafPayload &payload) noexcept {
50 switch (type) {
51 case ArtifactType::Grid: return std::holds_alternative<Grid2D>(payload);
52 case ArtifactType::PointSet: return std::holds_alternative<PointSet>(payload);
53 case ArtifactType::MeshData: return std::holds_alternative<MeshData>(payload);
54 case ArtifactType::ImageData: return std::holds_alternative<ImageData>(payload);
55 case ArtifactType::Collider: return std::holds_alternative<Collider>(payload);
56 case ArtifactType::Composite: return false;
57 }
58 return false;
59}
60
61bool typeMatches(ArtifactType type, const GeneratedArtifact::Payload &payload) noexcept {
62 return type == artifactType(payload);
63}
64
65bool finiteBounds(const Bounds &bounds) noexcept {
66 return bounds.valid && std::isfinite(bounds.minX) && std::isfinite(bounds.minY) && std::isfinite(bounds.minZ) &&
67 std::isfinite(bounds.maxX) && std::isfinite(bounds.maxY) && std::isfinite(bounds.maxZ) &&
68 bounds.minX <= bounds.maxX && bounds.minY <= bounds.maxY && bounds.minZ <= bounds.maxZ;
69}
70
71bool validMesh(const MeshData &mesh) noexcept {
72 if (mesh.empty() || mesh.positions().size() % 3u != 0u || mesh.indices().size() % 3u != 0u) return false;
73 const std::size_t vertices = mesh.positions().size() / 3u;
74 if ((!mesh.normals().empty() && mesh.normals().size() != mesh.positions().size()) ||
75 (!mesh.uvs().empty() && mesh.uvs().size() != vertices * 2u))
76 return false;
77 return std::all_of(mesh.positions().begin(), mesh.positions().end(),
78 [](float value) { return std::isfinite(value); }) &&
79 std::all_of(mesh.normals().begin(), mesh.normals().end(),
80 [](float value) { return std::isfinite(value); }) &&
81 std::all_of(mesh.uvs().begin(), mesh.uvs().end(), [](float value) { return std::isfinite(value); }) &&
82 std::all_of(mesh.indices().begin(), mesh.indices().end(),
83 [vertices](std::uint32_t index) { return index < vertices; });
84}
85
86bool validLeaf(ArtifactType type, const ArtifactLeafPayload &payload) noexcept {
87 if (!typeMatches(type, payload)) return false;
88 return std::visit(
89 [](const auto &value) {
90 using T = std::decay_t<decltype(value)>;
91 if constexpr (std::is_same_v<T, Grid2D>)
92 return value.getWidth() > 0 && value.getHeight() > 0;
93 else if constexpr (std::is_same_v<T, PointSet>)
94 return !value.points().empty() &&
95 std::all_of(value.points().begin(), value.points().end(), [](const auto &point) {
96 return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.z);
97 });
98 else if constexpr (std::is_same_v<T, MeshData>)
99 return validMesh(value);
100 else
101 return value.isValid();
102 },
103 payload);
104}
105
106bool validDependencies(ArtifactId self, const std::vector<ArtifactId> &dependencies) {
107 std::unordered_set<ArtifactId> seen;
108 for (ArtifactId dependency : dependencies) {
109 if (dependency.isNil() || dependency == self || !seen.emplace(dependency).second) return false;
110 }
111 return true;
112}
113
114bool validPart(const ArtifactPart &part) {
115 return !part.role.empty() && !part.id.isNil() && !part.schemaVersion.isZero() && !part.buildKey.empty() &&
116 finiteBounds(part.bounds) && validDependencies(part.id, part.dependencies) &&
117 validLeaf(part.type, part.payload);
118}
119
120bool validArtifact(const GeneratedArtifact &artifact) {
121 if (artifact.id.isNil() || artifact.schemaVersion.isZero() || artifact.buildKey.empty() ||
122 !finiteBounds(artifact.bounds) || !validDependencies(artifact.id, artifact.dependencies) ||
123 !typeMatches(artifact.type, artifact.payload))
124 return false;
125 if (artifact.type != ArtifactType::Composite) {
126 return std::visit(
127 [&](const auto &value) {
128 using T = std::decay_t<decltype(value)>;
129 if constexpr (std::is_same_v<T, CompositeArtifact>)
130 return false;
131 else
132 return validLeaf(artifact.type, ArtifactLeafPayload(value));
133 },
134 artifact.payload);
135 }
136 const auto &composite = std::get<CompositeArtifact>(artifact.payload);
137 if (composite.children.empty()) return false;
138 std::unordered_set<ArtifactId> identities;
139 std::unordered_set<std::string> roles;
140 for (const ArtifactPart &child : composite.children) {
141 if (child.id == artifact.id || !validPart(child) || !identities.emplace(child.id).second ||
142 !roles.emplace(child.role).second)
143 return false;
144 }
145 return true;
146}
147
148eve::Result<void> validateBatchAgainstStore(const ArtifactStore &store,
149 const std::vector<GeneratedArtifact> &artifacts) {
150 if (artifacts.empty())
152 eve::DiagnosticCode::InvalidArgument, "artifact publish batch must not be empty", "artifacts"));
153
154 std::unordered_map<ArtifactId, ArtifactType> batch;
155 batch.reserve(artifacts.size() * 5u);
156 for (const GeneratedArtifact &artifact : artifacts) {
157 if (!validArtifact(artifact))
159 eve::DiagnosticCode::InvalidArgument, "artifact structure or typed payload is invalid", "artifact"));
160 if (store.typeOf(artifact.id).has_value() || !batch.emplace(artifact.id, artifact.type).second)
162 eve::DiagnosticCode::Conflict, "artifact identity is already published", "artifact.id"));
163 if (artifact.type == ArtifactType::Composite) {
164 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children) {
165 if (store.typeOf(part.id).has_value() || !batch.emplace(part.id, part.type).second)
167 eve::DiagnosticCode::Conflict, "composite part identity conflicts", "artifact.children.id"));
168 }
169 }
170 }
171
172 const auto typeFor = [&store, &batch](ArtifactId id) -> std::optional<ArtifactType> {
173 const auto inBatch = batch.find(id);
174 if (inBatch != batch.end()) return inBatch->second;
175 return store.typeOf(id);
176 };
177 const auto checkDependencies =
178 [&typeFor](ArtifactType type, const std::vector<ArtifactId> &dependencies) -> std::optional<eve::Diagnostic> {
179 bool meshDependency = false;
180 for (ArtifactId dependency : dependencies) {
181 const auto found = typeFor(dependency);
182 if (!found)
184 "artifact dependency is not published or in batch",
185 "artifact.dependencies");
186 meshDependency = meshDependency || *found == ArtifactType::MeshData;
187 }
188 if (type == ArtifactType::Collider && !meshDependency)
190 "collider must depend on a mesh artifact", "artifact.dependencies");
191 return std::nullopt;
192 };
193 for (const GeneratedArtifact &artifact : artifacts) {
194 if (auto error = checkDependencies(artifact.type, artifact.dependencies))
195 return eve::Result<void>::failure(std::move(*error));
196 if (artifact.type == ArtifactType::Composite) {
197 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children) {
198 if (auto error = checkDependencies(part.type, part.dependencies))
199 return eve::Result<void>::failure(std::move(*error));
200 }
201 }
202 }
204}
205
206const eve::Value *stateMember(const eve::Value::Object &object, const char *name) noexcept {
207 const auto found = object.find(name);
208 return found == object.end() ? nullptr : &found->second;
209}
210
211eve::Value encodeFloatArray(const std::vector<float> &values) {
212 eve::Value::Array result;
213 result.reserve(values.size());
214 for (const float value : values) result.emplace_back(value);
215 return eve::Value(std::move(result));
216}
217
218eve::Value encodeUInt32Array(const std::vector<std::uint32_t> &values) {
219 eve::Value::Array result;
220 result.reserve(values.size());
221 for (const std::uint32_t value : values) result.emplace_back(std::int64_t(value));
222 return eve::Value(std::move(result));
223}
224
225eve::Value encodeUInt8Array(const std::vector<std::uint8_t> &values) {
226 eve::Value::Array result;
227 result.reserve(values.size());
228 for (const std::uint8_t value : values) result.emplace_back(std::int64_t(value));
229 return eve::Value(std::move(result));
230}
231
232eve::Value encodeIntArray(const std::vector<int> &values) {
233 eve::Value::Array result;
234 result.reserve(values.size());
235 for (const int value : values) result.emplace_back(std::int64_t(value));
236 return eve::Value(std::move(result));
237}
238
239eve::Value encodeBounds(const Bounds &bounds) {
240 eve::Value::Object result;
241 result.emplace("minX", eve::Value(bounds.minX));
242 result.emplace("minY", eve::Value(bounds.minY));
243 result.emplace("minZ", eve::Value(bounds.minZ));
244 result.emplace("maxX", eve::Value(bounds.maxX));
245 result.emplace("maxY", eve::Value(bounds.maxY));
246 result.emplace("maxZ", eve::Value(bounds.maxZ));
247 result.emplace("valid", eve::Value(bounds.valid));
248 return eve::Value(std::move(result));
249}
250
251eve::Value encodeMetadata(const eve::Value::Object &metadata) { return eve::Value(metadata); }
252
253eve::Value encodeDependencies(const std::vector<ArtifactId> &dependencies) {
254 eve::Value::Array result;
255 result.reserve(dependencies.size());
256 for (const ArtifactId dependency : dependencies) result.emplace_back(dependency.format());
257 return eve::Value(std::move(result));
258}
259
260eve::Value encodeStringMap(const std::unordered_map<std::string, std::string> &values) {
261 eve::Value::Object result;
262 for (const auto &[key, value] : values) result.emplace(key, eve::Value(value));
263 return eve::Value(std::move(result));
264}
265
266eve::Value encodeFloatMap(const std::unordered_map<std::string, float> &values) {
267 eve::Value::Object result;
268 for (const auto &[key, value] : values) result.emplace(key, eve::Value(value));
269 return eve::Value(std::move(result));
270}
271
272eve::Value encodeIntMap(const std::unordered_map<std::string, std::int64_t> &values) {
273 eve::Value::Object result;
274 for (const auto &[key, value] : values) result.emplace(key, eve::Value(value));
275 return eve::Value(std::move(result));
276}
277
278eve::Value encodeBoolMap(const std::unordered_map<std::string, bool> &values) {
279 eve::Value::Object result;
280 for (const auto &[key, value] : values) result.emplace(key, eve::Value(value));
281 return eve::Value(std::move(result));
282}
283
284eve::Value encodeVectorMap(const std::unordered_map<std::string, ProcgenAttributeVector> &values) {
285 eve::Value::Object result;
286 for (const auto &[key, value] : values) {
288 result.emplace(key, eve::Value(std::move(encoded)));
289 }
290 return eve::Value(std::move(result));
291}
292
293eve::Value encodeGrid(const Grid2D &grid) {
294 eve::Value::Object result;
295 result.emplace("width", eve::Value(grid.getWidth()));
296 result.emplace("height", eve::Value(grid.getHeight()));
297 result.emplace("cells", encodeUInt32Array(grid.cells()));
298 result.emplace("detail", encodeUInt8Array(grid.detail()));
299 result.emplace("metadata", encodeStringMap(grid.metadata()));
300 eve::Value::Array objects;
301 objects.reserve(grid.objects().size());
302 for (const GridObject &object : grid.objects()) {
303 eve::Value::Object encoded;
304 encoded.emplace("name", eve::Value(object.name));
305 encoded.emplace("type", eve::Value(object.type));
306 encoded.emplace("x", eve::Value(object.x));
307 encoded.emplace("y", eve::Value(object.y));
308 encoded.emplace("width", eve::Value(object.width));
309 encoded.emplace("height", eve::Value(object.height));
310 encoded.emplace("gid", eve::Value(std::int64_t(object.gid)));
311 objects.emplace_back(std::move(encoded));
312 }
313 result.emplace("objects", eve::Value(std::move(objects)));
314 return eve::Value(std::move(result));
315}
316
317eve::Value encodePointSet(const PointSet &points) {
318 eve::Value::Object result;
319 eve::Value::Array encodedPoints;
320 encodedPoints.reserve(points.points().size());
321 for (std::size_t index = 0; index < points.points().size(); ++index) {
322 const ProcgenPoint& point = points.points()[index];
323 eve::Value::Object encoded;
324 encoded.emplace("x", eve::Value(point.x));
325 encoded.emplace("y", eve::Value(point.y));
326 encoded.emplace("z", eve::Value(point.z));
327 encoded.emplace("normalX", eve::Value(point.normalX));
328 encoded.emplace("normalY", eve::Value(point.normalY));
329 encoded.emplace("normalZ", eve::Value(point.normalZ));
330 encoded.emplace("pitch", eve::Value(point.pitch));
331 encoded.emplace("yaw", eve::Value(point.yaw));
332 encoded.emplace("roll", eve::Value(point.roll));
333 encoded.emplace("scaleX", eve::Value(point.scaleX));
334 encoded.emplace("scaleY", eve::Value(point.scaleY));
335 encoded.emplace("scaleZ", eve::Value(point.scaleZ));
336 encoded.emplace("density", eve::Value(point.density));
337 encoded.emplace("seed", eve::Value(std::int64_t(point.seed)));
338 encoded.emplace("id", eve::Value(std::to_string(point.id)));
339 encoded.emplace("boundsMinX", eve::Value(point.boundsMinX));
340 encoded.emplace("boundsMinY", eve::Value(point.boundsMinY));
341 encoded.emplace("boundsMinZ", eve::Value(point.boundsMinZ));
342 encoded.emplace("boundsMaxX", eve::Value(point.boundsMaxX));
343 encoded.emplace("boundsMaxY", eve::Value(point.boundsMaxY));
344 encoded.emplace("boundsMaxZ", eve::Value(point.boundsMaxZ));
345 encoded.emplace("colorR", eve::Value(point.colorR));
346 encoded.emplace("colorG", eve::Value(point.colorG));
347 encoded.emplace("colorB", eve::Value(point.colorB));
348 encoded.emplace("colorA", eve::Value(point.colorA));
349 encoded.emplace("steepness", eve::Value(point.steepness));
350 eve::Value::Object floatAttributes;
351 eve::Value::Object intAttributes;
352 eve::Value::Object boolAttributes;
353 eve::Value::Object vectorAttributes;
354 eve::Value::Object stringAttributes;
355 for (std::size_t column = 0; column < points.attributes().columnCount(); ++column) {
356 const std::string name(points.attributes().columnName(column));
357 if (!points.attributes().has(index, name)) continue;
358 switch (*points.attributes().typeOf(name)) {
360 floatAttributes.emplace(name, eve::Value(*points.attributes().getFloat(index, name)));
361 break;
363 intAttributes.emplace(name, eve::Value(*points.attributes().getInt(index, name)));
364 break;
366 boolAttributes.emplace(name, eve::Value(*points.attributes().getBool(index, name)));
367 break;
369 const auto value = *points.attributes().getVector(index, name);
371 vectorAttributes.emplace(name, eve::Value(std::move(components)));
372 break;
373 }
375 stringAttributes.emplace(name,
376 eve::Value(std::string(*points.attributes().getString(index, name))));
377 break;
378 }
379 }
380 encoded.emplace("floatAttributes", eve::Value(std::move(floatAttributes)));
381 encoded.emplace("intAttributes", eve::Value(std::move(intAttributes)));
382 encoded.emplace("boolAttributes", eve::Value(std::move(boolAttributes)));
383 encoded.emplace("vectorAttributes", eve::Value(std::move(vectorAttributes)));
384 encoded.emplace("stringAttributes", eve::Value(std::move(stringAttributes)));
385 encodedPoints.emplace_back(std::move(encoded));
386 }
387 result.emplace("points", eve::Value(std::move(encodedPoints)));
388 return eve::Value(std::move(result));
389}
390
391eve::Value encodeMesh(const MeshData &mesh) {
392 eve::Value::Object result;
393 result.emplace("positions", encodeFloatArray(mesh.positions()));
394 result.emplace("normals", encodeFloatArray(mesh.normals()));
395 result.emplace("uvs", encodeFloatArray(mesh.uvs()));
396 result.emplace("indices", encodeUInt32Array(mesh.indices()));
397 result.emplace("groupNames", [&mesh] {
398 eve::Value::Array names;
399 names.reserve(mesh.groupNames().size());
400 for (const auto &name : mesh.groupNames()) names.emplace_back(name);
401 return eve::Value(std::move(names));
402 }());
403 result.emplace("triangleGroups", encodeIntArray(mesh.triangleGroups()));
404 result.emplace("activeGroup", eve::Value(mesh.activeGroup()));
405 result.emplace("metadata", encodeStringMap(mesh.metadata()));
406 return eve::Value(std::move(result));
407}
408
409eve::Value encodeImage(const ImageData &image) {
410 eve::Value::Object result;
411 result.emplace("width", eve::Value(image.width));
412 result.emplace("height", eve::Value(image.height));
413 result.emplace("channels", eve::Value(image.channels));
414 result.emplace("format", eve::Value(image.format));
415 result.emplace("pixels", encodeUInt8Array(image.pixels));
416 return eve::Value(std::move(result));
417}
418
419eve::Value encodeCollider(const Collider &collider) {
420 eve::Value::Object result;
421 result.emplace("vertices", encodeFloatArray(collider.vertices));
422 result.emplace("indices", encodeUInt32Array(collider.indices));
423 result.emplace("bounds", encodeBounds(collider.bounds));
424 result.emplace("shape", eve::Value(collider.shape));
425 return eve::Value(std::move(result));
426}
427
428eve::Value encodeLeafPayload(ArtifactType type, const ArtifactLeafPayload &payload) {
429 eve::Value::Object result;
430 result.emplace("kind", eve::Value(artifactTypeName(type)));
431 result.emplace("data", std::visit(
432 [](const auto &value) -> eve::Value {
433 using T = std::decay_t<decltype(value)>;
434 if constexpr (std::is_same_v<T, Grid2D>)
435 return encodeGrid(value);
436 else if constexpr (std::is_same_v<T, PointSet>)
437 return encodePointSet(value);
438 else if constexpr (std::is_same_v<T, MeshData>)
439 return encodeMesh(value);
440 else if constexpr (std::is_same_v<T, ImageData>)
441 return encodeImage(value);
442 else if constexpr (std::is_same_v<T, Collider>)
443 return encodeCollider(value);
444 else {
445 static_assert(std::is_same_v<T, void>, "unhandled artifact leaf payload");
446 return eve::Value{};
447 }
448 },
449 payload));
450 return eve::Value(std::move(result));
451}
452
453eve::Value encodePart(const ArtifactPart &part) {
454 eve::Value::Object result;
455 result.emplace("role", eve::Value(part.role));
456 result.emplace("artifactId", eve::Value(part.id.format()));
457 result.emplace("type", eve::Value(artifactTypeName(part.type)));
458 result.emplace("schemaVersion", eve::Value(std::to_string(part.schemaVersion.value())));
459 result.emplace("buildKey", eve::Value(part.buildKey.format()));
460 result.emplace("bounds", encodeBounds(part.bounds));
461 result.emplace("dependencies", encodeDependencies(part.dependencies));
462 result.emplace("metadata", encodeMetadata(part.metadata));
463 result.emplace("payload", encodeLeafPayload(part.type, part.payload));
464 return eve::Value(std::move(result));
465}
466
467eve::Value encodeArtifact(const GeneratedArtifact &artifact) {
468 eve::Value::Object result;
469 result.emplace("artifactId", eve::Value(artifact.id.format()));
470 result.emplace("type", eve::Value(artifactTypeName(artifact.type)));
471 result.emplace("schemaVersion", eve::Value(std::to_string(artifact.schemaVersion.value())));
472 result.emplace("buildKey", eve::Value(artifact.buildKey.format()));
473 result.emplace("bounds", encodeBounds(artifact.bounds));
474 result.emplace("dependencies", encodeDependencies(artifact.dependencies));
475 result.emplace("metadata", encodeMetadata(artifact.metadata));
476 if (artifact.type == ArtifactType::Composite) {
478 const auto &composite = std::get<CompositeArtifact>(artifact.payload);
479 children.reserve(composite.children.size());
480 for (const ArtifactPart &part : composite.children) children.emplace_back(encodePart(part));
482 payload.emplace("kind", eve::Value("composite"));
483 payload.emplace("children", eve::Value(std::move(children)));
484 result.emplace("payload", eve::Value(std::move(payload)));
485 } else {
486 result.emplace("payload", std::visit(
487 [&artifact](const auto &value) -> eve::Value {
488 using T = std::decay_t<decltype(value)>;
489 if constexpr (std::is_same_v<T, CompositeArtifact>)
490 return eve::Value{};
491 else
492 return encodeLeafPayload(artifact.type, ArtifactLeafPayload(value));
493 },
494 artifact.payload));
495 }
496 return eve::Value(std::move(result));
497}
498
499bool readStringValue(const eve::Value::Object &object, const char *name, std::string &output, bool allowEmpty = false) {
500 const eve::Value *value = stateMember(object, name);
501 const auto *text = value ? value->getIf<std::string>() : nullptr;
502 if (!text || (!allowEmpty && text->empty())) return false;
503 output = *text;
504 return true;
505}
506
507bool readIntValue(const eve::Value::Object &object, const char *name, std::int64_t &output) {
508 const eve::Value *value = stateMember(object, name);
509 const auto *integer = value ? value->getIf<std::int64_t>() : nullptr;
510 if (!integer) return false;
511 output = *integer;
512 return true;
513}
514
515bool readFloatValue(const eve::Value::Object &object, const char *name, float &output) {
516 const eve::Value *value = stateMember(object, name);
517 if (!value) return false;
518 if (const auto *number = value->getIf<double>()) {
519 if (!std::isfinite(*number) || *number < -std::numeric_limits<float>::max() ||
520 *number > std::numeric_limits<float>::max())
521 return false;
522 output = static_cast<float>(*number);
523 return std::isfinite(output);
524 }
525 if (const auto *integer = value->getIf<std::int64_t>()) {
526 output = static_cast<float>(*integer);
527 return std::isfinite(output);
528 }
529 return false;
530}
531
532bool readBoolValue(const eve::Value::Object &object, const char *name, bool &output) {
533 const eve::Value *value = stateMember(object, name);
534 const auto *boolean = value ? value->getIf<bool>() : nullptr;
535 if (!boolean) return false;
536 output = *boolean;
537 return true;
538}
539
540template <class T>
541bool decodeNumericArray(const eve::Value *encoded, std::vector<T> &output) {
542 const auto *array = encoded ? encoded->getIf<eve::Value::Array>() : nullptr;
543 if (!array) return false;
544 output.clear();
545 output.reserve(array->size());
546 for (const eve::Value &value : *array) {
547 if constexpr (std::is_same_v<T, float>) {
548 if (const auto *number = value.getIf<double>()) {
549 if (!std::isfinite(*number) || *number < -std::numeric_limits<float>::max() ||
550 *number > std::numeric_limits<float>::max())
551 return false;
552 output.push_back(static_cast<float>(*number));
553 } else if (const auto *integer = value.getIf<std::int64_t>()) {
554 const float converted = static_cast<float>(*integer);
555 if (!std::isfinite(converted)) return false;
556 output.push_back(converted);
557 } else {
558 return false;
559 }
560 } else if constexpr (std::is_same_v<T, std::uint8_t> || std::is_same_v<T, std::uint32_t>) {
561 const auto *integer = value.getIf<std::int64_t>();
562 if (!integer || *integer < 0 || static_cast<std::uint64_t>(*integer) > std::numeric_limits<T>::max())
563 return false;
564 output.push_back(static_cast<T>(*integer));
565 } else {
566 const auto *integer = value.getIf<std::int64_t>();
567 if (!integer || *integer < std::numeric_limits<T>::min() || *integer > std::numeric_limits<T>::max())
568 return false;
569 output.push_back(static_cast<T>(*integer));
570 }
571 }
572 return true;
573}
574
575bool decodeBounds(const eve::Value *encoded, Bounds &bounds) {
576 const auto *object = encoded ? encoded->getIf<eve::Value::Object>() : nullptr;
577 if (!object || !readFloatValue(*object, "minX", bounds.minX) || !readFloatValue(*object, "minY", bounds.minY) ||
578 !readFloatValue(*object, "minZ", bounds.minZ) || !readFloatValue(*object, "maxX", bounds.maxX) ||
579 !readFloatValue(*object, "maxY", bounds.maxY) || !readFloatValue(*object, "maxZ", bounds.maxZ) ||
580 !readBoolValue(*object, "valid", bounds.valid))
581 return false;
582 return finiteBounds(bounds);
583}
584
585bool decodeStringMap(const eve::Value *encoded, std::unordered_map<std::string, std::string> &output) {
586 const auto *object = encoded ? encoded->getIf<eve::Value::Object>() : nullptr;
587 if (!object) return false;
588 output.clear();
589 for (const auto &[key, value] : *object) {
590 const auto *text = value.getIf<std::string>();
591 if (!text) return false;
592 output.emplace(key, *text);
593 }
594 return true;
595}
596
597bool decodeFloatMap(const eve::Value *encoded, std::unordered_map<std::string, float> &output) {
598 const auto *object = encoded ? encoded->getIf<eve::Value::Object>() : nullptr;
599 if (!object) return false;
600 output.clear();
601 for (const auto &[key, value] : *object) {
602 if (const auto *number = value.getIf<double>()) {
603 if (!std::isfinite(*number) || *number < -std::numeric_limits<float>::max() ||
604 *number > std::numeric_limits<float>::max())
605 return false;
606 output.emplace(key, static_cast<float>(*number));
607 } else if (const auto *integer = value.getIf<std::int64_t>()) {
608 const float converted = static_cast<float>(*integer);
609 if (!std::isfinite(converted)) return false;
610 output.emplace(key, converted);
611 } else {
612 return false;
613 }
614 }
615 return true;
616}
617
618bool decodeOptionalIntMap(const eve::Value *encoded,
619 std::unordered_map<std::string, std::int64_t> &output) {
620 output.clear();
621 if (!encoded) return true;
622 const auto *object = encoded->getIf<eve::Value::Object>();
623 if (!object) return false;
624 for (const auto &[key, value] : *object) {
625 const auto *integer = value.getIf<std::int64_t>();
626 if (!integer) return false;
627 output.emplace(key, *integer);
628 }
629 return true;
630}
631
632bool decodeOptionalBoolMap(const eve::Value *encoded,
633 std::unordered_map<std::string, bool> &output) {
634 output.clear();
635 if (!encoded) return true;
636 const auto *object = encoded->getIf<eve::Value::Object>();
637 if (!object) return false;
638 for (const auto &[key, value] : *object) {
639 const auto *boolean = value.getIf<bool>();
640 if (!boolean) return false;
641 output.emplace(key, *boolean);
642 }
643 return true;
644}
645
646bool decodeOptionalVectorMap(
647 const eve::Value *encoded,
648 std::unordered_map<std::string, ProcgenAttributeVector> &output) {
649 output.clear();
650 if (!encoded) return true;
651 const auto *object = encoded->getIf<eve::Value::Object>();
652 if (!object) return false;
653 for (const auto &[key, value] : *object) {
654 std::vector<float> components;
655 if (!decodeNumericArray(&value, components) || components.size() != 3) return false;
656 output.emplace(key, ProcgenAttributeVector{components[0], components[1], components[2]});
657 }
658 return true;
659}
660
661bool readOptionalFloatValue(const eve::Value::Object &object, const char *name, float &output) {
662 const eve::Value *value = stateMember(object, name);
663 return !value || readFloatValue(object, name, output);
664}
665
666bool decodeMetadata(const eve::Value *encoded, eve::Value::Object &output) {
667 const auto *object = encoded ? encoded->getIf<eve::Value::Object>() : nullptr;
668 if (!object) return false;
669 output = *object;
670 return true;
671}
672
673bool decodeDependencies(const eve::Value *encoded, std::vector<ArtifactId> &output) {
674 const auto *array = encoded ? encoded->getIf<eve::Value::Array>() : nullptr;
675 if (!array) return false;
676 output.clear();
677 output.reserve(array->size());
678 for (const eve::Value &value : *array) {
679 const auto *text = value.getIf<std::string>();
680 const auto parsed = text ? ArtifactId::parse(*text) : std::nullopt;
681 if (!parsed || parsed->isNil()) return false;
682 output.push_back(*parsed);
683 }
684 return true;
685}
686
687std::optional<ArtifactType> decodeArtifactType(const eve::Value::Object &object, const char *name = "type") {
688 std::string text;
689 if (!readStringValue(object, name, text)) return std::nullopt;
692 if (text == artifactTypeName(type)) return type;
693 return std::nullopt;
694}
695
696bool decodeSchemaVersion(const eve::Value::Object &object, eve::SchemaVersion &version) {
697 std::string text;
698 if (!readStringValue(object, "schemaVersion", text)) return false;
699 std::uint64_t value = 0;
700 const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
701 if (error != std::errc{} || end != text.data() + text.size() || value == 0) return false;
702 version = eve::SchemaVersion(value);
703 return true;
704}
705
706bool decodePayload(ArtifactType type, const eve::Value *encoded, ArtifactLeafPayload &output) {
707 const auto *wrapper = encoded ? encoded->getIf<eve::Value::Object>() : nullptr;
708 std::string kind;
709 const eve::Value *data = wrapper ? stateMember(*wrapper, "data") : nullptr;
710 if (!wrapper || !readStringValue(*wrapper, "kind", kind) || kind != artifactTypeName(type) || !data) return false;
711 const auto *object = data->getIf<eve::Value::Object>();
712 if (!object) return false;
713 if (type == ArtifactType::Grid) {
714 std::int64_t width = 0, height = 0;
715 std::vector<std::uint32_t> cells;
716 std::vector<std::uint8_t> detail;
717 std::unordered_map<std::string, std::string> metadata;
718 const auto *objectsValue = stateMember(*object, "objects");
719 const auto *objects = objectsValue ? objectsValue->getIf<eve::Value::Array>() : nullptr;
720 if (!readIntValue(*object, "width", width) || !readIntValue(*object, "height", height) || width <= 0 ||
721 height <= 0 || width > std::numeric_limits<int>::max() || height > std::numeric_limits<int>::max() ||
722 !decodeNumericArray(stateMember(*object, "cells"), cells) ||
723 !decodeNumericArray(stateMember(*object, "detail"), detail) || !objects ||
724 !decodeStringMap(stateMember(*object, "metadata"), metadata))
725 return false;
726 const auto expected = static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
727 if (expected != cells.size() || expected != detail.size()) return false;
728 Grid2D grid;
729 grid.resize(static_cast<int>(width), static_cast<int>(height));
730 grid.cells() = std::move(cells);
731 grid.detail() = std::move(detail);
732 for (const auto &[key, value] : metadata) grid.setMeta(key, value);
733 for (const eve::Value &encodedObject : *objects) {
734 const auto *item = encodedObject.getIf<eve::Value::Object>();
735 std::string name, objectType;
736 float x = 0.f, y = 0.f, objectWidth = 0.f, objectHeight = 0.f;
737 std::int64_t gid = 0;
738 if (!item || !readStringValue(*item, "name", name, true) ||
739 !readStringValue(*item, "type", objectType, true) || !readFloatValue(*item, "x", x) ||
740 !readFloatValue(*item, "y", y) || !readFloatValue(*item, "width", objectWidth) ||
741 !readFloatValue(*item, "height", objectHeight) || !readIntValue(*item, "gid", gid) || gid < 0 ||
742 gid > std::numeric_limits<std::uint32_t>::max())
743 return false;
744 grid.addObject(name, objectType, x, y, objectWidth, objectHeight, static_cast<int>(gid));
745 }
746 output = std::move(grid);
747 return true;
748 }
750 const auto *encodedPointsValue = stateMember(*object, "points");
751 const auto *encodedPoints = encodedPointsValue ? encodedPointsValue->getIf<eve::Value::Array>() : nullptr;
752 if (!encodedPoints) return false;
754 for (const eve::Value &encodedPoint : *encodedPoints) {
755 const auto* item = encodedPoint.getIf<eve::Value::Object>();
756 ProcgenPoint point;
757 std::int64_t seed = 0;
758 std::string pointIdText;
759 std::unordered_map<std::string, float> floatAttributes;
760 std::unordered_map<std::string, std::int64_t> intAttributes;
761 std::unordered_map<std::string, bool> boolAttributes;
762 std::unordered_map<std::string, ProcgenAttributeVector> vectorAttributes;
763 std::unordered_map<std::string, std::string> stringAttributes;
764 if (!item || !readFloatValue(*item, "x", point.x) || !readFloatValue(*item, "y", point.y) ||
765 !readFloatValue(*item, "z", point.z) || !readFloatValue(*item, "normalX", point.normalX) ||
766 !readFloatValue(*item, "normalY", point.normalY) || !readFloatValue(*item, "normalZ", point.normalZ) ||
767 !readOptionalFloatValue(*item, "pitch", point.pitch) ||
768 !readFloatValue(*item, "yaw", point.yaw) ||
769 !readOptionalFloatValue(*item, "roll", point.roll) || !readFloatValue(*item, "scaleX", point.scaleX) ||
770 !readFloatValue(*item, "scaleY", point.scaleY) || !readFloatValue(*item, "scaleZ", point.scaleZ) ||
771 !readFloatValue(*item, "density", point.density) || !readIntValue(*item, "seed", seed) || seed < 0 ||
772 seed > std::numeric_limits<std::uint32_t>::max() ||
773 !readOptionalFloatValue(*item, "boundsMinX", point.boundsMinX) ||
774 !readOptionalFloatValue(*item, "boundsMinY", point.boundsMinY) ||
775 !readOptionalFloatValue(*item, "boundsMinZ", point.boundsMinZ) ||
776 !readOptionalFloatValue(*item, "boundsMaxX", point.boundsMaxX) ||
777 !readOptionalFloatValue(*item, "boundsMaxY", point.boundsMaxY) ||
778 !readOptionalFloatValue(*item, "boundsMaxZ", point.boundsMaxZ) ||
779 !readOptionalFloatValue(*item, "colorR", point.colorR) ||
780 !readOptionalFloatValue(*item, "colorG", point.colorG) ||
781 !readOptionalFloatValue(*item, "colorB", point.colorB) ||
782 !readOptionalFloatValue(*item, "colorA", point.colorA) ||
783 !readOptionalFloatValue(*item, "steepness", point.steepness) ||
784 !decodeFloatMap(stateMember(*item, "floatAttributes"), floatAttributes) ||
785 !decodeOptionalIntMap(stateMember(*item, "intAttributes"), intAttributes) ||
786 !decodeOptionalBoolMap(stateMember(*item, "boolAttributes"), boolAttributes) ||
787 !decodeOptionalVectorMap(stateMember(*item, "vectorAttributes"), vectorAttributes) ||
788 !decodeStringMap(stateMember(*item, "stringAttributes"), stringAttributes))
789 return false;
790 point.seed = static_cast<std::uint32_t>(seed);
791 if (const eve::Value* encodedId = stateMember(*item, "id")) {
792 const auto* text = encodedId->getIf<std::string>();
793 if (!text) return false;
794 const auto [end, error] = std::from_chars(text->data(), text->data() + text->size(), point.id);
795 if (error != std::errc{} || end != text->data() + text->size()) return false;
796 }
797 const int pointIndex = points.appendPoint(std::move(point));
798 for (const auto& [name, value] : floatAttributes)
799 if (!points.trySetFloatAttribute(pointIndex, name, value).ok()) return false;
800 for (const auto& [name, value] : intAttributes)
801 if (!points.trySetIntAttribute(pointIndex, name, value).ok()) return false;
802 for (const auto& [name, value] : boolAttributes)
803 if (!points.trySetBoolAttribute(pointIndex, name, value).ok()) return false;
804 for (const auto& [name, value] : vectorAttributes)
805 if (!points.trySetVectorAttribute(pointIndex, name, value.x, value.y, value.z).ok()) return false;
806 for (const auto& [name, value] : stringAttributes)
807 if (!points.trySetStringAttribute(pointIndex, name, value).ok()) return false;
808 }
809 output = std::move(points);
810 return true;
811 }
814 std::vector<std::string> names;
815 std::vector<int> assignments;
816 std::int64_t activeGroup = -1;
817 const auto *encodedNamesValue = stateMember(*object, "groupNames");
818 const auto *encodedNames = encodedNamesValue ? encodedNamesValue->getIf<eve::Value::Array>() : nullptr;
819 const auto *encodedMetadata = stateMember(*object, "metadata");
820 std::unordered_map<std::string, std::string> metadata;
821 if (!decodeNumericArray(stateMember(*object, "positions"), mesh.positions()) ||
822 !decodeNumericArray(stateMember(*object, "normals"), mesh.normals()) ||
823 !decodeNumericArray(stateMember(*object, "uvs"), mesh.uvs()) ||
824 !decodeNumericArray(stateMember(*object, "indices"), mesh.indices()) || !encodedNames ||
825 !decodeNumericArray(stateMember(*object, "triangleGroups"), assignments) ||
826 !readIntValue(*object, "activeGroup", activeGroup) || activeGroup < std::numeric_limits<int>::min() ||
827 activeGroup > std::numeric_limits<int>::max() || !decodeStringMap(encodedMetadata, metadata))
828 return false;
829 for (const eve::Value &encodedName : *encodedNames) {
830 const auto *name = encodedName.getIf<std::string>();
831 if (!name) return false;
832 names.push_back(*name);
833 }
834 auto restoredGroups =
835 mesh.restoreGroupData(std::move(names), std::move(assignments), static_cast<int>(activeGroup));
836 if (!restoredGroups.ok()) return false;
837 for (const auto &[key, value] : metadata) mesh.setMeta(key, value);
838 output = std::move(mesh);
839 return true;
840 }
843 std::int64_t width = 0, height = 0, channels = 0;
844 if (!readIntValue(*object, "width", width) || !readIntValue(*object, "height", height) ||
845 !readIntValue(*object, "channels", channels) || width <= 0 || height <= 0 || channels <= 0 ||
846 width > std::numeric_limits<int>::max() || height > std::numeric_limits<int>::max() ||
847 channels > std::numeric_limits<int>::max() || !readStringValue(*object, "format", image.format) ||
848 !decodeNumericArray(stateMember(*object, "pixels"), image.pixels))
849 return false;
850 image.width = static_cast<int>(width);
851 image.height = static_cast<int>(height);
852 image.channels = static_cast<int>(channels);
853 if (!image.isValid()) return false;
854 output = std::move(image);
855 return true;
856 }
858 Collider collider;
859 if (!decodeNumericArray(stateMember(*object, "vertices"), collider.vertices) ||
860 !decodeNumericArray(stateMember(*object, "indices"), collider.indices) ||
861 !decodeBounds(stateMember(*object, "bounds"), collider.bounds) ||
862 !readStringValue(*object, "shape", collider.shape) || !collider.isValid())
863 return false;
864 output = std::move(collider);
865 return true;
866 }
867 return false;
868}
869
870bool decodePart(const eve::Value &encoded, ArtifactPart &output) {
871 const auto *object = encoded.getIf<eve::Value::Object>();
872 std::string role, idText, buildKeyText;
873 eve::SchemaVersion schemaVersion;
874 Bounds bounds;
875 std::vector<ArtifactId> dependencies;
876 eve::Value::Object metadata;
877 const auto type = object ? decodeArtifactType(*object) : std::nullopt;
878 const auto parsedId =
879 object && readStringValue(*object, "artifactId", idText) ? ArtifactId::parse(idText) : std::nullopt;
880 if (!object || !readStringValue(*object, "role", role) || !parsedId || parsedId->isNil() || !type ||
881 *type == ArtifactType::Composite || !decodeSchemaVersion(*object, schemaVersion) ||
882 !readStringValue(*object, "buildKey", buildKeyText) || !decodeBounds(stateMember(*object, "bounds"), bounds) ||
883 !decodeDependencies(stateMember(*object, "dependencies"), dependencies) ||
884 !decodeMetadata(stateMember(*object, "metadata"), metadata))
885 return false;
886 const auto key = BuildKey::fromCanonical(buildKeyText);
887 if (!key) return false;
889 if (!decodePayload(*type, stateMember(*object, "payload"), payload)) return false;
890 auto result = makeArtifactPart(std::move(role), *parsedId, *type, schemaVersion, *key, bounds,
891 std::move(dependencies), std::move(metadata), std::move(payload));
892 if (!result.ok()) return false;
893 output = std::move(result).takeValue();
894 return true;
895}
896
897bool decodeArtifact(const eve::Value &encoded, GeneratedArtifact &output) {
898 const auto *object = encoded.getIf<eve::Value::Object>();
899 std::string idText, buildKeyText;
900 eve::SchemaVersion schemaVersion;
901 Bounds bounds;
902 std::vector<ArtifactId> dependencies;
903 eve::Value::Object metadata;
904 const auto type = object ? decodeArtifactType(*object) : std::nullopt;
905 const auto parsedId =
906 object && readStringValue(*object, "artifactId", idText) ? ArtifactId::parse(idText) : std::nullopt;
907 if (!object || !parsedId || parsedId->isNil() || !type || !decodeSchemaVersion(*object, schemaVersion) ||
908 !readStringValue(*object, "buildKey", buildKeyText) || !decodeBounds(stateMember(*object, "bounds"), bounds) ||
909 !decodeDependencies(stateMember(*object, "dependencies"), dependencies) ||
910 !decodeMetadata(stateMember(*object, "metadata"), metadata))
911 return false;
912 const auto key = BuildKey::fromCanonical(buildKeyText);
913 if (!key) return false;
916 const auto *payloadObjectValue = stateMember(*object, "payload");
917 const auto *payloadObject = payloadObjectValue ? payloadObjectValue->getIf<eve::Value::Object>() : nullptr;
918 std::string kind;
919 const auto *encodedChildren = payloadObject ? stateMember(*payloadObject, "children") : nullptr;
920 const auto *children = encodedChildren ? encodedChildren->getIf<eve::Value::Array>() : nullptr;
921 CompositeArtifact composite;
922 if (!payloadObject || !readStringValue(*payloadObject, "kind", kind) || kind != "composite" || !children)
923 return false;
924 composite.children.reserve(children->size());
925 for (const eve::Value &encodedPart : *children) {
926 ArtifactPart part;
927 if (!decodePart(encodedPart, part)) return false;
928 composite.children.push_back(std::move(part));
929 }
930 payload = std::move(composite);
931 } else {
933 if (!decodePayload(*type, stateMember(*object, "payload"), leaf)) return false;
934 payload = std::visit(
935 [](auto &&value) -> GeneratedArtifact::Payload {
936 return GeneratedArtifact::Payload(std::forward<decltype(value)>(value));
937 },
938 std::move(leaf));
939 }
940 auto result = makeArtifact(*parsedId, *type, schemaVersion, *key, bounds, std::move(dependencies),
941 std::move(metadata), std::move(payload));
942 if (!result.ok()) return false;
943 output = std::move(result).takeValue();
944 return true;
945}
946
947Bounds boundsForMesh(const MeshBuild &mesh) noexcept {
948 Bounds bounds;
949 for (int i = 0; i < mesh.getVertexCount(); ++i) {
950 bounds.include(mesh.getPositionX(i), mesh.getPositionY(i), mesh.getPositionZ(i));
951 }
952 return bounds;
953}
954
955Bounds boundsForGrid(const Grid2D &grid) noexcept {
956 Bounds bounds;
957 if (grid.getWidth() <= 0 || grid.getHeight() <= 0) return bounds;
958 bounds.include(0.f, 0.f, 0.f);
959 bounds.include(float(grid.getWidth()), 0.f, float(grid.getHeight()));
960 return bounds;
961}
962
963Bounds boundsForPoints(const PointSet &points) noexcept {
964 Bounds bounds;
965 for (const ProcgenPoint &point : points.points()) bounds.include(point.x, point.y, point.z);
966 return bounds;
967}
968
969Collider colliderForMesh(const MeshBuild &mesh) {
970 Collider collider;
971 collider.vertices = mesh.positions();
972 collider.indices = mesh.indices();
973 collider.bounds = boundsForMesh(mesh);
974 return collider;
975}
976
977Grid2D makeHexTopology(const Params &params) {
978 const int width = std::clamp(params.getInt("width", 32), 1, 256);
979 const int height = std::clamp(params.getInt("height", 24), 1, 256);
980 Grid2D topology;
981 topology.resize(width, height);
982 const std::uint32_t seed = params.getSeed();
983 for (int y = 0; y < height; ++y) {
984 for (int x = 0; x < width; ++x) {
985 // This is a compact cell-topology projection. Elevation and dense
986 // geometry remain in the mesh recipe; no per-cell ECS objects are
987 // created merely to publish topology.
988 const std::uint32_t value = seed ^ (std::uint32_t(x) * 0x9e3779b9u) ^ (std::uint32_t(y) * 0x85ebca6bu);
989 topology.setCell(x, y, int((value ^ (value >> 16u)) % 10u));
990 }
991 }
992 topology.setMeta("role", "topology");
993 topology.setMeta("algorithm", "mesh.hexterrain");
994 topology.setMeta("coordinateSpace", "hex_axial");
995 return topology;
996}
997
998PointSet makeHexAnchors(const Params &params) {
999 const float radius = params.getFloat("radius", 1.f);
1000 const int width = std::max(2, params.getInt("width", 32));
1001 const int height = std::max(2, params.getInt("height", 24));
1002 const float xStep = radius * 1.5f;
1003 const float zStep = radius * 1.7320508076f;
1004 PointSet anchors;
1005 anchors.add(0.f, 0.f, 0.f);
1006 anchors.add(float(width - 1) * xStep, 0.f, 0.f);
1007 anchors.add(0.f, 0.f, float(height - 1) * zStep);
1008 anchors.add(float(width - 1) * xStep, 0.f, (float(height - 1) + float((width - 1) & 1) * .5f) * zStep);
1009 anchors.setStringAttribute(0, "role", "spawn");
1010 anchors.setStringAttribute(1, "role", "edge");
1011 anchors.setStringAttribute(2, "role", "edge");
1012 anchors.setStringAttribute(3, "role", "edge");
1013 return anchors;
1014}
1015
1016Grid2D makeCastleTopology(const Params &params) {
1017 const int rings = std::clamp(params.getInt("rings", 2), 1, 4);
1018 Grid2D topology;
1019 topology.resize(rings, 1);
1020 for (int ring = 0; ring < rings; ++ring) topology.setCell(ring, 0, ring + 1);
1021 topology.setMeta("role", "topology");
1022 topology.setMeta("algorithm", "mesh.castle");
1023 topology.setMeta("coordinateSpace", "castle_ring");
1024 return topology;
1025}
1026
1027PointSet makeCastleAnchors(const Params &params) {
1028 const float width = std::max(16.f, params.getFloat("width", 42.f));
1029 const float depth = std::max(16.f, params.getFloat("depth", 36.f));
1030 const float gateWidth = std::clamp(params.getFloat("gateWidth", 5.f), 2.f, width * .3f);
1031 const float keepWidth = std::clamp(params.getFloat("keepWidth", width * .25f), 6.f, width * .55f);
1032 const float keepDepth = std::clamp(params.getFloat("keepDepth", depth * .24f), 6.f, depth * .55f);
1033 PointSet anchors;
1034 anchors.add(0.f, 0.f, -depth * .5f - 1.f);
1035 anchors.add(-width * .5f + gateWidth, 0.f, -depth * .5f);
1036 anchors.add(width * .5f - gateWidth, 0.f, -depth * .5f);
1037 anchors.add(-keepWidth * .5f, 0.f, -keepDepth * .5f);
1038 anchors.add(keepWidth * .5f, 0.f, keepDepth * .5f);
1039 anchors.setStringAttribute(0, "role", "gate");
1040 anchors.setStringAttribute(1, "role", "outer_gate_left");
1041 anchors.setStringAttribute(2, "role", "outer_gate_right");
1042 anchors.setStringAttribute(3, "role", "keep_min");
1043 anchors.setStringAttribute(4, "role", "keep_max");
1044 return anchors;
1045}
1046
1047eve::Result<ArtifactPart> partFromMesh(std::string role, ArtifactId parent, const BuildKey &parentKey, MeshBuild mesh,
1048 eve::Value::Object metadata) {
1049 auto childKey = BuildKey::fromCanonical(parentKey.format() + "/" + role);
1050 if (!childKey)
1051 return rejectedPart(eve::DiagnosticCode::InvalidArgument, "cannot derive child build key for " + role);
1052 const ArtifactId childId = parent.child(role);
1053 const Bounds bounds = boundsForMesh(mesh);
1054 return makeArtifactPart(std::move(role), childId, ArtifactType::MeshData, eve::SchemaVersion(1),
1055 std::move(*childKey), bounds, {}, std::move(metadata), std::move(mesh));
1056}
1057
1058eve::Result<ArtifactPart> partFromCollider(std::string role, ArtifactId parent, const BuildKey &parentKey,
1059 Collider collider, eve::Value::Object metadata) {
1060 auto childKey = BuildKey::fromCanonical(parentKey.format() + "/" + role);
1061 if (!childKey)
1062 return rejectedPart(eve::DiagnosticCode::InvalidArgument, "cannot derive child build key for " + role);
1063 const ArtifactId childId = parent.child(role);
1064 const Bounds bounds = collider.bounds;
1065 return makeArtifactPart(std::move(role), childId, ArtifactType::Collider, eve::SchemaVersion(1),
1066 std::move(*childKey), bounds, {parent.child("mesh")}, std::move(metadata),
1067 std::move(collider));
1068}
1069
1070eve::Result<ArtifactPart> partFromGrid(std::string role, ArtifactId parent, const BuildKey &parentKey, Grid2D grid,
1071 eve::Value::Object metadata) {
1072 auto childKey = BuildKey::fromCanonical(parentKey.format() + "/" + role);
1073 if (!childKey)
1074 return rejectedPart(eve::DiagnosticCode::InvalidArgument, "cannot derive child build key for " + role);
1075 const ArtifactId childId = parent.child(role);
1076 const Bounds bounds = boundsForGrid(grid);
1077 return makeArtifactPart(std::move(role), childId, ArtifactType::Grid, eve::SchemaVersion(1), std::move(*childKey),
1078 bounds, {}, std::move(metadata), std::move(grid));
1079}
1080
1081eve::Result<ArtifactPart> partFromPoints(std::string role, ArtifactId parent, const BuildKey &parentKey,
1082 PointSet points, eve::Value::Object metadata) {
1083 auto childKey = BuildKey::fromCanonical(parentKey.format() + "/" + role);
1084 if (!childKey)
1085 return rejectedPart(eve::DiagnosticCode::InvalidArgument, "cannot derive child build key for " + role);
1086 const ArtifactId childId = parent.child(role);
1087 const Bounds bounds = boundsForPoints(points);
1088 return makeArtifactPart(std::move(role), childId, ArtifactType::PointSet, eve::SchemaVersion(1),
1089 std::move(*childKey), bounds, {}, std::move(metadata), std::move(points));
1090}
1091
1092template <class T>
1093bool appendPart(eve::Result<ArtifactPart> &&result, std::vector<ArtifactPart> &parts, eve::Status &failure) {
1094 if (!result.ok()) {
1095 failure = result.status();
1096 return false;
1097 }
1098 parts.emplace_back(std::move(result).takeValue());
1099 return true;
1100}
1101
1102eve::Result<GeneratedArtifact> generateComposite(const Params &params, ArtifactId id, std::string_view recipe,
1103 bool (*generateMesh)(const Params &, MeshBuild &, std::string &),
1104 Grid2D topology, PointSet anchors) {
1105 if (id.isNil())
1106 return rejectedArtifact(eve::DiagnosticCode::InvalidArgument, "top-level artifact identity must not be nil");
1107 const auto key = BuildKey::forRecipe(recipe, params);
1108 if (!key)
1109 return rejectedArtifact(eve::DiagnosticCode::InvalidArgument, "recipe or parameters cannot form a build key");
1110
1111 MeshBuild mesh;
1112 std::string error;
1113 if (!generateMesh(params, mesh, error)) {
1114 return rejectedArtifact(eve::DiagnosticCode::Failed,
1115 error.empty() ? "procedural mesh generation failed" : error);
1116 }
1117 const Collider collider = colliderForMesh(mesh);
1118 std::vector<ArtifactPart> parts;
1119 parts.reserve(4);
1120 eve::Status failure;
1121 eve::Value::Object meshMeta;
1122 meshMeta.emplace("role", eve::Value("mesh"));
1123 meshMeta.emplace("densePayload", eve::Value(true));
1124 auto meshPart = partFromMesh("mesh", id, *key, std::move(mesh), std::move(meshMeta));
1125 if (!appendPart<ArtifactPart>(std::move(meshPart), parts, failure))
1126 return eve::Result<GeneratedArtifact>::failure(std::move(failure));
1127
1128 eve::Value::Object colliderMeta;
1129 colliderMeta.emplace("role", eve::Value("collider"));
1130 colliderMeta.emplace("source", eve::Value("mesh"));
1131 auto colliderPart = partFromCollider("collider", id, *key, collider, std::move(colliderMeta));
1132 if (!appendPart<ArtifactPart>(std::move(colliderPart), parts, failure))
1133 return eve::Result<GeneratedArtifact>::failure(std::move(failure));
1134
1135 eve::Value::Object topologyMeta;
1136 topologyMeta.emplace("role", eve::Value("topology"));
1137 auto topologyPart = partFromGrid("topology", id, *key, std::move(topology), std::move(topologyMeta));
1138 if (!appendPart<ArtifactPart>(std::move(topologyPart), parts, failure))
1139 return eve::Result<GeneratedArtifact>::failure(std::move(failure));
1140
1141 eve::Value::Object anchorsMeta;
1142 anchorsMeta.emplace("role", eve::Value("anchors"));
1143 anchorsMeta.emplace("coordinateSpace", eve::Value("world"));
1144 auto anchorsPart = partFromPoints("anchors", id, *key, std::move(anchors), std::move(anchorsMeta));
1145 if (!appendPart<ArtifactPart>(std::move(anchorsPart), parts, failure))
1146 return eve::Result<GeneratedArtifact>::failure(std::move(failure));
1147
1148 CompositeArtifact composite;
1149 composite.children = std::move(parts);
1150 eve::Value::Object metadata;
1151 metadata.emplace("algorithm", eve::Value(std::string(recipe)));
1152 metadata.emplace("childCount", eve::Value(std::int64_t(composite.children.size())));
1153 metadata.emplace("determinism", eve::Value("seeded_cpu"));
1154 metadata.emplace("payloadPolicy", eve::Value("dense data stays typed"));
1155 const Bounds bounds = composite.children.front().bounds;
1156 return makeArtifact(id, ArtifactType::Composite, eve::SchemaVersion(1), *key, bounds, {}, std::move(metadata),
1157 GeneratedArtifact::Payload(std::move(composite)));
1158}
1159
1160} // namespace
1161
1163public:
1164 ArtifactStoreStage(ArtifactStore &owner, std::vector<GeneratedArtifact> artifacts)
1165 : owner_(&owner), artifacts_(std::move(artifacts)) {}
1166
1168
1169 void commit() noexcept override {
1170 if (!owner_) return;
1171 owner_->commitStaged(std::move(artifacts_));
1172 owner_ = nullptr;
1173 }
1174
1175 void rollback() noexcept override {
1176 if (!owner_) return;
1177 owner_->rollbackStaged();
1178 owner_ = nullptr;
1179 artifacts_.clear();
1180 }
1181
1182private:
1183 ArtifactStore *owner_ = nullptr;
1184 std::vector<GeneratedArtifact> artifacts_;
1185};
1186
1187std::optional<BuildKey> BuildKey::fromCanonical(std::string_view canonical) {
1188 if (canonical.empty() || hasControl(canonical)) return std::nullopt;
1189 return BuildKey(std::string(canonical));
1190}
1191
1192std::optional<BuildKey> BuildKey::forRecipe(std::string_view recipeId, const Params &params) {
1193 if (recipeId.empty() || hasControl(recipeId)) return std::nullopt;
1194 std::ostringstream value;
1195 value << "procgen/v1/" << recipeId << '/' << std::hex << std::setw(16) << std::setfill('0')
1196 << fnv1a(std::string(recipeId) + '\0' + params.canonicalString());
1197 return BuildKey(value.str());
1198}
1199
1200void Bounds::include(float x, float y, float z) noexcept {
1201 if (!valid) {
1202 minX = maxX = x;
1203 minY = maxY = y;
1204 minZ = maxZ = z;
1205 valid = true;
1206 return;
1207 }
1208 minX = std::min(minX, x);
1209 minY = std::min(minY, y);
1210 minZ = std::min(minZ, z);
1211 maxX = std::max(maxX, x);
1212 maxY = std::max(maxY, y);
1213 maxZ = std::max(maxZ, z);
1214}
1215
1216bool ImageData::isValid() const noexcept {
1217 if (width <= 0 || height <= 0 || channels <= 0) return false;
1218 const auto expected =
1219 static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height) * static_cast<std::uint64_t>(channels);
1220 return expected == pixels.size() && !format.empty();
1221}
1222
1223bool Collider::isValid() const noexcept {
1224 return finiteBounds(bounds) && !shape.empty() && vertices.size() % 3u == 0u && indices.size() % 3u == 0u &&
1225 !vertices.empty() && !indices.empty() &&
1226 std::all_of(vertices.begin(), vertices.end(), [](float value) { return std::isfinite(value); }) &&
1227 std::all_of(indices.begin(), indices.end(),
1228 [&](std::uint32_t index) { return index < vertices.size() / 3u; });
1229}
1230
1231const ArtifactPart *CompositeArtifact::find(std::string_view role) const noexcept {
1232 const auto found =
1233 std::find_if(children.begin(), children.end(), [&](const ArtifactPart &part) { return part.role == role; });
1234 return found == children.end() ? nullptr : &*found;
1235}
1236
1238 return std::visit(
1239 [](const auto &value) -> ArtifactType {
1240 using T = std::decay_t<decltype(value)>;
1241 if constexpr (std::is_same_v<T, Grid2D>) return ArtifactType::Grid;
1242 if constexpr (std::is_same_v<T, PointSet>) return ArtifactType::PointSet;
1243 if constexpr (std::is_same_v<T, MeshData>) return ArtifactType::MeshData;
1244 if constexpr (std::is_same_v<T, ImageData>) return ArtifactType::ImageData;
1245 if constexpr (std::is_same_v<T, Collider>) return ArtifactType::Collider;
1247 },
1248 payload);
1249}
1250
1251const char *artifactTypeName(ArtifactType type) noexcept {
1252 switch (type) {
1253 case ArtifactType::Grid: return "grid";
1254 case ArtifactType::PointSet: return "point_set";
1255 case ArtifactType::MeshData: return "mesh_data";
1256 case ArtifactType::ImageData: return "image_data";
1257 case ArtifactType::Collider: return "collider";
1258 case ArtifactType::Composite: return "composite";
1259 }
1260 return "unknown";
1261}
1262
1264 BuildKey buildKey, Bounds bounds, std::vector<ArtifactId> dependencies,
1266 GeneratedArtifact result;
1267 result.id = id;
1268 result.type = type;
1269 result.schemaVersion = schemaVersion;
1270 result.buildKey = std::move(buildKey);
1271 result.bounds = bounds;
1272 result.dependencies = std::move(dependencies);
1273 result.metadata = std::move(metadata);
1274 result.payload = std::move(payload);
1275 if (!validArtifact(result))
1276 return rejectedArtifact(eve::DiagnosticCode::InvalidArgument, "artifact structure or typed payload is invalid");
1277 return eve::Result<GeneratedArtifact>::success(std::move(result));
1278}
1279
1281 eve::SchemaVersion schemaVersion, BuildKey buildKey, Bounds bounds,
1282 std::vector<ArtifactId> dependencies, eve::Value::Object metadata,
1284 ArtifactPart result;
1285 result.role = std::move(role);
1286 result.id = id;
1287 result.type = type;
1288 result.schemaVersion = schemaVersion;
1289 result.buildKey = std::move(buildKey);
1290 result.bounds = bounds;
1291 result.dependencies = std::move(dependencies);
1292 result.metadata = std::move(metadata);
1293 result.payload = std::move(payload);
1294 if (!validPart(result))
1295 return rejectedPart(eve::DiagnosticCode::InvalidArgument,
1296 "artifact part '" + result.role + "' structure or typed payload is invalid");
1297 return eve::Result<ArtifactPart>::success(std::move(result));
1298}
1299
1301 auto published = publishBatch({std::move(artifact)});
1302 if (!published.ok()) return eve::Result<ArtifactId>::failure(published.status());
1303 auto ids = std::move(published).takeValue();
1304 return eve::Result<ArtifactId>::success(ids.front());
1305}
1306
1307eve::Result<std::vector<ArtifactId>> ArtifactStore::publishBatch(std::vector<GeneratedArtifact> artifacts) {
1308 if (stageActive_)
1309 return eve::Result<std::vector<ArtifactId>>::failure(
1310 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "artifact store already has a pending stage"));
1311 if (artifacts.empty())
1313 eve::DiagnosticCode::InvalidArgument, "artifact publish batch must not be empty", "artifacts"));
1314
1315 std::unordered_map<ArtifactId, ArtifactType> available;
1316 available.reserve(artifacts_.size() + partOwners_.size() + artifacts.size() * 5u);
1317 for (const auto &[id, artifact] : artifacts_) available.emplace(id, artifact.type);
1318 for (const auto &[partId, ownerId] : partOwners_) {
1319 const GeneratedArtifact *owner = find(ownerId);
1320 if (!owner || owner->type != ArtifactType::Composite) continue;
1321 const auto &children = std::get<CompositeArtifact>(owner->payload).children;
1322 const auto found = std::find_if(children.begin(), children.end(),
1323 [partId](const ArtifactPart &part) { return part.id == partId; });
1324 if (found != children.end()) available.emplace(partId, found->type);
1325 }
1326
1327 std::vector<ArtifactId> ids;
1328 ids.reserve(artifacts.size());
1329 for (const GeneratedArtifact &artifact : artifacts) {
1330 if (!validArtifact(artifact))
1331 return eve::Result<std::vector<ArtifactId>>::failure(eve::Diagnostic::error(
1332 eve::DiagnosticCode::InvalidArgument, "artifact structure or typed payload is invalid", "artifact"));
1333 if (!available.emplace(artifact.id, artifact.type).second)
1334 return eve::Result<std::vector<ArtifactId>>::failure(eve::Diagnostic::error(
1335 eve::DiagnosticCode::Conflict, "artifact or part identity is already published", "artifact.id"));
1336 ids.push_back(artifact.id);
1337 if (artifact.type == ArtifactType::Composite) {
1338 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children) {
1339 if (!available.emplace(part.id, part.type).second)
1340 return eve::Result<std::vector<ArtifactId>>::failure(eve::Diagnostic::error(
1341 eve::DiagnosticCode::Conflict, "composite part identity conflicts", "artifact.children.id"));
1342 }
1343 }
1344 }
1345
1346 const auto checkDependencies = [&](ArtifactType type,
1347 const std::vector<ArtifactId> &dependencies) -> std::optional<eve::Diagnostic> {
1348 bool meshDependency = false;
1349 for (ArtifactId dependency : dependencies) {
1350 const auto found = available.find(dependency);
1351 if (found == available.end())
1353 "artifact dependency is not published or in batch",
1354 "artifact.dependencies");
1355 meshDependency = meshDependency || found->second == ArtifactType::MeshData;
1356 }
1357 if (type == ArtifactType::Collider && !meshDependency)
1359 "collider must depend on a mesh artifact", "artifact.dependencies");
1360 return std::nullopt;
1361 };
1362 for (const GeneratedArtifact &artifact : artifacts) {
1363 if (auto error = checkDependencies(artifact.type, artifact.dependencies))
1364 return eve::Result<std::vector<ArtifactId>>::failure(std::move(*error));
1365 if (artifact.type == ArtifactType::Composite)
1366 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children)
1367 if (auto error = checkDependencies(part.type, part.dependencies))
1368 return eve::Result<std::vector<ArtifactId>>::failure(std::move(*error));
1369 }
1370
1371 try {
1372 artifacts_.reserve(artifacts_.size() + artifacts.size());
1373 std::size_t newParts = 0;
1374 for (const GeneratedArtifact &artifact : artifacts)
1375 if (artifact.type == ArtifactType::Composite)
1376 newParts += std::get<CompositeArtifact>(artifact.payload).children.size();
1377 partOwners_.reserve(partOwners_.size() + newParts);
1378 for (GeneratedArtifact &artifact : artifacts) {
1379 const ArtifactId owner = artifact.id;
1380 if (artifact.type == ArtifactType::Composite)
1381 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children)
1382 partOwners_.emplace(part.id, owner);
1383 artifacts_.emplace(owner, std::move(artifact));
1384 }
1385 } catch (...) {
1386 for (ArtifactId id : ids) artifacts_.erase(id);
1387 for (auto owner = partOwners_.begin(); owner != partOwners_.end();) {
1388 if (std::find(ids.begin(), ids.end(), owner->second) != ids.end())
1389 owner = partOwners_.erase(owner);
1390 else
1391 ++owner;
1392 }
1394 eve::DiagnosticCode::Failed, "artifact store allocation failed; batch rolled back", "artifacts"));
1395 }
1396 return eve::Result<std::vector<ArtifactId>>::success(std::move(ids));
1397}
1398
1400 GeneratedArtifact artifact) {
1401 if (stageActive_)
1403 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "artifact store already has a pending stage"));
1404 std::vector<GeneratedArtifact> artifacts;
1405 try {
1406 artifacts.emplace_back(std::move(artifact));
1407 auto valid = validateBatchAgainstStore(*this, artifacts);
1408 if (!valid.ok())
1410 std::size_t newParts = 0;
1411 for (const auto &entry : artifacts)
1412 if (entry.type == ArtifactType::Composite)
1413 newParts += std::get<CompositeArtifact>(entry.payload).children.size();
1414 // Reserve while the store is still hidden. The no-throw commit only
1415 // transfers already-owned values into these reserved containers.
1416 artifacts_.reserve(artifacts_.size() + artifacts.size());
1417 partOwners_.reserve(partOwners_.size() + newParts);
1418 stageActive_ = true;
1420 std::make_unique<ArtifactStoreStage>(*this, std::move(artifacts)));
1421 } catch (...) {
1422 stageActive_ = false;
1424 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "artifact store stage allocation failed"));
1425 }
1426}
1427
1428void ArtifactStore::commitStaged(std::vector<GeneratedArtifact> artifacts) noexcept {
1429 for (GeneratedArtifact &artifact : artifacts) {
1430 const ArtifactId owner = artifact.id;
1431 if (artifact.type == ArtifactType::Composite)
1432 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children)
1433 partOwners_.emplace(part.id, owner);
1434 artifacts_.emplace(owner, std::move(artifact));
1435 }
1436 stageActive_ = false;
1437}
1438
1439void ArtifactStore::rollbackStaged() noexcept { stageActive_ = false; }
1440
1442 const auto found = artifacts_.find(id);
1443 return found == artifacts_.end() ? nullptr : &found->second;
1444}
1445
1447 const auto owner = partOwners_.find(id);
1448 if (owner == partOwners_.end()) return nullptr;
1449 const GeneratedArtifact *artifact = find(owner->second);
1450 if (!artifact || artifact->type != ArtifactType::Composite) return nullptr;
1451 const auto &children = std::get<CompositeArtifact>(artifact->payload).children;
1452 const auto found =
1453 std::find_if(children.begin(), children.end(), [id](const ArtifactPart &part) { return part.id == id; });
1454 return found == children.end() ? nullptr : &*found;
1455}
1456
1457std::optional<ArtifactType> ArtifactStore::typeOf(ArtifactId id) const noexcept {
1458 const auto artifact = artifacts_.find(id);
1459 if (artifact != artifacts_.end()) return artifact->second.type;
1460 const auto part = partOwners_.find(id);
1461 if (part != partOwners_.end()) {
1462 const auto owner = artifacts_.find(part->second);
1463 if (owner != artifacts_.end() && owner->second.type == ArtifactType::Composite) {
1464 const auto &children = std::get<CompositeArtifact>(owner->second.payload).children;
1465 const auto found = std::find_if(children.begin(), children.end(),
1466 [id](const ArtifactPart &value) { return value.id == id; });
1467 if (found != children.end()) return found->type;
1468 }
1469 }
1470 return std::nullopt;
1471}
1472
1473std::vector<ArtifactId> ArtifactStore::findByBuildKey(const BuildKey &key) const {
1474 std::vector<ArtifactId> result;
1475 for (const auto &entry : artifacts_) {
1476 if (entry.second.buildKey == key) result.push_back(entry.first);
1477 }
1478 std::sort(result.begin(), result.end(),
1479 [](const ArtifactId &lhs, const ArtifactId &rhs) { return lhs.format() < rhs.format(); });
1480 return result;
1481}
1482
1484 const auto found = artifacts_.find(id);
1485 if (found == artifacts_.end()) {
1487 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "artifact identity is not published", "artifact.id"));
1488 }
1489 if (found->second.type == ArtifactType::Composite)
1490 for (const ArtifactPart &part : std::get<CompositeArtifact>(found->second.payload).children)
1491 partOwners_.erase(part.id);
1492 artifacts_.erase(found);
1494}
1495
1496void ArtifactStore::clear() noexcept {
1497 partOwners_.clear();
1498 artifacts_.clear();
1499 stageActive_ = false;
1500}
1501
1502std::size_t ArtifactStore::partCount() const noexcept { return partOwners_.size(); }
1503
1505 try {
1506 std::vector<ArtifactId> ids;
1507 ids.reserve(artifacts_.size());
1508 for (const auto &entry : artifacts_) ids.push_back(entry.first);
1509 std::sort(ids.begin(), ids.end(), [](ArtifactId lhs, ArtifactId rhs) { return lhs.format() < rhs.format(); });
1510
1511 eve::Value::Array records;
1512 records.reserve(ids.size());
1513 for (ArtifactId id : ids) records.emplace_back(encodeArtifact(artifacts_.at(id)));
1515 state.emplace("provider", eve::Value("procgen.cpu-artifact-store"));
1516 state.emplace("version", eve::Value(std::int64_t(2)));
1517 state.emplace("unknownFields", eve::Value("ignore"));
1518 state.emplace("artifacts", eve::Value(std::move(records)));
1520 } catch (...) {
1522 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "artifact store snapshot allocation failed"));
1523 }
1524}
1525
1527 if (stageActive_ || !artifacts_.empty())
1529 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "artifact store restore requires an empty store"));
1530 const auto *object = state.getIf<eve::Value::Object>();
1531 const auto *provider = object ? stateMember(*object, "provider") : nullptr;
1532 const auto *providerName = provider ? provider->getIf<std::string>() : nullptr;
1533 const auto *encodedVersion = object ? stateMember(*object, "version") : nullptr;
1534 const auto *version = encodedVersion ? encodedVersion->getIf<std::int64_t>() : nullptr;
1535 const auto *encodedArtifacts = object ? stateMember(*object, "artifacts") : nullptr;
1536 const auto *encodedList = encodedArtifacts ? encodedArtifacts->getIf<eve::Value::Array>() : nullptr;
1537 if (!object || !providerName || *providerName != "procgen.cpu-artifact-store" || !version || *version != 2 ||
1538 !encodedList)
1540 "artifact store requires dense snapshot version 2"));
1541
1542 std::vector<GeneratedArtifact> candidate;
1543 try {
1544 candidate.reserve(encodedList->size());
1545 for (const eve::Value &encoded : *encodedList) {
1546 GeneratedArtifact artifact;
1547 if (!decodeArtifact(encoded, artifact))
1549 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid dense artifact store record"));
1550 candidate.push_back(std::move(artifact));
1551 }
1552 } catch (...) {
1554 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "artifact store restore allocation failed"));
1555 }
1556 auto valid = validateBatchAgainstStore(*this, candidate);
1557 if (!valid.ok()) return valid;
1558 std::unordered_map<ArtifactId, GeneratedArtifact> restoredArtifacts;
1559 std::unordered_map<ArtifactId, ArtifactId> restoredParts;
1560 try {
1561 restoredArtifacts.reserve(candidate.size());
1562 std::size_t partCount = 0;
1563 for (const GeneratedArtifact &artifact : candidate)
1564 if (artifact.type == ArtifactType::Composite)
1565 partCount += std::get<CompositeArtifact>(artifact.payload).children.size();
1566 restoredParts.reserve(partCount);
1567 for (GeneratedArtifact &artifact : candidate) {
1568 const ArtifactId owner = artifact.id;
1569 if (artifact.type == ArtifactType::Composite)
1570 for (const ArtifactPart &part : std::get<CompositeArtifact>(artifact.payload).children)
1571 restoredParts.emplace(part.id, owner);
1572 restoredArtifacts.emplace(owner, std::move(artifact));
1573 }
1574 } catch (...) {
1576 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "artifact store restore indexing allocation failed"));
1577 }
1578 artifacts_.swap(restoredArtifacts);
1579 partOwners_.swap(restoredParts);
1581}
1582
1584 return generateComposite(params, id, "mesh.hexterrain", generateHexTerrainMesh, makeHexTopology(params),
1585 makeHexAnchors(params));
1586}
1587
1589 return generateComposite(params, id, "mesh.castle", generateCastleMesh, makeCastleTopology(params),
1590 makeCastleAnchors(params));
1591}
1592
1594 if (recipeId == "mesh.hexterrain") return generateHexTerrainArtifact(params, id);
1595 if (recipeId == "mesh.castle") return generateCastleArtifact(params, id);
1596 if (recipeId == "mesh.roadNetwork") return generateRoadNetworkArtifact(params, id);
1597 if (id.isNil())
1598 return rejectedArtifact(eve::DiagnosticCode::InvalidArgument, "top-level artifact identity must not be nil");
1599 const auto key = BuildKey::forRecipe(recipeId, params);
1600 if (!key)
1601 return rejectedArtifact(eve::DiagnosticCode::InvalidArgument, "recipe or parameters cannot form a build key");
1604 std::string error;
1605 if (!MeshRecipeRegistry::instance().generate(std::string(recipeId), params, mesh, error))
1606 return rejectedArtifact(eve::DiagnosticCode::Failed,
1607 error.empty() ? "procedural mesh generation failed" : error);
1608 eve::Value::Object metadata;
1609 metadata.emplace("algorithm", eve::Value(std::string(recipeId)));
1610 metadata.emplace("determinism", eve::Value("seeded_cpu"));
1611 const Bounds bounds = boundsForMesh(mesh);
1612 return makeArtifact(id, ArtifactType::MeshData, eve::SchemaVersion(1), *key, bounds, {}, std::move(metadata),
1613 std::move(mesh));
1614}
1615
1616} // namespace eve::procgen
double value
Value::Object payload
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::map< std::string, std::vector< Key >, std::less<> > channels
std::vector< eve::artifact::PartView > parts
std::string output
int column
std::map< std::string, Var > values
Owning, backend-neutral products published by procedural generation.
std::string message
DiagnosticCode code
std::uint32_t key
vk::UniqueImage image
glm::uvec4 ids
float u
Definition Grass.cpp:233
std::uint32_t height
std::uint32_t width
std::string text
TokenKind kind
std::int32_t parent
std::string name
bool valid
std::string error
Definition Package.cpp:60
Topology topology
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::vector< Point > vertices
float radius
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
std::shared_ptr< const std::vector< glm::vec2 > > points
int detail
Mesh * mesh
double number
bool found
bool boolean
Json object
int children
Definition TreeMesh.cpp:295
float size
Definition TreeMesh.cpp:156
uint32_t index
std::uint32_t depth
glm::vec3 point
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
T takeValue() &&
Move the value out and remove it from this Result.
Definition Result.h:339
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
A prepared consumer mutation with a non-throwing visibility boundary.
Id128 public API.
Definition Identity.h:112
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
Definition Identity.h:151
Id128 child(std::string_view role) const noexcept
Derives a deterministic child UUID for a hierarchical identity.
Definition Identity.h:192
void rollback() noexcept override
Discard the prepared mutation; idempotent and non-throwing.
ArtifactStoreStage(ArtifactStore &owner, std::vector< GeneratedArtifact > artifacts)
void commit() noexcept override
Make the prepared mutation observable; the operation cannot fail.
In-memory owner and publisher for generated artifacts.
std::optional< ArtifactType > typeOf(ArtifactId id) const noexcept
Return the kind of a live identity or composite part.
std::size_t partCount() const noexcept
Return the number of indexed composite child products.
eve::Result< std::unique_ptr< eve::artifact::PreparedPublication > > stagePublish(GeneratedArtifact artifact)
Prepare one store mutation for a larger provider transaction.
const ArtifactPart * findPart(ArtifactId id) const noexcept
Look up a composite child part.
eve::Result< ArtifactId > publish(GeneratedArtifact artifact)
Validate and atomically publish one artifact and all composite parts.
std::vector< ArtifactId > findByBuildKey(const BuildKey &key) const
Find all identities with a matching deterministic build key.
void clear() noexcept
Remove all published artifacts.
const GeneratedArtifact * find(ArtifactId id) const noexcept
Look up a published artifact.
eve::Result< void > restoreState(const eve::Value &state)
Restore complete identity/build-key and typed payload state transactionally.
eve::Result< eve::Value > snapshotState() const
Export identity, build-key and complete typed dense payload state.
eve::Result< std::vector< ArtifactId > > publishBatch(std::vector< GeneratedArtifact > artifacts)
Validate and atomically publish a dependency-connected batch.
eve::Result< void > remove(ArtifactId id)
Remove one artifact; NotFound is returned when absent.
Stable key for deterministic-equivalent generation inputs.
BuildKey()=default
Construct an empty invalid build key.
static std::optional< BuildKey > fromCanonical(std::string_view canonical)
Construct a build key from canonical text.
static std::optional< BuildKey > forRecipe(std::string_view recipeId, const Params &params)
Derive a compact deterministic key for a recipe and parameters.
CPU triangle mesh from procedural mesh recipes (e.g. marching cubes). Positions/normals are xyz-packe...
Definition MeshBuild.h:19
void registerBuiltins()
Registers builtins.
static MeshRecipeRegistry & instance()
Access the process-wide mesh recipe registry.
Owning, typed generation parameters.
Definition Params.h:27
std::vector< ParamSpec > params
EVENGINE_API_DOMAINS eve::Result< GeneratedArtifact > generateRoadNetworkArtifact(const Params &params, ArtifactId id)
Generate a road composite containing mesh, collider, terrain footprint, navigation and placement poin...
eve::Result< GeneratedArtifact > generateHexTerrainArtifact(const Params &params, ArtifactId id)
Generate a hex-terrain composite containing mesh, collider, topology and world anchors.
eve::Result< GeneratedArtifact > generateMeshArtifact(std::string_view recipeId, const Params &params, ArtifactId id)
Generate any registered mesh recipe as a CPU artifact.
ArtifactType artifactType(const GeneratedArtifact::Payload &payload) noexcept
Return the variant kind for a payload.
bool generateHexTerrainMesh(const Params &params, MeshBuild &out, std::string &error)
Build a deterministic, flat-top hexagonal continent mesh.
bool generateCastleMesh(const Params &p, MeshBuild &out, std::string &error)
Build a complete, walkable, multi-ring medieval castle.
const char * artifactTypeName(ArtifactType type) noexcept
Return the stable textual spelling of an artifact kind.
eve::Result< ArtifactPart > makeArtifactPart(std::string role, ArtifactId id, ArtifactType type, eve::SchemaVersion schemaVersion, BuildKey buildKey, Bounds bounds, std::vector< ArtifactId > dependencies, eve::Value::Object metadata, ArtifactLeafPayload payload)
Validate and construct a leaf part for a composite artifact.
MeshBuild MeshData
Existing dense CPU mesh type under the artifact vocabulary.
ArtifactType
Runtime payload kind; it is checked against the variant on publish.
eve::ArtifactId ArtifactId
Common strong UUID identity for one published procedural artifact.
std::variant< Grid2D, PointSet, MeshData, ImageData, Collider > ArtifactLeafPayload
Leaf payloads used by a CompositeArtifact.
eve::Result< GeneratedArtifact > generateCastleArtifact(const Params &params, ArtifactId id)
Generate a castle composite containing mesh, collider, ring topology and gate/keep anchors.
eve::Result< GeneratedArtifact > makeArtifact(ArtifactId id, ArtifactType type, eve::SchemaVersion schemaVersion, BuildKey buildKey, Bounds bounds, std::vector< ArtifactId > dependencies, eve::Value::Object metadata, GeneratedArtifact::Payload payload)
Validate and construct one artifact record.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
WidgetDesc grid(int columns, std::vector< WidgetDesc > children, std::string id)
Fixed-column grid; children are placed in source order.
Definition Widget.cpp:687
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
One typed, role-named child product inside a composite artifact.
eve::SchemaVersion schemaVersion
std::vector< ArtifactId > dependencies
Axis-aligned bounds in the artifact's declared world coordinate space.
void include(float x, float y, float z) noexcept
Expand this box to include one point.
std::vector< float > vertices
std::vector< std::uint32_t > indices
bool isValid() const noexcept
Return whether the collider has complete triangle data.
A coherent set of products published from one deterministic build.
const ArtifactPart * find(std::string_view role) const noexcept
Find a child by its stable role.
Complete immutable-once-published artifact record.
std::variant< Grid2D, PointSet, MeshData, ImageData, Collider, CompositeArtifact > Payload
std::vector< ArtifactId > dependencies
std::vector< std::uint8_t > pixels
bool isValid() const noexcept
Return whether dimensions and pixel storage describe an image.
std::string id
Definition Widget.h:17
glm::vec4 bounds