From 1141f6fb359b98395428cd13e14b77c96279c858 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Thu, 13 Mar 2025 19:52:01 -0400 Subject: [PATCH] Add !setpos debug command to forcefully move the player This is useful in some territories where you might not spawn in the correct position. It currently locks up movement afterwards, but it's still useful. --- src/bin/kawari-world.rs | 51 +++++++++++++++++++++++++++++++++++++++-- src/ipc.rs | 15 ++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 68a1f8f..e1aa623 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,14 +1,14 @@ use std::time::{SystemTime, UNIX_EPOCH}; use kawari::client_select_data::ClientCustomizeData; -use kawari::ipc::{GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData}; +use kawari::ipc::{ActorSetPos, GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData}; use kawari::oodle::FFXIVOodle; use kawari::packet::{ CompressionType, PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet, }; use kawari::world::{ ActorControlSelf, ActorControlType, CustomizeData, InitZone, PlayerSetup, PlayerSpawn, - PlayerStats, UpdateClassInfo, + PlayerStats, Position, UpdateClassInfo, }; use kawari::{CONTENT_ID, WORLD_ID, ZONE_ID}; use tokio::io::AsyncReadExt; @@ -483,6 +483,53 @@ async fn main() { } IPCStructData::ChatMessage { message, .. } => { tracing::info!("Client sent chat message: {message}!"); + + let parts: Vec<&str> = message.split(' ').collect(); + match parts[0] { + "!setpos" => { + let pos_x = parts[1].parse::().unwrap(); + let pos_y = parts[2].parse::().unwrap(); + let pos_z = parts[3].parse::().unwrap(); + + // set pos + { + let ipc = IPCSegment { + unk1: 14, + unk2: 0, + op_code: IPCOpCode::ActorSetPos, + server_id: WORLD_ID, + timestamp: timestamp_secs(), + data: IPCStructData::ActorSetPos( + ActorSetPos { + unk: 0x020fa3b8, + position: Position { + x: pos_x, + y: pos_y, + z: pos_z, + }, + ..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; + } + } + _ => tracing::info!("Unrecognized debug command!"), + } } IPCStructData::GameMasterCommand { command, arg, .. } => { tracing::info!("Got a game master command!"); diff --git a/src/ipc.rs b/src/ipc.rs index 99e6b6f..ff9d1ef 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -81,6 +81,8 @@ pub enum IPCOpCode { ChatMessage = 0xCA, // Sent by the client when they send a GM command. This can only be sent by the client if they are sent a GM rank. GameMasterCommand = 0x3B3, + // Sent by the server to modify the client's position + ActorSetPos = 0x223, } #[binrw] @@ -112,6 +114,15 @@ pub struct Server { pub name: String, } +#[binrw] +#[derive(Debug, Clone, Default)] +pub struct ActorSetPos { + pub unk: u32, + pub layer_id: u32, + pub position: Position, + pub unk3: u32, +} + #[binrw] #[derive(Debug, Clone, Default)] pub struct CharacterDetails { @@ -417,6 +428,8 @@ pub enum IPCStructData { // TODO: guessed unk: [u8; 8], }, + #[br(pre_assert(false))] + ActorSetPos(ActorSetPos), } #[binrw] @@ -474,6 +487,7 @@ impl IPCSegment { IPCStructData::Disconnected { .. } => todo!(), IPCStructData::ChatMessage { .. } => 1056, IPCStructData::GameMasterCommand { .. } => todo!(), + IPCStructData::ActorSetPos { .. } => 24, } } } @@ -523,6 +537,7 @@ mod tests { IPCStructData::PlayerSetup(PlayerSetup::default()), IPCStructData::UpdateClassInfo(UpdateClassInfo::default()), IPCStructData::PlayerSpawn(PlayerSpawn::default()), + IPCStructData::ActorSetPos(ActorSetPos::default()), ]; for ipc in &ipc_types {