diff --git a/.gitignore b/.gitignore index e7e600c..f605e46 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ config.json *.db config.yaml src/opcodes.rs +backups/ diff --git a/Cargo.lock b/Cargo.lock index c3c9f25..dd6883f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "array-init" version = "2.1.0" @@ -141,18 +150,49 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "bytemuck" version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.17" @@ -168,12 +208,53 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "either" version = "1.15.0" @@ -214,6 +295,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -431,6 +522,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "zip", ] [[package]] @@ -481,6 +573,18 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + [[package]] name = "lua-src" version = "547.0.0" @@ -500,6 +604,16 @@ dependencies = [ "which", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "matchit" version = "0.8.4" @@ -875,6 +989,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -1184,8 +1304,39 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zip" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88" +dependencies = [ + "arbitrary", + "bzip2", + "crc32fast", + "crossbeam-utils", + "flate2", + "indexmap", + "lzma-rs", + "memchr", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05" + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 2deba1d..c4fdc57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,3 +87,6 @@ bitflags = { version = "1.3", default-features = false } # For server-side scripting mlua = { version = "0.10", features = ["lua51", "vendored", "send", "async"], default-features = false } + +# For character backup decompression +zip = { version = "2.2", features = ["deflate", "lzma", "bzip2"], default-features = false } diff --git a/USAGE.md b/USAGE.md index 161cfd4..9292a48 100644 --- a/USAGE.md +++ b/USAGE.md @@ -72,6 +72,12 @@ In this example, lobby number 4 will replace the **Aether data center**, but the Some other launchers (like XIVLauncher) will allow you to specify these extra arguments, but they will still authenticate to the retail servers. You can still connect to Kawari with this way, but **make sure to specify your own session ID, or your retail account's session ID will be sent to the lobby server**! +## Importing characters from retail + +It's possible to import existing characters from the retail server using [Auracite](https://auracite.xiv.zone). Place the backup ZIP under `backups/` and the World server will import it into the database the next time it's started. + +This feature is still a work-in-progress, and not all data is imported yet. + ## Chat commands ### Debug commands diff --git a/src/common/customize_data.rs b/src/common/customize_data.rs index 76ac0da..5983c69 100644 --- a/src/common/customize_data.rs +++ b/src/common/customize_data.rs @@ -32,6 +32,39 @@ pub struct CustomizeData { pub face_paint_color: u8, } +impl From for CustomizeData { + fn from(value: physis::chardat::CustomizeData) -> Self { + Self { + race: value.race as u8 + 1, + gender: value.gender as u8, + age: value.age, + height: value.height, + subrace: value.subrace as u8 + 1, + face: value.face, + hair: value.hair, + enable_highlights: value.enable_highlights as u8, + skin_tone: value.skin_tone, + right_eye_color: value.right_eye_color, + hair_tone: value.hair_tone, + highlights: value.highlights, + facial_features: value.facial_features, + facial_feature_color: value.facial_feature_color, + eyebrows: value.eyebrows, + left_eye_color: value.left_eye_color, + eyes: value.eyes, + nose: value.nose, + jaw: value.jaw, + mouth: value.mouth, + lips_tone_fur_pattern: value.lips_tone_fur_pattern, + race_feature_size: value.race_feature_size, + race_feature_type: value.race_feature_type, + bust: value.bust, + face_paint: value.face_paint, + face_paint_color: value.face_paint_color, + } + } +} + impl CustomizeData { pub fn to_json(&self) -> Value { json!([ diff --git a/src/lobby/chara_make.rs b/src/lobby/chara_make.rs index a0daa50..b891e62 100644 --- a/src/lobby/chara_make.rs +++ b/src/lobby/chara_make.rs @@ -32,12 +32,12 @@ impl CharaMake { pub fn to_json(&self) -> String { let content = json!([ self.customize.to_json(), - self.unk1, - self.guardian, - self.birth_month, - self.birth_day, - self.classjob_id, - self.unk2, + self.unk1.to_string(), + self.guardian.to_string(), + self.birth_month.to_string(), + self.birth_day.to_string(), + self.classjob_id.to_string(), + self.unk2.to_string(), ]); let obj = json!({ diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 689798b..a562d74 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -591,13 +591,16 @@ pub async fn send_custom_world_packet(segment: CustomIpcSegment) -> Option(&buf[..n], &mut packet_state).await; - let (segments, _) = parse_packet::(&buf[..n], &mut packet_state).await; - - match &segments[0].segment_type { - SegmentType::CustomIpc { data } => Some(data.clone()), - _ => None, + return match &segments[0].segment_type { + SegmentType::CustomIpc { data } => Some(data.clone()), + _ => None, + }; } + + None } diff --git a/src/world/database.rs b/src/world/database.rs index e671c9a..6e1bd55 100644 --- a/src/world/database.rs +++ b/src/world/database.rs @@ -1,9 +1,10 @@ -use std::sync::Mutex; +use std::{io::Read, sync::Mutex}; use rusqlite::Connection; +use serde::Deserialize; use crate::{ - common::Position, + common::{CustomizeData, Position}, lobby::{ CharaMake, ClientSelectData, RemakeMode, ipc::{CharacterDetails, CharacterFlag}, @@ -46,9 +47,73 @@ impl WorldDatabase { connection.execute(query, ()).unwrap(); } - Self { + let this = Self { connection: Mutex::new(connection), + }; + + // Import any backups + // NOTE: This won't make sense when service accounts are a real thing, so the functionality will probably be moved + { + if let Ok(paths) = std::fs::read_dir("./backups") { + for path in paths { + let path = path.unwrap().path(); + if path.extension().unwrap() == "zip" { + this.import_character(path.to_str().unwrap()); + } + } + } } + + this + } + + fn import_character(&self, path: &str) { + tracing::info!("Importing character backup {path}..."); + + let file = std::fs::File::open(path).unwrap(); + + let mut archive = zip::ZipArchive::new(file).unwrap(); + + #[derive(Deserialize)] + struct CharacterJson { + name: String, + } + + let character: CharacterJson; + { + let mut character_file = archive.by_name("character.json").unwrap(); + + let mut json_string = String::new(); + character_file.read_to_string(&mut json_string).unwrap(); + + character = serde_json::from_str(&json_string).unwrap(); + } + + if !self.check_is_name_free(&character.name) { + tracing::warn!("* Skipping since this character already exists."); + return; + } + + let charsave_file = archive.by_name("FFXIV_CHARA_01.dat").unwrap(); + let charsave_bytes: Vec = charsave_file.bytes().map(|x| x.unwrap()).collect(); + let charsave = physis::chardat::CharacterData::from_existing(&charsave_bytes).unwrap(); + + let customize = CustomizeData::from(charsave.customize); + + let chara_make = CharaMake { + customize, + unk1: 73, + guardian: 1, // TODO: extract these as well + birth_month: 1, + birth_day: 1, + classjob_id: 5, + unk2: 1, + }; + + // TODO: extract city-state + self.create_player_data(&character.name, &chara_make.to_json(), 2, 132); + + tracing::info!("{} added to the world!", character.name); } pub fn find_player_data(&self, actor_id: u32) -> PlayerData {