#![warn(missing_docs)] //! This subcrate is responsible for loading, parsing, validating game content, //! which is usually stored in `./content`. mod handle; mod part; mod spriteautomaton; mod util; use anyhow::{bail, Context, Result}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use std::{ collections::HashMap, fs::File, io::Read, num::NonZeroU32, path::{Path, PathBuf}, }; use toml; use walkdir::WalkDir; pub use handle::*; 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; } /// Stores temporary data while building context objects #[derive(Debug)] pub(crate) struct ContentBuildContext { /// Map effect names to handles pub effect_index: HashMap, /// Maps sprite handles to a map of section name -> section index pub sprite_section_index: HashMap>, } impl ContentBuildContext { fn new() -> Self { Self { effect_index: HashMap::new(), sprite_section_index: HashMap::new(), } } } /// Represents static game content #[derive(Debug, Clone)] pub struct Content { /// Sprites pub sprites: Vec, /// Map strings to texture names. /// This is only necessary because we need to hard-code a few texture names for UI elements. sprite_index: HashMap, /// Keeps track of which images are in which texture sprite_atlas: SpriteAtlas, outfits: Vec, guns: Vec, // TODO: merge with outfit ships: Vec, systems: Vec, factions: Vec, effects: Vec, 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(path: PathBuf, asset_root: PathBuf, atlas_index: PathBuf) -> Result { let mut root = syntax::Root::new(); for e in WalkDir::new(path).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") { println!("[WARNING] {e:#?} is not a toml file, skipping."); continue; } } None => { println!("[WARNING] {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, &atlas) .with_context(|| "while parsing config table")? } else { bail!("failed loading content: no config table specified") } }, sprite_atlas: atlas, systems: Vec::new(), ships: Vec::new(), guns: Vec::new(), outfits: Vec::new(), sprites: Vec::new(), factions: Vec::new(), effects: Vec::new(), sprite_index: 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 { /// Iterate over all valid system handles pub fn iter_systems(&self) -> impl Iterator { (0..self.systems.len()).map(|x| SystemHandle { index: x }) } /// Get a handle from a sprite name pub fn get_sprite_handle(&self, name: &str) -> SpriteHandle { return match self.sprite_index.get(name) { Some(s) => *s, None => unreachable!("get_sprite_handle was called with a bad name!"), }; } /// Get a sprite from a handle pub fn get_sprite(&self, h: SpriteHandle) -> &Sprite { // In theory, this could fail if h has a bad index, but that shouldn't ever happen. // The only handles that exist should be created by this crate. return &self.sprites[h.index as usize]; } /// 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) } /// Get an outfit from a handle pub fn get_outfit(&self, h: OutfitHandle) -> &Outfit { return &self.outfits[h.index]; } /// Get a gun from a handle pub fn get_gun(&self, h: GunHandle) -> &Gun { return &self.guns[h.index]; } /// Get a ship from a handle pub fn get_ship(&self, h: ShipHandle) -> &Ship { return &self.ships[h.index]; } /// Get a system from a handle pub fn get_system(&self, h: SystemHandle) -> &System { return &self.systems[h.index]; } /// Get a system object from a handle pub fn get_system_object(&self, h: SystemObjectHandle) -> &SystemObject { return &self.get_system(h.system_handle).objects[h.body_index]; } /// Get a faction from a handle pub fn get_faction(&self, h: FactionHandle) -> &Faction { return &self.factions[h.index]; } /// Get an effect from a handle pub fn get_effect(&self, h: EffectHandle) -> &Effect { return &self.effects[h.index]; } /// Get content configuration pub fn get_config(&self) -> &Config { return &self.config; } }