physis/
exd.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#![allow(unused_variables)] // just binrw things with br(temp)
5
6use std::io::{BufWriter, Cursor};
7
8use binrw::BinRead;
9use binrw::{BinWrite, binrw};
10
11use crate::common::Language;
12use crate::exd_file_operations::{parse_rows, read_data_sections, write_rows};
13use crate::exh::{EXH, ExcelDataPagination};
14use crate::{ByteBuffer, ByteSpan};
15
16#[binrw]
17#[brw(magic = b"EXDF")]
18#[brw(big)]
19#[allow(dead_code)]
20#[derive(Debug)]
21pub(crate) struct EXDHeader {
22    /// Usually 2, I don't think I've seen any other version
23    pub(crate) version: u16,
24    /// Seems to be 0?
25    pub(crate) unk1: u16,
26    /// Size of the data offsets in bytes
27    pub(crate) data_offset_size: u32,
28    #[brw(pad_after = 16)] // padding
29    /// Size of the data sections in bytes
30    pub(crate) data_section_size: u32,
31}
32
33#[binrw]
34#[brw(big)]
35#[derive(Debug)]
36pub(crate) struct ExcelDataOffset {
37    /// The row ID associated with this data offset
38    pub(crate) row_id: u32,
39    /// Offset to it's data section in bytes from the start of the file.
40    pub(crate) offset: u32,
41}
42
43#[binrw]
44#[brw(big)]
45#[allow(dead_code)]
46#[derive(Debug)]
47pub(crate) struct DataSection {
48    /// Size of the data section in bytes.
49    pub(crate) size: u32,
50    /// The number of rows in this data section.
51    pub(crate) row_count: u16,
52    /// The bytes of this data section.
53    /// We currently don't use this in our parsing, see parse_rows.
54    #[br(temp, count = size)]
55    #[bw(ignore)]
56    data: Vec<u8>,
57}
58
59/// An Excel data file. Represents a page in an Excel Sheet.
60#[binrw]
61#[brw(big)]
62#[allow(dead_code)]
63#[derive(Debug)]
64#[brw(import(exh: &EXH))]
65pub struct EXD {
66    header: EXDHeader,
67
68    #[br(count = header.data_offset_size / core::mem::size_of::<ExcelDataOffset>() as u32)]
69    #[bw(ignore)] // write_rows handles writing this
70    data_offsets: Vec<ExcelDataOffset>,
71
72    #[br(parse_with = read_data_sections, args(&header))]
73    #[bw(ignore)] // write_rows handles writing this
74    data: Vec<DataSection>,
75
76    /// The rows contained in this EXD.
77    #[br(parse_with = parse_rows, args(exh, &data_offsets))]
78    #[bw(write_with = write_rows, args(exh))]
79    pub rows: Vec<ExcelRow>,
80}
81
82#[derive(Debug, Clone, PartialEq)]
83pub enum ColumnData {
84    String(String),
85    Bool(bool),
86    Int8(i8),
87    UInt8(u8),
88    Int16(i16),
89    UInt16(u16),
90    Int32(i32),
91    UInt32(u32),
92    Float32(f32),
93    Int64(i64),
94    UInt64(u64),
95}
96
97impl ColumnData {
98    // Returns a Some(String) if this column was a String, otherwise None.
99    pub fn into_string(&self) -> Option<&String> {
100        if let ColumnData::String(value) = self {
101            return Some(value);
102        }
103        None
104    }
105
106    // Returns a Some(bool) if this column was a Bool, otherwise None.
107    pub fn into_bool(&self) -> Option<&bool> {
108        if let ColumnData::Bool(value) = self {
109            return Some(value);
110        }
111        None
112    }
113
114    // Returns a Some(i8) if this column was a Int8, otherwise None.
115    pub fn into_i8(&self) -> Option<&i8> {
116        if let ColumnData::Int8(value) = self {
117            return Some(value);
118        }
119        None
120    }
121
122    // Returns a Some(u8) if this column was a UInt8, otherwise None.
123    pub fn into_u8(&self) -> Option<&u8> {
124        if let ColumnData::UInt8(value) = self {
125            return Some(value);
126        }
127        None
128    }
129
130    // Returns a Some(i16) if this column was a Int16, otherwise None.
131    pub fn into_i16(&self) -> Option<&i16> {
132        if let ColumnData::Int16(value) = self {
133            return Some(value);
134        }
135        None
136    }
137
138    // Returns a Some(u16) if this column was a UInt16, otherwise None.
139    pub fn into_u16(&self) -> Option<&u16> {
140        if let ColumnData::UInt16(value) = self {
141            return Some(value);
142        }
143        None
144    }
145
146    // Returns a Some(i32) if this column was a Int32, otherwise None.
147    pub fn into_i32(&self) -> Option<&i32> {
148        if let ColumnData::Int32(value) = self {
149            return Some(value);
150        }
151        None
152    }
153
154    // Returns a Some(u32) if this column was a UInt32, otherwise None.
155    pub fn into_u32(&self) -> Option<&u32> {
156        if let ColumnData::UInt32(value) = self {
157            return Some(value);
158        }
159        None
160    }
161
162    // Returns a Some(f32) if this column was a Float32, otherwise None.
163    pub fn into_f32(&self) -> Option<&f32> {
164        if let ColumnData::Float32(value) = self {
165            return Some(value);
166        }
167        None
168    }
169
170    // Returns a Some(i64) if this column was a Int64, otherwise None.
171    pub fn into_i64(&self) -> Option<&i64> {
172        if let ColumnData::Int64(value) = self {
173            return Some(value);
174        }
175        None
176    }
177
178    // Returns a Some(u64) if this column was a UInt64, otherwise None.
179    pub fn into_u64(&self) -> Option<&u64> {
180        if let ColumnData::UInt64(value) = self {
181            return Some(value);
182        }
183        None
184    }
185}
186
187#[derive(Debug, Clone, PartialEq)]
188pub struct ExcelSingleRow {
189    pub columns: Vec<ColumnData>,
190}
191
192#[derive(Debug, Clone, PartialEq)]
193pub enum ExcelRowKind {
194    SingleRow(ExcelSingleRow),
195    SubRows(Vec<ExcelSingleRow>),
196}
197
198/// Represents an entry in the EXD.
199#[derive(Debug)]
200pub struct ExcelRow {
201    /// The row ID associated with this entry.
202    pub row_id: u32,
203    /// The kind of entry.
204    pub kind: ExcelRowKind,
205}
206
207impl EXD {
208    /// Parse an EXD from an existing file.
209    pub fn from_existing(exh: &EXH, buffer: ByteSpan) -> Option<EXD> {
210        EXD::read_args(&mut Cursor::new(&buffer), (exh,)).ok()
211    }
212
213    /// Finds the entry with the specified ID, otherwise returns `None`.
214    pub fn get_row(&self, row_id: u32) -> Option<ExcelRowKind> {
215        for row in &self.rows {
216            if row.row_id == row_id {
217                return Some(row.kind.clone());
218            }
219        }
220
221        None
222    }
223
224    /// Calculate the filename of an EXD from the `name`, `language`, and `page`.
225    pub fn calculate_filename(
226        name: &str,
227        language: Language,
228        page: &ExcelDataPagination,
229    ) -> String {
230        use crate::common::get_language_code;
231
232        match language {
233            Language::None => {
234                format!("{name}_{}.exd", page.start_id)
235            }
236            lang => {
237                format!("{name}_{}_{}.exd", page.start_id, get_language_code(&lang))
238            }
239        }
240    }
241
242    /// Write this EXD back to it's serialized binary form.
243    pub fn write_to_buffer(&self, exh: &EXH) -> Option<ByteBuffer> {
244        let mut buffer = ByteBuffer::new();
245
246        {
247            let cursor = Cursor::new(&mut buffer);
248            let mut writer = BufWriter::new(cursor);
249
250            self.write_args(&mut writer, (exh,)).unwrap();
251        }
252
253        Some(buffer)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use crate::exh::EXHHeader;
260    use std::fs::read;
261    use std::path::PathBuf;
262
263    use super::*;
264
265    #[test]
266    fn test_invalid() {
267        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
268        d.push("resources/tests");
269        d.push("random");
270
271        let exh = EXH {
272            header: EXHHeader {
273                version: 0,
274                data_offset: 0,
275                column_count: 0,
276                page_count: 0,
277                language_count: 0,
278                row_count: 0,
279                unk1: 0,
280            },
281            column_definitions: vec![],
282            pages: vec![],
283            languages: vec![],
284        };
285
286        // Feeding it invalid data should not panic
287        EXD::from_existing(&exh, &read(d).unwrap());
288    }
289
290    // super simple EXD to read, it's just a few rows of only int8's
291    #[test]
292    fn test_read() {
293        // exh
294        let exh;
295        {
296            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
297            d.push("resources/tests");
298            d.push("gcshop.exh");
299
300            exh = EXH::from_existing(&read(d).unwrap()).unwrap();
301        }
302
303        // exd
304        let exd;
305        {
306            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
307            d.push("resources/tests");
308            d.push("gcshop_1441792.exd");
309
310            exd = EXD::from_existing(&exh, &read(d).unwrap()).unwrap();
311        }
312
313        assert_eq!(exd.rows.len(), 4);
314
315        // row 0
316        assert_eq!(exd.rows[0].row_id, 1441792);
317        assert_eq!(
318            exd.rows[0].kind,
319            ExcelRowKind::SingleRow(ExcelSingleRow {
320                columns: vec![ColumnData::Int8(0)]
321            })
322        );
323
324        // row 1
325        assert_eq!(exd.rows[1].row_id, 1441793);
326        assert_eq!(
327            exd.rows[1].kind,
328            ExcelRowKind::SingleRow(ExcelSingleRow {
329                columns: vec![ColumnData::Int8(1)]
330            })
331        );
332
333        // row 2
334        assert_eq!(exd.rows[2].row_id, 1441794);
335        assert_eq!(
336            exd.rows[2].kind,
337            ExcelRowKind::SingleRow(ExcelSingleRow {
338                columns: vec![ColumnData::Int8(2)]
339            })
340        );
341
342        // row 3
343        assert_eq!(exd.rows[3].row_id, 1441795);
344        assert_eq!(
345            exd.rows[3].kind,
346            ExcelRowKind::SingleRow(ExcelSingleRow {
347                columns: vec![ColumnData::Int8(3)]
348            })
349        );
350    }
351
352    // super simple EXD to write, it's just a few rows of only int8's
353    #[test]
354    fn test_write() {
355        // exh
356        let exh;
357        {
358            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
359            d.push("resources/tests");
360            d.push("gcshop.exh");
361
362            exh = EXH::from_existing(&read(d).unwrap()).unwrap();
363        }
364
365        // exd
366        let expected_exd_bytes;
367        let expected_exd;
368        {
369            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
370            d.push("resources/tests");
371            d.push("gcshop_1441792.exd");
372
373            expected_exd_bytes = read(d).unwrap();
374            expected_exd = EXD::from_existing(&exh, &expected_exd_bytes).unwrap();
375        }
376
377        let actual_exd_bytes = expected_exd.write_to_buffer(&exh).unwrap();
378        assert_eq!(actual_exd_bytes, expected_exd_bytes);
379    }
380
381    // slightly more complex to read, because it has STRINGS
382    #[test]
383    fn test_read_strings() {
384        // exh
385        let exh;
386        {
387            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
388            d.push("resources/tests");
389            d.push("openingsystemdefine.exh");
390
391            exh = EXH::from_existing(&read(d).unwrap()).unwrap();
392        }
393
394        // exd
395        let exd;
396        {
397            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
398            d.push("resources/tests");
399            d.push("openingsystemdefine_0.exd");
400
401            exd = EXD::from_existing(&exh, &read(d).unwrap()).unwrap();
402        }
403
404        assert_eq!(exd.rows.len(), 8);
405
406        // row 0
407        assert_eq!(exd.rows[0].row_id, 0);
408        assert_eq!(
409            exd.rows[0].kind,
410            ExcelRowKind::SingleRow(ExcelSingleRow {
411                columns: vec![
412                    ColumnData::String("HOWTO_MOVE_AND_CAMERA".to_string()),
413                    ColumnData::UInt32(1)
414                ]
415            })
416        );
417
418        // row 1
419        assert_eq!(exd.rows[1].row_id, 1);
420        assert_eq!(
421            exd.rows[1].kind,
422            ExcelRowKind::SingleRow(ExcelSingleRow {
423                columns: vec![
424                    ColumnData::String("HOWTO_ANNOUNCE_AND_QUEST".to_string()),
425                    ColumnData::UInt32(2)
426                ]
427            })
428        );
429
430        // row 2
431        assert_eq!(exd.rows[2].row_id, 2);
432        assert_eq!(
433            exd.rows[2].kind,
434            ExcelRowKind::SingleRow(ExcelSingleRow {
435                columns: vec![
436                    ColumnData::String("HOWTO_QUEST_REWARD".to_string()),
437                    ColumnData::UInt32(11)
438                ]
439            })
440        );
441
442        // row 3
443        assert_eq!(exd.rows[3].row_id, 3);
444        assert_eq!(
445            exd.rows[3].kind,
446            ExcelRowKind::SingleRow(ExcelSingleRow {
447                columns: vec![
448                    ColumnData::String("BGM_MUSIC_NO_MUSIC".to_string()),
449                    ColumnData::UInt32(1001)
450                ]
451            })
452        );
453
454        // row 4
455        assert_eq!(exd.rows[4].row_id, 4);
456        assert_eq!(
457            exd.rows[4].kind,
458            ExcelRowKind::SingleRow(ExcelSingleRow {
459                columns: vec![
460                    ColumnData::String("ITEM_INITIAL_RING_A".to_string()),
461                    ColumnData::UInt32(4423)
462                ]
463            })
464        );
465
466        // row 5
467        assert_eq!(exd.rows[5].row_id, 5);
468        assert_eq!(
469            exd.rows[5].kind,
470            ExcelRowKind::SingleRow(ExcelSingleRow {
471                columns: vec![
472                    ColumnData::String("ITEM_INITIAL_RING_B".to_string()),
473                    ColumnData::UInt32(4424)
474                ]
475            })
476        );
477
478        // row 6
479        assert_eq!(exd.rows[6].row_id, 6);
480        assert_eq!(
481            exd.rows[6].kind,
482            ExcelRowKind::SingleRow(ExcelSingleRow {
483                columns: vec![
484                    ColumnData::String("ITEM_INITIAL_RING_C".to_string()),
485                    ColumnData::UInt32(4425)
486                ]
487            })
488        );
489
490        // row 7
491        assert_eq!(exd.rows[7].row_id, 7);
492        assert_eq!(
493            exd.rows[7].kind,
494            ExcelRowKind::SingleRow(ExcelSingleRow {
495                columns: vec![
496                    ColumnData::String("ITEM_INITIAL_RING_D".to_string()),
497                    ColumnData::UInt32(4426)
498                ]
499            })
500        );
501    }
502
503    // slightly more complex to write, because it has STRINGS
504    #[test]
505    fn test_write_strings() {
506        // exh
507        let exh;
508        {
509            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
510            d.push("resources/tests");
511            d.push("openingsystemdefine.exh");
512
513            exh = EXH::from_existing(&read(d).unwrap()).unwrap();
514        }
515
516        // exd
517        let expected_exd_bytes;
518        let expected_exd;
519        {
520            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
521            d.push("resources/tests");
522            d.push("openingsystemdefine_0.exd");
523
524            expected_exd_bytes = read(d).unwrap();
525            expected_exd = EXD::from_existing(&exh, &expected_exd_bytes).unwrap();
526        }
527
528        let actual_exd_bytes = expected_exd.write_to_buffer(&exh).unwrap();
529        assert_eq!(actual_exd_bytes, expected_exd_bytes);
530    }
531
532    // this doesn't have any strings, but a LOT of columns and some packed booleans!
533    #[test]
534    fn test_write_many_columns() {
535        // exh
536        let exh;
537        {
538            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
539            d.push("resources/tests");
540            d.push("physicsgroup.exh");
541
542            exh = EXH::from_existing(&read(d).unwrap()).unwrap();
543        }
544
545        // exd
546        let expected_exd_bytes;
547        let expected_exd;
548        {
549            let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
550            d.push("resources/tests");
551            d.push("physicsgroup_1.exd");
552
553            expected_exd_bytes = read(d).unwrap();
554            expected_exd = EXD::from_existing(&exh, &expected_exd_bytes).unwrap();
555        }
556
557        let actual_exd_bytes = expected_exd.write_to_buffer(&exh).unwrap();
558        assert_eq!(actual_exd_bytes, expected_exd_bytes);
559    }
560}