use std::{ffi::OsStr, path::Path};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use smol::{Task, unblock};
use crate::{
scene::{ResourceRequirements, Scene, scene_id_by_name},
script::ScriptDeclaration,
};
type FileTask = Task<std::io::Result<FileData>>;
pub struct FileManager {
active_tasks: FxHashMap<Box<Path>, FileTask>,
cache: FxHashMap<Box<Path>, FileData>,
}
#[derive(Debug, Clone)]
pub struct GameManifest {
pub name: Box<str>,
pub first_scene: usize,
pub scenes: Box<[Scene]>,
pub scripts: Box<[ScriptDeclaration]>,
}
#[derive(Debug, Serialize, Deserialize)]
struct TomlGameManifest {
name: Box<str>,
first_scene: Box<str>,
#[serde(default)]
scene: Box<[TomlScene]>,
#[serde(default)]
script: Box<[TomlScript]>,
}
#[derive(Debug, Serialize, Deserialize)]
struct TomlScene {
name: Box<str>,
#[serde(default)]
preload_scenes: Box<[Box<str>]>,
#[serde(default)]
scripts: Box<[Box<str>]>,
#[serde(default)]
preload_scripts: Box<[Box<str>]>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
enum TomlScript {
Wasm { name: Box<str>, path: Box<Path> },
}
pub enum FileData {
GameManifest(GameManifest),
WasmModule(wasmi::Module),
}
fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> {
let mut scripts = Vec::with_capacity(manifest.script.len());
let script_map: FxHashMap<Box<str>, usize> = manifest
.script
.into_iter()
.map(|script| match script {
TomlScript::Wasm { name, path } => {
scripts.push(ScriptDeclaration::Wasm {
name: name.clone(),
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<FileData> {
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<Path>,
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_cached_file(&self, path: &Path) -> Option<&FileData> {
self.cache.get(path)
}
pub fn start_loading_file(&mut self, path: impl AsRef<Path>, 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<Path>) -> 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<Path>) {
self.active_tasks.remove(path.as_ref());
self.cache.remove(path.as_ref());
}
}
impl Default for FileManager {
fn default() -> Self {
Self::new()
}
}
|