flatland_presentation/
catalog.rs1use 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#[derive(Debug, Clone, Default)]
89pub struct SpriteSheetModes {
90 pub sheet_id: String,
91 pub mode_ids: Vec<String>,
92}
93
94#[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 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
140pub 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
189pub 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 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}