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, Serialize};
7
8use std::collections::BTreeMap;
9
10fn default_resource_mode_defaults() -> BTreeMap<String, String> {
11    BTreeMap::from([
12        ("available".into(), "idle".into()),
13        ("harvesting".into(), "harvest".into()),
14        ("depleted".into(), "depleted".into()),
15    ])
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SpriteFallback {
20    pub tile_id: String,
21    pub mode_id: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ResourcePresentationDefaults {
26    #[serde(default = "default_resource_mode_defaults")]
27    pub modes: BTreeMap<String, String>,
28    #[serde(default)]
29    pub fallbacks: BTreeMap<String, SpriteFallback>,
30}
31
32impl Default for ResourcePresentationDefaults {
33    fn default() -> Self {
34        Self {
35            modes: default_resource_mode_defaults(),
36            fallbacks: BTreeMap::new(),
37        }
38    }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SpritePresentationDefaults {
43    #[serde(default = "default_presentation_version")]
44    pub version: u32,
45    #[serde(default)]
46    pub resource: ResourcePresentationDefaults,
47}
48
49fn default_presentation_version() -> u32 {
50    1
51}
52
53impl Default for SpritePresentationDefaults {
54    fn default() -> Self {
55        Self {
56            version: default_presentation_version(),
57            resource: ResourcePresentationDefaults::default(),
58        }
59    }
60}
61
62#[derive(Debug, Clone, Deserialize)]
63struct SpriteManifest {
64    #[allow(dead_code)]
65    version: u32,
66    sheets: Vec<SpriteManifestEntry>,
67}
68
69#[derive(Debug, Clone, Deserialize)]
70struct SpriteManifestEntry {
71    #[allow(dead_code)]
72    id: String,
73    file: String,
74}
75
76#[derive(Debug, Clone, Deserialize)]
77struct SpriteSheetDef {
78    id: String,
79    modes: Vec<SpriteModeDef>,
80}
81
82#[derive(Debug, Clone, Deserialize)]
83struct SpriteModeDef {
84    id: String,
85}
86
87/// Mode ids for one sprite sheet.
88#[derive(Debug, Clone, Default)]
89pub struct SpriteSheetModes {
90    pub sheet_id: String,
91    pub mode_ids: Vec<String>,
92}
93
94/// Index of all sprite sheets and their mode ids.
95#[derive(Debug, Clone, Default)]
96pub struct SpriteCatalog {
97    sheets: HashMap<String, SpriteSheetModes>,
98    presentation_defaults: SpritePresentationDefaults,
99}
100
101impl SpriteCatalog {
102    pub fn mode_ids(&self, sheet_id: &str) -> &[String] {
103        self.sheets
104            .get(sheet_id)
105            .map(|s| s.mode_ids.as_slice())
106            .unwrap_or(&[])
107    }
108
109    pub fn sheet_ids(&self) -> impl Iterator<Item = &String> {
110        self.sheets.keys()
111    }
112
113    pub fn len(&self) -> usize {
114        self.sheets.len()
115    }
116
117    pub fn is_empty(&self) -> bool {
118        self.sheets.is_empty()
119    }
120
121    pub fn presentation_defaults(&self) -> &SpritePresentationDefaults {
122        &self.presentation_defaults
123    }
124
125    pub fn set_presentation_defaults(&mut self, defaults: SpritePresentationDefaults) {
126        self.presentation_defaults = defaults;
127    }
128
129    /// Register or replace mode ids for a sheet (e.g. gfx-baked `paperdoll.*` sheets).
130    pub fn register_sheet_modes(&mut self, sheet_id: impl Into<String>, mode_ids: Vec<String>) {
131        let sheet_id = sheet_id.into();
132        if mode_ids.is_empty() {
133            return;
134        }
135        self.sheets
136            .insert(sheet_id.clone(), SpriteSheetModes { sheet_id, mode_ids });
137    }
138}
139
140/// Load sprite mode index from `dir` (expects `manifest.yaml` + sheet YAML files).
141pub fn load_sprite_catalog(dir: &Path) -> anyhow::Result<SpriteCatalog> {
142    let manifest_path = dir.join("manifest.yaml");
143    if !manifest_path.is_file() {
144        return Ok(SpriteCatalog::default());
145    }
146    let text = std::fs::read_to_string(&manifest_path)?;
147    let manifest: SpriteManifest = serde_yaml::from_str(&text)?;
148    let presentation_defaults = load_sprite_presentation_defaults(dir)?;
149    let mut sheets = HashMap::new();
150
151    for entry in &manifest.sheets {
152        let sheet_path = dir.join(&entry.file);
153        let sheet_text = match std::fs::read_to_string(&sheet_path) {
154            Ok(t) => t,
155            Err(_) => continue,
156        };
157        let def: SpriteSheetDef = match serde_yaml::from_str(&sheet_text) {
158            Ok(d) => d,
159            Err(_) => continue,
160        };
161        let mode_ids: Vec<String> = def.modes.into_iter().map(|m| m.id).collect();
162        if mode_ids.is_empty() {
163            continue;
164        }
165        sheets.insert(
166            def.id.clone(),
167            SpriteSheetModes {
168                sheet_id: def.id,
169                mode_ids,
170            },
171        );
172    }
173
174    Ok(SpriteCatalog {
175        sheets,
176        presentation_defaults,
177    })
178}
179
180pub fn load_sprite_presentation_defaults(dir: &Path) -> anyhow::Result<SpritePresentationDefaults> {
181    let path = dir.join("presentation-defaults.yaml");
182    if !path.is_file() {
183        return Ok(SpritePresentationDefaults::default());
184    }
185    let text = std::fs::read_to_string(path)?;
186    Ok(serde_yaml::from_str(&text)?)
187}
188
189/// Resolve sprites directory from env, repo checkout, or synced install cache.
190pub fn default_sprites_dir() -> Option<PathBuf> {
191    if let Ok(p) = std::env::var("FLATLAND_SPRITES_DIR") {
192        let path = PathBuf::from(p);
193        if path.join("manifest.yaml").is_file() {
194            return Some(path);
195        }
196    }
197    // Prefer the live repo tree when developing / validating content so new sheets
198    // (e.g. LoRA imports) are visible before the next client asset sync.
199    let mut dir = std::env::current_dir().ok()?;
200    for _ in 0..8 {
201        let candidate = dir.join("assets/gfx/sprites");
202        if candidate.join("manifest.yaml").is_file() {
203            return Some(candidate);
204        }
205        if !dir.pop() {
206            break;
207        }
208    }
209    if let Ok(home) = std::env::var("HOME") {
210        let current = PathBuf::from(home).join(".flatland3/assets/current");
211        if current.join("manifest.yaml").is_file() {
212            return Some(current);
213        }
214    }
215    None
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn loads_starter_sprite_catalog() {
224        let dir = default_sprites_dir().expect("sprites dir");
225        let cat = load_sprite_catalog(&dir).expect("load");
226        assert!(!cat.is_empty());
227        let modes = cat.mode_ids("resource.oak_log");
228        assert!(modes.contains(&"idle".to_string()));
229        assert_eq!(modes.len(), 1);
230    }
231}