1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-10 16:07:45 +00:00

Allow loading unpacked game files, add unpacking mode

This enables you to subsitute game files with your own more easily,
along with running a server with only a limited amount of game
data.
This commit is contained in:
Joshua Goins 2025-07-08 22:39:31 -04:00
parent 0291221dc7
commit d16c2c6583
5 changed files with 98 additions and 12 deletions

2
Cargo.lock generated
View file

@ -1060,7 +1060,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "physis" name = "physis"
version = "0.5.0" version = "0.5.0"
source = "git+https://github.com/redstrate/physis#c2eb47cca0300b9ecaae69473f03539a9e1e4662" source = "git+https://github.com/redstrate/physis#c0d3df99c36e1c3aedc8fd203192df556eddcc29"
dependencies = [ dependencies = [
"binrw", "binrw",
"bitflags", "bitflags",

View file

@ -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: Afterwards, create a `config.yaml` in the current directory. Currently the minimal config you need to run most services looks like this:
```yaml ```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. 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.

View file

@ -38,7 +38,7 @@ fn do_game_version_check(client_version_str: &str) -> bool {
} }
let game_exe_path = [ let game_exe_path = [
config.game_location, config.filesystem.game_path,
MAIN_SEPARATOR_STR.to_string(), MAIN_SEPARATOR_STR.to_string(),
exe_name.to_string(), exe_name.to_string(),
] ]

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
use icarus::Action::ActionSheet; use icarus::Action::ActionSheet;
use icarus::Aetheryte::AetheryteSheet; use icarus::Aetheryte::AetheryteSheet;
use icarus::ClassJob::ClassJobSheet; use icarus::ClassJob::ClassJobSheet;
@ -11,7 +13,10 @@ use icarus::{Tribe::TribeSheet, Warp::WarpSheet};
use physis::common::{Language, Platform}; use physis::common::{Language, Platform};
use physis::exd::{EXD, ExcelRowKind}; use physis::exd::{EXD, ExcelRowKind};
use physis::exh::EXH; 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}; 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 /// Convenient methods built on top of Physis to access data relevant to the server
pub struct GameData { pub struct GameData {
pub resource: SqPackResource, pub resource: ResourceResolver,
pub item_exh: EXH, pub item_exh: EXH,
pub item_pages: Vec<EXD>, pub item_pages: Vec<EXD>,
pub classjob_exp_indexes: Vec<i8>, pub classjob_exp_indexes: Vec<i8>,
@ -57,20 +62,39 @@ impl GameData {
pub fn new() -> Self { pub fn new() -> Self {
let config = get_config(); 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 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() { for (i, _) in item_exh.pages.iter().enumerate() {
item_pages.push( 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 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 // TODO: ids are hardcoded until we have API in Icarus to do this
for i in 0..43 { for i in 0..43 {
let row = sheet.get_row(i).unwrap(); let row = sheet.get_row(i).unwrap();
@ -79,7 +103,7 @@ impl GameData {
} }
Self { Self {
resource: game_data, resource: resource_resolver,
item_exh, item_exh,
item_pages, item_pages,
classjob_exp_indexes, classjob_exp_indexes,
@ -406,3 +430,46 @@ pub enum TerritoryNameKind {
Region, Region,
Place, 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<physis::ByteBuffer> {
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)
}
}

View file

@ -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<String>,
/// 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. /// Global and all-encompassing config.
/// Settings that affect all servers belong here. /// Settings that affect all servers belong here.
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -367,7 +385,7 @@ pub struct Config {
pub supported_platforms: Vec<String>, pub supported_platforms: Vec<String>,
#[serde(default)] #[serde(default)]
pub game_location: String, pub filesystem: FilesystemConfig,
#[serde(default)] #[serde(default)]
pub admin: AdminConfig, pub admin: AdminConfig,
@ -409,7 +427,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
supported_platforms: default_supported_platforms(), supported_platforms: default_supported_platforms(),
game_location: String::new(), filesystem: FilesystemConfig::default(),
admin: AdminConfig::default(), admin: AdminConfig::default(),
frontier: FrontierConfig::default(), frontier: FrontierConfig::default(),
lobby: LobbyConfig::default(), lobby: LobbyConfig::default(),