Skip to main content

flatland_presentation/
catalog.rs

1//! Load gfx sprite sheet mode lists from `assets/gfx/sprites/`.
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use serde::Deserialize;
7
8#[derive(Debug, Clone, Deserialize)]
9struct SpriteManifest {
10    #[allow(dead_code)]
11    version: u32,
12    sheets: Vec<SpriteManifestEntry>,
13}
14
15#[derive(Debug, Clone, Deserialize)]
16struct SpriteManifestEntry {
17    #[allow(dead_code)]
18    id: String,
19    file: String,
20}
21
22#[derive(Debug, Clone, Deserialize)]
23struct SpriteSheetDef {
24    id: String,
25    modes: Vec<SpriteModeDef>,
26}
27
28#[derive(Debug, Clone, Deserialize)]
29struct SpriteModeDef {
30    id: String,
31}
32
33/// Mode ids for one sprite sheet.
34#[derive(Debug, Clone, Default)]
35pub struct SpriteSheetModes {
36    pub sheet_id: String,
37    pub mode_ids: Vec<String>,
38}
39
40/// Index of all sprite sheets and their mode ids.
41#[derive(Debug, Clone, Default)]
42pub struct SpriteCatalog {
43    sheets: HashMap<String, SpriteSheetModes>,
44}
45
46impl SpriteCatalog {
47    pub fn mode_ids(&self, sheet_id: &str) -> &[String] {
48        self.sheets
49            .get(sheet_id)
50            .map(|s| s.mode_ids.as_slice())
51            .unwrap_or(&[])
52    }
53
54    pub fn sheet_ids(&self) -> impl Iterator<Item = &String> {
55        self.sheets.keys()
56    }
57
58    pub fn len(&self) -> usize {
59        self.sheets.len()
60    }
61
62    pub fn is_empty(&self) -> bool {
63        self.sheets.is_empty()
64    }
65
66    /// Register or replace mode ids for a sheet (e.g. gfx-baked `paperdoll.*` sheets).
67    pub fn register_sheet_modes(&mut self, sheet_id: impl Into<String>, mode_ids: Vec<String>) {
68        let sheet_id = sheet_id.into();
69        if mode_ids.is_empty() {
70            return;
71        }
72        self.sheets.insert(
73            sheet_id.clone(),
74            SpriteSheetModes {
75                sheet_id,
76                mode_ids,
77            },
78        );
79    }
80}
81
82/// Load sprite mode index from `dir` (expects `manifest.yaml` + sheet YAML files).
83pub fn load_sprite_catalog(dir: &Path) -> anyhow::Result<SpriteCatalog> {
84    let manifest_path = dir.join("manifest.yaml");
85    if !manifest_path.is_file() {
86        return Ok(SpriteCatalog::default());
87    }
88    let text = std::fs::read_to_string(&manifest_path)?;
89    let manifest: SpriteManifest = serde_yaml::from_str(&text)?;
90    let mut sheets = HashMap::new();
91
92    for entry in &manifest.sheets {
93        let sheet_path = dir.join(&entry.file);
94        let sheet_text = match std::fs::read_to_string(&sheet_path) {
95            Ok(t) => t,
96            Err(_) => continue,
97        };
98        let def: SpriteSheetDef = match serde_yaml::from_str(&sheet_text) {
99            Ok(d) => d,
100            Err(_) => continue,
101        };
102        let mode_ids: Vec<String> = def.modes.into_iter().map(|m| m.id).collect();
103        if mode_ids.is_empty() {
104            continue;
105        }
106        sheets.insert(
107            def.id.clone(),
108            SpriteSheetModes {
109                sheet_id: def.id,
110                mode_ids,
111            },
112        );
113    }
114
115    Ok(SpriteCatalog { sheets })
116}
117
118/// Resolve sprites directory from env, synced install cache, or repo checkout.
119pub fn default_sprites_dir() -> Option<PathBuf> {
120    if let Ok(p) = std::env::var("FLATLAND_SPRITES_DIR") {
121        let path = PathBuf::from(p);
122        if path.join("manifest.yaml").is_file() {
123            return Some(path);
124        }
125    }
126    if let Ok(home) = std::env::var("HOME") {
127        let current = PathBuf::from(home).join(".flatland3/assets/current");
128        if current.join("manifest.yaml").is_file() {
129            return Some(current);
130        }
131    }
132    let mut dir = std::env::current_dir().ok()?;
133    for _ in 0..8 {
134        let candidate = dir.join("assets/gfx/sprites");
135        if candidate.join("manifest.yaml").is_file() {
136            return Some(candidate);
137        }
138        if !dir.pop() {
139            break;
140        }
141    }
142    None
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn loads_starter_sprite_catalog() {
151        let dir = default_sprites_dir().expect("sprites dir");
152        let cat = load_sprite_catalog(&dir).expect("load");
153        assert!(!cat.is_empty());
154        let modes = cat.mode_ids("resource.oak_log");
155        assert!(modes.contains(&"idle".to_string()));
156        assert_eq!(modes.len(), 1);
157    }
158}