From fd1fbe7188900eaad4b27532bd9f2b0bfa4840f0 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Thu, 8 May 2025 22:53:36 -0400 Subject: [PATCH] Start adding support for propagating actor control state This begins figuring out how we are going to be propagating actor control state: e.g. targets, poses, and other misc effects. I ended up sending client triggers to the global server state, who then creates the needed actor control packet for the other players. Now players can see what other players are targeting! --- resources/opcodes.json | 7 ++++- src/bin/kawari-world.rs | 19 ++++-------- src/ipc/zone/actor_control.rs | 23 +++++++++++++++ src/ipc/zone/client_trigger.rs | 30 +++++++++++++++++++ src/ipc/zone/mod.rs | 22 +++++--------- src/world/connection.rs | 53 +++++++++++++++++++++++++++++++--- src/world/server.rs | 35 +++++++++++++++++++++- 7 files changed, 156 insertions(+), 33 deletions(-) create mode 100644 src/ipc/zone/client_trigger.rs diff --git a/resources/opcodes.json b/resources/opcodes.json index a96e31a..f307e38 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -164,6 +164,11 @@ "name": "Unk18", "opcode": 116, "size": 16 + }, + { + "name": "ActorControlTarget", + "opcode": 277, + "size": 28 } ], "ClientZoneIpcType": [ @@ -178,7 +183,7 @@ "size": 72 }, { - "name": "Unk1", + "name": "ClientTrigger", "opcode": 428, "size": 32 }, diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 025118a..f2e5962 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -374,18 +374,9 @@ async fn client_loop( // tell the other players we're here connection.handle.send(ToServer::ActorSpawned(connection.id, Actor { id: ObjectId(connection.player_data.actor_id), hp: 100, spawn_index: 0 }, common)).await; } - ClientZoneIpcData::Unk1 { - category, .. - } => { - tracing::info!("Recieved Unk1! {category:#?}"); - - /*match category { - 3 => { - // set target - tracing::info!("Targeting actor {param1}"); - } - _ => {} - }*/ + ClientZoneIpcData::ClientTrigger(trigger) => { + // inform the server of our trigger, it will handle sending it to other clients + connection.handle.send(ToServer::ClientTrigger(connection.id, connection.player_data.actor_id, trigger.clone())).await; } ClientZoneIpcData::Unk2 { .. } => { tracing::info!("Recieved Unk2!"); @@ -853,7 +844,9 @@ async fn client_loop( FromServer::Message(msg)=> connection.send_message(&msg).await, FromServer::ActorSpawn(actor, common) => connection.spawn_actor(actor, common).await, FromServer::ActorMove(actor_id, position, rotation) => connection.set_actor_position(actor_id, position, rotation).await, - FromServer::ActorDespawn(actor_id) => connection.remove_actor(actor_id).await + FromServer::ActorDespawn(actor_id) => connection.remove_actor(actor_id).await, + FromServer::ActorControl(actor_id, actor_control) => connection.actor_control(actor_id, actor_control).await, + FromServer::ActorControlTarget(actor_id, actor_control) => connection.actor_control_target(actor_id, actor_control).await, }, None => break, } diff --git a/src/ipc/zone/actor_control.rs b/src/ipc/zone/actor_control.rs index 8096905..d79dba6 100644 --- a/src/ipc/zone/actor_control.rs +++ b/src/ipc/zone/actor_control.rs @@ -4,6 +4,8 @@ use crate::common::{read_bool_from, write_bool_as}; use super::OnlineStatus; +// TODO: these are all somewhat related, but maybe should be separated? + // See https://github.com/awgil/ffxiv_reverse/blob/f35b6226c1478234ca2b7149f82d251cffca2f56/vnetlog/vnetlog/ServerIPC.cs#L266 for a REALLY useful list of known values #[binrw] #[derive(Debug, Eq, PartialEq, Clone)] @@ -34,6 +36,11 @@ pub enum ActorControlCategory { }, #[brw(magic = 0x261u16)] ToggleWireframeRendering(), + #[brw(magic = 0x32u16)] + SetTarget { + #[brw(pad_before = 22)] // actually full of info, and 2 bytes of padding at the beginning + actor_id: u32, + }, } #[binrw] @@ -68,3 +75,19 @@ impl Default for ActorControlSelf { } } } + +// Has more padding than ActorControl? +#[binrw] +#[derive(Debug, Clone)] +pub struct ActorControlTarget { + #[brw(pad_size_to = 28)] // take into account categories without params + pub category: ActorControlCategory, +} + +impl Default for ActorControlTarget { + fn default() -> Self { + Self { + category: ActorControlCategory::ToggleInvisibility { invisible: false }, + } + } +} diff --git a/src/ipc/zone/client_trigger.rs b/src/ipc/zone/client_trigger.rs new file mode 100644 index 0000000..7b4ba09 --- /dev/null +++ b/src/ipc/zone/client_trigger.rs @@ -0,0 +1,30 @@ +use binrw::binrw; + +#[binrw] +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum ClientTriggerCommand { + #[brw(magic = 0x3u16)] + SetTarget { + #[brw(pad_before = 2)] + actor_id: u32, + }, + #[brw(magic = 0xC81u16)] + Unk1 {}, + #[brw(magic = 0xC9u16)] + Unk2 {}, +} + +#[binrw] +#[derive(Debug, Clone)] +pub struct ClientTrigger { + #[brw(pad_size_to = 32)] // take into account categories without params + pub trigger: ClientTriggerCommand, +} + +impl Default for ClientTrigger { + fn default() -> Self { + Self { + trigger: ClientTriggerCommand::SetTarget { actor_id: 0 }, + } + } +} diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 350dcbc..1be6197 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -24,7 +24,7 @@ mod player_stats; pub use player_stats::PlayerStats; mod actor_control; -pub use actor_control::{ActorControl, ActorControlCategory, ActorControlSelf}; +pub use actor_control::{ActorControl, ActorControlCategory, ActorControlSelf, ActorControlTarget}; mod init_zone; pub use init_zone::InitZone; @@ -76,6 +76,9 @@ pub use item_operation::ItemOperation; mod equip; pub use equip::Equip; +mod client_trigger; +pub use client_trigger::{ClientTrigger, ClientTriggerCommand}; + use crate::common::ObjectTypeId; use crate::common::Position; use crate::common::read_string; @@ -234,6 +237,8 @@ pub enum ServerZoneIpcData { Unk18 { unk: [u8; 16], // all zero... }, + /// Used to control target information + ActorControlTarget(ActorControlTarget), } #[binrw] @@ -252,19 +257,8 @@ pub enum ClientZoneIpcData { // TODO: full of possibly interesting information unk: [u8; 72], }, - /// FIXME: 32 bytes of something from the client, not sure what yet - #[br(pre_assert(*magic == ClientZoneIpcType::Unk1))] - Unk1 { - // 3 = target - category: u32, - param1: u32, - param2: u32, - param3: u32, - param4: u32, - param5: u32, - param6: u32, - param7: u32, - }, + #[br(pre_assert(*magic == ClientZoneIpcType::ClientTrigger))] + ClientTrigger(ClientTrigger), /// FIXME: 16 bytes of something from the client, not sure what yet #[br(pre_assert(*magic == ClientZoneIpcType::Unk2))] Unk2 { diff --git a/src/world/connection.rs b/src/world/connection.rs index a2f9adf..88888bb 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -16,10 +16,11 @@ use crate::{ ipc::{ chat::ServerChatIpcSegment, zone::{ - ActorControlSelf, ClientZoneIpcSegment, CommonSpawn, ContainerInfo, DisplayFlag, Equip, - InitZone, ItemInfo, Move, NpcSpawn, ObjectKind, PlayerStats, PlayerSubKind, - ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect, StatusEffectList, - UpdateClassInfo, Warp, WeatherChange, + ActorControl, ActorControlSelf, ActorControlTarget, ClientTrigger, + ClientZoneIpcSegment, CommonSpawn, ContainerInfo, DisplayFlag, Equip, InitZone, + ItemInfo, Move, NpcSpawn, ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData, + ServerZoneIpcSegment, StatusEffect, StatusEffectList, UpdateClassInfo, Warp, + WeatherChange, }, }, opcodes::ServerZoneIpcType, @@ -67,6 +68,10 @@ pub enum FromServer { ActorMove(u32, Position, f32), // An actor has despawned. ActorDespawn(u32), + /// We need to update an actor + ActorControl(u32, ActorControl), + /// We need to update an actor's target' + ActorControlTarget(u32, ActorControlTarget), } #[derive(Debug, Clone)] @@ -106,6 +111,7 @@ pub enum ToServer { ActorSpawned(ClientId, Actor, CommonSpawn), ActorMoved(ClientId, u32, Position, f32), ActorDespawned(ClientId, u32), + ClientTrigger(ClientId, u32, ClientTrigger), ZoneLoaded(ClientId), Disconnected(ClientId), FatalError(std::io::Error), @@ -752,6 +758,45 @@ impl ZoneConnection { .await; } + pub async fn actor_control(&mut self, actor_id: u32, actor_control: ActorControl) { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ActorControl, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ActorControl(actor_control), + ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + + pub async fn actor_control_target(&mut self, actor_id: u32, actor_control: ActorControlTarget) { + tracing::info!( + "we are sending actor control target to {actor_id}: {actor_control:#?} and WE ARE {:#?}", + self.player_data.actor_id + ); + + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ActorControlTarget, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ActorControlTarget(actor_control), + ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + pub fn get_player_common_spawn( &self, exit_position: Option, diff --git a/src/world/server.rs b/src/world/server.rs index 8cecef6..5433842 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; use tokio::sync::mpsc::Receiver; -use crate::{common::ObjectId, ipc::zone::CommonSpawn}; +use crate::{ + common::ObjectId, + ipc::zone::{ActorControlCategory, ActorControlTarget, ClientTriggerCommand, CommonSpawn}, +}; use super::{Actor, ClientHandle, ClientId, FromServer, ToServer}; @@ -113,6 +116,36 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i } } } + ToServer::ClientTrigger(from_id, from_actor_id, trigger) => { + for (id, handle) in &mut data.clients { + let id = *id; + + // there's no reason to tell the actor what it just did + if id == from_id { + continue; + } + + tracing::info!("{:#?}", trigger); + + match &trigger.trigger { + ClientTriggerCommand::SetTarget { actor_id } => { + let msg = FromServer::ActorControlTarget( + from_actor_id, + ActorControlTarget { + category: ActorControlCategory::SetTarget { + actor_id: *actor_id, + }, + }, + ); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + } + _ => tracing::warn!("Server doesn't know what to do with {:#?}", trigger), + } + } + } ToServer::Disconnected(from_id) => { to_remove.push(from_id); }