flatland-presentation 0.2.24

Gfx sprite mode resolution — maps game states to sprite sheet mode ids
Documentation
//! Load gfx sprite sheet mode lists from `assets/gfx/sprites/`.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
struct SpriteManifest {
    #[allow(dead_code)]
    version: u32,
    sheets: Vec<SpriteManifestEntry>,
}

#[derive(Debug, Clone, Deserialize)]
struct SpriteManifestEntry {
    #[allow(dead_code)]
    id: String,
    file: String,
}

#[derive(Debug, Clone, Deserialize)]
struct SpriteSheetDef {
    id: String,
    modes: Vec<SpriteModeDef>,
}

#[derive(Debug, Clone, Deserialize)]
struct SpriteModeDef {
    id: String,
}

/// Mode ids for one sprite sheet.
#[derive(Debug, Clone, Default)]
pub struct SpriteSheetModes {
    pub sheet_id: String,
    pub mode_ids: Vec<String>,
}

/// Index of all sprite sheets and their mode ids.
#[derive(Debug, Clone, Default)]
pub struct SpriteCatalog {
    sheets: HashMap<String, SpriteSheetModes>,
}

impl SpriteCatalog {
    pub fn mode_ids(&self, sheet_id: &str) -> &[String] {
        self.sheets
            .get(sheet_id)
            .map(|s| s.mode_ids.as_slice())
            .unwrap_or(&[])
    }

    pub fn sheet_ids(&self) -> impl Iterator<Item = &String> {
        self.sheets.keys()
    }

    pub fn len(&self) -> usize {
        self.sheets.len()
    }

    pub fn is_empty(&self) -> bool {
        self.sheets.is_empty()
    }
}

/// Load sprite mode index from `dir` (expects `manifest.yaml` + sheet YAML files).
pub fn load_sprite_catalog(dir: &Path) -> anyhow::Result<SpriteCatalog> {
    let manifest_path = dir.join("manifest.yaml");
    if !manifest_path.is_file() {
        return Ok(SpriteCatalog::default());
    }
    let text = std::fs::read_to_string(&manifest_path)?;
    let manifest: SpriteManifest = serde_yaml::from_str(&text)?;
    let mut sheets = HashMap::new();

    for entry in &manifest.sheets {
        let sheet_path = dir.join(&entry.file);
        let sheet_text = match std::fs::read_to_string(&sheet_path) {
            Ok(t) => t,
            Err(_) => continue,
        };
        let def: SpriteSheetDef = match serde_yaml::from_str(&sheet_text) {
            Ok(d) => d,
            Err(_) => continue,
        };
        let mode_ids: Vec<String> = def.modes.into_iter().map(|m| m.id).collect();
        if mode_ids.is_empty() {
            continue;
        }
        sheets.insert(
            def.id.clone(),
            SpriteSheetModes {
                sheet_id: def.id,
                mode_ids,
            },
        );
    }

    Ok(SpriteCatalog { sheets })
}

/// Resolve sprites directory from env, synced install cache, or repo checkout.
pub fn default_sprites_dir() -> Option<PathBuf> {
    if let Ok(p) = std::env::var("FLATLAND_SPRITES_DIR") {
        let path = PathBuf::from(p);
        if path.join("manifest.yaml").is_file() {
            return Some(path);
        }
    }
    if let Ok(home) = std::env::var("HOME") {
        let current = PathBuf::from(home).join(".flatland3/assets/current");
        if current.join("manifest.yaml").is_file() {
            return Some(current);
        }
    }
    let mut dir = std::env::current_dir().ok()?;
    for _ in 0..8 {
        let candidate = dir.join("assets/gfx/sprites");
        if candidate.join("manifest.yaml").is_file() {
            return Some(candidate);
        }
        if !dir.pop() {
            break;
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn loads_starter_sprite_catalog() {
        let dir = default_sprites_dir().expect("sprites dir");
        let cat = load_sprite_catalog(&dir).expect("load");
        assert!(!cat.is_empty());
        let modes = cat.mode_ids("resource.oak_log");
        assert!(modes.contains(&"idle".to_string()));
        assert_eq!(modes.len(), 1);
    }
}