1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-05-10 04:17:46 +00:00

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.
This commit is contained in:
Joshua Goins 2025-05-09 15:16:23 -04:00
parent 940edac964
commit e751ffa765
12 changed files with 81 additions and 376 deletions

View file

@ -3,50 +3,45 @@
If you're interested to read how these formats work in more detail, see [xiv.dev](https://xiv.dev/) and 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). [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 ## Testing
One of the main goals of Physis is to avoid accidental regressions, this is especially important when handling game We have several tests to ensure Physis is able to read and write the game's various formats.
data that might take hours to download.
### Unit Testing ### 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`. 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!)
This does **NOT** contain copyrighted material, but actually fake game data created by Physis itself. These tests are
run automatically by the CI. When adding new functionality, I highly encourage adding new test cases but this is not a hard requirement.
### Retail Testing ### Retail Testing
There are some tests and benchmarks require the environment variable `FFXIV_GAME_DIR` to be set. By default, these are disabled 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.
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.
### Patch Testing ### Patch Testing
Patching is an extremely sensitive operation since it is not easily reversible if done wrong. Repairing the game files The current patch testing code has bit-rotten, and was removed. It will be replaced with a better version eventually.
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.
1. Enable the `patch_testing` feature. ## Dependencies
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.
As you can see, you must have the previous patches downloaded first as well as the installer before running the tests. 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.
This is left up to the developer to figure out how to download them legally.
**Note:** These tests create the `game_test` and `game_test_xivlauncher` folders in `$HOME` and does not 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.
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.
### 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 ## Semantic Versioning and Dependency Checks
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!
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 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.
versions.
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.

View file

@ -5,59 +5,30 @@ authors = ["Joshua Goins <josh@redstrate.com>"]
edition = "2024" edition = "2024"
description = "Library for reading and writing FFXIV data." description = "Library for reading and writing FFXIV data."
license = "GPL-3.0" license = "GPL-3.0"
homepage = "https://xiv.zone/physis"
repository = "https://github.com/redstrate/Physis" repository = "https://github.com/redstrate/Physis"
keywords = ["ffxiv", "modding"] keywords = ["ffxiv", "modding"]
documentation = "https://docs.xiv.zone/docs/physis/" documentation = "https://docs.xiv.zone/docs/physis/"
readme = "README.md" readme = "README.md"
[profile.release]
lto = true
[[bench]]
name = "benchmark"
harness = false
[[bench]]
name = "retail_benchmark"
harness = false
required-features = ["retail_testing"]
[[test]] [[test]]
name = "retail_test" name = "retail_test"
required-features = ["retail_testing"] 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] [features]
default = [] default = []
# testing only features # testing only features
retail_testing = [] retail_testing = []
patch_testing = []
[dependencies] [dependencies]
# amazing binary parsing/writing library # amazing binary parsing/writing library
binrw = { version = "0.14", features = ["std"], default-features = false } binrw = { version = "0.15", features = ["std"], default-features = false }
# for logging
tracing = { version = "0.1", features = ["std"], default-features = false }
# used for zlib compression in sqpack files # used for zlib compression in sqpack files
libz-rs-sys = { version = "0.5", features = ["std", "rust-allocator"], default-features = false } 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 # 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) # 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 = "2.9", default-features = false }
bitflags = { version = "1.3", default-features = false }

View file

@ -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) [![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 ## Supported Game Versions
@ -14,6 +14,10 @@ Physis compiles and runs on all major platforms including Windows, macOS, Linux
## Usage ## 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`: If you want to use Physis in your Rust project, you can simply add it as a dependency in `Cargo.toml`:
```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 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. 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 ## 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. 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 ## Credits & Thank You

View file

@ -1,11 +0,0 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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),);

View file

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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),
);

View file

@ -3,7 +3,6 @@
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::warn;
use crate::patch::{PatchError, ZiPatch}; use crate::patch::{PatchError, ZiPatch};
@ -36,7 +35,7 @@ impl BootData {
.unwrap(), .unwrap(),
}, },
false => { 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 { BootData {
path: directory.parse().ok().unwrap(), path: directory.parse().ok().unwrap(),
version: String::default(), version: String::default(),

View file

@ -120,37 +120,19 @@ impl BitXorAssign<XivCrc32> for XivCrc32 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crc::{Algorithm, Crc};
#[test] #[test]
fn check_jamcrc() { fn check_jamcrc() {
use crc::{CRC_32_JAMCRC, Crc}; const CRC: Jamcrc = Jamcrc::new();
const JAMCR: Crc<u32> = Crc::<u32>::new(&CRC_32_JAMCRC);
let bytes: [u8; 9] = [1, 1, 2, 4, 5, 6, 12, 12, 12]; let bytes: [u8; 9] = [1, 1, 2, 4, 5, 6, 12, 12, 12];
const CRC: Jamcrc = Jamcrc::new(); assert_eq!(2411431516, CRC.checksum(&bytes))
assert_eq!(JAMCR.checksum(&bytes), CRC.checksum(&bytes))
} }
#[test] #[test]
fn check_xivcrc() { fn check_xivcrc() {
const CRC_32_TEST: Algorithm<u32> = Algorithm {
width: 32,
poly: 0x04c11db7,
init: 0x00000000,
refin: true,
refout: true,
xorout: 0x00000000,
check: 0x765e7680,
residue: 0xc704dd7b,
};
const JAMCR: Crc<u32> = Crc::<u32>::new(&CRC_32_TEST);
let str = "Default"; let str = "Default";
assert_eq!(XivCrc32::from(str).crc, JAMCR.checksum(str.as_bytes())); assert_eq!(XivCrc32::from(str).crc, 2978997821);
} }
} }

View file

@ -6,8 +6,6 @@ use std::fs;
use std::fs::{DirEntry, ReadDir}; use std::fs::{DirEntry, ReadDir};
use std::path::PathBuf; use std::path::PathBuf;
use tracing::{debug, warn};
use crate::ByteBuffer; use crate::ByteBuffer;
use crate::common::{Language, Platform, read_version}; use crate::common::{Language, Platform, read_version};
use crate::exd::EXD; use crate::exd::EXD;
@ -32,7 +30,6 @@ fn is_valid(path: &str) -> bool {
let d = PathBuf::from(path); let d = PathBuf::from(path);
if fs::metadata(d.as_path()).is_err() { if fs::metadata(d.as_path()).is_err() {
warn!("Game directory not found.");
return false; return false;
} }
@ -69,8 +66,6 @@ impl GameData {
/// GameData::from_existing(Platform::Win32, "$FFXIV/game"); /// GameData::from_existing(Platform::Win32, "$FFXIV/game");
/// ``` /// ```
pub fn from_existing(platform: Platform, directory: &str) -> GameData { pub fn from_existing(platform: Platform, directory: &str) -> GameData {
debug!(directory, "Loading game directory");
match is_valid(directory) { match is_valid(directory) {
true => { true => {
let mut data = Self { let mut data = Self {
@ -82,7 +77,7 @@ impl GameData {
data data
} }
false => { 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 { Self {
game_directory: String::from(directory), game_directory: String::from(directory),
repositories: vec![], repositories: vec![],
@ -181,8 +176,6 @@ impl GameData {
/// file.write(data.as_slice()).unwrap(); /// file.write(data.as_slice()).unwrap();
/// ``` /// ```
pub fn extract(&mut self, path: &str) -> Option<ByteBuffer> { pub fn extract(&mut self, path: &str) -> Option<ByteBuffer> {
debug!(file = path, "Extracting file");
let slice = self.find_entry(path); let slice = self.find_entry(path);
match slice { match slice {
Some((entry, chunk)) => { Some((entry, chunk)) => {

View file

@ -10,8 +10,11 @@ use std::sync::Arc;
use bitflags::bitflags; use bitflags::bitflags;
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct HavokValueType(u32);
bitflags! { bitflags! {
pub struct HavokValueType: u32 { impl HavokValueType: u32 {
const EMPTY = 0; const EMPTY = 0;
const BYTE = 1; const BYTE = 1;
const INT = 2; const INT = 2;
@ -25,42 +28,42 @@ bitflags! {
const STRING = 10; const STRING = 10;
const ARRAY = 0x10; const ARRAY = 0x10;
const ARRAYBYTE = Self::ARRAY.bits | Self::BYTE.bits; const ARRAYBYTE = Self::ARRAY.bits() | Self::BYTE.bits();
const ARRAYINT = Self::ARRAY.bits | Self::INT.bits; const ARRAYINT = Self::ARRAY.bits() | Self::INT.bits();
const ARRAYREAL = Self::ARRAY.bits | Self::REAL.bits; const ARRAYREAL = Self::ARRAY.bits() | Self::REAL.bits();
const ARRAYVEC4 = Self::ARRAY.bits | Self::VEC4.bits; const ARRAYVEC4 = Self::ARRAY.bits() | Self::VEC4.bits();
const ARRAYVEC8 = Self::ARRAY.bits | Self::VEC8.bits; const ARRAYVEC8 = Self::ARRAY.bits() | Self::VEC8.bits();
const ARRAYVEC12 = Self::ARRAY.bits | Self::VEC12.bits; const ARRAYVEC12 = Self::ARRAY.bits() | Self::VEC12.bits();
const ARRAYVEC16 = Self::ARRAY.bits | Self::VEC16.bits; const ARRAYVEC16 = Self::ARRAY.bits() | Self::VEC16.bits();
const ARRAYOBJECT = Self::ARRAY.bits | Self::OBJECT.bits; const ARRAYOBJECT = Self::ARRAY.bits() | Self::OBJECT.bits();
const ARRAYSTRUCT = Self::ARRAY.bits | Self::STRUCT.bits; const ARRAYSTRUCT = Self::ARRAY.bits() | Self::STRUCT.bits();
const ARRAYSTRING = Self::ARRAY.bits | Self::STRING.bits; const ARRAYSTRING = Self::ARRAY.bits() | Self::STRING.bits();
const TUPLE = 0x20; const TUPLE = 0x20;
const TUPLEBYTE = Self::TUPLE.bits | Self::BYTE.bits; const TUPLEBYTE = Self::TUPLE.bits() | Self::BYTE.bits();
const TUPLEINT = Self::TUPLE.bits | Self::INT.bits; const TUPLEINT = Self::TUPLE.bits() | Self::INT.bits();
const TUPLEREAL = Self::TUPLE.bits | Self::REAL.bits; const TUPLEREAL = Self::TUPLE.bits() | Self::REAL.bits();
const TUPLEVEC4 = Self::TUPLE.bits | Self::VEC4.bits; const TUPLEVEC4 = Self::TUPLE.bits() | Self::VEC4.bits();
const TUPLEVEC8 = Self::TUPLE.bits | Self::VEC8.bits; const TUPLEVEC8 = Self::TUPLE.bits() | Self::VEC8.bits();
const TUPLEVEC12 = Self::TUPLE.bits | Self::VEC12.bits; const TUPLEVEC12 = Self::TUPLE.bits() | Self::VEC12.bits();
const TUPLEVEC16 = Self::TUPLE.bits | Self::VEC16.bits; const TUPLEVEC16 = Self::TUPLE.bits() | Self::VEC16.bits();
const TUPLEOBJECT = Self::TUPLE.bits | Self::OBJECT.bits; const TUPLEOBJECT = Self::TUPLE.bits() | Self::OBJECT.bits();
const TUPLESTRUCT = Self::TUPLE.bits | Self::STRUCT.bits; const TUPLESTRUCT = Self::TUPLE.bits() | Self::STRUCT.bits();
const TUPLESTRING = Self::TUPLE.bits | Self::STRING.bits; const TUPLESTRING = Self::TUPLE.bits() | Self::STRING.bits();
} }
} }
impl HavokValueType { impl HavokValueType {
pub fn is_tuple(self) -> bool { pub fn is_tuple(self) -> bool {
(self.bits & HavokValueType::TUPLE.bits) != 0 (self.bits() & HavokValueType::TUPLE.bits()) != 0
} }
pub fn is_array(self) -> bool { pub fn is_array(self) -> bool {
(self.bits & HavokValueType::ARRAY.bits) != 0 (self.bits() & HavokValueType::ARRAY.bits()) != 0
} }
pub fn base_type(self) -> HavokValueType { 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 { pub fn is_vec(self) -> bool {

View file

@ -10,7 +10,6 @@ use std::path::{Path, PathBuf};
use crate::ByteBuffer; use crate::ByteBuffer;
use binrw::BinRead; use binrw::BinRead;
use binrw::{BinWrite, binrw}; use binrw::{BinWrite, binrw};
use tracing::{debug, warn};
use crate::common::{Platform, Region, get_platform_string}; use crate::common::{Platform, Region, get_platform_string};
use crate::common_file_operations::{ use crate::common_file_operations::{
@ -634,12 +633,12 @@ impl ZiPatch {
file.seek(SeekFrom::Start(fop.offset))?; file.seek(SeekFrom::Start(fop.offset))?;
file.write_all(&data)?; file.write_all(&data)?;
} else { } else {
warn!("{file_path} does not exist, skipping."); // silently skip if it does not exist
} }
} }
SqpkFileOperation::DeleteFile => { SqpkFileOperation::DeleteFile => {
if fs::remove_file(file_path.as_str()).is_err() { 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 => { SqpkFileOperation::RemoveAll => {
@ -662,30 +661,26 @@ impl ZiPatch {
} }
SqpkOperation::PatchInfo(_) => { SqpkOperation::PatchInfo(_) => {
// Currently, there's nothing we need from PatchInfo. Intentional NOP. // Currently, there's nothing we need from PatchInfo. Intentional NOP.
debug!("PATCH: NOP PatchInfo");
} }
SqpkOperation::TargetInfo(new_target_info) => { SqpkOperation::TargetInfo(new_target_info) => {
target_info = Some(new_target_info); target_info = Some(new_target_info);
} }
SqpkOperation::Index(_) => { SqpkOperation::Index(_) => {
// Currently, there's nothing we need from Index command. Intentional NOP. // Currently, there's nothing we need from Index command. Intentional NOP.
debug!("PATCH: NOP Index");
} }
} }
} }
ChunkType::FileHeader(_) => { ChunkType::FileHeader(_) => {
// Currently there's nothing very useful in the FileHeader, so it's an intentional NOP. // Currently there's nothing very useful in the FileHeader, so it's an intentional NOP.
debug!("PATCH: NOP FileHeader");
} }
ChunkType::ApplyOption(_) => { ChunkType::ApplyOption(_) => {
// Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP. // Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP.
debug!("PATCH: NOP ApplyOption");
} }
ChunkType::AddDirectory(_) => { ChunkType::AddDirectory(_) => {
debug!("PATCH: NOP AddDirectory"); // another NOP
} }
ChunkType::DeleteDirectory(_) => { ChunkType::DeleteDirectory(_) => {
debug!("PATCH: NOP DeleteDirectory"); // another NOP
} }
ChunkType::EndOfFile => { ChunkType::EndOfFile => {
return Ok(()); return Ok(());

View file

@ -13,10 +13,13 @@ use binrw::BinRead;
use binrw::binrw; use binrw::binrw;
use bitflags::bitflags; 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) // Attributes and Format are adapted from Lumina (https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/TexFile.cs)
bitflags! { bitflags! {
#[binrw] impl TextureAttribute : u32 {
struct TextureAttribute : u32 {
const DISCARD_PER_FRAME = 0x1; const DISCARD_PER_FRAME = 0x1;
const DISCARD_PER_MAP = 0x2; const DISCARD_PER_MAP = 0x2;

View file

@ -1,201 +0,0 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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<Path>) -> Vec<PathBuf> {
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<String, [u8; 64]> {
let mut file_hashes: HashMap<String, [u8; 64]> = 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);
}