changeset 638:a806f28e94fc

Add anm0 support.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Wed, 03 Jul 2019 22:17:04 +0200
parents afa012bb8021
children a8e0219162b6
files Cargo.toml src/th06/anm0.rs src/th06/mod.rs
diffstat 3 files changed, 323 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Wed Jul 03 16:27:12 2019 +0200
+++ b/Cargo.toml	Wed Jul 03 22:17:04 2019 +0200
@@ -10,3 +10,4 @@
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+nom = "5"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/th06/anm0.rs	Wed Jul 03 22:17:04 2019 +0200
@@ -0,0 +1,321 @@
+//! ANM0 animation format support.
+
+use nom::{
+    IResult,
+    bytes::complete::{tag, take_while_m_n},
+    number::complete::{le_u8, le_u16, le_u32, le_f32},
+};
+use std::collections::HashMap;
+
+/// Coordinates of a sprite into the image.
+#[derive(Debug, Clone)]
+pub struct Sprite {
+    index: u32,
+    x: f32,
+    y: f32,
+    width: f32,
+    height: f32,
+}
+
+/// A single instruction, part of a `Script`.
+#[derive(Debug, Clone)]
+struct Instruction {
+    time: u16,
+    opcode: u8,
+    args: Vec<Arg>,
+}
+
+/// Script driving an animation.
+#[derive(Debug, Clone)]
+pub struct Script {
+    instructions: Vec<Instruction>,
+    interrupts: HashMap<u32, u8>
+}
+
+/// Main struct of the ANM0 animation format.
+#[derive(Debug, Clone)]
+pub struct Anm0 {
+    /// Resolution of the image used by this ANM.
+    pub size: (u32, u32),
+
+    /// Format of this ANM.
+    pub format: u32,
+
+    /// File name of the main image.
+    pub first_name: String,
+
+    /// File name of an alpha channel image.
+    pub second_name: String,
+
+    /// A list of sprites, coordinates into the attached image.
+    pub sprites: Vec<Sprite>,
+
+    /// A map of scripts.
+    pub scripts: HashMap<u8, Script>,
+}
+
+impl Anm0 {
+    /// Parse a slice of bytes into an `Anm0` struct.
+    pub fn from_slice(data: &[u8]) -> Result<Anm0, ()> {
+        // XXX: report the exact nom error instead!
+        let (_, anm0) = parse_anm0(data).or_else(|_| Err(()))?;
+        assert_eq!(anm0.len(), 1);
+        Ok(anm0[0].clone())
+    }
+}
+
+fn parse_name(i: &[u8], is_present: bool) -> IResult<&[u8], String> {
+    if !is_present {
+        return Ok((i, String::new()));
+    }
+    let (_, slice) = take_while_m_n(0, 32, |c| c != 0)(i)?;
+    // XXX: no unwrap!
+    let string = String::from_utf8(slice.to_vec()).unwrap();
+    Ok((i, string))
+}
+
+fn parse_sprite(i: &[u8]) -> IResult<&[u8], Sprite> {
+    let (i, index) = le_u32(i)?;
+    let (i, x) = le_f32(i)?;
+    let (i, y) = le_f32(i)?;
+    let (i, width) = le_f32(i)?;
+    let (i, height) = le_f32(i)?;
+    Ok((i, Sprite {
+        index,
+        x,
+        y,
+        width,
+        height,
+    }))
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Arg {
+    U8(u8),
+    U32(u32),
+    F32(f32),
+    Ptr(u8),
+}
+
+fn parse_u8_arg(i: &[u8]) -> IResult<&[u8], Arg> {
+    let (i, value) = le_u8(i)?;
+    Ok((i, Arg::U8(value)))
+}
+
+fn parse_u32_arg(i: &[u8]) -> IResult<&[u8], Arg> {
+    let (i, value) = le_u32(i)?;
+    Ok((i, Arg::U32(value)))
+}
+
+fn parse_f32_arg(i: &[u8]) -> IResult<&[u8], Arg> {
+    let (i, value) = le_f32(i)?;
+    Ok((i, Arg::F32(value)))
+}
+
+macro_rules! declare_anm_instructions {
+    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ty),*)),*,) => {
+        fn parse_instruction_args(input: &[u8], opcode: u8) -> IResult<&[u8], Vec<Arg>> {
+            let mut args = vec![];
+            let mut i = &input[..];
+            match opcode {
+                $(
+                    $opcode => {
+                        $(
+                            let (i2, data) = match stringify!($arg_type) {
+                                "u8" => parse_u8_arg(i),
+                                "u32" => parse_u32_arg(i),
+                                "f32" => parse_f32_arg(i),
+                                "x8" => parse_u8_arg(i),
+                                _ => unreachable!(),
+                            }?;
+                            i = i2;
+                            if stringify!($arg_type) != "x8" {
+                                args.push(data);
+                            }
+                        )*
+                    }
+                )*
+                _ => unreachable!()
+            }
+            Ok((i, args))
+        }
+    };
+}
+
+declare_anm_instructions!{
+    0 => fn delete(),
+    1 => fn set_sprite(sprite_number: u32),
+    2 => fn set_scale(sx: f32, sy: f32),
+    3 => fn set_alpha(alpha: u32),
+    4 => fn set_color(red: u8, green: u8, blue: u8, XXX: x8),
+    5 => fn jump(instruction: u32),
+    7 => fn toggle_mirrored(),
+    9 => fn set_3d_rotations(x: f32, y: f32, z: f32),
+    10 => fn set_3d_rotations_speed(x: f32, y: f32, z: f32),
+    11 => fn set_scale_speed(sx: f32, sy: f32),
+    12 => fn fade(alpha: u32, duration: u32),
+    13 => fn set_blendmode_add(),
+    14 => fn set_blendmode_alphablend(),
+    15 => fn keep_still(),
+    16 => fn set_random_sprite(min_index: u32, amplitude: u32),
+    17 => fn set_3d_translation(x: f32, y: f32, z: f32),
+    18 => fn move_to_linear(x: f32, y: f32, z: f32, duration: u32),
+    19 => fn move_to_decel(x: f32, y: f32, z: f32, duration: u32),
+    20 => fn move_to_accel(x: f32, y: f32, z: f32, duration: u32),
+    21 => fn wait(),
+    22 => fn interrupt_label(label: u32),
+    23 => fn set_corner_relative_placement(),
+    24 => fn wait_ex(),
+    25 => fn set_allow_offset(allow: u32), // TODO: better name
+    26 => fn set_automatic_orientation(automatic: u32),
+    27 => fn shift_texture_x(dx: f32),
+    28 => fn shift_texture_y(dy: f32),
+    29 => fn set_visible(visible: u32),
+    30 => fn scale_in(sx: f32, sy: f32, duration: u32),
+    31 => fn TODO(TODO: u32),
+}
+
+fn parse_anm0(input: &[u8]) -> IResult<&[u8], Vec<Anm0>> {
+    let mut list = vec![];
+    let start_offset = 0;
+    loop {
+        let i = &input[start_offset..];
+        let (i, num_sprites) = le_u32(i)?;
+        let (i, num_scripts) = le_u32(i)?;
+        let (i, _) = tag(b"\0\0\0\0")(i)?;
+        let (i, width) = le_u32(i)?;
+        let (i, height) = le_u32(i)?;
+        let (i, format) = le_u32(i)?;
+        let (i, _unknown1) = le_u32(i)?;
+        let (i, first_name_offset) = le_u32(i)?;
+        let (i, _unused) = le_u32(i)?;
+        let (i, second_name_offset) = le_u32(i)?;
+        let (i, version) = le_u32(i)?;
+        let (i, _unknown2) = le_u32(i)?;
+        let (i, _texture_offset) = le_u32(i)?;
+        let (i, has_data) = le_u32(i)?;
+        let (i, _next_offset) = le_u32(i)?;
+        let (mut i, unknown3) = le_u32(i)?;
+
+        assert_eq!(version, 0);
+        assert_eq!(unknown3, 0);
+        assert_eq!(has_data, 0);
+
+        let mut sprite_offsets = vec![];
+        for _ in 0..num_sprites {
+            let (i2, offset) = le_u32(i)?;
+            sprite_offsets.push(offset as usize);
+            i = i2;
+        }
+
+        let mut script_offsets = vec![];
+        for _ in 0..num_scripts {
+            let (i2, index) = le_u32(i)?;
+            let (i2, offset) = le_u32(i2)?;
+            script_offsets.push((index as u8, offset as usize));
+            i = i2;
+        }
+
+        let i = &input[start_offset + first_name_offset as usize..];
+        let (_, first_name) = parse_name(i, first_name_offset > 0)?;
+
+        let i = &input[start_offset + second_name_offset as usize..];
+        let (_, second_name) = parse_name(i, second_name_offset > 0)?;
+
+        let mut sprites = vec![];
+        let mut i;
+        for offset in sprite_offsets {
+            i = &input[start_offset + offset..];
+            let (_, sprite) = parse_sprite(i)?;
+            sprites.push(sprite);
+        }
+
+        let mut scripts = HashMap::new();
+        for (index, offset) in script_offsets {
+            i = &input[start_offset + offset..];
+            let mut instruction_offsets = vec![];
+
+            let mut instructions = vec![];
+            loop {
+                let tell = input.len() - i.len();
+                instruction_offsets.push(tell - (start_offset + offset));
+                let (i2, time) = le_u16(i)?;
+                let (i2, opcode) = le_u8(i2)?;
+                // TODO: maybe check against the size of parsed data?
+                let (i2, _size) = le_u8(i2)?;
+                let (i2, args) = parse_instruction_args(i2, opcode)?;
+                instructions.push(Instruction { time, opcode, args });
+                i = i2;
+                if opcode == 0 {
+                    break;
+                }
+            }
+            let mut interrupts = HashMap::new();
+            let mut j = 0;
+            for Instruction { time: _, opcode, args } in &mut instructions {
+                match opcode {
+                    5 => {
+                        let offset = match args[0] {
+                            Arg::U32(offset) => offset as usize,
+                            _ => panic!("Wrong argument type for jump!"),
+                        };
+                        let result = instruction_offsets.binary_search(&offset);
+                        let ptr = match result {
+                            Ok(ptr) => ptr as u8,
+                            // TODO: make that a recoverable error instead.
+                            Err(ptr) => panic!("Instruction offset not found for pointer: {}", ptr),
+                        };
+                        args[0] = Arg::Ptr(ptr);
+                    }
+                    22 => {
+                        // TODO: maybe also remove this instruction, as the label is already
+                        // present.
+                        if let Arg::U32(interrupt) = args[0] {
+                            interrupts.insert(interrupt, j + 1);
+                        } else {
+                            panic!("Wrong argument type for interrupt!");
+                        }
+                    }
+                    _ => ()
+                }
+                j += 1;
+            }
+            scripts.insert(index, Script {
+                instructions,
+                interrupts,
+            });
+        }
+
+        assert!(has_data == 0);
+
+        let anm0 = Anm0 {
+            size: (width, height),
+            format,
+            first_name,
+            second_name,
+            sprites,
+            scripts,
+        };
+        list.push(anm0);
+        break;
+    }
+    Ok((b"", list))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    #[test]
+    fn anm0() {
+        let file = File::open("/home/linkmauve/games/pc/東方/TH06 ~ The Embodiment of Scarlet Devil/CM/player01.anm").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let anm0 = Anm0::from_slice(&buf).unwrap();
+        assert_eq!(anm0.size, (256, 256));
+        assert_eq!(anm0.format, 5);
+    }
+}
--- a/src/th06/mod.rs	Wed Jul 03 16:27:12 2019 +0200
+++ b/src/th06/mod.rs	Wed Jul 03 22:17:04 2019 +0200
@@ -1,3 +1,4 @@
 //! Touhou 6: EoSD implementation.
 
 pub mod pbg3;
+pub mod anm0;