summaryrefslogtreecommitdiff
path: root/src/file.rs
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-06-28 11:09:28 -0400
committerMica White <botahamec@outlook.com>2026-06-28 11:09:28 -0400
commit3eb02cbe5b106cd332f25d658430f7e0ae320745 (patch)
tree20153b185b2e7d5c41b2b2d29c6c28bba8bd31b9 /src/file.rs
parentb9570962cfc214bbac0f3fef1e6c1f9848f52658 (diff)
initial commit
Diffstat (limited to 'src/file.rs')
-rw-r--r--src/file.rs154
1 files changed, 154 insertions, 0 deletions
diff --git a/src/file.rs b/src/file.rs
new file mode 100644
index 0000000..8e96fa9
--- /dev/null
+++ b/src/file.rs
@@ -0,0 +1,154 @@
+use std::{ffi::OsStr, path::Path, sync::Arc};
+
+use rustc_hash::FxHashMap;
+use serde::{Deserialize, Serialize};
+use smol::{Task, block_on, unblock};
+
+use crate::scene::{Scene, scene_id_by_name};
+
+type FileTask = Task<std::io::Result<FileData>>;
+
+pub struct FileManager {
+ pub wasm_compiler: Arc<wasmi::Engine>,
+ active_tasks: FxHashMap<Box<Path>, FileTask>,
+ cache: FxHashMap<Box<Path>, FileData>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct GameManifest {
+ pub name: Box<str>,
+ pub first_scene: usize,
+ pub scenes: Box<[Scene]>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct TomlGameManifest {
+ name: Box<str>,
+ first_scene: Box<str>,
+ scene: Box<[Scene]>,
+}
+
+pub enum FileData {
+ GameManifest(GameManifest),
+ WasmModule(wasmi::Module),
+}
+
+fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> {
+ let first_scene = scene_id_by_name(&manifest.scene, &manifest.first_scene).ok_or(
+ std::io::Error::other("Invalid first scene: scene not found"),
+ )?;
+ Ok(GameManifest {
+ name: manifest.name,
+ first_scene,
+ scenes: manifest.scene,
+ })
+}
+
+fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result<FileData> {
+ if path == "manifest.postcard" {
+ return Ok(FileData::GameManifest(
+ postcard::from_bytes(data).map_err(std::io::Error::other)?,
+ ));
+ } else 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 {
+ wasm_compiler: Arc::new(wasmi::Engine::default()),
+ active_tasks: FxHashMap::default(),
+ cache: FxHashMap::default(),
+ }
+ }
+
+ pub fn load_file(&mut self, path: Box<Path>) -> std::io::Result<&FileData> {
+ let data = compile_data(
+ path.as_ref(),
+ &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?,
+ &self.wasm_compiler.clone(),
+ )?;
+ self.cache.entry(path.clone()).insert_entry(data);
+ Ok(self.cache.get(&path).unwrap())
+ }
+
+ pub fn get_loaded_file(&mut self, path: Box<Path>) -> Option<std::io::Result<&FileData>> {
+ 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<Path>) {
+ let boxed_path = Box::from(path.as_ref());
+ let compiler = self.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()
+ }
+}