2022-07-21 19:58:58 -04:00
|
|
|
use crate::common::Language;
|
2022-07-19 19:29:41 -04:00
|
|
|
use crate::dat::DatFile;
|
2022-07-21 19:58:58 -04:00
|
|
|
use crate::exd::EXD;
|
|
|
|
use crate::exh::EXH;
|
|
|
|
use crate::exl::EXL;
|
2022-07-19 19:29:41 -04:00
|
|
|
use crate::index::IndexFile;
|
2022-08-09 21:51:52 -04:00
|
|
|
use crate::patch::{apply_patch, PatchError};
|
2022-08-16 11:52:07 -04:00
|
|
|
use crate::repository::{string_to_category, Category, Repository};
|
2022-07-19 19:29:41 -04:00
|
|
|
use crate::sqpack::calculate_hash;
|
2022-08-16 11:52:07 -04:00
|
|
|
use std::fs;
|
|
|
|
use std::fs::DirEntry;
|
|
|
|
use std::path::PathBuf;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
/// Framework for operating on game data.
|
|
|
|
pub struct GameData {
|
|
|
|
/// The game directory to operate on.
|
|
|
|
pub game_directory: String,
|
|
|
|
|
|
|
|
/// Repositories in the game directory.
|
|
|
|
pub repositories: Vec<Repository>,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_valid(path: &str) -> bool {
|
2022-07-27 21:21:50 -04:00
|
|
|
let d = PathBuf::from(path);
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
if fs::metadata(d.as_path()).is_err() {
|
|
|
|
println!("Failed game directory.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type MemoryBuffer = Vec<u8>;
|
|
|
|
|
|
|
|
impl GameData {
|
|
|
|
/// Read game data from an existing game installation.
|
|
|
|
///
|
|
|
|
/// This will return _None_ if the game directory is not valid, but it does not check the validity
|
|
|
|
/// of each individual file.
|
|
|
|
///
|
|
|
|
/// **Note**: None of the repositories are searched, and it's required to call `reload_repositories()`.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// # use physis::gamedata::GameData;
|
|
|
|
/// GameData::from_existing("$FFXIV/game");
|
|
|
|
/// ```
|
|
|
|
pub fn from_existing(directory: &str) -> Option<GameData> {
|
|
|
|
match is_valid(directory) {
|
|
|
|
true => Some(Self {
|
|
|
|
game_directory: String::from(directory),
|
|
|
|
repositories: vec![],
|
|
|
|
}),
|
|
|
|
false => {
|
|
|
|
println!("Game data is not valid!");
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reloads all repository information from disk. This is a fast operation, as it's not actually
|
|
|
|
/// reading any dat files yet.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```should_panic
|
|
|
|
/// # use physis::gamedata::GameData;
|
|
|
|
/// let mut game = GameData::from_existing("$FFXIV/game").unwrap();
|
|
|
|
/// game.reload_repositories();
|
|
|
|
/// ```
|
|
|
|
pub fn reload_repositories(&mut self) {
|
|
|
|
self.repositories.clear();
|
|
|
|
|
|
|
|
let mut d = PathBuf::from(self.game_directory.as_str());
|
|
|
|
d.push("sqpack");
|
|
|
|
|
|
|
|
let repository_paths: Vec<DirEntry> = fs::read_dir(d.as_path())
|
|
|
|
.unwrap()
|
|
|
|
.filter_map(Result::ok)
|
|
|
|
.filter(|s| s.file_type().unwrap().is_dir())
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
for repository_path in repository_paths {
|
2022-08-16 11:52:07 -04:00
|
|
|
self.repositories
|
|
|
|
.push(Repository::from_existing(repository_path.path().to_str().unwrap()).unwrap());
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
self.repositories.sort();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_index_file(&self, path: &str) -> Option<IndexFile> {
|
|
|
|
let (repository, category) = self.parse_repository_category(path).unwrap();
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let index_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
2022-08-09 20:00:02 -04:00
|
|
|
"sqpack".to_string(),
|
|
|
|
repository.name.clone(),
|
2022-08-16 11:52:07 -04:00
|
|
|
repository.index_filename(category),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-09 20:00:02 -04:00
|
|
|
IndexFile::from_existing(index_path.to_str()?)
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
fn get_dat_file(&self, path: &str, data_file_id: u32) -> Option<DatFile> {
|
2022-08-09 20:03:18 -04:00
|
|
|
let (repository, category) = self.parse_repository_category(path).unwrap();
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let dat_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
2022-08-09 20:03:18 -04:00
|
|
|
"sqpack".to_string(),
|
|
|
|
repository.name.clone(),
|
2022-08-16 11:52:07 -04:00
|
|
|
repository.dat_filename(category, data_file_id),
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-08-09 20:03:18 -04:00
|
|
|
|
|
|
|
DatFile::from_existing(dat_path.to_str()?)
|
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
/// Checks if a file located at `path` exists.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```should_panic
|
|
|
|
/// # use physis::gamedata::GameData;
|
|
|
|
/// # let mut game = GameData::from_existing("SquareEnix/Final Fantasy XIV - A Realm Reborn/game").unwrap();
|
|
|
|
/// if game.exists("exd/cid.exl") {
|
|
|
|
/// println!("Cid really does exist!");
|
|
|
|
/// } else {
|
|
|
|
/// println!("Oh noes!");
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
pub fn exists(&self, path: &str) -> bool {
|
|
|
|
let hash = calculate_hash(path);
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let index_file = self
|
|
|
|
.get_index_file(path)
|
2022-07-19 19:29:41 -04:00
|
|
|
.expect("Failed to find index file.");
|
|
|
|
|
|
|
|
index_file.entries.iter().any(|s| s.hash == hash)
|
|
|
|
}
|
|
|
|
|
2022-08-06 18:05:16 -04:00
|
|
|
/// Extracts the file located at `path`. This is returned as an in-memory buffer, and will usually
|
2022-07-19 19:29:41 -04:00
|
|
|
/// have to be further parsed.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```should_panic
|
|
|
|
/// # use physis::gamedata::GameData;
|
|
|
|
/// # use std::io::Write;
|
|
|
|
/// # let mut game = GameData::from_existing("SquareEnix/Final Fantasy XIV - A Realm Reborn/game").unwrap();
|
|
|
|
/// let data = game.extract("exd/root.exl").unwrap();
|
|
|
|
///
|
|
|
|
/// let mut file = std::fs::File::create("root.exl").unwrap();
|
2022-08-09 23:44:11 -04:00
|
|
|
/// file.write(data.as_slice()).unwrap();
|
2022-07-19 19:29:41 -04:00
|
|
|
/// ```
|
|
|
|
pub fn extract(&self, path: &str) -> Option<MemoryBuffer> {
|
|
|
|
let hash = calculate_hash(path);
|
|
|
|
|
2022-08-09 22:43:04 -04:00
|
|
|
let index_file = self.get_index_file(path)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
let slice = index_file.entries.iter().find(|s| s.hash == hash);
|
|
|
|
match slice {
|
|
|
|
Some(entry) => {
|
2022-08-09 20:03:18 -04:00
|
|
|
let mut dat_file = self.get_dat_file(path, entry.bitfield.data_file_id())?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
dat_file.read_from_offset(entry.bitfield.offset())
|
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
None => None,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parses a path structure and spits out the corresponding category and repository.
|
|
|
|
fn parse_repository_category(&self, path: &str) -> Option<(&Repository, Category)> {
|
|
|
|
let tokens: Vec<&str> = path.split('/').collect(); // TODO: use split_once here
|
|
|
|
let repository_token = tokens[0];
|
|
|
|
|
|
|
|
if tokens.len() < 2 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
for repository in &self.repositories {
|
|
|
|
if repository.name == repository_token {
|
|
|
|
return Some((repository, string_to_category(tokens[1])?));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Some((&self.repositories[0], string_to_category(tokens[0])?))
|
|
|
|
}
|
2022-07-21 19:58:58 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
pub fn read_excel_sheet_header(&self, name: &str) -> Option<EXH> {
|
2022-08-09 22:43:04 -04:00
|
|
|
let root_exl_file = self.extract("exd/root.exl")?;
|
2022-07-21 19:58:58 -04:00
|
|
|
|
2022-08-09 22:43:04 -04:00
|
|
|
let root_exl = EXL::from_existing(&root_exl_file)?;
|
2022-07-21 19:58:58 -04:00
|
|
|
|
|
|
|
for (row, _) in root_exl.entries {
|
|
|
|
if row == name {
|
|
|
|
let new_filename = name.to_lowercase();
|
|
|
|
|
|
|
|
let path = format!("exd/{new_filename}.exh");
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
return EXH::from_existing(&self.extract(&path)?);
|
2022-07-21 19:58:58 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
pub fn read_excel_sheet(
|
|
|
|
&self,
|
|
|
|
name: &str,
|
|
|
|
exh: &EXH,
|
|
|
|
language: Language,
|
|
|
|
page: usize,
|
|
|
|
) -> Option<EXD> {
|
|
|
|
let exd_path = format!(
|
|
|
|
"exd/{}",
|
|
|
|
EXD::calculate_filename(name, language, &exh.pages[page])
|
|
|
|
);
|
2022-07-21 19:58:58 -04:00
|
|
|
|
|
|
|
let exd_file = self.extract(&exd_path).unwrap();
|
|
|
|
|
2022-08-16 11:50:18 -04:00
|
|
|
EXD::from_existing(exh, &exd_file)
|
2022-07-21 19:58:58 -04:00
|
|
|
}
|
2022-08-09 21:51:52 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
pub fn apply_patch(&self, patch_path: &str) -> Result<(), PatchError> {
|
2022-08-09 21:51:52 -04:00
|
|
|
apply_patch(&self.game_directory, patch_path)
|
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2022-08-16 11:52:07 -04:00
|
|
|
use crate::repository::Category::EXD;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
fn common_setup_data() -> GameData {
|
|
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
d.push("resources/tests");
|
|
|
|
d.push("valid_sqpack");
|
|
|
|
d.push("game");
|
|
|
|
|
|
|
|
GameData::from_existing(d.to_str().unwrap()).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn repository_ordering() {
|
|
|
|
let mut data = common_setup_data();
|
|
|
|
data.reload_repositories();
|
|
|
|
|
|
|
|
assert_eq!(data.repositories[0].name, "ffxiv");
|
|
|
|
assert_eq!(data.repositories[1].name, "ex1");
|
|
|
|
assert_eq!(data.repositories[2].name, "ex2");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn repository_and_category_parsing() {
|
|
|
|
let mut data = common_setup_data();
|
|
|
|
data.reload_repositories();
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
assert_eq!(
|
|
|
|
data.parse_repository_category("exd/root.exl").unwrap(),
|
|
|
|
(&data.repositories[0], EXD)
|
|
|
|
);
|
|
|
|
assert!(data
|
|
|
|
.parse_repository_category("what/some_font.dat")
|
|
|
|
.is_none());
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|