summaryrefslogtreecommitdiff
path: root/src/script/lua.rs
blob: a5c7f6a71853df057189824a95b8272752447b32 (plain)
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())
}