From bcbe62af06f49a89e99bd6f644b278c3d17a9fa2 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 28 Mar 2025 20:19:17 -0400 Subject: [PATCH] Add packets related to playing events and scenes --- resources/opcodes.json | 15 ++++++ resources/tests/event_play.bin | Bin 0 -> 40 bytes resources/tests/event_start.bin | Bin 0 -> 24 bytes src/bin/kawari-world.rs | 10 ++++ src/common/mod.rs | 4 +- src/packet/compression.rs | 2 + src/world/chat_handler.rs | 90 +++++++++++++++++++++++++++++++- src/world/ipc/actor_control.rs | 7 +++ src/world/ipc/common_spawn.rs | 3 +- src/world/ipc/event_play.rs | 57 ++++++++++++++++++++ src/world/ipc/event_start.rs | 49 +++++++++++++++++ src/world/ipc/mod.rs | 25 +++++++++ 12 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 resources/tests/event_play.bin create mode 100644 resources/tests/event_start.bin create mode 100644 src/world/ipc/event_play.rs create mode 100644 src/world/ipc/event_start.rs diff --git a/resources/opcodes.json b/resources/opcodes.json index b2a3409..53944ab 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -129,6 +129,16 @@ "name": "ContainerInfo", "opcode": 566, "size": 16 + }, + { + "name": "EventPlay", + "opcode": 991, + "size": 40 + }, + { + "name": "EventStart", + "opcode": 955, + "size": 24 } ], "ClientZoneIpcType": [ @@ -231,6 +241,11 @@ "name": "Unk18", "opcode": 221, "size": 8 + }, + { + "name": "EventRelatedUnk", + "opcode": 861, + "size": 16 } ], "ServerLobbyIpcType": [ diff --git a/resources/tests/event_play.bin b/resources/tests/event_play.bin new file mode 100644 index 0000000000000000000000000000000000000000..957749153c9d70c360d88eab128b3e7e06f111ec GIT binary patch literal 40 ccmY$K(jveB1k4P=Ac}!?jW?KP1d|8?07}dPT>t<8 literal 0 HcmV?d00001 diff --git a/resources/tests/event_start.bin b/resources/tests/event_start.bin new file mode 100644 index 0000000000000000000000000000000000000000..73a37e4618dbc3b9fcc6d6834820eae781aaa60e GIT binary patch literal 24 YcmY$K(jveB1k4P=4E#WP8;Ao003^Z!B>(^b literal 0 HcmV?d00001 diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index dc7dcb6..73877a3 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -745,6 +745,16 @@ async fn main() { ClientZoneIpcData::Unk18 { .. } => { tracing::info!("Recieved Unk18!"); } + ClientZoneIpcData::EventRelatedUnk { + unk1, + unk2, + unk3, + unk4, + } => { + tracing::info!( + "Recieved EventRelatedUnk! {unk1} {unk2} {unk3} {unk4}" + ); + } } } SegmentType::KeepAlive { id, timestamp } => { diff --git a/src/common/mod.rs b/src/common/mod.rs index 1aad8f2..0b1d9c7 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -20,7 +20,7 @@ pub use position::Position; #[binrw] #[brw(little)] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ObjectId(pub u32); impl Default for ObjectId { @@ -32,7 +32,7 @@ impl Default for ObjectId { // See https://github.com/aers/FFXIVClientStructs/blob/main/FFXIVClientStructs/FFXIV/Client/Game/Object/GameObject.cs#L158 #[binrw] #[brw(little)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ObjectTypeId { pub object_id: ObjectId, #[brw(pad_after = 3)] diff --git a/src/packet/compression.rs b/src/packet/compression.rs index 4b7565c..924b72f 100644 --- a/src/packet/compression.rs +++ b/src/packet/compression.rs @@ -46,6 +46,8 @@ pub(crate) fn decompress( let mut cursor = Cursor::new(&data); + std::fs::write("decompressed.bin", &data).unwrap(); + for _ in 0..header.segment_count { let current_position = cursor.position(); segments.push(PacketSegment::read_options( diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 200f289..d3ca5d0 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -4,8 +4,9 @@ use crate::{ opcodes::ServerZoneIpcType, packet::{PacketSegment, SegmentType}, world::ipc::{ - ActorControl, ActorControlCategory, BattleNpcSubKind, CommonSpawn, DisplayFlag, NpcSpawn, - ObjectKind, PlayerSpawn, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment, + ActorControl, ActorControlCategory, BattleNpcSubKind, CommonSpawn, DisplayFlag, EventPlay, + EventStart, NpcSpawn, ObjectKind, OnlineStatus, PlayerSpawn, PlayerSubKind, + ServerZoneIpcData, ServerZoneIpcSegment, }, }; @@ -265,6 +266,91 @@ impl ChatHandler { .await; } } + "!playscene" => { + // only works in ul'dah opening + + // Load the game script for this event on the client + { + let ipc = ServerZoneIpcSegment { + unk1: 20, + unk2: 0, + op_code: ServerZoneIpcType::EventStart, + server_id: 0, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::EventStart(EventStart { + target_id: ObjectTypeId { + object_id: ObjectId(connection.player_data.actor_id), + object_type: 0, + }, + event_type: 15, + event_id: 0x130003, + flags: 0, + event_arg: 182, + }), + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + + // set our status icon to viewing cutscene + { + let ipc = ServerZoneIpcSegment { + unk1: 20, + unk2: 0, + op_code: ServerZoneIpcType::ActorControl, + server_id: 0, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ActorControl(ActorControl { + category: ActorControlCategory::SetStatusIcon { + icon: OnlineStatus::ViewingCutscene, + }, + }), + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + + // play the scene, bart + { + let ipc = ServerZoneIpcSegment { + unk1: 20, + unk2: 0, + op_code: ServerZoneIpcType::EventPlay, + server_id: 0, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::EventPlay(EventPlay { + actor_id: ObjectTypeId { + object_id: ObjectId(connection.player_data.actor_id), + object_type: 0, + }, + event_id: 0x130003, + scene: 1, + scene_flags: 4959237, + ..Default::default() + }), + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + } _ => tracing::info!("Unrecognized debug command!"), } } diff --git a/src/world/ipc/actor_control.rs b/src/world/ipc/actor_control.rs index 20b4d2c..bdc7f8f 100644 --- a/src/world/ipc/actor_control.rs +++ b/src/world/ipc/actor_control.rs @@ -1,5 +1,7 @@ use binrw::binrw; +use super::OnlineStatus; + // 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)] @@ -21,6 +23,11 @@ pub enum ActorControlCategory { unk1: u32, unk2: u32, }, + #[brw(magic = 0x01F8u16)] + SetStatusIcon { + #[brw(pad_before = 2)] + icon: OnlineStatus, + }, } #[binrw] diff --git a/src/world/ipc/common_spawn.rs b/src/world/ipc/common_spawn.rs index 2ce37f8..52ec4fd 100644 --- a/src/world/ipc/common_spawn.rs +++ b/src/world/ipc/common_spawn.rs @@ -86,7 +86,7 @@ pub enum CharacterMode { #[binrw] #[brw(little)] #[brw(repr = u8)] -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub enum OnlineStatus { Offline = 0x0, GameQA = 1, @@ -94,6 +94,7 @@ pub enum OnlineStatus { GameMasterBlue = 3, EventParticipant = 4, NewAdventurer = 32, // TODO: This is actually a flag! + ViewingCutscene = 15, #[default] Online = 47, } diff --git a/src/world/ipc/event_play.rs b/src/world/ipc/event_play.rs new file mode 100644 index 0000000..0d1f33a --- /dev/null +++ b/src/world/ipc/event_play.rs @@ -0,0 +1,57 @@ +use binrw::binrw; + +use crate::common::ObjectTypeId; + +#[binrw] +#[brw(little)] +#[derive(Debug, Clone, Default)] +pub struct EventPlay { + pub actor_id: ObjectTypeId, + pub event_id: u32, + pub scene: u16, + #[brw(pad_before = 2)] // FIXME: um, i don't think this is empty!! + pub scene_flags: u32, + pub unk1: u32, + pub unk2: u8, + #[brw(pad_before = 3)] + pub unk3: u32, + pub unk4: u32, + pub unk5: u32, +} + +#[cfg(test)] +mod tests { + use std::{fs::read, io::Cursor, path::PathBuf}; + + use binrw::BinRead; + + use crate::common::ObjectId; + + use super::*; + + #[test] + fn read_intro_event_start() { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests/event_play.bin"); + + let buffer = read(d).unwrap(); + let mut buffer = Cursor::new(&buffer); + + let event_play = EventPlay::read_le(&mut buffer).unwrap(); + assert_eq!( + event_play.actor_id, + ObjectTypeId { + object_id: ObjectId(277124129), + object_type: 0 + } + ); + assert_eq!(event_play.event_id, 0x130003); // aether intro + assert_eq!(event_play.scene, 0); + assert_eq!(event_play.scene_flags, 4959237); + assert_eq!(event_play.unk1, 0); + assert_eq!(event_play.unk2, 1); + assert_eq!(event_play.unk3, 0); + assert_eq!(event_play.unk4, 0); + assert_eq!(event_play.unk5, 0); + } +} diff --git a/src/world/ipc/event_start.rs b/src/world/ipc/event_start.rs new file mode 100644 index 0000000..62297dd --- /dev/null +++ b/src/world/ipc/event_start.rs @@ -0,0 +1,49 @@ +use binrw::binrw; + +use crate::common::ObjectTypeId; + +#[binrw] +#[brw(little)] +#[derive(Debug, Clone, Default)] +pub struct EventStart { + pub target_id: ObjectTypeId, + pub event_id: u32, + pub event_type: u8, + pub flags: u8, + #[brw(pad_before = 2)] + #[brw(pad_after = 4)] + pub event_arg: u32, +} + +#[cfg(test)] +mod tests { + use std::{fs::read, io::Cursor, path::PathBuf}; + + use binrw::BinRead; + + use crate::common::ObjectId; + + use super::*; + + #[test] + fn read_intro_event_start() { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests/event_start.bin"); + + let buffer = read(d).unwrap(); + let mut buffer = Cursor::new(&buffer); + + let event_start = EventStart::read_le(&mut buffer).unwrap(); + assert_eq!( + event_start.target_id, + ObjectTypeId { + object_id: ObjectId(277124129), + object_type: 0 + } + ); + assert_eq!(event_start.event_id, 0x130003); // aether intro + assert_eq!(event_start.event_type, 15); + assert_eq!(event_start.flags, 0); + assert_eq!(event_start.event_arg, 182); + } +} diff --git a/src/world/ipc/mod.rs b/src/world/ipc/mod.rs index 9fcc187..3f00f5b 100644 --- a/src/world/ipc/mod.rs +++ b/src/world/ipc/mod.rs @@ -53,6 +53,12 @@ pub use container_info::{ContainerInfo, ContainerType}; mod item_info; pub use item_info::ItemInfo; +mod event_play; +pub use event_play::EventPlay; + +mod event_start; +pub use event_start::EventStart; + use crate::common::Position; use crate::common::read_string; use crate::common::write_string; @@ -205,6 +211,10 @@ pub enum ServerZoneIpcData { ItemInfo(ItemInfo), /// Sent to inform the client of container status ContainerInfo(ContainerInfo), + /// Sent to tell the client to play a scene + EventPlay(EventPlay), + /// Sent to tell the client to load a scene, but not play it + EventStart(EventStart), } #[binrw] @@ -324,6 +334,13 @@ pub enum ClientZoneIpcData { Unk18 { unk: [u8; 8], // TODO: unknown }, + #[br(pre_assert(*magic == ClientZoneIpcType::EventRelatedUnk))] + EventRelatedUnk { + unk1: u32, + unk2: u32, + unk3: u32, + unk4: u32, + }, } #[cfg(test)] @@ -396,6 +413,14 @@ mod tests { ServerZoneIpcType::ContainerInfo, ServerZoneIpcData::ContainerInfo(ContainerInfo::default()), ), + ( + ServerZoneIpcType::EventPlay, + ServerZoneIpcData::EventPlay(EventPlay::default()), + ), + ( + ServerZoneIpcType::EventStart, + ServerZoneIpcData::EventStart(EventStart::default()), + ), ]; for (opcode, data) in &ipc_types {