2023-12-29 15:46:09 -08:00
|
|
|
#![warn(missing_docs)]
|
|
|
|
|
2024-02-05 18:29:05 -08:00
|
|
|
//! This subcrate is responsible for loading, parsing, validating game content.
|
2023-12-30 16:57:03 -08:00
|
|
|
mod part;
|
2024-01-20 15:18:12 -08:00
|
|
|
mod spriteautomaton;
|
2023-12-29 15:14:04 -08:00
|
|
|
mod util;
|
2023-12-24 23:03:00 -08:00
|
|
|
|
2024-01-10 22:44:22 -08:00
|
|
|
use anyhow::{bail, Context, Result};
|
2024-01-04 17:17:55 -08:00
|
|
|
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
|
2024-01-23 21:21:31 -08:00
|
|
|
use log::warn;
|
2024-02-05 18:29:05 -08:00
|
|
|
use rhai::ImmutableString;
|
|
|
|
use serde::{Deserialize, Deserializer};
|
|
|
|
use smartstring::{LazyCompact, SmartString};
|
2023-12-30 10:58:17 -08:00
|
|
|
use std::{
|
2024-02-08 20:37:32 -08:00
|
|
|
borrow::Borrow,
|
2023-12-30 10:58:17 -08:00
|
|
|
collections::HashMap,
|
2024-02-05 18:29:05 -08:00
|
|
|
fmt::Display,
|
2023-12-30 10:58:17 -08:00
|
|
|
fs::File,
|
|
|
|
io::Read,
|
2024-01-21 11:57:51 -08:00
|
|
|
num::NonZeroU32,
|
2023-12-30 10:58:17 -08:00
|
|
|
path::{Path, PathBuf},
|
2024-02-05 18:29:05 -08:00
|
|
|
sync::Arc,
|
2023-12-30 10:58:17 -08:00
|
|
|
};
|
2023-12-27 19:51:58 -08:00
|
|
|
use toml;
|
2023-12-25 09:01:12 -08:00
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
2024-01-05 18:04:30 -08:00
|
|
|
pub use part::*;
|
2024-01-20 15:18:12 -08:00
|
|
|
pub use spriteautomaton::*;
|
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
|
|
|
|
2024-01-10 22:44:22 -08:00
|
|
|
use crate::{
|
|
|
|
config,
|
|
|
|
part::{effect, faction, outfit, ship, sprite, system},
|
|
|
|
};
|
2023-12-27 19:51:58 -08:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
pub struct Root {
|
2023-12-27 20:13:39 -08:00
|
|
|
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>>,
|
2024-01-04 17:17:55 -08:00
|
|
|
pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>,
|
2023-12-30 16:57:03 -08:00
|
|
|
pub faction: Option<HashMap<String, faction::syntax::Faction>>,
|
2024-01-05 12:09:59 -08:00
|
|
|
pub effect: Option<HashMap<String, effect::syntax::Effect>>,
|
2024-01-10 22:44:22 -08:00
|
|
|
pub config: Option<config::syntax::Config>,
|
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 {
|
|
|
|
ship: None,
|
|
|
|
system: None,
|
2023-12-30 20:27:53 -08:00
|
|
|
outfit: None,
|
2024-01-04 17:17:55 -08:00
|
|
|
sprite: None,
|
2023-12-30 16:57:03 -08:00
|
|
|
faction: None,
|
2024-01-05 12:09:59 -08:00
|
|
|
effect: None,
|
2024-01-10 22:44:22 -08:00
|
|
|
config: 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.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")?;
|
2024-01-04 17:17:55 -08:00
|
|
|
merge_hashmap(&mut self.sprite, other.sprite)
|
|
|
|
.with_context(|| "while merging sprites")?;
|
2024-01-01 15:41:47 -08:00
|
|
|
merge_hashmap(&mut self.faction, other.faction)
|
|
|
|
.with_context(|| "while merging factions")?;
|
2024-01-05 12:09:59 -08:00
|
|
|
merge_hashmap(&mut self.effect, other.effect)
|
|
|
|
.with_context(|| "while merging effects")?;
|
2024-01-25 21:58:41 -08:00
|
|
|
|
2024-01-10 22:44:22 -08:00
|
|
|
if self.config.is_some() {
|
|
|
|
if other.config.is_some() {
|
|
|
|
bail!("invalid content dir, multiple config tables")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.config = other.config;
|
|
|
|
}
|
2023-12-30 10:58:17 -08:00
|
|
|
return Ok(());
|
|
|
|
}
|
2023-12-27 19:51:58 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trait Build {
|
2024-01-04 17:17:55 -08:00
|
|
|
type InputSyntaxType;
|
2023-12-30 10:58:17 -08:00
|
|
|
|
2023-12-27 19:51:58 -08:00
|
|
|
/// Build a processed System struct from raw serde data
|
2024-01-05 12:09:59 -08:00
|
|
|
fn build(
|
|
|
|
root: Self::InputSyntaxType,
|
|
|
|
build_context: &mut ContentBuildContext,
|
|
|
|
content: &mut Content,
|
|
|
|
) -> Result<()>
|
2023-12-27 19:51:58 -08:00
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
}
|
|
|
|
|
2024-02-05 18:29:05 -08:00
|
|
|
/// 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<SmartString<LazyCompact>>);
|
|
|
|
|
|
|
|
impl From<ImmutableString> for ContentIndex {
|
|
|
|
fn from(value: ImmutableString) -> Self {
|
2024-02-08 20:37:32 -08:00
|
|
|
Self(Arc::new(value.into()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ContentIndex> 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<LazyCompact> = value.0.borrow();
|
|
|
|
ImmutableString::from(x.clone())
|
2024-02-05 18:29:05 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-05 12:09:59 -08:00
|
|
|
/// Stores temporary data while building context objects
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub(crate) struct ContentBuildContext {
|
2024-02-07 15:40:43 -08:00
|
|
|
pub effect_index: HashMap<ContentIndex, Arc<Effect>>,
|
2024-01-05 12:09:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ContentBuildContext {
|
|
|
|
fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
effect_index: HashMap::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-30 17:39:19 -08:00
|
|
|
/// Represents static game content
|
2024-02-02 16:47:16 -08:00
|
|
|
#[derive(Debug)]
|
2023-12-27 19:51:58 -08:00
|
|
|
pub struct Content {
|
2024-01-04 17:17:55 -08:00
|
|
|
/// Keeps track of which images are in which texture
|
|
|
|
sprite_atlas: SpriteAtlas,
|
2023-12-30 10:58:17 -08:00
|
|
|
|
2024-02-05 18:29:05 -08:00
|
|
|
/// Sprites defined in this game
|
|
|
|
pub sprites: HashMap<ContentIndex, Arc<Sprite>>,
|
|
|
|
|
|
|
|
/// Outfits defined in this game
|
|
|
|
pub outfits: HashMap<ContentIndex, Arc<Outfit>>,
|
|
|
|
|
|
|
|
/// Ships defined in this game
|
|
|
|
pub ships: HashMap<ContentIndex, Arc<Ship>>,
|
|
|
|
|
|
|
|
/// Systems defined in this game
|
|
|
|
pub systems: HashMap<ContentIndex, Arc<System>>,
|
|
|
|
|
|
|
|
/// Factions defined in this game
|
|
|
|
pub factions: HashMap<ContentIndex, Arc<Faction>>,
|
|
|
|
|
|
|
|
/// Effects defined in this game
|
|
|
|
pub effects: HashMap<ContentIndex, Arc<Effect>>,
|
|
|
|
|
|
|
|
/// Game configuration
|
|
|
|
pub config: Config,
|
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.
|
2024-02-02 16:45:23 -08:00
|
|
|
pub fn load_dir(
|
|
|
|
content_root: PathBuf,
|
|
|
|
asset_root: PathBuf,
|
|
|
|
atlas_index: PathBuf,
|
|
|
|
) -> Result<Self> {
|
2023-12-30 10:58:17 -08:00
|
|
|
let mut root = syntax::Root::new();
|
2023-12-27 19:51:58 -08:00
|
|
|
|
2024-02-02 16:45:23 -08:00
|
|
|
for e in WalkDir::new(&content_root)
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|e| e.ok())
|
|
|
|
{
|
2023-12-27 19:51:58 -08:00
|
|
|
if e.metadata().unwrap().is_file() {
|
|
|
|
// TODO: better warnings
|
|
|
|
match e.path().extension() {
|
|
|
|
Some(t) => {
|
|
|
|
if t.to_str() != Some("toml") {
|
2024-01-23 21:21:31 -08:00
|
|
|
warn!("{e:#?} is not a toml file, skipping");
|
2023-12-27 19:51:58 -08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
2024-01-23 21:21:31 -08:00
|
|
|
warn!("{e:#?} is not a toml file, skipping");
|
2023-12-27 19:51:58 -08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let path = e.path();
|
2023-12-30 10:58:17 -08:00
|
|
|
let this_root = Self::try_parse(path)
|
2024-01-23 21:21:31 -08:00
|
|
|
.with_context(|| format!("could not read {}", path.display()))?;
|
2023-12-30 10:58:17 -08:00
|
|
|
|
|
|
|
root.merge(this_root)
|
2024-01-23 21:21:31 -08:00
|
|
|
.with_context(|| format!("could not parse {}", path.display()))?;
|
2023-12-25 09:01:12 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-04 17:17:55 -08:00
|
|
|
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)?
|
|
|
|
};
|
|
|
|
|
2024-01-05 12:09:59 -08:00
|
|
|
let mut build_context = ContentBuildContext::new();
|
|
|
|
|
2023-12-30 10:58:17 -08:00
|
|
|
let mut content = Self {
|
2024-01-10 22:44:22 -08:00
|
|
|
config: {
|
|
|
|
if let Some(c) = root.config {
|
2024-02-02 16:45:23 -08:00
|
|
|
c.build(&asset_root, &content_root, &atlas)
|
2024-01-10 22:44:22 -08:00
|
|
|
.with_context(|| "while parsing config table")?
|
|
|
|
} else {
|
|
|
|
bail!("failed loading content: no config table specified")
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2024-01-04 17:17:55 -08:00
|
|
|
sprite_atlas: atlas,
|
2024-02-05 18:29:05 -08:00
|
|
|
systems: HashMap::new(),
|
|
|
|
ships: HashMap::new(),
|
|
|
|
outfits: HashMap::new(),
|
|
|
|
sprites: HashMap::new(),
|
|
|
|
factions: HashMap::new(),
|
|
|
|
effects: HashMap::new(),
|
2023-12-30 10:58:17 -08:00
|
|
|
};
|
|
|
|
|
2024-01-05 12:09:59 -08:00
|
|
|
// TODO: enforce sprite and image limits
|
|
|
|
|
|
|
|
// Order matters. Some content types require another to be fully initialized
|
2024-01-04 17:17:55 -08:00
|
|
|
if root.sprite.is_some() {
|
2024-01-05 12:09:59 -08:00
|
|
|
part::sprite::Sprite::build(
|
|
|
|
root.sprite.take().unwrap(),
|
|
|
|
&mut build_context,
|
|
|
|
&mut content,
|
|
|
|
)?;
|
|
|
|
}
|
2024-01-25 21:58:41 -08:00
|
|
|
|
2024-01-05 12:09:59 -08:00
|
|
|
if root.effect.is_some() {
|
|
|
|
part::effect::Effect::build(
|
|
|
|
root.effect.take().unwrap(),
|
|
|
|
&mut build_context,
|
|
|
|
&mut content,
|
|
|
|
)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
2024-01-04 17:17:55 -08:00
|
|
|
|
2024-01-05 12:09:59 -08:00
|
|
|
// Order below this line does not matter
|
2023-12-30 10:58:17 -08:00
|
|
|
if root.ship.is_some() {
|
2024-01-05 12:09:59 -08:00
|
|
|
part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
2023-12-30 20:27:53 -08:00
|
|
|
if root.outfit.is_some() {
|
2024-01-05 12:09:59 -08:00
|
|
|
part::outfit::Outfit::build(
|
|
|
|
root.outfit.take().unwrap(),
|
|
|
|
&mut build_context,
|
|
|
|
&mut content,
|
|
|
|
)?;
|
2023-12-30 10:58:17 -08:00
|
|
|
}
|
|
|
|
if root.system.is_some() {
|
2024-01-05 12:09:59 -08:00
|
|
|
part::system::System::build(
|
|
|
|
root.system.take().unwrap(),
|
|
|
|
&mut build_context,
|
|
|
|
&mut content,
|
|
|
|
)?;
|
2023-12-30 16:57:03 -08:00
|
|
|
}
|
|
|
|
if root.faction.is_some() {
|
2024-01-05 12:09:59 -08:00
|
|
|
part::faction::Faction::build(
|
|
|
|
root.faction.take().unwrap(),
|
|
|
|
&mut build_context,
|
|
|
|
&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 {
|
2024-01-04 22:17:34 -08:00
|
|
|
/// Get the list of atlas files we may use
|
|
|
|
pub fn atlas_files(&self) -> &Vec<String> {
|
|
|
|
return &self.sprite_atlas.atlas_list;
|
|
|
|
}
|
|
|
|
|
2024-01-22 08:49:42 -08:00
|
|
|
/// Get sprite atlas metadata
|
|
|
|
pub fn get_atlas(&self) -> &SpriteAtlas {
|
|
|
|
return &self.sprite_atlas;
|
|
|
|
}
|
|
|
|
|
2024-01-20 09:36:12 -08:00
|
|
|
/// Get a texture by its index
|
2024-01-21 11:57:51 -08:00
|
|
|
pub fn get_image(&self, idx: NonZeroU32) -> &SpriteAtlasImage {
|
|
|
|
&self.sprite_atlas.get_by_idx(idx)
|
2024-01-01 15:41:47 -08:00
|
|
|
}
|
|
|
|
}
|