use crate::math::expression as expr_eval;
use crate::{Actor, ActorBehavior, Message, Port};
use anyhow::{Error, Result};
use reflow_actor::{message::EncodableValue, ActorContext};
use reflow_actor_macro::actor;
use reflow_assets::get_or_create_db;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
static START_TIME: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
static FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
#[actor(
BehaviorSystemActor,
inports::<10>(tick, dt, entity_id),
outports::<1>(metadata),
state(MemoryState)
)]
pub async fn behavior_system_actor(ctx: ActorContext) -> Result<HashMap<String, Message>, Error> {
let payload = ctx.get_payload();
let config = ctx.get_config_hashmap();
let db_path = config
.get("$db")
.and_then(|v| v.as_str())
.unwrap_or("./assets.db");
let dt = match payload.get("dt") {
Some(Message::Float(f)) => *f,
_ => config
.get("dt")
.and_then(|v| v.as_f64())
.unwrap_or(1.0 / 60.0),
};
let target_entity = match payload.get("entity_id") {
Some(Message::String(s)) => Some(s.to_string()),
_ => config
.get("entity")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
};
let start = START_TIME.get_or_init(Instant::now);
let time = start.elapsed().as_secs_f64();
let frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
let db = get_or_create_db(db_path)?;
let behavior_entries = if let Some(ref entity) = target_entity {
match db.get_entry(&format!("{}:behavior", entity)) {
Ok(entry) => vec![entry],
Err(_) => Vec::new(),
}
} else {
db.query(&reflow_assets::AssetQuery::new().asset_type("behavior"))?
};
let mut rules_fired = 0u64;
let mut rules_skipped = 0u64;
let mut entities_processed = 0u64;
for entry in &behavior_entries {
let data = match &entry.inline_data {
Some(v) => v.clone(),
None => continue,
};
let entity = &entry.name; let rules = match data.get("rules").and_then(|v| v.as_array()) {
Some(r) => r,
None => continue,
};
entities_processed += 1;
for rule in rules {
let enabled = rule
.get("enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true);
if !enabled {
rules_skipped += 1;
continue;
}
let expr_str = match rule.get("expr").and_then(|v| v.as_str()) {
Some(s) => s,
None => continue,
};
let target = match rule.get("target").and_then(|v| v.as_str()) {
Some(s) => s,
None => continue,
};
let mut vars: HashMap<String, f64> = HashMap::new();
vars.insert("time".to_string(), time);
vars.insert("dt".to_string(), dt);
vars.insert("frame".to_string(), frame as f64);
if let Some(var_map) = rule.get("vars").and_then(|v| v.as_object()) {
for (name, val) in var_map {
if let Some(path) = val.as_str() {
if let Some(query) = path
.strip_prefix("@layout(")
.and_then(|s| s.strip_suffix(')'))
{
if let Some(v) =
reflow_assets::layout::resolve_layout_query(db_path, query)
{
vars.insert(name.clone(), v);
}
} else {
let resolved_path = if let Some(rest) = path.strip_prefix("self:") {
format!("{}:{}", entity, rest)
} else {
path.to_string()
};
if let Some(v) = resolve_path(&db, &resolved_path) {
vars.insert(name.clone(), v);
}
}
} else if let Some(n) = val.as_f64() {
vars.insert(name.clone(), n);
}
}
}
if let Some(when_expr) = rule.get("when").and_then(|v| v.as_str()) {
match expr_eval::eval(when_expr, &vars) {
Some(v) if v > 0.0 => {} _ => {
rules_skipped += 1;
continue;
}
}
}
let trimmed = expr_str.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let inner = &trimmed[1..trimmed.len() - 1];
let parts: Vec<&str> = split_top_level(inner);
let values: Vec<f64> = parts
.iter()
.filter_map(|p| expr_eval::eval(p.trim(), &vars))
.collect();
if !values.is_empty() {
let full_target = format!("{}:{}", entity, target);
write_target(&db, &full_target, &json!(values));
rules_fired += 1;
}
} else {
if let Some(result) = expr_eval::eval(expr_str, &vars) {
let full_target = format!("{}:{}", entity, target);
write_target(&db, &full_target, &json!(result));
rules_fired += 1;
}
}
}
}
let mut out = HashMap::new();
out.insert(
"metadata".to_string(),
Message::object(EncodableValue::from(json!({
"entitiesProcessed": entities_processed,
"rulesFired": rules_fired,
"rulesSkipped": rules_skipped,
"time": time,
"frame": frame,
}))),
);
Ok(out)
}
fn split_top_level(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0;
let mut start = 0;
for (i, c) in s.char_indices() {
match c {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
',' if depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < s.len() {
parts.push(&s[start..]);
}
parts
}
fn resolve_path(db: &std::sync::Arc<reflow_assets::AssetDB>, path: &str) -> Option<f64> {
let parts: Vec<&str> = path.splitn(2, '.').collect();
let entity_component = parts[0];
let field_path = parts.get(1).copied();
let asset = db.get(entity_component).ok()?;
let v: Value = if let Some(ref inline) = asset.entry.inline_data {
inline.clone()
} else {
serde_json::from_slice(&asset.data).ok()?
};
let target = if let Some(fp) = field_path {
let mut current = &v;
for key in fp.split('.') {
if let Ok(idx) = key.parse::<usize>() {
current = current.get(idx)?;
} else {
current = current.get(key)?;
}
}
current.clone()
} else {
v
};
target.as_f64()
}
fn write_target(db: &std::sync::Arc<reflow_assets::AssetDB>, path: &str, value: &Value) {
let parts: Vec<&str> = path.splitn(2, '.').collect();
let entity_component = parts[0];
if parts.len() == 1 {
let _ = db.put_json(entity_component, value.clone(), json!({}));
return;
}
let field_path = parts[1];
if let Ok(asset) = db.get(entity_component) {
let mut current: Value = if let Some(ref inline) = asset.entry.inline_data {
inline.clone()
} else {
serde_json::from_slice(&asset.data).unwrap_or(json!({}))
};
set_json_path(&mut current, field_path, value.clone());
let _ = db.put_json(entity_component, current, asset.entry.metadata);
}
}
fn set_json_path(obj: &mut Value, path: &str, value: Value) {
let keys: Vec<&str> = path.split('.').collect();
let mut current = obj;
for (i, key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
if let Ok(idx) = key.parse::<usize>() {
if let Value::Array(ref mut arr) = current {
if idx < arr.len() {
arr[idx] = value;
return;
}
}
}
current[key] = value;
return;
}
if let Ok(idx) = key.parse::<usize>() {
current = &mut current[idx];
} else {
if !current
.get(key)
.map(|v| v.is_object() || v.is_array())
.unwrap_or(false)
{
current[key] = json!({});
}
current = &mut current[key];
}
}
}