summaryrefslogtreecommitdiff
path: root/src/script.rs
blob: 8539c92efe0259993a50c3402bc4ecf5ed0a919f (plain)
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<usize>,
	entities: FxHashMap<Box<str>, Box<[u8]>>,
	script_states: FxHashMap<usize, Box<[u8]>>,
}

pub struct WasmRunner {
	pub engine: wasmi::Engine,
	pub linker: wasmi::Linker<FullScriptContext<'static>>,
}

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<Operation>,
}

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<Operation>,
}

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<str>,
		new_state: Box<[u8]>,
	},
	UpdateStorage {
		new_save: Box<[u8]>,
	},
	Exit,
}

type StaticScriptFunction = Option<fn(&mut ScriptContext<'_>, &mut Box<[u8]>)>;

#[derive(Debug, Clone)]
pub enum ScriptDeclaration {
	Static {
		name: Box<str>,
		start: StaticScriptFunction,
		update: StaticScriptFunction,
		end: StaticScriptFunction,
	},
	Wasm {
		name: Box<str>,
		path: Box<Path>,
	},
}

impl<'a> ScriptContext<'a> {
	pub fn exit(&mut self) -> ! {
		let _ = self.buffer.finish();
		std::process::exit(0);
	}
}

fn get_script_state(states: &mut FxHashMap<usize, Box<[u8]>>, 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<FullScriptContext<'_>>,
				wasmi::Linker<FullScriptContext<'_>>,
			>(wasm::linker(&engine).unwrap())
		};
		Self { engine, linker }
	}

	pub fn compile(&self, data: &[u8]) -> Result<wasmi::Module, wasmi::Error> {
		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<'_>>(
					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<usize> {
		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);
	}
}