flatland_presentation/
catalog.rs1use 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#[derive(Debug, Clone, Default)]
35pub struct SpriteSheetModes {
36 pub sheet_id: String,
37 pub mode_ids: Vec<String>,
38}
39
40#[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 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
82pub 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
118pub 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}