From 36304a2e574b75b9862268eacfe88b0d1dbfa99e Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 10 May 2025 21:38:47 -0400 Subject: [PATCH] Finish basic EXD write support, add tests for GCShop Expanding upon the EXH tests of this same data, we can successfully write it back (hence why it's testdata now!) A lot of the writing logic is specific for this EXD, but it's a start. --- resources/tests/gcshop_1441792.exd | Bin 0 -> 112 bytes src/exd.rs | 172 +++++++++++++++++++++++++---- src/exh.rs | 3 +- 3 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 resources/tests/gcshop_1441792.exd diff --git a/resources/tests/gcshop_1441792.exd b/resources/tests/gcshop_1441792.exd new file mode 100644 index 0000000000000000000000000000000000000000..31ce8e6c9ae6159d5ef034ceba79e10a4efd95d3 GIT binary patch literal 112 zcmZ>baB*W`VgLaJAT|IoP=FYi=fEJwzzAgd0BMlw2q4W2q*H*H4J-p>K-eH BinResult> { + let mut rows = Vec::new(); + + // we have to do this annoying thing because they specified it in bytes, + // not an actual count of data sections + let begin_pos = reader.stream_position().unwrap(); + loop { + let current_pos = reader.stream_position().unwrap(); + if current_pos - begin_pos >= header.data_section_size as u64 { + break; + } + + let data_section = DataSection::read_be(reader).unwrap(); + rows.push(data_section); + } + + Ok(rows) } #[binrw::parser(reader)] @@ -49,7 +60,10 @@ fn parse_rows(exh: &EXH, data_offsets: &Vec) -> BinResult) -> BinResult 1 { + let new_row = if row_count > 1 { let mut rows = Vec::new(); - for i in 0..row_header.row_count { + for i in 0..row_count { let subrow_offset = data_offset + (i * exh.header.data_offset + 2 * (i + 1)) as u32; rows.push(read_row(subrow_offset).unwrap()); @@ -109,9 +123,10 @@ fn write_rows(rows: &Vec, exh: &EXH) -> BinResult<()> { offset: writer.stream_position().unwrap() as u32, }); - let row_header = ExcelDataRowHeader { - data_size: 0, - row_count: 0, + let row_header = DataSection { + size: 6, // TODO: hardcoded + row_count: 1, // TODO: hardcoded + data: Vec::new(), // TOOD: not used here }; row_header.write(writer).unwrap(); @@ -131,6 +146,11 @@ fn write_rows(rows: &Vec, exh: &EXH) -> BinResult<()> { } } } + + // TODO: temporary padding for now + for _ in 0..4 { + 0u8.write_le(writer).unwrap(); + } } // write strings at the end of column data @@ -168,24 +188,44 @@ fn write_rows(rows: &Vec, exh: &EXH) -> BinResult<()> { Ok(()) } +#[binrw] +#[brw(big)] +#[allow(dead_code)] +#[derive(Debug)] +pub struct DataSection { + #[br(dbg)] + size: u32, + row_count: u16, + #[br(count = size)] + #[bw(ignore)] + data: Vec, +} + #[binrw] #[brw(big)] #[allow(dead_code)] #[derive(Debug)] #[brw(import(exh: &EXH))] pub struct EXD { + #[br(dbg)] header: EXDHeader, #[br(count = header.index_size / core::mem::size_of::() as u32)] - #[bw(ignore)] + #[bw(ignore)] // write_rows handles writing this + #[br(dbg)] data_offsets: Vec, + #[br(dbg)] + #[br(parse_with = read_data_sections, args(&header))] + #[bw(ignore)] // write_rows handles writing this + data: Vec, + #[br(parse_with = parse_rows, args(&exh, &data_offsets))] #[bw(write_with = write_rows, args(&exh))] pub rows: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum ColumnData { String(String), Bool(bool), @@ -290,12 +330,12 @@ impl ColumnData { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ExcelSingleRow { pub columns: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum ExcelRowKind { SingleRow(ExcelSingleRow), SubRows(Vec), @@ -499,4 +539,96 @@ mod tests { // Feeding it invalid data should not panic EXD::from_existing(&exh, &read(d).unwrap()); } + + // super simple EXD to read, it's just a few rows of only int8's + #[test] + fn test_read() { + // exh + let exh; + { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("gcshop.exh"); + + exh = EXH::from_existing(&read(d).unwrap()).unwrap(); + } + + // exd + let exd; + { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("gcshop_1441792.exd"); + + exd = EXD::from_existing(&exh, &read(d).unwrap()).unwrap(); + } + + assert_eq!(exd.rows.len(), 4); + + // row 0 + assert_eq!(exd.rows[0].row_id, 1441792); + assert_eq!( + exd.rows[0].kind, + ExcelRowKind::SingleRow(ExcelSingleRow { + columns: vec![ColumnData::Int8(0)] + }) + ); + + // row 1 + assert_eq!(exd.rows[1].row_id, 1441793); + assert_eq!( + exd.rows[1].kind, + ExcelRowKind::SingleRow(ExcelSingleRow { + columns: vec![ColumnData::Int8(1)] + }) + ); + + // row 2 + assert_eq!(exd.rows[2].row_id, 1441794); + assert_eq!( + exd.rows[2].kind, + ExcelRowKind::SingleRow(ExcelSingleRow { + columns: vec![ColumnData::Int8(2)] + }) + ); + + // row 3 + assert_eq!(exd.rows[3].row_id, 1441795); + assert_eq!( + exd.rows[3].kind, + ExcelRowKind::SingleRow(ExcelSingleRow { + columns: vec![ColumnData::Int8(3)] + }) + ); + } + + // super simple EXD to write, it's just a few rows of only int8's + #[test] + fn test_write() { + // exh + let exh; + { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("gcshop.exh"); + + exh = EXH::from_existing(&read(d).unwrap()).unwrap(); + } + + // exd + let expected_exd_bytes; + let expected_exd; + { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("gcshop_1441792.exd"); + + expected_exd_bytes = read(d).unwrap(); + expected_exd = EXD::from_existing(&exh, &expected_exd_bytes).unwrap(); + } + + let actual_exd_bytes = expected_exd.write_to_buffer(&exh).unwrap(); + + assert_eq!(actual_exd_bytes, expected_exd_bytes); + } } diff --git a/src/exh.rs b/src/exh.rs index 4d73742..a02756f 100644 --- a/src/exh.rs +++ b/src/exh.rs @@ -22,11 +22,12 @@ use crate::common::Language; pub struct EXHHeader { pub(crate) version: u16, - pub data_offset: u16, + pub data_offset: u16, // TODO: might not be an offset pub(crate) column_count: u16, pub(crate) page_count: u16, pub(crate) language_count: u16, + #[br(dbg)] pub unk1: u16, #[br(temp)]