1use 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 #[serde(default)]
33 pub sheet_suffix_fallbacks: BTreeMap<String, BTreeMap<String, SpriteFallback>>,
34}
35
36impl Default for ResourcePresentationDefaults {
37 fn default() -> Self {
38 Self {
39 modes: default_resource_mode_defaults(),
40 fallbacks: BTreeMap::new(),
41 sheet_suffix_fallbacks: BTreeMap::new(),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
48pub struct FarmPresentationDefaults {
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub growth_fallback: Option<FarmGrowthFallback>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub struct FarmGrowthFallback {
56 pub tile_id: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SpritePresentationDefaults {
61 #[serde(default = "default_presentation_version")]
62 pub version: u32,
63 #[serde(default)]
64 pub resource: ResourcePresentationDefaults,
65 #[serde(default)]
66 pub farm: FarmPresentationDefaults,
67}
68
69fn default_presentation_version() -> u32 {
70 1
71}
72
73impl Default for SpritePresentationDefaults {
74 fn default() -> Self {
75 Self {
76 version: default_presentation_version(),
77 resource: ResourcePresentationDefaults::default(),
78 farm: FarmPresentationDefaults::default(),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Deserialize)]
84struct SpriteManifest {
85 #[allow(dead_code)]
86 version: u32,
87 sheets: Vec<SpriteManifestEntry>,
88}
89
90#[derive(Debug, Clone, Deserialize)]
91struct SpriteManifestEntry {
92 #[allow(dead_code)]
93 id: String,
94 file: String,
95}
96
97#[derive(Debug, Clone, Deserialize)]
98struct SpriteSheetDef {
99 id: String,
100 modes: Vec<SpriteModeDef>,
101}
102
103#[derive(Debug, Clone, Deserialize)]
104struct SpriteModeDef {
105 id: String,
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct SpriteSheetModes {
111 pub sheet_id: String,
112 pub mode_ids: Vec<String>,
113}
114
115#[derive(Debug, Clone, Default)]
117pub struct SpriteCatalog {
118 sheets: HashMap<String, SpriteSheetModes>,
119 presentation_defaults: SpritePresentationDefaults,
120}
121
122impl SpriteCatalog {
123 pub fn mode_ids(&self, sheet_id: &str) -> &[String] {
124 self.sheets
125 .get(sheet_id)
126 .map(|s| s.mode_ids.as_slice())
127 .unwrap_or(&[])
128 }
129
130 pub fn sheet_ids(&self) -> impl Iterator<Item = &String> {
131 self.sheets.keys()
132 }
133
134 pub fn len(&self) -> usize {
135 self.sheets.len()
136 }
137
138 pub fn is_empty(&self) -> bool {
139 self.sheets.is_empty()
140 }
141
142 pub fn presentation_defaults(&self) -> &SpritePresentationDefaults {
143 &self.presentation_defaults
144 }
145
146 pub fn set_presentation_defaults(&mut self, defaults: SpritePresentationDefaults) {
147 self.presentation_defaults = defaults;
148 }
149
150 pub fn register_sheet_modes(&mut self, sheet_id: impl Into<String>, mode_ids: Vec<String>) {
152 let sheet_id = sheet_id.into();
153 if mode_ids.is_empty() {
154 return;
155 }
156 self.sheets
157 .insert(sheet_id.clone(), SpriteSheetModes { sheet_id, mode_ids });
158 }
159}
160
161pub fn load_sprite_catalog(dir: &Path) -> anyhow::Result<SpriteCatalog> {
163 let manifest_path = dir.join("manifest.yaml");
164 if !manifest_path.is_file() {
165 return Ok(SpriteCatalog::default());
166 }
167 let text = std::fs::read_to_string(&manifest_path)?;
168 let manifest: SpriteManifest = serde_yaml::from_str(&text)?;
169 let presentation_defaults = load_sprite_presentation_defaults(dir)?;
170 let mut sheets = HashMap::new();
171
172 for entry in &manifest.sheets {
173 let sheet_path = dir.join(&entry.file);
174 let sheet_text = match std::fs::read_to_string(&sheet_path) {
175 Ok(t) => t,
176 Err(_) => continue,
177 };
178 let def: SpriteSheetDef = match serde_yaml::from_str(&sheet_text) {
179 Ok(d) => d,
180 Err(_) => continue,
181 };
182 let mode_ids: Vec<String> = def.modes.into_iter().map(|m| m.id).collect();
183 if mode_ids.is_empty() {
184 continue;
185 }
186 sheets.insert(
187 def.id.clone(),
188 SpriteSheetModes {
189 sheet_id: def.id,
190 mode_ids,
191 },
192 );
193 }
194
195 Ok(SpriteCatalog {
196 sheets,
197 presentation_defaults,
198 })
199}
200
201pub fn load_sprite_presentation_defaults(dir: &Path) -> anyhow::Result<SpritePresentationDefaults> {
202 let path = dir.join("presentation-defaults.yaml");
203 if !path.is_file() {
204 return Ok(SpritePresentationDefaults::default());
205 }
206 let text = std::fs::read_to_string(path)?;
207 Ok(serde_yaml::from_str(&text)?)
208}
209
210pub fn default_sprites_dir() -> Option<PathBuf> {
212 if let Ok(p) = std::env::var("FLATLAND_SPRITES_DIR") {
213 let path = PathBuf::from(p);
214 if path.join("manifest.yaml").is_file() {
215 return Some(path);
216 }
217 }
218 let mut dir = std::env::current_dir().ok()?;
221 for _ in 0..8 {
222 let candidate = dir.join("assets/gfx/sprites");
223 if candidate.join("manifest.yaml").is_file() {
224 return Some(candidate);
225 }
226 if !dir.pop() {
227 break;
228 }
229 }
230 if let Ok(home) = std::env::var("HOME") {
231 let current = PathBuf::from(home).join(".flatland3/assets/current");
232 if current.join("manifest.yaml").is_file() {
233 return Some(current);
234 }
235 }
236 None
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn loads_starter_sprite_catalog() {
245 let dir = default_sprites_dir().expect("sprites dir");
246 let cat = load_sprite_catalog(&dir).expect("load");
247 assert!(!cat.is_empty());
248 let modes = cat.mode_ids("resource.oak_log");
249 assert!(modes.contains(&"idle".to_string()));
250 assert_eq!(modes.len(), 1);
251 }
252}