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