diff --git a/Cargo.lock b/Cargo.lock index 735616f..d633fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,7 +1060,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "physis" version = "0.5.0" -source = "git+https://github.com/redstrate/physis#c2eb47cca0300b9ecaae69473f03539a9e1e4662" +source = "git+https://github.com/redstrate/physis#c0d3df99c36e1c3aedc8fd203192df556eddcc29" dependencies = [ "binrw", "bitflags", diff --git a/USAGE.md b/USAGE.md index c6f0543..fcfab62 100644 --- a/USAGE.md +++ b/USAGE.md @@ -33,7 +33,8 @@ For the World server to function, Kawari needs to be built with `--features oodl Afterwards, create a `config.yaml` in the current directory. Currently the minimal config you need to run most services looks like this: ```yaml -game_location: /path/to/gamedir/ +filesystem: + game_path: "C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game" ``` More configuration options can be found in `config.rs`, such as changing the ports services run on. If you plan on just running it locally for yourself, you don't need to set anything else. diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 0d1ab95..e7d2a62 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -38,7 +38,7 @@ fn do_game_version_check(client_version_str: &str) -> bool { } let game_exe_path = [ - config.game_location, + config.filesystem.game_path, MAIN_SEPARATOR_STR.to_string(), exe_name.to_string(), ] diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index 5f8100f..c51b8b9 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use icarus::Action::ActionSheet; use icarus::Aetheryte::AetheryteSheet; use icarus::ClassJob::ClassJobSheet; @@ -11,7 +13,10 @@ use icarus::{Tribe::TribeSheet, Warp::WarpSheet}; use physis::common::{Language, Platform}; use physis::exd::{EXD, ExcelRowKind}; use physis::exh::EXH; -use physis::resource::{SqPackResource, read_excel_sheet, read_excel_sheet_header}; +use physis::resource::{ + Resource, ResourceResolver, SqPackResource, UnpackedResource, read_excel_sheet, + read_excel_sheet_header, +}; use crate::{common::Attributes, config::get_config}; @@ -19,7 +24,7 @@ use super::timestamp_secs; /// Convenient methods built on top of Physis to access data relevant to the server pub struct GameData { - pub resource: SqPackResource, + pub resource: ResourceResolver, pub item_exh: EXH, pub item_pages: Vec, pub classjob_exp_indexes: Vec, @@ -57,20 +62,39 @@ impl GameData { pub fn new() -> Self { let config = get_config(); - let mut game_data = SqPackResource::from_existing(Platform::Win32, &config.game_location); + // setup resolvers + let sqpack_resource = SqPackResourceSpy::from( + SqPackResource::from_existing(Platform::Win32, &config.filesystem.game_path), + &config.filesystem.unpack_path, + ); + let mut resource_resolver = ResourceResolver::new(); + for path in config.filesystem.additional_search_paths { + let unpacked_resource = UnpackedResource::from_existing(&path); + resource_resolver.add_source(Box::new(unpacked_resource)); + } + resource_resolver.add_source(Box::new(sqpack_resource)); let mut item_pages = Vec::new(); - let item_exh = read_excel_sheet_header(&mut game_data, "Item").unwrap(); + let item_exh = read_excel_sheet_header(&mut resource_resolver, "Item") + .expect("Failed to read Item EXH, does the file exist?"); for (i, _) in item_exh.pages.iter().enumerate() { item_pages.push( - read_excel_sheet(&mut game_data, "Item", &item_exh, Language::English, i).unwrap(), + read_excel_sheet( + &mut resource_resolver, + "Item", + &item_exh, + Language::English, + i, + ) + .expect("Failed to read Item EXD, does the file exist?"), ); } let mut classjob_exp_indexes = Vec::new(); - let sheet = ClassJobSheet::read_from(&mut game_data, Language::English).unwrap(); + let sheet = ClassJobSheet::read_from(&mut resource_resolver, Language::English) + .expect("Failed to read ClassJobSheet, does the Excel files exist?"); // TODO: ids are hardcoded until we have API in Icarus to do this for i in 0..43 { let row = sheet.get_row(i).unwrap(); @@ -79,7 +103,7 @@ impl GameData { } Self { - resource: game_data, + resource: resource_resolver, item_exh, item_pages, classjob_exp_indexes, @@ -406,3 +430,46 @@ pub enum TerritoryNameKind { Region, Place, } + +/// Wrapper around SqPackResource to let us spy when it reads files +struct SqPackResourceSpy { + sqpack_resource: SqPackResource, + output_directory: String, +} + +impl SqPackResourceSpy { + pub fn from(sqpack_resource: SqPackResource, output_directory: &str) -> Self { + Self { + sqpack_resource, + output_directory: output_directory.to_string(), + } + } +} + +impl Resource for SqPackResourceSpy { + fn read(&mut self, path: &str) -> Option { + if let Some(buffer) = self.sqpack_resource.read(path) { + let mut new_path = PathBuf::from(&self.output_directory); + new_path.push(path.to_lowercase()); + + if !std::fs::exists(&new_path).unwrap_or_default() { + // create directory if it doesn't exist' + let parent_directory = new_path.parent().unwrap(); + if !std::fs::exists(parent_directory).unwrap_or_default() { + std::fs::create_dir_all(parent_directory) + .expect("Couldn't create directory for extraction?!"); + } + + std::fs::write(new_path, &buffer).expect("Couldn't extract file!!"); + } + + return Some(buffer); + } + + None + } + + fn exists(&mut self, path: &str) -> bool { + self.sqpack_resource.exists(path) + } +} diff --git a/src/config.rs b/src/config.rs index e6a35fa..ac6148c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -359,6 +359,24 @@ impl SaveDataBankConfig { } } +/// Configuration for the game filesystem. +#[derive(Serialize, Deserialize, Default)] +pub struct FilesystemConfig { + /// Path to the game directory. For example, "C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game". + #[serde(default)] + pub game_path: String, + + /// Additional search paths for *unpacked game files*. + /// These are ordered from highest-to-lowest, these are always preferred over retail game files. + #[serde(default)] + pub additional_search_paths: Vec, + + /// Unpack used files to the specified directory. + /// If the directory is not specified, Kawari won't save file contents. + #[serde(default)] + pub unpack_path: String, +} + /// Global and all-encompassing config. /// Settings that affect all servers belong here. #[derive(Serialize, Deserialize)] @@ -367,7 +385,7 @@ pub struct Config { pub supported_platforms: Vec, #[serde(default)] - pub game_location: String, + pub filesystem: FilesystemConfig, #[serde(default)] pub admin: AdminConfig, @@ -409,7 +427,7 @@ impl Default for Config { fn default() -> Self { Self { supported_platforms: default_supported_platforms(), - game_location: String::new(), + filesystem: FilesystemConfig::default(), admin: AdminConfig::default(), frontier: FrontierConfig::default(), lobby: LobbyConfig::default(),