#![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 std::{ collections::HashMap, fs::File, io::Read, path::{Path, PathBuf}, }; use toml; use walkdir::WalkDir; pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle}; pub use part::{ EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, Ship, System, Texture, }; mod syntax { use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, hash::Hash}; use crate::part::{faction, gun, outfit, ship, system, texture}; #[derive(Debug, Deserialize)] pub struct Root { pub gun: Option>, pub ship: Option>, pub system: Option>, pub outfit: Option>, pub texture: Option>, pub faction: 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, texture: None, faction: 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.texture, other.texture) .with_context(|| "while merging textures")?; merge_hashmap(&mut self.faction, other.faction) .with_context(|| "while merging factions")?; return Ok(()); } } } trait Build { type InputSyntax; /// Build a processed System struct from raw serde data fn build(root: Self::InputSyntax, ct: &mut Content) -> Result<()> where Self: Sized; } /// Represents static game content #[derive(Debug)] pub struct Content { /* Configuration values */ /// Root directory for textures texture_root: PathBuf, /// Name of starfield texture starfield_texture_name: String, /// Textures pub textures: Vec, texture_index: HashMap, /// The texture to use for starfield stars starfield_handle: Option, /// Outfits outfits: Vec, /// Ship guns guns: Vec, /// Ship bodies ships: Vec, /// Star systems pub systems: Vec, /// Factions factions: 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, 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 mut content = Self { systems: Vec::new(), ships: Vec::new(), guns: Vec::new(), outfits: Vec::new(), textures: Vec::new(), factions: Vec::new(), texture_index: HashMap::new(), starfield_handle: None, texture_root, starfield_texture_name, }; // Order here matters, usually if root.texture.is_some() { part::texture::Texture::build(root.texture.take().unwrap(), &mut content)?; } if root.ship.is_some() { part::ship::Ship::build(root.ship.take().unwrap(), &mut content)?; } if root.gun.is_some() { part::gun::Gun::build(root.gun.take().unwrap(), &mut content)?; } if root.outfit.is_some() { part::outfit::Outfit::build(root.outfit.take().unwrap(), &mut content)?; } if root.system.is_some() { part::system::System::build(root.system.take().unwrap(), &mut content)?; } if root.faction.is_some() { part::faction::Faction::build(root.faction.take().unwrap(), &mut content)?; } return Ok(content); } } // Access methods impl Content { /// Get the texture handle for the starfield texture pub fn get_starfield_handle(&self) -> TextureHandle { match self.starfield_handle { Some(h) => h, None => unreachable!("Starfield texture hasn't been loaded yet!"), } } /// Get a handle from a texture name pub fn get_texture_handle(&self, name: &str) -> TextureHandle { return match self.texture_index.get(name) { Some(s) => *s, None => unreachable!("get_texture_handle was called with a bad handle!"), }; } /// Get a texture from a handle pub fn get_texture(&self, h: TextureHandle) -> &Texture { // In theory, this could fail if h has a bad index, but that shouldn't ever happen. // The only TextureHandles that exist should be created by this crate. return &self.textures[h.index]; } /// 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 texture 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]; } }