2023-12-29 15:46:09 -08:00
|
|
|
#![warn(missing_docs)]
|
|
|
|
|
|
|
|
//! This subcrate is responsible for loading, parsing, validating game content,
|
|
|
|
//! which is usually stored in `./content`.
|
|
|
|
|
2023-12-30 16:57:03 -08:00
|
|
|
mod handle;
|
|
|
|
mod part;
|
2023-12-29 15:14:04 -08:00
|
|
|
mod util;
|
2023-12-24 23:03:00 -08:00
|
|
|
|
2023-12-30 16:57:03 -08:00
|
|
|
use anyhow::{Context, Result};
|
2023-12-30 10:58:17 -08:00
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
fs::File,
|
|
|
|
io::Read,
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
};
|
2023-12-27 19:51:58 -08:00
|
|
|
use toml;
|
2023-12-25 09:01:12 -08:00
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
2024-01-01 15:41:47 -08:00
|
|
|
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle};
|
2023-12-30 21:05:06 -08:00
|
|
|
pub use part::{
|
2024-01-01 15:41:47 -08:00
|
|
|
EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, Ship,
|
|
|
|
System, Texture,
|
2023-12-30 21:05:06 -08:00
|
|
|
};
|
2023-12-30 16:57:03 -08:00
|
|
|
|
2023-12-27 19:51:58 -08:00
|
|
|
mod syntax {
|
2024-01-01 15:41:47 -08:00
|
|
|
use anyhow::{bail, Context, Result};
|
2023-12-27 19:51:58 -08:00
|
|
|
use serde::Deserialize;
|
2024-01-01 15:41:47 -08:00
|
|
|
use std::{collections::HashMap, fmt::Display, hash::Hash};
|
2023-12-30 16:57:03 -08:00
|
|
|
|
2023-12-30 20:27:53 -08:00
|
|
|
use crate::part::{faction, gun, outfit, ship, system, texture};
|
2023-12-27 19:51:58 -08:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
pub struct Root {
|
2023-12-27 20:13:39 -08:00
|
|
|
pub gun: Option<HashMap<String, gun::syntax::Gun>>,
|
|
|
|
pub ship: Option<HashMap<String, ship::syntax::Ship>>,
|
|
|
|
pub system: Option<HashMap<String, system::syntax::System>>,
|
2023-12-30 20:27:53 -08:00
|
|
|
pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>,
|
2023-12-30 10:58:17 -08:00
|
|
|
pub texture: Option<HashMap<String, texture::syntax::Texture>>,
|
2023-12-30 16:57:03 -08:00
|
|
|
pub faction: Option<HashMap<String, faction::syntax::Faction>>,
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
|
2024-01-01 15:41:47 -08:00
|
|
|
fn merge_hashmap<K, V>(
|
|
|
|
to: &mut Option<HashMap<K, V>>,
|
|
|
|
mut from: Option<HashMap<K, V>>,
|
|
|
|
) -> 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(());
|
|
|
|
}
|
|
|
|
|
2023-12-30 10:58:17 -08:00
|
|
|
impl Root {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
gun: None,
|
|
|
|
ship: None,
|
|
|
|
system: None,
|
2023-12-30 20:27:53 -08:00
|
|
|
outfit: None,
|
2023-12-30 10:58:17 -08:00
|
|
|
texture: None,
|
2023-12-30 16:57:03 -08:00
|
|
|
faction: None,
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn merge(&mut self, other: Root) -> Result<()> {
|
2024-01-01 15:41:47 -08:00
|
|
|
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")?;
|
2023-12-30 10:58:17 -08:00
|
|
|
return Ok(());
|
|
|
|
}
|
2023-12-27 19:51:58 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trait Build {
|
2023-12-30 10:58:17 -08:00
|
|
|
type InputSyntax;
|
|
|
|
|
2023-12-27 19:51:58 -08:00
|
|
|
/// Build a processed System struct from raw serde data
|
2023-12-30 10:58:17 -08:00
|
|
|
fn build(root: Self::InputSyntax, ct: &mut Content) -> Result<()>
|
2023-12-27 19:51:58 -08:00
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
}
|
|
|
|
|
2023-12-30 17:39:19 -08:00
|
|
|
/// Represents static game content
|
2023-12-27 19:51:58 -08:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Content {
|
2024-01-01 15:41:47 -08:00
|
|
|
/* Configuration values */
|
|
|
|
/// Root directory for textures
|
|
|
|
texture_root: PathBuf,
|
|
|
|
/// Name of starfield texture
|
|
|
|
starfield_texture_name: String,
|
2023-12-27 19:51:58 -08:00
|
|
|
|
2023-12-30 10:58:17 -08:00
|
|
|
/// Textures
|
2023-12-30 16:57:03 -08:00
|
|
|
pub textures: Vec<part::texture::Texture>,
|
2024-01-01 15:46:39 -08:00
|
|
|
/// Map strings to texture names.
|
|
|
|
/// This is only necessary because we need to hard-code a few texture names for UI elements.
|
2023-12-30 16:57:03 -08:00
|
|
|
texture_index: HashMap<String, handle::TextureHandle>,
|
2023-12-30 10:58:17 -08:00
|
|
|
/// The texture to use for starfield stars
|
2023-12-30 16:57:03 -08:00
|
|
|
starfield_handle: Option<handle::TextureHandle>,
|
2023-12-30 10:58:17 -08:00
|
|
|
|
2024-01-01 15:41:47 -08:00
|
|
|
/// Outfits
|
|
|
|
outfits: Vec<part::outfit::Outfit>,
|
2023-12-30 10:58:17 -08:00
|
|
|
|
2024-01-01 15:41:47 -08:00
|
|
|
/// Ship guns
|
|
|
|
guns: Vec<part::gun::Gun>,
|
|
|
|
|
|
|
|
/// Ship bodies
|
|
|
|
ships: Vec<part::ship::Ship>,
|
|
|
|
|
|
|
|
/// Star systems
|
2024-01-01 15:46:39 -08:00
|
|
|
systems: Vec<part::system::System>,
|
2024-01-01 15:41:47 -08:00
|
|
|
|
|
|
|
/// Factions
|
|
|
|
factions: Vec<part::faction::Faction>,
|
2023-12-27 20:13:39 -08:00
|
|
|
}
|
2023-12-27 19:51:58 -08:00
|
|
|
|
2024-01-01 15:41:47 -08:00
|
|
|
// Loading methods
|
2023-12-27 20:13:39 -08:00
|
|
|
impl Content {
|
|
|
|
fn try_parse(path: &Path) -> Result<syntax::Root> {
|
|
|
|
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)?);
|
|
|
|
}
|
2023-12-25 09:01:12 -08:00
|
|
|
|
2023-12-29 15:46:09 -08:00
|
|
|
/// Load content from a directory.
|
2023-12-30 10:58:17 -08:00
|
|
|
pub fn load_dir(
|
|
|
|
path: PathBuf,
|
|
|
|
texture_root: PathBuf,
|
|
|
|
starfield_texture_name: String,
|
|
|
|
) -> Result<Self> {
|
|
|
|
let mut root = syntax::Root::new();
|
2023-12-27 19:51:58 -08:00
|
|
|
|
|
|
|
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();
|
2023-12-30 10:58:17 -08:00
|
|
|
let this_root = Self::try_parse(path)
|
|
|
|
.with_context(|| format!("Could not read {}", path.display()))?;
|
|
|
|
|
|
|
|
root.merge(this_root)
|
2023-12-27 19:51:58 -08:00
|
|
|
.with_context(|| format!("Could not parse {}", path.display()))?;
|
2023-12-25 09:01:12 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-30 10:58:17 -08:00
|
|
|
let mut content = Self {
|
|
|
|
systems: Vec::new(),
|
|
|
|
ships: Vec::new(),
|
|
|
|
guns: Vec::new(),
|
2023-12-30 20:27:53 -08:00
|
|
|
outfits: Vec::new(),
|
2023-12-30 10:58:17 -08:00
|
|
|
textures: Vec::new(),
|
2023-12-30 16:57:03 -08:00
|
|
|
factions: Vec::new(),
|
2023-12-30 10:58:17 -08:00
|
|
|
texture_index: HashMap::new(),
|
|
|
|
starfield_handle: None,
|
|
|
|
texture_root,
|
|
|
|
starfield_texture_name,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Order here matters, usually
|
|
|
|
if root.texture.is_some() {
|
2023-12-30 16:57:03 -08:00
|
|
|
part::texture::Texture::build(root.texture.take().unwrap(), &mut content)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
if root.ship.is_some() {
|
2023-12-30 16:57:03 -08:00
|
|
|
part::ship::Ship::build(root.ship.take().unwrap(), &mut content)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
if root.gun.is_some() {
|
2023-12-30 16:57:03 -08:00
|
|
|
part::gun::Gun::build(root.gun.take().unwrap(), &mut content)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
2023-12-30 20:27:53 -08:00
|
|
|
if root.outfit.is_some() {
|
|
|
|
part::outfit::Outfit::build(root.outfit.take().unwrap(), &mut content)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
if root.system.is_some() {
|
2023-12-30 16:57:03 -08:00
|
|
|
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)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
|
2023-12-27 19:51:58 -08:00
|
|
|
return Ok(content);
|
|
|
|
}
|
2023-12-25 09:01:12 -08:00
|
|
|
}
|
2024-01-01 15:41:47 -08:00
|
|
|
|
|
|
|
// 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];
|
|
|
|
}
|
|
|
|
}
|