summaryrefslogtreecommitdiff
path: root/scripts/src/lib.rs
blob: 214001c12de492daf0654cd3c4f97d1a60c7768d (plain)
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;
	}
}