From e751ffa765ccd316112b3cb6ccdbb7a80fab8ccf Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 9 May 2025 15:16:23 -0400 Subject: [PATCH] Update dependencies, remove bitrotten code, update docs I updated our dependencies like binrw to 0.15, which is pretty nice as that means we no longer depend on Syn 1.x. I also finally upgraded to bitflags 2.x, which doesn't really mean anything except we're on better supported version. Additionally, I removed some bitrotten code that no longer compiles. This was mostly benchmark stuff, but since I don't actively keep track of that I felt it was better to remove it. I can always add it back once I'm ready to tackle that again. --- CONTRIBUTING.md | 55 +++++----- Cargo.toml | 35 +------ README.md | 16 ++- benches/benchmark.rs | 11 -- benches/retail_benchmark.rs | 32 ------ src/bootdata.rs | 3 +- src/crc.rs | 24 +---- src/gamedata.rs | 9 +- src/havok/object.rs | 51 ++++----- src/patch.rs | 13 +-- src/tex.rs | 7 +- tests/patch_test.rs | 201 ------------------------------------ 12 files changed, 81 insertions(+), 376 deletions(-) delete mode 100755 benches/benchmark.rs delete mode 100644 benches/retail_benchmark.rs delete mode 100644 tests/patch_test.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index faaa9ff..fb57dd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,50 +3,45 @@ If you're interested to read how these formats work in more detail, see [xiv.dev](https://xiv.dev/) and [docs.xiv.zone](https://docs.xiv.zone). +## Pull Requests + +PRs are welcome, from adding new formats to fixing bugs. Please ensure you: + +* Run the test suite frequently with `cargo test`. +* Check lints using `cargo clippy`. +* Format your code before committing with `cargo fmt`. + ## Testing -One of the main goals of Physis is to avoid accidental regressions, this is especially important when handling game -data that might take hours to download. +We have several tests to ensure Physis is able to read and write the game's various formats. ### Unit Testing -There are a set of basic unit tests you can run via `cargo test`. You can also find the relevant test resources in `resources/tests`. -This does **NOT** contain copyrighted material, but actually fake game data created by Physis itself. These tests are -run automatically by the CI. +Our standalone tests are run with `cargo test`. You can find the relevant resources that it reads under `resources/tests`. Test data should be creatable with Physis, and not simply copied from the retail game. These tests are run automatically by the CI, and you are expected to keep them working. (Unless the test itself is wrong, of course!) + +When adding new functionality, I highly encourage adding new test cases but this is not a hard requirement. ### Retail Testing -There are some tests and benchmarks require the environment variable `FFXIV_GAME_DIR` to be set. By default, these are disabled -since they require a legitimate copy of the retail game data. These tests can be turned on via the `retail_testing` -feature. - -I have a testing platform that tests Physis against multiple game versions. Currently it has to be manually run, and it's lacking a results web page. +I have a testing platform that tests Physis against multiple game versions. Currently it has to be manually run, and it's lacking a results web page. I don't expect you to keep this working until that's available. ### Patch Testing -Patching is an extremely sensitive operation since it is not easily reversible if done wrong. Repairing the game files -is an option, but it's time-consuming and not yet implemented in Physis. To prevent regressions in patching the -game, I have set up a testing bed for cross-checking our implementation with others. Currently, this is limited to XIVLauncher's implementation, -but I will eventually adopt a way to test the retail patch installer as well. +The current patch testing code has bit-rotten, and was removed. It will be replaced with a better version eventually. -1. Enable the `patch_testing` feature. -2. Set a couple of environment variables: - * `FFXIV_PATCH_DIR` is the directory of patches to install. It should be structured as `$FFXIV_PATCH_DIR/game/D2017.07.11.0000.0001.patch`. - * `FFXIV_XIV_LAUNCHER_PATCHER` should be the path to the XIVLauncher patcher executable. If you're running on Linux, we will handle running Wine for you. - * `FFXIV_INSTALLER` is the path to the installer executable. This will be installed using the usual InstallShield emulation physis already includes. +## Dependencies -As you can see, you must have the previous patches downloaded first as well as the installer before running the tests. -This is left up to the developer to figure out how to download them legally. +Please keep our dependencies to the bare minimum. If you include a new dependency then there should be a clear benefit in doing so. For example, if there is a new format that's uses gzip - we do _not_ want to implement our own decompression algorithm. But if there's a new variant of CRC - which might only take 100 lines of Rust code - please just copy it into our source tree. -**Note:** These tests create the `game_test` and `game_test_xivlauncher` folders in `$HOME` and does not -delete them on exit, in case you want to check on the results. You may want to remove these folders as they -are full game installations and take up a considerable amount of space. +If you absolutely must include a new dependency, it's own dependencies should be up-to-date. Don't make Physis (and by extension, any library consumers) compile _both_ Syn 1 & 2 for example. -### Semver and Dependency Checks +We want to keep dependencies to a minimum because: +* Consumers of our library usually have their own set of large dependencies, and we don't want to make their compile times worse. +* Every new crate adds another failure point in terms of bugs. +* It's an additional 3rd party we have to trust. -Even though package management in Rust is easier, it's a double-edged sword. I try to prevent getting carried away -from including crates - but the ones we do include, have to get checked often. I use `cargo deny` to check my -dependencies for mismatched versions, deprecation warnings, updates and more. This is also run on the CI! +## Semantic Versioning and Dependency Checks -Making sure that the library is semver-compliant is also important, and I use `cargo semver` for this task. This is to ensure the API does not break when moving between patch -versions. +Physis uses `cargo deny` to check my dependencies for mismatched versions, deprecation warnings, updates and more. This is also run on the CI, so you will know if you made it unhappy. + +Making sure that the library is semver-compliant is important, and we use `cargo semver` for this. This is to ensure the API does not break when releasing new minor versions. diff --git a/Cargo.toml b/Cargo.toml index cdb8f2a..28b6b4b 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,59 +5,30 @@ authors = ["Joshua Goins "] edition = "2024" description = "Library for reading and writing FFXIV data." license = "GPL-3.0" -homepage = "https://xiv.zone/physis" repository = "https://github.com/redstrate/Physis" keywords = ["ffxiv", "modding"] documentation = "https://docs.xiv.zone/docs/physis/" readme = "README.md" -[profile.release] -lto = true - -[[bench]] -name = "benchmark" -harness = false - -[[bench]] -name = "retail_benchmark" -harness = false -required-features = ["retail_testing"] - [[test]] name = "retail_test" required-features = ["retail_testing"] -[[test]] -name = "patch_test" -required-features = ["patch_testing"] - -[dev-dependencies] -# used while rust doesn't have native benchmarking capability -brunch = { version = "0.9", default-features = false } - -# used for testing our crc implementations -crc = "3" - [features] default = [] # testing only features retail_testing = [] -patch_testing = [] [dependencies] # amazing binary parsing/writing library -binrw = { version = "0.14", features = ["std"], default-features = false } - -# for logging -tracing = { version = "0.1", features = ["std"], default-features = false } +binrw = { version = "0.15", features = ["std"], default-features = false } # used for zlib compression in sqpack files libz-rs-sys = { version = "0.5", features = ["std", "rust-allocator"], default-features = false } # needed for half-float support which FFXIV uses in its model data -half = { version = "2", features = ["std"], default-features = false } +half = { version = "2.6", features = ["std"], default-features = false } # needed for c-style bitflags used in some formats (such as tex files) -# cannot upgrade to 2.0.0, breaking changes that aren't recoverable: https://github.com/bitflags/bitflags/issues/314 -bitflags = { version = "1.3", default-features = false } +bitflags = { version = "2.9", default-features = false } diff --git a/README.md b/README.md index 51b197a..ae345a1 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Crates.io](https://img.shields.io/crates/v/physis)](https://crates.io/crates/physis) [![Docs Badge](https://img.shields.io/badge/docs-latest-blue)](https://docs.xiv.zone/docs/physis) -Physis is a library for reading and writing FFXIV data. It doesn't only know how to read many formats, but it can write some of them too. +Physis is a library for reading and writing FFXIV data. It knows how to read many of the game's formats, and can write some of them too. ## Supported Game Versions @@ -14,6 +14,10 @@ Physis compiles and runs on all major platforms including Windows, macOS, Linux ## Usage +Physis exposes it's API in a few different languages: + +### Rust + If you want to use Physis in your Rust project, you can simply add it as a dependency in `Cargo.toml`: ```toml @@ -24,9 +28,13 @@ physis = "0.4" Documentation is available online at [docs.xiv.zone](https://docs.xiv.zone/docs/physis). It's automatically updated as new commits are pushed to the main branch. -C# projects can use [PhysisSharp](https://github.com/redstrate/PhysisSharp) which exposes Physis in C#. +### C/C++ -C/C++ projects (or anything that can interface with C libraries) can use [libphysis](https://github.com/redstrate/libphysis). +C/C++ projects (or any language that can interface with C) can use [libphysis](https://github.com/redstrate/libphysis). + +### C# + +C# projects can use [PhysisSharp](https://github.com/redstrate/PhysisSharp) which exposes part of the Physis API to C#. ## Building @@ -36,7 +44,7 @@ You need to set up [Rust](https://www.rust-lang.org/learn/get-started) and then Feel free to submit patches to help fix bugs or add functionality. Filing issues is appreciated, but I do this in my free time so please don't expect professional support. -See [CONTRIBUTING](CONTRIBUTING.md) for more information about the project. +See [CONTRIBUTING](CONTRIBUTING.md) for more information about contributing back to the project! ## Credits & Thank You diff --git a/benches/benchmark.rs b/benches/benchmark.rs deleted file mode 100755 index f6eafd6..0000000 --- a/benches/benchmark.rs +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Joshua Goins -// SPDX-License-Identifier: GPL-3.0-or-later - -use brunch::Bench; -use physis::index::IndexFile; - -fn bench_calculate_hash() { - IndexFile::calculate_hash("exd/root.exl"); -} - -brunch::benches!(Bench::new("hash c alc").run(bench_calculate_hash),); diff --git a/benches/retail_benchmark.rs b/benches/retail_benchmark.rs deleted file mode 100644 index fbfffe8..0000000 --- a/benches/retail_benchmark.rs +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Joshua Goins -// SPDX-License-Identifier: GPL-3.0-or-later - -use std::env; - -use brunch::Bench; -use physis::common::Platform; - -fn reload_repos() { - let game_dir = env::var("FFXIV_GAME_DIR").unwrap(); - physis::gamedata::GameData::from_existing( - Platform::Win32, - format!("{}/game", game_dir).as_str(), - ) - .unwrap(); -} - -fn fetch_data() { - let game_dir = env::var("FFXIV_GAME_DIR").unwrap(); - let mut gamedata = physis::gamedata::GameData::from_existing( - Platform::Win32, - format!("{}/game", game_dir).as_str(), - ) - .unwrap(); - - gamedata.extract("exd/root.exl"); -} - -brunch::benches!( - Bench::new("gamedata reloading repositories").run(reload_repos), - Bench::new("gamedata extract").run(fetch_data), -); diff --git a/src/bootdata.rs b/src/bootdata.rs index 89c0a49..b16bec2 100755 --- a/src/bootdata.rs +++ b/src/bootdata.rs @@ -3,7 +3,6 @@ use std::fs; use std::path::PathBuf; -use tracing::warn; use crate::patch::{PatchError, ZiPatch}; @@ -36,7 +35,7 @@ impl BootData { .unwrap(), }, false => { - warn!("Boot data is not valid! Returning one anyway, but without a version."); + // Boot data is not valid! Returning one anyway, but without a version. BootData { path: directory.parse().ok().unwrap(), version: String::default(), diff --git a/src/crc.rs b/src/crc.rs index 874b347..f87193a 100644 --- a/src/crc.rs +++ b/src/crc.rs @@ -120,37 +120,19 @@ impl BitXorAssign for XivCrc32 { #[cfg(test)] mod tests { use super::*; - use crc::{Algorithm, Crc}; #[test] fn check_jamcrc() { - use crc::{CRC_32_JAMCRC, Crc}; - - const JAMCR: Crc = Crc::::new(&CRC_32_JAMCRC); - + const CRC: Jamcrc = Jamcrc::new(); let bytes: [u8; 9] = [1, 1, 2, 4, 5, 6, 12, 12, 12]; - const CRC: Jamcrc = Jamcrc::new(); - - assert_eq!(JAMCR.checksum(&bytes), CRC.checksum(&bytes)) + assert_eq!(2411431516, CRC.checksum(&bytes)) } #[test] fn check_xivcrc() { - const CRC_32_TEST: Algorithm = Algorithm { - width: 32, - poly: 0x04c11db7, - init: 0x00000000, - refin: true, - refout: true, - xorout: 0x00000000, - check: 0x765e7680, - residue: 0xc704dd7b, - }; - const JAMCR: Crc = Crc::::new(&CRC_32_TEST); - let str = "Default"; - assert_eq!(XivCrc32::from(str).crc, JAMCR.checksum(str.as_bytes())); + assert_eq!(XivCrc32::from(str).crc, 2978997821); } } diff --git a/src/gamedata.rs b/src/gamedata.rs index 0d12daf..7d989ea 100755 --- a/src/gamedata.rs +++ b/src/gamedata.rs @@ -6,8 +6,6 @@ use std::fs; use std::fs::{DirEntry, ReadDir}; use std::path::PathBuf; -use tracing::{debug, warn}; - use crate::ByteBuffer; use crate::common::{Language, Platform, read_version}; use crate::exd::EXD; @@ -32,7 +30,6 @@ fn is_valid(path: &str) -> bool { let d = PathBuf::from(path); if fs::metadata(d.as_path()).is_err() { - warn!("Game directory not found."); return false; } @@ -69,8 +66,6 @@ impl GameData { /// GameData::from_existing(Platform::Win32, "$FFXIV/game"); /// ``` pub fn from_existing(platform: Platform, directory: &str) -> GameData { - debug!(directory, "Loading game directory"); - match is_valid(directory) { true => { let mut data = Self { @@ -82,7 +77,7 @@ impl GameData { data } false => { - warn!("Game data is not valid! Treating it as a new install..."); + // Game data is not valid! Treating it as a new install... Self { game_directory: String::from(directory), repositories: vec![], @@ -181,8 +176,6 @@ impl GameData { /// file.write(data.as_slice()).unwrap(); /// ``` pub fn extract(&mut self, path: &str) -> Option { - debug!(file = path, "Extracting file"); - let slice = self.find_entry(path); match slice { Some((entry, chunk)) => { diff --git a/src/havok/object.rs b/src/havok/object.rs index 892e138..7aeb124 100644 --- a/src/havok/object.rs +++ b/src/havok/object.rs @@ -10,8 +10,11 @@ use std::sync::Arc; use bitflags::bitflags; +#[derive(PartialEq, Eq, Clone, Copy)] +pub struct HavokValueType(u32); + bitflags! { - pub struct HavokValueType: u32 { + impl HavokValueType: u32 { const EMPTY = 0; const BYTE = 1; const INT = 2; @@ -25,42 +28,42 @@ bitflags! { const STRING = 10; const ARRAY = 0x10; - const ARRAYBYTE = Self::ARRAY.bits | Self::BYTE.bits; - const ARRAYINT = Self::ARRAY.bits | Self::INT.bits; - const ARRAYREAL = Self::ARRAY.bits | Self::REAL.bits; - const ARRAYVEC4 = Self::ARRAY.bits | Self::VEC4.bits; - const ARRAYVEC8 = Self::ARRAY.bits | Self::VEC8.bits; - const ARRAYVEC12 = Self::ARRAY.bits | Self::VEC12.bits; - const ARRAYVEC16 = Self::ARRAY.bits | Self::VEC16.bits; - const ARRAYOBJECT = Self::ARRAY.bits | Self::OBJECT.bits; - const ARRAYSTRUCT = Self::ARRAY.bits | Self::STRUCT.bits; - const ARRAYSTRING = Self::ARRAY.bits | Self::STRING.bits; + const ARRAYBYTE = Self::ARRAY.bits() | Self::BYTE.bits(); + const ARRAYINT = Self::ARRAY.bits() | Self::INT.bits(); + const ARRAYREAL = Self::ARRAY.bits() | Self::REAL.bits(); + const ARRAYVEC4 = Self::ARRAY.bits() | Self::VEC4.bits(); + const ARRAYVEC8 = Self::ARRAY.bits() | Self::VEC8.bits(); + const ARRAYVEC12 = Self::ARRAY.bits() | Self::VEC12.bits(); + const ARRAYVEC16 = Self::ARRAY.bits() | Self::VEC16.bits(); + const ARRAYOBJECT = Self::ARRAY.bits() | Self::OBJECT.bits(); + const ARRAYSTRUCT = Self::ARRAY.bits() | Self::STRUCT.bits(); + const ARRAYSTRING = Self::ARRAY.bits() | Self::STRING.bits(); const TUPLE = 0x20; - const TUPLEBYTE = Self::TUPLE.bits | Self::BYTE.bits; - const TUPLEINT = Self::TUPLE.bits | Self::INT.bits; - const TUPLEREAL = Self::TUPLE.bits | Self::REAL.bits; - const TUPLEVEC4 = Self::TUPLE.bits | Self::VEC4.bits; - const TUPLEVEC8 = Self::TUPLE.bits | Self::VEC8.bits; - const TUPLEVEC12 = Self::TUPLE.bits | Self::VEC12.bits; - const TUPLEVEC16 = Self::TUPLE.bits | Self::VEC16.bits; - const TUPLEOBJECT = Self::TUPLE.bits | Self::OBJECT.bits; - const TUPLESTRUCT = Self::TUPLE.bits | Self::STRUCT.bits; - const TUPLESTRING = Self::TUPLE.bits | Self::STRING.bits; + const TUPLEBYTE = Self::TUPLE.bits() | Self::BYTE.bits(); + const TUPLEINT = Self::TUPLE.bits() | Self::INT.bits(); + const TUPLEREAL = Self::TUPLE.bits() | Self::REAL.bits(); + const TUPLEVEC4 = Self::TUPLE.bits() | Self::VEC4.bits(); + const TUPLEVEC8 = Self::TUPLE.bits() | Self::VEC8.bits(); + const TUPLEVEC12 = Self::TUPLE.bits() | Self::VEC12.bits(); + const TUPLEVEC16 = Self::TUPLE.bits() | Self::VEC16.bits(); + const TUPLEOBJECT = Self::TUPLE.bits() | Self::OBJECT.bits(); + const TUPLESTRUCT = Self::TUPLE.bits() | Self::STRUCT.bits(); + const TUPLESTRING = Self::TUPLE.bits() | Self::STRING.bits(); } } impl HavokValueType { pub fn is_tuple(self) -> bool { - (self.bits & HavokValueType::TUPLE.bits) != 0 + (self.bits() & HavokValueType::TUPLE.bits()) != 0 } pub fn is_array(self) -> bool { - (self.bits & HavokValueType::ARRAY.bits) != 0 + (self.bits() & HavokValueType::ARRAY.bits()) != 0 } pub fn base_type(self) -> HavokValueType { - HavokValueType::from_bits(self.bits & 0x0f).unwrap() + HavokValueType::from_bits(self.bits() & 0x0f).unwrap() } pub fn is_vec(self) -> bool { diff --git a/src/patch.rs b/src/patch.rs index d5da3f2..904f99c 100755 --- a/src/patch.rs +++ b/src/patch.rs @@ -10,7 +10,6 @@ use std::path::{Path, PathBuf}; use crate::ByteBuffer; use binrw::BinRead; use binrw::{BinWrite, binrw}; -use tracing::{debug, warn}; use crate::common::{Platform, Region, get_platform_string}; use crate::common_file_operations::{ @@ -634,12 +633,12 @@ impl ZiPatch { file.seek(SeekFrom::Start(fop.offset))?; file.write_all(&data)?; } else { - warn!("{file_path} does not exist, skipping."); + // silently skip if it does not exist } } SqpkFileOperation::DeleteFile => { if fs::remove_file(file_path.as_str()).is_err() { - warn!("Failed to remove {file_path}"); + // TODO: return an error if we failed to remove the file } } SqpkFileOperation::RemoveAll => { @@ -662,30 +661,26 @@ impl ZiPatch { } SqpkOperation::PatchInfo(_) => { // Currently, there's nothing we need from PatchInfo. Intentional NOP. - debug!("PATCH: NOP PatchInfo"); } SqpkOperation::TargetInfo(new_target_info) => { target_info = Some(new_target_info); } SqpkOperation::Index(_) => { // Currently, there's nothing we need from Index command. Intentional NOP. - debug!("PATCH: NOP Index"); } } } ChunkType::FileHeader(_) => { // Currently there's nothing very useful in the FileHeader, so it's an intentional NOP. - debug!("PATCH: NOP FileHeader"); } ChunkType::ApplyOption(_) => { // Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP. - debug!("PATCH: NOP ApplyOption"); } ChunkType::AddDirectory(_) => { - debug!("PATCH: NOP AddDirectory"); + // another NOP } ChunkType::DeleteDirectory(_) => { - debug!("PATCH: NOP DeleteDirectory"); + // another NOP } ChunkType::EndOfFile => { return Ok(()); diff --git a/src/tex.rs b/src/tex.rs index 8c1109b..546171d 100644 --- a/src/tex.rs +++ b/src/tex.rs @@ -13,10 +13,13 @@ use binrw::BinRead; use binrw::binrw; use bitflags::bitflags; +#[binrw] +#[derive(Debug)] +struct TextureAttribute(u32); + // Attributes and Format are adapted from Lumina (https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/TexFile.cs) bitflags! { - #[binrw] - struct TextureAttribute : u32 { + impl TextureAttribute : u32 { const DISCARD_PER_FRAME = 0x1; const DISCARD_PER_MAP = 0x2; diff --git a/tests/patch_test.rs b/tests/patch_test.rs deleted file mode 100644 index 973f5e0..0000000 --- a/tests/patch_test.rs +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Joshua Goins -// SPDX-License-Identifier: GPL-3.0-or-later - -use std::fs::read_dir; - -use std::path::{Path, PathBuf}; - -fn make_temp_install_dir(name: &str) -> String { - use physis::installer::install_game; - - let installer_exe = env::var("FFXIV_INSTALLER") - .expect("$FFXIV_INSTALLER needs to point to the retail installer"); - - let mut game_dir = env::home_dir().unwrap(); - game_dir.push(name); - - if std::fs::read_dir(&game_dir).ok().is_some() { - std::fs::remove_dir_all(&game_dir).unwrap(); - } - - std::fs::create_dir_all(&game_dir).unwrap(); - - install_game(&installer_exe, game_dir.as_path().to_str().unwrap()) - .ok() - .unwrap(); - - game_dir.as_path().to_str().unwrap().parse().unwrap() -} - -// Shamelessly taken from https://stackoverflow.com/a/76820878 -fn recurse(path: impl AsRef) -> Vec { - let Ok(entries) = read_dir(path) else { - return vec![]; - }; - entries - .flatten() - .flat_map(|entry| { - let Ok(meta) = entry.metadata() else { - return vec![]; - }; - if meta.is_dir() { - return recurse(entry.path()); - } - if meta.is_file() { - return vec![entry.path()]; - } - vec![] - }) - .collect() -} - -fn fill_dir_hash(game_dir: &str) -> HashMap { - let mut file_hashes: HashMap = HashMap::new(); - - recurse(game_dir).into_iter().for_each(|x| { - let path = x.as_path(); - let file = std::fs::read(path).unwrap(); - - let mut hash = Hash::new(); - hash.update(&file); - let sha = hash.finalize(); - - let mut rel_path = path; - rel_path = rel_path.strip_prefix(game_dir).unwrap(); - - file_hashes.insert(rel_path.to_str().unwrap().to_string(), sha); - }); - - file_hashes -} - -fn physis_install_patch(game_directory: &str, data_directory: &str, patch_name: &str) { - let patch_dir = env::var("FFXIV_PATCH_DIR").unwrap(); - - let patch_path = format!("{}/{}", patch_dir, &patch_name); - let data_dir = format!("{}/{}", game_directory, data_directory); - - ZiPatch::apply(&data_dir, &patch_path).unwrap(); -} - -fn xivlauncher_install_patch(game_directory: &str, data_directory: &str, patch_name: &str) { - let patch_dir = env::var("FFXIV_PATCH_DIR").unwrap(); - let patcher_exe = env::var("FFXIV_XIV_LAUNCHER_PATCHER") - .expect("$FFXIV_XIV_LAUNCHER_PATCHER must point to XIVLauncher.PatchInstaller.exe"); - - let patch_path = format!("Z:\\{}\\{}", patch_dir, &patch_name); - let game_dir = format!("Z:\\{}\\{}", game_directory, data_directory); - - // TODO: check for windows systems - let output = Command::new("wine") - .args([&patcher_exe, "install", &patch_path, &game_dir]) - .output() - .unwrap(); - - // If there is some kind of catostrophic failure, make sure it's printed. - // For example, missing .NET in your wine prefix - if (!output.status.success()) { - std::io::stdout().write_all(&output.stdout).unwrap(); - std::io::stderr().write_all(&output.stderr).unwrap(); - } - - assert!(output.status.success()); -} - -fn check_if_files_match(xivlauncher_dir: &str, physis_dir: &str) { - let xivlauncher_files = fill_dir_hash(xivlauncher_dir); - let physis_files = fill_dir_hash(physis_dir); - - for file in xivlauncher_files.keys() { - if xivlauncher_files[file] != physis_files[file] { - println!("!! {} does not match!", file); - } - } - - assert_eq!(physis_files, xivlauncher_files); -} - -#[test] -fn test_patching() { - println!("Beginning game installation..."); - - let physis_dir = make_temp_install_dir("game_install_physis"); - let xivlauncher_dir = make_temp_install_dir("game_install_xivquicklauncher"); - - println!("Done with game installation! Now checking if the checksums match first..."); - - check_if_files_match(&xivlauncher_dir, &physis_dir); - - println!("* Directories match."); - - let boot_patches = [ - "boot/2023.04.28.0000.0001.patch", - "boot/2023.04.28.0000.0001.patch", - "boot/2024.03.07.0000.0001.patch", - "boot/2024.03.21.0000.0001.patch", - "boot/2024.04.09.0000.0001.patch", - "boot/2024.05.24.0000.0001.patch", - ]; - - println!("Now beginning boot patching..."); - - for patch in boot_patches { - let patch_dir = env::var("FFXIV_PATCH_DIR") - .expect("$FFXIV_PATCH_DIR must point to the directory where the patches are stored"); - if !Path::new(&(patch_dir + "/" + patch)).exists() { - println!("Skipping {} because it doesn't exist locally.", patch); - continue; - } - - println!("Installing {}...", patch); - - xivlauncher_install_patch(&xivlauncher_dir, "boot", patch); - physis_install_patch(&physis_dir, "boot", patch); - - check_if_files_match(&xivlauncher_dir, &physis_dir); - } - - let game_patches = [ - "game/H2017.06.06.0000.0001a.patch", - "game/H2017.06.06.0000.0001b.patch", - "game/H2017.06.06.0000.0001c.patch", - "game/H2017.06.06.0000.0001d.patch", - "game/H2017.06.06.0000.0001e.patch", - "game/H2017.06.06.0000.0001f.patch", - "game/H2017.06.06.0000.0001g.patch", - "game/H2017.06.06.0000.0001h.patch", - "game/H2017.06.06.0000.0001i.patch", - "game/H2017.06.06.0000.0001j.patch", - "game/H2017.06.06.0000.0001k.patch", - "game/H2017.06.06.0000.0001l.patch", - "game/H2017.06.06.0000.0001m.patch", - "game/H2017.06.06.0000.0001n.patch", - "game/D2017.07.11.0000.0001.patch", - "game/D2017.09.24.0000.0001.patch", - "ex1/H2017.06.01.0000.0001a.patch", - "ex1/H2017.06.01.0000.0001b.patch", - "ex1/H2017.06.01.0000.0001c.patch", - "ex1/H2017.06.01.0000.0001d.patch", - ]; - - println!("Boot patching is now complete. Now running game patching..."); - - for patch in game_patches { - let patch_dir = env::var("FFXIV_PATCH_DIR").unwrap(); - if !Path::new(&(patch_dir + "/" + patch)).exists() { - println!("Skipping {} because it doesn't exist locally.", patch); - continue; - } - - println!("Installing {}...", patch); - - xivlauncher_install_patch(&xivlauncher_dir, "game", patch); - physis_install_patch(&physis_dir, "game", patch); - - check_if_files_match(&xivlauncher_dir, &physis_dir); - } - - println!("Game patching is now complete. Proceeding to checksum matching..."); - - check_if_files_match(&xivlauncher_dir, &physis_dir); -}