Galactica/crates/content/src/part/sprite.rs

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(());
}
}