From 6c672f3d27c9a4b8ff7eea02c6dd2777606ed347 Mon Sep 17 00:00:00 2001 From: gered Date: Sat, 15 May 2021 14:48:33 -0400 Subject: [PATCH] initial quest file bin/dat support includes loading as compressed/uncompressed. no saving support yet. --- psoutils/assets/test/q058-ret-gc.bin | Bin 0 -> 1438 bytes psoutils/assets/test/q058-ret-gc.dat | Bin 0 -> 15097 bytes .../assets/test/q058-ret-gc.uncompressed.bin | Bin 0 -> 6476 bytes .../assets/test/q058-ret-gc.uncompressed.dat | Bin 0 -> 36820 bytes psoutils/assets/test/q118-vr-gc.bin | Bin 0 -> 14208 bytes psoutils/assets/test/q118-vr-gc.dat | Bin 0 -> 11802 bytes .../assets/test/q118-vr-gc.uncompressed.bin | Bin 0 -> 55332 bytes .../assets/test/q118-vr-gc.uncompressed.dat | Bin 0 -> 36924 bytes psoutils/src/bytes.rs | 110 ++++ psoutils/src/lib.rs | 3 + psoutils/src/quest.rs | 50 ++ psoutils/src/quest/bin.rs | 343 ++++++++++++ psoutils/src/quest/dat.rs | 522 ++++++++++++++++++ psoutils/src/text.rs | 118 ++++ 14 files changed, 1146 insertions(+) create mode 100755 psoutils/assets/test/q058-ret-gc.bin create mode 100755 psoutils/assets/test/q058-ret-gc.dat create mode 100644 psoutils/assets/test/q058-ret-gc.uncompressed.bin create mode 100644 psoutils/assets/test/q058-ret-gc.uncompressed.dat create mode 100755 psoutils/assets/test/q118-vr-gc.bin create mode 100755 psoutils/assets/test/q118-vr-gc.dat create mode 100644 psoutils/assets/test/q118-vr-gc.uncompressed.bin create mode 100644 psoutils/assets/test/q118-vr-gc.uncompressed.dat create mode 100644 psoutils/src/bytes.rs create mode 100644 psoutils/src/quest.rs create mode 100644 psoutils/src/quest/bin.rs create mode 100644 psoutils/src/quest/dat.rs create mode 100644 psoutils/src/text.rs diff --git a/psoutils/assets/test/q058-ret-gc.bin b/psoutils/assets/test/q058-ret-gc.bin new file mode 100755 index 0000000000000000000000000000000000000000..7b8515438bf423b96e1f7b65edd130db4f10c08f GIT binary patch literal 1438 zcmXwz2~ZPv7{&h!20*X~(Y(=NiGFI>y5f20lD2N&bsb~@s2+5LccAM7ey!XDD-^@4jUQ{OnKn-Uy zz1olVV%b}^Ff*ZMNi=TdigYyX)%rE5v4CQ^sjQe(;o?Fu%3`ocj425Pi{_{ZImA$I ztO}Eig!7$$nK8165(OEC(iwzOiYwIIxoB>IF-ok0DFtGBRTxB*icw~ny$DmIB*vF^ zKyC&qPrO}}JA%gV8q{%dNiKOKT!PNB31D;3pelR%*(ks^fXlvBGgxOsUFEbgK*!&( zCLRMv+v+Ji&eAQI?4LF+Iq$YDyfNSoQT0L&N5qMg)M52?M}oXaxIjd?&8d( zPKpFgwaOHXDYnf>^w!Yv#!1HnMdmp$wO4A1)@Dg~; zl&0VpZ7*f!Ay=V)KgyNjGTE$Z{sQ45t$nt-xfh~2L;<3hcxUL`>wbBtfy^Cf-CFIHivO`R&t!odKn9!olFyAz}*9LywYaEpkSy{uy5X&ihh89!LRbr9Y z3*TLB9#+liHSXKU^Ub6i9(pC@`>#8cx{sxn#!(D~ETOP%>=($`&C#;ay7$5u3=yz7 zDok;1$sw5%MubP9X!2wcrna&mSs0IEGI_1{V}Hx|xInU*dZabYA*CuHTlOOdmA<;d2)Ss^`yh0lM-^*^)V}gA4oU zrAV{Q^AF6!*yPcV+CsLsS9sjrLFGrrW=^LMis&ZV^rC8f3dL@hy6&(i`C@LD<0h2S zr|Fr5JL>Se)G%OnM`?ZlHX=>)Xzq6QrG$(4+H9IUK$p_>8v2Lo;ku))RpyUN*xLU$ T{mXeDfo5Ch>};4(-2lKpe7@n< literal 0 HcmV?d00001 diff --git a/psoutils/assets/test/q058-ret-gc.dat b/psoutils/assets/test/q058-ret-gc.dat new file mode 100755 index 0000000000000000000000000000000000000000..53775402851e02cafe3806cb8c07f366fd4ced6f GIT binary patch literal 15097 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 a2y_wXBG5&ki$E8F+Yf>BI1k;Av(Vqyn^>m+ literal 0 HcmV?d00001 diff --git a/psoutils/assets/test/q058-ret-gc.uncompressed.dat b/psoutils/assets/test/q058-ret-gc.uncompressed.dat new file mode 100644 index 0000000000000000000000000000000000000000..32eb508df03f01acd6bee6b186e4a13f1418c705 GIT binary patch literal 36820 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 literal 0 HcmV?d00001 diff --git a/psoutils/assets/test/q118-vr-gc.bin b/psoutils/assets/test/q118-vr-gc.bin new file mode 100755 index 0000000000000000000000000000000000000000..1d3aabf3ad710d8373018d21af7041e89e2111c9 GIT binary patch literal 14208 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 literal 0 HcmV?d00001 diff --git a/psoutils/assets/test/q118-vr-gc.uncompressed.bin b/psoutils/assets/test/q118-vr-gc.uncompressed.bin new file mode 100644 index 0000000000000000000000000000000000000000..ff2bec5176791461f5c33f01aa764095b5095ef1 GIT binary patch literal 55332 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 literal 0 HcmV?d00001 diff --git a/psoutils/src/bytes.rs b/psoutils/src/bytes.rs new file mode 100644 index 0000000..8cac339 --- /dev/null +++ b/psoutils/src/bytes.rs @@ -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: Sized { + fn read_from_bytes(reader: &mut T) -> Result; +} + +#[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 { + 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; +} + +impl + ?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 { + 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)); + } +} diff --git a/psoutils/src/lib.rs b/psoutils/src/lib.rs index ea1a455..310b20c 100644 --- a/psoutils/src/lib.rs +++ b/psoutils/src/lib.rs @@ -1,2 +1,5 @@ +pub mod bytes; pub mod compression; pub mod encryption; +pub mod quest; +pub mod text; diff --git a/psoutils/src/quest.rs b/psoutils/src/quest.rs new file mode 100644 index 0000000..7ed26ab --- /dev/null +++ b/psoutils/src/quest.rs @@ -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 { + 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 { + let bin = QuestBin::from_uncompressed_file(bin_path)?; + let dat = QuestDat::from_uncompressed_file(dat_path)?; + + Ok(Quest { bin, dat }) + } +} + +#[cfg(test)] +mod tests {} diff --git a/psoutils/src/quest/bin.rs b/psoutils/src/quest/bin.rs new file mode 100644 index 0000000..f6cd511 --- /dev/null +++ b/psoutils/src/quest/bin.rs @@ -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 { + 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 { + 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 { + 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, WriteBytesError> { + let bytes = self.to_uncompressed_bytes()?; + Ok(prs_compress(bytes.as_ref())) + } + + pub fn to_uncompressed_bytes(&self) -> Result, 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 ReadFromBytes for QuestBin { + fn read_from_bytes(reader: &mut T) -> Result { + let object_code_offset = reader.read_u32::()?; + let function_offset_table_offset = reader.read_u32::()?; + let bin_size = reader.read_u32::()?; + let _xfffffff = reader.read_u32::()?; // always 0xffffffff + let is_download = reader.read_u8()?; + let language = reader.read_u8()?; + let quest_number_and_episode = reader.read_u16::()?; + + 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 WriteAsBytes 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::(object_code_offset as u32)?; + writer.write_u32::(function_offset_table_offset as u32)?; + writer.write_u32::(bin_size as u32)?; + writer.write_u32::(0xfffffff)?; // always 0xffffffff + writer.write_u8(self.header.is_download as u8)?; + writer.write_u8(self.header.language as u8)?; + writer.write_u16::(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(()) + } +} diff --git a/psoutils/src/quest/dat.rs b/psoutils/src/quest/dat.rs new file mode 100644 index 0000000..6d90f16 --- /dev/null +++ b/psoutils/src/quest/dat.rs @@ -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 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 { + 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 { + 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 { + 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, WriteBytesError> { + let bytes = self.to_uncompressed_bytes()?; + Ok(prs_compress(bytes.as_ref())) + } + + pub fn to_uncompressed_bytes(&self) -> Result, 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 ReadFromBytes for QuestDat { + fn read_from_bytes(reader: &mut T) -> Result { + let mut tables = Vec::new(); + loop { + let table_type = reader.read_u32::()?; + let table_size = reader.read_u32::()?; + let area = reader.read_u32::()?; + let table_body_size = reader.read_u32::()?; + + // 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 WriteAsBytes 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::((&table.header.table_type).into())?; + writer.write_u32::(table_size)?; + writer.write_u32::(table.header.area)?; + writer.write_u32::(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::(0)?; // table_type + writer.write_u32::(0)?; // table_size + writer.write_u32::(0)?; // area + writer.write_u32::(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(()) + } +} diff --git a/psoutils/src/text.rs b/psoutils/src/text.rs new file mode 100644 index 0000000..36c738a --- /dev/null +++ b/psoutils/src/text.rs @@ -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 { + 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 { + 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, 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(_, _)) + ); + } +}