diff --git a/src/excel/iterators.rs b/src/excel/iterators.rs new file mode 100644 index 0000000..7a93f34 --- /dev/null +++ b/src/excel/iterators.rs @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2026 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::excel::{Page, Row, Sheet}; + +/// Iterator over a [Sheet]. +#[derive(Clone)] +pub struct SheetIterator<'a> { + sheet: &'a Sheet, + page_index: u32, + page_iterator: Option>, +} + +impl<'a> IntoIterator for &'a Sheet { + type Item = (u32, &'a [(u16, Row)]); + type IntoIter = SheetIterator<'a>; + + fn into_iter(self) -> SheetIterator<'a> { + SheetIterator { + sheet: self, + page_index: 0, + page_iterator: None, + } + } +} + +impl<'a> Iterator for SheetIterator<'a> { + type Item = (u32, &'a [(u16, Row)]); + + fn next(&mut self) -> Option { + let page_index = self.page_index; + + if let Some(iterator) = &mut self.page_iterator { + iterator.next() + } else { + self.page_index += 1; + + if page_index > self.sheet.pages.len() as u32 { + return None; + } + + self.page_iterator = Some(self.sheet.pages[page_index as usize].into_iter()); + + if let Some(iterator) = &mut self.page_iterator { + iterator.next() + } else { + None + } + } + } +} + +/// Iterator over a [Page]. +/// +/// This includes subrows, if you know you don't have those then use [Self::flatten_subrows]. +#[derive(Clone)] +pub struct PageIterator<'a> { + page: &'a Page, + row_index: u32, +} + +impl<'a> PageIterator<'a> { + /// Flattens this iterator, giving you one that only contains rows. + /// + /// If this sheet actually has subrows, then it only takes the first one in each row. + pub fn flatten_subrows(&self) -> RowIterator<'a> { + RowIterator { + page: self.page, + row_index: self.row_index, + } + } +} + +impl<'a> IntoIterator for &'a Page { + type Item = (u32, &'a [(u16, Row)]); + type IntoIter = PageIterator<'a>; + + fn into_iter(self) -> PageIterator<'a> { + PageIterator { + page: self, + row_index: 0, + } + } +} + +impl<'a> Iterator for PageIterator<'a> { + type Item = (u32, &'a [(u16, Row)]); + + fn next(&mut self) -> Option { + let row_index = self.row_index; + self.row_index += 1; + + if row_index as usize > self.page.row_count() { + return None; + } + + let row = &self.page.entries.get(row_index as usize)?; + + Some((row.id, row.subrows.as_slice())) + } +} + +/// Iterator over an [Page], but only the rows. +/// +/// To create this iterator, use [PageIterator::flatten_subrows]. +#[derive(Clone)] +pub struct RowIterator<'a> { + page: &'a Page, + row_index: u32, +} + +impl<'a> Iterator for RowIterator<'a> { + type Item = (u32, &'a Row); + + fn next(&mut self) -> Option { + let row_index = self.row_index; + self.row_index += 1; + + if row_index as usize > self.page.row_count() { + return None; + } + + let row = &self.page.entries.get(row_index as usize)?; + + Some((row.id, &row.subrows.first()?.1)) + } +} + +#[cfg(test)] +mod tests { + use crate::ReadableFile; + use crate::common::Platform; + use crate::excel::{Field, Row, Sheet}; + use crate::exd::EXD; + use crate::exh::EXH; + use std::fs::read; + use std::path::PathBuf; + + use super::*; + + // 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(Platform::Win32, &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(Platform::Win32, &read(d).unwrap()).unwrap(); + } + + let page = Page::from_exd(&exh, exd); + + let excel = Sheet { + exh, + pages: vec![page], + }; + + assert_eq!(excel.pages[0].entries.len(), 4); + + // page behavior + let expected_iterator: Vec<(u32, &[(u16, Row)])> = excel.into_iter().collect(); + assert_eq!( + expected_iterator, + vec![ + ( + 1441792u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(0)] + } + )] + .as_slice() + ), + ( + 1441793u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(1)] + } + )] + .as_slice() + ), + ( + 1441794u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(2)] + } + )] + .as_slice() + ), + ( + 1441795u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(3)] + } + )] + .as_slice() + ) + ] + ); + + // normal behavior that includes subrows + let expected_iterator: Vec<(u32, &[(u16, Row)])> = excel.pages[0].into_iter().collect(); + assert_eq!( + expected_iterator, + vec![ + ( + 1441792u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(0)] + } + )] + .as_slice() + ), + ( + 1441793u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(1)] + } + )] + .as_slice() + ), + ( + 1441794u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(2)] + } + )] + .as_slice() + ), + ( + 1441795u32, + [( + 0u16, + Row { + columns: vec![Field::Int8(3)] + } + )] + .as_slice() + ) + ] + ); + + // API for row-only sheets + let expected_iterator: Vec<(u32, &Row)> = + excel.pages[0].into_iter().flatten_subrows().collect(); + assert_eq!( + expected_iterator, + vec![ + ( + 1441792u32, + &Row { + columns: vec![Field::Int8(0)] + }, + ), + ( + 1441793u32, + &Row { + columns: vec![Field::Int8(1)] + }, + ), + ( + 1441794u32, + &Row { + columns: vec![Field::Int8(2)] + }, + ), + ( + 1441795u32, + &Row { + columns: vec![Field::Int8(3)] + }, + ) + ] + ); + } +} diff --git a/src/excel.rs b/src/excel/mod.rs similarity index 57% rename from src/excel.rs rename to src/excel/mod.rs index 177ddcf..6de1158 100644 --- a/src/excel.rs +++ b/src/excel/mod.rs @@ -12,9 +12,12 @@ use crate::{ exh::{EXH, SheetRowKind}, }; +mod iterators; +pub use iterators::*; + /// Contains a single column's data, which can be various underlying types. #[derive(Debug, Clone, PartialEq)] -pub enum ColumnData { +pub enum Field { /// String. String(String), /// Boolean. @@ -39,10 +42,10 @@ pub enum ColumnData { UInt64(u64), } -impl ColumnData { +impl Field { /// Returns a `Some(String)` if this column was a `String`, otherwise `None`. pub fn into_string(&self) -> Option<&String> { - if let ColumnData::String(value) = self { + if let Field::String(value) = self { return Some(value); } None @@ -50,7 +53,7 @@ impl ColumnData { /// Returns a `Some(bool)` if this column was a `Bool`, otherwise `None`. pub fn into_bool(&self) -> Option<&bool> { - if let ColumnData::Bool(value) = self { + if let Field::Bool(value) = self { return Some(value); } None @@ -58,7 +61,7 @@ impl ColumnData { /// Returns a `Some(i8)` if this column was a `Int8`, otherwise `None`. pub fn into_i8(&self) -> Option<&i8> { - if let ColumnData::Int8(value) = self { + if let Field::Int8(value) = self { return Some(value); } None @@ -66,7 +69,7 @@ impl ColumnData { /// Returns a `Some(u8)` if this column was a `UInt8`, otherwise `None`. pub fn into_u8(&self) -> Option<&u8> { - if let ColumnData::UInt8(value) = self { + if let Field::UInt8(value) = self { return Some(value); } None @@ -74,7 +77,7 @@ impl ColumnData { /// Returns a `Some(i16)` if this column was a `Int16`, otherwise `None`. pub fn into_i16(&self) -> Option<&i16> { - if let ColumnData::Int16(value) = self { + if let Field::Int16(value) = self { return Some(value); } None @@ -82,7 +85,7 @@ impl ColumnData { /// Returns a `Some(u16)` if this column was a `UInt16`, otherwise `None`. pub fn into_u16(&self) -> Option<&u16> { - if let ColumnData::UInt16(value) = self { + if let Field::UInt16(value) = self { return Some(value); } None @@ -90,7 +93,7 @@ impl ColumnData { /// Returns a `Some(i32)` if this column was a `Int32`, otherwise `None`. pub fn into_i32(&self) -> Option<&i32> { - if let ColumnData::Int32(value) = self { + if let Field::Int32(value) = self { return Some(value); } None @@ -98,7 +101,7 @@ impl ColumnData { /// Returns a `Some(u32)` if this column was a `UInt32`, otherwise `None`. pub fn into_u32(&self) -> Option<&u32> { - if let ColumnData::UInt32(value) = self { + if let Field::UInt32(value) = self { return Some(value); } None @@ -106,7 +109,7 @@ impl ColumnData { /// Returns a `Some(f32)` if this column was a `Float32`, otherwise `None`. pub fn into_f32(&self) -> Option<&f32> { - if let ColumnData::Float32(value) = self { + if let Field::Float32(value) = self { return Some(value); } None @@ -114,7 +117,7 @@ impl ColumnData { /// Returns a `Some(i64)` if this column was a `Int64`, otherwise `None`. pub fn into_i64(&self) -> Option<&i64> { - if let ColumnData::Int64(value) = self { + if let Field::Int64(value) = self { return Some(value); } None @@ -122,59 +125,51 @@ impl ColumnData { /// Returns a `Some(u64)` if this column was a `UInt64`, otherwise `None`. pub fn into_u64(&self) -> Option<&u64> { - if let ColumnData::UInt64(value) = self { + if let Field::UInt64(value) = self { return Some(value); } None } } -/// A single row of Excel data. -// TODO: Rename to ExcelRow +/// A single row of data. #[derive(Debug, Clone, PartialEq)] -pub struct ExcelSingleRow { +pub struct Row { /// The columns in this row. - pub columns: Vec, + pub columns: Vec, } -/// Contains either a single row, or multiple subrows. +/// Represents an entry in the page, which is made up of one to multiple subrows. #[derive(Debug, Clone, PartialEq)] -pub enum ExcelRowKind { - /// A single row. - SingleRow(ExcelSingleRow), - /// Multiple subrows, with their IDs as the key. - SubRows(Vec<(u16, ExcelSingleRow)>), +pub struct Entry { + /// The row ID. + pub id: u32, + /// A list of subrows. The first value of this tuple is the subrow ID. + pub subrows: Vec<(u16, Row)>, } -/// Represents an entry in the EXD. +/// A page of rows, inside of a [Sheet]. #[derive(Debug, Clone)] -pub struct ExcelRow { - /// Row ID associated with this entry. - pub row_id: u32, - /// What kind of entry this represents. - pub kind: ExcelRowKind, -} - -#[derive(Debug, Clone)] -pub struct ExcelSheetPage { - pub(crate) row_count: u32, +pub struct Page { exd: EXD, - /// The rows in this page. - pub rows: Vec, + /// The row descriptors for this page. + /// + /// You most likely don't want to use this, prefer [Sheet::row] or [Sheet::subrow] instead. + // (NOTE: This is currently public because of write support, but this may change in the future.) + pub entries: Vec, } -impl ExcelSheetPage { - pub(crate) fn from_exd(page_index: u16, exh: &EXH, exd: EXD) -> Self { - let rows = Self::get_rows(exh, &exd); +impl Page { + pub(crate) fn from_exd(exh: &EXH, exd: EXD) -> Self { + let descriptors = Self::read_descriptors(exh, &exd); Self { - row_count: exh.pages[page_index as usize].row_count, exd, - rows, + entries: descriptors, } } - fn get_rows(exh: &EXH, exd: &EXD) -> Vec { + fn read_descriptors(exh: &EXH, exd: &EXD) -> Vec { let mut cursor = Cursor::new(&exd.remaining_data); let header_offset = EXDHeader::SIZE as u64 + exd.header.data_offset_size as u64; let mut rows = Vec::new(); @@ -188,7 +183,7 @@ impl ExcelSheetPage { let data_offset = cursor.stream_position().unwrap(); - let kind = if exh.header.row_kind == SheetRowKind::SubRows { + let subrows = if exh.header.row_kind == SheetRowKind::SubRows { let mut rows = Vec::with_capacity(row_header.row_count as usize); for i in 0..row_header.row_count { let subrow_offset = data_offset + i as u64 * (2 + exh.header.row_size as u64); @@ -200,13 +195,13 @@ impl ExcelSheetPage { read_row(&mut cursor, exh, subrow_offset + 2).unwrap(), )); } - ExcelRowKind::SubRows(rows) + rows } else { - ExcelRowKind::SingleRow(read_row(&mut cursor, exh, data_offset).unwrap()) + vec![(0, read_row(&mut cursor, exh, data_offset).unwrap())] }; - rows.push(ExcelRow { - row_id: offset.row_id, - kind, + rows.push(Entry { + id: offset.row_id, + subrows, }); } @@ -224,15 +219,15 @@ impl ExcelSheetPage { let data_offsets_pos = cursor.stream_position().unwrap(); cursor .seek(SeekFrom::Current( - (core::mem::size_of::() * self.rows.len()) as i64, + (core::mem::size_of::() * self.entries.len()) as i64, )) .unwrap(); - let mut data_offsets = Vec::with_capacity(self.rows.len()); + let mut data_offsets = Vec::with_capacity(self.entries.len()); - for row in &self.rows { + for row in &self.entries { data_offsets.push(ExcelDataOffset { - row_id: row.row_id, + row_id: row.id, offset: cursor.stream_position().unwrap() as u32, }); @@ -244,12 +239,12 @@ impl ExcelSheetPage { let old_pos = cursor.stream_position().unwrap(); // write column data - match &row.kind { - ExcelRowKind::SingleRow(excel_single_row) => { - write_row(&mut cursor, exh, excel_single_row) + match &exh.header.row_kind { + SheetRowKind::SingleRow => { + write_row(&mut cursor, exh, &row.subrows.first().unwrap().1) } - ExcelRowKind::SubRows(excel_single_rows) => { - for (id, row) in excel_single_rows { + SheetRowKind::SubRows => { + for (id, row) in &row.subrows { let subrow_header = SubRowHeader { subrow_id: *id }; subrow_header.write_ne(&mut cursor).ok()?; @@ -260,9 +255,9 @@ impl ExcelSheetPage { // write strings at the end of column data { - let mut write_row_strings = |row: &ExcelSingleRow| { + let mut write_row_strings = |row: &Row| { for column in &row.columns { - if let ColumnData::String(val) = column { + if let Field::String(val) = column { let bytes = val.as_bytes(); bytes.write(&mut cursor).unwrap(); @@ -272,15 +267,8 @@ impl ExcelSheetPage { } }; - match &row.kind { - ExcelRowKind::SingleRow(excel_single_row) => { - write_row_strings(excel_single_row) - } - ExcelRowKind::SubRows(excel_single_rows) => { - for (_, row) in excel_single_rows { - write_row_strings(row); - } - } + for (_, row) in &row.subrows { + write_row_strings(row); } } @@ -312,42 +300,58 @@ impl ExcelSheetPage { Some(cursor.into_inner()) } + + /// The number of rows in this sheet. Does *not* take into account subrows. + pub fn row_count(&self) -> usize { + self.entries.len() + } } +/// Represents an Excel sheet, which contains multiple pages of columns separated by either rows or subrows. +/// +/// To read a sheet, use [ResourceResolver::read_excel_sheet](crate::resource::ResourceResolver::read_excel_sheet) or [SqPackResource::read_excel_sheet](crate::resource::SqPackResource::read_excel_sheet). #[derive(Debug, Clone)] -pub struct ExcelSheet { +pub struct Sheet { /// The EXH for this sheet. + // (NOTE: This is currently public because of Icarus, but this may change in the future.) pub exh: EXH, - /// All of the pages for this Excel sheet. - pub pages: Vec, + /// The pages for this sheet. + pub pages: Vec, } -impl ExcelSheet { - /// Finds the entry with the specified `row_id` and returns a reference to it, otherwise returns `None`. - pub fn get_row(&self, row_id: u32) -> Option<&ExcelRowKind> { +impl Sheet { + /// Returns the entry matching `row_id` and returns a reference to it, otherwise returns [None]. + /// + /// This is only useful if you need to discriminate between single row and subrow sheets. In most cases, you want to use [row](Self::row) or [subrow](Self::subrow). + pub fn entry(&self, row_id: u32) -> Option<&Entry> { let page_index = self.exh.get_page(row_id); let page = self.pages.get(page_index)?; - page.rows - .iter() - .find(|row| row.row_id == row_id) - .map(|row| &row.kind) + page.entries.iter().find(|row| row.id == row_id) } - /// Finds the entry with the specified `row_id` and `subrow_id` and returns a reference to it, otherwise returns `None`. - pub fn get_subrow(&self, row_id: u32, subrow_id: u16) -> Option<&ExcelSingleRow> { - let page_index = self.exh.get_page(row_id); - let page = self.pages.get(page_index)?; - - let row = page.rows.iter().find(|row| row.row_id == row_id)?; + /// Finds a row matching `row_id` and returns a reference to it, otherwise returns [None]. + /// + /// For sheets that have subrows, this will always return subrow 0. It's recommended to use [subrow](Self::subrow) instead. + pub fn row(&self, row_id: u32) -> Option<&Row> { + let row = self.entry(row_id)?; + row.subrows.first().map(|(_, row)| row) + } - match &row.kind { - ExcelRowKind::SubRows(subrows) => subrows - .iter() - .find(|(id, _)| *id == subrow_id) - .map(|(_, single_row)| single_row), - ExcelRowKind::SingleRow(_) => None, + /// Finds a row matching `row_id` and `subrow_id` and returns a reference to it, otherwise returns [None]. + /// + /// For sheets that don't have subrows, this will always return [None]. + pub fn subrow(&self, row_id: u32, subrow_id: u16) -> Option<&Row> { + // Grabbing a subrow here never makes sense, and is just misuse of the API. + if self.exh.header.row_kind == SheetRowKind::SingleRow { + return None; } + + let row = self.entry(row_id)?; + row.subrows + .iter() + .find(|(id, _)| *id == subrow_id) + .map(|(_, row)| row) } } @@ -383,57 +387,77 @@ mod tests { exd = EXD::from_existing(Platform::Win32, &read(d).unwrap()).unwrap(); } - let page = ExcelSheetPage::from_exd(0, &exh, exd); + let page = Page::from_exd(&exh, exd); - let excel = ExcelSheet { + let excel = Sheet { exh, pages: vec![page], }; - assert_eq!(excel.pages[0].rows.len(), 4); + assert_eq!(excel.pages[0].entries.len(), 4); // row 0 - assert_eq!(excel.pages[0].rows[0].row_id, 1441792); assert_eq!( - excel.pages[0].rows[0].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(0)] - }) + excel.pages[0].entries[0], + Entry { + id: 1441792, + subrows: vec![( + 0, + Row { + columns: vec![Field::Int8(0)] + } + )] + } ); - assert!(excel.get_row(1441792).is_some()); + assert!(excel.row(1441792).is_some()); // row 1 - assert_eq!(excel.pages[0].rows[1].row_id, 1441793); assert_eq!( - excel.pages[0].rows[1].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(1)] - }) + excel.pages[0].entries[1], + Entry { + id: 1441793, + subrows: vec![( + 0, + Row { + columns: vec![Field::Int8(1)] + } + )] + } ); - assert!(excel.get_row(1441793).is_some()); + assert!(excel.row(1441793).is_some()); // row 2 - assert_eq!(excel.pages[0].rows[2].row_id, 1441794); assert_eq!( - excel.pages[0].rows[2].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(2)] - }) + excel.pages[0].entries[2], + Entry { + id: 1441794, + subrows: vec![( + 0, + Row { + columns: vec![Field::Int8(2)] + } + )] + } ); - assert!(excel.get_row(1441794).is_some()); + assert!(excel.row(1441794).is_some()); // row 3 - assert_eq!(excel.pages[0].rows[3].row_id, 1441795); assert_eq!( - excel.pages[0].rows[3].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(3)] - }) + excel.pages[0].entries[3], + Entry { + id: 1441795, + subrows: vec![( + 0, + Row { + columns: vec![Field::Int8(3)] + } + )] + } ); - assert!(excel.get_row(1441795).is_some()); + assert!(excel.row(1441795).is_some()); // non-existent row 4 - assert!(excel.get_row(1019181719).is_none()); + assert!(excel.row(1019181719).is_none()); } // slightly more complex to read, because it has STRINGS @@ -459,109 +483,149 @@ mod tests { exd = EXD::from_existing(Platform::Win32, &read(d).unwrap()).unwrap(); } - let page = ExcelSheetPage::from_exd(0, &exh, exd); + let page = Page::from_exd(&exh, exd); - let excel = ExcelSheet { + let excel = Sheet { exh, pages: vec![page], }; - assert_eq!(excel.pages[0].rows.len(), 8); + assert_eq!(excel.pages[0].entries.len(), 8); // row 0 - assert_eq!(excel.pages[0].rows[0].row_id, 0); assert_eq!( - excel.pages[0].rows[0].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("HOWTO_MOVE_AND_CAMERA".to_string()), - ColumnData::UInt32(1) - ] - }) + excel.pages[0].entries[0], + Entry { + id: 0, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("HOWTO_MOVE_AND_CAMERA".to_string()), + Field::UInt32(1) + ] + } + )] + } ); // row 1 - assert_eq!(excel.pages[0].rows[1].row_id, 1); assert_eq!( - excel.pages[0].rows[1].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("HOWTO_ANNOUNCE_AND_QUEST".to_string()), - ColumnData::UInt32(2) - ] - }) + excel.pages[0].entries[1], + Entry { + id: 1, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("HOWTO_ANNOUNCE_AND_QUEST".to_string()), + Field::UInt32(2) + ] + } + )] + } ); // row 2 - assert_eq!(excel.pages[0].rows[2].row_id, 2); assert_eq!( - excel.pages[0].rows[2].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("HOWTO_QUEST_REWARD".to_string()), - ColumnData::UInt32(11) - ] - }) + excel.pages[0].entries[2], + Entry { + id: 2, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("HOWTO_QUEST_REWARD".to_string()), + Field::UInt32(11) + ] + } + )] + } ); // row 3 - assert_eq!(excel.pages[0].rows[3].row_id, 3); assert_eq!( - excel.pages[0].rows[3].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("BGM_MUSIC_NO_MUSIC".to_string()), - ColumnData::UInt32(1001) - ] - }) + excel.pages[0].entries[3], + Entry { + id: 3, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("BGM_MUSIC_NO_MUSIC".to_string()), + Field::UInt32(1001) + ] + } + )] + } ); // row 4 - assert_eq!(excel.pages[0].rows[4].row_id, 4); assert_eq!( - excel.pages[0].rows[4].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("ITEM_INITIAL_RING_A".to_string()), - ColumnData::UInt32(4423) - ] - }) + excel.pages[0].entries[4], + Entry { + id: 4, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("ITEM_INITIAL_RING_A".to_string()), + Field::UInt32(4423) + ] + } + )] + } ); // row 5 - assert_eq!(excel.pages[0].rows[5].row_id, 5); assert_eq!( - excel.pages[0].rows[5].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("ITEM_INITIAL_RING_B".to_string()), - ColumnData::UInt32(4424) - ] - }) + excel.pages[0].entries[5], + Entry { + id: 5, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("ITEM_INITIAL_RING_B".to_string()), + Field::UInt32(4424) + ] + } + )] + } ); // row 6 - assert_eq!(excel.pages[0].rows[6].row_id, 6); assert_eq!( - excel.pages[0].rows[6].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("ITEM_INITIAL_RING_C".to_string()), - ColumnData::UInt32(4425) - ] - }) + excel.pages[0].entries[6], + Entry { + id: 6, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("ITEM_INITIAL_RING_C".to_string()), + Field::UInt32(4425) + ] + } + )] + } ); // row 7 - assert_eq!(excel.pages[0].rows[7].row_id, 7); assert_eq!( - excel.pages[0].rows[7].kind, - ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ - ColumnData::String("ITEM_INITIAL_RING_D".to_string()), - ColumnData::UInt32(4426) - ] - }) + excel.pages[0].entries[7], + Entry { + id: 7, + subrows: vec![( + 0, + Row { + columns: vec![ + Field::String("ITEM_INITIAL_RING_D".to_string()), + Field::UInt32(4426) + ] + } + )] + } ); } @@ -590,7 +654,7 @@ mod tests { expected_exd = EXD::from_existing(Platform::Win32, &expected_exd_bytes).unwrap(); } - let page = ExcelSheetPage::from_exd(0, &exh, expected_exd); + let page = Page::from_exd(&exh, expected_exd); let actual_exd_bytes = page.write_to_buffer(&exh).unwrap(); assert_eq!(actual_exd_bytes, expected_exd_bytes); @@ -621,7 +685,7 @@ mod tests { expected_exd = EXD::from_existing(Platform::Win32, &expected_exd_bytes).unwrap(); } - let page = ExcelSheetPage::from_exd(0, &exh, expected_exd); + let page = Page::from_exd(&exh, expected_exd); let actual_exd_bytes = page.write_to_buffer(&exh).unwrap(); assert_eq!(actual_exd_bytes, expected_exd_bytes); @@ -652,7 +716,7 @@ mod tests { expected_exd = EXD::from_existing(Platform::Win32, &expected_exd_bytes).unwrap(); } - let page = ExcelSheetPage::from_exd(0, &exh, expected_exd); + let page = Page::from_exd(&exh, expected_exd); let actual_exd_bytes = page.write_to_buffer(&exh).unwrap(); assert_eq!(actual_exd_bytes, expected_exd_bytes); diff --git a/src/excel_iterators.rs b/src/excel_iterators.rs deleted file mode 100644 index 12866b2..0000000 --- a/src/excel_iterators.rs +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Joshua Goins -// SPDX-License-Identifier: GPL-3.0-or-later - -use crate::excel::{ExcelRowKind, ExcelSheetPage}; - -pub struct ExcelSheetPageIterator<'a> { - page: &'a ExcelSheetPage, - row_index: u32, -} - -impl<'a> IntoIterator for &'a ExcelSheetPage { - type Item = (u32, &'a ExcelRowKind); - type IntoIter = ExcelSheetPageIterator<'a>; - fn into_iter(self) -> ExcelSheetPageIterator<'a> { - ExcelSheetPageIterator { - page: self, - row_index: 0, - } - } -} - -impl<'a> Iterator for ExcelSheetPageIterator<'a> { - type Item = (u32, &'a ExcelRowKind); - - fn next(&mut self) -> Option { - let row_index = self.row_index; - self.row_index += 1; - - if row_index > self.page.row_count { - return None; - } - - let row = &self.page.rows.get(row_index as usize)?; - - Some((row.row_id, &row.kind)) - } -} - -#[cfg(test)] -mod tests { - use crate::ReadableFile; - use crate::common::Platform; - use crate::excel::{ColumnData, ExcelSheet, ExcelSingleRow}; - use crate::exd::EXD; - use crate::exh::EXH; - use std::fs::read; - use std::path::PathBuf; - - use super::*; - - // 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(Platform::Win32, &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(Platform::Win32, &read(d).unwrap()).unwrap(); - } - - let page = ExcelSheetPage::from_exd(0, &exh, exd); - - let excel = ExcelSheet { - exh, - pages: vec![page], - }; - - assert_eq!(excel.pages[0].rows.len(), 4); - - let expected_iterator: Vec<(u32, &ExcelRowKind)> = excel.pages[0].into_iter().collect(); - assert_eq!( - expected_iterator, - vec![ - ( - 1441792, - &ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(0)] - }) - ), - ( - 1441793, - &ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(1)] - }) - ), - ( - 1441794, - &ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(2)] - }) - ), - ( - 1441795, - &ExcelRowKind::SingleRow(ExcelSingleRow { - columns: vec![ColumnData::Int8(3)] - }) - ) - ] - ); - } -} diff --git a/src/exd_file_operations.rs b/src/exd_file_operations.rs index 230254d..c282ea6 100644 --- a/src/exd_file_operations.rs +++ b/src/exd_file_operations.rs @@ -9,17 +9,13 @@ use std::{ use binrw::{BinRead, BinWrite, Endian}; use crate::{ - excel::{ColumnData, ExcelSingleRow}, + excel::{Field, Row}, exd::EXD, exh::{ColumnDataType, EXH, ExcelColumnDefinition}, }; -pub(crate) fn read_row( - reader: &mut T, - exh: &EXH, - row_offset: u64, -) -> Option { - let mut subrow = ExcelSingleRow { +pub(crate) fn read_row(reader: &mut T, exh: &EXH, row_offset: u64) -> Option { + let mut subrow = Row { columns: Vec::with_capacity(exh.column_definitions.len()), }; @@ -36,8 +32,8 @@ pub(crate) fn read_row( Some(subrow) } -pub(crate) fn write_row(writer: &mut T, exh: &EXH, row: &ExcelSingleRow) { - let mut column_definitions: Vec<(ExcelColumnDefinition, ColumnData)> = exh +pub(crate) fn write_row(writer: &mut T, exh: &EXH, row: &Row) { + let mut column_definitions: Vec<(ExcelColumnDefinition, Field)> = exh .column_definitions .clone() .into_iter() @@ -61,7 +57,7 @@ pub(crate) fn write_row(writer: &mut T, exh: &EXH, row: &ExcelS // process packed bools before continuing, since we need to know what their final byte form is for (definition, column) in &column_definitions { - if let ColumnData::Bool(val) = &column { + if let Field::Bool(val) = &column { match definition.data_type { ColumnDataType::PackedBool0 => write_packed_bool(definition, 0, val), ColumnDataType::PackedBool1 => write_packed_bool(definition, 1, val), @@ -111,7 +107,7 @@ impl EXD { exh: &EXH, row_offset: u64, column: &ExcelColumnDefinition, - ) -> Option { + ) -> Option { let mut read_packed_bool = |shift: i32| -> bool { let bit = 1 << shift; let bool_data: u8 = Self::read_data_raw(cursor).unwrap_or(0); @@ -137,39 +133,31 @@ impl EXD { byte = Self::read_data_raw(cursor).unwrap(); } - Some(ColumnData::String(string)) + Some(Field::String(string)) } ColumnDataType::Bool => { // FIXME: i believe Bool is int8? let bool_data: i32 = Self::read_data_raw(cursor).unwrap(); - Some(ColumnData::Bool(bool_data == 1)) + Some(Field::Bool(bool_data == 1)) } - ColumnDataType::Int8 => Some(ColumnData::Int8(Self::read_data_raw(cursor).unwrap())), - ColumnDataType::UInt8 => Some(ColumnData::UInt8(Self::read_data_raw(cursor).unwrap())), - ColumnDataType::Int16 => Some(ColumnData::Int16(Self::read_data_raw(cursor).unwrap())), - ColumnDataType::UInt16 => { - Some(ColumnData::UInt16(Self::read_data_raw(cursor).unwrap())) - } - ColumnDataType::Int32 => Some(ColumnData::Int32(Self::read_data_raw(cursor).unwrap())), - ColumnDataType::UInt32 => { - Some(ColumnData::UInt32(Self::read_data_raw(cursor).unwrap())) - } - ColumnDataType::Float32 => { - Some(ColumnData::Float32(Self::read_data_raw(cursor).unwrap())) - } - ColumnDataType::Int64 => Some(ColumnData::Int64(Self::read_data_raw(cursor).unwrap())), - ColumnDataType::UInt64 => { - Some(ColumnData::UInt64(Self::read_data_raw(cursor).unwrap())) - } - ColumnDataType::PackedBool0 => Some(ColumnData::Bool(read_packed_bool(0))), - ColumnDataType::PackedBool1 => Some(ColumnData::Bool(read_packed_bool(1))), - ColumnDataType::PackedBool2 => Some(ColumnData::Bool(read_packed_bool(2))), - ColumnDataType::PackedBool3 => Some(ColumnData::Bool(read_packed_bool(3))), - ColumnDataType::PackedBool4 => Some(ColumnData::Bool(read_packed_bool(4))), - ColumnDataType::PackedBool5 => Some(ColumnData::Bool(read_packed_bool(5))), - ColumnDataType::PackedBool6 => Some(ColumnData::Bool(read_packed_bool(6))), - ColumnDataType::PackedBool7 => Some(ColumnData::Bool(read_packed_bool(7))), + ColumnDataType::Int8 => Some(Field::Int8(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::UInt8 => Some(Field::UInt8(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::Int16 => Some(Field::Int16(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::UInt16 => Some(Field::UInt16(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::Int32 => Some(Field::Int32(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::UInt32 => Some(Field::UInt32(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::Float32 => Some(Field::Float32(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::Int64 => Some(Field::Int64(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::UInt64 => Some(Field::UInt64(Self::read_data_raw(cursor).unwrap())), + ColumnDataType::PackedBool0 => Some(Field::Bool(read_packed_bool(0))), + ColumnDataType::PackedBool1 => Some(Field::Bool(read_packed_bool(1))), + ColumnDataType::PackedBool2 => Some(Field::Bool(read_packed_bool(2))), + ColumnDataType::PackedBool3 => Some(Field::Bool(read_packed_bool(3))), + ColumnDataType::PackedBool4 => Some(Field::Bool(read_packed_bool(4))), + ColumnDataType::PackedBool5 => Some(Field::Bool(read_packed_bool(5))), + ColumnDataType::PackedBool6 => Some(Field::Bool(read_packed_bool(6))), + ColumnDataType::PackedBool7 => Some(Field::Bool(read_packed_bool(7))), } } @@ -179,18 +167,18 @@ impl EXD { pub(crate) fn write_column( cursor: &mut T, - column: &ColumnData, + column: &Field, column_definition: &ExcelColumnDefinition, strings_len: &mut u32, packed_bools: &mut HashMap, ) { match column { - ColumnData::String(val) => { + Field::String(val) => { let string_offset = *strings_len; Self::write_data_raw(cursor, &string_offset); *strings_len += val.len() as u32 + 1; } - ColumnData::Bool(_) => match column_definition.data_type { + Field::Bool(_) => match column_definition.data_type { ColumnDataType::Bool => todo!(), // packed bools are handled in write_rows ColumnDataType::PackedBool0 @@ -210,15 +198,15 @@ impl EXD { } _ => panic!("This makes no sense!"), }, - ColumnData::Int8(val) => Self::write_data_raw(cursor, val), - ColumnData::UInt8(val) => Self::write_data_raw(cursor, val), - ColumnData::Int16(val) => Self::write_data_raw(cursor, val), - ColumnData::UInt16(val) => Self::write_data_raw(cursor, val), - ColumnData::Int32(val) => Self::write_data_raw(cursor, val), - ColumnData::UInt32(val) => Self::write_data_raw(cursor, val), - ColumnData::Float32(val) => Self::write_data_raw(cursor, val), - ColumnData::Int64(val) => Self::write_data_raw(cursor, val), - ColumnData::UInt64(val) => Self::write_data_raw(cursor, val), + Field::Int8(val) => Self::write_data_raw(cursor, val), + Field::UInt8(val) => Self::write_data_raw(cursor, val), + Field::Int16(val) => Self::write_data_raw(cursor, val), + Field::UInt16(val) => Self::write_data_raw(cursor, val), + Field::Int32(val) => Self::write_data_raw(cursor, val), + Field::UInt32(val) => Self::write_data_raw(cursor, val), + Field::Float32(val) => Self::write_data_raw(cursor, val), + Field::Int64(val) => Self::write_data_raw(cursor, val), + Field::UInt64(val) => Self::write_data_raw(cursor, val), } } } diff --git a/src/lib.rs b/src/lib.rs index 9a1dafb..95af4c8 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,9 +163,6 @@ pub mod cutb; /// Higher-level Excel API. pub mod excel; -/// Iterators for Excel types!! -pub mod excel_iterators; - mod bcn; mod error; diff --git a/src/resource/mod.rs b/src/resource/mod.rs index 2bcf0d8..091af2e 100644 --- a/src/resource/mod.rs +++ b/src/resource/mod.rs @@ -13,7 +13,7 @@ pub use unpacked::UnpackedResource; use crate::{ ByteBuffer, Error, ReadableFile, common::{Language, Platform}, - excel::{ExcelSheet, ExcelSheetPage}, + excel::{Page, Sheet}, exd::EXD, exh::EXH, exl::EXL, @@ -129,14 +129,17 @@ pub fn generic_read_excel_sheet( exh: &EXH, name: &str, language: Language, -) -> Result { +) -> Result { let mut pages = Vec::with_capacity(exh.header.page_count as usize); for page in 0..exh.header.page_count { - let exd = generic_read_excel_exd(resource, name, &exh, language, page as usize)?; - pages.push(ExcelSheetPage::from_exd(page, &exh, exd)); + let exd = generic_read_excel_exd(resource, name, exh, language, page as usize)?; + pages.push(Page::from_exd(exh, exd)); } - Ok(ExcelSheet { exh: exh.clone(), pages }) + Ok(Sheet { + exh: exh.clone(), + pages, + }) } /// Returns all known sheet names listed in the root list. You most likely want to use the method in `ResourceResolver.` diff --git a/src/resource/resolver.rs b/src/resource/resolver.rs index 28507bc..3527cfc 100644 --- a/src/resource/resolver.rs +++ b/src/resource/resolver.rs @@ -4,7 +4,7 @@ use crate::{ ByteBuffer, Error, ReadableFile, common::Language, - excel::ExcelSheet, + excel::Sheet, exh::EXH, resource::{ generic_get_all_sheet_names, generic_parsed, generic_read_excel_sheet, @@ -93,7 +93,7 @@ impl ResourceResolver { exh: &EXH, name: &str, language: Language, - ) -> Result { + ) -> Result { self.execute_first_found( |resource| generic_read_excel_sheet(resource, exh, name, language), Error::Unknown, diff --git a/src/resource/sqpack.rs b/src/resource/sqpack.rs index 5131836..b7ec995 100644 --- a/src/resource/sqpack.rs +++ b/src/resource/sqpack.rs @@ -10,7 +10,7 @@ use std::{ use crate::{ ByteBuffer, Error, ReadableFile, common::{Language, PLATFORM_LIST, Platform, read_version}, - excel::ExcelSheet, + excel::Sheet, exh::EXH, repository::{Category, Repository, RepositoryType, string_to_category}, resource::{ @@ -456,7 +456,7 @@ impl SqPackResource { exh: &EXH, name: &str, language: Language, - ) -> Result { + ) -> Result { generic_read_excel_sheet(self, exh, name, language) } diff --git a/tests/retail_test.rs b/tests/retail_test.rs index 032a956..e2d30a8 100755 --- a/tests/retail_test.rs +++ b/tests/retail_test.rs @@ -6,7 +6,7 @@ use std::env; use physis::{ ReadableFile, common::{Language, Platform}, - excel::{ColumnData, ExcelRowKind}, + excel::{ExcelRowKind, Field}, race::{Gender, Race, Tribe, build_skeleton_path}, resource::{Resource, ResourceResolver, SqPackResource}, skeleton::Skeleton, @@ -45,7 +45,7 @@ fn test_item_read() { for row in &exd.pages[0].rows { match &row.kind { ExcelRowKind::SingleRow(row) => match &row.columns[9] { - ColumnData::String(val) => { + Field::String(val) => { if val == "Dated Canvas Beret" { return; }