use std::{path::Path, sync::Arc, time::Duration}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use crate::{ buffer::{Buffer, Cell}, file::FileManager, input::InputManager, manifest::{DeserializedGameManifest, GameManifest, GameManifestPatch}, scene::switch_scenes, script::wasm::WasmModule, storage::StorageManager, time::TimeContext, }; pub mod lua; pub mod wasm; pub struct ScriptManager<'a> { scripts: &'a [ScriptDeclaration], pub active_scripts: Vec, entities: FxHashMap, Box<[u8]>>, script_states: FxHashMap>, } #[derive(Clone)] pub struct WasmRunner { pub engine: tinywasm::Engine, pub store: Arc>, pub linker: tinywasm::Imports, } pub struct ScriptContext<'a> { pub framerate_limit: &'a mut Option, pub manifest: &'a GameManifest, pub buffer: &'a mut Buffer, pub input: &'a InputManager, pub time: &'a TimeContext, pub storage: &'a mut StorageManager, pub operation_queue: Vec, } pub struct FullScriptContext<'a> { pub script_id: usize, pub manifest: &'a GameManifest, pub buffer: &'a Buffer, pub input: &'a InputManager, pub time: &'a TimeContext, pub scripts: &'a ScriptManager<'a>, pub files: Box<[Box]>, pub storage: &'a StorageManager, pub wasm_runner: &'a WasmRunner, pub operation_queue: &'a mut Vec, } pub enum Operation { AddCell(Cell), SetFrameLimit(Option), SwitchScene(usize), ActivateScript(usize), DeactivateScript(usize), UpdateState { script_id: usize, new_state: Box<[u8]>, }, UpdateEntity { name: Box, new_state: Box<[u8]>, }, UpdateStorage { new_save: Box<[u8]>, }, UpdateConfig { new_save: Box<[u8]>, }, Exit, } pub type StaticScriptFunction = Option) + Send + Sync>>; #[derive(Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum ScriptDeclaration { #[serde(skip)] Static { name: Box, start: StaticScriptFunction, update: StaticScriptFunction, end: StaticScriptFunction, }, Wasm { name: Box, path: Box, }, } impl<'a> ScriptContext<'a> { pub fn exit(&mut self) -> ! { let _ = self.buffer.finish(); std::process::exit(0); } } impl Default for WasmRunner { fn default() -> Self { Self::new() } } impl WasmRunner { pub fn new() -> Self { 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 { let module = tinywasm::parse_bytes(data)?; let instance = tinywasm::ModuleInstance::instantiate( &mut self.store.lock(), &module, Some(self.linker.clone()), )?; Ok(WasmModule { instance }) } pub fn manifest( &self, path: Box, module: &WasmModule, ) -> Result { let mut store = self.store.lock(); let offset = module .instance .func::<(), u32>(&store, "manifest_addr")? .call(&mut store, ())?; let length = module .instance .func::<(), u32>(&store, "manifest_len")? .call(&mut store, ())?; let manifest = module .instance .memory("memory")? .read_vec(&store, offset as usize, length as usize)?; let manifest = messagepack_serde::from_slice::(&manifest).unwrap(); Ok(GameManifest::from_wasm(path, manifest)) } pub fn patch(&self, module: &WasmModule) -> Result { let mut store = self.store.lock(); let offset = module .instance .func::<(), u32>(&store, "manifest_addr")? .call(&mut store, ())?; let length = module .instance .func::<(), u32>(&store, "manifest_len")? .call(&mut store, ())?; let manifest = module .instance .memory("memory")? .read_vec(&store, offset as usize, length as usize)?; let manifest = messagepack_serde::from_slice::(&manifest).unwrap(); Ok(manifest) } #[expect(clippy::too_many_arguments)] pub fn run_function( &self, script_id: usize, module: &WasmModule, scripts: &ScriptManager, files: Box<[Box]>, wasm_runner: &WasmRunner, context: &mut ScriptContext<'_>, function_name: &str, ) -> 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::(&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 } } impl ScriptDeclaration { pub fn name(&self) -> &str { match self { Self::Static { name, .. } => name, Self::Wasm { name, .. } => name, } } } impl<'a> ScriptManager<'a> { pub fn new(scripts: &'a [ScriptDeclaration]) -> Self { Self { scripts, active_scripts: Vec::new(), entities: FxHashMap::default(), script_states: FxHashMap::default(), } } pub fn script_id_by_name(&self, needle: &str) -> Option { for (i, script) in self.scripts.iter().enumerate() { match script { ScriptDeclaration::Static { name, .. } => { if &**name == needle { return Some(i); } } ScriptDeclaration::Wasm { name, .. } => { if &**name == needle { return Some(i); } } } } None } pub fn handle_queue( &mut self, context: &mut ScriptContext<'_>, wasm_runner: &WasmRunner, files: &mut FileManager, ) { for operation in &context.operation_queue { match operation { Operation::AddCell(cell) => context.buffer.cells.push(*cell), Operation::SetFrameLimit(duration) => *context.framerate_limit = *duration, Operation::SwitchScene(id) => switch_scenes( self, &mut ScriptContext { framerate_limit: context.framerate_limit, manifest: context.manifest, buffer: context.buffer, input: context.input, time: context.time, storage: context.storage, operation_queue: Vec::new(), }, files, wasm_runner, *id, ), Operation::ActivateScript(id) => self.activate_script( *id, &mut ScriptContext { manifest: context.manifest, buffer: context.buffer, input: context.input, time: context.time, storage: context.storage, operation_queue: Vec::new(), framerate_limit: context.framerate_limit, }, wasm_runner, files, ), Operation::DeactivateScript(id) => self.deactivate_script( *id, &mut ScriptContext { manifest: context.manifest, buffer: context.buffer, input: context.input, time: context.time, storage: context.storage, operation_queue: Vec::new(), framerate_limit: context.framerate_limit, }, wasm_runner, files, ), Operation::UpdateState { script_id, new_state, } => { self.script_states.insert(*script_id, new_state.clone()); } Operation::UpdateEntity { name, new_state } => { self.entities.insert(name.clone(), new_state.clone()); } Operation::UpdateStorage { new_save } => { context.storage.background_save_data(new_save.clone()); } Operation::UpdateConfig { new_save } => { context.storage.background_save_config(new_save.clone()); } Operation::Exit => context.exit(), } } context.operation_queue.clear(); } pub fn activate_script( &mut self, script_id: usize, context: &mut ScriptContext<'_>, wasm_runner: &WasmRunner, files: &mut FileManager, ) { let Some(script_declaration) = self.scripts.get(script_id) else { unreachable!("The active script does not exist"); }; match script_declaration { ScriptDeclaration::Static { start, .. } => { start.as_ref().inspect(|s| { let loaded_files = files.loaded_files(); s(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, }) }); } ScriptDeclaration::Wasm { path, .. } => { let module = files.load_file(path.clone(), wasm_runner); let module = module.as_ref().as_ref().unwrap(); let loaded_files = files.loaded_files(); if let Err(e) = wasm_runner.run_function( script_id, module, self, loaded_files, wasm_runner, context, "start_script", ) { log::error!("{e}"); } } } self.active_scripts.push(script_id); self.handle_queue(context, wasm_runner, files); } pub fn deactivate_script( &mut self, script_id: usize, context: &mut ScriptContext<'_>, wasm_runner: &WasmRunner, files: &mut FileManager, ) { for script_id in self .active_scripts .iter() .cloned() .filter(|element| *element == script_id) { let Some(script) = self.scripts.get(script_id) else { unreachable!("The active script does not exist"); }; self.script_states.remove(&script_id); match script { ScriptDeclaration::Static { end, .. } => { end.as_ref().inspect(|s| { let loaded_files = files.loaded_files(); s(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, }); }); } ScriptDeclaration::Wasm { path, .. } => { let module = files.load_file(path.clone(), wasm_runner); let module = module.as_ref().as_ref().unwrap(); let loaded_files = files.loaded_files(); if let Err(e) = wasm_runner.run_function( script_id, module, self, loaded_files, wasm_runner, context, "end_script", ) { log::error!("{e}"); } } }; } self.active_scripts.retain(|element| *element != script_id); self.handle_queue(context, wasm_runner, files); } pub fn run_scripts( &mut self, context: &mut ScriptContext<'_>, wasm_runner: &WasmRunner, files: &mut FileManager, ) { puffin::profile_function!(); for script_id in &self.active_scripts { let Some(script) = self.scripts.get(*script_id) else { unreachable!("The active script does not exist"); }; match script { ScriptDeclaration::Static { name, update, .. } => { if let Some(s) = update { puffin::profile_scope!("run script", name); let loaded_files = files.loaded_files(); s(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, }); } } ScriptDeclaration::Wasm { path, .. } => { let module = files.load_file(path.clone(), wasm_runner); let module = module.as_ref().as_ref().unwrap(); let loaded_files = files.loaded_files(); if let Err(e) = wasm_runner.run_function( *script_id, module, self, loaded_files, wasm_runner, context, "update_script", ) { log::error!("Error while running scripts: {e}"); } } } } self.handle_queue(context, wasm_runner, files); } }