summaryrefslogtreecommitdiff
path: root/src/file.rs
blob: 42fecfae1f18393c5066e8235d4811d2f7f081ed (plain)
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, Clone, 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()
	}
}