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 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