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;
}
}
|