use anyhow::{bail, Context, Result}; use image::io::Reader; use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; use crate::{handle::SpriteHandle, Content, ContentBuildContext}; 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 timing: Timing, pub repeat: RepeatMode, pub random_start_frame: Option, } #[derive(Debug, Deserialize)] pub enum Timing { #[serde(rename = "duration")] Duration(f32), #[serde(rename = "fps")] Fps(f32), } } /// 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, /// Play this animation in reverse after the last frame #[serde(rename = "reverse")] Reverse, } impl RepeatMode { /// Represent this repeatmode as an integer /// Used to pass this enum into shaders pub fn as_int(&self) -> u32 { match self { Self::Repeat => 0, Self::Once => 1, Self::Reverse => 2, } } } /// 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, /// If true, start on a random frame of this sprite. pub random_start_frame: bool, } impl crate::Build for Sprite { type InputSyntaxType = HashMap; fn build( sprites: Self::InputSyntaxType, _build_context: &mut ContentBuildContext, content: &mut Content, ) -> Result<()> { for (sprite_name, t) in sprites { match t { syntax::Sprite::Static(t) => { let file = content.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: content.sprites.len(), aspect: dim.0 as f32 / dim.1 as f32, }; if sprite_name == content.starfield_sprite_name { if content.starfield_handle.is_none() { content.starfield_handle = Some(h) } else { // This can't happen, since this is a hashmap. unreachable!("Found two starfield sprites! Something is very wrong.") } } content.sprite_index.insert(sprite_name.clone(), h); content.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, random_start_frame: false, }); } syntax::Sprite::Frames(t) => { let mut dim = None; for f in &t.frames { let file = content.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: content.sprites.len(), aspect: dim.0 as f32 / dim.1 as f32, }; if sprite_name == content.starfield_sprite_name { unreachable!("Starfield texture may not be animated") } let fps = match t.timing { syntax::Timing::Duration(d) => d / t.frames.len() as f32, syntax::Timing::Fps(f) => 1.0 / f, }; content.sprite_index.insert(sprite_name.clone(), h); content.sprites.push(Self { name: sprite_name, frames: t.frames, fps, handle: h, repeat: t.repeat, aspect: dim.0 as f32 / dim.1 as f32, random_start_frame: t.random_start_frame.unwrap_or(false), }); } } } if content.starfield_handle.is_none() { bail!( "Could not find a starfield texture (name: `{}`)", content.starfield_sprite_name ) } return Ok(()); } }