Patch existing entities and construct missing ones from schema.
Entities whose id exists in the fabric are patched in place. Missing entities are constructed, their callables resolved via the fabric's function registry (no-op fallback with warning if absent), and wired. Supported wiring kinds for v2: every, move_to, commit_driven. Unsupported kinds (on, use, bind) fall back to commit_driven + warning. Hard failure (bad schema, unreadable EXR, dimension mismatch) sets last_error() and returns a zeroed result.
391{
392 ReconstructionResult result;
394
395 const std::string json_path = base_path + ".json";
396 const std::string exr_path = base_path + ".exr";
397
398 IO::JSONSerializer ser;
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
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 };
453 const float radius = denormalize((*
pixels)[row2 + 0], r.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
468
469
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 }
485 if (entry.color) {
486 e->set_color(read_color());
487 }
488 if (entry.size) {
489 e->set_size(read_size());
490 }
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 }
507 break;
508 }
510 auto a = fabric.get_agent(entry.id);
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 }
525 if (entry.color) {
526 a->set_color(read_color());
527 }
528 if (entry.size) {
529 a->set_size(read_size());
530 }
533 break;
534 }
535 }
536 ++result.patched;
537
538 } else {
539
540
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));
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 }
582 entry.perception_fn_name, std::move(fn));
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 }
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 }
612 entry.perception_fn_name, std::move(pfn),
613 entry.influence_fn_name, std::move(ifn));
616 agent->set_radius(
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}
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
const std::vector< float > * pixels
Cycle Behavior: The for_cycles(N) configuration controls how many times the capture operation execute...
std::function< void(const PerceptionContext &)> PerceptionFn
std::function< void(const InfluenceContext &)> InfluenceFn
std::function< void(const InfluenceContext &)> InfluenceFn
std::function< void(const PerceptionContext &)> PerceptionFn
@ FileIO
Filesystem I/O operations.
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.