summaryrefslogtreecommitdiff
path: root/src/script/lua.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/script/lua.rs')
-rw-r--r--src/script/lua.rs495
1 files changed, 495 insertions, 0 deletions
diff --git a/src/script/lua.rs b/src/script/lua.rs
new file mode 100644
index 0000000..a5c7f6a
--- /dev/null
+++ b/src/script/lua.rs
@@ -0,0 +1,495 @@
+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<Self> {
+ 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::<FullScriptContext>(*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> {
+ 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<String>) -> 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<Table> {
+ 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<bool> {
+ 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::<u8>().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<MyLuaValue> {
+ 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<MyLuaValue> {
+ 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<Self> {
+ 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<Value> {
+ 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<MyLuaValue> {
+ 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<f64> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context.time.monotonic_now().as_secs_f64())
+}
+
+fn time(lua: &Lua, input: Option<Table>) -> mlua::Result<f64> {
+ 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<f64> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context.time.delta_time().as_secs_f64())
+}