Mercurial > touhou
view 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 |
line wrap: on
line source
//! Touhou 6 Replay (T6RP) format support. //! //! This module provides classes for handling the Touhou 6 Replay file format. //! The T6RP file format is an encrypted format describing different aspects of //! a game of EoSD. Since the EoSD engine is entirely deterministic, a small //! replay file is sufficient to unfold a full game. use std::convert::TryInto; use nom::{ bytes::complete::tag, multi::count, number::complete::{le_f32, le_i8, le_u16, le_u32, le_u8}, IResult, Parser, }; /// A single level of a replay. #[derive(Debug, Clone, Default)] pub struct Level { /// The score before starting the level. pub score: u32, /// The seed initializing the PRNG. pub random_seed: u16, /// The amount of point items collected before starting this level. pub point_items: u16, /// The power level at the beginning of the level. pub power: u8, /// How many lives are remaining before starting this level. pub lives: i8, /// How many bombs are remaining before starting this level. pub bombs: i8, /// The hidden difficulty at the beginning of this level. pub difficulty: u8, unknown: u32, /// The list of keys pressed during this level. pub keys: Vec<(u32, u16, u16)>, } /// The unencrypted part of a replay. #[derive(Debug, Clone)] pub struct Header { /// The version, only 1.02 is supported so far. pub version: u16, /// The character picked by the player, 0 = Reimu A, 3 = Marisa B. pub character: u8, /// The rank picked by the player, 0 = Easy, 4 = Extra. pub rank: u8, /// The checksum of the encrypted part of this replay. pub checksum: u32, unknown1: u8, unknown2: u8, /// The encryption key for the rest of this replay. pub key: u8, } /// The encrypted part of a replay. #[derive(Debug, Clone)] pub struct T6rp { unknown3: u8, /// The date at which this replay got recorded. pub date: [u8; 9], /// The name of the player. pub name: [u8; 9], unknown4: u16, /// The score at the end of this replay. pub score: u32, unknown5: u32, /// How much slowdown has been experienced while recording this replay. pub slowdown: f32, unknown6: u32, /// Each of the stages that got recorded, only 0..=6 or 7 can be set. pub levels: [Option<Level>; 7], } // TODO: Add a newtype for `encrypted`, to provide the decrypt() method on it and then another // newtype for the decrypted data to have verify() and parse(). impl T6rp { /// Parse a slice of bytes into a `Header` struct, which is the unencrypted part of T6RP. pub fn parse_header(input: &[u8]) -> IResult<&[u8], Header> { let (encrypted, (_, version, character, rank, checksum, unknown1, unknown2, key)) = ( tag(&b"T6RP"[..]), le_u16, le_u8, le_u8, le_u32, le_u8, le_u8, le_u8, ) .parse(input)?; Ok(( encrypted, Header { version, character, rank, checksum, unknown1, unknown2, key, }, )) } /// Decrypt the contents of the replay with the header’s key. pub fn decrypt(key: u8, encrypted: &[u8]) -> Vec<u8> { let mut decrypted = Vec::with_capacity(encrypted.len()); for (i, c) in encrypted.iter().enumerate() { decrypted.push(c.wrapping_sub(key).wrapping_sub((7 * i) as u8)); } decrypted } /// Verify decrypted data against the header’s checksum. pub fn verify(key: u8, checksum: u32, data: &[u8]) -> bool { let real_sum = data .iter() .map(|c| *c as u32) .sum::<u32>() .wrapping_add(0x3f000318) .wrapping_add(key as u32); checksum == real_sum } /// Parse a slice of bytes into a `T6rp` struct. pub fn from_slice(input: &[u8]) -> IResult<&[u8], T6rp> { let ( _, (unknown3, date, name, unknown4, score, unknown5, slowdown, unknown6, stage_offsets), ) = ( le_u8, count(le_u8, 9), count(le_u8, 9), le_u16, le_u32, le_u32, le_f32, le_u32, count(le_u32, 7), ) .parse(input)?; let date = date.try_into().unwrap(); let name = name.try_into().unwrap(); let mut levels = [const { None }; 7]; for (index, offset) in stage_offsets.into_iter().enumerate() { if offset == 0 { continue; } let data = &input[offset as usize - 15..]; let ( mut data, (score, random_seed, point_items, power, lives, bombs, difficulty, unknown), ) = (le_u32, le_u16, le_u16, le_u8, le_i8, le_i8, le_u8, le_u32).parse(data)?; let mut keys = Vec::new(); loop { let (data2, (time, keystate, unknown)) = (le_u32, le_u16, le_u16).parse(data)?; data = data2; if time == 9999999 { break; } keys.push((time, keystate, unknown)); } levels[index] = Some(Level { score, random_seed, point_items, power, lives, bombs, difficulty, unknown, keys, }); } Ok(( b"", T6rp { unknown3, date, name, unknown4, score, unknown5, slowdown, unknown6, levels, }, )) } } #[cfg(test)] mod tests { use super::*; use std::fs::File; use std::io::{self, Read}; #[test] fn t6rp() { let file = File::open("EoSD/CM.bak/demo00.rpy").unwrap(); let mut file = io::BufReader::new(file); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); let (encrypted, header) = T6rp::parse_header(&buf).unwrap(); assert_eq!(header.version, 0x0102); assert_eq!(header.character, 0); assert_eq!(header.rank, 3); assert_eq!(header.checksum, 0x3f03188d); assert_eq!(header.key, 0x9d); let data = T6rp::decrypt(header.key, &encrypted); assert!(T6rp::verify(header.key, header.checksum, &data)); let (_, t6rp) = T6rp::from_slice(&data).unwrap(); assert_eq!(t6rp.date, *b"08/21/02\0"); assert_eq!(t6rp.name, *b"A \0"); assert_eq!(t6rp.score, 7837690); assert_eq!(t6rp.slowdown, 0.62111616); let level4 = t6rp.levels[4 - 1].as_ref().unwrap(); assert_eq!(level4.score, 7837690); assert_eq!(level4.random_seed, 0xbf81); assert_eq!(level4.point_items, 0); assert_eq!(level4.power, 128); assert_eq!(level4.lives, 2); assert_eq!(level4.bombs, 3); assert_eq!(level4.difficulty, 16); assert_eq!(level4.keys.len(), 322); } }
