diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/file.rs | 67 | ||||
| -rw-r--r-- | src/main.rs | 199 | ||||
| -rw-r--r-- | src/scene.rs | 6 | ||||
| -rw-r--r-- | src/script.rs | 249 | ||||
| -rw-r--r-- | src/script/lua.rs | 495 | ||||
| -rw-r--r-- | src/script/wasm.rs | 361 | ||||
| -rw-r--r-- | src/storage.rs | 41 |
7 files changed, 1124 insertions, 294 deletions
diff --git a/src/file.rs b/src/file.rs index 9b9888c..4aedd48 100644 --- a/src/file.rs +++ b/src/file.rs @@ -6,7 +6,7 @@ use smol::{Task, unblock}; use crate::{ scene::{ResourceRequirements, Scene, scene_id_by_name}, - script::ScriptDeclaration, + script::{ScriptDeclaration, WasmRunner, lua::LuaModule, wasm::WasmModule}, }; type FileTask = Task<std::io::Result<FileData>>; @@ -25,7 +25,7 @@ pub struct GameManifest { } #[derive(Debug, Serialize, Deserialize)] -struct TomlGameManifest { +pub struct TomlGameManifest { name: Box<str>, first_scene: Box<str>, #[serde(default)] @@ -49,14 +49,16 @@ struct TomlScene { #[serde(tag = "type", rename_all = "kebab-case")] enum TomlScript { Wasm { name: Box<str>, path: Box<Path> }, + Lua { name: Box<str>, path: Box<Path> }, } pub enum FileData { GameManifest(GameManifest), - WasmModule(wasmi::Module), + WasmModule(Box<WasmModule>), + LuaModule(Box<LuaModule>), } -fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> { +pub fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> { let mut scripts = Vec::with_capacity(manifest.script.len()); let script_map: FxHashMap<Box<str>, usize> = manifest .script @@ -69,6 +71,13 @@ fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameMa }); (name, scripts.len() - 1) } + TomlScript::Lua { name, path } => { + scripts.push(ScriptDeclaration::Lua { + name: name.clone(), + path, + }); + (name, scripts.len() - 1) + } }) .collect(); let scenes: Box<[Scene]> = manifest @@ -102,8 +111,8 @@ fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameMa }) } -fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result<FileData> { - if path == "manifest.toml" { +fn compile_data(path: &Path, data: &[u8], compiler: &WasmRunner) -> std::io::Result<FileData> { + if path == "tortuise.toml" { return Ok(FileData::GameManifest(normalize_game_manifest( toml::from_slice(data).map_err(std::io::Error::other)?, )?)); @@ -113,9 +122,14 @@ fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io:: .and_then(OsStr::to_str) .map(|ext| ext.to_lowercase()); if extension.as_deref() == Some("wasm") { - return Ok(FileData::WasmModule( - wasmi::Module::new(compiler, data).map_err(std::io::Error::other)?, - )); + return Ok(FileData::WasmModule(Box::new( + compiler.compile(data).map_err(std::io::Error::other)?, + ))); + } + if extension.as_deref() == Some("lua") { + return Ok(FileData::LuaModule(Box::new( + LuaModule::new(data).map_err(std::io::Error::other)?, + ))); } Err(std::io::Error::other("Unrecognized file extension")) } @@ -129,7 +143,23 @@ impl FileData { } } - pub fn unwrap_wasm_module(&self) -> &wasmi::Module { + pub fn unwrap_wasm_module(&self) -> &WasmModule { + if let FileData::WasmModule(module) = self { + module + } else { + panic!("Not a WASM module") + } + } + + pub fn unwrap_lua_module(&self) -> &LuaModule { + if let FileData::LuaModule(module) = self { + module + } else { + panic!("Not a Lua module") + } + } + + pub fn unwrap_wasm_module_mut(&mut self) -> &mut WasmModule { if let FileData::WasmModule(module) = self { module } else { @@ -149,7 +179,7 @@ impl FileManager { pub fn load_file( &mut self, path: Box<Path>, - wasm_compiler: &wasmi::Engine, + wasm_compiler: &WasmRunner, ) -> std::io::Result<&FileData> { if !self.cache.contains_key(&path) { let data = compile_data( @@ -166,7 +196,11 @@ impl FileManager { self.cache.get(path) } - pub fn start_loading_file(&mut self, path: impl AsRef<Path>, wasm_compiler: &wasmi::Engine) { + pub fn get_cached_file_mut(&mut self, path: &Path) -> Option<&mut FileData> { + self.cache.get_mut(path) + } + + pub fn start_loading_file(&mut self, path: impl AsRef<Path>, wasm_compiler: &WasmRunner) { let boxed_path = Box::from(path.as_ref()); let compiler = wasm_compiler.clone(); let task = unblock(move || { @@ -180,6 +214,15 @@ impl FileManager { self.active_tasks.insert(boxed_path, task); } + pub fn loaded_files(&self) -> Box<[Box<Path>]> { + self.active_tasks + .iter() + .filter(|&(_, v)| v.is_finished()) + .map(|(k, _)| k.clone()) + .chain(self.cache.keys().cloned()) + .collect() + } + pub fn is_file_loaded(&self, path: impl AsRef<Path>) -> bool { self.active_tasks .get(path.as_ref()) diff --git a/src/main.rs b/src/main.rs index 66e4cf4..75068fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use std::io::BufWriter; +use bpaf::Bpaf; use mimalloc::MiMalloc; +use prologger::{ColorMode, ProLogger, RotationConfig}; use tortuise::{ buffer::{Buffer, Event}, - file::FileManager, + file::{FileManager, TomlGameManifest, normalize_game_manifest}, input::InputManager, scene::switch_scenes, script::{ScriptContext, ScriptManager, WasmRunner}, @@ -11,70 +13,143 @@ use tortuise::{ time::TimeContext, }; -#[global_allocator] -static GLOBAL: MiMalloc = MiMalloc; +// #[global_allocator] +// static GLOBAL: MiMalloc = MiMalloc; + +#[derive(Debug, Clone, Bpaf)] +#[bpaf(options)] +enum Options { + #[bpaf(command)] + Run, + #[bpaf(command)] + Compile, +} fn main() { - let mut files = FileManager::default(); - let wasm_runner = WasmRunner::new(); - let manifest = files - .load_file(Box::from("manifest.toml".as_ref()), &wasm_runner.engine) - .unwrap() - .unwrap_manifest() - .clone(); + let options = options().run(); + + match options { + Options::Compile => { + let manifest = std::fs::read("tortuise.toml").unwrap(); + let manifest = toml::from_slice::<TomlGameManifest>(&manifest).unwrap(); + let manifest = normalize_game_manifest(manifest).unwrap(); + + let manifest_decl = format!( + r#"let manifest = GameManifest {{ + name: "{}".to_string(), + first_scene: {}, + scenes: Box::from([{}]), + scripts: Box::from([]), + }}"#, + manifest.name, + manifest.first_scene, + manifest + .scenes + .iter() + .map(|scene| format!( + r#"Scene {{ + name: \"{}\".to_string(), + preload_scenes: Box::from([{}]), + scripts: ResourceRequirements {{ + required: Box::from([{}]), + preload: Box::from([{}]), + }} + }}"#, + scene.name, + scene + .preload_scenes + .iter() + .map(|i| i.to_string()) + .collect::<Vec<_>>() + .join(","), + scene + .scripts + .required + .iter() + .map(|i| i.to_string()) + .collect::<Vec<_>>() + .join(","), + scene + .scripts + .preload + .iter() + .map(|i| i.to_string()) + .collect::<Vec<_>>() + .join(","), + )) + .collect::<Vec<_>>() + .join(",") + ); + } + Options::Run => { + ProLogger::builder() + .with_rotating_file(".log", RotationConfig::new(100_000, 2)) + .with_color(ColorMode::Never) + .init() + .unwrap(); + + let mut files = FileManager::default(); + let wasm_runner = WasmRunner::new(); + let manifest = files + .load_file(Box::from("tortuise.toml".as_ref()), &wasm_runner) + .unwrap() + .unwrap_manifest() + .clone(); - let stdout = Box::new(BufWriter::new(std::io::stdout().lock())); - let mut buffer = Buffer::new(stdout).unwrap(); - let mut input = InputManager::new(); - let mut time = TimeContext::new(); - let mut storage = StorageManager::load(etcetera::AppStrategyArgs { - top_level_domain: "com".into(), - author: "botahamec".into(), - app_name: manifest.name.to_string(), - }) - .unwrap(); - let mut scripts = ScriptManager::new(&manifest.scripts); - let mut context = ScriptContext { - manifest: &manifest, - buffer: &mut buffer, - storage: &mut storage, - input: &input, - time: &time, - operation_queue: Vec::new(), - }; + let stdout = Box::new(BufWriter::new(std::io::stdout().lock())); + let mut buffer = Buffer::new(stdout).unwrap(); + let mut input = InputManager::new(); + let mut time = TimeContext::new(); + let mut storage = StorageManager::load(etcetera::AppStrategyArgs { + top_level_domain: "com".into(), + author: "botahamec".into(), + app_name: manifest.name.to_string(), + }) + .unwrap(); + let mut scripts = ScriptManager::new(&manifest.scripts); + let mut context = ScriptContext { + manifest: &manifest, + buffer: &mut buffer, + storage: &mut storage, + input: &input, + time: &time, + operation_queue: Vec::new(), + }; - switch_scenes( - &mut scripts, - &mut context, - &mut files, - &wasm_runner, - manifest.first_scene, - ); + switch_scenes( + &mut scripts, + &mut context, + &mut files, + &wasm_runner, + manifest.first_scene, + ); - let server_addr = format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT); - let _puffin_server = puffin_http::Server::new(&server_addr).unwrap(); - puffin::set_scopes_on(true); - buffer - .render_loop(|buffer, event| match event { - Event::Key(event) => { - if event.is_press() { - input.press_key(event.code); - } - } - Event::Render => { - buffer.cells.clear(); - let mut context = ScriptContext { - manifest: &manifest, - buffer, - storage: &mut storage, - input: &input, - time: &time, - operation_queue: Vec::new(), - }; - scripts.run_scripts(&mut context, &wasm_runner, &mut files); - input.reset_next_frame(); - time.next_frame(); - } - }) - .unwrap(); + let server_addr = format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT); + let _puffin_server = puffin_http::Server::new(&server_addr).unwrap(); + puffin::set_scopes_on(true); + buffer + .render_loop(|buffer, event| match event { + Event::Key(event) => { + if event.is_press() { + input.press_key(event.code); + } + } + Event::Render => { + buffer.cells.clear(); + let mut context = ScriptContext { + manifest: &manifest, + buffer, + storage: &mut storage, + input: &input, + time: &time, + operation_queue: Vec::new(), + }; + scripts.run_scripts(&mut context, &wasm_runner, &mut files); + input.reset_next_frame(); + time.next_frame(); + } + }) + .unwrap(); + } + } } diff --git a/src/scene.rs b/src/scene.rs index 89a0d99..807628d 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use rustc_hash::FxHashSet; use serde::{Deserialize, Serialize}; @@ -45,7 +47,7 @@ pub fn preload_scene( pub fn is_scene_loaded( scenes: &[Scene], scripts: &ScriptManager, - files: &FileManager, + loaded_files: &[Box<Path>], id: usize, ) -> bool { let scene = &scenes[id]; @@ -54,7 +56,7 @@ pub fn is_scene_loaded( .required .iter() .cloned() - .all(|script_id| scripts.is_script_loaded(script_id, files)) + .all(|script_id| scripts.is_script_loaded(script_id, loaded_files)) } pub fn switch_scenes( diff --git a/src/script.rs b/src/script.rs index 8539c92..a2cb76d 100644 --- a/src/script.rs +++ b/src/script.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{path::Path, sync::Arc}; use rustc_hash::FxHashMap; @@ -7,11 +7,13 @@ use crate::{ file::{FileManager, GameManifest}, input::InputManager, scene::{preload_scene, switch_scenes}, + script::wasm::WasmModule, storage::StorageManager, time::TimeContext, }; -mod wasm; +pub mod lua; +pub mod wasm; pub struct ScriptManager<'a> { scripts: &'a [ScriptDeclaration], @@ -20,9 +22,11 @@ pub struct ScriptManager<'a> { script_states: FxHashMap<usize, Box<[u8]>>, } +#[derive(Clone)] pub struct WasmRunner { - pub engine: wasmi::Engine, - pub linker: wasmi::Linker<FullScriptContext<'static>>, + pub engine: tinywasm::Engine, + pub store: Arc<spin::Mutex<tinywasm::Store>>, + pub linker: tinywasm::Imports, } pub struct ScriptContext<'a> { @@ -41,7 +45,7 @@ pub struct FullScriptContext<'a> { pub input: &'a InputManager, pub time: &'a TimeContext, pub scripts: &'a ScriptManager<'a>, - pub files: &'a FileManager, + pub files: Box<[Box<Path>]>, pub storage: &'a StorageManager, pub wasm_runner: &'a WasmRunner, pub operation_queue: &'a mut Vec<Operation>, @@ -79,6 +83,10 @@ pub enum ScriptDeclaration { update: StaticScriptFunction, end: StaticScriptFunction, }, + Lua { + name: Box<str>, + path: Box<Path>, + }, Wasm { name: Box<str>, path: Box<Path>, @@ -104,66 +112,62 @@ impl Default for WasmRunner { impl WasmRunner { pub fn new() -> Self { - let engine = wasmi::Engine::default(); - let linker = unsafe { - std::mem::transmute::< - wasmi::Linker<FullScriptContext<'_>>, - wasmi::Linker<FullScriptContext<'_>>, - >(wasm::linker(&engine).unwrap()) - }; - Self { engine, linker } + let engine = tinywasm::Engine::default(); + let mut store = tinywasm::Store::new(engine.clone()); + let linker = wasm::linker(&mut store).unwrap(); + let store = Arc::new(spin::Mutex::new(store)); + Self { + engine, + store, + linker, + } } - pub fn compile(&self, data: &[u8]) -> Result<wasmi::Module, wasmi::Error> { - wasmi::Module::new(&self.engine, data) + pub fn compile(&self, data: &[u8]) -> Result<WasmModule, tinywasm::Error> { + let module = tinywasm::parse_bytes(data)?; + let instance = tinywasm::ModuleInstance::instantiate( + &mut self.store.lock(), + &module, + Some(self.linker.clone()), + )?; + Ok(WasmModule { instance }) } - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub fn run_function( &self, script_id: usize, - module: &wasmi::Module, + module: &mut WasmModule, scripts: &ScriptManager, - files: &FileManager, + files: Box<[Box<Path>]>, wasm_runner: &WasmRunner, context: &mut ScriptContext<'_>, function_name: &str, - ) -> Result<(), wasmi::Error> { - // safety: the store won't be kept around after this function ends. Only the - // lifetimes are being transmuted - let mut store = { - puffin::profile_scope!("create store"); - wasmi::Store::new(&self.engine, unsafe { - std::mem::transmute::<FullScriptContext<'_>, FullScriptContext<'_>>( - FullScriptContext { - script_id, - manifest: context.manifest, - buffer: context.buffer, - input: context.input, - time: context.time, - storage: context.storage, - scripts, - files, - wasm_runner, - operation_queue: &mut context.operation_queue, - }, - ) - }) - }; - let instance = { - puffin::profile_scope!("instantiate"); - self.linker.instantiate_and_start(&mut store, module)? - }; - let function = { - puffin::profile_scope!("get function"); - instance.get_func(&store, function_name).ok_or_else(|| { - wasmi::Error::new(format!("No function named {function_name} was exported")) - })? - }; - { - puffin::profile_scope!("call function"); - function.call(&mut store, &[], &mut []) - } + ) -> Result<(), tinywasm::Error> { + puffin::profile_function!(); + + let context = std::ptr::from_mut(Box::leak(Box::new(FullScriptContext { + script_id, + manifest: context.manifest, + buffer: context.buffer, + input: context.input, + time: context.time, + storage: context.storage, + scripts, + files, + wasm_runner, + operation_queue: &mut context.operation_queue, + }))); + + let function = module + .instance + .func::<u64, ()>(&wasm_runner.store.lock(), function_name)?; + let r = function.call( + &mut wasm_runner.store.lock(), + context.expose_provenance() as u64, + ); + drop(unsafe { Box::from_raw(context) }); + r } } @@ -185,6 +189,11 @@ impl<'a> ScriptManager<'a> { return Some(i); } } + ScriptDeclaration::Lua { name, .. } => { + if &**name == needle { + return Some(i); + } + } ScriptDeclaration::Wasm { name, .. } => { if &**name == needle { return Some(i); @@ -205,17 +214,17 @@ impl<'a> ScriptManager<'a> { let script = &self.scripts[script_id]; match script { ScriptDeclaration::Static { .. } => (), - ScriptDeclaration::Wasm { path, .. } => { - files.start_loading_file(path, &wasm_runner.engine) - } + ScriptDeclaration::Lua { path, .. } => files.start_loading_file(path, wasm_runner), + ScriptDeclaration::Wasm { path, .. } => files.start_loading_file(path, wasm_runner), } } - pub fn is_script_loaded(&self, script_id: usize, files: &FileManager) -> bool { + pub fn is_script_loaded(&self, script_id: usize, loaded_files: &[Box<Path>]) -> bool { let script = &self.scripts[script_id]; match script { ScriptDeclaration::Static { .. } => true, - ScriptDeclaration::Wasm { path, .. } => files.is_file_loaded(path), + ScriptDeclaration::Lua { path, .. } => loaded_files.contains(path), + ScriptDeclaration::Wasm { path, .. } => loaded_files.contains(path), } } @@ -223,6 +232,7 @@ impl<'a> ScriptManager<'a> { let script = &self.scripts[script_id]; match script { ScriptDeclaration::Static { .. } => (), + ScriptDeclaration::Lua { path, .. } => files.unload_file(path), ScriptDeclaration::Wasm { path, .. } => files.unload_file(path), } } @@ -284,7 +294,9 @@ impl<'a> ScriptManager<'a> { Operation::UpdateState { script_id, new_state, - } => *get_script_state(&mut self.script_states, *script_id) = new_state.clone(), + } => { + self.script_states.insert(*script_id, new_state.clone()); + } Operation::UpdateEntity { name, new_state } => { self.entities.insert(name.clone(), new_state.clone()); } @@ -316,18 +328,46 @@ impl<'a> ScriptManager<'a> { ) }); } + ScriptDeclaration::Lua { path, .. } => { + files.load_file(path.clone(), wasm_runner).unwrap(); + let loaded_files = files.loaded_files(); + let module = files.get_cached_file_mut(path).unwrap().unwrap_lua_module(); + if let Err(e) = module.run_function( + "start", + FullScriptContext { + script_id, + manifest: context.manifest, + buffer: context.buffer, + input: context.input, + time: context.time, + scripts: self, + files: loaded_files, + storage: context.storage, + wasm_runner, + operation_queue: &mut context.operation_queue, + }, + ) { + log::error!("{e}"); + } + } ScriptDeclaration::Wasm { path, .. } => { - files.load_file(path.clone(), &wasm_runner.engine).unwrap(); - let module = files.get_cached_file(path).unwrap().unwrap_wasm_module(); - wasm_runner.run_function( + files.load_file(path.clone(), wasm_runner).unwrap(); + let loaded_files = files.loaded_files(); + let module = files + .get_cached_file_mut(path) + .unwrap() + .unwrap_wasm_module_mut(); + if let Err(e) = wasm_runner.run_function( script_id, module, self, - files, + loaded_files, wasm_runner, context, "start", - ); + ) { + log::error!("{e}"); + } } } self.active_scripts.push(script_id); @@ -360,18 +400,46 @@ impl<'a> ScriptManager<'a> { ); }); } + ScriptDeclaration::Lua { path, .. } => { + files.load_file(path.clone(), wasm_runner).unwrap(); + let loaded_files = files.loaded_files(); + let module = files.get_cached_file_mut(path).unwrap().unwrap_lua_module(); + if let Err(e) = module.run_function( + "end", + FullScriptContext { + script_id, + manifest: context.manifest, + buffer: context.buffer, + input: context.input, + time: context.time, + scripts: self, + files: loaded_files, + storage: context.storage, + wasm_runner, + operation_queue: &mut context.operation_queue, + }, + ) { + log::error!("{e}"); + } + } ScriptDeclaration::Wasm { path, .. } => { - files.load_file(path.clone(), &wasm_runner.engine).unwrap(); - let module = files.get_cached_file(path).unwrap().unwrap_wasm_module(); - wasm_runner.run_function( + files.load_file(path.clone(), wasm_runner).unwrap(); + let loaded_files = files.loaded_files(); + let module = files + .get_cached_file_mut(path) + .unwrap() + .unwrap_wasm_module_mut(); + if let Err(e) = wasm_runner.run_function( script_id, module, self, - files, + loaded_files, wasm_runner, context, "end", - ); + ) { + log::error!("{e}"); + } } }; } @@ -399,21 +467,46 @@ impl<'a> ScriptManager<'a> { ) } } + ScriptDeclaration::Lua { path, .. } => { + files.load_file(path.clone(), wasm_runner).unwrap(); + let loaded_files = files.loaded_files(); + let module = files.get_cached_file_mut(path).unwrap().unwrap_lua_module(); + if let Err(e) = module.run_function( + "update", + FullScriptContext { + script_id: *script_id, + manifest: context.manifest, + buffer: context.buffer, + input: context.input, + time: context.time, + scripts: self, + files: loaded_files, + storage: context.storage, + wasm_runner, + operation_queue: &mut context.operation_queue, + }, + ) { + log::error!("{e}"); + } + } ScriptDeclaration::Wasm { path, .. } => { - let module = { - puffin::profile_scope!("load module"); - files.load_file(path.clone(), &wasm_runner.engine).unwrap(); - files.get_cached_file(path).unwrap().unwrap_wasm_module() - }; - wasm_runner.run_function( + files.load_file(path.clone(), wasm_runner).unwrap(); + let loaded_files = files.loaded_files(); + let module = files + .get_cached_file_mut(path) + .unwrap() + .unwrap_wasm_module_mut(); + if let Err(e) = wasm_runner.run_function( *script_id, module, self, - files, + loaded_files, wasm_runner, context, "update", - ); + ) { + log::error!("Error while running scripts: {e}"); + } } } } 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()) +} diff --git a/src/script/wasm.rs b/src/script/wasm.rs index 25632fa..9b9cd91 100644 --- a/src/script/wasm.rs +++ b/src/script/wasm.rs @@ -1,41 +1,54 @@ -use core::slice; use std::time::UNIX_EPOCH; use crossterm::{ event::KeyCode, style::{Attribute, Attributes, Color, ContentStyle}, }; -use wasmi::{Caller, Engine, Linker}; +use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; use crate::{buffer::Cell, script::FullScriptContext}; use super::Operation; -unsafe fn str_from_memory<'a>( - caller: &'a Caller<FullScriptContext<'_>>, - offset: u32, - length: u32, -) -> &'a str { - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); +pub struct WasmModule { + pub instance: ModuleInstance, +} + +fn str_from_memory(caller: &FuncContext<'_>, offset: u32, length: u32) -> tinywasm::Result<String> { + let memory = caller.memory("memory").unwrap(); + memory.read_string(caller.store(), offset as usize, length as usize) +} + +fn context_mut<'b>(address: u64) -> &'b mut FullScriptContext<'b> { unsafe { - str::from_utf8_unchecked(slice::from_raw_parts( - memory.data_ptr(caller).byte_add(offset as usize), - length as usize, - )) + std::ptr::with_exposed_provenance_mut::<FullScriptContext<'b>>(address as usize) + .as_mut() + .unwrap_unchecked() + } +} + +fn context_ref<'b>(address: u64) -> &'b FullScriptContext<'b> { + unsafe { + std::ptr::with_exposed_provenance::<FullScriptContext<'b>>(address as usize) + .as_ref() + .unwrap_unchecked() } } -#[allow(clippy::too_many_arguments)] fn add_cell( - mut caller: Caller<FullScriptContext<'_>>, - character: u32, - location: u32, - z: u32, - foreground_color: u32, - background_color: u32, - underline_color: u32, - attributes: u32, -) { + _caller: FuncContext<'_>, + ( + context, + character, + location, + z, + foreground_color, + background_color, + underline_color, + attributes, + ): (u64, u32, u32, u32, u32, u32, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); const STANDARD_COLOR: u32 = 1 << 24; const ANSI_COLOR: u32 = 2 << 24; const RGB_COLOR: u32 = 3 << 24; @@ -127,8 +140,7 @@ fn add_cell( attribute_set.set(Attribute::OverLined); } - caller - .data_mut() + context_mut(context) .operation_queue .push(Operation::AddCell(Cell { character: char::from_u32(character).unwrap_or_default(), @@ -142,14 +154,20 @@ fn add_cell( y: (location & 0xffff) as u16, z: z as u8, })); + Ok(()) } -fn window_size(caller: Caller<FullScriptContext<'_>>) -> u32 { - let window_size = &caller.data().buffer.window_size; - ((window_size.rows as u32) << 16) | (window_size.columns as u32) +fn window_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let window_size = &context_ref(context).buffer.window_size; + Ok(((window_size.rows as u32) << 16) | (window_size.columns as u32)) } -fn is_key_pressed_this_frame(caller: Caller<FullScriptContext<'_>>, key_code: u32) -> u32 { +fn is_key_pressed_this_frame( + _caller: FuncContext<'_>, + (context, key_code): (u64, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); const BACKSPACE: u32 = 1 << 24; const ENTER: u32 = 2 << 24; const LEFT: u32 = 3 << 24; @@ -189,21 +207,24 @@ fn is_key_pressed_this_frame(caller: Caller<FullScriptContext<'_>>, key_code: u3 _ => KeyCode::Null, }; - if caller.data().input.is_key_pressed_this_frame(key_code) { - 1 - } else { - 0 - } + Ok( + if context_ref(context) + .input + .is_key_pressed_this_frame(key_code) + { + 1 + } else { + 0 + }, + ) } -fn log(caller: Caller<FullScriptContext<'_>>, severity: u32, offset: u32, length: u32) { - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let message = unsafe { - str::from_utf8_unchecked(slice::from_raw_parts( - memory.data_ptr(&caller).byte_add(offset as usize), - length as usize, - )) - }; +fn log( + caller: FuncContext<'_>, + (severity, offset, length): (u32, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); + let message = str_from_memory(&caller, offset, length)?; let severity = match severity { 1 => log::Level::Error, 2 => log::Level::Warn, @@ -213,103 +234,129 @@ fn log(caller: Caller<FullScriptContext<'_>>, severity: u32, offset: u32, length _ => log::Level::Error, }; log::log!(severity, "{message}"); + Ok(()) } -fn exit(mut caller: Caller<FullScriptContext<'_>>) { - caller.data_mut().operation_queue.push(Operation::Exit) +fn exit(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context).operation_queue.push(Operation::Exit); + Ok(()) } -fn random_u64() -> u64 { - getrandom::u64().unwrap_or(4) +fn random_u64(_caller: FuncContext<'_>, (): ()) -> tinywasm::Result<u64> { + puffin::profile_function!(); + Ok(getrandom::u64().unwrap_or(4)) } -fn scene_id_by_name(caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) -> u32 { - let name = unsafe { str_from_memory(&caller, offset, length) }; - crate::scene::scene_id_by_name(&caller.data().manifest.scenes, name) - .map(|u| u as u32) - .unwrap_or(u32::MAX) +fn scene_id_by_name( + caller: FuncContext<'_>, + (context, offset, length): (u64, u32, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let name = str_from_memory(&caller, offset, length)?; + Ok( + crate::scene::scene_id_by_name(&context_ref(context).manifest.scenes, &name) + .map(|u| u as u32) + .unwrap_or(u32::MAX), + ) } -fn preload_scene(mut caller: Caller<FullScriptContext<'_>>, id: u32) { - caller - .data_mut() +fn preload_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context) .operation_queue - .push(Operation::PreloadScene(id as usize)) + .push(Operation::PreloadScene(id as usize)); + Ok(()) } -fn is_scene_loaded(caller: Caller<FullScriptContext<'_>>, id: u32) -> u32 { - let context = caller.data(); - crate::scene::is_scene_loaded( +fn is_scene_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let context = context_ref(context); + Ok(crate::scene::is_scene_loaded( &context.manifest.scenes, context.scripts, - context.files, + &context.files, id as usize, - ) as u32 + ) as u32) } -fn switch_scene(mut caller: Caller<FullScriptContext<'_>>, id: u32) { - caller - .data_mut() +fn switch_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context) .operation_queue .push(Operation::SwitchScene(id as usize)); + Ok(()) } -fn script_id_by_name(caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) -> u32 { - let context = caller.data(); - let name = unsafe { str_from_memory(&caller, offset, length) }; - context +fn script_id_by_name( + caller: FuncContext<'_>, + (context, offset, length): (u64, u32, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let name = str_from_memory(&caller, offset, length)?; + let context = context_ref(context); + Ok(context .scripts - .script_id_by_name(name) + .script_id_by_name(&name) .map(|u| u as u32) - .unwrap_or(u32::MAX) + .unwrap_or(u32::MAX)) } -fn preload_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) { - caller - .data_mut() +fn preload_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context) .operation_queue .push(Operation::PreloadScript(id as usize)); + Ok(()) } -fn is_script_loaded(caller: Caller<FullScriptContext<'_>>, id: u32) -> u32 { - let context = caller.data(); - context.scripts.is_script_loaded(id as usize, context.files) as u32 +fn is_script_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let context = context_ref(context); + Ok(context + .scripts + .is_script_loaded(id as usize, &context.files) as u32) } -fn unload_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) { - caller - .data_mut() +fn unload_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context) .operation_queue .push(Operation::UnloadScript(id as usize)); + Ok(()) } -fn activate_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) { - caller - .data_mut() +fn activate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context) .operation_queue .push(Operation::ActivateScript(id as usize)); + Ok(()) } -fn deactivate_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) { - caller - .data_mut() +fn deactivate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + context_mut(context) .operation_queue .push(Operation::DeactivateScript(id as usize)); + Ok(()) } -fn state_size(caller: Caller<FullScriptContext<'_>>) -> u32 { - let context = caller.data(); - context +fn state_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let context = context_ref(context); + Ok(context .scripts .script_states .get(&context.script_id) .map(|state| state.len()) - .unwrap_or_default() as u32 + .unwrap_or_default() as u32) } -fn load_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32) { - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let context = caller.data(); +fn load_state(mut caller: FuncContext<'_>, (context, offset): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + let memory = caller.memory("memory").unwrap(); + let context = context_ref(context); let state = context .scripts .script_states @@ -317,119 +364,153 @@ fn load_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32) { .map(|state| &**state) .unwrap_or(&[]); memory.write(&mut caller, offset as usize, state).unwrap(); + Ok(()) } -fn save_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) { +fn save_state( + mut caller: FuncContext<'_>, + (context, offset, length): (u64, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); let offset = offset as usize; let length = length as usize; - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let new_state = Box::from(&memory.data(&caller)[offset..(offset + length)]); - let context = caller.data_mut(); + let memory = caller.memory("memory").unwrap(); + let new_state = memory + .read_vec(caller.store_mut(), offset, length) + .unwrap() + .into_boxed_slice(); + let context = context_mut(context); context.operation_queue.push(Operation::UpdateState { script_id: context.script_id, new_state, }); + Ok(()) } -fn entity_size(caller: Caller<FullScriptContext<'_>>, name: u32, name_len: u32) -> u32 { - let context = caller.data(); - let name = unsafe { str_from_memory(&caller, name, name_len) }; - context +fn entity_size( + caller: FuncContext<'_>, + (context, name, name_len): (u64, u32, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let name = str_from_memory(&caller, name, name_len)?; + let context = context_ref(context); + Ok(context .scripts .entities - .get(name) + .get(&*name) .map(|entity| entity.len()) - .unwrap_or_default() as u32 + .unwrap_or_default() as u32) } -fn load_entity(mut caller: Caller<FullScriptContext<'_>>, name: u32, name_len: u32, offset: u32) { - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let name = unsafe { str_from_memory(&caller, name, name_len) }; - let context = caller.data(); +fn load_entity( + mut caller: FuncContext<'_>, + (context, name, name_len, offset): (u64, u32, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); + let name = str_from_memory(&caller, name, name_len)?; + let memory = caller.memory("memory").unwrap(); + let context = context_ref(context); let entity = context .scripts .entities - .get(name) + .get(&*name) .map(|entity| &**entity) .unwrap_or(&[]); memory.write(&mut caller, offset as usize, entity).unwrap(); + Ok(()) } fn save_entity( - mut caller: Caller<FullScriptContext<'_>>, - name: u32, - name_len: u32, - offset: u32, - length: u32, -) { + mut caller: FuncContext<'_>, + (context, name, name_len, offset, length): (u64, u32, u32, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); let offset = offset as usize; let length = length as usize; - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let new_state = Box::from(&memory.data(&caller)[offset..(offset + length)]); - let name = Box::from(unsafe { str_from_memory(&caller, name, name_len) }); - let context = caller.data_mut(); + let memory = caller.memory("memory").unwrap(); + let new_state = memory + .read_vec(caller.store_mut(), offset, length) + .unwrap() + .into_boxed_slice(); + let name = str_from_memory(&caller, name, name_len)?.into_boxed_str(); + let context = context_mut(context); context .operation_queue .push(Operation::UpdateEntity { name, new_state }); + Ok(()) } -fn storage_size(caller: Caller<FullScriptContext<'_>>) -> u32 { - caller - .data() +fn storage_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(context_ref(context) .storage .get() .map(|data| data.len()) - .unwrap_or_default() as u32 + .unwrap_or_default() as u32) } -fn load_storage(mut caller: Caller<FullScriptContext<'_>>, offset: u32) { - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let context = caller.data(); +fn load_storage( + mut caller: FuncContext<'_>, + (context, offset): (u64, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); + let memory = caller.memory("memory").unwrap(); + let context = context_ref(context); let state = context.storage.get().unwrap_or(&[]); memory.write(&mut caller, offset as usize, state).unwrap(); + Ok(()) } -fn save_storage(mut caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) { +fn save_storage( + mut caller: FuncContext<'_>, + (context, offset, length): (u64, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); let offset = offset as usize; let length = length as usize; - let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); - let new_save = Box::from(&memory.data(&caller)[offset..(offset + length)]); - let context = caller.data_mut(); + let memory = caller.memory("memory").unwrap(); + let new_save = memory + .read_vec(caller.store_mut(), offset, length) + .unwrap() + .into_boxed_slice(); + let context = context_mut(context); context .operation_queue .push(Operation::UpdateStorage { new_save }); + Ok(()) } -fn system_now(caller: Caller<FullScriptContext<'_>>) -> u64 { - caller - .data() +fn system_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> { + puffin::profile_function!(); + Ok(context_ref(context) .time .system_now() .duration_since(UNIX_EPOCH) .unwrap() - .as_nanos() as u64 + .as_nanos() as u64) } -fn monotonic_now(caller: Caller<FullScriptContext<'_>>) -> u64 { - caller.data().time.monotonic_now().as_nanos() as u64 +fn monotonic_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> { + puffin::profile_function!(); + Ok(context_ref(context).time.monotonic_now().as_nanos() as u64) } -fn delta_time(caller: Caller<FullScriptContext<'_>>) -> u64 { - caller.data().time.delta_time().as_nanos() as u64 +fn delta_time(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> { + puffin::profile_function!(); + Ok(context_ref(context).time.delta_time().as_nanos() as u64) } -pub fn linker<'ctx>( - engine: &Engine, -) -> Result<wasmi::Linker<FullScriptContext<'ctx>>, wasmi::Error> { +pub fn linker(store: &mut Store) -> Result<Imports, tinywasm::Error> { macro_rules! add_fns { - ($linker: expr, [$($func: ident),*]) => { - $($linker.func_wrap("tortuise", stringify!($func), $func)?;)* + ($linker: expr, $store: expr, [$($func: ident),*]) => { + $($linker.define("tortuise", stringify!($func), HostFunction::from($store, $func));)* }; } - let mut linker = Linker::new(engine); + let mut linker = Imports::new(); add_fns!( linker, + store, [ add_cell, window_size, diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..90abf05 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,41 @@ +use etcetera::{AppStrategy, AppStrategyArgs, app_strategy::Xdg}; +use smol::unblock; + +pub struct StorageManager { + xdg: Xdg, + data: Option<Box<[u8]>>, +} + +impl StorageManager { + pub fn load(app: AppStrategyArgs) -> std::io::Result<Self> { + let xdg = etcetera::choose_app_strategy(app).map_err(std::io::Error::other)?; + let data = std::fs::read(xdg.in_data_dir("save")) + .or_else(|_| std::fs::read(xdg.in_data_dir("save.tmp"))) + .map(|data| data.into_boxed_slice()) + .ok(); + + Ok(Self { data, xdg }) + } + + pub fn get(&self) -> Option<&[u8]> { + self.data.as_deref() + } + + pub fn background_save(&mut self, data: Box<[u8]>) { + let tmp_file = self.xdg.in_data_dir("save.tmp"); + let save_file = self.xdg.in_data_dir("save"); + + self.data = Some(data.clone()); + unblock(move || { + std::fs::write(&tmp_file, &data)?; + + let saved_data = std::fs::read(&tmp_file)?; + if *saved_data != *data { + return Err(std::io::Error::other("Failed to verify file integrity")); + } + + std::fs::rename(tmp_file, save_file)?; + Ok(()) + }); + } +} |
