use std::time::SystemTime; use crossterm::{ event::KeyCode, style::{Attribute, Attributes, Color, ContentStyle}, }; use hex_color::HexColor; use mlua::{FromLua, IntoLua, Lua, LuaOptions, StdLib, Table, Value}; use serde::{Deserialize, Serialize}; use crate::{ buffer::Cell, scene::scene_id_by_name, script::{FullScriptContext, Operation}, }; pub struct LuaModule { interpreter: Lua, } impl LuaModule { pub fn new(data: &[u8]) -> mlua::Result { macro_rules! add_fn { ($lua: expr, $table: expr, $fn: ident) => { $table.set(stringify!($fn), $lua.create_function($fn)?)?; }; } macro_rules! add_fns { ($lua: expr, [$($fn: ident),*]) => { let tortuise_module = $lua.create_table()?; $(add_fn!($lua, tortuise_module, $fn);)* let globals = $lua.globals(); globals.set("tortuise", tortuise_module)?; $lua.set_globals(globals)?; }; } let interpreter = Lua::new_with( StdLib::TABLE | StdLib::STRING | StdLib::MATH, LuaOptions::new().catch_rust_panics(true), )?; add_fns!( interpreter, [ add_cell, window_size, is_key_pressed_this_frame, log, warn, error, exit, is_scene_loaded, preload_scene, switch_scene, is_script_loaded, preload_script, unload_script, activate_script, deactivate_script, state, set_state, entity, set_entity, storage, set_storage, clock, time, delta_time ] ); interpreter.load(data).exec()?; Ok(Self { interpreter }) } pub fn run_function(&self, func: &str, ctx: FullScriptContext) -> mlua::Result<()> { unsafe { let ctx = std::ptr::from_mut(Box::leak(Box::new(ctx))); self.interpreter.set_app_data(ctx.expose_provenance()); self.interpreter .load(format!("if {func} then {func}() end")) .exec()?; drop(Box::from_raw(ctx)); Ok(()) } } } #[allow(clippy::mut_from_ref)] unsafe fn context(lua: &Lua) -> &mut FullScriptContext<'_> { unsafe { std::ptr::with_exposed_provenance_mut::(*lua.app_data_ref().unwrap()) .as_mut_unchecked() } } fn add_cell(lua: &Lua, cell: Table) -> mlua::Result<()> { fn parse_color(mut color: String) -> Option { color.make_ascii_uppercase(); color.retain(|c| c != '-' && c != ' ' && c != '_'); match &*color { "RESET" => return Some(Color::Reset), "BLACK" => return Some(Color::Black), "DARKGREY" => return Some(Color::DarkGrey), "DARKGRAY" => return Some(Color::DarkGrey), "RED" => return Some(Color::Red), "DARKRED" => return Some(Color::DarkRed), "GREEN" => return Some(Color::Green), "DARKGREEN" => return Some(Color::DarkGreen), "YELLOW" => return Some(Color::Yellow), "DARKYELLOW" => return Some(Color::DarkYellow), "BLUE" => return Some(Color::Blue), "DARKBLUE" => return Some(Color::DarkBlue), "MAGENTA" => return Some(Color::Magenta), "DARKMAGENTA" => return Some(Color::DarkMagenta), "CYAN" => return Some(Color::Cyan), "DARKCYAN" => return Some(Color::DarkCyan), "WHITE" => return Some(Color::White), "GREY" => return Some(Color::Grey), "GRAY" => return Some(Color::Grey), _ => (), }; if color.starts_with('#') { let color = HexColor::parse(&color).ok()?; return Some(Color::Rgb { r: color.r, g: color.g, b: color.b, }); } None } fn parse_attributes(value: Vec) -> Attributes { let mut attributes = Attributes::none(); for mut value in value { value.make_ascii_uppercase(); value.retain(|c| c != '-' && c != ' ' && c != '_'); match &*value { "BOLD" => attributes.set(Attribute::Bold), "DIM" => attributes.set(Attribute::Dim), "ITALIC" => attributes.set(Attribute::Italic), "UNDERLINED" => attributes.set(Attribute::Underlined), "DOUBLEUNDERLINED" => attributes.set(Attribute::DoubleUnderlined), "UNDERCURLED" => attributes.set(Attribute::Undercurled), "UNDERDOTTED" => attributes.set(Attribute::Underdotted), "SLOWBLINK" => attributes.set(Attribute::SlowBlink), "RAPIDBLINK" => attributes.set(Attribute::RapidBlink), "REVERSE" => attributes.set(Attribute::Reverse), "HIDDEN" => attributes.set(Attribute::Hidden), "CROSSEDOUT" => attributes.set(Attribute::CrossedOut), "FRAKTUR" => attributes.set(Attribute::Fraktur), "FRAMED" => attributes.set(Attribute::Framed), "ENCIRCLED" => attributes.set(Attribute::Encircled), "OVERLINED" => attributes.set(Attribute::OverLined), _ => (), } } attributes } puffin::profile_function!(); unsafe { context(lua) } .operation_queue .push(Operation::AddCell(Cell { character: cell.get("character")?, style: ContentStyle { foreground_color: parse_color(cell.get("foreground_color").unwrap_or_default()), background_color: parse_color(cell.get("background_color").unwrap_or_default()), underline_color: parse_color(cell.get("underline_color").unwrap_or_default()), attributes: parse_attributes(cell.get("attributes").unwrap_or_default()), }, x: cell.get("x")?, y: cell.get("y")?, z: cell.get("z")?, })); Ok(()) } fn window_size(lua: &Lua, _: ()) -> mlua::Result { puffin::profile_function!(); let window_size = &unsafe { context(lua) }.buffer.window_size; let value = lua.create_table()?; value.set("rows", window_size.rows)?; value.set("columns", window_size.columns)?; value.set("width", window_size.width)?; value.set("height", window_size.height)?; Ok(value) } fn is_key_pressed_this_frame(lua: &Lua, mut key: String) -> mlua::Result { puffin::profile_function!(); key.make_ascii_uppercase(); key.retain(|c| c != '-' && c != '_' && c != ' '); let key = match &*key { "BACKSPACE" => KeyCode::Backspace, "ENTER" => KeyCode::Enter, "LEFT" => KeyCode::Left, "RIGHT" => KeyCode::Right, "UP" => KeyCode::Up, "DOWN" => KeyCode::Down, "HOME" => KeyCode::Home, "END" => KeyCode::End, "PAGEUP" => KeyCode::PageUp, "PAGEDOWN" => KeyCode::PageDown, "TAB" => KeyCode::Tab, "BACKTAB" => KeyCode::BackTab, "DELETE" => KeyCode::Delete, "INSERT" => KeyCode::Insert, "ESC" => KeyCode::Esc, "ESCAPE" => KeyCode::Esc, f if f.starts_with('F') => KeyCode::F(f[1..].parse::().unwrap()), c if c.len() == 1 => KeyCode::Char(c.chars().next().unwrap()), _ => return Err(mlua::Error::runtime(format!("Invalid key code: {key}"))), }; Ok(unsafe { context(lua).input.is_key_pressed_this_frame(key) }) } fn log(_: &Lua, value: Value) -> mlua::Result<()> { puffin::profile_function!(); log::info!("{}", value.to_string()?); Ok(()) } fn warn(_: &Lua, value: Value) -> mlua::Result<()> { puffin::profile_function!(); log::warn!("{}", value.to_string()?); Ok(()) } fn error(_: &Lua, value: Value) -> mlua::Result<()> { puffin::profile_function!(); log::error!("{}", value.to_string()?); Ok(()) } fn exit(lua: &Lua, _: ()) -> mlua::Result<()> { puffin::profile_function!(); unsafe { context(lua) } .operation_queue .push(Operation::Exit); Ok(()) } fn is_scene_loaded(lua: &Lua, scene: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let scene = scene_id_by_name(&context.manifest.scenes, &scene) .ok_or(mlua::Error::runtime("No scene with the given name found"))?; crate::scene::is_scene_loaded( &context.manifest.scenes, context.scripts, &context.files, scene, ); Ok(()) } fn preload_scene(lua: &Lua, scene: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let scene = scene_id_by_name(&context.manifest.scenes, &scene) .ok_or(mlua::Error::runtime("No scene with the given name found"))?; context.operation_queue.push(Operation::PreloadScene(scene)); Ok(()) } fn switch_scene(lua: &Lua, scene: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let scene = scene_id_by_name(&context.manifest.scenes, &scene) .ok_or(mlua::Error::runtime("No scene with the given name found"))?; context.operation_queue.push(Operation::SwitchScene(scene)); Ok(()) } fn is_script_loaded(lua: &Lua, script: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let script = context .scripts .script_id_by_name(&script) .ok_or(mlua::Error::runtime("No script with the given name found"))?; context.scripts.is_script_loaded(script, &context.files); Ok(()) } fn preload_script(lua: &Lua, script: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let script = context .scripts .script_id_by_name(&script) .ok_or(mlua::Error::runtime("No script with the given name found"))?; context .operation_queue .push(Operation::PreloadScript(script)); Ok(()) } fn unload_script(lua: &Lua, script: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let script = context .scripts .script_id_by_name(&script) .ok_or(mlua::Error::runtime("No script with the given name found"))?; context .operation_queue .push(Operation::UnloadScript(script)); Ok(()) } fn activate_script(lua: &Lua, script: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let script = context .scripts .script_id_by_name(&script) .ok_or(mlua::Error::runtime("No script with the given name found"))?; context .operation_queue .push(Operation::ActivateScript(script)); Ok(()) } fn deactivate_script(lua: &Lua, script: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; let script = context .scripts .script_id_by_name(&script) .ok_or(mlua::Error::runtime("No script with the given name found"))?; context .operation_queue .push(Operation::DeactivateScript(script)); Ok(()) } fn state(lua: &Lua, default: MyLuaValue) -> mlua::Result { puffin::profile_function!(); let context = unsafe { context(lua) }; Ok(context .scripts .script_states .get(&context.script_id) .map(|state| postcard::from_bytes(state).unwrap()) .unwrap_or(default)) } fn set_state(lua: &Lua, new_state: MyLuaValue) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; context.operation_queue.push(Operation::UpdateState { script_id: context.script_id, new_state: postcard::to_stdvec(&new_state).unwrap().into_boxed_slice(), }); Ok(()) } fn entity(lua: &Lua, (name, default): (String, MyLuaValue)) -> mlua::Result { puffin::profile_function!(); let context = unsafe { context(lua) }; Ok(context .scripts .entities .get(&*name) .map(|state| postcard::from_bytes(state).unwrap()) .unwrap_or(default)) } fn set_entity(lua: &Lua, (name, new_state): (String, MyLuaValue)) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; context.operation_queue.push(Operation::UpdateEntity { name: name.into_boxed_str(), new_state: postcard::to_stdvec(&new_state).unwrap().into_boxed_slice(), }); Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize)] enum MyLuaValue { Nil, Boolean(bool), Number(f64), String(String), Table(Vec<(MyLuaValue, MyLuaValue)>), } impl FromLua for MyLuaValue { fn from_lua(value: Value, _: &Lua) -> mlua::Result { Ok(match value { Value::Nil => MyLuaValue::Nil, Value::Boolean(b) => MyLuaValue::Boolean(b), Value::Number(n) => MyLuaValue::Number(n), Value::Integer(n) => MyLuaValue::Number(n as f64), Value::String(s) => MyLuaValue::String(s.to_string_lossy()), Value::Table(t) => { let mut pairs = Vec::new(); t.for_each(|k, v| { pairs.push((k, v)); Ok(()) })?; MyLuaValue::Table(pairs) } _ => return Err(mlua::Error::runtime("Invalid storage type")), }) } } impl IntoLua for MyLuaValue { fn into_lua(self, lua: &Lua) -> mlua::Result { Ok(match self { MyLuaValue::Nil => Value::Nil, MyLuaValue::Boolean(b) => Value::Boolean(b), MyLuaValue::Number(n) => Value::Number(n), MyLuaValue::String(s) => Value::String(lua.create_string(s)?), MyLuaValue::Table(items) => { let table = lua.create_table()?; for (k, v) in items { table.set(k.into_lua(lua)?, v.into_lua(lua)?)?; } Value::Table(table) } }) } } fn storage(lua: &Lua, default: MyLuaValue) -> mlua::Result { puffin::profile_function!(); let context = unsafe { context(lua) }; Ok(context .storage .get() .map(|state| postcard::from_bytes(state).unwrap()) .unwrap_or(default)) } fn set_storage(lua: &Lua, new_state: MyLuaValue) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; context.operation_queue.push(Operation::UpdateStorage { new_save: postcard::to_stdvec(&new_state).unwrap().into_boxed_slice(), }); Ok(()) } fn clock(lua: &Lua, _: ()) -> mlua::Result { puffin::profile_function!(); let context = unsafe { context(lua) }; Ok(context.time.monotonic_now().as_secs_f64()) } fn time(lua: &Lua, input: Option
) -> mlua::Result { puffin::profile_function!(); let context = unsafe { context(lua) }; if let Some(input) = input { let date = chrono::NaiveDate::from_ymd_opt( input.get("year")?, input.get("month")?, input.get("day")?, ) .unwrap(); let time = chrono::NaiveTime::from_hms_opt( input.get("hour").unwrap_or_default(), input.get("min").unwrap_or_default(), input.get("sec").unwrap_or_default(), ) .unwrap(); let datetime = chrono::NaiveDateTime::new(date, time) .and_local_timezone(chrono::Local) .unwrap(); Ok(datetime .signed_duration_since(chrono::DateTime::UNIX_EPOCH) .as_seconds_f64()) } else { Ok(context .time .system_now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs_f64()) } } fn delta_time(lua: &Lua, _: ()) -> mlua::Result { puffin::profile_function!(); let context = unsafe { context(lua) }; Ok(context.time.delta_time().as_secs_f64()) }