1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-09 15:37: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]]
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",

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:
```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.

View file

@ -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(),
]

View file

@ -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<EXD>,
pub classjob_exp_indexes: Vec<i8>,
@ -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<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.
/// Settings that affect all servers belong here.
#[derive(Serialize, Deserialize)]
@ -367,7 +385,7 @@ pub struct Config {
pub supported_platforms: Vec<String>,
#[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(),