use crate::{error::LoadError, GameSettings}; use cairo::{Context, Format, ImageSurface, Rectangle}; use ggez::graphics::Image; use librsvg::{CairoRenderer, Loader}; use std::{ collections::BTreeMap, error::Error, ffi::OsStr, fs::{read_dir, File}, io::BufWriter, io::Read, path::{Path, PathBuf}, }; use tempfile::tempdir; pub type Result = std::result::Result; /// Construct a `node` prefixed URI pub fn node(tt: &str) -> URI { ("node/".to_owned() + tt).into() } #[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 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, } impl Assets { fn new() -> Self { Self { inner: Default::default(), } } pub fn find>(&self, u: U) -> Option { 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 { 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 { 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()) }