diff options
| author | Micha White <botahamec@outlook.com> | 2023-11-08 14:09:17 -0500 |
|---|---|---|
| committer | Micha White <botahamec@outlook.com> | 2023-11-08 14:09:17 -0500 |
| commit | 279c233cb1f32ed42419ed6a9c2e14c1c1bc80e7 (patch) | |
| tree | b72aee1d6363b3e65dcd20581590f902a88fbc0a /scripts/src/lib.rs | |
| parent | 1ec2599c45a51dde87496edce7cd3ab301a18539 (diff) | |
Create a script system
Diffstat (limited to 'scripts/src/lib.rs')
| -rw-r--r-- | scripts/src/lib.rs | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/scripts/src/lib.rs b/scripts/src/lib.rs new file mode 100644 index 0000000..214001c --- /dev/null +++ b/scripts/src/lib.rs @@ -0,0 +1,318 @@ +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<Box<str>, ScriptId>, + + vtable_scripts: Vec<Script<VTableScript>>, + wasm_scripts: Vec<Script<WasmScript>>, + + relink_every_frame: bool, + wasm_engine: Engine, + wasm_imports: Linker<WasmScriptState>, +} + +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<str>, 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<str>, + path: impl AsRef<Path>, + trusted: bool, + ) -> Result<ScriptId, InvalidWasmScript> { + 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<Path>, + 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<Result<(), InvalidWasmScript>> { + 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<Params, Args>( + &mut self, + module: &str, + name: &str, + func: impl IntoFunc<WasmScriptState, Params, Args> + 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<bool> { + 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<bool> { + 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<S> { + inner: S, + enabled: bool, +} + +impl<S> Script<S> { + 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; + } +} |
