use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
struct SpriteManifest {
#[allow(dead_code)]
version: u32,
sheets: Vec<SpriteManifestEntry>,
}
#[derive(Debug, Clone, Deserialize)]
struct SpriteManifestEntry {
#[allow(dead_code)]
id: String,
file: String,
}
#[derive(Debug, Clone, Deserialize)]
struct SpriteSheetDef {
id: String,
modes: Vec<SpriteModeDef>,
}
#[derive(Debug, Clone, Deserialize)]
struct SpriteModeDef {
id: String,
}
#[derive(Debug, Clone, Default)]
pub struct SpriteSheetModes {
pub sheet_id: String,
pub mode_ids: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct SpriteCatalog {
sheets: HashMap<String, SpriteSheetModes>,
}
impl SpriteCatalog {
pub fn mode_ids(&self, sheet_id: &str) -> &[String] {
self.sheets
.get(sheet_id)
.map(|s| s.mode_ids.as_slice())
.unwrap_or(&[])
}
pub fn sheet_ids(&self) -> impl Iterator<Item = &String> {
self.sheets.keys()
}
pub fn len(&self) -> usize {
self.sheets.len()
}
pub fn is_empty(&self) -> bool {
self.sheets.is_empty()
}
pub fn register_sheet_modes(&mut self, sheet_id: impl Into<String>, mode_ids: Vec<String>) {
let sheet_id = sheet_id.into();
if mode_ids.is_empty() {
return;
}
self.sheets.insert(
sheet_id.clone(),
SpriteSheetModes {
sheet_id,
mode_ids,
},
);
}
}
pub fn load_sprite_catalog(dir: &Path) -> anyhow::Result<SpriteCatalog> {
let manifest_path = dir.join("manifest.yaml");
if !manifest_path.is_file() {
return Ok(SpriteCatalog::default());
}
let text = std::fs::read_to_string(&manifest_path)?;
let manifest: SpriteManifest = serde_yaml::from_str(&text)?;
let mut sheets = HashMap::new();
for entry in &manifest.sheets {
let sheet_path = dir.join(&entry.file);
let sheet_text = match std::fs::read_to_string(&sheet_path) {
Ok(t) => t,
Err(_) => continue,
};
let def: SpriteSheetDef = match serde_yaml::from_str(&sheet_text) {
Ok(d) => d,
Err(_) => continue,
};
let mode_ids: Vec<String> = def.modes.into_iter().map(|m| m.id).collect();
if mode_ids.is_empty() {
continue;
}
sheets.insert(
def.id.clone(),
SpriteSheetModes {
sheet_id: def.id,
mode_ids,
},
);
}
Ok(SpriteCatalog { sheets })
}
pub fn default_sprites_dir() -> Option<PathBuf> {
if let Ok(p) = std::env::var("FLATLAND_SPRITES_DIR") {
let path = PathBuf::from(p);
if path.join("manifest.yaml").is_file() {
return Some(path);
}
}
if let Ok(home) = std::env::var("HOME") {
let current = PathBuf::from(home).join(".flatland3/assets/current");
if current.join("manifest.yaml").is_file() {
return Some(current);
}
}
let mut dir = std::env::current_dir().ok()?;
for _ in 0..8 {
let candidate = dir.join("assets/gfx/sprites");
if candidate.join("manifest.yaml").is_file() {
return Some(candidate);
}
if !dir.pop() {
break;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_starter_sprite_catalog() {
let dir = default_sprites_dir().expect("sprites dir");
let cat = load_sprite_catalog(&dir).expect("load");
assert!(!cat.is_empty());
let modes = cat.mode_ids("resource.oak_log");
assert!(modes.contains(&"idle".to_string()));
assert_eq!(modes.len(), 1);
}
}