237 lines
5.6 KiB
Rust
237 lines
5.6 KiB
Rust
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<PathBuf>,
|
|
pub timing: Timing,
|
|
pub repeat: RepeatMode,
|
|
pub random_start_frame: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub enum TimingVariant {
|
|
#[serde(rename = "duration")]
|
|
Duration(f32),
|
|
|
|
#[serde(rename = "fps")]
|
|
Fps(f32),
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Timing {
|
|
#[serde(flatten)]
|
|
pub variant: TimingVariant,
|
|
//pub uniform_rng: Option<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<PathBuf>,
|
|
|
|
/// The speed of this sprite's animation.
|
|
/// This is zero for unanimate sprites.
|
|
pub frame_duration: f32,
|
|
|
|
/// All frames will be sped up/slowed by this factor.
|
|
//pub frame_uniform_rng: 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<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 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],
|
|
frame_duration: 0.0,
|
|
//frame_uniform_rng: 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 frame_duration = match t.timing.variant {
|
|
syntax::TimingVariant::Duration(d) => d / t.frames.len() as f32,
|
|
syntax::TimingVariant::Fps(f) => 1.0 / f,
|
|
};
|
|
|
|
content.sprite_index.insert(sprite_name.clone(), h);
|
|
content.sprites.push(Self {
|
|
name: sprite_name,
|
|
frames: t.frames,
|
|
frame_duration,
|
|
//frame_uniform_rng: t.timing.uniform_rng.unwrap_or(0.0),
|
|
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(());
|
|
}
|
|
}
|