use futures::stream::FuturesUnordered; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::io::Write; use std::path::PathBuf; use std::{collections::HashMap, fs::File, io::ErrorKind, path::Path}; use tokio_stream::StreamExt; #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] pub struct Config { pub name: String, #[serde(default = "default_version")] pub version: String, #[serde(default)] pub authors: Vec, pub script: String, pub available_cards: HashMap, pub default_back: Option, pub piles: HashMap, pub player_piles: HashMap, } fn default_version() -> String { "0.0.0".into() } #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] pub struct Card { pub image: PathBuf, pub back_image: Option, #[serde(flatten)] pub other: HashMap, } #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] pub struct Pile { pub name: String, #[serde(default)] pub cards: Vec, #[serde(default)] pub face_down: bool, #[serde(default = "default_visible")] pub visible: bool, #[serde(flatten)] pub other: HashMap, } fn default_visible() -> bool { true } impl Config { pub fn load + std::fmt::Debug>(file: P) -> Self { let s: Config = serde_json::from_reader(std::fs::File::open(&file).unwrap()) .map_err(|e| { log::error!( "Malformed game defintion file @ {}", file.as_ref().display() ); log::error!("JSON Error: {}", e); panic!() }) .unwrap(); if s.default_back.is_none() { for (name, card) in &s.available_cards { if card.back_image.is_none() { panic!("Card {} from game {} can not have a default back if there's not default back", name, s.name) } } } s.start_image_caching(file.as_ref().parent().unwrap().to_path_buf()); s } fn start_image_caching(&self, folder: PathBuf) { let mut join_handles = FuturesUnordered::new(); if let Some(p) = &self.default_back { let p = p.clone(); let folder = folder.clone(); join_handles.push(tokio::task::spawn_blocking(|| { cache_image(&p, folder); p })); } for Card { image, back_image, other: _, } in self.available_cards.values() { { let folder = folder.clone(); let p = image.clone(); join_handles.push(tokio::task::spawn_blocking(|| { cache_image(&p, folder); p })); }; if let Some(back_image) = back_image { let p = back_image.clone(); let folder = folder.clone(); join_handles.push(tokio::task::spawn_blocking(|| { cache_image(&p, folder); p })); } } tokio::spawn(async move { let l = join_handles.len(); let mut i = 1; while let Some(r) = join_handles.next().await { match r { Ok(p) => log::info!("[{}/{}] Image {} cached!", i, l, p.display()), Err(e) => log::error!("[{}/{}] Error chaching image: {}", i, l, e), } i += 1; } }); } pub fn reload_cache(&self, folder: PathBuf) { std::fs::remove_dir_all(folder.join(".cache")).unwrap(); self.start_image_caching(folder); } } fn cache_image, P2: AsRef>(p: P1, folder: P2) { let original = folder.as_ref().join(p.as_ref()); let cache_folder = folder.as_ref().join(".cache"); let mut cached = cache_folder.join(p); // log::info!("Caching {} on {}", original.display(), cached.display()); // log::info!("Creating {}", cache_folder.display()); match std::fs::create_dir(cache_folder) { Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), // Ignore if folder already exists x => x, } .unwrap(); cached.set_extension("png"); if cached.exists() && cached.metadata().unwrap().modified().unwrap() > original.metadata().unwrap().modified().unwrap() { // Cache is updated, do nothing log::info!("cache for {} is up to date", original.display()); } else { // Update cache // log::info!("Updating cache for: {}", original.display()); let mut face_buf = Vec::new(); image::open(&original) .unwrap_or_else(|e| panic!("Error loading the image in {:?} ({})", original, e)) .write_to(&mut face_buf, image::ImageOutputFormat::Png) .unwrap(); match std::fs::create_dir_all(cached.parent().unwrap()) { Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), // Ignore if folder already exists x => x, } .unwrap(); File::create(cached).unwrap().write_all(&face_buf).unwrap(); log::info!("Updated cache for: {}", original.display()); } } pub fn setup() { #[cfg(debug_assertions)] { if let Ok(e) = std::env::var("CARGO_MANIFEST_DIR") { std::fs::write( AsRef::::as_ref(&e) .join("schema") .join("game-config.json"), serde_json::to_string_pretty(&schemars::schema_for!(Config)).unwrap(), ) .unwrap() } } }