good-web-game 0.6.0

An alternative implementation of the ggez game engine, based on miniquad
Documentation
// large parts directly stolen from macroquad: https://github.com/not-fl3/macroquad/blob/854aa50302a00ce590d505e28c9ecc42ae24be58/src/file.rs

use std::sync::{Arc, Mutex};
use std::{collections::HashMap, io, path};

use crate::GameError::ResourceLoadError;
use crate::{conf::Conf, Context, GameError, GameResult};
use std::panic::panic_any;

#[derive(Debug, Clone)]
pub struct File {
    pub bytes: io::Cursor<Vec<u8>>,
}

impl io::Read for File {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.bytes.read(buf)
    }
}

/// A structure that contains the filesystem state and cache.
#[derive(Debug)]
pub struct Filesystem {
    root: Option<path::PathBuf>,
    files: HashMap<path::PathBuf, File>,
}

impl Filesystem {
    #[allow(clippy::redundant_closure)]
    pub(crate) fn new(conf: &Conf) -> Filesystem {
        let mut files = HashMap::new();

        if let Some(tar_file) = conf.cache {
            let mut archive = tar::Archive::new(tar_file);

            for file in archive.entries().unwrap_or_else(|e| panic_any(e)) {
                use std::io::Read;

                let mut file = file.unwrap_or_else(|e| panic_any(e));
                let filename =
                    std::path::PathBuf::from(file.path().unwrap_or_else(|e| panic_any(e)));
                let mut buf = vec![];

                file.read_to_end(&mut buf).unwrap_or_else(|e| panic_any(e));
                if !buf.is_empty() {
                    files.insert(
                        filename,
                        File {
                            bytes: io::Cursor::new(buf),
                        },
                    );
                }
            }
        }

        let root = conf.physical_root_dir.clone();
        Filesystem { root, files }
    }

    /// Opens the given `path` and returns the resulting `File`
    /// in read-only mode.
    pub fn open<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
        let mut path = path::PathBuf::from(path.as_ref());

        // workaround for ggez-style pathes: in ggez paths starts with "/", while in the cache
        // dictionary they are presented without "/"
        if let Ok(stripped) = path.strip_prefix("/") {
            path = path::PathBuf::from(stripped);
        }

        // first check the cache
        if self.files.contains_key(&path) {
            Ok(self.files[&path].clone())
        } else {
            // the file is not inside the cache, so it has to be loaded (locally, or via http url)
            let file = self.load_file(&path)?;
            Ok(file)
        }
    }

    #[cfg(not(target_os = "wasm32"))]
    /// Load file from the path and block until its loaded
    /// Will use filesystem on PC and Android and fail on WASM
    fn load_file<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
        fn load_file_inner(path: &str) -> GameResult<Vec<u8>> {
            let contents = Arc::new(Mutex::new(None));

            {
                let contents = contents.clone();
                let err_path = path.to_string();

                miniquad::fs::load_file(path, move |bytes| {
                    *contents.lock().unwrap() = Some(bytes.map_err(|kind| {
                        GameError::ResourceLoadError(format!(
                            "Couldn't load file {}: {}",
                            err_path, kind
                        ))
                    }));
                });
            }

            // wait until the file has been loaded
            // as miniquad::fs::load_file internally uses non-asynchronous loading for everything
            // except wasm, waiting should only ever occur on wasm (TODO: since this holds the main
            // thread hostage no progress is ever made and this just blocks forever... perhaps this
            // could be worked around by using "asyncify", but that would be both hard and also
            // require an additional post processing step on the generated wasm file)
            loop {
                let mut contents_guard = contents.lock().unwrap();
                if let Some(contents) = contents_guard.take() {
                    return contents;
                }
                drop(contents_guard);
                std::thread::yield_now();
            }
        }

        #[cfg(target_os = "ios")]
        let _ = std::env::set_current_dir(std::env::current_exe().unwrap().parent().unwrap());

        let path = path
            .as_ref()
            .as_os_str()
            .to_os_string()
            .into_string()
            .map_err(|os_string| {
                ResourceLoadError(format!("utf-8-invalid path: {:?}", os_string))
            })?;

        #[cfg(not(target_os = "android"))]
        let path = if let Some(ref root) = self.root {
            format!(
                "{}/{}",
                root.as_os_str()
                    .to_os_string()
                    .into_string()
                    .map_err(|os_string| ResourceLoadError(format!(
                        "utf-8-invalid root: {:?}",
                        os_string
                    )))?,
                path
            )
        } else {
            path
        };

        let buf = load_file_inner(&path)?;
        let bytes = io::Cursor::new(buf);
        Ok(File { bytes })
    }

    #[cfg(target_os = "wasm32")]
    /// Load file from the path and block until its loaded
    /// Will use filesystem on PC and Android and fail on WASM
    fn load_file<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
        Err(GameError::ResourceLoadError(format!(
            "Couldn't load file {}",
            path.as_display()
        )))
    }
}

/// Opens the given path and returns the resulting `File`
/// in read-only mode.
///
/// Checks the [embedded tar file](../conf/struct.Conf.html#method.high_dpi), if there is one, first and if the file cannot be found there
/// continues to either load the file using the OS-filesystem, or just fail on WASM, as blocking loads
/// are impossible there.
pub fn open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
    ctx.filesystem.open(path)
}

/// Loads a file from the path returning an `Option` that will be `Some` once it has been loaded (or loading it failed).
/// Will use filesystem on PC and Android and a http request on WASM.
///
/// Note: Don't wait for the `Option` to become `Some` inside of a loop, as that would create an infinite loop
/// on WASM, where progress on the GET request can only be made _between_ frames of your application.
pub fn load_file_async<P: AsRef<path::Path>>(path: P) -> Arc<Mutex<Option<GameResult<File>>>> {
    // TODO: Create an example showcasing the use of this.
    let contents = Arc::new(Mutex::new(None));
    let path = path
        .as_ref()
        .as_os_str()
        .to_os_string()
        .into_string()
        .map_err(|os_string| ResourceLoadError(format!("utf-8-invalid path: {:?}", os_string)));

    if let Ok(path) = path {
        let contents = contents.clone();

        miniquad::fs::load_file(&*(path.clone()), move |response| {
            let result = match response {
                Ok(bytes) => Ok(File {
                    bytes: io::Cursor::new(bytes),
                }),
                Err(e) => Err(GameError::ResourceLoadError(format!(
                    "Couldn't load file {}: {}",
                    path, e
                ))),
            };
            *contents.lock().unwrap() = Some(result);
        });
    }

    contents
}