载入中...
搜索中...
未找到
LevelFormat.cpp
浏览该文件的文档.
2#include "common/Json.h"
4
5#include <algorithm>
6#include <atomic>
7#include <bit>
8#include <charconv>
9#include <chrono>
10#include <cmath>
11#include <filesystem>
12#include <fstream>
13#include <limits>
14#include <map>
15#include <set>
16#include <sstream>
17
18#if defined(_WIN32)
19#ifndef NOMINMAX
20#define NOMINMAX
21#endif
22#include <windows.h>
23#endif
24
25namespace eve::level_editing {
26namespace {
29
30bool validProperties(eve::json::Value value) {
31 if (!value) return true;
32 if (value.isObject()) {
33 for (const auto& key : value.keys()) {
34 auto item = value.get(key.c_str());
35 if (!item.isString() && !item.isBool() && !item.isNumber()) return false;
36 }
37 return true;
38 }
39 if (!value.isArray()) return false;
40 std::set<std::string> names;
41 for (size_t index = 0; index < value.size(); ++index) {
42 auto item = value.at(index);
43 if (!item.isObject() || !item.get("name").isString() || !item.get("value") ||
44 !names.insert(item.getString("name")).second)
45 return false;
46 }
47 return true;
48}
49
50bool validNumbers(eve::json::Value value, std::initializer_list<const char*> fields) {
51 for (const auto* key : fields) {
52 auto item = value.get(key);
53 if (item && (!item.isNumber() || !std::isfinite(item.asFloat()))) return false;
54 }
55 return true;
56}
57
58bool validId(const eve::Value& object, bool tiled) {
59 const auto* id = object.find("id");
60 if (!id) return true;
61 return tiled ? id->isInt64() && id->asInt() > 0 && id->asInt() < std::numeric_limits<int>::max()
62 : id->isString() && !id->asString().empty();
63}
64
65eve::Value::Object extraFields(const eve::Value& value, std::initializer_list<const char*> known) {
66 Object out;
67 if (const auto* fields = value.getIf<Object>()) {
68 for (const auto& [key, field] : *fields) {
69 if (std::none_of(known.begin(), known.end(), [&](const char* name) { return key == name; }))
70 out.emplace(key, field);
71 }
72 }
73 return out;
74}
75
76// String properties remain in the existing editable map; other typed properties
77// retain their complete Tiled records, without a second copy of editable values.
78void readProperties(eve::json::Value v, const eve::Value* owned, std::unordered_map<std::string, std::string>& out,
79 Object& extras) {
80 extras.erase("properties");
81 if (v.isObject()) {
82 Object typed;
83 for (const auto& k : v.keys()) {
84 if (v.get(k.c_str()).isString())
85 out[k] = v.get(k.c_str()).asString();
86 else if (owned)
87 typed[k] = *owned->find(k);
88 }
89 if (!typed.empty()) extras["properties"] = std::move(typed);
90 } else if (v.isArray()) {
91 Array typed;
92 for (size_t i = 0; i < v.size(); ++i) {
93 auto p = v.at(i);
94 if (p.getString("type", "string") == "string" && p.get("value").isString() && p.keys().size() == 3)
95 out[p.getString("name")] = p.get("value").asString();
96 else if (owned)
97 typed.push_back(owned->at(i));
98 }
99 if (!typed.empty()) extras["properties"] = std::move(typed);
100 }
101}
102
103eve::Value propertyValue(const std::unordered_map<std::string, std::string>& properties, const Object& extras,
104 bool tiled) {
105 const auto found = extras.find("properties");
106 if (!tiled && (found == extras.end() || found->second.isObject())) {
107 Object out = found == extras.end() ? Object{} : *found->second.getIf<Object>();
108 for (const auto& [key, value] : properties) out[key] = value;
109 return out;
110 }
111 Array out;
112 if (found != extras.end() && found->second.isArray()) {
113 for (const auto& item : *found->second.getIf<Array>()) {
114 const auto* name = item.find("name");
115 if (!name || !name->isString() || !properties.contains(name->asString())) out.push_back(item);
116 }
117 }
118 if (found != extras.end() && found->second.isObject()) {
119 for (const auto& [key, value] : *found->second.getIf<Object>()) {
120 if (properties.contains(key)) continue;
121 out.emplace_back(Object{{"name", key},
122 {"type", value.isBool() ? "bool"
123 : value.isInt64() ? "int"
124 : "float"},
125 {"value", value}});
126 }
127 }
128 // Sort the editable map before emission to make saves deterministic.
129 for (const auto& [key, value] : std::map<std::string, std::string>(properties.begin(), properties.end()))
130 out.emplace_back(Object{{"name", key}, {"type", "string"}, {"value", value}});
131 return out;
132}
133
134std::int64_t numericId(const std::string& id) {
135 std::int64_t value = 0;
136 auto parsed = std::from_chars(id.data(), id.data() + id.size(), value);
137 return parsed.ec == std::errc{} && parsed.ptr == id.data() + id.size() && value > 0 &&
138 value < std::numeric_limits<int>::max()
139 ? value
140 : 0;
141}
142} // namespace
143
144// Sole codec for the owned document and its unmodeled format fields.
146public:
147 static Object& extensions(LevelDocument& document) { return document.extensions_; }
148 static const Object& extensions(const LevelDocument& document) { return document.extensions_; }
149};
150
151namespace {
152class JsonLevelFormat final : public LevelFormat {
153public:
154 std::string id() const override { return "eve.level"; }
155 std::vector<std::string> extensions() const override { return {".level.json", ".evelevel"}; }
156 bool canRead(const std::string& s) const override {
158 return d.valid() && d.root().getString("format") == "eve.level";
159 }
160 eve::Result<std::unique_ptr<LevelDocument>> read(const std::string& s) const override { return readJson(s, false); }
161 eve::Result<std::string> write(const LevelDocument& d) const override { return writeJson(d, false); }
162 static eve::Result<std::unique_ptr<LevelDocument>> readJson(const std::string& s, bool tiled) {
163 std::string error;
164 auto json = eve::json::Document::parse(s, &error);
165 if (!json.valid())
169 if (!owned.ok()) return eve::Result<std::unique_ptr<LevelDocument>>::failure(owned.status());
170 const auto fail = [](const std::string& message, eve::DiagnosticCode code = eve::DiagnosticCode::Unsupported) {
172 eve::Diagnostic::error(code, message, {}, {}, "editor.level"));
173 };
174 auto r = json.root();
175 if (!r.isObject()) return fail("Map root must be an object", eve::DiagnosticCode::ParseError);
176 const auto* version = owned.value().find("version");
177 if (!tiled &&
178 (r.getString("format") != "eve.level" || (version && (!version->isInt64() || version->asInt() != 1))))
179 return fail("Unsupported eve.level schema version");
180 if (tiled && r.getString("type") != "map") return fail("Expected a Tiled map");
181 if (r.getBool("infinite", false)) return fail("Infinite maps are not yet editable by LevelDocument");
182 if (!validProperties(r.get("properties")) || !validNumbers(r, {"tilewidth", "tileheight"}))
183 return fail("Invalid map properties or tile dimensions", eve::DiagnosticCode::ParseError);
184 int w = r.getInt("width"), h = r.getInt("height");
185 const auto* widthValue = owned.value().find("width");
186 const auto* heightValue = owned.value().find("height");
187 if (!widthValue || !heightValue || !widthValue->isInt64() || !heightValue->isInt64())
188 return fail("Map dimensions must be integers", eve::DiagnosticCode::ParseError);
189 float tw = r.getFloat("tilewidth", 32), th = r.getFloat("tileheight", 32);
190 if (w < 1 || h < 1 || int64_t(w) * h > 16 * 1024 * 1024 || !std::isfinite(tw) || !std::isfinite(th) ||
191 tw <= 0 || th <= 0) {
194 "map dimensions must be positive, finite and within the 16M-cell budget", {}, {}, "editor.level"));
195 }
196 const std::string orientation = r.getString("orientation", "orthogonal");
197 if (orientation != "orthogonal" && orientation != "isometric" && orientation != "staggered" &&
198 orientation != "hexagonal")
199 return fail("Unsupported map orientation");
200 auto out = std::make_unique<LevelDocument>(w, h, tw, th);
201 out->setOrientation(orientation);
202 LevelFormatCodec::extensions(*out) = extraFields(
203 owned.value(),
204 {"format", "version", "type", "width", "height", "tilewidth", "tileheight", "orientation", "layers"});
205 readProperties(r.get("properties"), owned.value().find("properties"), out->properties(),
207 auto ls = r.get("layers");
208 if (!ls.isArray()) return fail("Map layers must be an array", eve::DiagnosticCode::ParseError);
209 std::set<std::string> layerIds, objectIds;
210 for (size_t i = 0; i < ls.size(); ++i) {
211 auto v = ls.at(i);
212 std::string kind = v.getString("type");
213 bool obj = kind == "objectgroup" || kind == "objects";
214 if (!v.isObject() || (!obj && kind != "tilelayer" && kind != "tiles"))
215 return fail("Unsupported layer type: " + kind);
216 if (!validId(owned.value().find("layers")->at(i), tiled) || !validProperties(v.get("properties")) ||
217 !validNumbers(v, {"opacity", "offsetx", "offsety"}) || v.getFloat("opacity", 1) < 0 ||
218 v.getFloat("opacity", 1) > 1)
219 return fail("Invalid layer identity, properties or presentation", eve::DiagnosticCode::ParseError);
220 if (v.get("chunks") || v.get("encoding") || v.get("compression"))
221 return fail("Chunked or encoded tile layers require the runtime importer; editing is not supported");
222 if (!obj && (v.getInt("width", w) != w || v.getInt("height", h) != h))
223 return fail("Layer dimensions must match the editable map");
224 int li = obj ? out->addObjectLayer(v.getString("name")) : out->addTileLayer(v.getString("name"));
225 auto layerRef = out->layer(li);
226 auto* l = &layerRef->get();
227 l->id = tiled ? std::to_string(v.getInt("id", int(i + 1))) : v.getString("id", l->id);
228 if (!layerIds.insert(l->id).second) return fail("Duplicate layer id", eve::DiagnosticCode::ParseError);
229 l->extensions = extraFields(owned.value().find("layers")->at(i),
230 {"id", "name", "type", "visible", "locked", "opacity", "offsetx", "offsety",
231 "objects", "data", "width", "height"});
232 l->locked = v.getBool("locked", false);
233 l->visible = v.getBool("visible", true);
234 l->opacity = v.getFloat("opacity", 1);
235 l->offsetX = v.getFloat("offsetx", 0);
236 l->offsetY = v.getFloat("offsety", 0);
237 readProperties(v.get("properties"), owned.value().find("layers")->at(i).find("properties"), l->properties,
238 l->extensions);
239 if (obj) {
240 auto os = v.get("objects");
241 if (!os.isArray()) return fail("Objects must be an array", eve::DiagnosticCode::ParseError);
242 for (size_t j = 0; j < os.size(); ++j) {
243 auto q = os.at(j);
244 if (!q.isObject()) return fail("Map object must be an object", eve::DiagnosticCode::ParseError);
245 if (!validId(owned.value().find("layers")->at(i).find("objects")->at(j), tiled) ||
246 !validProperties(q.get("properties")) ||
247 !validNumbers(q, {"x", "y", "width", "height", "rotation"}))
248 return fail("Invalid object identity, properties or transform",
250 const std::string objectClass = q.getString("class");
251 int oi = out->addObject(li, objectClass.empty() ? q.getString("type") : objectClass,
252 q.getFloat("x"), q.getFloat("y"));
253 auto objectRef = out->object(li, oi);
254 auto* ob = &objectRef->get();
255 ob->id = tiled ? std::to_string(q.getInt("id", int(j + 1))) : q.getString("id", ob->id);
256 if (!objectIds.insert(ob->id).second)
257 return fail("Duplicate object id", eve::DiagnosticCode::ParseError);
258 const auto& ownedObject = owned.value().find("layers")->at(i).find("objects")->at(j);
259 ob->extensions = extraFields(ownedObject, {"id", "name", "type", "class", "x", "y", "width",
260 "height", "rotation", "visible"});
261 ob->name = q.getString("name");
262 ob->width = q.getFloat("width");
263 ob->height = q.getFloat("height");
264 ob->rotation = q.getFloat("rotation");
265 ob->visible = q.getBool("visible", true);
266 readProperties(q.get("properties"), ownedObject.find("properties"), ob->properties, ob->extensions);
267 }
268 } else {
269 auto data = v.get("data");
270 if (!data.isArray() || data.size() != size_t(w) * size_t(h))
271 return fail("Tile data must contain exactly width * height GIDs", eve::DiagnosticCode::ParseError);
272 for (size_t n = 0; n < data.size(); ++n) {
273 const auto& raw = owned.value().find("layers")->at(i).find("data")->at(n);
274 // Native v1 historically emitted signed int GIDs. Accept
275 // that representation only on native import; emit uint32.
276 const int64_t minimumGid = tiled ? 0 : std::numeric_limits<int32_t>::min();
277 if (!raw.isInt64() || raw.asInt() < minimumGid ||
278 raw.asInt() > std::numeric_limits<uint32_t>::max())
279 return fail("Tile GID must be an unsigned 32-bit integer", eve::DiagnosticCode::ParseError);
280 l->tiles->setGid(int(n) % w, int(n) / w, std::bit_cast<int>(uint32_t(raw.asInt())));
281 }
282 }
283 }
284 return eve::Result<std::unique_ptr<LevelDocument>>::success(std::move(out));
285 }
286 static eve::Result<std::string> writeJson(const LevelDocument& d, bool tiled) {
288 if (!tiled)
289 root["format"] = "eve.level";
290 else
291 root["type"] = "map";
292 root["version"] = tiled ? eve::Value("1.10") : eve::Value(1);
293 root["orientation"] = d.getOrientation();
294 root["width"] = d.getWidth();
295 root["height"] = d.getHeight();
296 root["tilewidth"] = d.getTileWidth();
297 root["tileheight"] = d.getTileHeight();
298 root["properties"] = propertyValue(d.properties(), LevelFormatCodec::extensions(d), tiled);
299 int64_t nextLayer = 1, nextObject = 1;
300 for (const auto& layer : d.layers()) {
301 nextLayer = std::max(nextLayer, numericId(layer.id) + 1);
302 for (const auto& object : layer.objects) nextObject = std::max(nextObject, numericId(object.id) + 1);
303 }
304 Array layers;
305 for (const auto& layer : d.layers()) {
306 Object value = layer.extensions;
307 value["id"] =
308 tiled ? eve::Value(numericId(layer.id) ? numericId(layer.id) : nextLayer++) : eve::Value(layer.id);
309 value["name"] = layer.name;
310 value["type"] = layer.kind == LevelLayer::Kind::Tiles ? "tilelayer" : "objectgroup";
311 value["visible"] = layer.visible;
312 value["locked"] = layer.locked;
313 value["opacity"] = layer.opacity;
314 value["offsetx"] = layer.offsetX;
315 value["offsety"] = layer.offsetY;
316 value["properties"] = propertyValue(layer.properties, layer.extensions, tiled);
317 if (layer.tiles) {
318 value["width"] = d.getWidth();
319 value["height"] = d.getHeight();
320 Array data;
321 for (int y = 0; y < d.getHeight(); ++y)
322 for (int x = 0; x < d.getWidth(); ++x)
323 data.emplace_back(int64_t(uint32_t(layer.tiles->getGid(x, y))));
324 value["data"] = std::move(data);
325 } else {
326 Array objects;
327 for (const auto& object : layer.objects) {
328 Object item = object.extensions;
329 item["id"] = tiled ? eve::Value(numericId(object.id) ? numericId(object.id) : nextObject++)
330 : eve::Value(object.id);
331 item["name"] = object.name;
332 item["type"] = object.type;
333 if (tiled) item["class"] = object.type;
334 item["x"] = object.x;
335 item["y"] = object.y;
336 item["width"] = object.width;
337 item["height"] = object.height;
338 item["rotation"] = object.rotation;
339 item["visible"] = object.visible;
340 item["properties"] = propertyValue(object.properties, object.extensions, tiled);
341 objects.emplace_back(std::move(item));
342 }
343 value["objects"] = std::move(objects);
344 }
345 layers.emplace_back(std::move(value));
346 }
347 if (tiled) {
348 root["nextlayerid"] = nextLayer;
349 root["nextobjectid"] = nextObject;
350 }
351 root["layers"] = std::move(layers);
353 }
354};
355class TiledFormat final : public LevelFormat {
356public:
357 std::string id() const override { return "tiled.json"; }
358 std::vector<std::string> extensions() const override { return {".tmj", ".json"}; }
359 bool canRead(const std::string& s) const override {
361 return d.valid() && d.root().getString("type") == "map";
362 }
363 eve::Result<std::unique_ptr<LevelDocument>> read(const std::string& s) const override {
364 return JsonLevelFormat::readJson(s, true);
365 }
366 eve::Result<std::string> write(const LevelDocument& d) const override {
367 return JsonLevelFormat::writeJson(d, true);
368 }
369};
370} // namespace
371
373 registerFormat(std::make_unique<JsonLevelFormat>()).expect("register native level format");
374 registerFormat(std::make_unique<TiledFormat>()).expect("register Tiled level format");
375}
377 if (!f)
379 eve::DiagnosticCode::InvalidArgument, "level format must not be null", {}, {}, "editor.level"));
380 for (auto& i : formats_)
381 if (i->id() == f->id()) {
382 i = std::move(f);
384 }
385 formats_.push_back(std::move(f));
387}
388std::string LevelFormatRegistry::getFormatId(int i) const {
389 return i < 0 || i >= int(formats_.size()) ? std::string() : formats_[i]->id();
390}
391eve::OptionalRef<const LevelFormat> LevelFormatRegistry::find(const std::string& id) const {
392 for (auto& i : formats_)
393 if (i->id() == id) return std::cref(*i);
394 return {};
395}
396std::string LevelFormatRegistry::detect(const std::string& p, const std::string& t) const {
397 for (auto& i : formats_)
398 if (i->canRead(t)) return i->id();
399 for (auto& i : formats_)
400 for (auto& e : i->extensions())
401 if (p.size() >= e.size() && p.compare(p.size() - e.size(), e.size(), e) == 0) return i->id();
402 return {};
403}
405 const std::string& t) const {
406 auto f = find(id);
407 if (!f)
409 eve::DiagnosticCode::Unsupported, "unknown level format: " + id, {}, {}, "editor.level"));
410 return f->get().read(t);
411}
413 auto f = find(id);
414 if (!f)
416 eve::DiagnosticCode::Unsupported, "unknown level format: " + id, {}, {}, "editor.level"));
417 return f->get().write(d);
418}
420 const std::string& id) const {
421 std::ifstream f(p, std::ios::binary);
422 if (!f) {
424 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "cannot open level", p, {}, "editor.level"));
425 }
426 std::ostringstream s;
427 s << f.rdbuf();
428 auto fmt = id.empty() ? detect(p, s.str()) : id;
429 return decode(fmt, s.str());
430}
431eve::Result<void> LevelFormatRegistry::save(const std::string& p, const LevelDocument& d, const std::string& id) const {
432 auto fmt = id.empty() ? detect(p, "") : id;
433 if (fmt.empty()) fmt = "eve.level";
434 auto encoded = encode(fmt, d);
435 if (!encoded) return eve::Result<void>::failure(encoded.status());
436 const std::filesystem::path destination(p);
437 // A uniquely created sibling directory reserves our temporary path. The
438 // rename stays on the destination filesystem and never truncates its file.
439 static std::atomic<unsigned long long> sequence{0};
440 std::filesystem::path staging;
441 std::error_code error;
442 for (int attempt = 0; attempt < 8; ++attempt) {
443 staging = destination;
444 staging += ".save-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
445 std::to_string(sequence.fetch_add(1));
446 if (std::filesystem::create_directory(staging, error)) break;
447 staging.clear();
448 if (error) break;
449 }
450 if (staging.empty())
452 eve::DiagnosticCode::Failed, "cannot create save staging directory", p, {}, "editor.level"));
453 struct StagingCleanup {
454 std::filesystem::path path;
455 ~StagingCleanup() {
456 std::error_code ignored;
457 std::filesystem::remove_all(path, ignored);
458 }
459 } cleanup{staging};
460 const auto temporary = staging / "document.json";
461 std::ofstream f(temporary, std::ios::binary);
462 if (!f) {
464 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot write level", p, {}, "editor.level"));
465 }
466 f << encoded.value();
467 f.close();
468 if (!f)
470 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "failed while writing level", p, {}, "editor.level"));
471#if defined(_WIN32)
472 if (!MoveFileExW(temporary.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
474 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot replace level file", p, {}, "editor.level"));
475#else
476 std::filesystem::rename(temporary, destination, error);
477 if (error)
479 eve::DiagnosticCode::Failed, "cannot replace level file: " + error.message(), p, {}, "editor.level"));
480#endif
482}
483} // namespace eve::level_editing
double value
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
ActiveSource owned
const std::string & s
glm::vec4 p[6]
std::string message
DiagnosticCode code
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
std::array< double, 10 > q
double r
float v
std::int32_t second
int h
TokenKind kind
std::map< std::string, PropertyRule > properties
std::string name
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
std::string error
Definition Package.cpp:60
TileLayer * layer
float f
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::string id
Definition PlayHost.cpp:108
float begin
float d
float t
bool found
std::string string
std::uint32_t count
Json object
float size
Definition TreeMesh.cpp:156
uint32_t index
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
void expect(std::string_view message) const
Require success for a void operation.
Definition Result.h:548
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
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
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
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
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
Format-neutral, editable top-down level model.
static const Object & extensions(const LevelDocument &document)
static Object & extensions(LevelDocument &document)
std::string getFormatId(int index) const
Returns the format id.
eve::Result< std::unique_ptr< LevelDocument > > load(const std::string &path, const std::string &format={}) const
Loads .
eve::Result< void > registerFormat(std::unique_ptr< LevelFormat > format)
Registers format.
LevelFormatRegistry()
Level format registry.
eve::Result< void > save(const std::string &path, const LevelDocument &level, const std::string &format={}) const
Encode then atomically replace a file; failures preserve an existing destination.
std::string detect(const std::string &path, const std::string &text) const
Detect.
eve::Result< std::string > encode(const std::string &format, const LevelDocument &level) const
Encode.
eve::Result< std::unique_ptr< LevelDocument > > decode(const std::string &format, const std::string &text) const
Decode.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
Result< std::string > stringify(const eve::Value &value)
Serialize the canonical owning Value as deterministic compact JSON.
Definition Json.cpp:563
T read(const RuntimeTensor &v, size_t i)
Reads read.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
std::optional< std::reference_wrapper< T > > OptionalRef
Optional borrowed reference; it does not extend T's lifetime.
Definition BorrowedRef.h:24
enum EVENGINE_API_FOUNDATION Array
OT_ARRAY (editable via Runtime array APIs / ReflectedPropertyModel).
Definition Runtime.h:120