use anyhow::{anyhow, bail, Context, Result}; use std::collections::HashMap; use crate::{handle::SpriteHandle, Content, ContentBuildContext}; pub(crate) mod syntax { use crate::{Content, ContentBuildContext}; use anyhow::{anyhow, bail, Context, Ok, Result}; use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; use super::AnimSectionHandle; // Raw serde syntax structs. // These are never seen by code outside this crate. /// Convenience variants of sprite definitions #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum Sprite { Static(StaticSprite), OneSection(SpriteSection), Complete(CompleteSprite), } /// Two ways to specify animation length #[derive(Debug, Deserialize)] pub enum TimingVariant { /// The duration of this whole section #[serde(rename = "duration")] Duration(f32), /// The fps of this section #[serde(rename = "fps")] Fps(f32), } #[derive(Debug, Deserialize)] pub struct Timing { #[serde(flatten)] pub variant: TimingVariant, } /// An unanimated sprite #[derive(Debug, Deserialize)] pub struct StaticSprite { pub file: PathBuf, } /// The proper, full sprite definition #[derive(Debug, Deserialize)] pub struct CompleteSprite { pub section: HashMap, pub start_at: SectionEdge, } /// A single animation section #[derive(Debug, Deserialize)] pub struct SpriteSection { pub frames: Vec, pub timing: Timing, pub top: Option, pub bot: Option, } impl SpriteSection { pub fn add_to( &self, _build_context: &mut ContentBuildContext, content: &mut Content, // An index of all sections in this sprite, used to resolve // top and bot edges. all_sections: &HashMap, ) -> Result<((u32, u32), super::SpriteSection)> { // Make sure all frames have the same size and add them // to the frame vector let mut dim = None; let mut frames = Vec::new(); for f in &self.frames { let idx = match content.sprite_atlas.path_map.get(f) { Some(s) => *s, None => { bail!("error: file `{}` isn't in the sprite atlas", f.display()); } }; let img = &content.sprite_atlas.index[idx as usize]; match dim { None => dim = Some(img.true_size), Some(e) => { if img.true_size != e { bail!("failed to load section frames because frames have different sizes.",) } } } frames.push(img.idx); } let dim = dim.unwrap(); let frame_duration = match self.timing.variant { TimingVariant::Duration(d) => d / self.frames.len() as f32, TimingVariant::Fps(f) => 1.0 / f, }; if frame_duration <= 0.0 { bail!("frame duration must be positive (and therefore nonzero).") } let edge_top = match &self.top { Some(x) => x.resolve_as_edge(all_sections)?, None => super::SectionEdge::Stop, }; let edge_bot = match &self.bot { Some(x) => x.resolve_as_edge(all_sections)?, None => super::SectionEdge::Stop, }; return Ok(( dim, super::SpriteSection { frames, frame_duration, edge_top, edge_bot, }, )); } } /// A link between two animation sections #[derive(Debug, Deserialize)] #[serde(transparent)] pub struct SectionEdge { pub val: String, } impl SectionEdge { pub fn resolve_as_start( &self, all_sections: &HashMap, ) -> Result { let e = self .resolve_as_edge(all_sections) .with_context(|| format!("while resolving start edge"))?; match e { super::SectionEdge::Bot { section } => Ok(super::SpriteStart::Bot { section }), super::SectionEdge::Top { section } => Ok(super::SpriteStart::Top { section }), _ => { bail!("bad section start specification `{}`", self.val); } } } pub fn resolve_as_edge( &self, all_sections: &HashMap, ) -> Result { if self.val == "stop" { return Ok(super::SectionEdge::Stop); } if self.val == "reverse" { return Ok(super::SectionEdge::Reverse); } if self.val == "restart" { return Ok(super::SectionEdge::Restart); } let (s, p) = match self.val.split_once(":") { Some(x) => x, None => { bail!("bad section edge specification `{}`", self.val); } }; let section = match all_sections.get(s) { Some(s) => *s, None => { return Err(anyhow!("bad section edge specification `{}`", self.val)) .with_context(|| format!("section `{}` doesn't exist", s)); } }; match p { "top" => Ok(super::SectionEdge::Top { section }), "bot" => Ok(super::SectionEdge::Bot { section }), _ => { return Err(anyhow!("bad section edge specification `{}`", self.val)) .with_context(|| format!("invalid target `{}`", p)); } } } } } // TODO: should be pub crate /// A handle for an animation section inside a sprite #[derive(Debug, Copy, Clone)] pub struct AnimSectionHandle(pub(crate) usize); /// An edge between two animation sections #[derive(Debug, Clone)] pub enum SectionEdge { /// Stop at the last frame of this section Stop, /// Play the given section from the bottm Bot { /// The section to play section: AnimSectionHandle, }, /// Play the given section from the top Top { /// The section to play section: AnimSectionHandle, }, /// Replay this section in the opposite direction Reverse, /// Restart this section from the opposite end Restart, } /// Where to start an animation #[derive(Debug, Clone)] pub enum SpriteStart { /// Play the given section from the bottm Bot { /// The section to play section: AnimSectionHandle, }, /// Play the given section from the top Top { /// The section to play section: AnimSectionHandle, }, } /// 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, /// Where this sprite starts playing pub start_at: SpriteStart, /// This sprite's animation sections sections: Vec, /// Aspect ratio of this sprite (width / height) pub aspect: f32, } impl Sprite { /// Get an animation section from a handle pub fn get_section(&self, section: AnimSectionHandle) -> &SpriteSection { &self.sections[section.0] } /// Get this sprite's first frame pub fn get_first_frame(&self) -> u32 { match self.start_at { SpriteStart::Bot { section } => *self.get_section(section).frames.last().unwrap(), SpriteStart::Top { section } => *self.get_section(section).frames.first().unwrap(), } } /// Iterate this sprite's sections pub fn iter_sections(&self) -> impl Iterator { self.sections.iter() } } /// A part of a sprite's animation #[derive(Debug, Clone)] pub struct SpriteSection { /// The texture index of each frame in this animation section. /// unanimated sections have one frame. pub frames: Vec, /// The speed of this sprite's animation. /// This must always be positive (and therefore, nonzero) pub frame_duration: f32, /// What to do when we reach the top of this section pub edge_top: SectionEdge, /// What to do when we reach the bottom of this section pub edge_bot: SectionEdge, } 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 idx = match content.sprite_atlas.path_map.get(&t.file) { Some(s) => *s, None => { return Err( anyhow!("error while processing sprite `{}`", sprite_name,), ) .with_context(|| { format!( "file `{}` isn't in the sprite atlas, cannot proceed", t.file.display() ) }); } }; let img = &content.sprite_atlas.index[idx as usize]; let aspect = img.w / img.h; let h = SpriteHandle { index: content.sprites.len(), aspect, }; content.sprite_index.insert(sprite_name.clone(), h); content.sprites.push(Self { name: sprite_name, start_at: SpriteStart::Top { section: AnimSectionHandle(0), }, sections: vec![SpriteSection { frames: vec![img.idx], // We implement unanimated sprites with a very fast framerate // and STOP endpoints. frame_duration: 0.01, edge_top: SectionEdge::Stop, edge_bot: SectionEdge::Stop, }], handle: h, aspect, }); } syntax::Sprite::OneSection(s) => { let mut section_names: HashMap = HashMap::new(); // Name the one section in this sprite "anim" section_names.insert("anim".to_owned(), AnimSectionHandle(0)); let (dim, section) = s .add_to(build_context, content, §ion_names) .with_context(|| format!("while parsing sprite `{}`", sprite_name))?; let aspect = dim.0 as f32 / dim.1 as f32; let h = SpriteHandle { index: content.sprites.len(), aspect, }; let mut sections = Vec::new(); sections.push(section); content.sprite_index.insert(sprite_name.clone(), h); content.sprites.push(Self { name: sprite_name, sections, start_at: SpriteStart::Bot { section: AnimSectionHandle(0), }, handle: h, aspect, }); } syntax::Sprite::Complete(s) => { let mut idx = 0; let mut section_names = HashMap::new(); for (name, _) in &s.section { section_names.insert(name.to_owned(), AnimSectionHandle(idx)); idx += 1; } let start_at = s .start_at .resolve_as_start(§ion_names) .with_context(|| format!("while loading sprite `{}`", sprite_name))?; let mut sections = Vec::with_capacity(idx); let mut dim = None; // Make sure we add sections in order let mut names = section_names.iter().collect::>(); names.sort_by(|a, b| (a.1).0.cmp(&(b.1).0)); for (k, _) in names { let v = s.section.get(k).unwrap(); let (d, s) = v .add_to(build_context, content, §ion_names) .with_context(|| format!("while parsing sprite `{}`", sprite_name)) .with_context(|| format!("while parsing section `{}`", k))?; // Make sure all dimensions are the same if dim.is_none() { dim = Some(d); } else if dim.unwrap() != d { bail!( "could not load sprite `{}`, image sizes in section `{}` are different", sprite_name, k ); } sections.push(s); } let dim = dim.unwrap(); let aspect = dim.0 as f32 / dim.1 as f32; let h = SpriteHandle { index: content.sprites.len(), aspect, }; content.sprite_index.insert(sprite_name.clone(), h); content.sprites.push(Self { name: sprite_name, sections, start_at, handle: h, aspect, }); } } } return Ok(()); } }