Mercurial > touhou
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 } |
