133 const std::string json_path = base_path +
".json";
134 const std::string exr_path = base_path +
".exr";
143 const auto& schema = *schema_opt;
146 m_last_error =
"Unsupported schema version: " + std::to_string(schema.version)
152 if (schema.entities.empty()) {
153 m_last_error =
"Schema contains no entities: " + json_path;
159 auto pv_opt = load_exr(exr_path,
static_cast<uint32_t
>(schema.entities.size()), expected_rows,
m_last_error);
165 const auto*
pixels = pv.image.as_float();
166 const uint32_t
width = pv.width;
167 const auto& r = schema.ranges;
169 for (
size_t i = 0; i < schema.entities.size(); ++i) {
170 const auto& entry = schema.entities[i];
174 "StateDecoder: unknown kind '{}' for id {}, skipping", entry.kind, entry.id);
183 const glm::vec3 position {
184 denormalize((*
pixels)[row0 + 0], r.pos_x),
185 denormalize((*
pixels)[row0 + 1], r.pos_y),
186 denormalize((*
pixels)[row0 + 2], r.pos_z),
194 "StateDecoder: id {} not found as Emitter, skipping", entry.id);
198 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
200 "StateDecoder: Emitter {} fn_name mismatch: schema='{}' live='{}'",
201 entry.id, entry.influence_fn_name, e->fn_name());
203 e->set_position(position);
204 e->set_intensity(denormalize((*
pixels)[row0 + 3], r.intensity));
206 e->set_color(glm::vec3 {
207 denormalize((*
pixels)[row1 + 0], r.color_r),
208 denormalize((*
pixels)[row1 + 1], r.color_g),
209 denormalize((*
pixels)[row1 + 2], r.color_b),
213 e->set_size(denormalize((*
pixels)[row1 + 3], r.size));
215 e->set_radius(denormalize((*
pixels)[row2 + 0], r.radius));
222 "StateDecoder: id {} not found as Sensor, skipping", entry.id);
226 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
228 "StateDecoder: Sensor {} fn_name mismatch: schema='{}' live='{}'",
229 entry.id, entry.perception_fn_name, s->fn_name());
231 s->set_position(position);
232 s->set_query_radius(denormalize((*
pixels)[row2 + 1], r.query_radius));
239 "StateDecoder: id {} not found as Agent, skipping", entry.id);
243 if (!entry.perception_fn_name.empty() &&
a->perception_fn_name() != entry.perception_fn_name) {
245 "StateDecoder: Agent {} perception_fn_name mismatch: schema='{}' live='{}'",
246 entry.id, entry.perception_fn_name,
a->perception_fn_name());
248 if (!entry.influence_fn_name.empty() &&
a->influence_fn_name() != entry.influence_fn_name) {
250 "StateDecoder: Agent {} influence_fn_name mismatch: schema='{}' live='{}'",
251 entry.id, entry.influence_fn_name,
a->influence_fn_name());
253 a->set_position(position);
254 a->set_intensity(denormalize((*
pixels)[row0 + 3], r.intensity));
256 a->set_color(glm::vec3 {
257 denormalize((*
pixels)[row1 + 0], r.color_r),
258 denormalize((*
pixels)[row1 + 1], r.color_g),
259 denormalize((*
pixels)[row1 + 2], r.color_b),
263 a->set_size(denormalize((*
pixels)[row1 + 3], r.size));
265 a->set_radius(denormalize((*
pixels)[row2 + 0], r.radius));
266 a->set_query_radius(denormalize((*
pixels)[row2 + 1], r.query_radius));
268 if (entry.locus_nav) {
269 if (!apply_locus_nav(
a, *entry.locus_nav)) {
271 "StateDecoder: Agent {} has locus_nav in schema but is not a Locus at runtime",
276 if (
auto presence = std::dynamic_pointer_cast<Presence>(
a)) {
277 if (!entry.falloff_curve_name.empty()) {
278 if (
auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
279 presence->set_falloff_curve(*fc);
281 if (entry.falloff_radius)
282 presence->set_falloff_radius(*entry.falloff_radius);
291 for (
const auto& xrec : schema.expanses) {
292 if (xrec.fn_name.empty()) {
294 "StateDecoder: Expanse {} has no fn_name, skipping", xrec.id);
298 if (!contains_fn || !*contains_fn) {
300 "StateDecoder: Expanse {} fn '{}' not in registry, skipping",
301 xrec.id, xrec.fn_name);
304 auto on_enter_fn = xrec.on_enter_fn_name.empty()
310 auto on_exit_fn = xrec.on_exit_fn_name.empty()
316 auto expanse = std::make_shared<Expanse>(
318 xrec.on_enter_fn_name,
319 xrec.on_exit_fn_name,
321 std::move(on_enter_fn),
322 std::move(on_exit_fn));
327 "StateDecoder: patched {} entities ({} missing) from {} + {}",
342 const std::string json_path = base_path +
".json";
343 const std::string exr_path = base_path +
".exr";
352 const auto& schema = *schema_opt;
355 m_last_error =
"Unsupported schema version: " + std::to_string(schema.version)
361 if (schema.entities.empty()) {
362 m_last_error =
"Schema contains no entities: " + json_path;
368 auto pv_opt = load_exr(exr_path,
static_cast<uint32_t
>(schema.entities.size()), expected_rows,
m_last_error);
374 const auto*
pixels = pv.image.as_float();
375 const uint32_t
width = pv.width;
376 const auto& r = schema.ranges;
378 const auto existing_ids = fabric.
all_ids();
379 const std::unordered_set<uint32_t>
existing(existing_ids.begin(), existing_ids.end());
381 for (
size_t i = 0; i < schema.entities.size(); ++i) {
382 const auto& entry = schema.entities[i];
385 result.
warnings.push_back(
"Unknown kind '" + entry.kind
386 +
"' for id " + std::to_string(entry.id) +
", skipping");
395 const glm::vec3 position {
396 denormalize((*
pixels)[row0 + 0], r.pos_x),
397 denormalize((*
pixels)[row0 + 1], r.pos_y),
398 denormalize((*
pixels)[row0 + 2], r.pos_z),
400 const float intensity = denormalize((*
pixels)[row0 + 3], r.intensity);
401 const float radius = denormalize((*
pixels)[row2 + 0], r.radius);
402 const float query_radius = denormalize((*
pixels)[row2 + 1], r.query_radius);
404 auto read_color = [&]() -> glm::vec3 {
406 denormalize((*
pixels)[row1 + 0], r.color_r),
407 denormalize((*
pixels)[row1 + 1], r.color_g),
408 denormalize((*
pixels)[row1 + 2], r.color_b),
411 auto read_size = [&]() {
412 return denormalize((*
pixels)[row1 + 3], r.size);
426 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
427 result.
warnings.push_back(
"Emitter " + std::to_string(entry.id)
428 +
" fn_name mismatch: schema='" + entry.influence_fn_name
429 +
"' live='" + e->fn_name() +
"'");
431 e->set_position(position);
432 e->set_intensity(intensity);
434 e->set_color(read_color());
437 e->set_size(read_size());
439 e->set_radius(radius);
448 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
449 result.
warnings.push_back(
"Sensor " + std::to_string(entry.id)
450 +
" fn_name mismatch: schema='" + entry.perception_fn_name
451 +
"' live='" + s->fn_name() +
"'");
453 s->set_position(position);
454 s->set_query_radius(query_radius);
463 if (!entry.perception_fn_name.empty() &&
a->perception_fn_name() != entry.perception_fn_name) {
464 result.
warnings.push_back(
"Agent " + std::to_string(entry.id)
465 +
" perception_fn mismatch: schema='" + entry.perception_fn_name +
"'");
467 if (!entry.influence_fn_name.empty() &&
a->influence_fn_name() != entry.influence_fn_name) {
468 result.
warnings.push_back(
"Agent " + std::to_string(entry.id)
469 +
" influence_fn mismatch: schema='" + entry.influence_fn_name +
"'");
471 a->set_position(position);
472 a->set_intensity(intensity);
474 a->set_color(read_color());
477 a->set_size(read_size());
479 a->set_radius(radius);
480 a->set_query_radius(query_radius);
494 if (!fn_ptr || !*fn_ptr) {
495 result.
warnings.push_back(
"Emitter: unknown influence_fn '"
496 + entry.influence_fn_name +
"', using no-op");
501 auto emitter = std::make_shared<Emitter>(entry.influence_fn_name, std::move(fn));
502 emitter->set_position(position);
503 emitter->set_intensity(intensity);
504 emitter->set_radius(radius);
506 emitter->set_color(read_color());
509 emitter->set_size(read_size());
511 auto wiring = fabric.
wire(emitter);
512 if (emitter->id() != entry.id) {
513 result.
warnings.push_back(
"Emitter schema_id=" + std::to_string(entry.id)
514 +
" reconstructed as runtime_id=" + std::to_string(emitter->id()));
516 apply_wiring(std::move(wiring), entry.wiring, result.
warnings);
522 if (!fn_ptr || !*fn_ptr) {
523 result.
warnings.push_back(
"Sensor: unknown perception_fn '"
524 + entry.perception_fn_name +
"', using no-op");
529 auto sensor = std::make_shared<Sensor>(query_radius,
530 entry.perception_fn_name, std::move(fn));
531 sensor->set_position(position);
532 auto wiring = fabric.
wire(sensor);
533 if (sensor->id() != entry.id) {
534 result.
warnings.push_back(
"Sensor schema_id=" + std::to_string(entry.id)
535 +
" reconstructed as runtime_id=" + std::to_string(sensor->id()));
537 apply_wiring(std::move(wiring), entry.wiring, result.
warnings);
543 if (!pfn_ptr || !*pfn_ptr) {
544 result.
warnings.push_back(
"Agent: unknown perception_fn '"
545 + entry.perception_fn_name +
"', using no-op");
552 if (!ifn_ptr || !*ifn_ptr) {
553 result.
warnings.push_back(
"Agent: unknown influence_fn '"
554 + entry.influence_fn_name +
"', using no-op");
559 std::shared_ptr<Agent> agent;
560 if (entry.subkind ==
"locus" && entry.locus_nav) {
561 const auto& nav = *entry.locus_nav;
564 .initial_target = nav.target,
565 .fov_radians = nav.fov,
566 .near_plane = nav.near_plane,
567 .far_plane = nav.far_plane,
568 .move_speed = nav.speed,
570 agent = std::make_shared<Locus>(cfg, query_radius,
571 entry.perception_fn_name, std::move(pfn),
572 entry.influence_fn_name, std::move(ifn));
573 result.
warnings.push_back(
"Locus " + std::to_string(entry.id)
574 +
": view_targets must be reconnected by caller");
576 }
else if (entry.subkind ==
"presence") {
579 if (!rfn_ptr || !*rfn_ptr) {
580 result.
warnings.push_back(
"Presence: unknown radiate_fn '"
581 + entry.radiate_fn_name +
"', using no-op");
582 rfn = [](uint32_t, float) { };
586 auto presence = std::make_shared<Presence>(query_radius,
587 entry.perception_fn_name, std::move(pfn),
588 entry.influence_fn_name, std::move(ifn),
589 entry.radiate_fn_name, std::move(rfn));
590 if (!entry.falloff_curve_name.empty()) {
591 if (
auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
592 presence->set_falloff_curve(*fc);
594 if (entry.falloff_radius)
595 presence->set_falloff_radius(*entry.falloff_radius);
596 agent = std::move(presence);
599 if (entry.subkind ==
"locus") {
600 result.
warnings.push_back(
"Locus " + std::to_string(entry.id)
601 +
": no locus_nav in schema, reconstructed as plain Agent");
603 agent = std::make_shared<Agent>(query_radius,
604 entry.perception_fn_name, std::move(pfn),
605 entry.influence_fn_name, std::move(ifn));
607 agent->set_position(position);
608 agent->set_intensity(intensity);
609 agent->set_radius(radius);
610 agent->set_query_radius(query_radius);
612 agent->set_color(read_color());
615 agent->set_size(read_size());
617 auto wiring = fabric.
wire(agent);
618 if (agent->id() != entry.id) {
619 result.
warnings.push_back(
"Agent schema_id=" + std::to_string(entry.id)
620 +
" reconstructed as runtime_id=" + std::to_string(agent->id()));
622 apply_wiring(std::move(wiring), entry.wiring, result.
warnings);
630 for (
const auto& xrec : schema.expanses) {
631 if (xrec.fn_name.empty()) {
632 result.
warnings.push_back(
"Expanse " + std::to_string(xrec.id)
633 +
": no fn_name, skipping");
637 if (!contains_fn || !*contains_fn) {
638 result.
warnings.push_back(
"Expanse " + std::to_string(xrec.id)
639 +
": fn '" + xrec.fn_name +
"' not in registry, skipping");
642 auto on_enter_fn = xrec.on_enter_fn_name.empty()
648 auto on_exit_fn = xrec.on_exit_fn_name.empty()
654 auto expanse = std::make_shared<Expanse>(
656 xrec.on_enter_fn_name,
657 xrec.on_exit_fn_name,
659 std::move(on_enter_fn),
660 std::move(on_exit_fn));
666 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",