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())
}
|