use std::{collections::HashMap, path::Path}; pub use vtable::VTableScript; use wasmtime::{Engine, IntoFunc, Linker, Module, Store}; pub use wasm::InvalidWasmScript; use wasm::{WasmScript, WasmScriptState}; mod libs; mod vtable; mod wasm; /// A sturcture to contain and use any needed scripts. /// /// # Performance /// /// V-Table scripts are faster than WebAssembly scripts. WebAssembly scripting /// is provided to make hot-reloading possible. Either way, scripts should be /// reasonably fast. /// /// A single long WebAssembly script is typically faster than lots of small /// scripts. Try to keep the number of scripts small. No more than twenty /// scripts should be enabled at once. #[derive(Default)] pub struct ScriptManager { script_map: HashMap, ScriptId>, vtable_scripts: Vec>, wasm_scripts: Vec>, relink_every_frame: bool, wasm_engine: Engine, wasm_imports: Linker, } impl ScriptManager { /// Creates a new, empty script manager pub fn new() -> Self { let wasm_engine = Engine::default(); let mut wasm_imports = Linker::new(&wasm_engine); wasm_imports.allow_shadowing(true); let mut this = Self { script_map: HashMap::new(), vtable_scripts: Vec::new(), wasm_scripts: Vec::new(), relink_every_frame: false, wasm_imports, wasm_engine, }; libs::add_alligator_library(&mut this); this } /// If set to `true`, then every frame, the scripts will be relinked with /// it's dependencies on each call. /// /// The main reason to do this would be to ensure that the state is reset /// each frame. However, the link is slow, so it's not recommended to use /// this setting for releasing a game. This is only recommended for /// debugging. pub fn set_relink_every_frame(&mut self, setting: bool) { self.relink_every_frame = setting; } /// Returns `true` if the scripts are set to be relinked each frame. See /// [`set_relink_every_frame`] for more details. pub fn relink_every_frame(&self) -> bool { self.relink_every_frame } /// Adds a script to this manager. /// /// By default, all scripts are disabled. To enable it, call /// [`enable_script`]. pub fn add_vtable_script(&mut self, name: Box, script: VTableScript) -> ScriptId { let idx = self.vtable_scripts.len(); self.vtable_scripts.push(Script::new(script)); let id = ScriptId { kind: ScriptKind::VTable, idx, }; self.script_map.insert(name, id); id } /// Adds a WebAssembly script to this manager. /// /// By default, all scripts are disabled. To enable it, call /// [`enable_script`]. /// /// # Errors /// /// This function returns an error if it could not use the given script. pub fn add_wasm_script( &mut self, name: Box, path: impl AsRef, trusted: bool, ) -> Result { let idx = self.vtable_scripts.len(); let script = WasmScript::new( path.as_ref(), &self.wasm_engine, &self.wasm_imports, trusted, )?; self.wasm_scripts.push(Script::new(script)); let id = ScriptId { kind: ScriptKind::Wasm, idx, }; self.script_map.insert(name, id); Ok(id) } /// Add a WebAssembly module which can be called by other WebAssembly /// scripts. /// /// If there are two libraries with the same name, only the newest module /// is used. This feature can be used to allow libraries to be hot /// reloaded. /// /// The `trusted` parameter refers to whether this library can call /// functions which require trust. /// /// # Performance /// /// Running this function relinks all WebAssembly scripts. For best /// performance, all WebAssembly libraries should be added before any /// scripts are loaded. pub fn add_wasm_library( &mut self, name: &str, path: impl AsRef, trusted: bool, ) -> Result<(), InvalidWasmScript> { let module = Module::from_file(&self.wasm_engine, path)?; let mut store = Store::new(&self.wasm_engine, WasmScriptState::new(trusted)); let instance = self.wasm_imports.instantiate(&mut store, &module)?; self.wasm_imports.instance(store, name, instance)?; for script in &mut self.wasm_scripts { if let Err(e) = script.inner.relink(&self.wasm_engine, &self.wasm_imports) { log::error!("{e}"); } } Ok(()) } /// Reloads a WebAssembly script from disk. pub fn reload_script(&mut self, name: &str) -> Option> { let id = self.script_map.get(name).unwrap(); let result = match id.kind { ScriptKind::Wasm => self .wasm_scripts .get_mut(id.idx) .unwrap() .inner .reload(&self.wasm_engine, &self.wasm_imports), _ => return None, }; Some(result) } /// Adds a library to the manager pub fn add_library_function( &mut self, module: &str, name: &str, func: impl IntoFunc + Clone, ) -> Option<()> { self.wasm_imports.func_wrap(module, name, func).ok()?; Some(()) } /// Checks if the script with the given name is enabled pub fn is_script_enabled(&self, name: &str) -> Option { let id = self.script_map.get(name)?; let enabled = match id.kind { ScriptKind::VTable => self.vtable_scripts.get(id.idx)?.is_enabled(), ScriptKind::Wasm => self.wasm_scripts.get(id.idx)?.is_enabled(), }; Some(enabled) } /// Checks if the script with the given name is trusted pub fn is_script_trusted(&self, name: &str) -> Option { let id = self.script_map.get(name)?; let trusted = match id.kind { ScriptKind::VTable => true, ScriptKind::Wasm => self.wasm_scripts.get(id.idx)?.inner.is_trusted(), }; Some(trusted) } /// This enables the script, allowing it to run each frame. /// /// Calling this automatically calls the `begin` function for the script. pub fn enable_script(&mut self, name: &str) -> Option<()> { let id = self.script_map.get(name)?; match id.kind { ScriptKind::VTable => { let script = self.vtable_scripts.get_mut(id.idx)?; if script.is_enabled() { // avoid calling `begin` if the script is already enabled return Some(()); } script.enable(); script.inner.begin(); } ScriptKind::Wasm => { let script = self.wasm_scripts.get_mut(id.idx)?; if script.is_enabled() { return Some(()); } script.enable(); if let Err(e) = script.inner.begin() { log::error!("{}", e); } } }; Some(()) } /// This disables the script, preventing it from running pub fn disable_script(&mut self, name: &str) -> Option<()> { let id = self.script_map.get(name)?; match id.kind { ScriptKind::VTable => self.vtable_scripts.get_mut(id.idx)?.disable(), ScriptKind::Wasm => self.wasm_scripts.get_mut(id.idx)?.disable(), }; Some(()) } /// This allows the script to run functions which require trust pub fn trust_script(&mut self, name: &str) -> Option<()> { let id = self.script_map.get(name)?; if id.kind == ScriptKind::Wasm { self.wasm_scripts.get_mut(id.idx)?.inner.trust(); } Some(()) } /// This prevents the script from running functions which require trust pub fn untrust_script(&mut self, name: &str) -> Option<()> { let id = self.script_map.get(name)?; if id.kind == ScriptKind::Wasm { self.wasm_scripts.get_mut(id.idx)?.inner.untrust(); } Some(()) } /// Runs the `update` function for all enabled scripts pub fn run_update_scripts(&mut self) { for script in &mut self.vtable_scripts { if script.is_enabled() { script.inner.update(); } } for script in &mut self.wasm_scripts { if script.is_enabled() { if self.relink_every_frame { if let Err(e) = script.inner.relink(&self.wasm_engine, &self.wasm_imports) { log::error!("{e}"); } } if let Err(e) = script.inner.update() { log::error!("{}", e); } } } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum ScriptKind { VTable, Wasm, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ScriptId { kind: ScriptKind, idx: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct Script { inner: S, enabled: bool, } impl Script { fn new(inner: S) -> Self { Self { inner, enabled: true, } } fn is_enabled(&self) -> bool { self.enabled } fn enable(&mut self) { self.enabled = true; } fn disable(&mut self) { self.enabled = false; } }