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); }