mirror of
https://github.com/redstrate/Physis.git
synced 2025-05-16 23:17:45 +00:00
Move EXD parsing functions into their own module
It was getting hard to work in here, so I moved all of the private-API-parsing-things into their own file.
This commit is contained in:
parent
8d94c630ab
commit
8a3e8dad64
5 changed files with 370 additions and 355 deletions
372
src/exd.rs
372
src/exd.rs
|
@ -1,13 +1,14 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
use std::io::{BufWriter, Cursor, Read, Seek, SeekFrom, Write};
|
use std::io::{BufWriter, Cursor};
|
||||||
|
|
||||||
use binrw::{BinRead, Endian};
|
use binrw::BinRead;
|
||||||
use binrw::{BinResult, BinWrite, binrw};
|
use binrw::{BinWrite, binrw};
|
||||||
|
|
||||||
use crate::common::Language;
|
use crate::common::Language;
|
||||||
use crate::exh::{ColumnDataType, EXH, ExcelColumnDefinition, ExcelDataPagination};
|
use crate::exd_file_operations::{parse_rows, read_data_sections, write_rows};
|
||||||
|
use crate::exh::{EXH, ExcelDataPagination};
|
||||||
use crate::{ByteBuffer, ByteSpan};
|
use crate::{ByteBuffer, ByteSpan};
|
||||||
|
|
||||||
#[binrw]
|
#[binrw]
|
||||||
|
@ -15,262 +16,37 @@ use crate::{ByteBuffer, ByteSpan};
|
||||||
#[brw(big)]
|
#[brw(big)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct EXDHeader {
|
pub(crate) struct EXDHeader {
|
||||||
/// Usually 2, I don't think I've seen any other version
|
/// Usually 2, I don't think I've seen any other version
|
||||||
version: u16,
|
pub(crate) version: u16,
|
||||||
/// Seems to be 0?
|
/// Seems to be 0?
|
||||||
unk1: u16,
|
pub(crate) unk1: u16,
|
||||||
/// Size of the data offsets in bytes
|
/// Size of the data offsets in bytes
|
||||||
data_offset_size: u32,
|
pub(crate) data_offset_size: u32,
|
||||||
#[brw(pad_after = 16)] // padding
|
#[brw(pad_after = 16)] // padding
|
||||||
/// Size of the data sections in bytes
|
/// Size of the data sections in bytes
|
||||||
data_section_size: u32,
|
pub(crate) data_section_size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[binrw]
|
#[binrw]
|
||||||
#[brw(big)]
|
#[brw(big)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ExcelDataOffset {
|
pub(crate) struct ExcelDataOffset {
|
||||||
/// The row ID associated with this data offset
|
/// The row ID associated with this data offset
|
||||||
row_id: u32,
|
pub(crate) row_id: u32,
|
||||||
/// Offset to it's data section in bytes from the start of the file.
|
/// Offset to it's data section in bytes from the start of the file.
|
||||||
pub offset: u32,
|
pub(crate) offset: u32,
|
||||||
}
|
|
||||||
|
|
||||||
#[binrw::parser(reader)]
|
|
||||||
fn read_data_sections(header: &EXDHeader) -> BinResult<Vec<DataSection>> {
|
|
||||||
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)]
|
|
||||||
fn parse_rows(exh: &EXH, data_offsets: &Vec<ExcelDataOffset>) -> BinResult<Vec<ExcelRow>> {
|
|
||||||
let mut rows = Vec::new();
|
|
||||||
|
|
||||||
for offset in data_offsets {
|
|
||||||
reader.seek(SeekFrom::Start(offset.offset.into()))?;
|
|
||||||
|
|
||||||
// TODO: use DataSection here
|
|
||||||
let size: u32 = u32::read_be(reader).unwrap();
|
|
||||||
let row_count: u16 = u16::read_be(reader).unwrap();
|
|
||||||
//let row_header = DataSection::read(reader)?;
|
|
||||||
|
|
||||||
let data_offset = reader.stream_position().unwrap() as u32;
|
|
||||||
|
|
||||||
let mut read_row = |row_offset: u32| -> Option<ExcelSingleRow> {
|
|
||||||
let mut subrow = ExcelSingleRow {
|
|
||||||
columns: Vec::with_capacity(exh.column_definitions.len()),
|
|
||||||
};
|
|
||||||
|
|
||||||
for column in &exh.column_definitions {
|
|
||||||
reader
|
|
||||||
.seek(SeekFrom::Start((row_offset + column.offset as u32).into()))
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
subrow
|
|
||||||
.columns
|
|
||||||
.push(EXD::read_column(reader, exh, row_offset, column).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(subrow)
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_row = if row_count > 1 {
|
|
||||||
let mut rows = Vec::new();
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
ExcelRowKind::SubRows(rows)
|
|
||||||
} else {
|
|
||||||
ExcelRowKind::SingleRow(read_row(data_offset).unwrap())
|
|
||||||
};
|
|
||||||
rows.push(ExcelRow {
|
|
||||||
row_id: offset.row_id,
|
|
||||||
kind: new_row,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[binrw::writer(writer)]
|
|
||||||
fn write_rows(rows: &Vec<ExcelRow>, exh: &EXH) -> BinResult<()> {
|
|
||||||
// seek past the data offsets, which we will write later
|
|
||||||
let data_offsets_pos = writer.stream_position().unwrap();
|
|
||||||
writer
|
|
||||||
.seek(SeekFrom::Current(
|
|
||||||
(core::mem::size_of::<ExcelDataOffset>() * rows.len()) as i64,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut data_offsets = Vec::new();
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
data_offsets.push(ExcelDataOffset {
|
|
||||||
row_id: row.row_id,
|
|
||||||
offset: writer.stream_position().unwrap() as u32,
|
|
||||||
});
|
|
||||||
|
|
||||||
// skip row header for now, because we don't know the size yet!
|
|
||||||
let row_header_pos = writer.stream_position().unwrap();
|
|
||||||
|
|
||||||
writer.seek(SeekFrom::Current(6)).unwrap(); // u32 + u16
|
|
||||||
|
|
||||||
let old_pos = writer.stream_position().unwrap();
|
|
||||||
|
|
||||||
// write column data
|
|
||||||
{
|
|
||||||
let mut write_row = |row: &ExcelSingleRow| {
|
|
||||||
let mut column_definitions: Vec<(ExcelColumnDefinition, ColumnData)> = exh
|
|
||||||
.column_definitions
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.zip(row.columns.clone().into_iter())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// we need to sort them by offset
|
|
||||||
column_definitions.sort_by(|(a, _), (b, _)| a.offset.cmp(&b.offset));
|
|
||||||
|
|
||||||
for (definition, column) in &column_definitions {
|
|
||||||
EXD::write_column(writer, &column, &definition);
|
|
||||||
|
|
||||||
// TODO: temporary workaround until i can figure out why it has 4 extra bytes in test_write's case
|
|
||||||
if definition.data_type == ColumnDataType::Int8 && column_definitions.len() == 1
|
|
||||||
{
|
|
||||||
0u32.write_le(writer).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle packed bools
|
|
||||||
let mut packed_byte = 0u8;
|
|
||||||
let mut byte_offset = 0;
|
|
||||||
|
|
||||||
let mut write_packed_bool =
|
|
||||||
|definition: &ExcelColumnDefinition, shift: i32, boolean: &bool| {
|
|
||||||
byte_offset = definition.offset; // NOTE: it looks like there is only one byte for all of the packed booleans
|
|
||||||
|
|
||||||
if *boolean {
|
|
||||||
let bit = 1 << shift;
|
|
||||||
packed_byte |= bit;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (definition, column) in &column_definitions {
|
|
||||||
match &column {
|
|
||||||
ColumnData::Bool(val) => match definition.data_type {
|
|
||||||
ColumnDataType::PackedBool0 => write_packed_bool(definition, 0, val),
|
|
||||||
ColumnDataType::PackedBool1 => write_packed_bool(definition, 1, val),
|
|
||||||
ColumnDataType::PackedBool2 => write_packed_bool(definition, 2, val),
|
|
||||||
ColumnDataType::PackedBool3 => write_packed_bool(definition, 3, val),
|
|
||||||
ColumnDataType::PackedBool4 => write_packed_bool(definition, 4, val),
|
|
||||||
ColumnDataType::PackedBool5 => write_packed_bool(definition, 5, val),
|
|
||||||
ColumnDataType::PackedBool6 => write_packed_bool(definition, 6, val),
|
|
||||||
ColumnDataType::PackedBool7 => write_packed_bool(definition, 7, val),
|
|
||||||
_ => {} // not relevant
|
|
||||||
},
|
|
||||||
_ => {} // not relevant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the new packed boolean byte
|
|
||||||
// NOTE: This is a terrible way to check if there are packed booleans
|
|
||||||
// NOTE: Assumption: the packed boolean is always at the end of the row
|
|
||||||
if byte_offset != 0 {
|
|
||||||
packed_byte.write_le(writer).unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match &row.kind {
|
|
||||||
ExcelRowKind::SingleRow(excel_single_row) => write_row(excel_single_row),
|
|
||||||
ExcelRowKind::SubRows(excel_single_rows) => {
|
|
||||||
for row in excel_single_rows {
|
|
||||||
write_row(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write strings at the end of column data
|
|
||||||
{
|
|
||||||
let mut write_row_strings = |row: &ExcelSingleRow| {
|
|
||||||
for column in &row.columns {
|
|
||||||
match column {
|
|
||||||
ColumnData::String(val) => {
|
|
||||||
let bytes = val.as_bytes();
|
|
||||||
bytes.write(writer).unwrap();
|
|
||||||
|
|
||||||
// nul terminator
|
|
||||||
0u8.write_le(writer).unwrap();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// aligned to the next 4 byte boundary
|
|
||||||
let boundary_pos = writer.stream_position().unwrap();
|
|
||||||
let remainder = (boundary_pos + 4 - 1) / 4 * 4;
|
|
||||||
for _ in 0..remainder - boundary_pos {
|
|
||||||
0u8.write_le(writer).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_pos = writer.stream_position().unwrap();
|
|
||||||
|
|
||||||
// write row header
|
|
||||||
writer.seek(SeekFrom::Start(row_header_pos)).unwrap();
|
|
||||||
|
|
||||||
let row_header = DataSection {
|
|
||||||
size: (new_pos - old_pos) as u32,
|
|
||||||
row_count: 1, // TODO: hardcoded
|
|
||||||
};
|
|
||||||
row_header.write(writer).unwrap();
|
|
||||||
|
|
||||||
// restore pos
|
|
||||||
writer.seek(SeekFrom::Start(new_pos)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// now write the data offsets
|
|
||||||
writer.seek(SeekFrom::Start(data_offsets_pos)).unwrap();
|
|
||||||
data_offsets.write(writer).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[binrw]
|
#[binrw]
|
||||||
#[brw(big)]
|
#[brw(big)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct DataSection {
|
pub(crate) struct DataSection {
|
||||||
/// Size of the data section in bytes.
|
/// Size of the data section in bytes.
|
||||||
size: u32,
|
pub(crate) size: u32,
|
||||||
/// The number of rows in this data section.
|
/// The number of rows in this data section.
|
||||||
row_count: u16,
|
pub(crate) row_count: u16,
|
||||||
/// The bytes of this data section.
|
/// The bytes of this data section.
|
||||||
/// We currently don't use this in our parsing, see parse_rows.
|
/// We currently don't use this in our parsing, see parse_rows.
|
||||||
#[br(temp, count = size)]
|
#[br(temp, count = size)]
|
||||||
|
@ -296,8 +72,8 @@ pub struct EXD {
|
||||||
data: Vec<DataSection>,
|
data: Vec<DataSection>,
|
||||||
|
|
||||||
/// The rows contained in this EXD.
|
/// The rows contained in this EXD.
|
||||||
#[br(parse_with = parse_rows, args(&exh, &data_offsets))]
|
#[br(parse_with = parse_rows, args(exh, &data_offsets))]
|
||||||
#[bw(write_with = write_rows, args(&exh))]
|
#[bw(write_with = write_rows, args(exh))]
|
||||||
pub rows: Vec<ExcelRow>,
|
pub rows: Vec<ExcelRow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,117 +216,7 @@ impl EXD {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return None;
|
None
|
||||||
}
|
|
||||||
|
|
||||||
fn read_data_raw<T: Read + Seek, Z: BinRead<Args<'static> = ()>>(cursor: &mut T) -> Option<Z> {
|
|
||||||
Z::read_options(cursor, Endian::Big, ()).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_column<T: Read + Seek>(
|
|
||||||
cursor: &mut T,
|
|
||||||
exh: &EXH,
|
|
||||||
row_offset: u32,
|
|
||||||
column: &ExcelColumnDefinition,
|
|
||||||
) -> Option<ColumnData> {
|
|
||||||
let mut read_packed_bool = |shift: i32| -> bool {
|
|
||||||
let bit = 1 << shift;
|
|
||||||
let bool_data: u8 = Self::read_data_raw(cursor).unwrap_or(0);
|
|
||||||
|
|
||||||
(bool_data & bit) == bit
|
|
||||||
};
|
|
||||||
|
|
||||||
match column.data_type {
|
|
||||||
ColumnDataType::String => {
|
|
||||||
let string_offset: u32 = Self::read_data_raw(cursor).unwrap();
|
|
||||||
|
|
||||||
cursor
|
|
||||||
.seek(SeekFrom::Start(
|
|
||||||
(row_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))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_data_raw<T: Write + Seek, Z: BinWrite<Args<'static> = ()>>(cursor: &mut T, value: &Z) {
|
|
||||||
value.write_options(cursor, Endian::Big, ()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_column<T: Write + Seek>(
|
|
||||||
cursor: &mut T,
|
|
||||||
column: &ColumnData,
|
|
||||||
column_definition: &ExcelColumnDefinition,
|
|
||||||
) {
|
|
||||||
match column {
|
|
||||||
ColumnData::String(_) => {
|
|
||||||
let string_offset = 0u32; // TODO, but 0 is fine for single string column data
|
|
||||||
Self::write_data_raw(cursor, &string_offset);
|
|
||||||
}
|
|
||||||
ColumnData::Bool(_) => match column_definition.data_type {
|
|
||||||
ColumnDataType::Bool => todo!(),
|
|
||||||
// packed bools are handled in write_rows
|
|
||||||
ColumnDataType::PackedBool0 => {}
|
|
||||||
ColumnDataType::PackedBool1 => {}
|
|
||||||
ColumnDataType::PackedBool2 => {}
|
|
||||||
ColumnDataType::PackedBool3 => {}
|
|
||||||
ColumnDataType::PackedBool4 => {}
|
|
||||||
ColumnDataType::PackedBool5 => {}
|
|
||||||
ColumnDataType::PackedBool6 => {}
|
|
||||||
ColumnDataType::PackedBool7 => {}
|
|
||||||
_ => 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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the filename of an EXD from the `name`, `language`, and `page`.
|
/// Calculate the filename of an EXD from the `name`, `language`, and `page`.
|
||||||
|
|
348
src/exd_file_operations.rs
Normal file
348
src/exd_file_operations.rs
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinResult, BinWrite, Endian};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
exd::{
|
||||||
|
ColumnData, DataSection, EXD, EXDHeader, ExcelDataOffset, ExcelRow, ExcelRowKind,
|
||||||
|
ExcelSingleRow,
|
||||||
|
},
|
||||||
|
exh::{ColumnDataType, EXH, ExcelColumnDefinition},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[binrw::parser(reader)]
|
||||||
|
pub fn read_data_sections(header: &EXDHeader) -> BinResult<Vec<DataSection>> {
|
||||||
|
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)]
|
||||||
|
pub fn parse_rows(exh: &EXH, data_offsets: &Vec<ExcelDataOffset>) -> BinResult<Vec<ExcelRow>> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
|
for offset in data_offsets {
|
||||||
|
reader.seek(SeekFrom::Start(offset.offset.into()))?;
|
||||||
|
|
||||||
|
// TODO: use DataSection here
|
||||||
|
let size: u32 = u32::read_be(reader).unwrap();
|
||||||
|
let row_count: u16 = u16::read_be(reader).unwrap();
|
||||||
|
//let row_header = DataSection::read(reader)?;
|
||||||
|
|
||||||
|
let data_offset = reader.stream_position().unwrap() as u32;
|
||||||
|
|
||||||
|
let mut read_row = |row_offset: u32| -> Option<ExcelSingleRow> {
|
||||||
|
let mut subrow = ExcelSingleRow {
|
||||||
|
columns: Vec::with_capacity(exh.column_definitions.len()),
|
||||||
|
};
|
||||||
|
|
||||||
|
for column in &exh.column_definitions {
|
||||||
|
reader
|
||||||
|
.seek(SeekFrom::Start((row_offset + column.offset as u32).into()))
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
subrow
|
||||||
|
.columns
|
||||||
|
.push(EXD::read_column(reader, exh, row_offset, column).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(subrow)
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_row = if row_count > 1 {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
ExcelRowKind::SubRows(rows)
|
||||||
|
} else {
|
||||||
|
ExcelRowKind::SingleRow(read_row(data_offset).unwrap())
|
||||||
|
};
|
||||||
|
rows.push(ExcelRow {
|
||||||
|
row_id: offset.row_id,
|
||||||
|
kind: new_row,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw::writer(writer)]
|
||||||
|
pub fn write_rows(rows: &Vec<ExcelRow>, exh: &EXH) -> BinResult<()> {
|
||||||
|
// seek past the data offsets, which we will write later
|
||||||
|
let data_offsets_pos = writer.stream_position().unwrap();
|
||||||
|
writer
|
||||||
|
.seek(SeekFrom::Current(
|
||||||
|
(core::mem::size_of::<ExcelDataOffset>() * rows.len()) as i64,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut data_offsets = Vec::new();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
data_offsets.push(ExcelDataOffset {
|
||||||
|
row_id: row.row_id,
|
||||||
|
offset: writer.stream_position().unwrap() as u32,
|
||||||
|
});
|
||||||
|
|
||||||
|
// skip row header for now, because we don't know the size yet!
|
||||||
|
let row_header_pos = writer.stream_position().unwrap();
|
||||||
|
|
||||||
|
writer.seek(SeekFrom::Current(6)).unwrap(); // u32 + u16
|
||||||
|
|
||||||
|
let old_pos = writer.stream_position().unwrap();
|
||||||
|
|
||||||
|
// write column data
|
||||||
|
{
|
||||||
|
let mut write_row = |row: &ExcelSingleRow| {
|
||||||
|
let mut column_definitions: Vec<(ExcelColumnDefinition, ColumnData)> = exh
|
||||||
|
.column_definitions
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.zip(row.columns.clone().into_iter())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// we need to sort them by offset
|
||||||
|
column_definitions.sort_by(|(a, _), (b, _)| a.offset.cmp(&b.offset));
|
||||||
|
|
||||||
|
for (definition, column) in &column_definitions {
|
||||||
|
EXD::write_column(writer, column, definition);
|
||||||
|
|
||||||
|
// TODO: temporary workaround until i can figure out why it has 4 extra bytes in test_write's case
|
||||||
|
if definition.data_type == ColumnDataType::Int8 && column_definitions.len() == 1
|
||||||
|
{
|
||||||
|
0u32.write_le(writer).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle packed bools
|
||||||
|
let mut packed_byte = 0u8;
|
||||||
|
let mut byte_offset = 0;
|
||||||
|
|
||||||
|
let mut write_packed_bool =
|
||||||
|
|definition: &ExcelColumnDefinition, shift: i32, boolean: &bool| {
|
||||||
|
byte_offset = definition.offset; // NOTE: it looks like there is only one byte for all of the packed booleans
|
||||||
|
|
||||||
|
if *boolean {
|
||||||
|
let bit = 1 << shift;
|
||||||
|
packed_byte |= bit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (definition, column) in &column_definitions {
|
||||||
|
match &column {
|
||||||
|
ColumnData::Bool(val) => match definition.data_type {
|
||||||
|
ColumnDataType::PackedBool0 => write_packed_bool(definition, 0, val),
|
||||||
|
ColumnDataType::PackedBool1 => write_packed_bool(definition, 1, val),
|
||||||
|
ColumnDataType::PackedBool2 => write_packed_bool(definition, 2, val),
|
||||||
|
ColumnDataType::PackedBool3 => write_packed_bool(definition, 3, val),
|
||||||
|
ColumnDataType::PackedBool4 => write_packed_bool(definition, 4, val),
|
||||||
|
ColumnDataType::PackedBool5 => write_packed_bool(definition, 5, val),
|
||||||
|
ColumnDataType::PackedBool6 => write_packed_bool(definition, 6, val),
|
||||||
|
ColumnDataType::PackedBool7 => write_packed_bool(definition, 7, val),
|
||||||
|
_ => {} // not relevant
|
||||||
|
},
|
||||||
|
_ => {} // not relevant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the new packed boolean byte
|
||||||
|
// NOTE: This is a terrible way to check if there are packed booleans
|
||||||
|
// NOTE: Assumption: the packed boolean is always at the end of the row
|
||||||
|
if byte_offset != 0 {
|
||||||
|
packed_byte.write_le(writer).unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &row.kind {
|
||||||
|
ExcelRowKind::SingleRow(excel_single_row) => write_row(excel_single_row),
|
||||||
|
ExcelRowKind::SubRows(excel_single_rows) => {
|
||||||
|
for row in excel_single_rows {
|
||||||
|
write_row(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write strings at the end of column data
|
||||||
|
{
|
||||||
|
let mut write_row_strings = |row: &ExcelSingleRow| {
|
||||||
|
for column in &row.columns {
|
||||||
|
if let ColumnData::String(val) = column {
|
||||||
|
let bytes = val.as_bytes();
|
||||||
|
bytes.write(writer).unwrap();
|
||||||
|
|
||||||
|
// nul terminator
|
||||||
|
0u8.write_le(writer).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// aligned to the next 4 byte boundary
|
||||||
|
let boundary_pos = writer.stream_position().unwrap();
|
||||||
|
let remainder = boundary_pos.div_ceil(4) * 4;
|
||||||
|
for _ in 0..remainder - boundary_pos {
|
||||||
|
0u8.write_le(writer).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_pos = writer.stream_position().unwrap();
|
||||||
|
|
||||||
|
// write row header
|
||||||
|
writer.seek(SeekFrom::Start(row_header_pos)).unwrap();
|
||||||
|
|
||||||
|
let row_header = DataSection {
|
||||||
|
size: (new_pos - old_pos) as u32,
|
||||||
|
row_count: 1, // TODO: hardcoded
|
||||||
|
};
|
||||||
|
row_header.write(writer).unwrap();
|
||||||
|
|
||||||
|
// restore pos
|
||||||
|
writer.seek(SeekFrom::Start(new_pos)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// now write the data offsets
|
||||||
|
writer.seek(SeekFrom::Start(data_offsets_pos)).unwrap();
|
||||||
|
data_offsets.write(writer).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EXD {
|
||||||
|
fn read_data_raw<T: Read + Seek, Z: BinRead<Args<'static> = ()>>(cursor: &mut T) -> Option<Z> {
|
||||||
|
Z::read_options(cursor, Endian::Big, ()).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_column<T: Read + Seek>(
|
||||||
|
cursor: &mut T,
|
||||||
|
exh: &EXH,
|
||||||
|
row_offset: u32,
|
||||||
|
column: &ExcelColumnDefinition,
|
||||||
|
) -> Option<ColumnData> {
|
||||||
|
let mut read_packed_bool = |shift: i32| -> bool {
|
||||||
|
let bit = 1 << shift;
|
||||||
|
let bool_data: u8 = Self::read_data_raw(cursor).unwrap_or(0);
|
||||||
|
|
||||||
|
(bool_data & bit) == bit
|
||||||
|
};
|
||||||
|
|
||||||
|
match column.data_type {
|
||||||
|
ColumnDataType::String => {
|
||||||
|
let string_offset: u32 = Self::read_data_raw(cursor).unwrap();
|
||||||
|
|
||||||
|
cursor
|
||||||
|
.seek(SeekFrom::Start(
|
||||||
|
(row_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))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_data_raw<T: Write + Seek, Z: BinWrite<Args<'static> = ()>>(cursor: &mut T, value: &Z) {
|
||||||
|
value.write_options(cursor, Endian::Big, ()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_column<T: Write + Seek>(
|
||||||
|
cursor: &mut T,
|
||||||
|
column: &ColumnData,
|
||||||
|
column_definition: &ExcelColumnDefinition,
|
||||||
|
) {
|
||||||
|
match column {
|
||||||
|
ColumnData::String(_) => {
|
||||||
|
let string_offset = 0u32; // TODO, but 0 is fine for single string column data
|
||||||
|
Self::write_data_raw(cursor, &string_offset);
|
||||||
|
}
|
||||||
|
ColumnData::Bool(_) => match column_definition.data_type {
|
||||||
|
ColumnDataType::Bool => todo!(),
|
||||||
|
// packed bools are handled in write_rows
|
||||||
|
ColumnDataType::PackedBool0 => {}
|
||||||
|
ColumnDataType::PackedBool1 => {}
|
||||||
|
ColumnDataType::PackedBool2 => {}
|
||||||
|
ColumnDataType::PackedBool3 => {}
|
||||||
|
ColumnDataType::PackedBool4 => {}
|
||||||
|
ColumnDataType::PackedBool5 => {}
|
||||||
|
ColumnDataType::PackedBool6 => {}
|
||||||
|
ColumnDataType::PackedBool7 => {}
|
||||||
|
_ => 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -292,7 +292,7 @@ impl GameData {
|
||||||
|
|
||||||
let exd_file = self.extract(&exd_path)?;
|
let exd_file = self.extract(&exd_path)?;
|
||||||
|
|
||||||
EXD::from_existing(&exh, &exd_file)
|
EXD::from_existing(exh, &exd_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the patch to game data and returns any errors it encounters. This function will not update the version in the GameData struct.
|
/// Applies the patch to game data and returns any errors it encounters. This function will not update the version in the GameData struct.
|
||||||
|
|
|
@ -90,6 +90,7 @@ pub mod layer;
|
||||||
pub mod tera;
|
pub mod tera;
|
||||||
|
|
||||||
mod common_file_operations;
|
mod common_file_operations;
|
||||||
|
mod exd_file_operations;
|
||||||
|
|
||||||
/// Reading word dictionaries, such as the vulgar word list.
|
/// Reading word dictionaries, such as the vulgar word list.
|
||||||
pub mod dic;
|
pub mod dic;
|
||||||
|
|
|
@ -190,7 +190,7 @@ impl PatchList {
|
||||||
size += patch.length;
|
size += patch.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return size;
|
size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue