140 const std::string json_path = base_path +
".json";
141 const std::string exr_path = base_path +
".exr";
150 const auto& schema = *schema_opt;
153 m_last_error =
"Unsupported schema version: " + std::to_string(schema.version)
159 if (schema.entities.empty()) {
160 m_last_error =
"Schema contains no entities: " + json_path;
166 auto pv_opt = load_exr(exr_path,
static_cast<uint32_t
>(schema.entities.size()), expected_rows,
m_last_error);
172 const auto*
pixels = pv.image.as_float();
173 const uint32_t
width = pv.width;
174 const auto& r = schema.ranges;
176 for (
size_t i = 0; i < schema.entities.size(); ++i) {
177 const auto& entry = schema.entities[i];
181 "StateDecoder: unknown kind '{}' for id {}, skipping", entry.kind, entry.id);
190 const glm::vec3 position {
191 denormalize((*
pixels)[row0 + 0], r.pos_x),
192 denormalize((*
pixels)[row0 + 1], r.pos_y),
193 denormalize((*
pixels)[row0 + 2], r.pos_z),
201 "StateDecoder: id {} not found as Emitter, skipping", entry.id);
205 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
207 "StateDecoder: Emitter {} fn_name mismatch: schema='{}' live='{}'",
208 entry.id, entry.influence_fn_name, e->fn_name());
210 e->set_position(position);
211 e->set_intensity(denormalize((*
pixels)[row0 + 3], r.intensity));
213 e->set_color(glm::vec3 {
214 denormalize((*
pixels)[row1 + 0], r.color_r),
215 denormalize((*
pixels)[row1 + 1], r.color_g),
216 denormalize((*
pixels)[row1 + 2], r.color_b),
220 e->set_size(denormalize((*
pixels)[row1 + 3], r.size));
222 e->set_radius(denormalize((*
pixels)[row2 + 0], r.radius));
229 "StateDecoder: id {} not found as Sensor, skipping", entry.id);
233 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
235 "StateDecoder: Sensor {} fn_name mismatch: schema='{}' live='{}'",
236 entry.id, entry.perception_fn_name, s->fn_name());
238 s->set_position(position);
239 s->set_query_radius(denormalize((*
pixels)[row2 + 1], r.query_radius));
246 "StateDecoder: id {} not found as Agent, skipping", entry.id);
250 if (!entry.perception_fn_name.empty() &&
a->perception_fn_name() != entry.perception_fn_name) {
252 "StateDecoder: Agent {} perception_fn_name mismatch: schema='{}' live='{}'",
253 entry.id, entry.perception_fn_name,
a->perception_fn_name());
255 if (!entry.influence_fn_name.empty() &&
a->influence_fn_name() != entry.influence_fn_name) {
257 "StateDecoder: Agent {} influence_fn_name mismatch: schema='{}' live='{}'",
258 entry.id, entry.influence_fn_name,
a->influence_fn_name());
260 a->set_position(position);
261 a->set_intensity(denormalize((*
pixels)[row0 + 3], r.intensity));
263 a->set_color(glm::vec3 {
264 denormalize((*
pixels)[row1 + 0], r.color_r),
265 denormalize((*
pixels)[row1 + 1], r.color_g),
266 denormalize((*
pixels)[row1 + 2], r.color_b),
270 a->set_size(denormalize((*
pixels)[row1 + 3], r.size));
272 a->set_radius(denormalize((*
pixels)[row2 + 0], r.radius));
273 a->set_query_radius(denormalize((*
pixels)[row2 + 1], r.query_radius));
275 if (entry.locus_nav) {
276 if (!apply_locus_nav(
a, *entry.locus_nav)) {
278 "StateDecoder: Agent {} has locus_nav in schema but is not a Locus at runtime",
283 if (
auto presence = std::dynamic_pointer_cast<Presence>(
a)) {
284 if (!entry.falloff_curve_name.empty()) {
285 if (
auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
286 presence->set_falloff_curve(*fc);
288 if (entry.falloff_radius)
289 presence->set_falloff_radius(*entry.falloff_radius);
298 for (
const auto& xrec : schema.expanses) {
299 if (xrec.fn_name.empty()) {
301 "StateDecoder: Expanse {} has no fn_name, skipping", xrec.id);
305 if (!contains_fn || !*contains_fn) {
307 "StateDecoder: Expanse {} fn '{}' not in registry, skipping",
308 xrec.id, xrec.fn_name);
311 auto on_enter_fn = xrec.on_enter_fn_name.empty()
317 auto on_exit_fn = xrec.on_exit_fn_name.empty()
323 auto expanse = std::make_shared<Expanse>(
325 xrec.on_enter_fn_name,
326 xrec.on_exit_fn_name,
328 std::move(on_enter_fn),
329 std::move(on_exit_fn));
334 "StateDecoder: patched {} entities ({} missing) from {} + {}",
349 const std::string json_path = base_path +
".json";
350 const std::string exr_path = base_path +
".exr";
359 const auto& schema = *schema_opt;
362 m_last_error =
"Unsupported schema version: " + std::to_string(schema.version)
368 if (schema.entities.empty()) {
369 m_last_error =
"Schema contains no entities: " + json_path;
375 auto pv_opt = load_exr(exr_path,
static_cast<uint32_t
>(schema.entities.size()), expected_rows,
m_last_error);
381 const auto*
pixels = pv.image.as_float();
382 const uint32_t
width = pv.width;
383 const auto& r = schema.ranges;
385 const auto existing_ids = fabric.
all_ids();
386 const std::unordered_set<uint32_t>
existing(existing_ids.begin(), existing_ids.end());
388 for (
size_t i = 0; i < schema.entities.size(); ++i) {
389 const auto& entry = schema.entities[i];
392 result.
warnings.push_back(
"Unknown kind '" + entry.kind
393 +
"' for id " + std::to_string(entry.id) +
", skipping");
402 const glm::vec3 position {
403 denormalize((*
pixels)[row0 + 0], r.pos_x),
404 denormalize((*
pixels)[row0 + 1], r.pos_y),
405 denormalize((*
pixels)[row0 + 2], r.pos_z),
407 const float intensity = denormalize((*
pixels)[row0 + 3], r.intensity);
408 const float radius = denormalize((*
pixels)[row2 + 0], r.radius);
409 const float query_radius = denormalize((*
pixels)[row2 + 1], r.query_radius);
411 auto read_color = [&]() -> glm::vec3 {
413 denormalize((*
pixels)[row1 + 0], r.color_r),
414 denormalize((*
pixels)[row1 + 1], r.color_g),
415 denormalize((*
pixels)[row1 + 2], r.color_b),
418 auto read_size = [&]() {
419 return denormalize((*
pixels)[row1 + 3], r.size);
433 if (!entry.influence_fn_name.empty() && e->fn_name() != entry.influence_fn_name) {
434 result.
warnings.push_back(
"Emitter " + std::to_string(entry.id)
435 +
" fn_name mismatch: schema='" + entry.influence_fn_name
436 +
"' live='" + e->fn_name() +
"'");
438 e->set_position(position);
439 e->set_intensity(intensity);
441 e->set_color(read_color());
444 e->set_size(read_size());
455 if (!entry.perception_fn_name.empty() && s->fn_name() != entry.perception_fn_name) {
456 result.
warnings.push_back(
"Sensor " + std::to_string(entry.id)
457 +
" fn_name mismatch: schema='" + entry.perception_fn_name
458 +
"' live='" + s->fn_name() +
"'");
460 s->set_position(position);
461 s->set_query_radius(query_radius);
470 if (!entry.perception_fn_name.empty() &&
a->perception_fn_name() != entry.perception_fn_name) {
471 result.
warnings.push_back(
"Agent " + std::to_string(entry.id)
472 +
" perception_fn mismatch: schema='" + entry.perception_fn_name +
"'");
474 if (!entry.influence_fn_name.empty() &&
a->influence_fn_name() != entry.influence_fn_name) {
475 result.
warnings.push_back(
"Agent " + std::to_string(entry.id)
476 +
" influence_fn mismatch: schema='" + entry.influence_fn_name +
"'");
478 a->set_position(position);
479 a->set_intensity(intensity);
481 a->set_color(read_color());
484 a->set_size(read_size());
487 a->set_query_radius(query_radius);
501 if (!fn_ptr || !*fn_ptr) {
502 result.
warnings.push_back(
"Emitter: unknown influence_fn '"
503 + entry.influence_fn_name +
"', using no-op");
508 auto emitter = std::make_shared<Emitter>(entry.influence_fn_name, std::move(fn));
509 emitter->set_position(position);
510 emitter->set_intensity(intensity);
511 emitter->set_radius(
radius);
513 emitter->set_color(read_color());
516 emitter->set_size(read_size());
518 auto wiring = fabric.
wire(emitter);
519 if (emitter->id() != entry.id) {
520 result.
warnings.push_back(
"Emitter schema_id=" + std::to_string(entry.id)
521 +
" reconstructed as runtime_id=" + std::to_string(emitter->id()));
523 apply_wiring(std::move(wiring), entry.wiring, result.
warnings);
529 if (!fn_ptr || !*fn_ptr) {
530 result.
warnings.push_back(
"Sensor: unknown perception_fn '"
531 + entry.perception_fn_name +
"', using no-op");
536 auto sensor = std::make_shared<Sensor>(query_radius,
537 entry.perception_fn_name, std::move(fn));
538 sensor->set_position(position);
539 auto wiring = fabric.
wire(sensor);
540 if (sensor->id() != entry.id) {
541 result.
warnings.push_back(
"Sensor schema_id=" + std::to_string(entry.id)
542 +
" reconstructed as runtime_id=" + std::to_string(sensor->id()));
544 apply_wiring(std::move(wiring), entry.wiring, result.
warnings);
550 if (!pfn_ptr || !*pfn_ptr) {
551 result.
warnings.push_back(
"Agent: unknown perception_fn '"
552 + entry.perception_fn_name +
"', using no-op");
559 if (!ifn_ptr || !*ifn_ptr) {
560 result.
warnings.push_back(
"Agent: unknown influence_fn '"
561 + entry.influence_fn_name +
"', using no-op");
566 std::shared_ptr<Agent> agent;
567 if (entry.subkind ==
"locus" && entry.locus_nav) {
568 const auto& nav = *entry.locus_nav;
571 .initial_target = nav.target,
572 .fov_radians = nav.fov,
573 .near_plane = nav.near_plane,
574 .far_plane = nav.far_plane,
575 .move_speed = nav.speed,
577 agent = std::make_shared<Locus>(cfg, query_radius,
578 entry.perception_fn_name, std::move(pfn),
579 entry.influence_fn_name, std::move(ifn));
580 result.
warnings.push_back(
"Locus " + std::to_string(entry.id)
581 +
": view_targets must be reconnected by caller");
583 }
else if (entry.subkind ==
"presence") {
586 if (!rfn_ptr || !*rfn_ptr) {
587 result.
warnings.push_back(
"Presence: unknown radiate_fn '"
588 + entry.radiate_fn_name +
"', using no-op");
589 rfn = [](uint32_t, float) { };
593 auto presence = std::make_shared<Presence>(query_radius,
594 entry.perception_fn_name, std::move(pfn),
595 entry.influence_fn_name, std::move(ifn),
596 entry.radiate_fn_name, std::move(rfn));
597 if (!entry.falloff_curve_name.empty()) {
598 if (
auto fc = Reflect::string_to_enum_case_insensitive<Presence::FalloffCurve>(entry.falloff_curve_name))
599 presence->set_falloff_curve(*fc);
601 if (entry.falloff_radius)
602 presence->set_falloff_radius(*entry.falloff_radius);
603 agent = std::move(presence);
606 if (entry.subkind ==
"locus") {
607 result.
warnings.push_back(
"Locus " + std::to_string(entry.id)
608 +
": no locus_nav in schema, reconstructed as plain Agent");
610 agent = std::make_shared<Agent>(query_radius,
611 entry.perception_fn_name, std::move(pfn),
612 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);
619 agent->set_color(read_color());
622 agent->set_size(read_size());
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()));
629 apply_wiring(std::move(wiring), entry.wiring, result.
warnings);
637 for (
const auto& xrec : schema.expanses) {
638 if (xrec.fn_name.empty()) {
639 result.
warnings.push_back(
"Expanse " + std::to_string(xrec.id)
640 +
": no fn_name, skipping");
644 if (!contains_fn || !*contains_fn) {
645 result.
warnings.push_back(
"Expanse " + std::to_string(xrec.id)
646 +
": fn '" + xrec.fn_name +
"' not in registry, skipping");
649 auto on_enter_fn = xrec.on_enter_fn_name.empty()
655 auto on_exit_fn = xrec.on_exit_fn_name.empty()
661 auto expanse = std::make_shared<Expanse>(
663 xrec.on_enter_fn_name,
664 xrec.on_exit_fn_name,
666 std::move(on_enter_fn),
667 std::move(on_exit_fn));
673 "StateDecoder::reconstruct: constructed={} patched={} skipped={} warnings={}",