1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-06-10 02:17:45 +00:00
kawari/src/ipc.rs
Joshua Goins 1141f6fb35 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.
2025-03-13 19:52:01 -04:00

566 lines
17 KiB
Rust

use binrw::binrw;
use crate::{
CHAR_NAME_MAX_LENGTH,
common::{read_string, write_string},
world::{
ActorControlSelf, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
UpdateClassInfo,
},
};
// NOTE: See https://github.com/karashiiro/FFXIVOpcodes/blob/master/FFXIVOpcodes/Ipcs.cs for opcodes
#[binrw]
#[brw(repr = u16)]
#[derive(Clone, PartialEq, Debug)]
pub enum IPCOpCode {
/// Sent by the server to Initialize something chat-related?
InitializeChat = 0x2,
/// Sent by the client when it requests the character list in the lobby.
RequestCharacterList = 0x3,
/// Sent by the client when it requests to enter a world.
RequestEnterWorld = 0x4,
/// Sent by the client after exchanging encryption information with the lobby server.
ClientVersionInfo = 0x5,
/// Sent by the client when they request something about the character (e.g. deletion.)
LobbyCharacterAction = 0xB,
/// Sent by the server to inform the client of their service accounts.
LobbyServiceAccountList = 0xC,
/// Sent by the server to inform the client of their characters.
LobbyCharacterList = 0xD,
/// Sent by the server to tell the client how to connect to the world server.
LobbyEnterWorld = 0xF,
/// Sent by the server to inform the client of their servers.
LobbyServerList = 0x15,
/// Sent by the server to inform the client of their retainers.
LobbyRetainerList = 0x17,
/// Sent by the client when they successfully initialize with the server, and they need several bits of information (e.g. what zone to load)
InitRequest = 0x2ED,
/// Sent by the server as response to ZoneInitRequest.
InitResponse = 0x1EF,
/// Sent by the server that tells the client which zone to load
InitZone = 0x0311,
/// Sent by the server for... something
ActorControlSelf = 0x018C,
/// Sent by the server containing character stats
PlayerStats = 0x01FA,
/// Sent by the server to setup the player on the client
PlayerSetup = 0x006B,
// Sent by the server to setup class info
UpdateClassInfo = 0x006A,
// Sent by the client when they're done loading and they need to be spawned in
FinishLoading = 0x397, // TODO: assumed
// Sent by the server to spawn the player in
PlayerSpawn = 0x1AB,
// FIXME: 32 bytes of something from the client, not sure what yet
Unk1 = 0x37C,
// FIXME: 16 bytes of something from the client, not sure what yet
Unk2 = 0x1A1,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk3 = 0x326,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk4 = 0x143,
SetSearchInfoHandler = 0x3B2, // TODO: assumed,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk5 = 0x2D0,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk6 = 0x2E5,
// FIXME: 32 bytes of something from the client, not sure what yet
Unk7 = 0x2B5,
UpdatePositionHandler = 0x249, // TODO: assumed
// Sent by the client when the user requests to log out
LogOut = 0x217,
// Sent by the server to indicate the log out is complete
LogOutComplete = 0x369,
// Sent by the client when it's actually disconnecting
Disconnected = 0x360,
// Sent by the client when they send a chat message
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]
#[derive(Debug, Clone, Default)]
pub struct ServiceAccount {
pub id: u32,
pub unk1: u32,
pub index: u32,
#[bw(pad_size_to = 0x44)]
#[br(count = 0x44)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub name: String,
}
#[binrw]
#[derive(Debug, Clone, Default)]
pub struct Server {
pub id: u16,
pub index: u16,
pub flags: u32,
#[brw(pad_before = 4)]
#[brw(pad_after = 4)]
pub icon: u32,
#[bw(pad_size_to = 64)]
#[br(count = 64)]
#[br(map = read_string)]
#[bw(map = write_string)]
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 {
#[brw(pad_after = 4)]
pub id: u32,
pub content_id: u64,
#[brw(pad_after = 4)]
pub index: u32,
pub origin_server_id: u16,
pub current_server_id: u16,
pub unk1: [u8; 16],
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub character_name: String,
#[bw(pad_size_to = 32)]
#[br(count = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub origin_server_name: String,
#[bw(pad_size_to = 32)]
#[br(count = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub current_server_name: String,
#[bw(pad_size_to = 1024)]
#[br(count = 1024)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub character_detail_json: String,
pub unk2: [u8; 20],
}
#[binrw]
#[brw(repr = u8)]
#[derive(Clone, PartialEq, Debug)]
pub enum LobbyCharacterAction {
ReserveName = 0x1,
Create = 0x2,
Rename = 0x3,
Delete = 0x4,
Move = 0x5,
RemakeRetainer = 0x6,
RemakeChara = 0x7,
SettingsUploadBegin = 0x8,
SettingsUpload = 0xC,
WorldVisit = 0xE,
DataCenterToken = 0xF,
Request = 0x15,
}
#[binrw]
#[brw(repr = u8)]
#[derive(Clone, PartialEq, Debug)]
pub enum GameMasterCommandType {
ChangeTerritory = 0x58,
}
#[binrw]
#[br(import(magic: &IPCOpCode))]
#[derive(Debug, Clone)]
pub enum IPCStructData {
// Client->Server IPC
#[br(pre_assert(*magic == IPCOpCode::ClientVersionInfo))]
ClientVersionInfo {
#[brw(pad_before = 18)] // full of nonsense i don't understand yet
#[br(count = 64)]
#[br(map = read_string)]
#[bw(ignore)]
session_id: String,
#[brw(pad_before = 8)] // empty
#[br(count = 128)]
#[br(map = read_string)]
#[bw(ignore)]
version_info: String,
// unknown stuff at the end, it's not completely empty'
},
#[br(pre_assert(*magic == IPCOpCode::RequestCharacterList))]
RequestCharacterList {
#[brw(pad_before = 16)]
sequence: u64,
// TODO: what is in here?
},
#[br(pre_assert(*magic == IPCOpCode::LobbyCharacterAction))]
LobbyCharacterAction {
request_number: u32,
unk1: u32,
character_id: u64,
#[br(pad_before = 8)]
character_index: u8,
action: LobbyCharacterAction,
world_id: u16,
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
name: String,
// TODO: what else is in here?
// according to TemporalStatis, chara make data? (probably op specific)
},
#[br(pre_assert(*magic == IPCOpCode::RequestEnterWorld))]
RequestEnterWorld {
#[brw(pad_before = 16)]
sequence: u64,
lookup_id: u64,
// TODO: what else is in here?
},
#[br(pre_assert(*magic == IPCOpCode::InitRequest))]
InitRequest {
// TODO: full of possibly interesting information
#[br(dbg)]
unk: [u8; 105],
},
#[br(pre_assert(*magic == IPCOpCode::FinishLoading))]
FinishLoading {
// TODO: full of possibly interesting information
unk: [u8; 72],
},
#[br(pre_assert(*magic == IPCOpCode::Unk1))]
Unk1 {
// TODO: full of possibly interesting information
unk: [u8; 32],
},
#[br(pre_assert(*magic == IPCOpCode::Unk2))]
Unk2 {
// TODO: full of possibly interesting information
unk: [u8; 16],
},
#[br(pre_assert(*magic == IPCOpCode::Unk3))]
Unk3 {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::Unk4))]
Unk4 {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::SetSearchInfoHandler))]
SetSearchInfoHandler {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::Unk5))]
Unk5 {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::Unk6))]
Unk6 {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::Unk7))]
Unk7 {
// TODO: full of possibly interesting information
unk: [u8; 32],
},
#[br(pre_assert(*magic == IPCOpCode::UpdatePositionHandler))]
UpdatePositionHandler {
// TODO: full of possibly interesting information
unk: [u8; 24],
},
#[br(pre_assert(*magic == IPCOpCode::LogOut))]
LogOut {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::Disconnected))]
Disconnected {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::ChatMessage))]
ChatMessage {
// TODO: incomplete
#[brw(pad_before = 4)] // empty
player_id: u32,
#[brw(pad_before = 4)] // empty
timestamp: u32,
#[brw(pad_before = 8)] // NOT empty
channel: u16,
#[br(count = 32)]
#[bw(pad_size_to = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
message: String,
},
#[br(pre_assert(*magic == IPCOpCode::GameMasterCommand))]
GameMasterCommand {
// TODO: incomplete
command: GameMasterCommandType,
#[br(pad_before = 3)] // idk, not empty though
arg: u32,
#[br(dbg)]
unk: [u8; 24],
},
// Server->Client IPC
#[br(pre_assert(false))]
LobbyServiceAccountList {
#[br(dbg)]
sequence: u64,
#[brw(pad_before = 1)]
num_service_accounts: u8,
unk1: u8,
#[brw(pad_after = 4)]
unk2: u8,
#[br(count = 8)]
service_accounts: Vec<ServiceAccount>,
},
#[br(pre_assert(false))]
LobbyServerList {
sequence: u64,
unk1: u16,
offset: u16,
#[brw(pad_after = 8)]
num_servers: u32,
#[br(count = 6)]
#[brw(pad_size_to = 504)]
servers: Vec<Server>,
},
#[br(pre_assert(false))]
LobbyRetainerList {
// TODO: what is in here?
#[brw(pad_before = 7)]
#[brw(pad_after = 202)]
unk1: u8,
},
#[br(pre_assert(false))]
LobbyCharacterList {
sequence: u64,
counter: u8,
#[brw(pad_after = 2)]
num_in_packet: u8,
unk1: u8,
unk2: u8,
unk3: u8,
/// Set to 128 if legacy character
unk4: u8,
unk5: [u32; 7],
unk6: u8,
veteran_rank: u8,
#[brw(pad_after = 1)]
unk7: u8,
days_subscribed: u32,
remaining_days: u32,
days_to_next_rank: u32,
max_characters_on_world: u16,
unk8: u16,
#[brw(pad_after = 12)]
entitled_expansion: u32,
#[br(count = 2)]
#[brw(pad_size_to = 2368)]
characters: Vec<CharacterDetails>,
},
#[br(pre_assert(false))]
LobbyEnterWorld {
sequence: u64,
character_id: u32,
#[brw(pad_before = 4)]
content_id: u64,
#[brw(pad_before = 4)]
#[bw(pad_size_to = 66)]
#[br(count = 66)]
#[br(map = read_string)]
#[bw(map = write_string)]
session_id: String,
port: u16,
#[brw(pad_after = 16)]
#[br(count = 48)]
#[br(map = read_string)]
#[bw(map = write_string)]
host: String,
},
#[br(pre_assert(false))]
InitializeChat { unk: [u8; 8] },
#[br(pre_assert(false))]
InitResponse {
unk1: u64,
character_id: u32,
unk2: u32,
},
#[br(pre_assert(false))]
InitZone(InitZone),
#[br(pre_assert(false))]
ActorControlSelf(ActorControlSelf),
#[br(pre_assert(false))]
PlayerStats(PlayerStats),
#[br(pre_assert(false))]
PlayerSetup(PlayerSetup),
#[br(pre_assert(false))]
UpdateClassInfo(UpdateClassInfo),
#[br(pre_assert(false))]
PlayerSpawn(PlayerSpawn),
#[br(pre_assert(false))]
LogOutComplete {
// TODO: guessed
unk: [u8; 8],
},
#[br(pre_assert(false))]
ActorSetPos(ActorSetPos),
}
#[binrw]
#[derive(Debug, Clone)]
pub struct IPCSegment {
pub unk1: u8,
pub unk2: u8,
#[br(dbg)]
pub op_code: IPCOpCode,
#[brw(pad_before = 2)] // empty
#[br(dbg)]
pub server_id: u16,
#[br(dbg)]
pub timestamp: u32,
#[brw(pad_before = 4)]
#[br(args(&op_code))]
pub data: IPCStructData,
}
impl IPCSegment {
pub fn calc_size(&self) -> u32 {
let header = 16;
header
+ match self.data {
IPCStructData::ClientVersionInfo { .. } => todo!(),
IPCStructData::LobbyServiceAccountList { .. } => 24 + (8 * 80),
IPCStructData::RequestCharacterList { .. } => todo!(),
IPCStructData::LobbyServerList { .. } => 24 + (6 * 84),
IPCStructData::LobbyRetainerList { .. } => 210,
IPCStructData::LobbyCharacterList { .. } => 80 + (2 * 1184),
IPCStructData::LobbyCharacterAction { .. } => todo!(),
IPCStructData::LobbyEnterWorld { .. } => 160,
IPCStructData::RequestEnterWorld { .. } => todo!(),
IPCStructData::InitializeChat { .. } => 8,
IPCStructData::InitRequest { .. } => 16,
IPCStructData::InitResponse { .. } => 16,
IPCStructData::InitZone { .. } => 103,
IPCStructData::ActorControlSelf { .. } => 32,
IPCStructData::PlayerStats { .. } => 224,
IPCStructData::PlayerSetup { .. } => 2545,
IPCStructData::UpdateClassInfo { .. } => 48,
IPCStructData::FinishLoading { .. } => todo!(),
IPCStructData::PlayerSpawn { .. } => 656,
IPCStructData::Unk1 { .. } => todo!(),
IPCStructData::Unk2 { .. } => todo!(),
IPCStructData::Unk3 { .. } => todo!(),
IPCStructData::Unk4 { .. } => todo!(),
IPCStructData::SetSearchInfoHandler { .. } => todo!(),
IPCStructData::Unk5 { .. } => todo!(),
IPCStructData::Unk6 { .. } => todo!(),
IPCStructData::Unk7 { .. } => todo!(),
IPCStructData::UpdatePositionHandler { .. } => todo!(),
IPCStructData::LogOut { .. } => todo!(),
IPCStructData::LogOutComplete { .. } => 8,
IPCStructData::Disconnected { .. } => todo!(),
IPCStructData::ChatMessage { .. } => 1056,
IPCStructData::GameMasterCommand { .. } => todo!(),
IPCStructData::ActorSetPos { .. } => 24,
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use binrw::BinWrite;
use super::*;
/// Ensure that the IPC data size as reported matches up with what we write
#[test]
fn test_ipc_sizes() {
let ipc_types = [
IPCStructData::LobbyServerList {
sequence: 0,
unk1: 0,
offset: 0,
num_servers: 0,
servers: Vec::new(),
},
IPCStructData::LobbyCharacterList {
sequence: 0,
counter: 0,
num_in_packet: 0,
unk1: 0,
unk2: 0,
unk3: 0,
unk4: 0,
unk5: [0; 7],
unk6: 0,
veteran_rank: 0,
unk7: 0,
days_subscribed: 0,
remaining_days: 0,
days_to_next_rank: 0,
max_characters_on_world: 0,
unk8: 0,
entitled_expansion: 0,
characters: Vec::new(),
},
IPCStructData::ActorControlSelf(ActorControlSelf::default()),
IPCStructData::InitializeChat { unk: [0; 8] },
IPCStructData::PlayerStats(PlayerStats::default()),
IPCStructData::PlayerSetup(PlayerSetup::default()),
IPCStructData::UpdateClassInfo(UpdateClassInfo::default()),
IPCStructData::PlayerSpawn(PlayerSpawn::default()),
IPCStructData::ActorSetPos(ActorSetPos::default()),
];
for ipc in &ipc_types {
let mut cursor = Cursor::new(Vec::new());
let ipc_segment = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::InitializeChat, // doesn't matter for this test
server_id: 0,
timestamp: 0,
data: ipc.clone(),
};
ipc_segment.write_le(&mut cursor).unwrap();
let buffer = cursor.into_inner();
assert_eq!(
buffer.len(),
ipc_segment.calc_size() as usize,
"{:?} did not match size!",
ipc
);
}
}
}