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 }