use std::path::Path; use rustc_hash::FxHashMap; use crate::{ buffer::{Buffer, Cell}, file::{FileManager, GameManifest}, input::InputManager, scene::{preload_scene, switch_scenes}, storage::StorageManager, time::TimeContext, }; mod wasm; pub struct ScriptManager<'a> { scripts: &'a [ScriptDeclaration], pub active_scripts: Vec, entities: FxHashMap, Box<[u8]>>, script_states: FxHashMap>, } pub struct WasmRunner { pub engine: wasmi::Engine, pub linker: wasmi::Linker>, } pub struct ScriptContext<'a> { 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: &'a FileManager, pub storage: &'a StorageManager, pub wasm_runner: &'a WasmRunner, pub operation_queue: &'a mut Vec, } pub enum Operation { AddCell(Cell), PreloadScene(usize), SwitchScene(usize), PreloadScript(usize), UnloadScript(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]>, }, Exit, } type StaticScriptFunction = Option, &mut Box<[u8]>)>; #[derive(Debug, Clone)] pub enum ScriptDeclaration { 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); } } fn get_script_state(states: &mut FxHashMap>, script_id: usize) -> &mut Box<[u8]> { states.entry(script_id).or_insert(Box::from([])) } impl Default for WasmRunner { fn default() -> Self { Self::new() } } impl WasmRunner { pub fn new() -> Self { let engine = wasmi::Engine::default(); let linker = unsafe { std::mem::transmute::< wasmi::Linker>, wasmi::Linker>, >(wasm::linker(&engine).unwrap()) }; Self { engine, linker } } pub fn compile(&self, data: &[u8]) -> Result { wasmi::Module::new(&self.engine, data) } #[allow(clippy::too_many_arguments)] pub fn run_function( &self, script_id: usize, module: &wasmi::Module, scripts: &ScriptManager, files: &FileManager, 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 { 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 []) } } } 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 preload_script( &self, script_id: usize, files: &mut FileManager, wasm_runner: &WasmRunner, ) { let script = &self.scripts[script_id]; match script { ScriptDeclaration::Static { .. } => (), ScriptDeclaration::Wasm { path, .. } => { files.start_loading_file(path, &wasm_runner.engine) } } } pub fn is_script_loaded(&self, script_id: usize, files: &FileManager) -> bool { let script = &self.scripts[script_id]; match script { ScriptDeclaration::Static { .. } => true, ScriptDeclaration::Wasm { path, .. } => files.is_file_loaded(path), } } pub fn unload_script(&self, script_id: usize, files: &mut FileManager) { let script = &self.scripts[script_id]; match script { ScriptDeclaration::Static { .. } => (), ScriptDeclaration::Wasm { path, .. } => files.unload_file(path), } } 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::PreloadScene(id) => { preload_scene(&context.manifest.scenes, self, files, wasm_runner, *id) } Operation::SwitchScene(id) => switch_scenes( self, &mut ScriptContext { manifest: context.manifest, buffer: context.buffer, input: context.input, time: context.time, storage: context.storage, operation_queue: Vec::new(), }, files, wasm_runner, *id, ), Operation::PreloadScript(id) => self.preload_script(*id, files, wasm_runner), Operation::UnloadScript(id) => self.unload_script(*id, files), 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(), }, 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(), }, wasm_runner, files, ), Operation::UpdateState { script_id, new_state, } => *get_script_state(&mut self.script_states, *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(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.map(|s| { s( context, get_script_state(&mut self.script_states, script_id), ) }); } 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( script_id, module, self, files, wasm_runner, context, "start", ); } } 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.map(|s| { s( context, get_script_state(&mut self.script_states, script_id), ); }); } 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( script_id, module, self, files, wasm_runner, context, "end", ); } }; } 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 { update, .. } => { if let Some(s) = update { s( context, get_script_state(&mut self.script_states, *script_id), ) } } 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( *script_id, module, self, files, wasm_runner, context, "update", ); } } } self.handle_queue(context, wasm_runner, files); } }