use freecs::Entity;
use rhai::Scope;
use winit::keyboard::KeyCode;
use super::api::register_api;
use super::components::{Script, ScriptSource};
use super::resources::ScriptRuntime;
use crate::ecs::world::commands::{WorldCommand, spawn_cube_at, spawn_sphere_at};
use crate::ecs::world::{SCRIPT, World};
pub fn run_scripts_system(world: &mut World) {
let _span = tracing::info_span!("scripts").entered();
let mut runtime = std::mem::take(&mut world.resources.script_runtime);
run_scripts_system_inner(world, &mut runtime);
world.resources.script_runtime = runtime;
}
fn run_scripts_system_inner(world: &mut World, runtime: &mut ScriptRuntime) {
register_api(&mut runtime.engine);
let delta_time = world.resources.window.timing.delta_time;
runtime.accumulated_time += delta_time as f64;
let pressed_keys = build_pressed_keys(world);
let just_pressed_keys = build_just_pressed_keys(world, runtime);
let mouse_position = {
let mouse = &world.resources.input.mouse;
(mouse.position.x as f64, mouse.position.y as f64)
};
#[cfg(not(target_arch = "wasm32"))]
{
let reloaded = runtime.check_hot_reload();
for path in reloaded {
tracing::info!("Hot-reloaded script: {}", path);
}
}
let entities: Vec<Entity> = world.query_entities(SCRIPT).collect();
let mut commands: Vec<(Entity, ScriptResult)> = Vec::new();
for entity in entities {
let script = match world.get_script(entity) {
Some(s) => s.clone(),
None => continue,
};
if !script.enabled {
continue;
}
let transform = world
.get_local_transform(entity)
.copied()
.unwrap_or_default();
let euler = quat_to_euler(&transform.rotation);
let mut scope = Scope::new();
scope.push("pos_x", transform.translation.x as f64);
scope.push("pos_y", transform.translation.y as f64);
scope.push("pos_z", transform.translation.z as f64);
scope.push("rot_x", euler.0 as f64);
scope.push("rot_y", euler.1 as f64);
scope.push("rot_z", euler.2 as f64);
scope.push("scale_x", transform.scale.x as f64);
scope.push("scale_y", transform.scale.y as f64);
scope.push("scale_z", transform.scale.z as f64);
scope.push("dt", delta_time as f64);
scope.push("delta_time", delta_time as f64);
scope.push("time", runtime.accumulated_time);
scope.push("mouse_x", mouse_position.0);
scope.push("mouse_y", mouse_position.1);
scope.push("pressed_keys", pressed_keys.clone());
scope.push("just_pressed_keys", just_pressed_keys.clone());
scope.push_constant("entity_id", entity.id as i64);
scope.push("spawn_cube_x", 0.0_f64);
scope.push("spawn_cube_y", 0.0_f64);
scope.push("spawn_cube_z", 0.0_f64);
scope.push("do_spawn_cube", false);
scope.push("spawn_sphere_x", 0.0_f64);
scope.push("spawn_sphere_y", 0.0_f64);
scope.push("spawn_sphere_z", 0.0_f64);
scope.push("do_spawn_sphere", false);
scope.push("do_despawn", false);
let despawn_names: rhai::Array = Vec::new();
scope.push("despawn_names", despawn_names);
let mut entities_map = rhai::Map::new();
let mut names_array: rhai::Array = Vec::new();
for (name, &ent) in &world.resources.entity_names {
names_array.push(rhai::Dynamic::from(name.clone()));
if let Some(transform) = world.get_local_transform(ent) {
let mut pos_map = rhai::Map::new();
pos_map.insert(
"x".into(),
rhai::Dynamic::from(transform.translation.x as f64),
);
pos_map.insert(
"y".into(),
rhai::Dynamic::from(transform.translation.y as f64),
);
pos_map.insert(
"z".into(),
rhai::Dynamic::from(transform.translation.z as f64),
);
pos_map.insert(
"scale_x".into(),
rhai::Dynamic::from(transform.scale.x as f64),
);
pos_map.insert(
"scale_y".into(),
rhai::Dynamic::from(transform.scale.y as f64),
);
pos_map.insert(
"scale_z".into(),
rhai::Dynamic::from(transform.scale.z as f64),
);
entities_map.insert(name.clone().into(), rhai::Dynamic::from(pos_map));
}
}
scope.push("entities", entities_map);
scope.push("entity_names", names_array);
let mut state_map = rhai::Map::new();
for (key, value) in &runtime.game_state {
state_map.insert(key.clone().into(), rhai::Dynamic::from(*value));
}
scope.push("state", state_map);
let result = execute_script(runtime, &script, &mut scope);
match result {
Ok(()) => {
let new_pos_x = scope
.get_value::<f64>("pos_x")
.unwrap_or(transform.translation.x as f64);
let new_pos_y = scope
.get_value::<f64>("pos_y")
.unwrap_or(transform.translation.y as f64);
let new_pos_z = scope
.get_value::<f64>("pos_z")
.unwrap_or(transform.translation.z as f64);
let new_scale_x = scope
.get_value::<f64>("scale_x")
.unwrap_or(transform.scale.x as f64);
let new_scale_y = scope
.get_value::<f64>("scale_y")
.unwrap_or(transform.scale.y as f64);
let new_scale_z = scope
.get_value::<f64>("scale_z")
.unwrap_or(transform.scale.z as f64);
let new_euler_x = scope.get_value::<f64>("rot_x").unwrap_or(euler.0 as f64);
let new_euler_y = scope.get_value::<f64>("rot_y").unwrap_or(euler.1 as f64);
let new_euler_z = scope.get_value::<f64>("rot_z").unwrap_or(euler.2 as f64);
let new_rotation =
euler_to_quat(new_euler_x as f32, new_euler_y as f32, new_euler_z as f32);
let do_spawn_cube = scope.get_value::<bool>("do_spawn_cube").unwrap_or(false);
let do_spawn_sphere = scope.get_value::<bool>("do_spawn_sphere").unwrap_or(false);
let do_despawn = scope.get_value::<bool>("do_despawn").unwrap_or(false);
let despawn_names: Vec<String> = scope
.get_value::<rhai::Array>("despawn_names")
.unwrap_or_default()
.into_iter()
.filter_map(|v| v.into_string().ok())
.collect();
let mut pending_spawns = Vec::new();
if do_spawn_cube {
let x = scope.get_value::<f64>("spawn_cube_x").unwrap_or(0.0);
let y = scope.get_value::<f64>("spawn_cube_y").unwrap_or(0.0);
let z = scope.get_value::<f64>("spawn_cube_z").unwrap_or(0.0);
pending_spawns.push((
"Cube".to_string(),
nalgebra_glm::vec3(x as f32, y as f32, z as f32),
));
}
if do_spawn_sphere {
let x = scope.get_value::<f64>("spawn_sphere_x").unwrap_or(0.0);
let y = scope.get_value::<f64>("spawn_sphere_y").unwrap_or(0.0);
let z = scope.get_value::<f64>("spawn_sphere_z").unwrap_or(0.0);
pending_spawns.push((
"Sphere".to_string(),
nalgebra_glm::vec3(x as f32, y as f32, z as f32),
));
}
if let Some(updated_state) = scope.get_value::<rhai::Map>("state") {
for (key, value) in updated_state {
if let Some(num) = value
.as_float()
.ok()
.or_else(|| value.as_int().ok().map(|i| i as f64))
{
runtime.game_state.insert(key.to_string(), num);
}
}
}
let pos_changed = (new_pos_x as f32 - transform.translation.x).abs() > f32::EPSILON
|| (new_pos_y as f32 - transform.translation.y).abs() > f32::EPSILON
|| (new_pos_z as f32 - transform.translation.z).abs() > f32::EPSILON;
let scale_changed = (new_scale_x as f32 - transform.scale.x).abs() > f32::EPSILON
|| (new_scale_y as f32 - transform.scale.y).abs() > f32::EPSILON
|| (new_scale_z as f32 - transform.scale.z).abs() > f32::EPSILON;
let rot_changed = (new_euler_x as f32 - euler.0).abs() > f32::EPSILON
|| (new_euler_y as f32 - euler.1).abs() > f32::EPSILON
|| (new_euler_z as f32 - euler.2).abs() > f32::EPSILON;
commands.push((
entity,
ScriptResult {
new_position: if pos_changed {
Some(nalgebra_glm::vec3(
new_pos_x as f32,
new_pos_y as f32,
new_pos_z as f32,
))
} else {
None
},
new_rotation: if rot_changed {
Some(new_rotation)
} else {
None
},
new_scale: if scale_changed {
Some(nalgebra_glm::vec3(
new_scale_x as f32,
new_scale_y as f32,
new_scale_z as f32,
))
} else {
None
},
pending_spawns,
should_despawn: do_despawn,
despawn_names,
},
));
}
Err(error) => {
tracing::error!("Script error on entity {:?}: {}", entity, error);
}
}
}
for (entity, result) in commands {
if result.should_despawn {
world.queue_command(WorldCommand::DespawnRecursive { entity });
runtime.remove_entity_scope(entity);
continue;
}
if let Some(pos) = result.new_position {
if let Some(transform) = world.get_local_transform_mut(entity) {
transform.translation = pos;
}
world.mark_local_transform_dirty(entity);
}
if let Some(rot) = result.new_rotation {
if let Some(transform) = world.get_local_transform_mut(entity) {
transform.rotation = rot;
}
world.mark_local_transform_dirty(entity);
}
if let Some(scale) = result.new_scale {
if let Some(transform) = world.get_local_transform_mut(entity) {
transform.scale = scale;
}
world.mark_local_transform_dirty(entity);
}
for (mesh_type, position) in result.pending_spawns {
match mesh_type.as_str() {
"Cube" => {
spawn_cube_at(world, position);
}
"Sphere" => {
spawn_sphere_at(world, position);
}
_ => {
tracing::warn!("Unknown spawn type: {}", mesh_type);
}
}
}
for name in result.despawn_names {
if let Some(&target_entity) = world.resources.entity_names.get(&name) {
world.queue_command(WorldCommand::DespawnRecursive {
entity: target_entity,
});
world.resources.entity_names.remove(&name);
}
}
}
runtime.previous_keys.clear();
for (key, state) in &world.resources.input.keyboard.keystates {
if *state == winit::event::ElementState::Pressed {
runtime.previous_keys.insert(*key);
}
}
}
struct ScriptResult {
new_position: Option<nalgebra_glm::Vec3>,
new_rotation: Option<nalgebra_glm::Quat>,
new_scale: Option<nalgebra_glm::Vec3>,
pending_spawns: Vec<(String, nalgebra_glm::Vec3)>,
should_despawn: bool,
despawn_names: Vec<String>,
}
fn execute_script(
runtime: &mut ScriptRuntime,
script: &Script,
scope: &mut Scope,
) -> Result<(), String> {
let (key, source) = match &script.source {
ScriptSource::File { path } => {
#[cfg(not(target_arch = "wasm32"))]
{
runtime.track_file(path);
let source = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read script file '{}': {}", path, e))?;
(path.clone(), source)
}
#[cfg(target_arch = "wasm32")]
{
let _ = path;
return Err("File-based scripts not supported on wasm".to_string());
}
}
ScriptSource::Embedded { source } => {
let key = format!(
"embedded_{}",
scope.get_value::<i64>("entity_id").unwrap_or(0)
);
(key, source.clone())
}
};
let ast = runtime.compile_script(&key, &source)?.clone();
runtime
.engine
.run_ast_with_scope(scope, &ast)
.map_err(|e| format!("Runtime error: {}", e))?;
Ok(())
}
fn build_pressed_keys(world: &World) -> rhai::Array {
let keyboard = &world.resources.input.keyboard;
let mut keys = Vec::new();
for (key, state) in &keyboard.keystates {
if *state == winit::event::ElementState::Pressed
&& let Some(name) = keycode_to_string(*key)
{
keys.push(rhai::Dynamic::from(name));
}
}
keys
}
fn build_just_pressed_keys(world: &World, runtime: &ScriptRuntime) -> rhai::Array {
let keyboard = &world.resources.input.keyboard;
let mut keys = Vec::new();
for (key, state) in &keyboard.keystates {
if *state == winit::event::ElementState::Pressed
&& !runtime.previous_keys.contains(key)
&& let Some(name) = keycode_to_string(*key)
{
keys.push(rhai::Dynamic::from(name));
}
}
keys
}
fn keycode_to_string(key: KeyCode) -> Option<String> {
let name = match key {
KeyCode::KeyA => "A",
KeyCode::KeyB => "B",
KeyCode::KeyC => "C",
KeyCode::KeyD => "D",
KeyCode::KeyE => "E",
KeyCode::KeyF => "F",
KeyCode::KeyG => "G",
KeyCode::KeyH => "H",
KeyCode::KeyI => "I",
KeyCode::KeyJ => "J",
KeyCode::KeyK => "K",
KeyCode::KeyL => "L",
KeyCode::KeyM => "M",
KeyCode::KeyN => "N",
KeyCode::KeyO => "O",
KeyCode::KeyP => "P",
KeyCode::KeyQ => "Q",
KeyCode::KeyR => "R",
KeyCode::KeyS => "S",
KeyCode::KeyT => "T",
KeyCode::KeyU => "U",
KeyCode::KeyV => "V",
KeyCode::KeyW => "W",
KeyCode::KeyX => "X",
KeyCode::KeyY => "Y",
KeyCode::KeyZ => "Z",
KeyCode::Digit0 => "0",
KeyCode::Digit1 => "1",
KeyCode::Digit2 => "2",
KeyCode::Digit3 => "3",
KeyCode::Digit4 => "4",
KeyCode::Digit5 => "5",
KeyCode::Digit6 => "6",
KeyCode::Digit7 => "7",
KeyCode::Digit8 => "8",
KeyCode::Digit9 => "9",
KeyCode::Space => "SPACE",
KeyCode::Enter => "ENTER",
KeyCode::Escape => "ESCAPE",
KeyCode::ShiftLeft | KeyCode::ShiftRight => "SHIFT",
KeyCode::ControlLeft | KeyCode::ControlRight => "CTRL",
KeyCode::AltLeft | KeyCode::AltRight => "ALT",
KeyCode::Tab => "TAB",
KeyCode::Backspace => "BACKSPACE",
KeyCode::ArrowUp => "UP",
KeyCode::ArrowDown => "DOWN",
KeyCode::ArrowLeft => "LEFT",
KeyCode::ArrowRight => "RIGHT",
_ => return None,
};
Some(name.to_string())
}
fn quat_to_euler(q: &nalgebra_glm::Quat) -> (f32, f32, f32) {
let sinr_cosp = 2.0 * (q.w * q.i + q.j * q.k);
let cosr_cosp = 1.0 - 2.0 * (q.i * q.i + q.j * q.j);
let roll = sinr_cosp.atan2(cosr_cosp);
let sinp = 2.0 * (q.w * q.j - q.k * q.i);
let pitch = if sinp.abs() >= 1.0 {
std::f32::consts::FRAC_PI_2.copysign(sinp)
} else {
sinp.asin()
};
let siny_cosp = 2.0 * (q.w * q.k + q.i * q.j);
let cosy_cosp = 1.0 - 2.0 * (q.j * q.j + q.k * q.k);
let yaw = siny_cosp.atan2(cosy_cosp);
(roll, pitch, yaw)
}
fn euler_to_quat(roll: f32, pitch: f32, yaw: f32) -> nalgebra_glm::Quat {
let cr = (roll * 0.5).cos();
let sr = (roll * 0.5).sin();
let cp = (pitch * 0.5).cos();
let sp = (pitch * 0.5).sin();
let cy = (yaw * 0.5).cos();
let sy = (yaw * 0.5).sin();
nalgebra_glm::quat(
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
cr * cp * cy + sr * sp * sy,
)
}