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:
parent
0291221dc7
commit
d16c2c6583
5 changed files with 98 additions and 12 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
3
USAGE.md
3
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:
|
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.
|
||||||
|
|
|
@ -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(),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue