From 059becf55f5b3cf08136586bb47b49b4eafee079 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 15 Mar 2025 19:34:29 -0400 Subject: [PATCH] Detect when players enter exit zones, and transistion them to the correct place It's a hack and only really works going from New -> Old Gridania, but this does work! It also means Kawari now actually requires a valid game installation, since it has to read layer group information. --- Cargo.lock | 56 ++++++++++-- Cargo.toml | 1 + src/bin/kawari-world.rs | 198 +++++++++++++++++++++++++++++++++++++++- src/config.rs | 4 + src/ipc.rs | 32 +++++++ src/world/init_zone.rs | 2 +- src/world/mod.rs | 3 + src/world/zone.rs | 75 +++++++++++++++ 8 files changed, 363 insertions(+), 8 deletions(-) create mode 100644 src/world/zone.rs diff --git a/Cargo.lock b/Cargo.lock index a89e26a..df44714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -170,6 +170,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -271,6 +277,16 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "half" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "headers" version = "0.3.9" @@ -366,6 +382,7 @@ dependencies = [ "binrw", "md5", "minijinja", + "physis", "rand", "serde", "serde_json", @@ -386,6 +403,15 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libz-rs-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d" +dependencies = [ + "zlib-rs", +] + [[package]] name = "matchit" version = "0.7.3" @@ -450,9 +476,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.0" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "percent-encoding" @@ -460,6 +486,18 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "physis" +version = "0.4.0" +source = "git+https://github.com/redstrate/physis#3273070cef2057b9fe6454d3038a2488484e22fa" +dependencies = [ + "binrw", + "bitflags", + "half", + "libz-rs-sys", + "tracing", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -691,9 +729,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", @@ -904,3 +942,9 @@ dependencies = [ "quote", "syn 2.0.100", ] + +[[package]] +name = "zlib-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05" diff --git a/Cargo.toml b/Cargo.toml index 8417370..49a4036 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,4 @@ rand = "0.8" minijinja = "2.0" binrw = { version = "0.14", features = ["std"], default-features = false } md5 = "0.7.0" +physis = { git = "https://github.com/redstrate/physis" } diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 9dfb94e..c054519 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,5 +1,9 @@ +use std::io::Cursor; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; +use binrw::BinRead; use kawari::client_select_data::ClientCustomizeData; use kawari::ipc::{ActorSetPos, GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData}; use kawari::oodle::FFXIVOodle; @@ -8,7 +12,7 @@ use kawari::packet::{ }; use kawari::world::{ ActorControlSelf, ActorControlType, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position, - UpdateClassInfo, + UpdateClassInfo, Zone, }; use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, ZONE_ID}; use tokio::io::AsyncReadExt; @@ -34,6 +38,10 @@ async fn main() { player_id: None, }; + let zone = Zone::load(010); + + let mut exit_position = None; + tokio::spawn(async move { let mut buf = [0; 2056]; loop { @@ -523,6 +531,8 @@ async fn main() { 0, // left finger 0, // right finger ], + pos: exit_position + .unwrap_or(Position::default()), ..Default::default() }), }; @@ -540,6 +550,36 @@ async fn main() { ) .await; } + + // fade in? + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::PrepareZoning, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::PrepareZoning { + unk: [0, 0, 0, 0], + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Oodle, + ) + .await; + } + + // wipe any exit position so it isn't accidentally reused + exit_position = None; } IPCStructData::Unk1 { .. } => { tracing::info!("Recieved Unk1!"); @@ -720,6 +760,162 @@ async fn main() { IPCStructData::Unk12 { .. } => { tracing::info!("Recieved Unk12!"); } + IPCStructData::EnterZoneLine { + exit_box_id, + position, + .. + } => { + tracing::info!( + "Character entered {exit_box_id} with a position of {position:#?}!" + ); + + // find the exit box id + let (_, exit_box) = + zone.find_exit_box(*exit_box_id).unwrap(); + tracing::info!("exit box: {:#?}", exit_box); + + // find the pop range on the other side + let new_zone = Zone::load(exit_box.territory_type); + let (destination_object, _) = new_zone + .find_pop_range(exit_box.destination_instance_id) + .unwrap(); + + // set the exit position + exit_position = Some(Position { + x: destination_object.transform.translation[0], + y: destination_object.transform.translation[1], + z: destination_object.transform.translation[2], + }); + + // fade out? + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::PrepareZoning, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::PrepareZoning { + unk: [0x01000000, 0, 0, 0], + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Oodle, + ) + .await; + } + + // fade out? x2 + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::PrepareZoning, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::PrepareZoning { + unk: [0, 0x00000085, 0x00030000, 0x000008ff], // last thing is probably a float? + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Oodle, + ) + .await; + } + + tracing::info!( + "sending them to {:#?}", + exit_box.territory_type + ); + + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests/init_zone.bin"); + + let buffer = std::fs::read(d).unwrap(); + let mut buffer = Cursor::new(&buffer); + + let init_zone = InitZone::read_le(&mut buffer).unwrap(); + + // Init Zone + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::InitZone, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::InitZone(InitZone { + server_id: WORLD_ID, + zone_id: exit_box.territory_type, + ..Default::default() + }), + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Oodle, + ) + .await; + } + + // idk + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::PrepareZoning, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::PrepareZoning { + unk: [0x01100000, 0, 0, 0], + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Oodle, + ) + .await; + } + } + IPCStructData::Unk13 { .. } => { + tracing::info!("Recieved Unk13!"); + } + IPCStructData::Unk14 { .. } => { + tracing::info!("Recieved Unk14!"); + } _ => panic!( "The server is recieving a IPC response or unknown packet!" ), diff --git a/src/config.rs b/src/config.rs index b813f01..4993201 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,9 @@ pub struct Config { #[serde(default)] pub boot_patches_location: String, + + #[serde(default)] + pub game_location: String, } impl Default for Config { @@ -22,6 +25,7 @@ impl Default for Config { login_open: false, boot_patches_location: String::new(), supported_platforms: default_supported_platforms(), + game_location: String::new(), } } } diff --git a/src/ipc.rs b/src/ipc.rs index dfa902f..eb62417 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -100,6 +100,15 @@ pub enum IPCOpCode { CharacterCreated = 0xE, // Unknown, client sends this for ??? Unk12 = 0x0E9, + // Sent by the client when the character walks into a zone transistion + EnterZoneLine = 0x205, + // Sent by the client after we sent a InitZone in TravelToZone?? + // TODO: Actually, I don't think is real... + Unk13 = 0x2EE, + // Sent by the server when it wants the client to... prepare to zone? + PrepareZoning = 0x308, + // Sent by the client for unknown reasons + Unk14 = 0x87, } #[binrw] @@ -363,6 +372,23 @@ pub enum IPCStructData { Unk12 { unk: [u8; 8], // TODO: unknown }, + #[br(pre_assert(*magic == IPCOpCode::EnterZoneLine))] + EnterZoneLine { + exit_box_id: u32, + position: Position, + #[brw(pad_after = 4)] // empty + landset_index: i32, + }, + #[br(pre_assert(*magic == IPCOpCode::Unk13))] + Unk13 { + #[br(dbg)] + unk: [u8; 16], // TODO: unknown + }, + #[br(pre_assert(*magic == IPCOpCode::Unk14))] + Unk14 { + #[br(dbg)] + unk: [u8; 8], // TODO: unknown + }, // Server->Client IPC #[br(pre_assert(false))] @@ -511,6 +537,8 @@ pub enum IPCStructData { #[brw(pad_after = 1136)] // empty details: CharacterDetails, }, + #[br(pre_assert(false))] + PrepareZoning { unk: [u32; 4] }, } #[binrw] @@ -578,6 +606,10 @@ impl IPCSegment { IPCStructData::NameRejection { .. } => 536, IPCStructData::CharacterCreated { .. } => 2568, IPCStructData::Unk12 { .. } => todo!(), + IPCStructData::EnterZoneLine { .. } => todo!(), + IPCStructData::Unk13 { .. } => todo!(), + IPCStructData::PrepareZoning { .. } => 16, + IPCStructData::Unk14 { .. } => todo!(), } } } diff --git a/src/world/init_zone.rs b/src/world/init_zone.rs index dfa45bd..447ccfd 100644 --- a/src/world/init_zone.rs +++ b/src/world/init_zone.rs @@ -12,7 +12,7 @@ pub struct InitZone { pub layer_set_id: u32, pub layout_id: u32, #[br(dbg)] - pub weather_id: u16, + pub weather_id: u16, // index into Weather sheet probably? pub unk_really: u16, pub unk_bitmask1: u8, pub unk_bitmask2: u8, diff --git a/src/world/mod.rs b/src/world/mod.rs index 7535a14..2eb6044 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -22,3 +22,6 @@ pub use actor_control_self::ActorControlType; mod init_zone; pub use init_zone::InitZone; + +mod zone; +pub use zone::Zone; diff --git a/src/world/zone.rs b/src/world/zone.rs new file mode 100644 index 0000000..c363e66 --- /dev/null +++ b/src/world/zone.rs @@ -0,0 +1,75 @@ +use physis::{ + common::Platform, + gamedata::GameData, + layer::{ + ExitRangeInstanceObject, InstanceObject, LayerEntryData, LayerGroup, PopRangeInstanceObject, + }, +}; + +use crate::{config::get_config, world::Position}; + +/// Represents a loaded zone +pub struct Zone { + id: u16, + layer_group: LayerGroup, +} + +impl Zone { + pub fn load(id: u16) -> Self { + let config = get_config(); + + let mut game_data = + GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); + let mdl; + println!("loading {id}"); + if id == 133 { + mdl = game_data + .extract("bg/ffxiv/fst_f1/twn/f1t2/level/planmap.lgb") + .unwrap(); + } else { + mdl = game_data + .extract("bg/ffxiv/fst_f1/twn/f1t1/level/planmap.lgb") + .unwrap(); + } + + let layer_group = LayerGroup::from_existing(&mdl).unwrap(); + Self { id, layer_group } + } + + /// Search for an exit box matching an id. + pub fn find_exit_box( + &self, + instance_id: u32, + ) -> Option<(&InstanceObject, &ExitRangeInstanceObject)> { + // TODO: also check position! + for group in &self.layer_group.layers { + for object in &group.objects { + if let LayerEntryData::ExitRange(exit_range) = &object.data { + if object.instance_id == instance_id { + return Some((object, exit_range)); + } + } + } + } + + None + } + + pub fn find_pop_range( + &self, + instance_id: u32, + ) -> Option<(&InstanceObject, &PopRangeInstanceObject)> { + // TODO: also check position! + for group in &self.layer_group.layers { + for object in &group.objects { + if let LayerEntryData::PopRange(pop_range) = &object.data { + if object.instance_id == instance_id { + return Some((object, pop_range)); + } + } + } + } + + None + } +}