use std::{ffi::OsStr, path::Path}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use smol::{Task, block_on, unblock}; use crate::{ scene::{ResourceRequirements, Scene, scene_id_by_name}, script::ScriptDeclaration, }; type FileTask = Task>; pub struct FileManager { active_tasks: FxHashMap, FileTask>, cache: FxHashMap, FileData>, } #[derive(Debug, Clone)] pub struct GameManifest { pub name: Box, pub first_scene: usize, pub scenes: Box<[Scene]>, pub scripts: Box<[ScriptDeclaration]>, } #[derive(Debug, Serialize, Deserialize)] struct TomlGameManifest { name: Box, first_scene: Box, #[serde(default)] scene: Box<[TomlScene]>, #[serde(default)] script: Box<[TomlScript]>, } #[derive(Debug, Serialize, Deserialize)] struct TomlScene { name: Box, #[serde(default)] preload_scenes: Box<[Box]>, #[serde(default)] scripts: Box<[Box]>, #[serde(default)] preload_scripts: Box<[Box]>, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "kebab-case")] enum TomlScript { Wasm { name: Box, path: Box }, } pub enum FileData { GameManifest(GameManifest), WasmModule(wasmi::Module), } fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result { let mut scripts = Vec::with_capacity(manifest.script.len()); let script_map: FxHashMap, usize> = manifest .script .into_iter() .map(|script| match script { TomlScript::Wasm { name, path } => { scripts.push(ScriptDeclaration::Wasm { path }); (name, scripts.len() - 1) } }) .collect(); let scenes: Box<[Scene]> = manifest .scene .into_iter() .map(|scene| Scene { name: scene.name, preload_scenes: Box::from([]), scripts: ResourceRequirements { required: scene .scripts .iter() .map(|s| *script_map.get(s).unwrap()) .collect(), preload: scene .preload_scripts .iter() .map(|s| *script_map.get(s).unwrap()) .collect(), }, }) .collect(); let first_scene = scene_id_by_name(&scenes, &manifest.first_scene).ok_or( std::io::Error::other("Invalid first scene: scene not found"), )?; Ok(GameManifest { name: manifest.name, first_scene, scenes, scripts: scripts.into_boxed_slice(), }) } fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result { if path == "manifest.toml" { return Ok(FileData::GameManifest(normalize_game_manifest( toml::from_slice(data).map_err(std::io::Error::other)?, )?)); } let extension = path .extension() .and_then(OsStr::to_str) .map(|ext| ext.to_lowercase()); if extension.as_deref() == Some("wasm") { return Ok(FileData::WasmModule( wasmi::Module::new(compiler, data).map_err(std::io::Error::other)?, )); } Err(std::io::Error::other("Unrecognized file extension")) } impl FileData { pub fn unwrap_manifest(&self) -> &GameManifest { if let FileData::GameManifest(manifest) = self { manifest } else { panic!("Not a game manifest"); } } pub fn unwrap_wasm_module(&self) -> &wasmi::Module { if let FileData::WasmModule(module) = self { module } else { panic!("Not a WASM module") } } } impl FileManager { pub fn new() -> Self { Self { active_tasks: FxHashMap::default(), cache: FxHashMap::default(), } } pub fn load_file( &mut self, path: Box, wasm_compiler: &wasmi::Engine, ) -> std::io::Result<&FileData> { if !self.cache.contains_key(&path) { let data = compile_data( path.as_ref(), &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?, wasm_compiler, )?; self.cache.entry(path.clone()).insert_entry(data); } Ok(self.cache.get(&path).unwrap()) } pub fn get_loaded_file(&mut self, path: Box) -> Option> { if let Some(task) = self.active_tasks.get(path.as_ref()) && task.is_finished() { let data = match block_on(self.active_tasks.remove(&path).unwrap()) { Ok(data) => data, Err(e) => return Some(Err(e)), }; self.cache.entry(path.clone()).insert_entry(data); Some(Ok(self.cache.get(&path).unwrap())) } else if let Some(data) = self.cache.get(&path) { Some(Ok(data)) } else { None } } pub fn start_loading_file(&mut self, path: impl AsRef, wasm_compiler: &wasmi::Engine) { let boxed_path = Box::from(path.as_ref()); let compiler = wasm_compiler.clone(); let task = unblock(move || { compile_data( &boxed_path, &std::fs::read(&boxed_path).map(Vec::into_boxed_slice)?, &compiler, ) }); let boxed_path = Box::from(path.as_ref()); self.active_tasks.insert(boxed_path, task); } pub fn is_file_loaded(&self, path: impl AsRef) -> bool { self.active_tasks .get(path.as_ref()) .is_some_and(|task| task.is_finished()) || self.cache.contains_key(path.as_ref()) } pub fn unload_file(&mut self, path: impl AsRef) { self.active_tasks.remove(path.as_ref()); self.cache.remove(path.as_ref()); } } impl Default for FileManager { fn default() -> Self { Self::new() } }