446 lines
11 KiB
Rust

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<String, SpriteSection>,
pub start_at: SectionEdge,
}
/// A single animation section
#[derive(Debug, Deserialize)]
pub struct SpriteSection {
pub frames: Vec<PathBuf>,
pub timing: Timing,
pub top: Option<SectionEdge>,
pub bot: Option<SectionEdge>,
}
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<String, AnimSectionHandle>,
) -> 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<String, AnimSectionHandle>,
) -> Result<super::SpriteStart> {
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<String, AnimSectionHandle>,
) -> Result<super::SectionEdge> {
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<SpriteSection>,
/// 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<Item = &SpriteSection> {
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<u32>,
/// 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<String, syntax::Sprite>;
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<String, _> = 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, &section_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(&section_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::<Vec<_>>();
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, &section_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(());
}
}