#![warn(missing_docs)] //! This subcrate is responsible for loading, parsing, validating game content, //! which is usually stored in `./content`. mod handle; mod part; mod util; use anyhow::{Context, Result}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage}; use std::{ collections::HashMap, fs::File, io::Read, path::{Path, PathBuf}, }; use toml; use walkdir::WalkDir; pub use handle::{ EffectHandle, FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle, }; pub use part::*; mod syntax { use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, hash::Hash}; use crate::part::{effect, faction, gun, outfit, ship, sprite, system}; #[derive(Debug, Deserialize)] pub struct Root { pub gun: Option>, pub ship: Option>, pub system: Option>, pub outfit: Option>, pub sprite: Option>, pub faction: Option>, pub effect: 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 { gun: None, ship: None, system: None, outfit: None, sprite: None, faction: None, effect: None, } } pub fn merge(&mut self, other: Root) -> Result<()> { merge_hashmap(&mut self.gun, other.gun).with_context(|| "while merging guns")?; 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")?; 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 { pub effect_index: HashMap, } impl ContentBuildContext { fn new() -> Self { Self { effect_index: HashMap::new(), } } } /// Represents static game content #[derive(Debug)] pub struct Content { /* Configuration values */ /// Root directory for image image_root: PathBuf, /// Name of starfield sprite starfield_sprite_name: String, /// 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, /// The texture to use for starfield stars starfield_handle: Option, /// 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, } // 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, texture_root: PathBuf, atlas_index: PathBuf, starfield_texture_name: String, ) -> 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 { 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(), starfield_handle: None, image_root: texture_root, starfield_sprite_name: starfield_texture_name, }; // 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.gun.is_some() { part::gun::Gun::build(root.gun.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 handle for the starfield sprite pub fn get_starfield_handle(&self) -> SpriteHandle { match self.starfield_handle { Some(h) => h, None => unreachable!("Starfield sprite hasn't been loaded yet!"), } } /// 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 a sprite from a path pub fn get_image(&self, p: &Path) -> &SpriteAtlasImage { self.sprite_atlas.index.get(p).unwrap() } /// 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 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]; } }