diff --git a/src/assets/builtin_font.fnt b/src/builtin/builtin_font.fnt similarity index 100% rename from src/assets/builtin_font.fnt rename to src/builtin/builtin_font.fnt diff --git a/src/assets/builtin_font_0.png b/src/builtin/builtin_font_0.png similarity index 100% rename from src/assets/builtin_font_0.png rename to src/builtin/builtin_font_0.png diff --git a/src/assets/builtin_font_1.png b/src/builtin/builtin_font_1.png similarity index 100% rename from src/assets/builtin_font_1.png rename to src/builtin/builtin_font_1.png diff --git a/src/builtin_fs.rs b/src/builtin_fs.rs new file mode 100644 index 0000000..f5106b5 --- /dev/null +++ b/src/builtin_fs.rs @@ -0,0 +1,245 @@ +use std::{fmt, io}; +use std::fmt::Debug; +use std::io::Cursor; +use std::io::ErrorKind; +use std::io::SeekFrom; +use std::path::{Component, Path, PathBuf}; + +use crate::ggez::{GameError, GameResult}; +use crate::ggez::GameError::FilesystemError; +use crate::ggez::vfs::{OpenOptions, VFile, VFS, VMetadata}; + +#[derive(Debug)] +pub struct BuiltinFile(Cursor<&'static [u8]>); + +impl BuiltinFile { + pub fn from(buf: &'static [u8]) -> Box { + Box::new(BuiltinFile(Cursor::new(buf))) + } +} + +impl io::Read for BuiltinFile { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } +} + +impl io::Seek for BuiltinFile { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + self.0.seek(pos) + } +} + +impl io::Write for BuiltinFile { + fn write(&mut self, buf: &[u8]) -> io::Result { + Err(io::Error::new(ErrorKind::PermissionDenied, "Built-in file system is read-only.")) + } + + fn flush(&mut self) -> io::Result<()> { + Err(io::Error::new(ErrorKind::PermissionDenied, "Built-in file system is read-only.")) + } +} + +struct BuiltinMetadata { + is_dir: bool, + size: u64, +} + +impl VMetadata for BuiltinMetadata { + fn is_dir(&self) -> bool { + self.is_dir + } + + fn is_file(&self) -> bool { + !self.is_dir + } + + fn len(&self) -> u64 { + self.size + } +} + +#[derive(Clone)] +enum FSNode { + File(&'static str, &'static [u8]), + Directory(&'static str, Vec), +} + +impl FSNode { + fn get_name(&self) -> &'static str { + match self { + FSNode::File(name, _) => { name } + FSNode::Directory(name, _) => { name } + } + } + + fn to_file(&self) -> GameResult> { + match self { + FSNode::File(_, buf) => { Ok(BuiltinFile::from(buf)) } + FSNode::Directory(name, _) => { Err(FilesystemError(format!("{} is a directory.", name))) } + } + } + + fn to_metadata(&self) -> Box { + match self { + FSNode::File(_, buf) => { + Box::new(BuiltinMetadata { + is_dir: false, + size: buf.len() as u64, + }) + } + FSNode::Directory(_, _) => { + Box::new(BuiltinMetadata { + is_dir: true, + size: 0, + }) + } + } + } +} + +pub struct BuiltinFS { + root: Vec, +} + +impl BuiltinFS { + pub fn new() -> Self { + Self { + root: vec![ + FSNode::Directory("builtin", vec![ + FSNode::File("builtin_font.fnt", include_bytes!("builtin/builtin_font.fnt")), + FSNode::File("builtin_font_0.png", include_bytes!("builtin/builtin_font_0.png")), + FSNode::File("builtin_font_1.png", include_bytes!("builtin/builtin_font_1.png")), + ]) + ], + } + } + + fn get_node(&self, path: &Path) -> GameResult { + let mut iter = path.components().peekable(); + + if let Some(Component::RootDir) = iter.next() { + let mut curr_dir = &self.root; + + if iter.peek().is_none() { + return Ok(FSNode::Directory("", self.root.clone())); + } + + while let Some(comp) = iter.next() { + let comp_name = comp.as_os_str().to_string_lossy(); + + for file in curr_dir { + match file { + FSNode::File(name, _) if comp_name.eq(name) => { + return if iter.peek().is_some() { + Err(FilesystemError(format!("Expected a directory, found a file: {:?}", path))) + } else { + Ok(file.clone()) + }; + } + FSNode::Directory(name, contents) if comp_name.eq(name) => { + if iter.peek().is_some() { + curr_dir = contents; + break; + } else { + return Ok(file.clone()); + } + } + _ => {} + } + } + } + } else { + return Err(FilesystemError("Path must be absolute.".to_string())); + } + + Err(FilesystemError("File not found.".to_string())) + } +} + +impl Debug for BuiltinFS { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "") + } +} + +impl VFS for BuiltinFS { + fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult> { + if open_options.write || open_options.create || open_options.append || open_options.truncate { + let msg = format!( + "Cannot alter file {:?} in root {:?}, filesystem read-only", + path, self + ); + return Err(FilesystemError(msg)); + } + + log::info!("open: {:?}", path); + + self.get_node(path)?.to_file() + } + + fn mkdir(&self, _path: &Path) -> GameResult<()> { + Err(FilesystemError("Tried to make directory {} but FS is read-only".to_string())) + } + + fn rm(&self, _path: &Path) -> GameResult<()> { + Err(FilesystemError("Tried to remove file {} but FS is read-only".to_string())) + } + + fn rmrf(&self, _path: &Path) -> GameResult<()> { + Err(FilesystemError("Tried to remove file/dir {} but FS is read-only".to_string())) + } + + fn exists(&self, path: &Path) -> bool { + self.get_node(path).is_ok() + } + + fn metadata(&self, path: &Path) -> GameResult> { + self.get_node(path).map(|v| v.to_metadata()) + } + + fn read_dir(&self, path: &Path) -> GameResult>>> { + match self.get_node(path) { + Ok(FSNode::Directory(_, contents)) => { + let mut vec = Vec::new(); + for node in contents { + vec.push(Ok(PathBuf::from(node.get_name()))) + } + + Ok(Box::new(vec.into_iter())) + } + Ok(FSNode::File(_, _)) => { + Err(FilesystemError(format!("Expected a directory, found a file: {:?}", path))) + } + Err(e) => { + Err(e) + } + } + } + + fn to_path_buf(&self) -> Option { + None + } +} + +/*#[test] +fn test_builtin_fs() { + let fs = BuiltinFS { + root: vec![ + FSNode::File("test.txt", &[]), + FSNode::Directory("memes", vec![ + FSNode::File("nothing.txt", &[]), + FSNode::Directory("secret stuff", vec![ + FSNode::File("passwords.txt", &[b'1', b'2', b'3', b'4', b'5', b'6',]), + ]), + ]), + FSNode::File("test2.txt", &[]), + ], + }; + + println!("{:?}", fs.get_node(Path::new("/")).unwrap()); + println!("{:?}", fs.get_node(Path::new("/test.txt")).unwrap()); + println!("{:?}", fs.get_node(Path::new("/memes")).unwrap()); + println!("{:?}", fs.get_node(Path::new("/memes/nothing.txt")).unwrap()); + println!("{:?}", fs.get_node(Path::new("/memes/secret stuff/passwords.txt")).unwrap()); +}*/ diff --git a/src/ggez/filesystem.rs b/src/ggez/filesystem.rs index 25bbcd6..9316d78 100644 --- a/src/ggez/filesystem.rs +++ b/src/ggez/filesystem.rs @@ -307,6 +307,10 @@ impl Filesystem { self.vfs.push_back(Box::new(physfs)); } + pub(crate) fn mount_vfs(&mut self, vfs: Box) { + self.vfs.push_back(vfs); + } + /// Looks for a file named `/conf.toml` in any resource directory and /// loads it if it finds it. /// If it can't read it for some reason, returns an error. @@ -434,6 +438,10 @@ pub fn mount(ctx: &mut Context, path: &path::Path, readonly: bool) { ctx.filesystem.mount(path, readonly) } +pub fn mount_vfs(ctx: &mut Context, vfs: Box) { + ctx.filesystem.mount_vfs(vfs) +} + /// Looks for a file named `/conf.toml` in any resource directory and /// loads it if it finds it. /// If it can't read it for some reason, returns an error. diff --git a/src/ggez/mod.rs b/src/ggez/mod.rs index 15b0cd4..1142540 100644 --- a/src/ggez/mod.rs +++ b/src/ggez/mod.rs @@ -100,7 +100,7 @@ pub mod filesystem; pub mod graphics; pub mod input; pub mod timer; -mod vfs; +pub mod vfs; pub use crate::ggez::context::{Context, ContextBuilder}; pub use crate::ggez::error::*; diff --git a/src/ggez/vfs.rs b/src/ggez/vfs.rs index b46c99c..fb790a2 100644 --- a/src/ggez/vfs.rs +++ b/src/ggez/vfs.rs @@ -37,11 +37,11 @@ impl VFile for T where T: Read + Write + Seek + Debug {} #[must_use] #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct OpenOptions { - read: bool, - write: bool, - create: bool, - append: bool, - truncate: bool, + pub(crate) read: bool, + pub(crate) write: bool, + pub(crate) create: bool, + pub(crate) append: bool, + pub(crate) truncate: bool, } impl OpenOptions { diff --git a/src/main.rs b/src/main.rs index b6f6e96..e656c94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,9 @@ use crate::stage::StageData; use crate::text_script::TextScriptVM; use crate::texture_set::TextureSet; use crate::ui::UI; +use crate::builtin_fs::BuiltinFS; +mod builtin_fs; mod caret; mod common; mod engine_constants; @@ -265,6 +267,8 @@ pub fn main() -> GameResult { .add_resource_path(resource_dir); let (ctx, event_loop) = &mut cb.build()?; + ctx.filesystem.mount_vfs(Box::new(BuiltinFS::new())); + let game = &mut Game::new(ctx)?; game.state.next_scene = Some(Box::new(LoadingScene::new()));