Mercurial > touhou
comparison formats/src/th06/t6rp.rs @ 794:8c2ef2d503c9 default tip
formats: Implement T6RP in Rust
This has been tested against demo00.rpy shipped in CM.DAT.
| author | Link Mauve <linkmauve@linkmauve.fr> |
|---|---|
| date | Tue, 02 Jun 2026 16:39:21 +0200 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 793:bdefd3e6d6f9 | 794:8c2ef2d503c9 |
|---|---|
| 1 //! Touhou 6 Replay (T6RP) format support. | |
| 2 //! | |
| 3 //! This module provides classes for handling the Touhou 6 Replay file format. | |
| 4 //! The T6RP file format is an encrypted format describing different aspects of | |
| 5 //! a game of EoSD. Since the EoSD engine is entirely deterministic, a small | |
| 6 //! replay file is sufficient to unfold a full game. | |
| 7 | |
| 8 use std::convert::TryInto; | |
| 9 | |
| 10 use nom::{ | |
| 11 bytes::complete::tag, | |
| 12 multi::count, | |
| 13 number::complete::{le_f32, le_i8, le_u16, le_u32, le_u8}, | |
| 14 IResult, Parser, | |
| 15 }; | |
| 16 | |
| 17 /// A single level of a replay. | |
| 18 #[derive(Debug, Clone, Default)] | |
| 19 pub struct Level { | |
| 20 /// The score before starting the level. | |
| 21 pub score: u32, | |
| 22 | |
| 23 /// The seed initializing the PRNG. | |
| 24 pub random_seed: u16, | |
| 25 | |
| 26 /// The amount of point items collected before starting this level. | |
| 27 pub point_items: u16, | |
| 28 | |
| 29 /// The power level at the beginning of the level. | |
| 30 pub power: u8, | |
| 31 | |
| 32 /// How many lives are remaining before starting this level. | |
| 33 pub lives: i8, | |
| 34 | |
| 35 /// How many bombs are remaining before starting this level. | |
| 36 pub bombs: i8, | |
| 37 | |
| 38 /// The hidden difficulty at the beginning of this level. | |
| 39 pub difficulty: u8, | |
| 40 | |
| 41 unknown: u32, | |
| 42 | |
| 43 /// The list of keys pressed during this level. | |
| 44 pub keys: Vec<(u32, u16, u16)>, | |
| 45 } | |
| 46 | |
| 47 /// The unencrypted part of a replay. | |
| 48 #[derive(Debug, Clone)] | |
| 49 pub struct Header { | |
| 50 /// The version, only 1.02 is supported so far. | |
| 51 pub version: u16, | |
| 52 | |
| 53 /// The character picked by the player, 0 = Reimu A, 3 = Marisa B. | |
| 54 pub character: u8, | |
| 55 | |
| 56 /// The rank picked by the player, 0 = Easy, 4 = Extra. | |
| 57 pub rank: u8, | |
| 58 | |
| 59 /// The checksum of the encrypted part of this replay. | |
| 60 pub checksum: u32, | |
| 61 | |
| 62 unknown1: u8, | |
| 63 | |
| 64 unknown2: u8, | |
| 65 | |
| 66 /// The encryption key for the rest of this replay. | |
| 67 pub key: u8, | |
| 68 } | |
| 69 | |
| 70 /// The encrypted part of a replay. | |
| 71 #[derive(Debug, Clone)] | |
| 72 pub struct T6rp { | |
| 73 unknown3: u8, | |
| 74 | |
| 75 /// The date at which this replay got recorded. | |
| 76 pub date: [u8; 9], | |
| 77 | |
| 78 /// The name of the player. | |
| 79 pub name: [u8; 9], | |
| 80 | |
| 81 unknown4: u16, | |
| 82 | |
| 83 /// The score at the end of this replay. | |
| 84 pub score: u32, | |
| 85 | |
| 86 unknown5: u32, | |
| 87 | |
| 88 /// How much slowdown has been experienced while recording this replay. | |
| 89 pub slowdown: f32, | |
| 90 | |
| 91 unknown6: u32, | |
| 92 | |
| 93 /// Each of the stages that got recorded, only 0..=6 or 7 can be set. | |
| 94 pub levels: [Option<Level>; 7], | |
| 95 } | |
| 96 | |
| 97 // TODO: Add a newtype for `encrypted`, to provide the decrypt() method on it and then another | |
| 98 // newtype for the decrypted data to have verify() and parse(). | |
| 99 impl T6rp { | |
| 100 /// Parse a slice of bytes into a `Header` struct, which is the unencrypted part of T6RP. | |
| 101 pub fn parse_header(input: &[u8]) -> IResult<&[u8], Header> { | |
| 102 let (encrypted, (_, version, character, rank, checksum, unknown1, unknown2, key)) = ( | |
| 103 tag(&b"T6RP"[..]), | |
| 104 le_u16, | |
| 105 le_u8, | |
| 106 le_u8, | |
| 107 le_u32, | |
| 108 le_u8, | |
| 109 le_u8, | |
| 110 le_u8, | |
| 111 ) | |
| 112 .parse(input)?; | |
| 113 Ok(( | |
| 114 encrypted, | |
| 115 Header { | |
| 116 version, | |
| 117 character, | |
| 118 rank, | |
| 119 checksum, | |
| 120 unknown1, | |
| 121 unknown2, | |
| 122 key, | |
| 123 }, | |
| 124 )) | |
| 125 } | |
| 126 | |
| 127 /// Decrypt the contents of the replay with the header’s key. | |
| 128 pub fn decrypt(key: u8, encrypted: &[u8]) -> Vec<u8> { | |
| 129 let mut decrypted = Vec::with_capacity(encrypted.len()); | |
| 130 for (i, c) in encrypted.iter().enumerate() { | |
| 131 decrypted.push(c.wrapping_sub(key).wrapping_sub((7 * i) as u8)); | |
| 132 } | |
| 133 decrypted | |
| 134 } | |
| 135 | |
| 136 /// Verify decrypted data against the header’s checksum. | |
| 137 pub fn verify(key: u8, checksum: u32, data: &[u8]) -> bool { | |
| 138 let real_sum = data | |
| 139 .iter() | |
| 140 .map(|c| *c as u32) | |
| 141 .sum::<u32>() | |
| 142 .wrapping_add(0x3f000318) | |
| 143 .wrapping_add(key as u32); | |
| 144 checksum == real_sum | |
| 145 } | |
| 146 | |
| 147 /// Parse a slice of bytes into a `T6rp` struct. | |
| 148 pub fn from_slice(input: &[u8]) -> IResult<&[u8], T6rp> { | |
| 149 let ( | |
| 150 _, | |
| 151 (unknown3, date, name, unknown4, score, unknown5, slowdown, unknown6, stage_offsets), | |
| 152 ) = ( | |
| 153 le_u8, | |
| 154 count(le_u8, 9), | |
| 155 count(le_u8, 9), | |
| 156 le_u16, | |
| 157 le_u32, | |
| 158 le_u32, | |
| 159 le_f32, | |
| 160 le_u32, | |
| 161 count(le_u32, 7), | |
| 162 ) | |
| 163 .parse(input)?; | |
| 164 let date = date.try_into().unwrap(); | |
| 165 let name = name.try_into().unwrap(); | |
| 166 | |
| 167 let mut levels = [const { None }; 7]; | |
| 168 for (index, offset) in stage_offsets.into_iter().enumerate() { | |
| 169 if offset == 0 { | |
| 170 continue; | |
| 171 } | |
| 172 | |
| 173 let data = &input[offset as usize - 15..]; | |
| 174 let ( | |
| 175 mut data, | |
| 176 (score, random_seed, point_items, power, lives, bombs, difficulty, unknown), | |
| 177 ) = (le_u32, le_u16, le_u16, le_u8, le_i8, le_i8, le_u8, le_u32).parse(data)?; | |
| 178 | |
| 179 let mut keys = Vec::new(); | |
| 180 loop { | |
| 181 let (data2, (time, keystate, unknown)) = (le_u32, le_u16, le_u16).parse(data)?; | |
| 182 data = data2; | |
| 183 | |
| 184 if time == 9999999 { | |
| 185 break; | |
| 186 } | |
| 187 keys.push((time, keystate, unknown)); | |
| 188 } | |
| 189 | |
| 190 levels[index] = Some(Level { | |
| 191 score, | |
| 192 random_seed, | |
| 193 point_items, | |
| 194 power, | |
| 195 lives, | |
| 196 bombs, | |
| 197 difficulty, | |
| 198 unknown, | |
| 199 keys, | |
| 200 }); | |
| 201 } | |
| 202 | |
| 203 Ok(( | |
| 204 b"", | |
| 205 T6rp { | |
| 206 unknown3, | |
| 207 date, | |
| 208 name, | |
| 209 unknown4, | |
| 210 score, | |
| 211 unknown5, | |
| 212 slowdown, | |
| 213 unknown6, | |
| 214 levels, | |
| 215 }, | |
| 216 )) | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 #[cfg(test)] | |
| 221 mod tests { | |
| 222 use super::*; | |
| 223 use std::fs::File; | |
| 224 use std::io::{self, Read}; | |
| 225 | |
| 226 #[test] | |
| 227 fn t6rp() { | |
| 228 let file = File::open("EoSD/CM.bak/demo00.rpy").unwrap(); | |
| 229 let mut file = io::BufReader::new(file); | |
| 230 let mut buf = Vec::new(); | |
| 231 file.read_to_end(&mut buf).unwrap(); | |
| 232 | |
| 233 let (encrypted, header) = T6rp::parse_header(&buf).unwrap(); | |
| 234 assert_eq!(header.version, 0x0102); | |
| 235 assert_eq!(header.character, 0); | |
| 236 assert_eq!(header.rank, 3); | |
| 237 assert_eq!(header.checksum, 0x3f03188d); | |
| 238 assert_eq!(header.key, 0x9d); | |
| 239 | |
| 240 let data = T6rp::decrypt(header.key, &encrypted); | |
| 241 | |
| 242 assert!(T6rp::verify(header.key, header.checksum, &data)); | |
| 243 | |
| 244 let (_, t6rp) = T6rp::from_slice(&data).unwrap(); | |
| 245 assert_eq!(t6rp.date, *b"08/21/02\0"); | |
| 246 assert_eq!(t6rp.name, *b"A \0"); | |
| 247 assert_eq!(t6rp.score, 7837690); | |
| 248 assert_eq!(t6rp.slowdown, 0.62111616); | |
| 249 | |
| 250 let level4 = t6rp.levels[4 - 1].as_ref().unwrap(); | |
| 251 assert_eq!(level4.score, 7837690); | |
| 252 assert_eq!(level4.random_seed, 0xbf81); | |
| 253 assert_eq!(level4.point_items, 0); | |
| 254 assert_eq!(level4.power, 128); | |
| 255 assert_eq!(level4.lives, 2); | |
| 256 assert_eq!(level4.bombs, 3); | |
| 257 assert_eq!(level4.difficulty, 16); | |
| 258 assert_eq!(level4.keys.len(), 322); | |
| 259 } | |
| 260 } |
