1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-22 07:27:44 +00:00
kawari/src/ipc.rs

776 lines
22 KiB
Rust
Raw Normal View History

use binrw::binrw;
use crate::common::{read_string, write_string};
// 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
}
#[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,
2025-03-11 22:02:49 -04:00
#[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 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],
2025-03-08 23:49:53 -05:00
#[bw(pad_size_to = 32)]
#[br(count = 32)]
#[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]
#[derive(Debug, Clone, Default)]
pub struct Position {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[binrw]
#[derive(Debug, Eq, PartialEq, Clone)]
#[brw(repr = u16)]
pub enum ActorControlType {
SetCharaGearParamUI = 0x260,
}
#[binrw]
#[derive(Debug, Clone, Copy, Default)]
pub struct StatusEffect {
effect_id: u16,
param: u16,
duration: f32,
source_actor_id: u32,
}
#[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 = 32)]
#[br(count = 32)]
#[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],
},
// 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)]
2025-03-11 22:02:49 -04:00
#[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,
2025-03-08 23:49:53 -05:00
max_characters_on_world: u16,
unk8: u16,
#[brw(pad_after = 12)]
entitled_expansion: u32,
#[br(count = 2)]
2025-03-11 22:02:49 -04:00
#[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 {
server_id: u16,
zone_id: u16,
zone_index: u16,
content_finder_condition_id: u16,
layer_set_id: u32,
layout_id: u32,
weather_id: u32,
unk_bitmask1: u8,
unk_bitmask2: u8,
unk1: u8,
unk2: u32,
festival_id: u16,
additional_festival_id: u16,
unk3: u32,
unk4: u32,
unk5: u32,
unk6: [u32; 4],
unk7: [u32; 3],
position: Position,
unk8: [u32; 4],
unk9: u32,
},
#[br(pre_assert(false))]
ActorControlSelf {
#[brw(pad_after = 2)]
category: ActorControlType,
param1: u32,
param2: u32,
param3: u32,
param4: u32,
param5: u32,
#[brw(pad_after = 4)]
param6: u32,
},
#[br(pre_assert(false))]
PlayerStats {
strength: u32,
dexterity: u32,
vitality: u32,
intelligence: u32,
mind: u32,
piety: u32,
hp: u32,
mp: u32,
tp: u32,
gp: u32,
cp: u32,
delay: u32,
tenacity: u32,
attack_power: u32,
defense: u32,
direct_hit_rate: u32,
evasion: u32,
magic_defense: u32,
critical_hit: u32,
attack_magic_potency: u32,
healing_magic_potency: u32,
elemental_bonus: u32,
determination: u32,
skill_speed: u32,
spell_speed: u32,
haste: u32,
craftmanship: u32,
control: u32,
gathering: u32,
perception: u32,
unk1: [u32; 26],
},
#[br(pre_assert(false))]
PlayerSetup {
content_id: u64,
crest: u64,
unknown10: u64,
char_id: u32,
rested_exp: u32,
companion_current_exp: u32,
unknown1c: u32,
fish_caught: u32,
use_bait_catalog_id: u32,
unknown28: u32,
unknown_pvp2c: u16,
unknown2e: u16,
pvp_frontline_overall_campaigns: u32,
unknown_timestamp34: u32,
unknown_timestamp38: u32,
unknown3c: u32,
unknown40: u32,
unknown44: u32,
companion_time_passed: f32,
unknown4c: u32,
unknown50: u16,
unknown_pvp52: [u16; 4],
pvp_series_exp: u16,
player_commendations: u16,
unknown64: [u16; 8],
pvp_rival_wings_total_matches: u16,
pvp_rival_wings_total_victories: u16,
pvp_rival_wings_weekly_matches: u16,
pvp_rival_wings_weekly_victories: u16,
max_level: u8,
expansion: u8,
unknown76: u8,
unknown77: u8,
unknown78: u8,
race: u8,
tribe: u8,
gender: u8,
current_job: u8,
current_class: u8,
deity: u8,
nameday_month: u8,
nameday_day: u8,
city_state: u8,
homepoint: u8,
unknown8d: [u8; 3],
companion_rank: u8,
companion_stars: u8,
companion_sp: u8,
companion_unk93: u8,
companion_color: u8,
companion_fav_feed: u8,
fav_aetheryte_count: u8,
unknown97: [u8; 5],
sightseeing21_to_80_unlock: u8,
sightseeing_heavensward_unlock: u8,
unknown9e: [u8; 26],
exp: [u32; 32],
pvp_total_exp: u32,
unknown_pvp124: u32,
pvp_exp: u32,
pvp_frontline_overall_ranks: [u32; 3],
unknown138: u32,
levels: [u16; 32],
unknown194: [u8; 218],
companion_name: [u8; 21],
companion_def_rank: u8,
companion_att_rank: u8,
companion_heal_rank: u8,
mount_guide_mask: [u8; 33],
ornament_mask: [u8; 4],
unknown281: [u8; 23],
#[br(count = 32)]
#[bw(pad_size_to = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
name: String,
unknown293: [u8; 16],
unknown2a3: u8,
unlock_bitmask: [u8; 64],
aetheryte: [u8; 26],
favorite_aetheryte_ids: [u16; 4],
free_aetheryte_id: u16,
ps_plus_free_aetheryte_id: u16,
discovery: [u8; 480],
howto: [u8; 36],
unknown554: [u8; 4],
minions: [u8; 60],
chocobo_taxi_mask: [u8; 12],
watched_cutscenes: [u8; 159],
companion_barding_mask: [u8; 12],
companion_equipped_head: u8,
companion_equipped_body: u8,
companion_equipped_legs: u8,
unknown_mask: [u8; 287],
pose: [u8; 7],
unknown6df: [u8; 3],
challenge_log_complete: [u8; 13],
secret_recipe_book_mask: [u8; 12],
unknown_mask6f7: [u8; 29],
relic_completion: [u8; 12],
sightseeing_mask: [u8; 37],
hunting_mark_mask: [u8; 102],
triple_triad_cards: [u8; 45],
unknown895: u8,
unknown7d7: [u8; 15],
unknown7d8: u8,
unknown7e6: [u8; 49],
regional_folklore_mask: [u8; 6],
orchestrion_mask: [u8; 87],
hall_of_novice_completion: [u8; 3],
anima_completion: [u8; 11],
unknown85e: [u8; 41],
unlocked_raids: [u8; 28],
unlocked_dungeons: [u8; 18],
unlocked_guildhests: [u8; 10],
unlocked_trials: [u8; 12],
unlocked_pvp: [u8; 5],
cleared_raids: [u8; 28],
cleared_dungeons: [u8; 18],
cleared_guildhests: [u8; 10],
cleared_trials: [u8; 12],
cleared_pvp: [u8; 5],
unknown948: [u8; 15],
},
#[br(pre_assert(false))]
UpdateClassInfo {
class_id: u16,
unknown: u8,
is_specialist: u8,
synced_level: u16,
class_level: u16,
role_actions: [u32; 10],
},
#[br(pre_assert(false))]
PlayerSpawn {
title: u16,
u1b: u16,
current_world_id: u16,
home_world_id: u16,
gm_rank: u8,
u3c: u8,
u4: u8,
online_status: u8,
pose: u8,
u5a: u8,
u5b: u8,
u5c: u8,
target_id: u64,
u6: u32,
u7: u32,
main_weapon_model: u64,
sec_weapon_model: u64,
craft_tool_model: u64,
u14: u32,
u15: u32,
b_npc_base: u32,
b_npc_name: u32,
u18: u32,
u19: u32,
director_id: u32,
owner_id: u32,
u22: u32,
padding4: [u8; 16],
hp_max: u32,
hp_curr: u32,
display_flags: u32,
fate_id: u16,
mp_curr: u16,
mp_max: u16,
unk: u16,
model_chara: u16,
rotation: u16,
current_mount: u16,
active_minion: u16,
u23: u8,
u24: u8,
u25: u8,
u26: u8,
spawn_index: u8,
state: u8,
persistent_emote: u8,
model_type: u8,
subtype: u8,
voice: u8,
enemy_type: u8,
unk27: u8,
level: u8,
class_job: u8,
unk28: u8,
unk29: u8,
unk30: u8,
mount_head: u8,
mount_body: u8,
mount_feet: u8,
mount_color: u8,
scale: u8,
element_data: [u8; 6],
padding2: [u8; 12],
effect: [StatusEffect; 30],
pos: Position,
models: [u32; 10],
unknown6_58: [u8; 10],
padding3: [u8; 7],
name: [u8; 32],
look: [u8; 26],
fc_tag: [u8; 6],
padding: [u8; 26],
},
}
#[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,
2025-03-08 23:49:53 -05:00
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 { .. } => 228,
IPCStructData::PlayerSetup { .. } => 2544,
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!(),
}
}
}
2025-03-11 22:02:49 -04:00
#[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 {
category: ActorControlType::SetCharaGearParamUI,
param1: 0,
param2: 0,
param3: 0,
param4: 0,
param5: 0,
param6: 0,
},
IPCStructData::InitializeChat { unk: [0; 8] },
2025-03-11 22:02:49 -04:00
];
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);
}
}
}