#![warn(missing_docs)] //! This subcrate is responsible for loading, parsing, validating game content. mod part; mod spriteautomaton; mod util; use anyhow::{bail, Context, Result}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use log::warn; use rhai::ImmutableString; use serde::{Deserialize, Deserializer}; use smartstring::{LazyCompact, SmartString}; use std::{ borrow::Borrow, collections::HashMap, fmt::Display, fs::File, io::Read, num::NonZeroU32, path::{Path, PathBuf}, sync::Arc, }; use toml; use walkdir::WalkDir; pub use part::*; pub use spriteautomaton::*; mod syntax { use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, hash::Hash}; use crate::{ config, part::{effect, faction, outfit, ship, sprite, system}, }; #[derive(Debug, Deserialize)] pub struct Root { pub ship: Option>, pub system: Option>, pub outfit: Option>, pub sprite: Option>, pub faction: Option>, pub effect: Option>, pub config: Option, } fn merge_hashmap( to: &mut Option>, mut from: Option>, ) -> Result<()> where K: Hash + Eq + Display, { if to.is_none() { *to = from.take(); return Ok(()); } if let Some(from_inner) = from { let to_inner = to.as_mut().unwrap(); for (k, v) in from_inner { if to_inner.contains_key(&k) { bail!("Found duplicate key `{k}`"); } else { to_inner.insert(k, v); } } } return Ok(()); } impl Root { pub fn new() -> Self { Self { ship: None, system: None, outfit: None, sprite: None, faction: None, effect: None, config: None, } } pub fn merge(&mut self, other: Root) -> Result<()> { merge_hashmap(&mut self.ship, other.ship).with_context(|| "while merging ships")?; merge_hashmap(&mut self.system, other.system) .with_context(|| "while merging systems")?; merge_hashmap(&mut self.outfit, other.outfit) .with_context(|| "while merging outfits")?; merge_hashmap(&mut self.sprite, other.sprite) .with_context(|| "while merging sprites")?; merge_hashmap(&mut self.faction, other.faction) .with_context(|| "while merging factions")?; merge_hashmap(&mut self.effect, other.effect) .with_context(|| "while merging effects")?; if self.config.is_some() { if other.config.is_some() { bail!("invalid content dir, multiple config tables") } } else { self.config = other.config; } return Ok(()); } } } trait Build { type InputSyntaxType; /// Build a processed System struct from raw serde data fn build( root: Self::InputSyntaxType, build_context: &mut ContentBuildContext, content: &mut Content, ) -> Result<()> where Self: Sized; } /// The type we use to index content objects /// These are only unique WITHIN each "type" of object! #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct ContentIndex(Arc>); impl From for ContentIndex { fn from(value: ImmutableString) -> Self { Self(Arc::new(value.into())) } } impl From for ImmutableString { fn from(value: ContentIndex) -> Self { ImmutableString::from(Arc::into_inner(value.0).unwrap()) } } impl From<&ContentIndex> for ImmutableString { fn from(value: &ContentIndex) -> Self { let x: &SmartString = value.0.borrow(); ImmutableString::from(x.clone()) } } impl ContentIndex { /// Make a new ContentIndex from a strign pub fn new(s: &str) -> Self { Self(SmartString::from(s).into()) } /// Get a &str to this index's content pub fn as_str(&self) -> &str { self.0.as_str() } } impl<'d> Deserialize<'d> for ContentIndex { fn deserialize(deserializer: D) -> Result where D: Deserializer<'d>, { let s = String::deserialize(deserializer)?; Ok(Self::new(&s)) } } impl Display for ContentIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) } } /// Stores temporary data while building context objects #[derive(Debug)] pub(crate) struct ContentBuildContext { pub effect_index: HashMap>, } impl ContentBuildContext { fn new() -> Self { Self { effect_index: HashMap::new(), } } } /// Represents static game content #[derive(Debug)] pub struct Content { /// Keeps track of which images are in which texture sprite_atlas: SpriteAtlas, /// Sprites defined in this game pub sprites: HashMap>, /// Outfits defined in this game pub outfits: HashMap>, /// Ships defined in this game pub ships: HashMap>, /// Systems defined in this game pub systems: HashMap>, /// Factions defined in this game pub factions: HashMap>, /// Effects defined in this game pub effects: HashMap>, /// Game configuration pub config: Config, } // Loading methods impl Content { fn try_parse(path: &Path) -> Result { let mut file_string = String::new(); let _ = File::open(path)?.read_to_string(&mut file_string); let file_string = file_string.trim(); return Ok(toml::from_str(&file_string)?); } /// Load content from a directory. pub fn load_dir( content_root: PathBuf, asset_root: PathBuf, atlas_index: PathBuf, ) -> Result { let mut root = syntax::Root::new(); for e in WalkDir::new(&content_root) .into_iter() .filter_map(|e| e.ok()) { if e.metadata().unwrap().is_file() { // TODO: better warnings match e.path().extension() { Some(t) => { if t.to_str() != Some("toml") { warn!("{e:#?} is not a toml file, skipping"); continue; } } None => { warn!("{e:#?} is not a toml file, skipping"); continue; } } let path = e.path(); let this_root = Self::try_parse(path) .with_context(|| format!("could not read {}", path.display()))?; root.merge(this_root) .with_context(|| format!("could not parse {}", path.display()))?; } } let atlas: SpriteAtlas = { let mut file_string = String::new(); let _ = File::open(atlas_index)?.read_to_string(&mut file_string); let file_string = file_string.trim(); toml::from_str(&file_string)? }; let mut build_context = ContentBuildContext::new(); let mut content = Self { config: { if let Some(c) = root.config { c.build(&asset_root, &content_root, &atlas) .with_context(|| "while parsing config table")? } else { bail!("failed loading content: no config table specified") } }, sprite_atlas: atlas, systems: HashMap::new(), ships: HashMap::new(), outfits: HashMap::new(), sprites: HashMap::new(), factions: HashMap::new(), effects: HashMap::new(), }; // TODO: enforce sprite and image limits // Order matters. Some content types require another to be fully initialized if root.sprite.is_some() { part::sprite::Sprite::build( root.sprite.take().unwrap(), &mut build_context, &mut content, )?; } if root.effect.is_some() { part::effect::Effect::build( root.effect.take().unwrap(), &mut build_context, &mut content, )?; } // Order below this line does not matter if root.ship.is_some() { part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?; } if root.outfit.is_some() { part::outfit::Outfit::build( root.outfit.take().unwrap(), &mut build_context, &mut content, )?; } if root.system.is_some() { part::system::System::build( root.system.take().unwrap(), &mut build_context, &mut content, )?; } if root.faction.is_some() { part::faction::Faction::build( root.faction.take().unwrap(), &mut build_context, &mut content, )?; } return Ok(content); } } // Access methods impl Content { /// Get the list of atlas files we may use pub fn atlas_files(&self) -> &Vec { return &self.sprite_atlas.atlas_list; } /// Get sprite atlas metadata pub fn get_atlas(&self) -> &SpriteAtlas { return &self.sprite_atlas; } /// Get a texture by its index pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage { &self.sprite_atlas.get_by_idx(idx) } }