use anyhow::{bail, Context, Result}; use image::io::Reader; use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; use crate::{handle::SpriteHandle, Content}; pub(crate) mod syntax { use serde::Deserialize; use std::path::PathBuf; use super::RepeatMode; // Raw serde syntax structs. // These are never seen by code outside this crate. #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum Sprite { Static(StaticSprite), Frames(FrameSprite), } #[derive(Debug, Deserialize)] pub struct StaticSprite { pub file: PathBuf, } #[derive(Debug, Deserialize)] pub struct FrameSprite { pub frames: Vec, pub duration: f32, pub repeat: RepeatMode, } } /// How to replay a texture's animation #[derive(Debug, Deserialize, Clone, Copy)] pub enum RepeatMode { /// Play this animation once, and stop at the last frame #[serde(rename = "once")] Once, /// After the first frame, jump to the last frame #[serde(rename = "repeat")] Repeat, } impl RepeatMode { /// Represent this repeatmode as an integer /// Used to pass this enum into shaders pub fn as_int(&self) -> u32 { match self { Self::Once => 0, Self::Repeat => 1, } } } /// Represents a sprite that may be used in the game. #[derive(Debug, Clone)] pub struct Sprite { /// The name of this sprite pub name: String, /// This sprite's handle pub handle: SpriteHandle, /// The file names of frames of this sprite. /// unanimated sprites have one frame. pub frames: Vec, /// The speed of this sprite's animation. /// unanimated sprites have zero fps. pub fps: f32, /// How to replay this sprite's animation pub repeat: RepeatMode, /// Aspect ratio of this sprite (width / height) pub aspect: f32, } impl crate::Build for Sprite { type InputSyntaxType = HashMap; fn build(sprites: Self::InputSyntaxType, ct: &mut Content) -> Result<()> { for (sprite_name, t) in sprites { match t { syntax::Sprite::Static(t) => { let file = ct.image_root.join(&t.file); let reader = Reader::open(&file).with_context(|| { format!( "Failed to read file `{}` in sprite `{}`", file.display(), sprite_name, ) })?; let dim = reader.into_dimensions().with_context(|| { format!( "Failed to get dimensions of file `{}` in sprite `{}`", file.display(), sprite_name, ) })?; let h = SpriteHandle { index: ct.sprites.len() as u32, aspect: dim.0 as f32 / dim.1 as f32, }; if sprite_name == ct.starfield_sprite_name { if ct.starfield_handle.is_none() { ct.starfield_handle = Some(h) } else { // This can't happen, since this is a hashmap. unreachable!("Found two starfield sprites! Something is very wrong.") } } ct.sprite_index.insert(sprite_name.clone(), h); ct.sprites.push(Self { name: sprite_name, frames: vec![t.file], fps: 0.0, handle: h, repeat: RepeatMode::Once, aspect: dim.0 as f32 / dim.1 as f32, }); } syntax::Sprite::Frames(t) => { let mut dim = None; for f in &t.frames { let file = ct.image_root.join(f); let reader = Reader::open(&file).with_context(|| { format!( "Failed to read file `{}` in sprite `{}`", file.display(), sprite_name, ) })?; let d = reader.into_dimensions().with_context(|| { format!( "Failed to get dimensions of file `{}` in sprite `{}`", file.display(), sprite_name, ) })?; match dim { None => dim = Some(d), Some(e) => { if d != e { bail!( "Failed to load frames of sprite `{}` because frames have different sizes.", sprite_name, ) } } } } let dim = dim.unwrap(); let h = SpriteHandle { index: ct.sprites.len() as u32, aspect: dim.0 as f32 / dim.1 as f32, }; if sprite_name == ct.starfield_sprite_name { unreachable!("Starfield texture may not be animated") } let fps = t.duration / t.frames.len() as f32; ct.sprite_index.insert(sprite_name.clone(), h); ct.sprites.push(Self { name: sprite_name, frames: t.frames, fps, handle: h, repeat: t.repeat, aspect: dim.0 as f32 / dim.1 as f32, }); } } } if ct.starfield_handle.is_none() { bail!( "Could not find a starfield texture (name: `{}`)", ct.starfield_sprite_name ) } return Ok(()); } }