2025-07-08 21:37:03 -04:00
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
fs::{self, DirEntry, ReadDir},
|
|
|
|
path::PathBuf,
|
|
|
|
};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
ByteBuffer,
|
2025-07-08 21:44:51 -04:00
|
|
|
common::{Platform, read_version},
|
2025-07-08 21:37:03 -04:00
|
|
|
patch::{PatchError, ZiPatch},
|
|
|
|
repository::{Category, Repository, string_to_category},
|
|
|
|
sqpack::{IndexEntry, SqPackData, SqPackIndex},
|
|
|
|
};
|
|
|
|
|
|
|
|
use super::Resource;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2023-12-02 20:12:12 -05:00
|
|
|
/// Possible actions to repair game files
|
2022-10-25 11:03:05 -04:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum RepairAction {
|
2023-12-02 20:12:12 -05:00
|
|
|
/// Indicates a version file is missing for a repository
|
2022-10-25 11:03:05 -04:00
|
|
|
VersionFileMissing,
|
2023-12-02 20:12:12 -05:00
|
|
|
/// The version file is missing, but it can be restored via a backup
|
2022-10-25 13:02:06 -04:00
|
|
|
VersionFileCanRestore,
|
2022-10-25 11:03:05 -04:00
|
|
|
}
|
|
|
|
|
2023-03-31 17:30:08 -04:00
|
|
|
#[derive(Debug)]
|
2023-12-02 20:12:12 -05:00
|
|
|
/// Possible errors emitted through the repair process
|
2022-10-25 11:03:05 -04:00
|
|
|
pub enum RepairError<'a> {
|
2023-12-02 20:12:12 -05:00
|
|
|
/// Failed to repair a repository
|
2022-10-25 13:02:06 -04:00
|
|
|
FailedRepair(&'a Repository),
|
2022-10-25 11:03:05 -04:00
|
|
|
}
|
|
|
|
|
2025-07-08 21:37:03 -04:00
|
|
|
/// Used to read files from the retail game, in their SqPack-compressed format.
|
|
|
|
pub struct SqPackResource {
|
|
|
|
/// The game directory to operate on.
|
|
|
|
pub game_directory: String,
|
|
|
|
|
|
|
|
/// Repositories in the game directory.
|
|
|
|
pub repositories: Vec<Repository>,
|
|
|
|
|
|
|
|
index_files: HashMap<String, SqPackIndex>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SqPackResource {
|
|
|
|
pub fn from_existing(platform: Platform, directory: &str) -> Self {
|
2022-07-19 19:29:41 -04:00
|
|
|
match is_valid(directory) {
|
2024-04-15 19:40:34 -04:00
|
|
|
true => {
|
|
|
|
let mut data = Self {
|
|
|
|
game_directory: String::from(directory),
|
|
|
|
repositories: vec![],
|
|
|
|
index_files: HashMap::new(),
|
|
|
|
};
|
|
|
|
data.reload_repositories(platform);
|
2025-05-05 17:07:49 -04:00
|
|
|
data
|
2024-04-15 19:40:34 -04:00
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
false => {
|
2025-05-09 15:16:23 -04:00
|
|
|
// Game data is not valid! Treating it as a new install...
|
2025-05-05 17:07:49 -04:00
|
|
|
Self {
|
|
|
|
game_directory: String::from(directory),
|
|
|
|
repositories: vec![],
|
|
|
|
index_files: HashMap::new(),
|
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-20 13:18:03 -04:00
|
|
|
|
2024-04-15 19:40:34 -04:00
|
|
|
fn reload_repositories(&mut self, platform: Platform) {
|
2022-07-19 19:29:41 -04:00
|
|
|
self.repositories.clear();
|
|
|
|
|
|
|
|
let mut d = PathBuf::from(self.game_directory.as_str());
|
2023-05-13 17:30:15 -04:00
|
|
|
|
|
|
|
// add initial ffxiv directory
|
2024-04-20 13:18:03 -04:00
|
|
|
if let Some(base_repository) =
|
|
|
|
Repository::from_existing_base(platform.clone(), d.to_str().unwrap())
|
|
|
|
{
|
2023-05-13 17:30:15 -04:00
|
|
|
self.repositories.push(base_repository);
|
|
|
|
}
|
|
|
|
|
|
|
|
// add expansions
|
2022-07-19 19:29:41 -04:00
|
|
|
d.push("sqpack");
|
|
|
|
|
2023-05-13 17:30:15 -04:00
|
|
|
if let Ok(repository_paths) = fs::read_dir(d.as_path()) {
|
2024-04-20 13:18:03 -04:00
|
|
|
let repository_paths: ReadDir = repository_paths;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2024-04-20 13:18:03 -04:00
|
|
|
let repository_paths: Vec<DirEntry> = repository_paths
|
2023-05-13 17:30:15 -04:00
|
|
|
.filter_map(Result::ok)
|
|
|
|
.filter(|s| s.file_type().unwrap().is_dir())
|
|
|
|
.collect();
|
2023-04-06 14:51:40 -04:00
|
|
|
|
2023-05-13 17:30:15 -04:00
|
|
|
for repository_path in repository_paths {
|
2024-04-20 13:18:03 -04:00
|
|
|
if let Some(expansion_repository) = Repository::from_existing_expansion(
|
|
|
|
platform.clone(),
|
|
|
|
repository_path.path().to_str().unwrap(),
|
|
|
|
) {
|
2023-05-13 17:30:15 -04:00
|
|
|
self.repositories.push(expansion_repository);
|
|
|
|
}
|
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
self.repositories.sort();
|
|
|
|
}
|
|
|
|
|
2025-03-11 17:15:10 -04:00
|
|
|
fn get_dat_file(&self, path: &str, chunk: u8, data_file_id: u32) -> Option<SqPackData> {
|
2022-08-09 20:03:18 -04:00
|
|
|
let (repository, category) = self.parse_repository_category(path).unwrap();
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let dat_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
2022-08-09 20:03:18 -04:00
|
|
|
"sqpack".to_string(),
|
|
|
|
repository.name.clone(),
|
2024-05-04 14:20:45 -04:00
|
|
|
repository.dat_filename(chunk, category, data_file_id),
|
2022-08-16 11:52:07 -04:00
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-08-09 20:03:18 -04:00
|
|
|
|
2025-03-11 17:15:10 -04:00
|
|
|
SqPackData::from_existing(dat_path.to_str()?)
|
2022-08-09 20:03:18 -04:00
|
|
|
}
|
|
|
|
|
2024-05-25 10:47:48 -04:00
|
|
|
/// Finds the offset inside of the DAT file for `path`.
|
2024-06-28 05:53:42 -04:00
|
|
|
pub fn find_offset(&mut self, path: &str) -> Option<u64> {
|
2024-05-25 10:47:48 -04:00
|
|
|
let slice = self.find_entry(path);
|
2024-06-28 06:10:37 -04:00
|
|
|
slice.map(|(entry, _)| entry.offset)
|
2024-05-25 10:47:48 -04:00
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
/// Parses a path structure and spits out the corresponding category and repository.
|
|
|
|
fn parse_repository_category(&self, path: &str) -> Option<(&Repository, Category)> {
|
2025-05-05 17:19:39 -04:00
|
|
|
if self.repositories.is_empty() {
|
2025-05-05 17:56:14 -04:00
|
|
|
return None;
|
2025-05-05 17:19:39 -04:00
|
|
|
}
|
|
|
|
|
2025-06-21 09:40:29 -04:00
|
|
|
let tokens: Vec<&str> = path.split('/').collect();
|
2024-05-04 14:20:45 -04:00
|
|
|
|
2025-06-21 09:40:29 -04:00
|
|
|
// Search for expansions
|
|
|
|
let repository_token = tokens[1];
|
2022-07-19 19:29:41 -04:00
|
|
|
for repository in &self.repositories {
|
|
|
|
if repository.name == repository_token {
|
2025-06-21 09:40:29 -04:00
|
|
|
return Some((repository, string_to_category(tokens[0])?));
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-21 09:40:29 -04:00
|
|
|
// Fallback to ffxiv
|
|
|
|
Some((&self.repositories[0], string_to_category(tokens[0])?))
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
2022-07-21 19:58:58 -04:00
|
|
|
|
2025-03-06 18:52:26 -05:00
|
|
|
fn get_index_filenames(&self, path: &str) -> Option<Vec<(String, u8)>> {
|
2024-04-29 19:09:51 -04:00
|
|
|
let (repository, category) = self.parse_repository_category(path)?;
|
2023-10-13 16:58:08 -04:00
|
|
|
|
2025-03-06 18:52:26 -05:00
|
|
|
let mut index_filenames = vec![];
|
2023-10-13 16:58:08 -04:00
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
for chunk in 0..255 {
|
|
|
|
let index_path: PathBuf = [
|
|
|
|
&self.game_directory,
|
|
|
|
"sqpack",
|
|
|
|
&repository.name,
|
|
|
|
&repository.index_filename(chunk, category),
|
|
|
|
]
|
2024-05-18 09:42:07 -04:00
|
|
|
.iter()
|
|
|
|
.collect();
|
2024-05-04 14:20:45 -04:00
|
|
|
|
2025-03-06 18:52:26 -05:00
|
|
|
index_filenames.push((index_path.into_os_string().into_string().unwrap(), chunk));
|
2024-05-04 14:20:45 -04:00
|
|
|
|
|
|
|
let index2_path: PathBuf = [
|
|
|
|
&self.game_directory,
|
|
|
|
"sqpack",
|
|
|
|
&repository.name,
|
|
|
|
&repository.index2_filename(chunk, category),
|
|
|
|
]
|
2024-05-18 09:42:07 -04:00
|
|
|
.iter()
|
|
|
|
.collect();
|
2024-05-04 14:20:45 -04:00
|
|
|
|
2025-03-06 18:52:26 -05:00
|
|
|
index_filenames.push((index2_path.into_os_string().into_string().unwrap(), chunk));
|
2024-05-04 14:20:45 -04:00
|
|
|
}
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2025-03-06 18:52:26 -05:00
|
|
|
Some(index_filenames)
|
2023-10-13 16:58:08 -04:00
|
|
|
}
|
|
|
|
|
2023-12-02 20:12:12 -05:00
|
|
|
/// Applies the patch to game data and returns any errors it encounters. This function will not update the version in the GameData struct.
|
2022-08-16 11:52:07 -04:00
|
|
|
pub fn apply_patch(&self, patch_path: &str) -> Result<(), PatchError> {
|
2024-06-29 12:47:00 -04:00
|
|
|
ZiPatch::apply(&self.game_directory, patch_path)
|
2022-08-09 21:51:52 -04:00
|
|
|
}
|
2022-10-25 11:03:05 -04:00
|
|
|
|
|
|
|
/// Detects whether or not the game files need a repair, right now it only checks for invalid
|
|
|
|
/// version files.
|
|
|
|
/// If the repair is needed, a list of invalid repositories is given.
|
|
|
|
pub fn needs_repair(&self) -> Option<Vec<(&Repository, RepairAction)>> {
|
2022-10-25 13:02:06 -04:00
|
|
|
let mut repositories: Vec<(&Repository, RepairAction)> = Vec::new();
|
2022-10-25 11:03:05 -04:00
|
|
|
for repository in &self.repositories {
|
|
|
|
if repository.version.is_none() {
|
|
|
|
// Check to see if a .bck file is created, as we might be able to use that
|
|
|
|
let ver_bak_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
|
|
|
"sqpack".to_string(),
|
|
|
|
repository.name.clone(),
|
|
|
|
format!("{}.bck", repository.name),
|
|
|
|
]
|
2022-10-25 13:02:06 -04:00
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-10-25 11:03:05 -04:00
|
|
|
|
|
|
|
let repair_action = if read_version(&ver_bak_path).is_some() {
|
|
|
|
RepairAction::VersionFileCanRestore
|
|
|
|
} else {
|
|
|
|
RepairAction::VersionFileMissing
|
|
|
|
};
|
|
|
|
|
|
|
|
repositories.push((repository, repair_action));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if repositories.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(repositories)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Performs the repair, assuming any damaging effects it may have
|
|
|
|
/// Returns true only if all actions were taken are successful.
|
|
|
|
/// NOTE: This is a destructive operation, especially for InvalidVersion errors.
|
2022-10-25 13:02:06 -04:00
|
|
|
pub fn perform_repair<'a>(
|
|
|
|
&self,
|
|
|
|
repositories: &Vec<(&'a Repository, RepairAction)>,
|
|
|
|
) -> Result<(), RepairError<'a>> {
|
2022-10-25 11:03:05 -04:00
|
|
|
for (repository, action) in repositories {
|
|
|
|
let ver_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
|
|
|
"sqpack".to_string(),
|
|
|
|
repository.name.clone(),
|
|
|
|
format!("{}.ver", repository.name),
|
|
|
|
]
|
2022-10-25 13:02:06 -04:00
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-10-25 11:03:05 -04:00
|
|
|
|
2022-10-25 13:02:06 -04:00
|
|
|
let new_version: String = match action {
|
2022-10-25 11:03:05 -04:00
|
|
|
RepairAction::VersionFileMissing => {
|
|
|
|
let repo_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
|
|
|
"sqpack".to_string(),
|
2022-10-25 13:02:06 -04:00
|
|
|
repository.name.clone(),
|
2022-10-25 11:03:05 -04:00
|
|
|
]
|
2022-10-25 13:02:06 -04:00
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-10-25 11:03:05 -04:00
|
|
|
|
2022-12-17 08:23:19 -05:00
|
|
|
fs::remove_dir_all(&repo_path)
|
2022-10-25 13:02:06 -04:00
|
|
|
.ok()
|
2022-10-25 11:03:05 -04:00
|
|
|
.ok_or(RepairError::FailedRepair(repository))?;
|
|
|
|
|
2022-12-17 08:23:19 -05:00
|
|
|
fs::create_dir_all(&repo_path)
|
2022-10-25 13:02:06 -04:00
|
|
|
.ok()
|
2022-10-25 11:03:05 -04:00
|
|
|
.ok_or(RepairError::FailedRepair(repository))?;
|
|
|
|
|
2022-10-25 13:02:06 -04:00
|
|
|
"2012.01.01.0000.0000".to_string() // TODO: is this correct for expansions?
|
2022-10-25 11:03:05 -04:00
|
|
|
}
|
|
|
|
RepairAction::VersionFileCanRestore => {
|
|
|
|
let ver_bak_path: PathBuf = [
|
|
|
|
self.game_directory.clone(),
|
|
|
|
"sqpack".to_string(),
|
|
|
|
repository.name.clone(),
|
|
|
|
format!("{}.bck", repository.name),
|
|
|
|
]
|
2022-10-25 13:02:06 -04:00
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-10-25 11:03:05 -04:00
|
|
|
|
2022-10-25 13:02:06 -04:00
|
|
|
read_version(&ver_bak_path).ok_or(RepairError::FailedRepair(repository))?
|
2022-10-25 11:03:05 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-02-20 16:07:48 -05:00
|
|
|
fs::write(ver_path, new_version)
|
2022-10-25 13:02:06 -04:00
|
|
|
.ok()
|
2022-10-25 11:03:05 -04:00
|
|
|
.ok_or(RepairError::FailedRepair(repository))?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-10-13 16:58:08 -04:00
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
fn cache_index_file(&mut self, filename: &str) {
|
|
|
|
if !self.index_files.contains_key(filename) {
|
2025-03-11 17:15:10 -04:00
|
|
|
if let Some(index_file) = SqPackIndex::from_existing(filename) {
|
2024-05-04 14:20:45 -04:00
|
|
|
self.index_files.insert(filename.to_string(), index_file);
|
2024-04-14 11:24:18 -04:00
|
|
|
}
|
|
|
|
}
|
2024-05-04 14:20:45 -04:00
|
|
|
}
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2025-03-11 17:15:10 -04:00
|
|
|
fn get_index_file(&self, filename: &str) -> Option<&SqPackIndex> {
|
2023-10-13 16:58:08 -04:00
|
|
|
self.index_files.get(filename)
|
|
|
|
}
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
fn find_entry(&mut self, path: &str) -> Option<(IndexEntry, u8)> {
|
2025-03-06 18:52:26 -05:00
|
|
|
let index_paths = self.get_index_filenames(path)?;
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
for (index_path, chunk) in index_paths {
|
|
|
|
self.cache_index_file(&index_path);
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
if let Some(index_file) = self.get_index_file(&index_path) {
|
|
|
|
if let Some(entry) = index_file.find_entry(path) {
|
|
|
|
return Some((entry, chunk));
|
|
|
|
}
|
2024-04-14 11:24:18 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2025-07-08 21:37:03 -04:00
|
|
|
impl Resource for SqPackResource {
|
|
|
|
fn read(&mut self, path: &str) -> Option<ByteBuffer> {
|
|
|
|
let slice = self.find_entry(path);
|
|
|
|
match slice {
|
|
|
|
Some((entry, chunk)) => {
|
|
|
|
let mut dat_file = self.get_dat_file(path, chunk, entry.data_file_id.into())?;
|
|
|
|
|
|
|
|
dat_file.read_from_offset(entry.offset)
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn exists(&mut self, path: &str) -> bool {
|
|
|
|
let Some(_) = self.get_index_filenames(path) else {
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
self.find_entry(path).is_some()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_valid(path: &str) -> bool {
|
|
|
|
let d = PathBuf::from(path);
|
|
|
|
|
|
|
|
if fs::metadata(d.as_path()).is_err() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2025-06-21 09:40:29 -04:00
|
|
|
use crate::repository::Category::*;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2023-08-06 08:25:04 -04:00
|
|
|
use super::*;
|
|
|
|
|
2025-07-08 21:37:03 -04:00
|
|
|
fn common_setup_data() -> SqPackResource {
|
2022-07-19 19:29:41 -04:00
|
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
d.push("resources/tests");
|
|
|
|
d.push("valid_sqpack");
|
|
|
|
d.push("game");
|
|
|
|
|
2025-07-08 21:37:03 -04:00
|
|
|
SqPackResource::from_existing(Platform::Win32, d.to_str().unwrap())
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn repository_ordering() {
|
2024-04-16 21:19:08 -04:00
|
|
|
let data = common_setup_data();
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
assert_eq!(data.repositories[0].name, "ffxiv");
|
|
|
|
assert_eq!(data.repositories[1].name, "ex1");
|
|
|
|
assert_eq!(data.repositories[2].name, "ex2");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn repository_and_category_parsing() {
|
2024-04-16 21:19:08 -04:00
|
|
|
let data = common_setup_data();
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2025-06-21 09:40:29 -04:00
|
|
|
// fallback to ffxiv
|
2022-08-16 11:52:07 -04:00
|
|
|
assert_eq!(
|
|
|
|
data.parse_repository_category("exd/root.exl").unwrap(),
|
|
|
|
(&data.repositories[0], EXD)
|
|
|
|
);
|
2025-06-21 09:40:29 -04:00
|
|
|
// ex1
|
|
|
|
assert_eq!(
|
|
|
|
data.parse_repository_category("bg/ex1/01_roc_r2/twn/r2t1/level/planevent.lgb")
|
|
|
|
.unwrap(),
|
|
|
|
(&data.repositories[1], Background)
|
|
|
|
);
|
|
|
|
// ex2
|
|
|
|
assert_eq!(
|
|
|
|
data.parse_repository_category("bg/ex2/01_gyr_g3/fld/g3fb/level/planner.lgb")
|
|
|
|
.unwrap(),
|
|
|
|
(&data.repositories[2], Background)
|
|
|
|
);
|
|
|
|
// invalid but should still parse I guess
|
2025-03-06 18:52:26 -05:00
|
|
|
assert!(
|
|
|
|
data.parse_repository_category("what/some_font.dat")
|
|
|
|
.is_none()
|
|
|
|
);
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|