Skip to main content

ygopro_data/data/
replay.rs

1//! Replay recording and replay files.
2//!
3//! Provides the replay header/flags and the replay (de)serialization, including
4//! lzma compression.
5
6use std::io::Cursor;
7use std::io::Read;
8use std::ops::Deref;
9
10use binrw::BinRead;
11use binrw::BinResult;
12use binrw::BinWrite;
13use binrw::binrw;
14use binrw::helpers::until_eof;
15use bitflags::bitflags;
16
17use lzma_rs::lzma_compress_with_options;
18use lzma_rs::lzma_decompress_with_options;
19
20use crate::constants::Mode;
21use crate::constants::Rule;
22use crate::data::Deck;
23use crate::data::Response;
24use crate::message::HostInfo;
25use crate::utils::string::FixedLengthString;
26
27const SIZE_REPLAY_SEED: usize = 8;
28
29#[repr(u32)]
30pub enum ReplayVersion {
31    /// string "yrp1"
32    V1 = 0x31707279,
33    /// string "yrp2"
34    V2 = 0x32707279
35}
36
37bitflags! {
38    #[derive(BinRead, BinWrite, Clone, Debug, PartialEq, Eq)]
39    #[br(map=|x: u32| Self::from_bits_retain(x))]
40    #[bw(map=|x: &Self| x.bits())]
41    pub struct ReplayHeaderFlags: u32 {
42        const Compressed = 1;
43        const Tag = 2;
44        const Decode = 4;
45        const SingleMode = 8;
46        const Uniform = 16;
47    }
48}
49
50bitflags! {
51    #[derive(BinRead, BinWrite, Clone, Debug)]
52    #[br(map=|x: u16| Self::from_bits_retain(x))]
53    #[bw(map=|x: &Self| x.bits())]
54    pub struct DuelOptions: u16 {
55        const TestMode = 0x1;
56        const AttackFirstTurn = 0x2;
57        const OldReplay = 0x4;
58        const ObsoleteRuling = 0x8;
59        const PseudoShuffle = 0x10;
60        const TagMode = 0x20;
61        const SimpleAI = 0x40;
62        const ReturnDeckTop = 0x80;
63        const RevealDeckSequence = 0x100;
64    }
65}
66
67bitflags! {
68    #[derive(BinRead, BinWrite, Clone, Debug)]
69    #[br(map=|x: u32| Self::from_bits_retain(x))]
70    #[bw(map=|x: &Self| x.bits())]
71    pub struct ReplayMode: u32 {
72        const SaveInServer = 1;
73        const WatcherNoSend = 2;
74        const IncludeChat = 4;
75    }
76}
77
78#[derive(BinRead, BinWrite, Clone, Debug)]
79pub struct ReplayHeader {
80    pub id: u32,
81    pub version: u32,
82    pub flag: ReplayHeaderFlags,
83    pub seed: u32,
84    pub data_size: u32,
85    pub start_time: u32,
86    pub props: [u8; 8],
87    #[br(if(id == ReplayVersion::V2 as u32))]
88    #[bw(if(*id == ReplayVersion::V2 as u32))]
89    pub seed_sequence: [u32; SIZE_REPLAY_SEED],
90    #[br(if(id == ReplayVersion::V2 as u32))]
91    #[bw(if(*id == ReplayVersion::V2 as u32))]
92    pub header_version: u32,
93    #[br(if(id == ReplayVersion::V2 as u32))]
94    #[bw(if(*id == ReplayVersion::V2 as u32))]
95    pub reserved: [u32; 3],
96}
97
98impl ReplayHeader {
99    pub fn is_compressed(&self) -> bool { self.flag.contains(ReplayHeaderFlags::Compressed) }
100    pub fn is_tag(&self)        -> bool { self.flag.contains(ReplayHeaderFlags::Tag) }
101    pub fn is_decoded(&self)    -> bool { self.flag.contains(ReplayHeaderFlags::Decode) }
102    pub fn is_single_mode(&self)-> bool { self.flag.contains(ReplayHeaderFlags::SingleMode) }
103    pub fn is_uniform(&self)    -> bool { self.flag.contains(ReplayHeaderFlags::Uniform) }
104}
105
106/// Deck saved in replay.
107/// 
108/// Replay Deck binary layout is different from [`Deck`].
109#[binrw]
110#[derive(PartialEq, Eq, Debug, Clone, Default)]
111pub struct ReplayDeck {
112    #[bw(calc = main.len() as u32)]
113    main_size: u32,
114    #[br(count = main_size)]
115    pub main: Vec<u32>,
116    #[bw(calc = extra.len() as u32)]
117    extra_size: u32,
118    #[br(count = extra_size)]
119    pub extra: Vec<u32>,
120}
121
122impl From<Deck> for ReplayDeck {
123    fn from(value: Deck) -> Self {
124        let mut main = value.main;
125        let mut extra = value.extra;
126        main.reverse();
127        extra.reverse();
128        Self { main, extra }
129    }
130}
131
132impl From<ReplayDeck> for Deck {
133    fn from(value: ReplayDeck) -> Self {
134        let mut main = value.main.clone();
135        let mut extra = value.extra.clone();
136        main.reverse();
137        extra.reverse();
138        Self { main, side: vec![], extra }
139    }
140}
141
142#[binrw]
143#[derive(Clone, Debug)]
144#[br(import(header: &ReplayHeader))]
145#[bw(import(header: &ReplayHeader))]
146pub struct ReplayBody {
147    pub host_name: FixedLengthString<20>,
148    #[br(if(header.is_tag()))]
149    #[bw(if(header.is_tag()))]
150    pub tag_host_name: Option<FixedLengthString<20>>,
151    #[br(if(header.is_tag()))]
152    #[bw(if(header.is_tag()))]
153    pub tag_client_name: Option<FixedLengthString<20>>,
154    pub client_name: FixedLengthString<20>,
155    pub start_lp: u32,
156    pub start_hand: u32,
157    pub draw_count: u32,
158    // pub opt: u32, -> Split into two parts...
159    pub duel_options: DuelOptions,
160    pub duel_rule: u16,
161    pub host_deck: ReplayDeck,
162    #[br(if(header.is_tag()))]
163    #[bw(if(header.is_tag()))]
164    pub tag_host_deck: Option<ReplayDeck>,
165    pub client_deck: ReplayDeck,
166    #[br(if(header.is_tag()))]
167    #[bw(if(header.is_tag()))]
168    pub tag_client_deck: Option<ReplayDeck>,
169    #[br(parse_with=until_eof)]
170    pub datas: Vec<ReplayData>
171}
172
173#[binrw]
174#[derive(Clone, Debug)]
175pub struct ReplayData {
176    #[bw(calc(data.len() as u8))]
177    size: u8,
178    #[br(count = size, map = |bytes: Vec<u8>| Response::Unknown(bytes))]
179    pub data: Response
180}
181
182impl From<Response> for ReplayData {
183    fn from(value: Response) -> Self {
184        ReplayData { data: value }
185    }
186}
187
188#[derive(BinRead, BinWrite, Debug, Clone)]
189pub struct Replay {
190    pub header: ReplayHeader,
191    #[br(parse_with = replay_parser, args(&header))]
192    #[bw(write_with = replay_writer, args(&header))]
193    pub body: ReplayBody
194}
195
196impl Replay {
197    pub fn fill_data_size(&mut self) {
198        let host = &self.body.host_deck;
199        let client = &self.body.client_deck;
200        let mut size = 40 + 40
201            + 4 + 4 + 4 + 2 + 2
202            + 4 + host.main.len() as u32 * 4 + 4 + host.extra.len() as u32 * 4
203            + 4 + client.main.len() as u32 * 4 + 4 + client.extra.len() as u32 * 4
204            + self.body.datas.iter().map(|d| 1 + d.data.len() as u32).sum::<u32>();
205            
206        if self.header.is_tag() {
207            size += 40 + 40;
208            if let Some(ref tag_host) = self.body.tag_host_deck {
209                size += 4 + tag_host.main.len() as u32 * 4 + 4 + tag_host.extra.len() as u32 * 4;
210            }
211            if let Some(ref tag_client) = self.body.tag_client_deck {
212                size += 4 + tag_client.main.len() as u32 * 4 + 4 + tag_client.extra.len() as u32 * 4;
213            }
214        }
215        self.header.data_size = size;
216    }
217
218    pub fn duel_rule(&self) -> crate::constants::MasterRule { 
219        if self.duel_options.contains(DuelOptions::ObsoleteRuling) { crate::constants::MasterRule::MasterRule1 }
220        else { crate::constants::MasterRule::try_from(self.duel_rule as u8).unwrap_or(crate::constants::MasterRule::MasterRule1) }
221    }
222
223    pub fn mode(&self) -> Mode {
224        if self.duel_options.contains(DuelOptions::TagMode) { Mode::Tag }
225        else { Mode::Single }
226    } 
227    pub fn no_shuffle_deck(&self) -> bool { self.duel_options.contains(DuelOptions::PseudoShuffle) }
228    pub fn is_tag(&self) -> bool { self.duel_options.contains(DuelOptions::TagMode) }
229
230    pub fn host_info(&self) -> HostInfo {
231        HostInfo { 
232            lflist: 999,
233            rule: Rule::OCG,
234            mode: self.mode(),
235            duel_rule: self.duel_rule(), 
236            no_check_deck: true,
237            no_shuffle_deck: self.no_shuffle_deck(), 
238            start_lp: self.start_lp, 
239            start_hand: self.start_hand as u8, 
240            draw_count: self.draw_count as u8, 
241            time_limit: 0 
242        }
243    }
244}
245
246impl Deref for Replay {
247    type Target = ReplayBody;
248
249    fn deref(&self) -> &Self::Target {
250        &self.body
251    }
252}
253
254#[derive(BinRead)]
255struct ReadHelper {
256    #[br(parse_with = until_eof)]
257    content: Vec<u8>
258}
259
260// ==================================================
261// Correct order: 
262// prop  dict_size  datasize
263//  93    0 0 0 1     u64
264// Ygopro replay header:
265// datasize  prop  dict_size  padding
266//   u32      93    0 0 0 1    0 0 0
267// ==================================================
268#[binrw::parser(reader, endian)]
269fn replay_parser(header: &ReplayHeader) -> BinResult<ReplayBody> {
270    let leading_props = Cursor::new(&header.props[0..5]);
271    let helper = ReadHelper::read_options(reader, endian, ())?;
272    let compressed_data = helper.content;
273    let decompressed_data = if header.is_compressed() {
274        let mut compressed_data = leading_props.chain(Cursor::new(compressed_data));
275        let mut decompressed_data = Vec::new();
276        lzma_decompress_with_options(&mut compressed_data, &mut decompressed_data, &lzma_rs::decompress::Options { 
277            unpacked_size: lzma_rs::decompress::UnpackedSize::UseProvided(Some(header.data_size as u64)), 
278            memlimit: None,
279            allow_incomplete: false 
280        }).map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
281        decompressed_data
282    }
283    else { compressed_data };
284    <_>::read_options(&mut Cursor::new(decompressed_data), endian, (header,))
285}
286
287// need fix header inner
288#[binrw::writer(writer, endian)]
289fn replay_writer(body: &ReplayBody, header: &ReplayHeader) -> BinResult<()> {
290    let mut decompressed_data = Cursor::new(Vec::new());
291    body.write_options(&mut decompressed_data, endian, (header,))?;
292    let compressed_data = if header.is_compressed() {
293        let mut compressed_data = Cursor::new(Vec::new());
294        decompressed_data.set_position(0);
295        lzma_compress_with_options(&mut decompressed_data, &mut compressed_data, &lzma_rs::compress::Options { 
296            unpacked_size: lzma_rs::compress::UnpackedSize::SkipWritingToHeader
297        })?;
298        let mut data = compressed_data.into_inner();
299        data.drain(..5); // replay_parser prepends header.props[0..5]; strip encoder's lzma header
300        data
301    } else { decompressed_data.into_inner() };
302    compressed_data.write_options(writer, endian, ())
303}
304
305mod test {
306    #![allow(unused_imports)]
307
308    use std::io::Cursor;
309    use binrw::BinRead;
310    use crate::data::Replay;
311
312    #[test]
313    #[ignore]
314    fn test_deserialize_replay() {
315       let arr = std::fs::read("/Users/iami/Downloads/极羽光_vs_爱尔琳妮_20260531225205_G1.yrp").unwrap();
316       let mut reader = Cursor::new(arr);
317       let replay = Replay::read_le(&mut reader);
318       println!("{:?}", replay);
319    }
320
321    #[test]
322    fn test_replay_roundtrip() {
323        use binrw::BinWrite;
324        use crate::data::ReplayHeader;
325        use crate::data::ReplayHeaderFlags;
326        use crate::data::ReplayVersion;
327        use crate::data::ReplayBody;
328        use crate::data::ReplayDeck;
329        use crate::data::ReplayData;
330        use crate::data::Response;
331        use crate::data::DuelOptions;
332        use crate::utils::string::FixedLengthString;
333
334        let mut original = Replay {
335            header: ReplayHeader {
336                id: ReplayVersion::V2 as u32,
337                version: 0x1362,
338                flag: ReplayHeaderFlags::Uniform | ReplayHeaderFlags::Compressed,
339                seed: 0,
340                data_size: 0,
341                start_time: 1234567890,
342                props: [93, 0, 0, 128, 0, 0, 0, 0],
343                seed_sequence: [1, 2, 3, 4, 5, 6, 7, 8],
344                header_version: 1,
345                reserved: [0; 3],
346            },
347            body: ReplayBody {
348                host_name: FixedLengthString::new("Host".to_string()),
349                client_name: FixedLengthString::new("Client".to_string()),
350                tag_host_name: None,
351                tag_client_name: None,
352                start_lp: 8000,
353                start_hand: 5,
354                draw_count: 1,
355                duel_options: DuelOptions::empty(),
356                duel_rule: 5,
357                host_deck: ReplayDeck::default(),
358                client_deck: ReplayDeck::default(),
359                tag_host_deck: None,
360                tag_client_deck: None,
361                datas: vec![ReplayData { data: Response::Unknown(vec![1, 2, 3]) }],
362            },
363        };
364        original.fill_data_size();
365
366        let mut buf = Cursor::new(Vec::new());
367        original.write_le(&mut buf).unwrap();
368        let written = buf.into_inner();
369
370        let decoded = Replay::read_le(&mut Cursor::new(written)).unwrap();
371        assert_eq!(decoded.header.id, original.header.id);
372        assert_eq!(decoded.header.version, original.header.version);
373        assert_eq!(decoded.header.flag, original.header.flag);
374        assert_eq!(decoded.header.data_size, original.header.data_size);
375        assert_eq!(decoded.header.seed_sequence, original.header.seed_sequence);
376        assert_eq!(decoded.body.host_name.to_string(), original.body.host_name.to_string());
377        assert_eq!(decoded.body.client_name.to_string(), original.body.client_name.to_string());
378        assert_eq!(decoded.body.start_lp, original.body.start_lp);
379        assert_eq!(decoded.body.duel_rule, original.body.duel_rule);
380        assert_eq!(decoded.body.datas.len(), original.body.datas.len());
381    }
382}