initial quest file bin/dat support
includes loading as compressed/uncompressed. no saving support yet.
This commit is contained in:
parent
c71f9997bd
commit
6c672f3d27
BIN
psoutils/assets/test/q058-ret-gc.bin
Executable file
BIN
psoutils/assets/test/q058-ret-gc.bin
Executable file
Binary file not shown.
BIN
psoutils/assets/test/q058-ret-gc.dat
Executable file
BIN
psoutils/assets/test/q058-ret-gc.dat
Executable file
Binary file not shown.
BIN
psoutils/assets/test/q058-ret-gc.uncompressed.bin
Normal file
BIN
psoutils/assets/test/q058-ret-gc.uncompressed.bin
Normal file
Binary file not shown.
BIN
psoutils/assets/test/q058-ret-gc.uncompressed.dat
Normal file
BIN
psoutils/assets/test/q058-ret-gc.uncompressed.dat
Normal file
Binary file not shown.
BIN
psoutils/assets/test/q118-vr-gc.bin
Executable file
BIN
psoutils/assets/test/q118-vr-gc.bin
Executable file
Binary file not shown.
BIN
psoutils/assets/test/q118-vr-gc.dat
Executable file
BIN
psoutils/assets/test/q118-vr-gc.dat
Executable file
Binary file not shown.
BIN
psoutils/assets/test/q118-vr-gc.uncompressed.bin
Normal file
BIN
psoutils/assets/test/q118-vr-gc.uncompressed.bin
Normal file
Binary file not shown.
BIN
psoutils/assets/test/q118-vr-gc.uncompressed.dat
Normal file
BIN
psoutils/assets/test/q118-vr-gc.uncompressed.dat
Normal file
Binary file not shown.
110
psoutils/src/bytes.rs
Normal file
110
psoutils/src/bytes.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ReadBytesError {
|
||||||
|
#[error("Unexpected error while reading bytes: {0}")]
|
||||||
|
UnexpectedError(String),
|
||||||
|
|
||||||
|
#[error("I/O error reading bytes")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ReadFromBytes<T: ReadBytesExt>: Sized {
|
||||||
|
fn read_from_bytes(reader: &mut T) -> Result<Self, ReadBytesError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum WriteBytesError {
|
||||||
|
#[error("Unexpected error while writing bytes: {0}")]
|
||||||
|
UnexpectedError(String),
|
||||||
|
|
||||||
|
#[error("I/O error writing bytes")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WriteAsBytes<T: WriteBytesExt> {
|
||||||
|
fn write_as_bytes(&self, writer: &mut T) -> Result<(), WriteBytesError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FixedLengthByteArrays {
|
||||||
|
fn as_unpadded_slice(&self) -> &[u8];
|
||||||
|
fn to_fixed_length(&self, length: usize) -> Vec<u8>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]> + ?Sized> FixedLengthByteArrays for T {
|
||||||
|
fn as_unpadded_slice(&self) -> &[u8] {
|
||||||
|
let end = self.as_ref().iter().take_while(|&b| *b != 0).count();
|
||||||
|
&self.as_ref()[0..end]
|
||||||
|
/*
|
||||||
|
self.as_ref()
|
||||||
|
.iter()
|
||||||
|
.take_while(|&b| *b != 0u8)
|
||||||
|
.map(|b| *b)
|
||||||
|
.collect()
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_fixed_length(&self, length: usize) -> Vec<u8> {
|
||||||
|
let mut result = self.as_ref().to_vec();
|
||||||
|
if result.len() != length {
|
||||||
|
result.resize(length, 0u8);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn fixed_length_byte_arrays() {
|
||||||
|
let bytes: &[u8] = &[
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72
|
||||||
|
],
|
||||||
|
bytes.as_unpadded_slice()
|
||||||
|
);
|
||||||
|
|
||||||
|
let bytes: &[u8] = &[
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72
|
||||||
|
],
|
||||||
|
bytes.as_unpadded_slice()
|
||||||
|
);
|
||||||
|
|
||||||
|
let bytes: &[u8] = &[
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
],
|
||||||
|
bytes.to_fixed_length(32)
|
||||||
|
);
|
||||||
|
|
||||||
|
let bytes: &[u8] = &[
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||||
|
],
|
||||||
|
bytes.to_fixed_length(14)
|
||||||
|
);
|
||||||
|
|
||||||
|
let bytes: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||||
|
assert_eq!(vec![0x01, 0x02, 0x03, 0x04], bytes.to_fixed_length(4));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,5 @@
|
||||||
|
pub mod bytes;
|
||||||
pub mod compression;
|
pub mod compression;
|
||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
|
pub mod quest;
|
||||||
|
pub mod text;
|
||||||
|
|
50
psoutils/src/quest.rs
Normal file
50
psoutils/src/quest.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::bytes::{ReadBytesError, WriteBytesError};
|
||||||
|
use crate::quest::bin::QuestBin;
|
||||||
|
use crate::quest::dat::QuestDat;
|
||||||
|
use crate::text::LanguageError;
|
||||||
|
|
||||||
|
pub mod bin;
|
||||||
|
pub mod dat;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum QuestError {
|
||||||
|
#[error("I/O error reading quest")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("String encoding error during processing of quest string field")]
|
||||||
|
StringEncodingError(#[from] LanguageError),
|
||||||
|
|
||||||
|
#[error("Error reading quest from bytes")]
|
||||||
|
ReadFromBytesError(#[from] ReadBytesError),
|
||||||
|
|
||||||
|
#[error("Error writing quest as bytes")]
|
||||||
|
WriteAsBytesError(#[from] WriteBytesError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Quest {
|
||||||
|
pub bin: QuestBin,
|
||||||
|
pub dat: QuestDat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Quest {
|
||||||
|
pub fn from_compressed_bindat(bin_path: &Path, dat_path: &Path) -> Result<Quest, QuestError> {
|
||||||
|
let bin = QuestBin::from_compressed_file(bin_path)?;
|
||||||
|
let dat = QuestDat::from_compressed_file(dat_path)?;
|
||||||
|
|
||||||
|
Ok(Quest { bin, dat })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_uncompressed_bindat(bin_path: &Path, dat_path: &Path) -> Result<Quest, QuestError> {
|
||||||
|
let bin = QuestBin::from_uncompressed_file(bin_path)?;
|
||||||
|
let dat = QuestDat::from_uncompressed_file(dat_path)?;
|
||||||
|
|
||||||
|
Ok(Quest { bin, dat })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
343
psoutils/src/quest/bin.rs
Normal file
343
psoutils/src/quest/bin.rs
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Cursor, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use crate::bytes::*;
|
||||||
|
use crate::compression::{prs_compress, prs_decompress};
|
||||||
|
use crate::quest::QuestError;
|
||||||
|
use crate::text::Language;
|
||||||
|
|
||||||
|
pub const QUEST_BIN_NAME_LENGTH: usize = 32;
|
||||||
|
pub const QUEST_BIN_SHORT_DESCRIPTION_LENGTH: usize = 128;
|
||||||
|
pub const QUEST_BIN_LONG_DESCRIPTION_LENGTH: usize = 288;
|
||||||
|
|
||||||
|
pub const QUEST_BIN_HEADER_SIZE: usize = 20
|
||||||
|
+ QUEST_BIN_NAME_LENGTH
|
||||||
|
+ QUEST_BIN_SHORT_DESCRIPTION_LENGTH
|
||||||
|
+ QUEST_BIN_LONG_DESCRIPTION_LENGTH;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct QuestNumberAndEpisode {
|
||||||
|
pub number: u8,
|
||||||
|
pub episode: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub union QuestNumber {
|
||||||
|
pub number_and_episode: QuestNumberAndEpisode,
|
||||||
|
pub number: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuestBinHeader {
|
||||||
|
pub is_download: bool,
|
||||||
|
pub language: Language,
|
||||||
|
pub quest_number: QuestNumber,
|
||||||
|
pub name: String,
|
||||||
|
pub short_description: String,
|
||||||
|
pub long_description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuestBinHeader {
|
||||||
|
// the reality is that i kind of have to support access to the quest_number/episode as u8's as
|
||||||
|
// well as the quest_number as a u16 simultaneously. it appears that all of sega's quests (at
|
||||||
|
// least, all of the ones i've looked at in detail) used the quest_number and episode fields as
|
||||||
|
// individual u8's, but there are quite a bunch of custom quests that stored quest_number
|
||||||
|
// values as a u16 (i believe this is Qedit's fault?)
|
||||||
|
|
||||||
|
pub fn quest_number(&self) -> u8 {
|
||||||
|
unsafe { self.quest_number.number_and_episode.number }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quest_number_u16(&self) -> u16 {
|
||||||
|
unsafe { self.quest_number.number }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn episode(&self) -> u8 {
|
||||||
|
unsafe { self.quest_number.number_and_episode.episode }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuestBin {
|
||||||
|
pub header: QuestBinHeader,
|
||||||
|
pub object_code: Box<[u8]>,
|
||||||
|
pub function_offset_table: Box<[u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuestBin {
|
||||||
|
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestBin, QuestError> {
|
||||||
|
let decompressed = prs_decompress(&bytes);
|
||||||
|
let mut reader = Cursor::new(decompressed);
|
||||||
|
Ok(QuestBin::read_from_bytes(&mut reader)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_compressed_file(path: &Path) -> Result<QuestBin, QuestError> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
file.read_to_end(&mut buffer)?;
|
||||||
|
QuestBin::from_compressed_bytes(&buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_uncompressed_file(path: &Path) -> Result<QuestBin, QuestError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
Ok(QuestBin::read_from_bytes(&mut reader)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, WriteBytesError> {
|
||||||
|
let bytes = self.to_uncompressed_bytes()?;
|
||||||
|
Ok(prs_compress(bytes.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_uncompressed_bytes(&self) -> Result<Box<[u8]>, WriteBytesError> {
|
||||||
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
self.write_as_bytes(&mut bytes)?;
|
||||||
|
Ok(bytes.into_inner().into_boxed_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_size(&self) -> usize {
|
||||||
|
QUEST_BIN_HEADER_SIZE
|
||||||
|
+ self.object_code.as_ref().len()
|
||||||
|
+ self.function_offset_table.as_ref().len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ReadBytesExt> ReadFromBytes<T> for QuestBin {
|
||||||
|
fn read_from_bytes(reader: &mut T) -> Result<Self, ReadBytesError> {
|
||||||
|
let object_code_offset = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let function_offset_table_offset = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let bin_size = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let _xfffffff = reader.read_u32::<LittleEndian>()?; // always 0xffffffff
|
||||||
|
let is_download = reader.read_u8()?;
|
||||||
|
let language = reader.read_u8()?;
|
||||||
|
let quest_number_and_episode = reader.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let is_download = if is_download == 0 { false } else { true };
|
||||||
|
let quest_number = QuestNumber {
|
||||||
|
number: quest_number_and_episode,
|
||||||
|
};
|
||||||
|
|
||||||
|
let language = match Language::from_number(language) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ReadBytesError::UnexpectedError(format!(
|
||||||
|
"Unsupported language value found in quest header: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(encoding) => encoding,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut name_bytes = [0u8; QUEST_BIN_NAME_LENGTH];
|
||||||
|
reader.read_exact(&mut name_bytes)?;
|
||||||
|
let name = match language.decode_text(name_bytes.as_unpadded_slice()) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ReadBytesError::UnexpectedError(format!(
|
||||||
|
"Error decoding string in quest 'name' field: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut short_description_bytes = [0u8; QUEST_BIN_SHORT_DESCRIPTION_LENGTH];
|
||||||
|
reader.read_exact(&mut short_description_bytes)?;
|
||||||
|
let short_description =
|
||||||
|
match language.decode_text(short_description_bytes.as_unpadded_slice()) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ReadBytesError::UnexpectedError(format!(
|
||||||
|
"Error decoding string in quest 'short_description' field: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut long_description_bytes = [0u8; QUEST_BIN_LONG_DESCRIPTION_LENGTH];
|
||||||
|
reader.read_exact(&mut long_description_bytes)?;
|
||||||
|
let long_description =
|
||||||
|
match language.decode_text(long_description_bytes.as_unpadded_slice()) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ReadBytesError::UnexpectedError(format!(
|
||||||
|
"Error decoding string in quest 'long_description' field: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut object_code =
|
||||||
|
vec![0u8; (function_offset_table_offset - object_code_offset) as usize];
|
||||||
|
reader.read_exact(&mut object_code)?;
|
||||||
|
|
||||||
|
let function_offset_table_size = bin_size - function_offset_table_offset;
|
||||||
|
if function_offset_table_size % 4 != 0 {
|
||||||
|
return Err(ReadBytesError::UnexpectedError(String::from("Non-dword-sized bytes found in quest bin where function offset table is expected (probably a PRS decompression issue?)")));
|
||||||
|
}
|
||||||
|
let mut function_offset_table = vec![0u8; function_offset_table_size as usize];
|
||||||
|
reader.read_exact(&mut function_offset_table)?;
|
||||||
|
|
||||||
|
Ok(QuestBin {
|
||||||
|
header: QuestBinHeader {
|
||||||
|
is_download,
|
||||||
|
language,
|
||||||
|
quest_number,
|
||||||
|
name,
|
||||||
|
short_description,
|
||||||
|
long_description,
|
||||||
|
},
|
||||||
|
object_code: object_code.into_boxed_slice(),
|
||||||
|
function_offset_table: function_offset_table.into_boxed_slice(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: WriteBytesExt> WriteAsBytes<T> for QuestBin {
|
||||||
|
fn write_as_bytes(&self, writer: &mut T) -> Result<(), WriteBytesError> {
|
||||||
|
let bin_size = self.calculate_size();
|
||||||
|
let object_code_offset = QUEST_BIN_HEADER_SIZE;
|
||||||
|
let function_offset_table_offset = QUEST_BIN_HEADER_SIZE + self.object_code.len();
|
||||||
|
|
||||||
|
writer.write_u32::<LittleEndian>(object_code_offset as u32)?;
|
||||||
|
writer.write_u32::<LittleEndian>(function_offset_table_offset as u32)?;
|
||||||
|
writer.write_u32::<LittleEndian>(bin_size as u32)?;
|
||||||
|
writer.write_u32::<LittleEndian>(0xfffffff)?; // always 0xffffffff
|
||||||
|
writer.write_u8(self.header.is_download as u8)?;
|
||||||
|
writer.write_u8(self.header.language as u8)?;
|
||||||
|
writer.write_u16::<LittleEndian>(unsafe { self.header.quest_number.number })?;
|
||||||
|
|
||||||
|
let language = self.header.language;
|
||||||
|
|
||||||
|
let name_bytes = match language.encode_text(&self.header.name) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(WriteBytesError::UnexpectedError(format!(
|
||||||
|
"Error encoding string for quest 'name' field: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
writer.write_all(&name_bytes.to_fixed_length(QUEST_BIN_NAME_LENGTH))?;
|
||||||
|
|
||||||
|
let short_description_bytes = match language.encode_text(&self.header.short_description) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(WriteBytesError::UnexpectedError(format!(
|
||||||
|
"Error encoding string for quest 'short_description_bytes' field: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
writer.write_all(
|
||||||
|
&short_description_bytes.to_fixed_length(QUEST_BIN_SHORT_DESCRIPTION_LENGTH),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let long_description_bytes = match language.encode_text(&self.header.long_description) {
|
||||||
|
Err(e) => {
|
||||||
|
return Err(WriteBytesError::UnexpectedError(format!(
|
||||||
|
"Error encoding string for quest 'long_description_bytes' field: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
writer.write_all(
|
||||||
|
&long_description_bytes.to_fixed_length(QUEST_BIN_LONG_DESCRIPTION_LENGTH),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
writer.write_all(self.object_code.as_ref())?;
|
||||||
|
writer.write_all(self.function_offset_table.as_ref())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn validate_quest_58_bin(bin: &QuestBin) {
|
||||||
|
assert_eq!(2000, bin.object_code.len());
|
||||||
|
assert_eq!(4008, bin.function_offset_table.len());
|
||||||
|
assert_eq!(6476, bin.calculate_size());
|
||||||
|
|
||||||
|
assert_eq!(58, unsafe { bin.header.quest_number.number });
|
||||||
|
assert_eq!(0, unsafe {
|
||||||
|
bin.header.quest_number.number_and_episode.episode
|
||||||
|
});
|
||||||
|
assert_eq!(58, unsafe {
|
||||||
|
bin.header.quest_number.number_and_episode.number
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(false, bin.header.is_download);
|
||||||
|
assert_eq!(Language::Japanese, bin.header.language);
|
||||||
|
|
||||||
|
assert_eq!("Lost HEAT SWORD", bin.header.name);
|
||||||
|
assert_eq!(
|
||||||
|
"Retrieve a\nweapon from\na Dragon!",
|
||||||
|
bin.header.short_description
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"Client: Hopkins, hunter\nQuest:\n My weapon was taken\n from me when I was\n fighting a Dragon.\nReward: ??? Meseta\n\n\n",
|
||||||
|
bin.header.long_description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_quest_118_bin(bin: &QuestBin) {
|
||||||
|
assert_eq!(32860, bin.object_code.len());
|
||||||
|
assert_eq!(22004, bin.function_offset_table.len());
|
||||||
|
assert_eq!(55332, bin.calculate_size());
|
||||||
|
|
||||||
|
assert_eq!(118, unsafe { bin.header.quest_number.number });
|
||||||
|
assert_eq!(0, unsafe {
|
||||||
|
bin.header.quest_number.number_and_episode.episode
|
||||||
|
});
|
||||||
|
assert_eq!(118, unsafe {
|
||||||
|
bin.header.quest_number.number_and_episode.number
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(false, bin.header.is_download);
|
||||||
|
assert_eq!(Language::Japanese, bin.header.language);
|
||||||
|
|
||||||
|
assert_eq!("Towards the Future", bin.header.name);
|
||||||
|
assert_eq!(
|
||||||
|
"Challenge the\nnew simulator.",
|
||||||
|
bin.header.short_description
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"Client: Principal\nQuest: Wishes to have\nhunters challenge the\nnew simulator\nReward: ??? Meseta",
|
||||||
|
bin.header.long_description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_compressed_quest_58_bin() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q058-ret-gc.bin");
|
||||||
|
let bin = QuestBin::from_compressed_file(&path)?;
|
||||||
|
validate_quest_58_bin(&bin);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_uncompressed_quest_58_bin() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q058-ret-gc.uncompressed.bin");
|
||||||
|
let bin = QuestBin::from_uncompressed_file(&path)?;
|
||||||
|
validate_quest_58_bin(&bin);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_compressed_quest_118_bin() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q118-vr-gc.bin");
|
||||||
|
let bin = QuestBin::from_compressed_file(&path)?;
|
||||||
|
validate_quest_118_bin(&bin);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_uncompressed_quest_118_bin() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q118-vr-gc.uncompressed.bin");
|
||||||
|
let bin = QuestBin::from_uncompressed_file(&path)?;
|
||||||
|
validate_quest_118_bin(&bin);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
522
psoutils/src/quest/dat.rs
Normal file
522
psoutils/src/quest/dat.rs
Normal file
|
@ -0,0 +1,522 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Cursor, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use crate::bytes::*;
|
||||||
|
use crate::compression::{prs_compress, prs_decompress};
|
||||||
|
use crate::quest::QuestError;
|
||||||
|
|
||||||
|
pub const QUEST_DAT_TABLE_HEADER_SIZE: usize = 16;
|
||||||
|
|
||||||
|
pub const QUEST_DAT_AREAS: [[&str; 18]; 2] = [
|
||||||
|
[
|
||||||
|
"Pioneer 2",
|
||||||
|
"Forest 1",
|
||||||
|
"Forest 2",
|
||||||
|
"Caves 1",
|
||||||
|
"Caves 2",
|
||||||
|
"Caves 3",
|
||||||
|
"Mines 1",
|
||||||
|
"Mines 2",
|
||||||
|
"Ruins 1",
|
||||||
|
"Ruins 2",
|
||||||
|
"Ruins 3",
|
||||||
|
"Under the Dome",
|
||||||
|
"Underground Channel",
|
||||||
|
"Monitor Room",
|
||||||
|
"????",
|
||||||
|
"Visual Lobby",
|
||||||
|
"VR Spaceship Alpha",
|
||||||
|
"VR Temple Alpha",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Lab",
|
||||||
|
"VR Temple Alpha",
|
||||||
|
"VR Temple Beta",
|
||||||
|
"VR Spaceship Alpha",
|
||||||
|
"VR Spaceship Beta",
|
||||||
|
"Central Control Area",
|
||||||
|
"Jungle North",
|
||||||
|
"Jungle East",
|
||||||
|
"Mountain",
|
||||||
|
"Seaside",
|
||||||
|
"Seabed Upper",
|
||||||
|
"Seabed Lower",
|
||||||
|
"Cliffs of Gal Da Val",
|
||||||
|
"Test Subject Disposal Area",
|
||||||
|
"VR Temple Final",
|
||||||
|
"VR Spaceship Final",
|
||||||
|
"Seaside Night",
|
||||||
|
"Control Tower",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
pub enum QuestDatTableType {
|
||||||
|
Object,
|
||||||
|
NPC,
|
||||||
|
Wave,
|
||||||
|
ChallengeModeSpawns,
|
||||||
|
ChallengeModeUnknown,
|
||||||
|
Unknown(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for QuestDatTableType {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use QuestDatTableType::*;
|
||||||
|
match self {
|
||||||
|
Object => write!(f, "Object"),
|
||||||
|
NPC => write!(f, "NPC"),
|
||||||
|
Wave => write!(f, "Wave"),
|
||||||
|
ChallengeModeSpawns => write!(f, "Challenge Mode Spawns"),
|
||||||
|
ChallengeModeUnknown => write!(f, "Challenge Mode Unknown"),
|
||||||
|
Unknown(n) => write!(f, "Unknown value ({})", n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for QuestDatTableType {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
// TODO: is there some way to cast an int back to an enum?
|
||||||
|
use QuestDatTableType::*;
|
||||||
|
match value {
|
||||||
|
1 => Object,
|
||||||
|
2 => NPC,
|
||||||
|
3 => Wave,
|
||||||
|
4 => ChallengeModeSpawns,
|
||||||
|
5 => ChallengeModeUnknown,
|
||||||
|
n => Unknown(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&QuestDatTableType> for u32 {
|
||||||
|
fn from(value: &QuestDatTableType) -> Self {
|
||||||
|
use QuestDatTableType::*;
|
||||||
|
match *value {
|
||||||
|
Object => 1,
|
||||||
|
NPC => 2,
|
||||||
|
Wave => 3,
|
||||||
|
ChallengeModeSpawns => 4,
|
||||||
|
ChallengeModeUnknown => 5,
|
||||||
|
Unknown(n) => n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuestDatTableHeader {
|
||||||
|
pub table_type: QuestDatTableType,
|
||||||
|
pub area: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuestDatTable {
|
||||||
|
pub header: QuestDatTableHeader,
|
||||||
|
pub bytes: Box<[u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum QuestArea {
|
||||||
|
Area(&'static str),
|
||||||
|
InvalidArea(u32),
|
||||||
|
InvalidEpisode(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuestDatTable {
|
||||||
|
pub fn table_type(&self) -> QuestDatTableType {
|
||||||
|
self.header.table_type
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area_name(&self, episode: u32) -> QuestArea {
|
||||||
|
use QuestArea::*;
|
||||||
|
match QUEST_DAT_AREAS.get(episode as usize) {
|
||||||
|
Some(list) => match list.get(self.header.area as usize) {
|
||||||
|
Some(area) => Area(area),
|
||||||
|
None => InvalidArea(self.header.area),
|
||||||
|
},
|
||||||
|
None => InvalidEpisode(episode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_size(&self) -> usize {
|
||||||
|
QUEST_DAT_TABLE_HEADER_SIZE + self.bytes.as_ref().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_size(&self) -> usize {
|
||||||
|
self.bytes.as_ref().len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QuestDat {
|
||||||
|
pub tables: Box<[QuestDatTable]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuestDat {
|
||||||
|
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestDat, QuestError> {
|
||||||
|
let decompressed = prs_decompress(&bytes);
|
||||||
|
let mut reader = Cursor::new(decompressed);
|
||||||
|
Ok(QuestDat::read_from_bytes(&mut reader)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_compressed_file(path: &Path) -> Result<QuestDat, QuestError> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
file.read_to_end(&mut buffer)?;
|
||||||
|
QuestDat::from_compressed_bytes(&buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_uncompressed_file(path: &Path) -> Result<QuestDat, QuestError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
Ok(QuestDat::read_from_bytes(&mut reader)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, WriteBytesError> {
|
||||||
|
let bytes = self.to_uncompressed_bytes()?;
|
||||||
|
Ok(prs_compress(bytes.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_uncompressed_bytes(&self) -> Result<Box<[u8]>, WriteBytesError> {
|
||||||
|
let mut bytes = Cursor::new(Vec::new());
|
||||||
|
self.write_as_bytes(&mut bytes)?;
|
||||||
|
Ok(bytes.into_inner().into_boxed_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_size(&self) -> usize {
|
||||||
|
self.tables
|
||||||
|
.iter()
|
||||||
|
.map(|table| QUEST_DAT_TABLE_HEADER_SIZE + table.body_size() as usize)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ReadBytesExt> ReadFromBytes<T> for QuestDat {
|
||||||
|
fn read_from_bytes(reader: &mut T) -> Result<Self, ReadBytesError> {
|
||||||
|
let mut tables = Vec::new();
|
||||||
|
loop {
|
||||||
|
let table_type = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let table_size = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let area = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let table_body_size = reader.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
// quest .dat files appear to always use a "zero-table" to mark the end of the file
|
||||||
|
if table_type == 0 && table_size == 0 && area == 0 && table_body_size == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body_bytes = vec![0u8; table_body_size as usize];
|
||||||
|
reader.read_exact(&mut body_bytes)?;
|
||||||
|
|
||||||
|
let table_type: QuestDatTableType = table_type.into();
|
||||||
|
|
||||||
|
tables.push(QuestDatTable {
|
||||||
|
header: QuestDatTableHeader { table_type, area },
|
||||||
|
bytes: body_bytes.into_boxed_slice(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(QuestDat {
|
||||||
|
tables: tables.into_boxed_slice(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: WriteBytesExt> WriteAsBytes<T> for QuestDat {
|
||||||
|
fn write_as_bytes(&self, writer: &mut T) -> Result<(), WriteBytesError> {
|
||||||
|
for table in self.tables.iter() {
|
||||||
|
let table_size = table.calculate_size() as u32;
|
||||||
|
let table_body_size = table.body_size() as u32;
|
||||||
|
|
||||||
|
writer.write_u32::<LittleEndian>((&table.header.table_type).into())?;
|
||||||
|
writer.write_u32::<LittleEndian>(table_size)?;
|
||||||
|
writer.write_u32::<LittleEndian>(table.header.area)?;
|
||||||
|
writer.write_u32::<LittleEndian>(table_body_size)?;
|
||||||
|
|
||||||
|
writer.write_all(table.bytes.as_ref())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write "zero table" at eof. this seems to be a convention used everywhere for quest .dat
|
||||||
|
writer.write_u32::<LittleEndian>(0)?; // table_type
|
||||||
|
writer.write_u32::<LittleEndian>(0)?; // table_size
|
||||||
|
writer.write_u32::<LittleEndian>(0)?; // area
|
||||||
|
writer.write_u32::<LittleEndian>(0)?; // table_body_size
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn validate_quest_58_dat(dat: &QuestDat) {
|
||||||
|
let episode = 0;
|
||||||
|
|
||||||
|
assert_eq!(11, dat.tables.len());
|
||||||
|
|
||||||
|
let table = &dat.tables[0];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(2260, table.calculate_size());
|
||||||
|
assert_eq!(2244, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[1];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(7020, table.calculate_size());
|
||||||
|
assert_eq!(7004, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 1"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[2];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(9536, table.calculate_size());
|
||||||
|
assert_eq!(9520, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[3];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(1376, table.calculate_size());
|
||||||
|
assert_eq!(1360, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[4];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(1672, table.calculate_size());
|
||||||
|
assert_eq!(1656, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[5];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(6064, table.calculate_size());
|
||||||
|
assert_eq!(6048, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 1"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[6];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(7432, table.calculate_size());
|
||||||
|
assert_eq!(7416, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[7];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(88, table.calculate_size());
|
||||||
|
assert_eq!(72, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[8];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(560, table.calculate_size());
|
||||||
|
assert_eq!(544, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 1"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[9];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(736, table.calculate_size());
|
||||||
|
assert_eq!(720, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[10];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(60, table.calculate_size());
|
||||||
|
assert_eq!(44, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_quest_118_dat(dat: &QuestDat) {
|
||||||
|
let episode = 0;
|
||||||
|
|
||||||
|
assert_eq!(25, dat.tables.len());
|
||||||
|
|
||||||
|
let table = &dat.tables[0];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(1988, table.calculate_size());
|
||||||
|
assert_eq!(1972, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[1];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(2872, table.calculate_size());
|
||||||
|
assert_eq!(2856, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Caves 3"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[2];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(2532, table.calculate_size());
|
||||||
|
assert_eq!(2516, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Mines 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[3];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(2668, table.calculate_size());
|
||||||
|
assert_eq!(2652, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Ruins 3"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[4];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(1580, table.calculate_size());
|
||||||
|
assert_eq!(1564, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[5];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(1104, table.calculate_size());
|
||||||
|
assert_eq!(1088, table.body_size());
|
||||||
|
assert_eq!(
|
||||||
|
QuestArea::Area("Underground Channel"),
|
||||||
|
table.area_name(episode)
|
||||||
|
);
|
||||||
|
|
||||||
|
let table = &dat.tables[6];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(2056, table.calculate_size());
|
||||||
|
assert_eq!(2040, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Monitor Room"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[7];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(2396, table.calculate_size());
|
||||||
|
assert_eq!(2380, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("????"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[8];
|
||||||
|
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||||
|
assert_eq!(1784, table.calculate_size());
|
||||||
|
assert_eq!(1768, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[9];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(1528, table.calculate_size());
|
||||||
|
assert_eq!(1512, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[10];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(2392, table.calculate_size());
|
||||||
|
assert_eq!(2376, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[11];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(3760, table.calculate_size());
|
||||||
|
assert_eq!(3744, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Caves 3"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[12];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(4480, table.calculate_size());
|
||||||
|
assert_eq!(4464, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Mines 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[13];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(3256, table.calculate_size());
|
||||||
|
assert_eq!(3240, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Ruins 3"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[14];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(88, table.calculate_size());
|
||||||
|
assert_eq!(72, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[15];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(88, table.calculate_size());
|
||||||
|
assert_eq!(72, table.body_size());
|
||||||
|
assert_eq!(
|
||||||
|
QuestArea::Area("Underground Channel"),
|
||||||
|
table.area_name(episode)
|
||||||
|
);
|
||||||
|
|
||||||
|
let table = &dat.tables[16];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(160, table.calculate_size());
|
||||||
|
assert_eq!(144, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Monitor Room"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[17];
|
||||||
|
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||||
|
assert_eq!(88, table.calculate_size());
|
||||||
|
assert_eq!(72, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("????"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[18];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(232, table.calculate_size());
|
||||||
|
assert_eq!(216, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[19];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(532, table.calculate_size());
|
||||||
|
assert_eq!(516, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Caves 3"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[20];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(768, table.calculate_size());
|
||||||
|
assert_eq!(752, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Mines 2"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[21];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(368, table.calculate_size());
|
||||||
|
assert_eq!(352, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Ruins 3"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[22];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(60, table.calculate_size());
|
||||||
|
assert_eq!(44, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||||
|
|
||||||
|
let table = &dat.tables[23];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(60, table.calculate_size());
|
||||||
|
assert_eq!(44, table.body_size());
|
||||||
|
assert_eq!(
|
||||||
|
QuestArea::Area("Underground Channel"),
|
||||||
|
table.area_name(episode)
|
||||||
|
);
|
||||||
|
|
||||||
|
let table = &dat.tables[24];
|
||||||
|
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||||
|
assert_eq!(68, table.calculate_size());
|
||||||
|
assert_eq!(52, table.body_size());
|
||||||
|
assert_eq!(QuestArea::Area("????"), table.area_name(episode));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_compressed_quest_58_dat() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q058-ret-gc.dat");
|
||||||
|
let dat = QuestDat::from_compressed_file(&path)?;
|
||||||
|
validate_quest_58_dat(&dat);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_uncompressed_quest_58_dat() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q058-ret-gc.uncompressed.dat");
|
||||||
|
let dat = QuestDat::from_uncompressed_file(&path)?;
|
||||||
|
validate_quest_58_dat(&dat);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_compressed_quest_118_dat() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q118-vr-gc.dat");
|
||||||
|
let dat = QuestDat::from_compressed_file(&path)?;
|
||||||
|
validate_quest_118_dat(&dat);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn read_uncompressed_quest_118_dat() -> Result<(), QuestError> {
|
||||||
|
let path = Path::new("assets/test/q118-vr-gc.uncompressed.dat");
|
||||||
|
let dat = QuestDat::from_uncompressed_file(&path)?;
|
||||||
|
validate_quest_118_dat(&dat);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
118
psoutils/src/text.rs
Normal file
118
psoutils/src/text.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use encoding_rs::{Encoding, SHIFT_JIS, WINDOWS_1252};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum LanguageError {
|
||||||
|
#[error("Error(s) encountered encoding text from {0} to {1}")]
|
||||||
|
EncodeError(String, String),
|
||||||
|
|
||||||
|
#[error("Error(s) encountered decoding text from {0} to {1}")]
|
||||||
|
DecodeError(String, String),
|
||||||
|
|
||||||
|
#[error("The number {0} does not correspond to any supported language")]
|
||||||
|
InvalidLanguageValue(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Language {
|
||||||
|
English = 1,
|
||||||
|
French = 3,
|
||||||
|
German = 2,
|
||||||
|
Japanese = 0,
|
||||||
|
Spanish = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Language {
|
||||||
|
pub fn from_number(number: u8) -> Result<Language, LanguageError> {
|
||||||
|
use Language::*;
|
||||||
|
let language = match number {
|
||||||
|
0 => Japanese,
|
||||||
|
1 => English,
|
||||||
|
2 => German,
|
||||||
|
3 => French,
|
||||||
|
4 => Spanish,
|
||||||
|
n => return Err(LanguageError::InvalidLanguageValue(n)),
|
||||||
|
};
|
||||||
|
Ok(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_encoding(&self) -> &'static Encoding {
|
||||||
|
use Language::*;
|
||||||
|
match self {
|
||||||
|
// we should technically be using ISO-8859-1, but encoding_rs does not have it ???
|
||||||
|
// this is probably close enough at any rate ...
|
||||||
|
English | French | German | Spanish => WINDOWS_1252,
|
||||||
|
Japanese => SHIFT_JIS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_text(&self, bytes: &[u8]) -> Result<String, LanguageError> {
|
||||||
|
let encoding = self.get_encoding();
|
||||||
|
let (cow, encoding_used, had_errors) = encoding.decode(bytes);
|
||||||
|
if had_errors {
|
||||||
|
Err(LanguageError::DecodeError(
|
||||||
|
encoding.name().to_string(),
|
||||||
|
encoding_used.name().to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(cow.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_text(&self, s: &str) -> Result<Vec<u8>, LanguageError> {
|
||||||
|
let encoding = self.get_encoding();
|
||||||
|
let (cow, encoding_used, had_errors) = encoding.encode(s);
|
||||||
|
if had_errors {
|
||||||
|
Err(LanguageError::EncodeError(
|
||||||
|
encoding.name().to_string(),
|
||||||
|
encoding_used.name().to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(cow.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use claim::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn language_encode_decode() {
|
||||||
|
assert_eq!(
|
||||||
|
"The East Tower",
|
||||||
|
Language::English
|
||||||
|
.decode_text(&[
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65,
|
||||||
|
0x72
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72
|
||||||
|
],
|
||||||
|
Language::English.encode_text("The East Tower").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"東天の塔",
|
||||||
|
Language::Japanese
|
||||||
|
.decode_text(&[0x93, 0x8c, 0x93, 0x56, 0x82, 0xcc, 0x93, 0x83])
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vec![0x93, 0x8c, 0x93, 0x56, 0x82, 0xcc, 0x93, 0x83],
|
||||||
|
Language::Japanese.encode_text("東天の塔").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
Language::English.encode_text("東天の塔"),
|
||||||
|
Err(LanguageError::EncodeError(_, _))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue