comparison formats/src/th06/msg.rs @ 782:a30ce01b9154

formats: Rewrite msg parsing in Rust
author Link Mauve <linkmauve@linkmauve.fr>
date Thu, 20 Nov 2025 19:02:19 +0100
parents
children
comparison
equal deleted inserted replaced
781:5b43c42fa680 782:a30ce01b9154
1 //! MSG format support.
2
3 use encoding_rs::SHIFT_JIS;
4 use nom::{
5 multi::length_count,
6 number::complete::{le_u16, le_u32, le_u8},
7 IResult, Parser,
8 };
9 use std::collections::BTreeMap;
10
11 /// Parse a SHIFT_JIS byte string of length 34 into a String.
12 #[allow(non_snake_case)]
13 pub fn le_String(i: &[u8]) -> IResult<&[u8], String> {
14 let data = i.splitn(2, |c| *c == b'\0').nth(0).unwrap();
15 let (string, _encoding, _replaced) = SHIFT_JIS.decode(data);
16 Ok((b"", string.into_owned()))
17 }
18
19 /// A single instruction, part of a `Script`.
20 #[derive(Debug, Clone)]
21 pub struct Call {
22 /// Time at which this instruction will be called.
23 pub time: u16,
24
25 /// The instruction to call.
26 pub instr: Instruction,
27 }
28
29 /// Main struct of the MSG format.
30 #[derive(Debug, Clone)]
31 pub struct Msg {
32 /// Map of indices to scripts in this msg.
33 pub scripts: BTreeMap<u8, Vec<Call>>,
34 }
35
36 impl Msg {
37 /// Parse a slice of bytes into a `Msg` struct.
38 pub fn from_slice(data: &[u8]) -> IResult<&[u8], Msg> {
39 parse_msg.parse(data)
40 }
41 }
42
43 macro_rules! gen_match {
44 ($arg_type:ident) => {
45 ${concat(le_, $arg_type)}
46 };
47 }
48
49 macro_rules! declare_msg_instructions {
50 ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
51 /// Available instructions in a `Msg`.
52 #[allow(missing_docs)]
53 #[derive(Debug, Clone, PartialEq)]
54 pub enum Instruction {
55 $(
56 $name($($arg_type),*)
57 ),*
58 }
59
60 fn parse_instruction_args(mut i: &[u8], opcode: u8) -> IResult<&[u8], Instruction> {
61 let instr = match opcode {
62 $(
63 $opcode => {
64 $(
65 let (i2, $arg) = gen_match!($arg_type)(i)?;
66 i = i2;
67 )*
68 Instruction::$name($($arg),*)
69 }
70 )*
71 // XXX: use a more specific error instead.
72 _ => return Err(nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::Eof)))
73 };
74 Ok((i, instr))
75 }
76 };
77 }
78
79 declare_msg_instructions! {
80 0 => fn Unk1(),
81 1 => fn Enter(side: u16, effect: u16),
82 2 => fn ChangeFace(side: u16, index: u16),
83 3 => fn DisplayText(side: u16, index: u16, text: String),
84 4 => fn Pause(duration: u32),
85 5 => fn Animate(side: u16, effect: u16),
86 6 => fn SpawnEnemySprite(),
87 7 => fn ChangeMusic(track: u32),
88 8 => fn DisplayDescription(side: u16, index: u16, text: String),
89 9 => fn ShowScores(unk1: u32),
90 10 => fn Freeze(),
91 11 => fn NextStage(),
92 12 => fn Unk2(),
93 13 => fn SetAllowSkip(boolean: u32),
94 14 => fn Unk3(),
95 }
96
97 fn parse_msg(input: &[u8]) -> IResult<&[u8], Msg> {
98 let (mut i, entry_offsets) = length_count(le_u32, le_u32).parse(input)?;
99 let first_offset = entry_offsets[0];
100
101 let mut scripts = BTreeMap::new();
102 for (index, offset) in entry_offsets
103 .into_iter()
104 .enumerate()
105 .map(|(index, offset)| (index as u8, offset))
106 {
107 if input.len() < offset as usize {
108 return Err(nom::Err::Failure(nom::error::Error::new(
109 input,
110 nom::error::ErrorKind::Eof,
111 )));
112 }
113
114 // In EoSD, Reimu’s scripts start at 0, and Marisa’s ones at 10.
115 // If Reimu has less than 10 scripts, the remaining offsets are equal to her first.
116 if index > 0 && offset == first_offset {
117 continue;
118 }
119
120 i = &input[offset as usize..];
121 let mut instructions = Vec::new();
122 loop {
123 let (i2, (time, opcode, size)) = (le_u16, le_u8, le_u8).parse(i)?;
124 if time == 0 && opcode == 0 && size == 0 {
125 break;
126 }
127 let (i2, data) = (&i2[size as usize..], &i2[..size as usize]);
128 let (empty, instr) = parse_instruction_args(data, opcode)?;
129 assert!(empty.is_empty());
130 instructions.push(Call { time, instr });
131 i = i2;
132 }
133 scripts.insert(index, instructions);
134 }
135
136 Ok((i, Msg { scripts }))
137 }
138
139 #[cfg(test)]
140 mod tests {
141 use super::*;
142 use std::fs::File;
143 use std::io::{self, Read};
144
145 #[test]
146 fn msg() {
147 println!("{}", std::env::current_dir().unwrap().display());
148 let file = File::open("EoSD/ST/msg1.dat").unwrap();
149 let mut file = io::BufReader::new(file);
150 let mut buf = Vec::new();
151 file.read_to_end(&mut buf).unwrap();
152 let (_, msg) = Msg::from_slice(&buf).unwrap();
153 assert_eq!(msg.scripts.len(), 4);
154 assert_eq!(msg.scripts[&0].len(), 89);
155 assert_eq!(msg.scripts[&1].len(), 13);
156 assert_eq!(msg.scripts[&10].len(), 58);
157 assert_eq!(msg.scripts[&11].len(), 13);
158 let script = &msg.scripts[&0];
159 assert_eq!(script[3].time, 60);
160 assert_eq!(
161 script[3].instr,
162 Instruction::DisplayText(0, 0, String::from("久々のお仕事だわ。"))
163 );
164 }
165 }