initial quest file bin/dat support

includes loading as compressed/uncompressed. no saving support yet.
This commit is contained in:
Gered 2021-05-15 14:48:33 -04:00
parent c71f9997bd
commit 6c672f3d27
14 changed files with 1146 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

110
psoutils/src/bytes.rs Normal file
View 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));
}
}

View file

@ -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
View 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
View 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
View 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
View 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(_, _))
);
}
}