1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-20 11:47:46 +00:00

Redesign how IndexFile parsing works, again

Now it reads much more of the index file format, and tries to handle
index1/index2 differences more opaquely. I'm not yet happy with the API, it
needs a bit more work.
This commit is contained in:
Joshua Goins 2025-03-06 18:52:26 -05:00
parent f0554c8c27
commit bb7c74fec8
41 changed files with 374 additions and 312 deletions

View file

@ -5,7 +5,7 @@ use std::io::{Cursor, Seek, SeekFrom};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::BinRead; use binrw::BinRead;
use binrw::{binread, binrw, BinReaderExt}; use binrw::{BinReaderExt, binread, binrw};
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -4,8 +4,8 @@
use std::io::{Cursor, Seek, SeekFrom}; use std::io::{Cursor, Seek, SeekFrom};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[br(little)] #[br(little)]

View file

@ -49,8 +49,10 @@ pub fn get_language_code(lang: &Language) -> &'static str {
#[brw(repr = i16)] #[brw(repr = i16)]
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Region { pub enum Region {
/// The global region. /// The global region, used for any region not specified.
Global = -1, // TODO: find patch codes for other regions :-) Global = -1,
/// Korea and China clients.
KoreaChina = 1,
} }
/// Reads a version file. /// Reads a version file.

View file

@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com> // SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use std::ffi::CString; use binrw::{BinReaderExt, BinResult, binread};
use binrw::{binread, BinReaderExt, BinResult};
use half::f16; use half::f16;
use std::ffi::CString;
use std::io::SeekFrom; use std::io::SeekFrom;
pub(crate) fn read_bool_from<T: std::convert::From<u8> + std::cmp::PartialEq>(x: T) -> bool { pub(crate) fn read_bool_from<T: std::convert::From<u8> + std::cmp::PartialEq>(x: T) -> bool {
@ -11,11 +11,7 @@ pub(crate) fn read_bool_from<T: std::convert::From<u8> + std::cmp::PartialEq>(x:
} }
pub(crate) fn write_bool_as<T: std::convert::From<u8>>(x: &bool) -> T { pub(crate) fn write_bool_as<T: std::convert::From<u8>>(x: &bool) -> T {
if *x { if *x { T::from(1u8) } else { T::from(0u8) }
T::from(1u8)
} else {
T::from(0u8)
}
} }
pub(crate) fn read_string(byte_stream: Vec<u8>) -> String { pub(crate) fn read_string(byte_stream: Vec<u8>) -> String {
@ -129,18 +125,27 @@ mod tests {
#[test] #[test]
fn read_string() { fn read_string() {
// The nul terminator is supposed to be removed // The nul terminator is supposed to be removed
assert_eq!(crate::common_file_operations::read_string(STRING_DATA.to_vec()), "FOO".to_string()); assert_eq!(
crate::common_file_operations::read_string(STRING_DATA.to_vec()),
"FOO".to_string()
);
} }
#[test] #[test]
fn write_string() { fn write_string() {
// Supposed to include the nul terminator // Supposed to include the nul terminator
assert_eq!(crate::common_file_operations::write_string(&"FOO".to_string()), STRING_DATA.to_vec()); assert_eq!(
crate::common_file_operations::write_string(&"FOO".to_string()),
STRING_DATA.to_vec()
);
} }
#[test] #[test]
fn get_string_len() { fn get_string_len() {
// Supposed to include the nul terminator // Supposed to include the nul terminator
assert_eq!(crate::common_file_operations::get_string_len(&"FOO".to_string()), 4); assert_eq!(
crate::common_file_operations::get_string_len(&"FOO".to_string()),
4
);
} }
} }

View file

@ -124,7 +124,7 @@ mod tests {
#[test] #[test]
fn check_jamcrc() { fn check_jamcrc() {
use crc::{Crc, CRC_32_JAMCRC}; use crc::{CRC_32_JAMCRC, Crc};
const JAMCR: Crc<u32> = Crc::<u32>::new(&CRC_32_JAMCRC); const JAMCR: Crc<u32> = Crc::<u32>::new(&CRC_32_JAMCRC);

View file

@ -7,7 +7,7 @@ use std::io::{Cursor, Read, Seek, SeekFrom};
use crate::ByteBuffer; use crate::ByteBuffer;
use binrw::BinRead; use binrw::BinRead;
use binrw::BinWrite; use binrw::BinWrite;
use binrw::{binrw, BinReaderExt}; use binrw::{BinReaderExt, binrw};
use crate::common_file_operations::read_bool_from; use crate::common_file_operations::read_bool_from;
#[cfg(feature = "visual_data")] #[cfg(feature = "visual_data")]

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com> // SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use crate::race::{get_race_id, Gender, Race, Subrace}; use crate::race::{Gender, Race, Subrace, get_race_id};
#[repr(u8)] #[repr(u8)]
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]

View file

@ -6,9 +6,9 @@ use std::io::{Cursor, Seek, SeekFrom};
use binrw::binrw; use binrw::binrw;
use binrw::{BinRead, Endian}; use binrw::{BinRead, Endian};
use crate::common::Language;
use crate::exh::{ColumnDataType, ExcelColumnDefinition, ExcelDataPagination, EXH};
use crate::ByteSpan; use crate::ByteSpan;
use crate::common::Language;
use crate::exh::{ColumnDataType, EXH, ExcelColumnDefinition, ExcelDataPagination};
#[binrw] #[binrw]
#[brw(magic = b"EXDF")] #[brw(magic = b"EXDF")]

View file

@ -5,11 +5,11 @@
use std::io::Cursor; use std::io::Cursor;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
use crate::common::Language;
use crate::ByteSpan; use crate::ByteSpan;
use crate::common::Language;
#[binrw] #[binrw]
#[brw(magic = b"EXHF")] #[brw(magic = b"EXHF")]

View file

@ -22,15 +22,15 @@ pub enum ExistingInstallType {
/// Installed via XIVOnMac /// Installed via XIVOnMac
XIVOnMac, XIVOnMac,
/// Installed via Astra /// Installed via Astra
Astra Astra,
} }
/// An existing install location on disk /// An existing install location on disk
pub struct ExistingGameDirectory { pub struct ExistingGameDirectory {
/// The application where this installation was from /// The application where this installation was from
pub install_type : ExistingInstallType, pub install_type: ExistingInstallType,
/// The path to the "main folder" where "game" and "boot" sits /// The path to the "main folder" where "game" and "boot" sits
pub path: String pub path: String,
} }
/// Finds existing installations on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid. /// Finds existing installations on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
@ -48,13 +48,15 @@ pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
// Official install (Steam) // Official install (Steam)
install_dirs.push(ExistingGameDirectory { install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::OfficialLauncher, install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir(".steam/steam/steamapps/common/FINAL FANTASY XIV - A Realm Reborn") path: from_home_dir(
".steam/steam/steamapps/common/FINAL FANTASY XIV - A Realm Reborn",
),
}); });
// XIVLauncherCore location // XIVLauncherCore location
install_dirs.push(ExistingGameDirectory { install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::XIVLauncherCore, install_type: ExistingInstallType::XIVLauncherCore,
path: from_home_dir(".xlcore/ffxiv") path: from_home_dir(".xlcore/ffxiv"),
}); });
// Astra location. But we have to iterate through each UUID. // Astra location. But we have to iterate through each UUID.
@ -73,7 +75,7 @@ pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
.for_each(|path| { .for_each(|path| {
install_dirs.push(ExistingGameDirectory { install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::Astra, install_type: ExistingInstallType::Astra,
path: path.into_os_string().into_string().unwrap() path: path.into_os_string().into_string().unwrap(),
}) })
}); });
} }
@ -91,7 +93,9 @@ pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
// Official install (Wine) // Official install (Wine)
install_dirs.push(ExistingGameDirectory { install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::OfficialLauncher, install_type: ExistingInstallType::OfficialLauncher,
path: "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn".parse().unwrap() path: "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"
.parse()
.unwrap(),
}); });
// TODO: Add Astra // TODO: Add Astra
@ -99,15 +103,18 @@ pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
&_ => {} &_ => {}
} }
install_dirs.into_iter().filter(|dir| is_valid_game_dir(&dir.path)).collect() install_dirs
.into_iter()
.filter(|dir| is_valid_game_dir(&dir.path))
.collect()
} }
/// An existing user directory /// An existing user directory
pub struct ExistingUserDirectory { pub struct ExistingUserDirectory {
/// The application where this directory was from /// The application where this directory was from
pub install_type : ExistingInstallType, pub install_type: ExistingInstallType,
/// The path to the user folder /// The path to the user folder
pub path: String pub path: String,
} }
/// Finds existing user folders on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid. /// Finds existing user folders on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
@ -123,13 +130,13 @@ pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
// Official install (Wine) // Official install (Wine)
user_dirs.push(ExistingUserDirectory { user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::OfficialLauncher, install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn") path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
}); });
// XIVLauncherCore location // XIVLauncherCore location
user_dirs.push(ExistingUserDirectory { user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::XIVLauncherCore, install_type: ExistingInstallType::XIVLauncherCore,
path: from_home_dir(".xlcore/ffxivConfig") path: from_home_dir(".xlcore/ffxivConfig"),
}); });
// Astra location. But we have to iterate through each UUID. // Astra location. But we have to iterate through each UUID.
@ -148,7 +155,7 @@ pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
.for_each(|path| { .for_each(|path| {
user_dirs.push(ExistingUserDirectory { user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::Astra, install_type: ExistingInstallType::Astra,
path: path.into_os_string().into_string().unwrap() path: path.into_os_string().into_string().unwrap(),
}) })
}); });
} }
@ -157,7 +164,7 @@ pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
// Official install (Wine) // Official install (Wine)
user_dirs.push(ExistingUserDirectory { user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::OfficialLauncher, install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn") path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
}) })
// TODO: Add XIV on Mac? // TODO: Add XIV on Mac?
@ -166,7 +173,7 @@ pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
// Official install // Official install
user_dirs.push(ExistingUserDirectory { user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::OfficialLauncher, install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn") path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
}) })
// TODO: Add Astra // TODO: Add Astra
@ -174,7 +181,10 @@ pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
&_ => {} &_ => {}
} }
user_dirs.into_iter().filter(|dir| is_valid_user_dir(&dir.path)).collect() user_dirs
.into_iter()
.filter(|dir| is_valid_user_dir(&dir.path))
.collect()
} }
fn from_home_dir(path: &'static str) -> String { fn from_home_dir(path: &'static str) -> String {

View file

@ -8,15 +8,15 @@ use std::path::PathBuf;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::common::{read_version, Language, Platform}; use crate::ByteBuffer;
use crate::common::{Language, Platform, read_version};
use crate::dat::DatFile; use crate::dat::DatFile;
use crate::exd::EXD; use crate::exd::EXD;
use crate::exh::EXH; use crate::exh::EXH;
use crate::exl::EXL; use crate::exl::EXL;
use crate::index::{Index2File, IndexEntry, IndexFile}; use crate::index::{IndexEntry, IndexFile};
use crate::patch::{PatchError, ZiPatch}; use crate::patch::{PatchError, ZiPatch};
use crate::repository::{string_to_category, Category, Repository}; use crate::repository::{Category, Repository, string_to_category};
use crate::ByteBuffer;
/// Framework for operating on game data. /// Framework for operating on game data.
pub struct GameData { pub struct GameData {
@ -27,7 +27,6 @@ pub struct GameData {
pub repositories: Vec<Repository>, pub repositories: Vec<Repository>,
index_files: HashMap<String, IndexFile>, index_files: HashMap<String, IndexFile>,
index2_files: HashMap<String, Index2File>,
} }
fn is_valid(path: &str) -> bool { fn is_valid(path: &str) -> bool {
@ -79,7 +78,6 @@ impl GameData {
game_directory: String::from(directory), game_directory: String::from(directory),
repositories: vec![], repositories: vec![],
index_files: HashMap::new(), index_files: HashMap::new(),
index2_files: HashMap::new(),
}; };
data.reload_repositories(platform); data.reload_repositories(platform);
Some(data) Some(data)
@ -157,7 +155,7 @@ impl GameData {
/// } /// }
/// ``` /// ```
pub fn exists(&mut self, path: &str) -> bool { pub fn exists(&mut self, path: &str) -> bool {
let Some((_, _)) = self.get_index_filenames(path) else { let Some(_) = self.get_index_filenames(path) else {
return false; return false;
}; };
@ -214,11 +212,10 @@ impl GameData {
Some((&self.repositories[0], string_to_category(tokens.0)?)) Some((&self.repositories[0], string_to_category(tokens.0)?))
} }
fn get_index_filenames(&self, path: &str) -> Option<(Vec<(String, u8)>, Vec<(String, u8)>)> { fn get_index_filenames(&self, path: &str) -> Option<Vec<(String, u8)>> {
let (repository, category) = self.parse_repository_category(path)?; let (repository, category) = self.parse_repository_category(path)?;
let mut index1_filenames = vec![]; let mut index_filenames = vec![];
let mut index2_filenames = vec![];
for chunk in 0..255 { for chunk in 0..255 {
let index_path: PathBuf = [ let index_path: PathBuf = [
@ -230,7 +227,7 @@ impl GameData {
.iter() .iter()
.collect(); .collect();
index1_filenames.push((index_path.into_os_string().into_string().unwrap(), chunk)); index_filenames.push((index_path.into_os_string().into_string().unwrap(), chunk));
let index2_path: PathBuf = [ let index2_path: PathBuf = [
&self.game_directory, &self.game_directory,
@ -241,10 +238,10 @@ impl GameData {
.iter() .iter()
.collect(); .collect();
index2_filenames.push((index2_path.into_os_string().into_string().unwrap(), chunk)); index_filenames.push((index2_path.into_os_string().into_string().unwrap(), chunk));
} }
Some((index1_filenames, index2_filenames)) Some(index_filenames)
} }
/// Read an excel sheet by name (e.g. "Achievement") /// Read an excel sheet by name (e.g. "Achievement")
@ -404,24 +401,12 @@ impl GameData {
} }
} }
fn cache_index2_file(&mut self, filename: &str) {
if !self.index2_files.contains_key(filename) {
if let Some(index_file) = Index2File::from_existing(filename) {
self.index2_files.insert(filename.to_string(), index_file);
}
}
}
fn get_index_file(&self, filename: &str) -> Option<&IndexFile> { fn get_index_file(&self, filename: &str) -> Option<&IndexFile> {
self.index_files.get(filename) self.index_files.get(filename)
} }
fn get_index2_file(&self, filename: &str) -> Option<&Index2File> {
self.index2_files.get(filename)
}
fn find_entry(&mut self, path: &str) -> Option<(IndexEntry, u8)> { fn find_entry(&mut self, path: &str) -> Option<(IndexEntry, u8)> {
let (index_paths, index2_paths) = self.get_index_filenames(path)?; let index_paths = self.get_index_filenames(path)?;
for (index_path, chunk) in index_paths { for (index_path, chunk) in index_paths {
self.cache_index_file(&index_path); self.cache_index_file(&index_path);
@ -433,16 +418,6 @@ impl GameData {
} }
} }
for (index2_path, chunk) in index2_paths {
self.cache_index2_file(&index2_path);
if let Some(index_file) = self.get_index2_file(&index2_path) {
if let Some(entry) = index_file.find_entry(path) {
return Some((entry, chunk));
}
}
}
None None
} }
} }
@ -479,8 +454,9 @@ mod tests {
data.parse_repository_category("exd/root.exl").unwrap(), data.parse_repository_category("exd/root.exl").unwrap(),
(&data.repositories[0], EXD) (&data.repositories[0], EXD)
); );
assert!(data assert!(
.parse_repository_category("what/some_font.dat") data.parse_repository_category("what/some_font.dat")
.is_none()); .is_none()
);
} }
} }

View file

@ -3,9 +3,9 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::havok::HavokAnimation;
use crate::havok::object::HavokObject; use crate::havok::object::HavokObject;
use crate::havok::spline_compressed_animation::HavokSplineCompressedAnimation; use crate::havok::spline_compressed_animation::HavokSplineCompressedAnimation;
use crate::havok::HavokAnimation;
use core::cell::RefCell; use core::cell::RefCell;
use std::sync::Arc; use std::sync::Arc;

View file

@ -1,10 +1,10 @@
// SPDX-FileCopyrightText: 2020 Inseok Lee // SPDX-FileCopyrightText: 2020 Inseok Lee
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use crate::havok::HavokAnimation;
use crate::havok::byte_reader::ByteReader; use crate::havok::byte_reader::ByteReader;
use crate::havok::object::HavokObject; use crate::havok::object::HavokObject;
use crate::havok::transform::HavokTransform; use crate::havok::transform::HavokTransform;
use crate::havok::HavokAnimation;
use core::{cell::RefCell, cmp}; use core::{cell::RefCell, cmp};
use std::f32; use std::f32;
use std::sync::Arc; use std::sync::Arc;

View file

@ -7,14 +7,17 @@
use std::io::SeekFrom; use std::io::SeekFrom;
use crate::common::Platform; use crate::common::Platform;
use crate::common::Region;
use crate::crc::Jamcrc; use crate::crc::Jamcrc;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
/// The type of this SqPack file. /// The type of this SqPack file.
#[binrw] #[binrw]
#[brw(repr = u8)] #[brw(repr = u8)]
enum SqPackFileType { enum SqPackFileType {
/// FFXIV Explorer says "SQDB", whatever that is.
SQDB = 0x0,
/// Dat files. /// Dat files.
Data = 0x1, Data = 0x1,
/// Index/Index2 files. /// Index/Index2 files.
@ -33,36 +36,54 @@ pub struct SqPackHeader {
file_type: SqPackFileType, file_type: SqPackFileType,
// some unknown value, zeroed out for index files // some unknown value, zeroed out for index files
// XivAlexandar says date/time, where does that come from?
unk1: u32, unk1: u32,
unk2: u32, unk2: u32,
// always 0xFFFFFFFF #[br(pad_size_to = 4)]
unk3: u32, region: Region,
#[brw(pad_before = 924)] #[brw(pad_before = 924)]
#[brw(pad_after = 44)] #[brw(pad_after = 44)]
// The SHA1 of the bytes immediately before this // The SHA1 of the bytes immediately before this
sha1_hash: [u8; 20] sha1_hash: [u8; 20],
} }
#[binrw] #[binrw]
#[derive(Debug)]
pub struct SegementDescriptor {
count: u32,
offset: u32,
size: u32,
#[brw(pad_after = 40)]
sha1_hash: [u8; 20],
}
#[binrw]
#[brw(repr = u8)]
#[derive(Debug, PartialEq)]
pub enum IndexType {
Index1,
Index2,
}
#[binrw]
#[derive(Debug)]
pub struct SqPackIndexHeader { pub struct SqPackIndexHeader {
size: u32, size: u32,
version: u32,
index_data_offset: u32, #[brw(pad_after = 4)]
index_data_size: u32, file_descriptor: SegementDescriptor,
index_data_hash: [u8; 64],
number_of_data_file: u32, // Count in this descriptor correlates to the number of dat files.
synonym_data_offset: u32, data_descriptor: SegementDescriptor,
synonym_data_size: u32,
synonym_data_hash: [u8; 64], unknown_descriptor: SegementDescriptor,
empty_block_data_offset: u32,
empty_block_data_size: u32, folder_descriptor: SegementDescriptor,
empty_block_data_hash: [u8; 64],
dir_index_data_offset: u32, #[brw(pad_size_to = 4)]
dir_index_data_size: u32, pub(crate) index_type: IndexType,
dir_index_data_hash: [u8; 64],
index_type: u32,
#[brw(pad_before = 656)] #[brw(pad_before = 656)]
#[brw(pad_after = 44)] #[brw(pad_after = 44)]
@ -71,14 +92,30 @@ pub struct SqPackIndexHeader {
} }
#[binrw] #[binrw]
#[br(import(index_type: &IndexType))]
#[derive(PartialEq)]
pub enum Hash {
#[br(pre_assert(*index_type == IndexType::Index1))]
SplitPath { name: u32, path: u32 },
#[br(pre_assert(*index_type == IndexType::Index2))]
FullPath(u32),
}
#[binrw]
#[br(import(index_type: &IndexType))]
pub struct IndexHashTableEntry { pub struct IndexHashTableEntry {
pub hash: u64, #[br(args(index_type))]
pub hash: Hash,
#[br(temp)] #[br(temp)]
#[bw(ignore)] #[bw(ignore)]
#[brw(pad_after = 4)]
data: u32, data: u32,
#[br(temp)]
#[bw(calc = 0)]
#[br(if(*index_type == IndexType::Index1))]
padding: u32,
#[br(calc = (data & 0b1) == 0b1)] #[br(calc = (data & 0b1) == 0b1)]
#[bw(ignore)] #[bw(ignore)]
pub is_synonym: bool, pub is_synonym: bool,
@ -117,6 +154,23 @@ pub struct Index2HashTableEntry {
pub offset: u64, pub offset: u64,
} }
#[binrw]
#[derive(Debug)]
pub struct DataEntry {
// A bunch of 0xFFFFFFFF
unk: [u8; 256],
}
#[binrw]
#[derive(Debug)]
pub struct FolderEntry {
hash: u32,
files_offset: u32,
// Divide by 0x10 to get the number of files
#[brw(pad_after = 4)]
total_files_size: u32,
}
#[derive(Debug)] #[derive(Debug)]
pub struct IndexEntry { pub struct IndexEntry {
pub hash: u64, pub hash: u64,
@ -132,22 +186,19 @@ pub struct IndexFile {
#[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))] #[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))]
index_header: SqPackIndexHeader, index_header: SqPackIndexHeader,
#[br(seek_before = SeekFrom::Start(index_header.index_data_offset.into()))] #[br(seek_before = SeekFrom::Start(index_header.file_descriptor.offset.into()), count = index_header.file_descriptor.size / 16, args { inner: (&index_header.index_type,) })]
#[br(count = index_header.index_data_size / 16)]
pub entries: Vec<IndexHashTableEntry>, pub entries: Vec<IndexHashTableEntry>,
}
#[binrw] #[br(seek_before = SeekFrom::Start(index_header.data_descriptor.offset.into()))]
#[br(little)] #[br(count = index_header.data_descriptor.size / 256)]
pub struct Index2File { pub data_entries: Vec<DataEntry>,
sqpack_header: SqPackHeader,
#[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))] /*#[br(seek_before = SeekFrom::Start(index_header.unknown_descriptor.offset.into()))]
index_header: SqPackIndexHeader, #[br(count = index_header.unknown_descriptor.size / 16)]
pub unknown_entries: Vec<IndexHashTableEntry>,*/
#[br(seek_before = SeekFrom::Start(index_header.index_data_offset.into()))] #[br(seek_before = SeekFrom::Start(index_header.folder_descriptor.offset.into()))]
#[br(count = index_header.index_data_size / 8)] #[br(count = index_header.folder_descriptor.size / 16)]
pub entries: Vec<Index2HashTableEntry>, pub folder_entries: Vec<FolderEntry>,
} }
const CRC: Jamcrc = Jamcrc::new(); const CRC: Jamcrc = Jamcrc::new();
@ -157,6 +208,8 @@ impl IndexFile {
pub fn from_existing(path: &str) -> Option<Self> { pub fn from_existing(path: &str) -> Option<Self> {
let mut index_file = std::fs::File::open(path).ok()?; let mut index_file = std::fs::File::open(path).ok()?;
println!("Reading {}!", path);
Self::read(&mut index_file).ok() Self::read(&mut index_file).ok()
} }
@ -168,68 +221,45 @@ impl IndexFile {
} }
/// Calculates a hash for `index` files from a game path. /// Calculates a hash for `index` files from a game path.
pub fn calculate_hash(path: &str) -> u64 { pub fn calculate_hash(&self, path: &str) -> Hash {
let lowercase = path.to_lowercase(); let lowercase = path.to_lowercase();
if let Some(pos) = lowercase.rfind('/') { return match &self.index_header.index_type {
let (directory, filename) = lowercase.split_at(pos); IndexType::Index1 => {
if let Some(pos) = lowercase.rfind('/') {
let (directory, filename) = lowercase.split_at(pos);
let directory_crc = CRC.checksum(directory.as_bytes()); let directory_crc = CRC.checksum(directory.as_bytes());
let filename_crc = CRC.checksum(filename[1..filename.len()].as_bytes()); let filename_crc = CRC.checksum(filename[1..filename.len()].as_bytes());
(directory_crc as u64) << 32 | (filename_crc as u64) Hash::SplitPath {
} else { name: filename_crc,
CRC.checksum(lowercase.as_bytes()) as u64 path: directory_crc,
} }
} else {
// TODO: is this ever hit?
panic!("This is unexpected, why is the file sitting outside of a folder?");
}
}
IndexType::Index2 => Hash::FullPath(CRC.checksum(lowercase.as_bytes())),
};
} }
// TODO: turn into traits?
pub fn exists(&self, path: &str) -> bool { pub fn exists(&self, path: &str) -> bool {
let hash = IndexFile::calculate_hash(path); let hash = self.calculate_hash(path);
self.entries.iter().any(|s| s.hash == hash) self.entries.iter().any(|s| s.hash == hash)
} }
pub fn find_entry(&self, path: &str) -> Option<IndexEntry> { pub fn find_entry(&self, path: &str) -> Option<IndexEntry> {
let hash = IndexFile::calculate_hash(path); let hash = self.calculate_hash(path);
if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) { if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) {
let full_hash = match hash {
Hash::SplitPath { name, path } => (path as u64) << 32 | (name as u64),
Hash::FullPath(hash) => hash as u64,
};
return Some(IndexEntry { return Some(IndexEntry {
hash: entry.hash, hash: 0,
data_file_id: entry.data_file_id,
offset: entry.offset,
});
}
None
}
}
impl Index2File {
/// Creates a new reference to an existing index2 file.
pub fn from_existing(path: &str) -> Option<Self> {
let mut index_file = std::fs::File::open(path).ok()?;
Self::read(&mut index_file).ok()
}
/// Calculates a hash for `index2` files from a game path.
pub fn calculate_hash(path: &str) -> u32 {
let lowercase = path.to_lowercase();
CRC.checksum(lowercase.as_bytes())
}
pub fn exists(&self, path: &str) -> bool {
let hash = Index2File::calculate_hash(path);
self.entries.iter().any(|s| s.hash == hash)
}
pub fn find_entry(&self, path: &str) -> Option<IndexEntry> {
let hash = Index2File::calculate_hash(path);
if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) {
return Some(IndexEntry {
hash: entry.hash as u64,
data_file_id: entry.data_file_id, data_file_id: entry.data_file_id,
offset: entry.offset, offset: entry.offset,
}); });
@ -254,14 +284,4 @@ mod tests {
// Feeding it invalid data should not panic // Feeding it invalid data should not panic
IndexFile::from_existing(d.to_str().unwrap()); IndexFile::from_existing(d.to_str().unwrap());
} }
#[test]
fn test_index2_invalid() {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/tests");
d.push("random");
// Feeding it invalid data should not panic
Index2File::from_existing(d.to_str().unwrap());
}
} }

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -5,7 +5,7 @@ use std::io::{Cursor, Seek, SeekFrom};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw; use binrw::binrw;
use binrw::{binread, BinRead, BinReaderExt}; use binrw::{BinRead, BinReaderExt, binread};
// From https://github.com/NotAdam/Lumina/tree/40dab50183eb7ddc28344378baccc2d63ae71d35/src/Lumina/Data/Parsing/Layer // From https://github.com/NotAdam/Lumina/tree/40dab50183eb7ddc28344378baccc2d63ae71d35/src/Lumina/Data/Parsing/Layer

View file

@ -4,8 +4,8 @@
use std::io::{Cursor, Seek, SeekFrom}; use std::io::{Cursor, Seek, SeekFrom};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[allow(dead_code)] #[allow(dead_code)]

View file

@ -8,10 +8,13 @@ use std::mem::size_of;
use binrw::BinRead; use binrw::BinRead;
use binrw::BinReaderExt; use binrw::BinReaderExt;
use binrw::{binrw, BinWrite, BinWriterExt}; use binrw::{BinWrite, BinWriterExt, binrw};
use crate::common_file_operations::{read_bool_from, write_bool_as}; use crate::common_file_operations::{read_bool_from, write_bool_as};
use crate::model_vertex_declarations::{vertex_element_parser, vertex_element_writer, VertexDeclaration, VertexType, VertexUsage, VERTEX_ELEMENT_SIZE}; use crate::model_vertex_declarations::{
VERTEX_ELEMENT_SIZE, VertexDeclaration, VertexType, VertexUsage, vertex_element_parser,
vertex_element_writer,
};
use crate::{ByteBuffer, ByteSpan}; use crate::{ByteBuffer, ByteSpan};
pub const NUM_VERTICES: u32 = 17; pub const NUM_VERTICES: u32 = 17;
@ -551,7 +554,8 @@ impl MDL {
MDL::read_byte_float4(&mut cursor).unwrap(); MDL::read_byte_float4(&mut cursor).unwrap();
} }
VertexType::Byte4 => { VertexType::Byte4 => {
vertices[k as usize].bone_weight = MDL::read_tangent(&mut cursor).unwrap(); vertices[k as usize].bone_weight =
MDL::read_tangent(&mut cursor).unwrap();
} }
VertexType::UnsignedShort4 => { VertexType::UnsignedShort4 => {
let bytes = MDL::read_unsigned_short4(&mut cursor).unwrap(); let bytes = MDL::read_unsigned_short4(&mut cursor).unwrap();
@ -746,8 +750,7 @@ impl MDL {
for shape_value in shape_values { for shape_value in shape_values {
let old_vertex = let old_vertex =
vertices[indices[shape_value.base_indices_index as usize] as usize]; vertices[indices[shape_value.base_indices_index as usize] as usize];
let new_vertex = vertices[shape_value.replacing_vertex_index let new_vertex = vertices[shape_value.replacing_vertex_index as usize];
as usize];
let vertex = &mut morphed_vertices let vertex = &mut morphed_vertices
[indices[shape_value.base_indices_index as usize] as usize]; [indices[shape_value.base_indices_index as usize] as usize];
@ -786,8 +789,9 @@ impl MDL {
.seek(SeekFrom::Start( .seek(SeekFrom::Start(
(model.lods[i as usize].vertex_data_offset (model.lods[i as usize].vertex_data_offset
+ model.meshes[j as usize].vertex_buffer_offsets + model.meshes[j as usize].vertex_buffer_offsets
[stream as usize] [stream as usize]
+ (z as u32 * stride as u32)) as u64, + (z as u32 * stride as u32))
as u64,
)) ))
.ok()?; .ok()?;
@ -797,7 +801,8 @@ impl MDL {
} }
vertex_streams.push(vertex_data); vertex_streams.push(vertex_data);
vertex_stream_strides.push(mesh.vertex_buffer_strides[stream as usize] as usize); vertex_stream_strides
.push(mesh.vertex_buffer_strides[stream as usize] as usize);
} }
parts.push(Part { parts.push(Part {
@ -808,7 +813,7 @@ impl MDL {
submeshes, submeshes,
shapes, shapes,
vertex_streams, vertex_streams,
vertex_stream_strides vertex_stream_strides,
}); });
} }
@ -1047,7 +1052,7 @@ impl MDL {
&mut cursor, &mut cursor,
&MDL::pad_slice(&vert.position, 1.0), &MDL::pad_slice(&vert.position, 1.0),
) )
.ok()?; .ok()?;
} }
VertexType::Half4 => { VertexType::Half4 => {
MDL::write_half4( MDL::write_half4(

View file

@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com> // SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use crate::model::MDL;
use crate::ByteSpan; use crate::ByteSpan;
use crate::model::MDL;
use binrw::{BinReaderExt, BinResult, BinWriterExt}; use binrw::{BinReaderExt, BinResult, BinWriterExt};
use half::f16; use half::f16;
use std::io::Cursor; use std::io::Cursor;

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use crate::model::NUM_VERTICES; use crate::model::NUM_VERTICES;
use binrw::{binrw, BinRead, BinResult, BinWrite}; use binrw::{BinRead, BinResult, BinWrite, binrw};
use std::io::SeekFrom; use std::io::SeekFrom;
/// Marker for end of stream (0xFF) /// Marker for end of stream (0xFF)

View file

@ -5,11 +5,13 @@
use std::io::Cursor; use std::io::Cursor;
use crate::common_file_operations::{Half1, Half2, Half3};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::{binread, binrw, BinRead, BinResult}; use crate::common_file_operations::{Half1, Half2, Half3};
use crate::mtrl::ColorDyeTable::{DawntrailColorDyeTable, LegacyColorDyeTable, OpaqueColorDyeTable}; use crate::mtrl::ColorDyeTable::{
DawntrailColorDyeTable, LegacyColorDyeTable, OpaqueColorDyeTable,
};
use crate::mtrl::ColorTable::{DawntrailColorTable, LegacyColorTable, OpaqueColorTable}; use crate::mtrl::ColorTable::{DawntrailColorTable, LegacyColorTable, OpaqueColorTable};
use binrw::{BinRead, BinResult, binread, binrw};
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]
@ -177,7 +179,7 @@ pub struct OpaqueColorTableData {
pub enum ColorTable { pub enum ColorTable {
LegacyColorTable(LegacyColorTableData), LegacyColorTable(LegacyColorTableData),
DawntrailColorTable(DawntrailColorTableData), DawntrailColorTable(DawntrailColorTableData),
OpaqueColorTable(OpaqueColorTableData) OpaqueColorTable(OpaqueColorTableData),
} }
#[binread] #[binread]
@ -285,7 +287,7 @@ pub struct OpaqueColorDyeTableData {
pub enum ColorDyeTable { pub enum ColorDyeTable {
LegacyColorDyeTable(LegacyColorDyeTableData), LegacyColorDyeTable(LegacyColorDyeTableData),
DawntrailColorDyeTable(DawntrailColorDyeTableData), DawntrailColorDyeTable(DawntrailColorDyeTableData),
OpaqueColorDyeTable(OpaqueColorDyeTableData) OpaqueColorDyeTable(OpaqueColorDyeTableData),
} }
#[binrw] #[binrw]
@ -386,7 +388,7 @@ fn parse_color_table(table_dimension_logs: u8) -> BinResult<Option<ColorTable>>
Ok(Some(match table_dimension_logs { Ok(Some(match table_dimension_logs {
0 | 0x42 => LegacyColorTable(LegacyColorTableData::read_options(reader, endian, ())?), 0 | 0x42 => LegacyColorTable(LegacyColorTableData::read_options(reader, endian, ())?),
0x53 => DawntrailColorTable(DawntrailColorTableData::read_options(reader, endian, ())?), 0x53 => DawntrailColorTable(DawntrailColorTableData::read_options(reader, endian, ())?),
_ => OpaqueColorTable(OpaqueColorTableData::read_options(reader, endian, ())?) _ => OpaqueColorTable(OpaqueColorTableData::read_options(reader, endian, ())?),
})) }))
} }
@ -394,8 +396,12 @@ fn parse_color_table(table_dimension_logs: u8) -> BinResult<Option<ColorTable>>
fn parse_color_dye_table(table_dimension_logs: u8) -> BinResult<Option<ColorDyeTable>> { fn parse_color_dye_table(table_dimension_logs: u8) -> BinResult<Option<ColorDyeTable>> {
Ok(Some(match table_dimension_logs { Ok(Some(match table_dimension_logs {
0 => LegacyColorDyeTable(LegacyColorDyeTableData::read_options(reader, endian, ())?), 0 => LegacyColorDyeTable(LegacyColorDyeTableData::read_options(reader, endian, ())?),
0x50...0x5F => DawntrailColorDyeTable(DawntrailColorDyeTableData::read_options(reader, endian, ())?), 0x50...0x5F => DawntrailColorDyeTable(DawntrailColorDyeTableData::read_options(
_ => OpaqueColorDyeTable(OpaqueColorDyeTableData::read_options(reader, endian, ())?) reader,
endian,
(),
)?),
_ => OpaqueColorDyeTable(OpaqueColorDyeTableData::read_options(reader, endian, ())?),
})) }))
} }

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -7,13 +7,15 @@ use std::fs::{File, OpenOptions, read, read_dir};
use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write}; use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use binrw::{binrw, BinWrite};
use binrw::BinRead;
use tracing::{debug, warn};
use crate::ByteBuffer; use crate::ByteBuffer;
use binrw::BinRead;
use binrw::{BinWrite, binrw};
use tracing::{debug, warn};
use crate::common::{get_platform_string, Platform, Region}; use crate::common::{Platform, Region, get_platform_string};
use crate::common_file_operations::{get_string_len, read_bool_from, read_string, write_bool_as, write_string}; use crate::common_file_operations::{
get_string_len, read_bool_from, read_string, write_bool_as, write_string,
};
use crate::sqpack::{read_data_block_patch, write_data_block_patch}; use crate::sqpack::{read_data_block_patch, write_data_block_patch};
#[binrw] #[binrw]
@ -460,8 +462,8 @@ impl ZiPatch {
&get_expansion_folder_sub(sub_id), &get_expansion_folder_sub(sub_id),
&filename, &filename,
] ]
.iter() .iter()
.collect(); .collect();
path.to_str().unwrap().to_string() path.to_str().unwrap().to_string()
}; };
@ -486,8 +488,8 @@ impl ZiPatch {
&get_expansion_folder_sub(sub_id), &get_expansion_folder_sub(sub_id),
&filename, &filename,
] ]
.iter() .iter()
.collect(); .collect();
path.to_str().unwrap().to_string() path.to_str().unwrap().to_string()
}; };
@ -580,7 +582,8 @@ impl ZiPatch {
), ),
}; };
let (left, _) = file_path.rsplit_once('/').ok_or(PatchError::ParseError)?; let (left, _) =
file_path.rsplit_once('/').ok_or(PatchError::ParseError)?;
fs::create_dir_all(left)?; fs::create_dir_all(left)?;
let mut new_file = OpenOptions::new() let mut new_file = OpenOptions::new()
@ -606,7 +609,8 @@ impl ZiPatch {
// reverse reading crc32 // reverse reading crc32
file.seek(SeekFrom::Current(-4))?; file.seek(SeekFrom::Current(-4))?;
let mut data: Vec<u8> = Vec::with_capacity(fop.file_size as usize); let mut data: Vec<u8> =
Vec::with_capacity(fop.file_size as usize);
while data.len() < fop.file_size as usize { while data.len() < fop.file_size as usize {
data.append(&mut read_data_block_patch(&mut file).unwrap()); data.append(&mut read_data_block_patch(&mut file).unwrap());
@ -639,10 +643,13 @@ impl ZiPatch {
} }
} }
SqpkFileOperation::RemoveAll => { SqpkFileOperation::RemoveAll => {
let path: PathBuf = let path: PathBuf = [
[data_dir, "sqpack", &get_expansion_folder(fop.expansion_id)] data_dir,
.iter() "sqpack",
.collect(); &get_expansion_folder(fop.expansion_id),
]
.iter()
.collect();
if fs::read_dir(&path).is_ok() { if fs::read_dir(&path).is_ok() {
fs::remove_dir_all(&path)?; fs::remove_dir_all(&path)?;
@ -702,18 +709,29 @@ impl ZiPatch {
let new_files = crate::patch::recurse(new_directory); let new_files = crate::patch::recurse(new_directory);
// A set of files not present in base, but in new (aka added files) // A set of files not present in base, but in new (aka added files)
let added_files: Vec<&PathBuf> = new_files.iter().filter(|item| { let added_files: Vec<&PathBuf> = new_files
let metadata = fs::metadata(item).unwrap(); .iter()
!base_files.contains(item) && metadata.len() > 0 // TODO: we filter out zero byte files here, but does SqEx do that? .filter(|item| {
}).collect(); let metadata = fs::metadata(item).unwrap();
!base_files.contains(item) && metadata.len() > 0 // TODO: we filter out zero byte files here, but does SqEx do that?
})
.collect();
// A set of files not present in the new directory, that used to be in base (aka removedf iles) // A set of files not present in the new directory, that used to be in base (aka removedf iles)
let removed_files: Vec<&PathBuf> = base_files.iter().filter(|item| !new_files.contains(item)).collect(); let removed_files: Vec<&PathBuf> = base_files
.iter()
.filter(|item| !new_files.contains(item))
.collect();
// Process added files // Process added files
for file in added_files { for file in added_files {
let file_data = read(file.to_str().unwrap()).unwrap(); let file_data = read(file.to_str().unwrap()).unwrap();
let relative_path = file.strip_prefix(new_directory).unwrap().to_str().unwrap().to_string(); let relative_path = file
.strip_prefix(new_directory)
.unwrap()
.to_str()
.unwrap()
.to_string();
let add_file_chunk = PatchChunk { let add_file_chunk = PatchChunk {
size: 0, size: 0,
@ -744,7 +762,12 @@ impl ZiPatch {
// Process deleted files // Process deleted files
for file in removed_files { for file in removed_files {
let relative_path = file.strip_prefix(base_directory).unwrap().to_str().unwrap().to_string(); let relative_path = file
.strip_prefix(base_directory)
.unwrap()
.to_str()
.unwrap()
.to_string();
let remove_file_chunk = PatchChunk { let remove_file_chunk = PatchChunk {
size: 0, size: 0,
@ -810,7 +833,9 @@ mod tests {
write(data_dir.clone() + "/test.patch", &read(d).unwrap()).unwrap(); write(data_dir.clone() + "/test.patch", &read(d).unwrap()).unwrap();
// Feeding it invalid data should not panic // Feeding it invalid data should not panic
let Err(PatchError::ParseError) = ZiPatch::apply(&data_dir.clone(), &(data_dir + "/test.patch")) else { let Err(PatchError::ParseError) =
ZiPatch::apply(&data_dir.clone(), &(data_dir + "/test.patch"))
else {
panic!("Expecting a parse error!"); panic!("Expecting a parse error!");
}; };
} }
@ -838,13 +863,19 @@ mod tests {
let old_files = recurse(&resources_dir); let old_files = recurse(&resources_dir);
let new_files = recurse(&data_dir); let new_files = recurse(&data_dir);
let mut old_relative_files: Vec<&Path> = old_files.iter().filter(|item| { let mut old_relative_files: Vec<&Path> = old_files
let metadata = fs::metadata(item).unwrap(); .iter()
metadata.len() > 0 // filter out zero byte files because ZiPatch::create does .filter(|item| {
}).map(|x| x.strip_prefix(&resources_dir).unwrap()).collect(); let metadata = fs::metadata(item).unwrap();
let mut new_relative_files: Vec<&Path> = new_files.iter().map(|x| x.strip_prefix(&data_dir).unwrap()).collect(); metadata.len() > 0 // filter out zero byte files because ZiPatch::create does
})
.map(|x| x.strip_prefix(&resources_dir).unwrap())
.collect();
let mut new_relative_files: Vec<&Path> = new_files
.iter()
.map(|x| x.strip_prefix(&data_dir).unwrap())
.collect();
assert_eq!(old_relative_files.sort(), new_relative_files.sort()); assert_eq!(old_relative_files.sort(), new_relative_files.sort());
} }
} }

View file

@ -34,7 +34,7 @@ pub struct PatchList {
/// The version that was requested from the server. /// The version that was requested from the server.
pub requested_version: String, pub requested_version: String,
/// The list of patches. /// The list of patches.
pub patches: Vec<PatchEntry> pub patches: Vec<PatchEntry>,
} }
/// The kind of patch list. /// The kind of patch list.
@ -45,7 +45,7 @@ pub enum PatchListType {
/// A boot patch list. /// A boot patch list.
Boot, Boot,
/// A game patch ist. /// A game patch ist.
Game Game,
} }
impl PatchList { impl PatchList {
@ -56,7 +56,8 @@ impl PatchList {
if let Some(patch_length_index) = encoded.find("X-Patch-Length: ") { if let Some(patch_length_index) = encoded.find("X-Patch-Length: ") {
let rest_of_string = &encoded[patch_length_index..]; let rest_of_string = &encoded[patch_length_index..];
if let Some(end_of_number_index) = rest_of_string.find("\r\n") { if let Some(end_of_number_index) = rest_of_string.find("\r\n") {
let patch_length_parse: Result<u64, _> = rest_of_string[0..end_of_number_index].parse(); let patch_length_parse: Result<u64, _> =
rest_of_string[0..end_of_number_index].parse();
if let Ok(p) = patch_length_parse { if let Ok(p) = patch_length_parse {
patch_length = p; patch_length = p;
} }
@ -187,7 +188,10 @@ mod tests {
let patch_list = PatchList::from_string(PatchListType::Boot, test_case); let patch_list = PatchList::from_string(PatchListType::Boot, test_case);
assert_eq!(patch_list.patches.len(), 1); assert_eq!(patch_list.patches.len(), 1);
assert_eq!(patch_list.patches[0].version, "2023.09.14.0000.0001"); assert_eq!(patch_list.patches[0].version, "2023.09.14.0000.0001");
assert_eq!(patch_list.patches[0].url, "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch"); assert_eq!(
patch_list.patches[0].url,
"http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch"
);
assert_eq!(patch_list.patches[0].size_on_disk, 69674819); assert_eq!(patch_list.patches[0].size_on_disk, 69674819);
} }
@ -251,7 +255,10 @@ mod tests {
let patch_list = PatchList::from_string(PatchListType::Game, test_case); let patch_list = PatchList::from_string(PatchListType::Game, test_case);
assert_eq!(patch_list.patches.len(), 19); assert_eq!(patch_list.patches.len(), 19);
assert_eq!(patch_list.patches[5].version, "2023.07.26.0000.0001"); assert_eq!(patch_list.patches[5].version, "2023.07.26.0000.0001");
assert_eq!(patch_list.patches[5].url, "http://patch-dl.ffxiv.com/game/ex1/6b936f08/D2023.07.26.0000.0001.patch"); assert_eq!(
patch_list.patches[5].url,
"http://patch-dl.ffxiv.com/game/ex1/6b936f08/D2023.07.26.0000.0001.patch"
);
assert_eq!(patch_list.patches[5].size_on_disk, 5854598228); assert_eq!(patch_list.patches[5].size_on_disk, 5854598228);
} }
@ -266,18 +273,17 @@ mod tests {
id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(), id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
requested_version: "D2023.04.28.0000.0001".to_string(), requested_version: "D2023.04.28.0000.0001".to_string(),
content_location: "ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http".to_string(), content_location: "ffxivpatch/2b5cbc63/metainfo/D2023.04.28.0000.0001.http".to_string(),
patches: vec![ patches: vec![PatchEntry {
PatchEntry { url: "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch"
url: "http://patch-dl.ffxiv.com/boot/2b5cbc63/D2023.09.14.0000.0001.patch".to_string(), .to_string(),
version: "2023.09.14.0000.0001".to_string(), version: "2023.09.14.0000.0001".to_string(),
hash_block_size: 0, hash_block_size: 0,
length: 22221335, length: 22221335,
size_on_disk: 69674819, size_on_disk: 69674819,
hashes: vec![], hashes: vec![],
unknown_a: 19, unknown_a: 19,
unknown_b: 18 unknown_b: 18,
} }],
],
patch_length: 22221335, patch_length: 22221335,
}; };
@ -305,49 +311,48 @@ mod tests {
id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(), id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
requested_version: "2023.07.26.0000.0000".to_string(), requested_version: "2023.07.26.0000.0000".to_string(),
content_location: "ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http".to_string(), content_location: "ffxivpatch/4e9a232b/metainfo/2023.07.26.0000.0000.http".to_string(),
patches: vec![ patches: vec![PatchEntry {
PatchEntry { url: "http://patch-dl.ffxiv.com/game/4e9a232b/D2023.09.15.0000.0000.patch"
url: "http://patch-dl.ffxiv.com/game/4e9a232b/D2023.09.15.0000.0000.patch".to_string(), .to_string(),
version: "2023.09.15.0000.0000".to_string(), version: "2023.09.15.0000.0000".to_string(),
hash_block_size: 50000000, hash_block_size: 50000000,
length: 1479062470, length: 1479062470,
size_on_disk: 44145529682, size_on_disk: 44145529682,
unknown_a: 71, unknown_a: 71,
unknown_b: 11, unknown_b: 11,
hashes: vec![ hashes: vec![
"1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c".to_string(), "1c66becde2a8cf26a99d0fc7c06f15f8bab2d87c".to_string(),
"950725418366c965d824228bf20f0496f81e0b9a".to_string(), "950725418366c965d824228bf20f0496f81e0b9a".to_string(),
"cabef48f7bf00fbf18b72843bdae2f61582ad264".to_string(), "cabef48f7bf00fbf18b72843bdae2f61582ad264".to_string(),
"53608de567b52f5fdb43fdb8b623156317e26704".to_string(), "53608de567b52f5fdb43fdb8b623156317e26704".to_string(),
"f0bc06cabf9ff6490f36114b25f62619d594dbe8".to_string(), "f0bc06cabf9ff6490f36114b25f62619d594dbe8".to_string(),
"3c5e4b962cd8445bd9ee29011ecdb331d108abd8".to_string(), "3c5e4b962cd8445bd9ee29011ecdb331d108abd8".to_string(),
"88e1a2a322f09de3dc28173d4130a2829950d4e0".to_string(), "88e1a2a322f09de3dc28173d4130a2829950d4e0".to_string(),
"1040667917dc99b9215dfccff0e458c2e8a724a8".to_string(), "1040667917dc99b9215dfccff0e458c2e8a724a8".to_string(),
"149c7e20e9e3e376377a130e0526b35fd7f43df2".to_string(), "149c7e20e9e3e376377a130e0526b35fd7f43df2".to_string(),
"1bb4e33807355cdf46af93ce828b6e145a9a8795".to_string(), "1bb4e33807355cdf46af93ce828b6e145a9a8795".to_string(),
"a79daff43db488f087da8e22bb4c21fd3a390f3c".to_string(), "a79daff43db488f087da8e22bb4c21fd3a390f3c".to_string(),
"6b04fadb656d467fb8318eba1c7f5ee8f030d967".to_string(), "6b04fadb656d467fb8318eba1c7f5ee8f030d967".to_string(),
"a6641e1c894db961a49b70fda2b0d6d87be487a7".to_string(), "a6641e1c894db961a49b70fda2b0d6d87be487a7".to_string(),
"edf419de49f42ef19bd6814f8184b35a25e9e977".to_string(), "edf419de49f42ef19bd6814f8184b35a25e9e977".to_string(),
"c1525c4df6001b66b575e2891db0284dc3a16566".to_string(), "c1525c4df6001b66b575e2891db0284dc3a16566".to_string(),
"01b7628095b07fa3c9c1aed2d66d32d118020321".to_string(), "01b7628095b07fa3c9c1aed2d66d32d118020321".to_string(),
"991b137ea0ebb11bd668f82149bc2392a4cbcf52".to_string(), "991b137ea0ebb11bd668f82149bc2392a4cbcf52".to_string(),
"ad3f74d4fca143a6cf507fc859544a4bcd501d85".to_string(), "ad3f74d4fca143a6cf507fc859544a4bcd501d85".to_string(),
"936a0f1711e273519cae6b2da0d8b435fe6aa020".to_string(), "936a0f1711e273519cae6b2da0d8b435fe6aa020".to_string(),
"023f19d8d8b3ecaaf865e3170e8243dd437a384c".to_string(), "023f19d8d8b3ecaaf865e3170e8243dd437a384c".to_string(),
"2d9e934de152956961a849e81912ca8d848265ca".to_string(), "2d9e934de152956961a849e81912ca8d848265ca".to_string(),
"8e32f9aa76c95c60a9dbe0967aee5792b812d5ec".to_string(), "8e32f9aa76c95c60a9dbe0967aee5792b812d5ec".to_string(),
"dee052b9aa1cc8863efd61afc63ac3c2d56f9acc".to_string(), "dee052b9aa1cc8863efd61afc63ac3c2d56f9acc".to_string(),
"fa81225aea53fa13a9bae1e8e02dea07de6d7052".to_string(), "fa81225aea53fa13a9bae1e8e02dea07de6d7052".to_string(),
"59b24693b1b62ea1660bc6f96a61f7d41b3f7878".to_string(), "59b24693b1b62ea1660bc6f96a61f7d41b3f7878".to_string(),
"349b691db1853f6c0120a8e66093c763ba6e3671".to_string(), "349b691db1853f6c0120a8e66093c763ba6e3671".to_string(),
"4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50".to_string(), "4561eb6f954d80cdb1ece3cc4d58cbd864bf2b50".to_string(),
"de94175c4db39a11d5334aefc7a99434eea8e4f9".to_string(), "de94175c4db39a11d5334aefc7a99434eea8e4f9".to_string(),
"55dd7215f24441d6e47d1f9b32cebdb041f2157f".to_string(), "55dd7215f24441d6e47d1f9b32cebdb041f2157f".to_string(),
"2ca09db645cfeefa41a04251dfcb13587418347a".to_string() "2ca09db645cfeefa41a04251dfcb13587418347a".to_string(),
], ],
} }],
],
patch_length: 1479062470, patch_length: 1479062470,
}; };

View file

@ -3,10 +3,10 @@
use std::io::{Cursor, SeekFrom}; use std::io::{Cursor, SeekFrom};
use crate::common_file_operations::strings_parser;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binread; use crate::common_file_operations::strings_parser;
use binrw::BinRead; use binrw::BinRead;
use binrw::binread;
#[binread] #[binread]
#[derive(Debug)] #[derive(Debug)]

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binread;
use binrw::BinRead; use binrw::BinRead;
use binrw::binread;
#[binread] #[binread]
#[derive(Debug)] #[derive(Debug)]

View file

@ -5,7 +5,7 @@ use std::cmp::Ordering;
use std::cmp::Ordering::{Greater, Less}; use std::cmp::Ordering::{Greater, Less};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::common::{get_platform_string, read_version, Platform}; use crate::common::{Platform, get_platform_string, read_version};
use crate::repository::RepositoryType::{Base, Expansion}; use crate::repository::RepositoryType::{Base, Expansion};
/// The type of repository, discerning game data from expansion data. /// The type of repository, discerning game data from expansion data.

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binread;
use binrw::BinRead; use binrw::BinRead;
use binrw::binread;
#[binread] #[binread]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -3,9 +3,9 @@
use std::io::{Cursor, SeekFrom}; use std::io::{Cursor, SeekFrom};
use crate::crc::XivCrc32;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::{binread, BinRead}; use crate::crc::XivCrc32;
use binrw::{BinRead, binread};
#[binread] #[binread]
#[br(little, import { #[br(little, import {

View file

@ -6,11 +6,11 @@
#![allow(clippy::upper_case_acronyms)] #![allow(clippy::upper_case_acronyms)]
use binrw::helpers::until_eof; use binrw::helpers::until_eof;
use binrw::{binread, BinRead}; use binrw::{BinRead, binread};
use std::io::{Cursor, SeekFrom}; use std::io::{Cursor, SeekFrom};
use crate::havok::{HavokAnimationContainer, HavokBinaryTagFileReader};
use crate::ByteSpan; use crate::ByteSpan;
use crate::havok::{HavokAnimationContainer, HavokBinaryTagFileReader};
#[binread] #[binread]
#[br(little)] #[br(little)]

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -90,4 +90,3 @@ pub fn write_data_block_patch<T: Write + Seek>(mut writer: T, data: Vec<u8>) {
data.write(&mut writer).unwrap(); data.write(&mut writer).unwrap();
} }

View file

@ -5,7 +5,7 @@ use std::io::{Cursor, Seek, SeekFrom};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::BinRead; use binrw::BinRead;
use binrw::{binrw, BinReaderExt}; use binrw::{BinReaderExt, binrw};
/// Maximum number of elements in one row /// Maximum number of elements in one row
const MAX_ELEMENTS: usize = 128; const MAX_ELEMENTS: usize = 128;

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]

View file

@ -6,8 +6,8 @@
use std::io::{Cursor, Read, Seek, SeekFrom}; use std::io::{Cursor, Read, Seek, SeekFrom};
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
use bitflags::bitflags; use bitflags::bitflags;
use texture2ddecoder::{decode_bc1, decode_bc3, decode_bc5}; use texture2ddecoder::{decode_bc1, decode_bc3, decode_bc5};

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -4,8 +4,8 @@
use std::io::Cursor; use std::io::Cursor;
use crate::ByteSpan; use crate::ByteSpan;
use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use binrw::binrw;
#[binrw] #[binrw]
#[derive(Debug)] #[derive(Debug)]

View file

@ -3,22 +3,23 @@
use hmac_sha512::Hash; use hmac_sha512::Hash;
use std::env; use std::env;
use std::io::Write;
use std::fs::{read, read_dir}; use std::fs::{read, read_dir};
use std::io::Write;
use std::process::Command; use std::process::Command;
use physis::common::Platform; use physis::common::Platform;
use physis::fiin::FileInfo; use physis::fiin::FileInfo;
use physis::index; use physis::index;
use physis::patch::ZiPatch;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use physis::patch::ZiPatch;
#[cfg(feature = "patch_testing")] #[cfg(feature = "patch_testing")]
fn make_temp_install_dir(name: &str) -> String { fn make_temp_install_dir(name: &str) -> String {
use physis::installer::install_game; use physis::installer::install_game;
let installer_exe = env::var("FFXIV_INSTALLER").expect("$FFXIV_INSTALLER needs to point to the retail installer"); 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(); let mut game_dir = env::home_dir().unwrap();
game_dir.push(name); game_dir.push(name);
@ -92,7 +93,8 @@ fn physis_install_patch(game_directory: &str, data_directory: &str, patch_name:
#[cfg(feature = "patch_testing")] #[cfg(feature = "patch_testing")]
fn xivlauncher_install_patch(game_directory: &str, data_directory: &str, patch_name: &str) { fn xivlauncher_install_patch(game_directory: &str, data_directory: &str, patch_name: &str) {
let patch_dir = env::var("FFXIV_PATCH_DIR").unwrap(); 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 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 patch_path = format!("Z:\\{}\\{}", patch_dir, &patch_name);
let game_dir = format!("Z:\\{}\\{}", game_directory, data_directory); let game_dir = format!("Z:\\{}\\{}", game_directory, data_directory);
@ -110,7 +112,7 @@ fn xivlauncher_install_patch(game_directory: &str, data_directory: &str, patch_n
std::io::stderr().write_all(&output.stderr).unwrap(); std::io::stderr().write_all(&output.stderr).unwrap();
} }
assert!(output. status.success()); assert!(output.status.success());
} }
#[cfg(feature = "patch_testing")] #[cfg(feature = "patch_testing")]
@ -147,13 +149,14 @@ fn test_patching() {
"boot/2024.03.07.0000.0001.patch", "boot/2024.03.07.0000.0001.patch",
"boot/2024.03.21.0000.0001.patch", "boot/2024.03.21.0000.0001.patch",
"boot/2024.04.09.0000.0001.patch", "boot/2024.04.09.0000.0001.patch",
"boot/2024.05.24.0000.0001.patch" "boot/2024.05.24.0000.0001.patch",
]; ];
println!("Now beginning boot patching..."); println!("Now beginning boot patching...");
for patch in boot_patches { 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"); 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() { if !Path::new(&(patch_dir + "/" + patch)).exists() {
println!("Skipping {} because it doesn't exist locally.", patch); println!("Skipping {} because it doesn't exist locally.", patch);
continue; continue;