From ca4962c876941b1e80b4da7dd6a5f84eaf38f213 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Thu, 21 Jul 2022 19:58:58 -0400 Subject: [PATCH] Add EXH and EXD parsers --- src/exd.rs | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ src/exh.rs | 82 +++++++++++++++++++ src/exl.rs | 10 +-- src/gamedata.rs | 30 +++++++ src/lib.rs | 4 +- 5 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 src/exd.rs create mode 100644 src/exh.rs diff --git a/src/exd.rs b/src/exd.rs new file mode 100644 index 0000000..4bd1bd8 --- /dev/null +++ b/src/exd.rs @@ -0,0 +1,207 @@ +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Cursor, Seek, SeekFrom}; +use crate::gamedata::MemoryBuffer; +use binrw::{binread, Endian, ReadOptions}; +use crate::common::Language; +use binrw::BinRead; +use crate::exh::{ColumnDataType, ExcelColumnDefinition, ExcelDataPagination, EXH}; + +#[binread] +#[br(magic = b"EXDF")] +#[br(big)] +struct EXDHeader { + version : u16, + + #[br(pad_before = 2)] + #[br(pad_after = 20)] + index_size : u32 +} + +#[binread] +#[br(big)] +struct ExcelDataOffset { + row_id : u32, + pub offset : u32 +} + +#[binread] +#[br(big)] +struct ExcelDataRowHeader { + data_size : u32, + row_count : u16 +} + +#[binread] +#[br(big)] +pub struct EXD { + header : EXDHeader, + + #[br(count = header.index_size / core::mem::size_of::() as u32)] + data_offsets : Vec, + + #[br(ignore)] + pub rows : Vec +} + +#[derive(Debug)] +pub enum ColumnData { + String(String), + Bool(bool), + Int8(i8), + UInt8(u8), + Int16(i16), + UInt16(u16), + Int32(i32), + UInt32(u32), + Float32(f32), + Int64(i64), + UInt64(u64) +} + +pub struct ExcelRow { + pub data : Vec +} + +impl EXD { + fn read_data_raw(cursor: &mut Cursor<&MemoryBuffer>) -> Option where ::Args: Default { + Some(Z::read_options(cursor, &ReadOptions::new(Endian::Big) , ::Args::default()).unwrap()) + } + + fn read_column(cursor: &mut Cursor<&MemoryBuffer>, exh: &EXH, offset : u32, column : &ExcelColumnDefinition) -> Option { + let mut read_packed_bool = | shift : i32 | -> bool { + let bit = 1 << shift; + let bool_data : i32 = Self::read_data_raw(cursor).unwrap(); + + (bool_data & bit) == bit + }; + + return match column.data_type { + ColumnDataType::String => { + let string_offset : u32 = Self::read_data_raw(cursor).unwrap(); + + cursor.seek(SeekFrom::Start((offset + exh.header.data_offset as u32 + string_offset).into())).ok()?; + + let mut string = String::new(); + + let mut byte : u8 = Self::read_data_raw(cursor).unwrap(); + while byte != 0 { + string.push(byte as char); + byte = Self::read_data_raw(cursor).unwrap(); + } + + Some(ColumnData::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)) + } + 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))) + } + } + } + + pub fn from_existing(exh : &EXH, buffer : &MemoryBuffer) -> Option { + let mut cursor = Cursor::new(buffer); + let mut exd = EXD::read(&mut cursor).ok()?; + + for i in 0..exh.header.row_count { + for offset in &exd.data_offsets { + if offset.row_id == i { + cursor.seek(SeekFrom::Start(offset.offset.into())).ok()?; + + let row_header = ExcelDataRowHeader::read(&mut cursor).ok()?; + + let header_offset = offset.offset + 6; + + let mut read_row = | offset : u32 | -> Option { + let mut subrow = ExcelRow { data : Vec::with_capacity(exh.column_definitions.len()) }; + + for column in &exh.column_definitions { + cursor.seek(SeekFrom::Start((offset + column.offset as u32).into())).ok()?; + + subrow.data.push(Self::read_column(&mut cursor, exh, offset, column).unwrap()); + } + + Some(subrow) + }; + + if row_header.row_count > 1 { + for i in 0..row_header.row_count { + let subrow_offset = header_offset + (i * exh.header.data_offset + 2 * (i + 1)) as u32; + + exd.rows.push(read_row(subrow_offset).unwrap()); + } + } else { + exd.rows.push(read_row(header_offset).unwrap()); + } + } + } + } + + Some(exd) + } + + pub fn calculate_filename(name : &str, language : Language, page : &ExcelDataPagination) -> String { + use crate::common::get_language_code; + + return match language { + Language::None => { + format!("{name}_{}.exd", page.start_id) + } + lang => { + format!("{name}_{}_{}.exd", page.start_id, get_language_code(&lang)) + } + } + } +} \ No newline at end of file diff --git a/src/exh.rs b/src/exh.rs new file mode 100644 index 0000000..d74fa6b --- /dev/null +++ b/src/exh.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Cursor}; +use crate::gamedata::MemoryBuffer; +use binrw::binread; +use crate::common::Language; +use binrw::BinRead; + +#[binread] +#[br(magic = b"EXHF")] +#[br(big)] +pub struct EXHHeader { + version : u16, + + pub(crate) data_offset : u16, + column_count : u16, + page_count : u16, + language_count : u16, + + #[br(pad_before = 6)] + #[br(pad_after = 8)] + pub(crate) row_count : u32 +} + +#[binread] +#[br(repr(u16))] +pub enum ColumnDataType { + String = 0x0, + Bool = 0x1, + Int8 = 0x2, + UInt8 = 0x3, + Int16 = 0x4, + UInt16 = 0x5, + Int32 = 0x6, + UInt32 = 0x7, + Float32 = 0x9, + Int64 = 0xA, + UInt64 = 0xB, + + PackedBool0 = 0x19, + PackedBool1 = 0x1A, + PackedBool2 = 0x1B, + PackedBool3 = 0x1C, + PackedBool4 = 0x1D, + PackedBool5 = 0x1E, + PackedBool6 = 0x1F, + PackedBool7 = 0x20, +} + +#[binread] +#[br(big)] +pub struct ExcelColumnDefinition { + pub data_type : ColumnDataType, + pub offset : u16, +} + +#[binread] +#[br(big)] +pub struct ExcelDataPagination { + pub start_id : u32, + row_count : u32, +} + +#[binread] +#[br(big)] +pub struct EXH { + pub(crate) header : EXHHeader, + + #[br(count = header.column_count)] + pub(crate) column_definitions : Vec, + + #[br(count = header.page_count)] + pub(crate) pages : Vec, + + #[br(count = header.language_count)] + languages : Vec +} + +impl EXH { + pub fn from_existing(buffer : &MemoryBuffer) -> Option { + Some(EXH::read(&mut Cursor::new(&buffer)).ok()?) + } +} \ No newline at end of file diff --git a/src/exl.rs b/src/exl.rs index d322b4a..fafa21b 100755 --- a/src/exl.rs +++ b/src/exl.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; -use std::io::{BufRead, BufReader}; +use std::io::{BufRead, BufReader, Cursor}; +use crate::gamedata::MemoryBuffer; /// Represents an Excel List. pub struct EXL { @@ -12,15 +13,14 @@ pub struct EXL { impl EXL { /// Initializes `EXL` from an existing list. - pub fn from_existing(path: &str) -> Option { + pub fn from_existing(buffer : &MemoryBuffer) -> Option { let mut exl = Self { version: 0, entries: HashMap::new(), }; - let file = std::fs::File::open(path).unwrap(); - - let reader = BufReader::new(file); + let cursor = Cursor::new(buffer); + let reader = BufReader::new(cursor); for (_, line) in reader.lines().enumerate() { // now parse the line! diff --git a/src/gamedata.rs b/src/gamedata.rs index 30f9171..6b042cd 100755 --- a/src/gamedata.rs +++ b/src/gamedata.rs @@ -2,7 +2,11 @@ use std::ffi::OsStr; use std::fs; use std::fs::DirEntry; use std::path::PathBuf; +use crate::common::Language; use crate::dat::DatFile; +use crate::exd::EXD; +use crate::exh::EXH; +use crate::exl::EXL; use crate::index::IndexFile; use crate::repository::{Category, Repository, string_to_category}; use crate::sqpack::calculate_hash; @@ -174,6 +178,32 @@ impl GameData { Some((&self.repositories[0], string_to_category(tokens[0])?)) } + + pub fn read_excel_sheet_header(&self, name : &str) -> Option { + let root_exl_file = self.extract("exd/root.exl").unwrap(); + + let root_exl = EXL::from_existing(&root_exl_file).unwrap(); + + for (row, _) in root_exl.entries { + if row == name { + let new_filename = name.to_lowercase(); + + let path = format!("exd/{new_filename}.exh"); + + return EXH::from_existing(&self.extract(&path).unwrap()) + } + } + + None + } + + pub fn read_excel_sheet(&self, name : &str, exh : &EXH, language : Language, page : usize) -> Option { + let exd_path = format!("exd/{}", EXD::calculate_filename(name, language, &exh.pages[page])); + + let exd_file = self.extract(&exd_path).unwrap(); + + EXD::from_existing(&exh, &exd_file) + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 6a9ab97..9cfbb70 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,4 +32,6 @@ mod macros; pub mod blowfish; mod blowfish_constants; -pub mod installer; \ No newline at end of file +pub mod installer; +pub mod exh; +pub mod exd; \ No newline at end of file