view formats/src/th06/msg.rs @ 792:11bc22bad1bf default tip

python: Replace the image crate with png We weren’t using any of its features anyway, so the png crate is exactly what we need, without the many heavy dependencies of image. https://github.com/image-rs/image-png/pull/670 will eventually make it even faster to build.
author Link Mauve <linkmauve@linkmauve.fr>
date Sat, 17 Jan 2026 22:22:25 +0100
parents a30ce01b9154
children
line wrap: on
line source

//! MSG format support.

use encoding_rs::SHIFT_JIS;
use nom::{
    multi::length_count,
    number::complete::{le_u16, le_u32, le_u8},
    IResult, Parser,
};
use std::collections::BTreeMap;

/// Parse a SHIFT_JIS byte string of length 34 into a String.
#[allow(non_snake_case)]
pub fn le_String(i: &[u8]) -> IResult<&[u8], String> {
    let data = i.splitn(2, |c| *c == b'\0').nth(0).unwrap();
    let (string, _encoding, _replaced) = SHIFT_JIS.decode(data);
    Ok((b"", string.into_owned()))
}

/// A single instruction, part of a `Script`.
#[derive(Debug, Clone)]
pub struct Call {
    /// Time at which this instruction will be called.
    pub time: u16,

    /// The instruction to call.
    pub instr: Instruction,
}

/// Main struct of the MSG format.
#[derive(Debug, Clone)]
pub struct Msg {
    /// Map of indices to scripts in this msg.
    pub scripts: BTreeMap<u8, Vec<Call>>,
}

impl Msg {
    /// Parse a slice of bytes into a `Msg` struct.
    pub fn from_slice(data: &[u8]) -> IResult<&[u8], Msg> {
        parse_msg.parse(data)
    }
}

macro_rules! gen_match {
    ($arg_type:ident) => {
        ${concat(le_, $arg_type)}
    };
}

macro_rules! declare_msg_instructions {
    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
        /// Available instructions in a `Msg`.
        #[allow(missing_docs)]
        #[derive(Debug, Clone, PartialEq)]
        pub enum Instruction {
            $(
                $name($($arg_type),*)
            ),*
        }

        fn parse_instruction_args(mut i: &[u8], opcode: u8) -> IResult<&[u8], Instruction> {
            let instr = match opcode {
                $(
                    $opcode => {
                        $(
                            let (i2, $arg) = gen_match!($arg_type)(i)?;
                            i = i2;
                        )*
                        Instruction::$name($($arg),*)
                    }
                )*
                // XXX: use a more specific error instead.
                _ => return Err(nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::Eof)))
            };
            Ok((i, instr))
        }
    };
}

declare_msg_instructions! {
    0 => fn Unk1(),
    1 => fn Enter(side: u16, effect: u16),
    2 => fn ChangeFace(side: u16, index: u16),
    3 => fn DisplayText(side: u16, index: u16, text: String),
    4 => fn Pause(duration: u32),
    5 => fn Animate(side: u16, effect: u16),
    6 => fn SpawnEnemySprite(),
    7 => fn ChangeMusic(track: u32),
    8 => fn DisplayDescription(side: u16, index: u16, text: String),
    9 => fn ShowScores(unk1: u32),
    10 => fn Freeze(),
    11 => fn NextStage(),
    12 => fn Unk2(),
    13 => fn SetAllowSkip(boolean: u32),
    14 => fn Unk3(),
}

fn parse_msg(input: &[u8]) -> IResult<&[u8], Msg> {
    let (mut i, entry_offsets) = length_count(le_u32, le_u32).parse(input)?;
    let first_offset = entry_offsets[0];

    let mut scripts = BTreeMap::new();
    for (index, offset) in entry_offsets
        .into_iter()
        .enumerate()
        .map(|(index, offset)| (index as u8, offset))
    {
        if input.len() < offset as usize {
            return Err(nom::Err::Failure(nom::error::Error::new(
                input,
                nom::error::ErrorKind::Eof,
            )));
        }

        // In EoSD, Reimu’s scripts start at 0, and Marisa’s ones at 10.
        // If Reimu has less than 10 scripts, the remaining offsets are equal to her first.
        if index > 0 && offset == first_offset {
            continue;
        }

        i = &input[offset as usize..];
        let mut instructions = Vec::new();
        loop {
            let (i2, (time, opcode, size)) = (le_u16, le_u8, le_u8).parse(i)?;
            if time == 0 && opcode == 0 && size == 0 {
                break;
            }
            let (i2, data) = (&i2[size as usize..], &i2[..size as usize]);
            let (empty, instr) = parse_instruction_args(data, opcode)?;
            assert!(empty.is_empty());
            instructions.push(Call { time, instr });
            i = i2;
        }
        scripts.insert(index, instructions);
    }

    Ok((i, Msg { scripts }))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::{self, Read};

    #[test]
    fn msg() {
        println!("{}", std::env::current_dir().unwrap().display());
        let file = File::open("EoSD/ST/msg1.dat").unwrap();
        let mut file = io::BufReader::new(file);
        let mut buf = Vec::new();
        file.read_to_end(&mut buf).unwrap();
        let (_, msg) = Msg::from_slice(&buf).unwrap();
        assert_eq!(msg.scripts.len(), 4);
        assert_eq!(msg.scripts[&0].len(), 89);
        assert_eq!(msg.scripts[&1].len(), 13);
        assert_eq!(msg.scripts[&10].len(), 58);
        assert_eq!(msg.scripts[&11].len(), 13);
        let script = &msg.scripts[&0];
        assert_eq!(script[3].time, 60);
        assert_eq!(
            script[3].instr,
            Instruction::DisplayText(0, 0, String::from("久々のお仕事だわ。"))
        );
    }
}