summaryrefslogtreecommitdiff
path: root/src/file.rs
blob: 20fdd68f368b57e40b2a906268397fad0a915b5b (plain)
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<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 { 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_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>, 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()
	}
}