aboutsummaryrefslogtreecommitdiff
path: root/games/rstnode/rst-client/src/assets.rs
use crate::{error::LoadError, GameSettings};
use cairo::{Context, Format, ImageSurface, Rectangle};
use ggez::graphics::Image;
use librsvg::{CairoRenderer, Loader};
use std::{
    collections::BTreeMap,
    ffi::OsStr,
    fs::{read_dir, File},
    io::BufWriter,
    io::Read,
    path::{Path, PathBuf},
};
use tempfile::tempdir;

pub type Result<T> = std::result::Result<T, LoadError>;

#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct URI(String, String);

impl From<&'static str> for URI {
    fn from(s: &'static str) -> Self {
        let mut v: Vec<_> = s.split("/").collect();
        Self(v.remove(0).into(), v.remove(0).into())
    }
}

impl From<String> for URI {
    fn from(s: String) -> Self {
        let mut v: Vec<_> = s.split("/").collect();
        Self(v.remove(0).into(), v.remove(0).into())
    }
}

/// Asset loader
#[derive(Debug)]
pub struct Assets {
    inner: BTreeMap<URI, Image>,
}

impl Assets {
    fn new() -> Self {
        Self {
            inner: Default::default(),
        }
    }

    pub fn find<U: Into<URI>>(&self, u: U) -> Option<Image> {
        self.inner.get(&u.into()).map(|i| i.clone())
    }

    /// Load an asset directory path
    fn load_tree(&mut self, ctx: &mut ggez::Context, tmpdir: &Path, p: &Path) -> Result<()> {
        let err: LoadError = p.to_str().unwrap().into();

        read_dir(p)
            .map_err(|_| err)?
            .map(|e| {
                let e = e.unwrap();
                let p = e.path();

                let ext = OsStr::new("svg");

                if p.is_dir() {
                    debug!(
                        "Entering directory {}",
                        p.file_name().unwrap().to_str().unwrap()
                    );
                    self.load_tree(ctx, tmpdir, p.as_path())?;
                } else if p.extension() == Some(ext) {
                    let png = load_svg(tmpdir, p.as_path())?;

                    let basepath = p.with_extension("");

                    let uri_cat = p.parent().unwrap().file_name().unwrap().to_str().unwrap();
                    let name = basepath.file_name().unwrap().to_str().unwrap();
                    let uri = format!("{}/{}", uri_cat, name);
                    let path_str = png.as_path().to_str().unwrap();

                    let mut content = vec![];
                    let mut f = File::open(png.as_path()).map_err(|_| {
                        LoadError::from(format!("No such file: {}", path_str).as_str())
                    })?;

                    f.read_to_end(&mut content).map_err(|e| {
                        LoadError::from(
                            format!("Read error for {}: {}", path_str, e.to_string()).as_str(),
                        )
                    })?;

                    self.inner.insert(
                        uri.into(),
                        Image::from_bytes(ctx, content.as_slice()).map_err(|e| {
                            LoadError::from(
                                format!("Read error for {}: {}", path_str, e.to_string()).as_str(),
                            )
                        })?,
                    );
                }

                Ok(())
            })
            .fold(Ok(()), |acc, res| match (acc, res) {
                (Ok(_), Ok(_)) => Ok(()),
                (Ok(_), Err(e)) => Err(e),
                (Err(e), _) => Err(e),
            })
    }
}

/// Load all game assets into the game
///
/// This function performs three main steps.
///
/// 1. Check that the provided path is a directory
/// 2. Recursively load Directories and files and call
///    [`load_svg`](self::load_svg) on each `.svg` file
/// 3. Re-load newly converted assets into [`Assets`](self::Assets)
pub fn load_tree(ctx: &mut ggez::Context, settings: &GameSettings) -> Result<Assets> {
    let path = match settings.assets.clone() {
        Some(s) => Ok(s),
        None => Err(LoadError::from("No assets path set!")),
    }?;

    debug!(
        "Starting assets loading harness on {}",
        path.to_str().unwrap()
    );

    let tmpdir = tempdir().unwrap();
    let mut assets = Assets::new();
    assets.load_tree(ctx, tmpdir.path(), path.as_path())?;
    info!("Asset loading complete!");
    Ok(assets)
}

/// A utility function to take an SVG and render it to a raster image
/// according to a render spec
pub fn load_svg(tmpdir: &Path, p: &Path) -> Result<PathBuf> {
    let err: LoadError = p.to_str().unwrap().into();

    let handle = Loader::new().read_path(p).map_err(|_| err.clone())?;
    let renderer = CairoRenderer::new(&handle);

    let surf = ImageSurface::create(Format::ARgb32, 256, 256).map_err(|_| err.clone())?;
    let cr = Context::new(&surf);

    renderer
        .render_document(
            &cr,
            &Rectangle {
                x: 0.0,
                y: 0.0,
                width: 256.0,
                height: 256.0,
            },
        )
        .map_err(|_| err.clone())?;

    let png = p.with_extension("png");
    let name = png
        .file_name()
        .map_or_else(|| Err(err.clone()), |name| Ok(name))?;

    let out = tmpdir.join(name.clone());
    let mut file = BufWriter::new(File::create(out.clone()).map_err(|_| err.clone())?);
    surf.write_to_png(&mut file).map_err(|_| err.clone())?;
    Ok(out.to_path_buf())
}