1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-20 06:37:45 +00:00

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.
This commit is contained in:
Joshua Goins 2025-03-15 19:34:29 -04:00
parent dbe1ef208c
commit 059becf55f
8 changed files with 363 additions and 8 deletions

56
Cargo.lock generated
View file

@ -25,9 +25,9 @@ checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.87" version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -170,6 +170,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -271,6 +277,16 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 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]] [[package]]
name = "headers" name = "headers"
version = "0.3.9" version = "0.3.9"
@ -366,6 +382,7 @@ dependencies = [
"binrw", "binrw",
"md5", "md5",
"minijinja", "minijinja",
"physis",
"rand", "rand",
"serde", "serde",
"serde_json", "serde_json",
@ -386,6 +403,15 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 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]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@ -450,9 +476,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.0" version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
@ -460,6 +486,18 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 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]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.10" version = "1.1.10"
@ -691,9 +729,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.0" version = "1.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -904,3 +942,9 @@ dependencies = [
"quote", "quote",
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "zlib-rs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"

View file

@ -47,3 +47,4 @@ rand = "0.8"
minijinja = "2.0" minijinja = "2.0"
binrw = { version = "0.14", features = ["std"], default-features = false } binrw = { version = "0.14", features = ["std"], default-features = false }
md5 = "0.7.0" md5 = "0.7.0"
physis = { git = "https://github.com/redstrate/physis" }

View file

@ -1,5 +1,9 @@
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use binrw::BinRead;
use kawari::client_select_data::ClientCustomizeData; use kawari::client_select_data::ClientCustomizeData;
use kawari::ipc::{ActorSetPos, GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData}; use kawari::ipc::{ActorSetPos, GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData};
use kawari::oodle::FFXIVOodle; use kawari::oodle::FFXIVOodle;
@ -8,7 +12,7 @@ use kawari::packet::{
}; };
use kawari::world::{ use kawari::world::{
ActorControlSelf, ActorControlType, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position, ActorControlSelf, ActorControlType, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
UpdateClassInfo, UpdateClassInfo, Zone,
}; };
use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, ZONE_ID}; use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, ZONE_ID};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
@ -34,6 +38,10 @@ async fn main() {
player_id: None, player_id: None,
}; };
let zone = Zone::load(010);
let mut exit_position = None;
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = [0; 2056]; let mut buf = [0; 2056];
loop { loop {
@ -523,6 +531,8 @@ async fn main() {
0, // left finger 0, // left finger
0, // right finger 0, // right finger
], ],
pos: exit_position
.unwrap_or(Position::default()),
..Default::default() ..Default::default()
}), }),
}; };
@ -540,6 +550,36 @@ async fn main() {
) )
.await; .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 { .. } => { IPCStructData::Unk1 { .. } => {
tracing::info!("Recieved Unk1!"); tracing::info!("Recieved Unk1!");
@ -720,6 +760,162 @@ async fn main() {
IPCStructData::Unk12 { .. } => { IPCStructData::Unk12 { .. } => {
tracing::info!("Recieved 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!( _ => panic!(
"The server is recieving a IPC response or unknown packet!" "The server is recieving a IPC response or unknown packet!"
), ),

View file

@ -13,6 +13,9 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub boot_patches_location: String, pub boot_patches_location: String,
#[serde(default)]
pub game_location: String,
} }
impl Default for Config { impl Default for Config {
@ -22,6 +25,7 @@ impl Default for Config {
login_open: false, login_open: false,
boot_patches_location: String::new(), boot_patches_location: String::new(),
supported_platforms: default_supported_platforms(), supported_platforms: default_supported_platforms(),
game_location: String::new(),
} }
} }
} }

View file

@ -100,6 +100,15 @@ pub enum IPCOpCode {
CharacterCreated = 0xE, CharacterCreated = 0xE,
// Unknown, client sends this for ??? // Unknown, client sends this for ???
Unk12 = 0x0E9, 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] #[binrw]
@ -363,6 +372,23 @@ pub enum IPCStructData {
Unk12 { Unk12 {
unk: [u8; 8], // TODO: unknown 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 // Server->Client IPC
#[br(pre_assert(false))] #[br(pre_assert(false))]
@ -511,6 +537,8 @@ pub enum IPCStructData {
#[brw(pad_after = 1136)] // empty #[brw(pad_after = 1136)] // empty
details: CharacterDetails, details: CharacterDetails,
}, },
#[br(pre_assert(false))]
PrepareZoning { unk: [u32; 4] },
} }
#[binrw] #[binrw]
@ -578,6 +606,10 @@ impl IPCSegment {
IPCStructData::NameRejection { .. } => 536, IPCStructData::NameRejection { .. } => 536,
IPCStructData::CharacterCreated { .. } => 2568, IPCStructData::CharacterCreated { .. } => 2568,
IPCStructData::Unk12 { .. } => todo!(), IPCStructData::Unk12 { .. } => todo!(),
IPCStructData::EnterZoneLine { .. } => todo!(),
IPCStructData::Unk13 { .. } => todo!(),
IPCStructData::PrepareZoning { .. } => 16,
IPCStructData::Unk14 { .. } => todo!(),
} }
} }
} }

View file

@ -12,7 +12,7 @@ pub struct InitZone {
pub layer_set_id: u32, pub layer_set_id: u32,
pub layout_id: u32, pub layout_id: u32,
#[br(dbg)] #[br(dbg)]
pub weather_id: u16, pub weather_id: u16, // index into Weather sheet probably?
pub unk_really: u16, pub unk_really: u16,
pub unk_bitmask1: u8, pub unk_bitmask1: u8,
pub unk_bitmask2: u8, pub unk_bitmask2: u8,

View file

@ -22,3 +22,6 @@ pub use actor_control_self::ActorControlType;
mod init_zone; mod init_zone;
pub use init_zone::InitZone; pub use init_zone::InitZone;
mod zone;
pub use zone::Zone;

75
src/world/zone.rs Normal file
View file

@ -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
}
}