MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
StateDecoder.cpp
Go to the documentation of this file.
1#include "StateDecoder.hpp"
2
6
7namespace MayaFlux::Nexus {
8
9namespace {
10
11 constexpr uint32_t k_exr_rows = 3;
12 constexpr uint32_t k_channels = 4;
13
14 // -------------------------------------------------------------------------
15 // Schema structs (mirrored from StateEncoder.cpp until centralized)
16 // -------------------------------------------------------------------------
17
18 struct Range {
19 float min { 0.0F };
20 float max { 1.0F };
21
22 static constexpr auto describe()
23 {
24 return std::make_tuple(
25 IO::member("min", &Range::min),
26 IO::member("max", &Range::max));
27 }
28 };
29
30 struct RangeSet {
31 Range pos_x, pos_y, pos_z;
32 Range intensity;
34 Range size;
35 Range radius;
37
38 static constexpr auto describe()
39 {
40 return std::make_tuple(
41 IO::member("position.x", &RangeSet::pos_x),
42 IO::member("position.y", &RangeSet::pos_y),
43 IO::member("position.z", &RangeSet::pos_z),
44 IO::member("intensity", &RangeSet::intensity),
45 IO::member("color.r", &RangeSet::color_r),
46 IO::member("color.g", &RangeSet::color_g),
47 IO::member("color.b", &RangeSet::color_b),
48 IO::member("size", &RangeSet::size),
49 IO::member("radius", &RangeSet::radius),
50 IO::member("query_radius", &RangeSet::query_radius));
51 }
52 };
53
54 struct WiringStep {
55 glm::vec3 position {};
56 double delay_seconds { 0.0 };
57
58 static constexpr auto describe()
59 {
60 return std::make_tuple(
61 IO::member("position", &WiringStep::position),
62 IO::member("delay", &WiringStep::delay_seconds));
63 }
64 };
65
66 struct WiringRecord {
67 std::string kind { "commit_driven" };
68 std::optional<double> interval;
69 std::optional<double> duration;
70 std::optional<size_t> times;
71 std::optional<std::vector<WiringStep>> steps;
72
73 static constexpr auto describe()
74 {
75 return std::make_tuple(
76 IO::member("kind", &WiringRecord::kind),
77 IO::opt_member("interval", &WiringRecord::interval),
78 IO::opt_member("duration", &WiringRecord::duration),
79 IO::opt_member("times", &WiringRecord::times),
80 IO::opt_member("steps", &WiringRecord::steps));
81 }
82 };
83
84 struct EntityRecord {
85 uint32_t id {};
86 std::string kind;
87 glm::vec3 position {};
88 float intensity { 0.0F };
89 float radius { 0.0F };
90 float query_radius { 0.0F };
91 std::optional<glm::vec3> color;
92 std::optional<float> size;
93 std::string influence_fn_name;
94 std::string perception_fn_name;
95 WiringRecord wiring;
96
97 static constexpr auto describe()
98 {
99 return std::make_tuple(
100 IO::member("id", &EntityRecord::id),
101 IO::member("kind", &EntityRecord::kind),
102 IO::member("position", &EntityRecord::position),
103 IO::member("intensity", &EntityRecord::intensity),
104 IO::member("radius", &EntityRecord::radius),
105 IO::member("query_radius", &EntityRecord::query_radius),
106 IO::opt_member("color", &EntityRecord::color),
107 IO::opt_member("size", &EntityRecord::size),
108 IO::member("influence_fn_name", &EntityRecord::influence_fn_name),
109 IO::member("perception_fn_name", &EntityRecord::perception_fn_name),
110 IO::member("wiring", &EntityRecord::wiring));
111 }
112 };
113
114 struct FabricSchema {
115 uint32_t version { 0 };
116 std::string fabric_name;
117 std::vector<EntityRecord> entities;
118 RangeSet ranges;
119
120 static constexpr auto describe()
121 {
122 return std::make_tuple(
123 IO::member("version", &FabricSchema::version),
124 IO::member("fabric_name", &FabricSchema::fabric_name),
125 IO::member("entities", &FabricSchema::entities),
126 IO::member("ranges", &FabricSchema::ranges));
127 }
128 };
129
130 // -------------------------------------------------------------------------
131 // Helpers
132 // -------------------------------------------------------------------------
133
134 float denormalize(float norm, const Range& r)
135 {
136 return r.min + norm * (r.max - r.min);
137 }
138
139 Fabric::Kind parse_kind(const std::string& s)
140 {
141 if (s == "sensor") {
143 }
144 if (s == "agent") {
145 return Fabric::Kind::Agent;
146 }
148 }
149
150 bool kind_known(const std::string& s)
151 {
152 return s == "emitter" || s == "sensor" || s == "agent";
153 }
154
155 void apply_wiring(Wiring wiring, const WiringRecord& rec, std::vector<std::string>& warnings)
156 {
157 if (rec.kind == "every") {
158 wiring.every(*rec.interval);
159 if (rec.duration) {
160 wiring.for_duration(*rec.duration);
161 }
162 if (rec.times && *rec.times > 1) {
163 wiring.times(*rec.times);
164 }
165 } else if (rec.kind == "move_to") {
166 if (rec.steps) {
167 for (const auto& s : *rec.steps) {
168 wiring.move_to(s.position, s.delay_seconds);
169 }
170 }
171 if (rec.times && *rec.times > 1) {
172 wiring.times(*rec.times);
173 }
174 } else if (rec.kind != "commit_driven") {
175 warnings.emplace_back("Unsupported wiring kind '" + rec.kind + "'; falling back to commit_driven");
176 }
177 wiring.finalise();
178 }
179
180 // -------------------------------------------------------------------------
181 // EXR load + validation, shared by decode() and reconstruct()
182 // -------------------------------------------------------------------------
183
184 struct PixelView {
185 IO::ImageData image;
186 const std::vector<float>* pixels { nullptr };
187 uint32_t width { 0 };
188 };
189
190 std::optional<PixelView> load_exr(
191 const std::string& exr_path,
192 uint32_t expected_entity_count,
193 uint32_t expected_rows,
194 std::string& error_out)
195 {
196 auto image_opt = IO::ImageReader::load(exr_path, 0);
197 if (!image_opt) {
198 error_out = "Failed to load EXR: " + exr_path;
199 return std::nullopt;
200 }
201
202 const auto* pixels = image_opt->as_float();
203 if (!pixels || pixels->empty()) {
204 error_out = "EXR has no float pixel data: " + exr_path;
205 return std::nullopt;
206 }
207 if (image_opt->channels != k_channels) {
208 error_out = "EXR channel count mismatch: expected "
209 + std::to_string(k_channels) + " got " + std::to_string(image_opt->channels);
210 return std::nullopt;
211 }
212 if (image_opt->height != expected_rows) {
213 error_out = "EXR row count mismatch: expected "
214 + std::to_string(expected_rows) + " got " + std::to_string(image_opt->height);
215 return std::nullopt;
216 }
217 if (image_opt->width != expected_entity_count) {
218 error_out = "EXR width (" + std::to_string(image_opt->width)
219 + ") does not match entity count (" + std::to_string(expected_entity_count) + ")";
220 return std::nullopt;
221 }
222
223 const uint32_t w = image_opt->width;
224 return PixelView { std::move(*image_opt), nullptr, w };
225 }
226
227} // namespace
228
229// -------------------------------------------------------------------------
230// decode()
231// -------------------------------------------------------------------------
232
233bool StateDecoder::decode(Fabric& fabric, const std::string& base_path)
234{
235 m_last_error.clear();
236 m_patched_count = 0;
237 m_missing_count = 0;
238
239 const std::string json_path = base_path + ".json";
240 const std::string exr_path = base_path + ".exr";
241
243 auto schema_opt = ser.read<FabricSchema>(json_path);
244 if (!schema_opt) {
245 m_last_error = "Failed to load schema: " + ser.last_error();
247 return false;
248 }
249 const auto& schema = *schema_opt;
250
251 if (schema.version < 2 || schema.version > 4) {
252 m_last_error = "Unsupported schema version: " + std::to_string(schema.version);
254 return false;
255 }
256
257 if (schema.entities.empty()) {
258 m_last_error = "Schema contains no entities: " + json_path;
260 return false;
261 }
262
263 const uint32_t expected_rows = schema.version >= 4 ? 5 : k_exr_rows;
264 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), expected_rows, m_last_error);
265 if (!pv_opt) {
267 return false;
268 }
269 auto& pv = *pv_opt;
270 const auto* pixels = pv.image.as_float();
271 const uint32_t width = pv.width;
272 const auto& r = schema.ranges;
273
274 for (size_t i = 0; i < schema.entities.size(); ++i) {
275 const auto& entry = schema.entities[i];
276
277 if (!kind_known(entry.kind)) {
279 "StateDecoder: unknown kind '{}' for id {}, skipping", entry.kind, entry.id);
281 continue;
282 }
283
284 const size_t row0 = (static_cast<size_t>(0) * width + i) * k_channels;
285 const size_t row1 = (static_cast<size_t>(1) * width + i) * k_channels;
286 const size_t row2 = (static_cast<size_t>(2) * width + i) * k_channels;
287
288 const glm::vec3 position {
289 denormalize((*pixels)[row0 + 0], r.pos_x),
290 denormalize((*pixels)[row0 + 1], r.pos_y),
291 denormalize((*pixels)[row0 + 2], r.pos_z),
292 };
293
294 switch (parse_kind(entry.kind)) {
296 auto e = fabric.get_emitter(entry.id);
297 if (!e) {
299 "StateDecoder: id {} not found as Emitter, skipping", entry.id);
301 continue;
302 }
303 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
305 "StateDecoder: Emitter {} fn_name mismatch: schema='{}' live='{}'",
306 entry.id, entry.influence_fn_name, e->fn_name());
307 }
308 e->set_position(position);
309 e->set_intensity(denormalize((*pixels)[row0 + 3], r.intensity));
310 if (entry.color) {
311 e->set_color(glm::vec3 {
312 denormalize((*pixels)[row1 + 0], r.color_r),
313 denormalize((*pixels)[row1 + 1], r.color_g),
314 denormalize((*pixels)[row1 + 2], r.color_b),
315 });
316 }
317 if (entry.size) {
318 e->set_size(denormalize((*pixels)[row1 + 3], r.size));
319 }
320 e->set_radius(denormalize((*pixels)[row2 + 0], r.radius));
321 break;
322 }
324 auto s = fabric.get_sensor(entry.id);
325 if (!s) {
327 "StateDecoder: id {} not found as Sensor, skipping", entry.id);
329 continue;
330 }
331 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
333 "StateDecoder: Sensor {} fn_name mismatch: schema='{}' live='{}'",
334 entry.id, entry.perception_fn_name, s->fn_name());
335 }
336 s->set_position(position);
337 s->set_query_radius(denormalize((*pixels)[row2 + 1], r.query_radius));
338 break;
339 }
340 case Fabric::Kind::Agent: {
341 auto a = fabric.get_agent(entry.id);
342 if (!a) {
344 "StateDecoder: id {} not found as Agent, skipping", entry.id);
346 continue;
347 }
348 if (!entry.perception_fn_name.empty() && a->perception_fn_name() != entry.perception_fn_name) {
350 "StateDecoder: Agent {} perception_fn_name mismatch: schema='{}' live='{}'",
351 entry.id, entry.perception_fn_name, a->perception_fn_name());
352 }
353 if (!entry.influence_fn_name.empty() && a->influence_fn_name() != entry.influence_fn_name) {
355 "StateDecoder: Agent {} influence_fn_name mismatch: schema='{}' live='{}'",
356 entry.id, entry.influence_fn_name, a->influence_fn_name());
357 }
358 a->set_position(position);
359 a->set_intensity(denormalize((*pixels)[row0 + 3], r.intensity));
360 if (entry.color) {
361 a->set_color(glm::vec3 {
362 denormalize((*pixels)[row1 + 0], r.color_r),
363 denormalize((*pixels)[row1 + 1], r.color_g),
364 denormalize((*pixels)[row1 + 2], r.color_b),
365 });
366 }
367 if (entry.size) {
368 a->set_size(denormalize((*pixels)[row1 + 3], r.size));
369 }
370 a->set_radius(denormalize((*pixels)[row2 + 0], r.radius));
371 a->set_query_radius(denormalize((*pixels)[row2 + 1], r.query_radius));
372 break;
373 }
374 }
375
377 }
378
380 "StateDecoder: patched {} entities ({} missing) from {} + {}",
381 m_patched_count, m_missing_count, exr_path, json_path);
382
383 return true;
384}
385
386// -------------------------------------------------------------------------
387// reconstruct()
388// -------------------------------------------------------------------------
389
391{
393 m_last_error.clear();
394
395 const std::string json_path = base_path + ".json";
396 const std::string exr_path = base_path + ".exr";
397
399 auto schema_opt = ser.read<FabricSchema>(json_path);
400 if (!schema_opt) {
401 m_last_error = "Failed to load schema: " + ser.last_error();
403 return result;
404 }
405 const auto& schema = *schema_opt;
406
407 if (schema.version < 2 || schema.version > 4) {
408 m_last_error = "Unsupported schema version: " + std::to_string(schema.version);
410 return result;
411 }
412
413 if (schema.entities.empty()) {
414 m_last_error = "Schema contains no entities: " + json_path;
416 return result;
417 }
418
419 const uint32_t expected_rows = schema.version >= 4 ? 5 : k_exr_rows;
420 auto pv_opt = load_exr(exr_path, static_cast<uint32_t>(schema.entities.size()), expected_rows, m_last_error);
421 if (!pv_opt) {
423 return result;
424 }
425 auto& pv = *pv_opt;
426 const auto* pixels = pv.image.as_float();
427 const uint32_t width = pv.width;
428 const auto& r = schema.ranges;
429
430 const auto existing_ids = fabric.all_ids();
431 const std::unordered_set<uint32_t> existing(existing_ids.begin(), existing_ids.end());
432
433 for (size_t i = 0; i < schema.entities.size(); ++i) {
434 const auto& entry = schema.entities[i];
435
436 if (!kind_known(entry.kind)) {
437 result.warnings.push_back("Unknown kind '" + entry.kind
438 + "' for id " + std::to_string(entry.id) + ", skipping");
439 ++result.skipped;
440 continue;
441 }
442
443 const size_t row0 = (static_cast<size_t>(0) * width + i) * k_channels;
444 const size_t row1 = (static_cast<size_t>(1) * width + i) * k_channels;
445 const size_t row2 = (static_cast<size_t>(2) * width + i) * k_channels;
446
447 const glm::vec3 position {
448 denormalize((*pixels)[row0 + 0], r.pos_x),
449 denormalize((*pixels)[row0 + 1], r.pos_y),
450 denormalize((*pixels)[row0 + 2], r.pos_z),
451 };
452 const float intensity = denormalize((*pixels)[row0 + 3], r.intensity);
453 const float radius = denormalize((*pixels)[row2 + 0], r.radius);
454 const float query_radius = denormalize((*pixels)[row2 + 1], r.query_radius);
455
456 auto read_color = [&]() -> glm::vec3 {
457 return {
458 denormalize((*pixels)[row1 + 0], r.color_r),
459 denormalize((*pixels)[row1 + 1], r.color_g),
460 denormalize((*pixels)[row1 + 2], r.color_b),
461 };
462 };
463 auto read_size = [&]() {
464 return denormalize((*pixels)[row1 + 3], r.size);
465 };
466
467 if (existing.count(entry.id)) {
468 // -----------------------------------------------------------------
469 // Patch existing entity.
470 // -----------------------------------------------------------------
471 switch (parse_kind(entry.kind)) {
473 auto e = fabric.get_emitter(entry.id);
474 if (!e) {
475 ++result.skipped;
476 continue;
477 }
478 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
479 result.warnings.push_back("Emitter " + std::to_string(entry.id)
480 + " fn_name mismatch: schema='" + entry.influence_fn_name
481 + "' live='" + e->fn_name() + "'");
482 }
483 e->set_position(position);
484 e->set_intensity(intensity);
485 if (entry.color) {
486 e->set_color(read_color());
487 }
488 if (entry.size) {
489 e->set_size(read_size());
490 }
491 e->set_radius(radius);
492 break;
493 }
495 auto s = fabric.get_sensor(entry.id);
496 if (!s) {
497 ++result.skipped;
498 continue;
499 }
500 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
501 result.warnings.push_back("Sensor " + std::to_string(entry.id)
502 + " fn_name mismatch: schema='" + entry.perception_fn_name
503 + "' live='" + s->fn_name() + "'");
504 }
505 s->set_position(position);
506 s->set_query_radius(query_radius);
507 break;
508 }
509 case Fabric::Kind::Agent: {
510 auto a = fabric.get_agent(entry.id);
511 if (!a) {
512 ++result.skipped;
513 continue;
514 }
515 if (!entry.perception_fn_name.empty() && a->perception_fn_name() != entry.perception_fn_name) {
516 result.warnings.push_back("Agent " + std::to_string(entry.id)
517 + " perception_fn mismatch: schema='" + entry.perception_fn_name + "'");
518 }
519 if (!entry.influence_fn_name.empty() && a->influence_fn_name() != entry.influence_fn_name) {
520 result.warnings.push_back("Agent " + std::to_string(entry.id)
521 + " influence_fn mismatch: schema='" + entry.influence_fn_name + "'");
522 }
523 a->set_position(position);
524 a->set_intensity(intensity);
525 if (entry.color) {
526 a->set_color(read_color());
527 }
528 if (entry.size) {
529 a->set_size(read_size());
530 }
531 a->set_radius(radius);
532 a->set_query_radius(query_radius);
533 break;
534 }
535 }
536 ++result.patched;
537
538 } else {
539 // -----------------------------------------------------------------
540 // Construct missing entity.
541 // -----------------------------------------------------------------
542 switch (parse_kind(entry.kind)) {
544 auto fn_ptr = fabric.resolve_influence_fn(entry.influence_fn_name);
546 if (!fn_ptr || !*fn_ptr) {
547 result.warnings.push_back("Emitter: unknown influence_fn '"
548 + entry.influence_fn_name + "', using no-op");
549 fn = [](const InfluenceContext&) { };
550 } else {
551 fn = *fn_ptr;
552 }
553 auto emitter = std::make_shared<Emitter>(entry.influence_fn_name, std::move(fn));
554 emitter->set_position(position);
555 emitter->set_intensity(intensity);
556 emitter->set_radius(radius);
557 if (entry.color) {
558 emitter->set_color(read_color());
559 }
560 if (entry.size) {
561 emitter->set_size(read_size());
562 }
563 auto wiring = fabric.wire(emitter);
564 if (emitter->id() != entry.id) {
565 result.warnings.push_back("Emitter schema_id=" + std::to_string(entry.id)
566 + " reconstructed as runtime_id=" + std::to_string(emitter->id()));
567 }
568 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
569 break;
570 }
572 auto fn_ptr = fabric.resolve_perception_fn(entry.perception_fn_name);
574 if (!fn_ptr || !*fn_ptr) {
575 result.warnings.push_back("Sensor: unknown perception_fn '"
576 + entry.perception_fn_name + "', using no-op");
577 fn = [](const PerceptionContext&) { };
578 } else {
579 fn = *fn_ptr;
580 }
581 auto sensor = std::make_shared<Sensor>(query_radius,
582 entry.perception_fn_name, std::move(fn));
583 sensor->set_position(position);
584 auto wiring = fabric.wire(sensor);
585 if (sensor->id() != entry.id) {
586 result.warnings.push_back("Sensor schema_id=" + std::to_string(entry.id)
587 + " reconstructed as runtime_id=" + std::to_string(sensor->id()));
588 }
589 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
590 break;
591 }
592 case Fabric::Kind::Agent: {
593 auto pfn_ptr = fabric.resolve_perception_fn(entry.perception_fn_name);
595 if (!pfn_ptr || !*pfn_ptr) {
596 result.warnings.push_back("Agent: unknown perception_fn '"
597 + entry.perception_fn_name + "', using no-op");
598 pfn = [](const PerceptionContext&) { };
599 } else {
600 pfn = *pfn_ptr;
601 }
602 auto ifn_ptr = fabric.resolve_influence_fn(entry.influence_fn_name);
604 if (!ifn_ptr || !*ifn_ptr) {
605 result.warnings.push_back("Agent: unknown influence_fn '"
606 + entry.influence_fn_name + "', using no-op");
607 ifn = [](const InfluenceContext&) { };
608 } else {
609 ifn = *ifn_ptr;
610 }
611 auto agent = std::make_shared<Agent>(query_radius,
612 entry.perception_fn_name, std::move(pfn),
613 entry.influence_fn_name, std::move(ifn));
614 agent->set_position(position);
615 agent->set_intensity(intensity);
616 agent->set_radius(radius);
617 agent->set_query_radius(query_radius);
618 if (entry.color) {
619 agent->set_color(read_color());
620 }
621 if (entry.size) {
622 agent->set_size(read_size());
623 }
624 auto wiring = fabric.wire(agent);
625 if (agent->id() != entry.id) {
626 result.warnings.push_back("Agent schema_id=" + std::to_string(entry.id)
627 + " reconstructed as runtime_id=" + std::to_string(agent->id()));
628 }
629 apply_wiring(std::move(wiring), entry.wiring, result.warnings);
630 break;
631 }
632 }
633 ++result.constructed;
634 }
635 }
636
638 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",
639 result.constructed, result.patched, result.skipped, result.warnings.size());
640
641 return result;
642}
643
644} // namespace MayaFlux::Nexus
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
size_t a
Range pos_x
Range color_b
RangeSet ranges
std::string perception_fn_name
IO::ImageData image
Range intensity
std::vector< EntityRecord > entities
float max
uint32_t width
Range radius
Range query_radius
Range color_r
std::optional< double > duration
std::optional< double > interval
const std::vector< float > * pixels
std::string fabric_name
Range size
WiringRecord wiring
Range color_g
double delay_seconds
glm::vec3 position
std::string kind
uint32_t version
std::optional< std::vector< WiringStep > > steps
float min
Range pos_z
std::optional< glm::vec3 > color
Range pos_y
std::string influence_fn_name
std::optional< size_t > times
Cycle Behavior: The for_cycles(N) configuration controls how many times the capture operation execute...
static std::optional< ImageData > load(const std::string &path, int desired_channels=4)
Load image from file (static utility)
std::optional< T > read(const std::string &path)
Read path and deserialize into T.
const std::string & last_error() const
Last error message, empty if no error.
Converts arbitrary C++ types to/from JSON strings and disk files.
std::function< void(const PerceptionContext &)> PerceptionFn
Definition Agent.hpp:33
std::function< void(const InfluenceContext &)> InfluenceFn
Definition Agent.hpp:32
std::function< void(const InfluenceContext &)> InfluenceFn
Definition Emitter.hpp:28
std::shared_ptr< Sensor > get_sensor(uint32_t id) const
Get the Sensor registered under id.
Definition Fabric.cpp:137
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
Definition Fabric.cpp:100
std::shared_ptr< Emitter::InfluenceFn > resolve_influence_fn(std::string_view name) const
Look up a registered influence function by name.
Definition Fabric.cpp:292
std::shared_ptr< Sensor::PerceptionFn > resolve_perception_fn(std::string_view name) const
Look up a registered perception function by name.
Definition Fabric.cpp:298
std::shared_ptr< Agent > get_agent(uint32_t id) const
Get the Agent registered under id.
Definition Fabric.cpp:148
std::shared_ptr< Emitter > get_emitter(uint32_t id) const
Get the Emitter registered under id.
Definition Fabric.cpp:126
Wiring wire(std::shared_ptr< Emitter > emitter)
Begin wiring an Emitter into the Fabric.
Definition Fabric.cpp:39
Orchestrates spatial indexing and scheduling for Nexus objects.
Definition Fabric.hpp:37
std::function< void(const PerceptionContext &)> PerceptionFn
Definition Sensor.hpp:21
ReconstructionResult reconstruct(Fabric &fabric, const std::string &base_path)
Patch existing entities and construct missing ones from schema.
bool decode(Fabric &fabric, const std::string &base_path)
Decode and apply to fabric.
constexpr auto member(std::string_view key, T Class::*ptr)
constexpr auto opt_member(std::string_view key, std::optional< T > Class::*ptr)
@ FileIO
Filesystem I/O operations.
@ Runtime
General runtime operations (default fallback)
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
Data passed to an Emitter or Agent influence function on each commit.
Data passed to a Sensor or Agent perception function on each commit.