use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::engine::ecs::{ComponentId, IntentValue, RxWorld, SignalEmitter, World};
use crate::engine::graphics::render_assets::RenderAssets;
use crate::scripting::object::{HeapHandle, MaterializedCE, Value};
use crate::scripting::world_evaluator::{
EvalRequest, EvalResponse, HostCallKind, HostValue, MeowMeowEvaluator, eval_mms_fn,
eval_module_source,
};
#[derive(Debug, Default)]
pub struct EvalOutput {
pub intents: Vec<IntentValue>,
pub errors: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct LoadedMmsModule {
pub named_exports: HashMap<String, Value>,
pub sequence: Vec<MaterializedCE>,
pub heap: HeapHandle,
pub source_path: Option<String>,
}
impl LoadedMmsModule {
pub fn named_export(&self, name: &str) -> Option<&Value> {
self.named_exports.get(name)
}
}
pub struct MeowMeowRunner;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleFactoryEvalMode {
Template,
Live,
}
fn headers_to_value(headers: &[(String, String)]) -> Value {
Value::Map(
headers
.iter()
.map(|(name, value)| (name.clone(), Value::String(value.clone())))
.collect(),
)
}
pub(crate) fn event_arg_value(signal: &crate::engine::ecs::Signal) -> Value {
match signal.event.as_ref() {
Some(crate::engine::ecs::EventSignal::FrameTick { dt_sec }) => {
Value::Map(HashMap::from([(
"dt_sec".to_string(),
Value::Number(*dt_sec as f64),
)]))
}
Some(crate::engine::ecs::EventSignal::GltfInitialized { gltf, uri }) => {
Value::Map(HashMap::from([
(
"gltf".to_string(),
Value::ComponentObject {
id: *gltf,
component_type: "GLTF".to_string(),
},
),
("uri".to_string(), Value::String(uri.clone())),
]))
}
Some(crate::engine::ecs::EventSignal::DataEvent { name, .. }) => {
Value::String(name.clone())
}
Some(crate::engine::ecs::EventSignal::ToggleChanged { toggle, value }) => {
Value::Map(HashMap::from([
(
"toggle".into(),
Value::ComponentObject {
id: *toggle,
component_type: "Toggle".into(),
},
),
("value".into(), Value::Bool(*value)),
]))
}
Some(crate::engine::ecs::EventSignal::XrButtonDown {
hand,
control,
value,
..
})
| Some(crate::engine::ecs::EventSignal::XrButtonUp {
hand,
control,
value,
..
})
| Some(crate::engine::ecs::EventSignal::XrButtonChanged {
hand,
control,
value,
..
}) => Value::Map(HashMap::from([
("hand".to_string(), Value::String(format!("{hand:?}"))),
("control".to_string(), Value::String(format!("{control:?}"))),
("value".to_string(), Value::Number(*value as f64)),
])),
Some(crate::engine::ecs::EventSignal::XrAxisChanged {
hand,
control,
value,
..
}) => Value::Map(HashMap::from([
("hand".to_string(), Value::String(format!("{hand:?}"))),
("control".to_string(), Value::String(format!("{control:?}"))),
(
"value".to_string(),
Value::Array(vec![
Value::Number(value[0] as f64),
Value::Number(value[1] as f64),
]),
),
])),
Some(crate::engine::ecs::EventSignal::TextInputChanged { text, caret, .. }) => {
Value::Map(HashMap::from([
("text".to_string(), Value::String(text.clone())),
("caret".to_string(), Value::Number(*caret as f64)),
]))
}
Some(crate::engine::ecs::EventSignal::HttpRequest {
request_id,
method,
path,
query,
url,
headers,
body_text,
remote_addr,
}) => Value::Map(HashMap::from([
("request_id".to_string(), Value::Number(*request_id as f64)),
("method".to_string(), Value::String(method.clone())),
("path".to_string(), Value::String(path.clone())),
(
"query".to_string(),
query
.as_ref()
.map(|query| Value::String(query.clone()))
.unwrap_or(Value::Null),
),
("url".to_string(), Value::String(url.clone())),
("target".to_string(), Value::String(url.clone())),
("headers".to_string(), headers_to_value(headers)),
("body_text".to_string(), Value::String(body_text.clone())),
(
"remote_addr".to_string(),
remote_addr
.as_ref()
.map(|addr| Value::String(addr.clone()))
.unwrap_or(Value::Null),
),
])),
Some(crate::engine::ecs::EventSignal::HttpResponse {
request_id,
status,
ok,
headers,
body_text,
url,
}) => Value::Map(HashMap::from([
("request_id".to_string(), Value::Number(*request_id as f64)),
("status".to_string(), Value::Number(*status as f64)),
("ok".to_string(), Value::Bool(*ok)),
("headers".to_string(), headers_to_value(headers)),
("body_text".to_string(), Value::String(body_text.clone())),
("url".to_string(), Value::String(url.clone())),
])),
Some(crate::engine::ecs::EventSignal::HttpError {
request_id,
phase,
message,
url,
bind_addr,
}) => Value::Map(HashMap::from([
(
"request_id".to_string(),
request_id
.map(|request_id| Value::Number(request_id as f64))
.unwrap_or(Value::Null),
),
("phase".to_string(), Value::String(phase.clone())),
("message".to_string(), Value::String(message.clone())),
(
"url".to_string(),
url.as_ref()
.map(|url| Value::String(url.clone()))
.unwrap_or(Value::Null),
),
(
"bind_addr".to_string(),
bind_addr
.as_ref()
.map(|bind_addr| Value::String(bind_addr.clone()))
.unwrap_or(Value::Null),
),
])),
_ => Value::Null,
}
}
impl MeowMeowRunner {
pub fn eval(source: &str) -> EvalOutput {
Self::eval_impl(source, None, Duration::from_secs(2))
}
pub fn eval_with_timeout(source: &str, timeout: Duration) -> EvalOutput {
Self::eval_impl(source, None, timeout)
}
pub fn eval_with_path(source: &str, path: &str) -> EvalOutput {
Self::eval_impl(source, Some(path), Duration::from_secs(2))
}
pub fn eval_file(path: &str) -> EvalOutput {
Self::eval_file_with_timeout(path, Duration::from_secs(2))
}
pub fn eval_file_with_timeout(path: &str, timeout: Duration) -> EvalOutput {
match std::fs::read_to_string(path) {
Ok(source) => Self::eval_impl(&source, Some(path), timeout),
Err(e) => {
let mut output = EvalOutput::default();
output
.errors
.push(format!("cannot read file '{}': {}", path, e));
output
}
}
}
pub fn load_module_source(
source: &str,
source_path: Option<&str>,
) -> Result<LoadedMmsModule, String> {
let module = match eval_module_source(source, source_path)? {
Value::Module {
named,
sequence,
heap,
} => Ok(LoadedMmsModule {
named_exports: named,
sequence,
heap,
source_path: source_path.map(|s| s.to_string()),
}),
other => Err(format!(
"load_module_source: expected module result, got {:?}",
other
)),
}?;
Ok(module)
}
pub fn load_module_file(path: &str) -> Result<LoadedMmsModule, String> {
let source = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read module '{}': {}", path, e))?;
Self::load_module_source(&source, Some(path))
}
pub fn call_mms_module_fn(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
channels: Option<&mut crate::scripting::world_evaluator::EvalChannels>,
world_host: Option<&mut World>,
emit: Option<&mut dyn SignalEmitter>,
) -> Result<Value, String> {
let Some(export) = module.named_export(name) else {
return Err(format!("call_mms_module_fn: export '{}' not found", name));
};
if !matches!(export, Value::Function { .. }) {
return Err(format!(
"call_mms_module_fn: export '{}' is not a function",
name
));
}
eval_mms_fn(export, args, channels, world_host, emit)
}
pub fn materialize_mms_module_component(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
world_host: Option<&mut World>,
emit: Option<&mut dyn SignalEmitter>,
) -> Result<MaterializedCE, String> {
Self::materialize_mms_module_component_in_mode(
module,
name,
args,
world_host,
emit,
ModuleFactoryEvalMode::Template,
)
}
pub fn materialize_mms_module_component_in_mode(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
world_host: Option<&mut World>,
emit: Option<&mut dyn SignalEmitter>,
mode: ModuleFactoryEvalMode,
) -> Result<MaterializedCE, String> {
match mode {
ModuleFactoryEvalMode::Template => {}
ModuleFactoryEvalMode::Live => {
return Err(
"materialize_mms_module_component_in_mode: live mode does not return a stable MaterializedCE; use a spawn/instantiate helper instead".to_string()
)
}
}
let _ = world_host;
let _ = emit;
let value = Self::call_mms_module_fn(module, name, args, None, None, None)?;
let Value::ComponentExpr(component_expr) = value else {
return Err(format!(
"materialize_mms_module_component: export '{}' did not return a component tree",
name
));
};
Ok(*component_expr)
}
pub fn materialize_mms_module_component_from_file(
path: &str,
name: &str,
args: Vec<Value>,
world_host: Option<&mut World>,
emit: Option<&mut dyn SignalEmitter>,
) -> Result<MaterializedCE, String> {
let module = Self::load_module_file(path)?;
Self::materialize_mms_module_component(&module, name, args, world_host, emit)
}
pub fn spawn_mms_module_component_uninitialized(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
world: &mut World,
emit: &mut dyn SignalEmitter,
) -> Result<ComponentId, String> {
Self::spawn_mms_module_component_uninitialized_with_assets(
module, name, args, world, None, emit,
)
}
pub fn spawn_mms_module_component_uninitialized_with_assets(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
world: &mut World,
render_assets: Option<&mut RenderAssets>,
emit: &mut dyn SignalEmitter,
) -> Result<ComponentId, String> {
Self::spawn_mms_module_component_value(
module,
name,
args,
None,
world,
render_assets,
emit,
false,
)
}
pub fn spawn_mms_module_component_uninitialized_from_file(
path: &str,
name: &str,
args: Vec<Value>,
world: &mut World,
emit: &mut dyn SignalEmitter,
) -> Result<ComponentId, String> {
let module = Self::load_module_file(path)?;
Self::spawn_mms_module_component_uninitialized(&module, name, args, world, emit)
}
pub fn spawn_mms_module_component(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
parent: Option<ComponentId>,
world: &mut World,
emit: &mut dyn SignalEmitter,
) -> Result<ComponentId, String> {
Self::spawn_mms_module_component_value(module, name, args, parent, world, None, emit, true)
}
pub fn spawn_mms_module_component_from_file(
path: &str,
name: &str,
args: Vec<Value>,
parent: Option<ComponentId>,
world: &mut World,
emit: &mut dyn SignalEmitter,
) -> Result<ComponentId, String> {
let module = Self::load_module_file(path)?;
Self::spawn_mms_module_component(&module, name, args, parent, world, emit)
}
fn spawn_mms_module_component_value(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
parent: Option<ComponentId>,
world: &mut World,
mut render_assets: Option<&mut RenderAssets>,
emit: &mut dyn SignalEmitter,
initialize: bool,
) -> Result<ComponentId, String> {
let value = Self::eval_mms_module_component_live(
module,
name,
args,
world,
render_assets.as_deref_mut(),
emit,
)?;
match value {
Value::ComponentObject { id, .. } => {
if let Some(p) = parent {
world
.add_child(p, id)
.map_err(|e| format!("attach live module component failed: {e}"))?;
}
if initialize {
let should_init = parent.map(|p| world.is_initialized(p)).unwrap_or(true);
if should_init {
world.init_component_tree(id, emit);
}
}
Ok(id)
}
Value::ComponentExpr(component_expr) => {
if let Some(render_assets) = render_assets.as_deref_mut() {
crate::scripting::component_registry::with_live_render_assets(
render_assets,
|| {
if initialize {
crate::scripting::component_registry::spawn_tree(
&component_expr,
parent,
world,
emit,
)
} else {
crate::scripting::component_registry::spawn_tree_uninitialized(
&component_expr,
world,
emit,
)
}
},
)
} else if initialize {
crate::scripting::component_registry::spawn_tree(
&component_expr,
parent,
world,
emit,
)
} else {
crate::scripting::component_registry::spawn_tree_uninitialized(
&component_expr,
world,
emit,
)
}
}
other => Err(format!(
"spawn_mms_module_component: export '{}' did not return a component tree, got {:?}",
name, other
)),
}
}
fn eval_mms_module_component_live(
module: &LoadedMmsModule,
name: &str,
args: Vec<Value>,
world: &mut World,
render_assets: Option<&mut RenderAssets>,
emit: &mut dyn SignalEmitter,
) -> Result<Value, String> {
if let Some(render_assets) = render_assets {
crate::scripting::component_registry::with_live_render_assets(render_assets, || {
Self::call_mms_module_fn(module, name, args, None, Some(world), Some(emit))
})
} else {
Self::call_mms_module_fn(module, name, args, None, Some(world), Some(emit))
}
}
pub fn eval_with_world(
source: &str,
world: &mut World,
rx: &mut RxWorld,
emit: &mut dyn SignalEmitter,
) -> EvalOutput {
Self::eval_with_world_at_path(source, None, world, rx, emit)
}
pub fn eval_with_world_at_path(
source: &str,
source_path: Option<&str>,
world: &mut World,
rx: &mut RxWorld,
emit: &mut dyn SignalEmitter,
) -> EvalOutput {
Self::eval_with_world_and_assets_at_path(source, source_path, world, rx, None, emit)
}
pub fn eval_with_world_and_assets(
source: &str,
world: &mut World,
rx: &mut RxWorld,
render_assets: &mut RenderAssets,
emit: &mut dyn SignalEmitter,
) -> EvalOutput {
Self::eval_with_world_and_assets_at_path(source, None, world, rx, Some(render_assets), emit)
}
pub fn eval_with_world_and_assets_at_path(
source: &str,
source_path: Option<&str>,
world: &mut World,
rx: &mut RxWorld,
mut render_assets: Option<&mut RenderAssets>,
emit: &mut dyn SignalEmitter,
) -> EvalOutput {
let mut handle = MeowMeowEvaluator::spawn(64);
handle
.requests
.push(EvalRequest::EvalScript {
source: source.to_string(),
source_path: source_path.map(|s| s.to_string()),
})
.expect("MeowMeowRunner: push EvalScript");
handle
.requests
.push(EvalRequest::Shutdown)
.expect("MeowMeowRunner: push Shutdown");
let mut output = EvalOutput::default();
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match handle.responses.pop() {
Ok(EvalResponse::Intent(iv)) => output.intents.push(iv),
Ok(EvalResponse::Error { message }) => output.errors.push(message),
Ok(EvalResponse::ParsedOk { .. }) => {}
Ok(EvalResponse::SnippetComplete { .. }) => {}
Ok(EvalResponse::NavigationComplete { .. } | EvalResponse::ReplReset) => {}
Ok(EvalResponse::ShutdownAck) => break,
Ok(EvalResponse::HostCall { id, kind }) => {
let reply = match kind {
HostCallKind::Spawn(ce) => {
let result = if let Some(render_assets) = render_assets.as_deref_mut() {
crate::scripting::component_registry::with_live_render_assets(
render_assets,
|| {
crate::scripting::component_registry::spawn_tree(
&ce, None, world, emit,
)
},
)
} else {
crate::scripting::component_registry::spawn_tree(
&ce, None, world, emit,
)
};
match result {
Ok(component_id) => HostValue::ComponentId(component_id),
Err(e) => {
output.errors.push(format!("HostCall::Spawn error: {e}"));
HostValue::Null
}
}
}
HostCallKind::Register(ce) => {
let result = if let Some(render_assets) = render_assets.as_deref_mut() {
crate::scripting::component_registry::with_live_render_assets(
render_assets,
|| {
crate::scripting::component_registry::spawn_tree_uninitialized(
&ce, world, emit,
)
},
)
} else {
crate::scripting::component_registry::spawn_tree_uninitialized(
&ce, world, emit,
)
};
match result {
Ok(component_id) => HostValue::ComponentId(component_id),
Err(e) => {
output.errors.push(format!("HostCall::Register error: {e}"));
HostValue::Null
}
}
}
HostCallKind::Attach { parent, child } => {
if let Some(p) = parent {
if let Err(e) = world.add_child(p, child) {
output.errors.push(format!("HostCall::Attach error: {e}"));
}
}
world.init_component_tree(child, emit);
HostValue::Null
}
HostCallKind::Query {
selector,
scope,
multiple,
} => {
let roots: Vec<crate::engine::ecs::ComponentId> = match scope {
Some(id) => world.scripting_query_roots(id),
None => world
.all_components()
.filter(|&id| world.parent_of(id).is_none())
.collect(),
};
let mut all_ids: Vec<crate::engine::ecs::ComponentId> = Vec::new();
for r in roots {
if multiple {
all_ids.extend(world.find_all_components(r, &selector));
} else if let Some(found) = world.find_component(r, &selector) {
all_ids.push(found);
break;
}
}
if multiple {
let list = all_ids
.into_iter()
.filter_map(|id| {
world.component_name(id).map(|t| (id, t.to_string()))
})
.collect();
HostValue::ComponentList(list)
} else {
match all_ids.into_iter().next() {
Some(id) => match world.component_name(id) {
Some(t) => HostValue::Component {
id,
component_type: t.to_string(),
},
None => HostValue::Null,
},
None => HostValue::Null,
}
}
}
HostCallKind::RegisterHandler {
scope,
signal_kind,
name,
handler,
} => {
let callback =
move |world: &mut World,
emit: &mut dyn SignalEmitter,
signal: &crate::engine::ecs::Signal| {
let arg = event_arg_value(signal);
if let Err(e) = eval_mms_fn(
&handler,
vec![arg],
None,
Some(world),
Some(emit),
) {
eprintln!("[mms] handler error: {e}");
}
};
if let Some(name) = name {
rx.add_handler_closure_named(
signal_kind,
scope,
Some(name),
callback,
);
} else {
rx.add_handler_closure(signal_kind, scope, callback);
}
HostValue::Null
}
HostCallKind::RegisterGlobalHandler {
signal_kind,
name,
handler,
} => {
let callback = move |world: &mut World,
emit: &mut dyn SignalEmitter,
signal: &crate::engine::ecs::Signal| {
let arg = event_arg_value(signal);
if let Err(e) = eval_mms_fn(
&handler,
vec![arg],
None,
Some(world),
Some(emit),
) {
eprintln!("[mms] global handler error: {e}");
}
};
if let Some(name) = name {
rx.add_global_handler_closure_named(
signal_kind,
Some(name),
callback,
);
} else {
rx.add_global_handler_closure(signal_kind, callback);
}
HostValue::Null
}
HostCallKind::AudioClipInstance {
source,
start_beat,
stop_beat,
} => {
use crate::engine::ecs::component::AudioClipComponent;
match world.get_component_by_id_as::<AudioClipComponent>(source) {
Some(src) => {
let mut c = AudioClipComponent::instance_of(src);
if let Some(sb) = start_beat {
c.start_beat = sb;
}
if let Some(eb) = stop_beat {
c.stop_beat = Some(eb);
}
let id = world.add_component(c);
HostValue::ComponentId(id)
}
None => {
output.errors.push(
"HostCall::AudioClipInstance: source is not an AudioClip"
.to_string(),
);
HostValue::Null
}
}
}
HostCallKind::InvokeComponentMethod {
id,
component_type,
method,
args,
} => match crate::scripting::component_method_registry::invoke_component_method(
world,
id,
&component_type,
&method,
&args,
|intent| output.intents.push(intent),
) {
Ok(value) => match value {
Value::Null => HostValue::Null,
Value::ComponentObject { id, component_type } => {
HostValue::Component { id, component_type }
}
other => HostValue::Value(other),
},
Err(e) => {
output
.errors
.push(format!("HostCall::InvokeComponentMethod error: {e}"));
HostValue::Null
}
},
HostCallKind::ReplTree { .. }
| HostCallKind::ReplDump { .. }
| HostCallKind::ReplHelp
| HostCallKind::ReplClear => HostValue::Null,
};
let _ = handle
.requests
.push(EvalRequest::HostCallResult { id, value: reply });
}
Err(rtrb::PopError::Empty) => {
if Instant::now() > deadline {
output
.errors
.push("MeowMeowRunner: timed out waiting for evaluator".into());
break;
}
std::thread::yield_now();
}
}
}
handle.shutdown_and_join();
output
}
fn eval_impl(source: &str, source_path: Option<&str>, timeout: Duration) -> EvalOutput {
let mut handle = MeowMeowEvaluator::spawn(64);
handle
.requests
.push(EvalRequest::EvalScript {
source: source.to_string(),
source_path: source_path.map(|s| s.to_string()),
})
.expect("MeowMeowRunner: push EvalScript");
handle
.requests
.push(EvalRequest::Shutdown)
.expect("MeowMeowRunner: push Shutdown");
let mut output = EvalOutput::default();
let deadline = Instant::now() + timeout;
loop {
match handle.responses.pop() {
Ok(EvalResponse::Intent(iv)) => output.intents.push(iv),
Ok(EvalResponse::Error { message }) => output.errors.push(message),
Ok(EvalResponse::ParsedOk { .. }) => {}
Ok(EvalResponse::SnippetComplete { .. }) => {}
Ok(EvalResponse::NavigationComplete { .. } | EvalResponse::ReplReset) => {}
Ok(EvalResponse::ShutdownAck) => break,
Ok(EvalResponse::HostCall { id, .. }) => {
let _ = handle.requests.push(EvalRequest::HostCallResult {
id,
value: HostValue::Null,
});
}
Err(rtrb::PopError::Empty) => {
if Instant::now() > deadline {
output
.errors
.push("MeowMeowRunner: timed out waiting for evaluator".into());
break;
}
std::thread::yield_now();
}
}
}
handle.shutdown_and_join();
output
}
}