From f5d75301b270d8a626a51164f1d515925a1a8a0f Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 16 Mar 2025 17:43:29 -0400 Subject: [PATCH] Re-organize the IPC structures so they live in their own server-specific module My old setup of throwing *all* of the IPC types and opcodes into *one* enum was becoming unbearable. Now that we have multiple things using the same opcodes (because they can overlap) I think it's time to repay this technical debt. This overhauls the structure of the project to move IPC structs into their own modules, and separate the opcode data/lists into separate ones depending on if it's clientbound and serverbound. Nothing has changed functionall, but this is going to make it way easier to add more IPC in the future. --- src/bin/kawari-lobby.rs | 43 +- src/bin/kawari-world.rs | 193 +++--- src/ipc.rs | 732 ---------------------- src/lib.rs | 8 +- src/lobby/connection.rs | 62 +- src/lobby/ipc/character_action.rs | 30 + src/lobby/ipc/character_list.rs | 39 ++ src/lobby/ipc/client_version_info.rs | 1 + src/lobby/ipc/mod.rs | 349 +++++++++++ src/lobby/ipc/server_list.rs | 19 + src/lobby/ipc/service_account_list.rs | 16 + src/lobby/mod.rs | 1 + src/{ => packet}/compression.rs | 15 +- src/{ => packet}/encryption.rs | 21 +- src/packet/ipc.rs | 30 + src/packet/mod.rs | 15 + src/{ => packet}/packet.rs | 68 +- src/world/chat_handler.rs | 14 +- src/world/connection.rs | 44 +- src/world/{ => ipc}/actor_control_self.rs | 0 src/world/{ => ipc}/chat_message.rs | 0 src/world/{ => ipc}/init_zone.rs | 0 src/world/ipc/mod.rs | 465 ++++++++++++++ src/world/{ => ipc}/player_setup.rs | 0 src/world/{ => ipc}/player_spawn.rs | 0 src/world/{ => ipc}/player_stats.rs | 0 src/world/{ => ipc}/position.rs | 0 src/world/{ => ipc}/social_list.rs | 0 src/world/{ => ipc}/status_effect.rs | 0 src/world/{ => ipc}/update_class_info.rs | 0 src/world/mod.rs | 35 +- 31 files changed, 1217 insertions(+), 983 deletions(-) delete mode 100644 src/ipc.rs create mode 100644 src/lobby/ipc/character_action.rs create mode 100644 src/lobby/ipc/character_list.rs create mode 100644 src/lobby/ipc/client_version_info.rs create mode 100644 src/lobby/ipc/mod.rs create mode 100644 src/lobby/ipc/server_list.rs create mode 100644 src/lobby/ipc/service_account_list.rs rename src/{ => packet}/compression.rs (84%) rename src/{ => packet}/encryption.rs (86%) create mode 100644 src/packet/ipc.rs create mode 100644 src/packet/mod.rs rename src/{ => packet}/packet.rs (86%) rename src/world/{ => ipc}/actor_control_self.rs (100%) rename src/world/{ => ipc}/chat_message.rs (100%) rename src/world/{ => ipc}/init_zone.rs (100%) create mode 100644 src/world/ipc/mod.rs rename src/world/{ => ipc}/player_setup.rs (100%) rename src/world/{ => ipc}/player_spawn.rs (100%) rename src/world/{ => ipc}/player_stats.rs (100%) rename src/world/{ => ipc}/position.rs (100%) rename src/world/{ => ipc}/social_list.rs (100%) rename src/world/{ => ipc}/status_effect.rs (100%) rename src/world/{ => ipc}/update_class_info.rs (100%) diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 883d7ce..930017c 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -1,9 +1,12 @@ use kawari::CONTENT_ID; -use kawari::ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, LobbyCharacterAction}; use kawari::lobby::chara_make::CharaMake; use kawari::lobby::connection::LobbyConnection; +use kawari::lobby::ipc::{ + CharacterDetails, ClientLobbyIpcData, LobbyCharacterAction, ServerLobbyIpcData, + ServerLobbyIpcSegment, ServerLobbyIpcType, +}; use kawari::oodle::FFXIVOodle; -use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive}; +use kawari::packet::{PacketSegment, PacketState, SegmentType, send_keep_alive}; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; @@ -18,7 +21,7 @@ async fn main() { loop { let (socket, _) = listener.accept().await.unwrap(); - let state = State { + let state = PacketState { client_key: None, session_id: None, clientbound_oodle: FFXIVOodle::new(), @@ -46,10 +49,10 @@ async fn main() { connection.initialize_encryption(phrase, key).await; } SegmentType::Ipc { data } => match &data.data { - IPCStructData::ClientVersionInfo { - sequence, + ClientLobbyIpcData::ClientVersionInfo { session_id, version_info, + .. } => { tracing::info!( "Client {session_id} ({version_info}) logging in!" @@ -62,13 +65,16 @@ async fn main() { // request an update //connection.send_error(*sequence, 1012, 13101).await; } - IPCStructData::RequestCharacterList { sequence } => { + ClientLobbyIpcData::RequestCharacterList { sequence } => { tracing::info!("Client is requesting character list..."); connection.send_lobby_info(*sequence).await; } - IPCStructData::LobbyCharacterAction { - action, name, json, .. + ClientLobbyIpcData::LobbyCharacterAction { + action, + name, + json, + .. } => { match action { LobbyCharacterAction::ReserveName => { @@ -84,7 +90,7 @@ async fn main() { op_code: IPCOpCode::InitializeChat, // wrong but technically right server_id: 0, timestamp: 0, - data: IPCStructData::NameRejection { + data: ClientLobbyIpcType::NameRejection { unk1: 0x03, unk2: 0x0bdb, unk3: 0x000132cc, @@ -107,13 +113,13 @@ async fn main() { // accept { - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::CharacterCreated, + op_code: ServerLobbyIpcType::CharacterCreated, server_id: 0, timestamp: 0, - data: IPCStructData::CharacterCreated { + data: ServerLobbyIpcData::CharacterCreated { unk1: 0x4, unk2: 0x00010101, details: CharacterDetails { @@ -147,13 +153,13 @@ async fn main() { // a slightly different character created packet now { - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::CharacterCreated, + op_code: ServerLobbyIpcType::CharacterCreated, server_id: 0, timestamp: 0, - data: IPCStructData::CharacterCreated { + data: ServerLobbyIpcData::CharacterCreated { unk1: 0x5, unk2: 0x00020101, details: CharacterDetails { @@ -192,7 +198,7 @@ async fn main() { LobbyCharacterAction::Request => todo!(), } } - IPCStructData::RequestEnterWorld { + ClientLobbyIpcData::RequestEnterWorld { sequence, lookup_id, } => { @@ -200,12 +206,9 @@ async fn main() { connection.send_enter_world(*sequence, *lookup_id).await; } - _ => { - panic!("The server is recieving a IPC response packet!") - } }, SegmentType::KeepAlive { id, timestamp } => { - send_keep_alive( + send_keep_alive::( &mut connection.socket, &mut connection.state, *id, diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 91e5eae..589e41e 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,11 +1,17 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use kawari::ipc::{GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData}; use kawari::oodle::FFXIVOodle; -use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive}; +use kawari::packet::{PacketSegment, PacketState, SegmentType, send_keep_alive}; +use kawari::world::ipc::{ + ClientZoneIpcData, GameMasterCommandType, ServerZoneIpcData, ServerZoneIpcSegment, + ServerZoneIpcType, SocialListRequestType, +}; use kawari::world::{ - ActorControlSelf, ActorControlType, ChatHandler, PlayerEntry, PlayerSetup, PlayerSpawn, - PlayerStats, Position, SocialList, Zone, ZoneConnection, + ChatHandler, Zone, ZoneConnection, + ipc::{ + ActorControlSelf, ActorControlType, PlayerEntry, PlayerSetup, PlayerSpawn, PlayerStats, + Position, SocialList, + }, }; use kawari::{ CHAR_NAME, CITY_STATE, CONTENT_ID, CUSTOMIZE_DATA, DEITY, NAMEDAY_DAY, NAMEDAY_MONTH, WORLD_ID, @@ -25,7 +31,7 @@ async fn main() { loop { let (socket, _) = listener.accept().await.unwrap(); - let state = State { + let state = PacketState { client_key: None, session_id: None, clientbound_oodle: FFXIVOodle::new(), @@ -116,10 +122,12 @@ async fn main() { } { - let ipc = IPCSegment { - op_code: IPCOpCode::InitializeChat, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InitializeChat, timestamp: timestamp_secs(), - data: IPCStructData::InitializeChat { unk: [0; 8] }, + data: ServerZoneIpcData::InitializeChat { + unk: [0; 8], + }, ..Default::default() }; @@ -139,17 +147,17 @@ async fn main() { } SegmentType::Ipc { data } => { match &data.data { - IPCStructData::InitRequest { .. } => { + ClientZoneIpcData::InitRequest { .. } => { tracing::info!( "Client is now requesting zone information. Sending!" ); // IPC Init(?) { - let ipc = IPCSegment { - op_code: IPCOpCode::Unk5, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InitResponse, timestamp: timestamp_secs(), - data: IPCStructData::InitResponse { + data: ServerZoneIpcData::InitResponse { unk1: 0, character_id: connection.player_id, unk2: 0, @@ -168,10 +176,10 @@ async fn main() { // Control Data { - let ipc = IPCSegment { - op_code: IPCOpCode::ActorControlSelf, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ActorControlSelf, timestamp: timestamp_secs(), - data: IPCStructData::ActorControlSelf( + data: ServerZoneIpcData::ActorControlSelf( ActorControlSelf { category: ActorControlType::SetCharaGearParamUI, @@ -197,10 +205,10 @@ async fn main() { // Stats { - let ipc = IPCSegment { - op_code: IPCOpCode::PlayerStats, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PlayerStats, timestamp: timestamp_secs(), - data: IPCStructData::PlayerStats(PlayerStats { + data: ServerZoneIpcData::PlayerStats(PlayerStats { strength: 1, hp: 100, mp: 100, @@ -220,10 +228,10 @@ async fn main() { // Player Setup { - let ipc = IPCSegment { - op_code: IPCOpCode::PlayerSetup, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PlayerSetup, timestamp: timestamp_secs(), - data: IPCStructData::PlayerSetup(PlayerSetup { + data: ServerZoneIpcData::PlayerSetup(PlayerSetup { content_id: CONTENT_ID, exp: [10000; 32], levels: [100; 32], @@ -254,10 +262,10 @@ async fn main() { // send welcome message { - let ipc = IPCSegment { - op_code: IPCOpCode::ServerChatMessage, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ServerChatMessage, timestamp: timestamp_secs(), - data: IPCStructData::ServerChatMessage { + data: ServerZoneIpcData::ServerChatMessage { message: "Welcome to Kawari!".to_string(), unk: 0, }, @@ -273,17 +281,17 @@ async fn main() { .await; } } - IPCStructData::FinishLoading { .. } => { + ClientZoneIpcData::FinishLoading { .. } => { tracing::info!( "Client has finished loading... spawning in!" ); // send player spawn { - let ipc = IPCSegment { - op_code: IPCOpCode::PlayerSpawn, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PlayerSpawn, timestamp: timestamp_secs(), - data: IPCStructData::PlayerSpawn(PlayerSpawn { + data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { content_id: CONTENT_ID, current_world_id: WORLD_ID, home_world_id: WORLD_ID, @@ -329,10 +337,10 @@ async fn main() { // fade in? { - let ipc = IPCSegment { - op_code: IPCOpCode::PrepareZoning, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PrepareZoning, timestamp: timestamp_secs(), - data: IPCStructData::PrepareZoning { + data: ServerZoneIpcData::PrepareZoning { unk: [0, 0, 0, 0], }, ..Default::default() @@ -350,47 +358,49 @@ async fn main() { // wipe any exit position so it isn't accidentally reused exit_position = None; } - IPCStructData::Unk1 { .. } => { + ClientZoneIpcData::Unk1 { .. } => { tracing::info!("Recieved Unk1!"); } - IPCStructData::Unk2 { .. } => { + ClientZoneIpcData::Unk2 { .. } => { tracing::info!("Recieved Unk2!"); } - IPCStructData::Unk3 { .. } => { + ClientZoneIpcData::Unk3 { .. } => { tracing::info!("Recieved Unk3!"); } - IPCStructData::Unk4 { .. } => { + ClientZoneIpcData::Unk4 { .. } => { tracing::info!("Recieved Unk4!"); } - IPCStructData::SetSearchInfoHandler { .. } => { + ClientZoneIpcData::SetSearchInfoHandler { .. } => { tracing::info!("Recieved SetSearchInfoHandler!"); } - IPCStructData::Unk5 { .. } => { + ClientZoneIpcData::Unk5 { .. } => { tracing::info!("Recieved Unk5!"); } - IPCStructData::SocialListRequest(request) => { + ClientZoneIpcData::SocialListRequest(request) => { tracing::info!("Recieved social list request!"); match &request.request_type { - kawari::world::SocialListRequestType::Party => { - let ipc = IPCSegment { - op_code: IPCOpCode::SocialList, + SocialListRequestType::Party => { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::SocialList, timestamp: timestamp_secs(), - data: IPCStructData::SocialList(SocialList { - request_type: request.request_type, - sequence: request.count, - entries: vec![PlayerEntry { - content_id: CONTENT_ID, - zone_id: connection.zone.id, - zone_id1: 0x0100, - class_job: 36, - level: 100, - one: 1, - name: CHAR_NAME.to_string(), - fc_tag: "LOCAL".to_string(), - ..Default::default() - }], - }), + data: ServerZoneIpcData::SocialList( + SocialList { + request_type: request.request_type, + sequence: request.count, + entries: vec![PlayerEntry { + content_id: CONTENT_ID, + zone_id: connection.zone.id, + zone_id1: 0x0100, + class_job: 36, + level: 100, + one: 1, + name: CHAR_NAME.to_string(), + fc_tag: "LOCAL".to_string(), + ..Default::default() + }], + }, + ), ..Default::default() }; @@ -404,15 +414,17 @@ async fn main() { }) .await; } - kawari::world::SocialListRequestType::Friends => { - let ipc = IPCSegment { - op_code: IPCOpCode::SocialList, + SocialListRequestType::Friends => { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::SocialList, timestamp: timestamp_secs(), - data: IPCStructData::SocialList(SocialList { - request_type: request.request_type, - sequence: request.count, - entries: Default::default(), - }), + data: ServerZoneIpcData::SocialList( + SocialList { + request_type: request.request_type, + sequence: request.count, + entries: Default::default(), + }, + ), ..Default::default() }; @@ -428,17 +440,17 @@ async fn main() { } } } - IPCStructData::Unk7 { + ClientZoneIpcData::Unk7 { timestamp, unk1, .. } => { tracing::info!("Recieved Unk7! {:#?}", unk1); // send unk11 in response { - let ipc = IPCSegment { - op_code: IPCOpCode::Unk11, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::Unk11, timestamp: timestamp_secs(), - data: IPCStructData::Unk11 { + data: ServerZoneIpcData::Unk11 { timestamp: *timestamp, unk: 333, }, @@ -454,18 +466,20 @@ async fn main() { .await; } } - IPCStructData::UpdatePositionHandler { .. } => { + ClientZoneIpcData::UpdatePositionHandler { .. } => { tracing::info!("Recieved UpdatePositionHandler!"); } - IPCStructData::LogOut { .. } => { + ClientZoneIpcData::LogOut { .. } => { tracing::info!("Recieved log out from client!"); // tell the client to disconnect { - let ipc = IPCSegment { - op_code: IPCOpCode::LogOutComplete, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::LogOutComplete, timestamp: timestamp_secs(), - data: IPCStructData::LogOutComplete { unk: [0; 8] }, + data: ServerZoneIpcData::LogOutComplete { + unk: [0; 8], + }, ..Default::default() }; @@ -478,17 +492,19 @@ async fn main() { .await; } } - IPCStructData::Disconnected { .. } => { + ClientZoneIpcData::Disconnected { .. } => { tracing::info!("Client disconnected!"); } - IPCStructData::ChatMessage(chat_message) => { + ClientZoneIpcData::ChatMessage(chat_message) => { ChatHandler::handle_chat_message( &mut connection, chat_message, ) .await } - IPCStructData::GameMasterCommand { command, arg, .. } => { + ClientZoneIpcData::GameMasterCommand { + command, arg, .. + } => { tracing::info!("Got a game master command!"); match &command { @@ -497,10 +513,10 @@ async fn main() { } } } - IPCStructData::Unk12 { .. } => { + ClientZoneIpcData::Unk12 { .. } => { tracing::info!("Recieved Unk12!"); } - IPCStructData::EnterZoneLine { + ClientZoneIpcData::EnterZoneLine { exit_box_id, position, .. @@ -535,10 +551,10 @@ async fn main() { // fade out? { - let ipc = IPCSegment { - op_code: IPCOpCode::PrepareZoning, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PrepareZoning, timestamp: timestamp_secs(), - data: IPCStructData::PrepareZoning { + data: ServerZoneIpcData::PrepareZoning { unk: [0x01000000, 0, 0, 0], }, ..Default::default() @@ -555,10 +571,10 @@ async fn main() { // fade out? x2 { - let ipc = IPCSegment { - op_code: IPCOpCode::PrepareZoning, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PrepareZoning, timestamp: timestamp_secs(), - data: IPCStructData::PrepareZoning { + data: ServerZoneIpcData::PrepareZoning { unk: [0, 0x00000085, 0x00030000, 0x000008ff], // last thing is probably a float? }, ..Default::default() @@ -577,19 +593,16 @@ async fn main() { connection.change_zone(new_territory).await; } - IPCStructData::Unk13 { .. } => { + ClientZoneIpcData::Unk13 { .. } => { tracing::info!("Recieved Unk13!"); } - IPCStructData::Unk14 { .. } => { + ClientZoneIpcData::Unk14 { .. } => { tracing::info!("Recieved Unk14!"); } - _ => panic!( - "The server is recieving a IPC response or unknown packet!" - ), } } SegmentType::KeepAlive { id, timestamp } => { - send_keep_alive( + send_keep_alive::( &mut connection.socket, &mut connection.state, *id, diff --git a/src/ipc.rs b/src/ipc.rs deleted file mode 100644 index 9b4259f..0000000 --- a/src/ipc.rs +++ /dev/null @@ -1,732 +0,0 @@ -use binrw::binrw; - -use crate::{ - CHAR_NAME_MAX_LENGTH, - common::{read_string, write_string}, - world::{ - ActorControlSelf, ChatMessage, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position, - SocialList, SocialListRequest, 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 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 = 0x2E5, - // 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 - /// ALSO Sent by the server as response to ZoneInitRequest. - Unk5 = 0x2D0, - // Sent by the client when it requests the friends list and other related info - SocialListRequest = 0x1A1, - // 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, - // Sent by the server when they send a chat message - ServerChatMessage = 0x196, - // Unknown, server sends to the client before player spawn - Unk8 = 0x134, - // Unknown, but seems to contain information on cross-world linkshells - LinkShellInformation = 0x234, - // Unknown, server sends to the client before player spawn - Unk9 = 0x189, - // Unknown, server sends to the client before player spawn. - // Seems to the same across two different characters? - Unk10 = 0x110, - // Unknown, server sends this in response to Unk7 - Unk11 = 0x156, - // Assumed what this is, but probably incorrect - CharacterCreated = 0xE, - // Unknown, client sends this for ??? - Unk12 = 0x0E9, - // Sent by the client when the character walks into a zone transistion - EnterZoneLine = 0x205, - // Sent by the client after we sent a InitZone in TravelToZone?? - // TODO: Actually, I don't think is real... - Unk13 = 0x2EE, - // Sent by the server when it wants the client to... prepare to zone? - PrepareZoning = 0x308, - // Sent by the client for unknown reasons - Unk14 = 0x87, - // Sent by the server??? - Unk15 = 0x28C, - // Sent by the server before init zone??? - Unk16 = 0x3AB, - // Sent by the server - ActorControl = 0x1B9, - // Sent by the server - ActorMove = 0x3D8, - // Sent by the server - Unk17 = 0x2A1, - // Sent by the server in response to SocialListRequest - SocialList = 0x36C, -} - -#[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] -#[derive(Clone, PartialEq, Debug)] -pub enum LobbyCharacterAction { - #[brw(magic = 0x1u8)] - ReserveName, - #[brw(magic = 0x2u8)] - Create, - #[brw(magic = 0x3u8)] - Rename, - #[brw(magic = 0x4u8)] - Delete, - #[brw(magic = 0x5u8)] - Move, - #[brw(magic = 0x6u8)] - RemakeRetainer, - #[brw(magic = 0x7u8)] - RemakeChara, - #[brw(magic = 0x8u8)] - SettingsUploadBegin, - #[brw(magic = 0xCu8)] - SettingsUpload, - #[brw(magic = 0xEu8)] - WorldVisit, - #[brw(magic = 0xFu8)] - DataCenterToken, - #[brw(magic = 0x15u8)] - Request, -} - -#[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 { - sequence: u64, - - #[brw(pad_before = 10)] // 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, - #[bw(pad_size_to = 436)] - #[br(count = 436)] - #[br(map = read_string)] - #[bw(map = write_string)] - json: String, - }, - #[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; 8], - }, - #[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::SocialListRequest))] - SocialListRequest(SocialListRequest), - #[br(pre_assert(*magic == IPCOpCode::Unk7))] - Unk7 { - // TODO: full of possibly interesting information - timestamp: u32, - #[brw(pad_before = 8)] // empty bytes - #[brw(pad_after = 4)] // empty bytes - unk1: [u8; 16], // something - }, - #[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(ChatMessage), - #[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], - }, - #[br(pre_assert(*magic == IPCOpCode::Unk12))] - Unk12 { - unk: [u8; 8], // TODO: unknown - }, - #[br(pre_assert(*magic == IPCOpCode::EnterZoneLine))] - EnterZoneLine { - exit_box_id: u32, - position: Position, - #[brw(pad_after = 4)] // empty - landset_index: i32, - }, - #[br(pre_assert(*magic == IPCOpCode::Unk13))] - Unk13 { - #[br(dbg)] - unk: [u8; 16], // TODO: unknown - }, - #[br(pre_assert(*magic == IPCOpCode::Unk14))] - Unk14 { - #[br(dbg)] - unk: [u8; 8], // TODO: unknown - }, - - // 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, - }, - #[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, - }, - #[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, - }, - #[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), - #[br(pre_assert(false))] - ServerChatMessage { - unk: u8, // channel? - #[brw(pad_after = 775)] - #[br(count = 775)] - #[br(map = read_string)] - #[bw(map = write_string)] - message: String, - }, - #[br(pre_assert(false))] - Unk8 { unk: [u8; 808] }, - #[br(pre_assert(false))] - LinkShellInformation { unk: [u8; 456] }, - #[br(pre_assert(false))] - Unk9 { unk: [u8; 24] }, - #[br(pre_assert(false))] - Unk10 { unk: u64 }, - #[br(pre_assert(false))] - Unk11 { - timestamp: u32, - #[brw(pad_after = 24)] // empty bytes - unk: u32, - }, - #[br(pre_assert(false))] - NameRejection { - // FIXME: This is opcode 0x2, which is InitializeChat. We need to separate the lobby/zone IPC codes. - unk1: u8, - #[brw(pad_before = 7)] // empty - unk2: u16, - #[brw(pad_before = 6)] // empty - #[brw(pad_after = 516)] // mostly empty - unk3: u32, - }, - #[br(pre_assert(false))] - CharacterCreated { - #[brw(pad_after = 4)] // empty - unk1: u32, - #[brw(pad_after = 4)] // empty - unk2: u32, - #[brw(pad_before = 32)] // empty - #[brw(pad_after = 1136)] // empty - details: CharacterDetails, - }, - #[br(pre_assert(false))] - PrepareZoning { unk: [u32; 4] }, - #[br(pre_assert(false))] - Unk15 { unk: u32, player_id: u32 }, - #[br(pre_assert(false))] - Unk16 { unk: [u8; 136] }, - #[br(pre_assert(false))] - ActorControl { - #[brw(pad_after = 20)] // empty - unk: u32, - }, - #[br(pre_assert(false))] - ActorMove { - #[brw(pad_after = 4)] // empty - pos: Position, - }, - #[br(pre_assert(false))] - Unk17 { unk: [u8; 104] }, - #[br(pre_assert(false))] - SocialList(SocialList), - #[br(pre_assert(false))] - LobbyError { - sequence: u64, - error: u32, - value: u32, - exd_error_id: u16, - unk1: u16, - }, -} - -#[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 Default for IPCSegment { - fn default() -> Self { - Self { - unk1: 0x14, - unk2: 0, - op_code: IPCOpCode::InitializeChat, - server_id: 0, - timestamp: 0, - data: IPCStructData::ClientVersionInfo { - session_id: String::new(), - version_info: String::new(), - sequence: 0, - }, - } - } -} - -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 { .. } => 2784, - 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::SocialListRequest { .. } => 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, - IPCStructData::ServerChatMessage { .. } => 776, - IPCStructData::Unk8 { .. } => 808, - IPCStructData::LinkShellInformation { .. } => 456, - IPCStructData::Unk9 { .. } => 24, - IPCStructData::Unk10 { .. } => 8, - IPCStructData::Unk11 { .. } => 32, - IPCStructData::NameRejection { .. } => 536, - IPCStructData::CharacterCreated { .. } => 2568, - IPCStructData::Unk12 { .. } => todo!(), - IPCStructData::EnterZoneLine { .. } => todo!(), - IPCStructData::Unk13 { .. } => todo!(), - IPCStructData::PrepareZoning { .. } => 16, - IPCStructData::Unk14 { .. } => todo!(), - IPCStructData::Unk15 { .. } => 8, - IPCStructData::Unk16 { .. } => 136, - IPCStructData::ActorControl { .. } => 24, - IPCStructData::ActorMove { .. } => 16, - IPCStructData::Unk17 { .. } => 104, - IPCStructData::SocialList { .. } => 1136, - IPCStructData::LobbyError { .. } => 536, - } - } -} - -#[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 - ); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index d5ffc63..48023d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,14 +9,9 @@ pub mod blowfish; /// Common functions, structures used between all servers. pub mod common; -mod compression; - /// Config management. pub mod config; -pub mod encryption; -pub mod ipc; pub mod oodle; -pub mod packet; /// Patch server-specific code. pub mod patch; @@ -27,6 +22,9 @@ pub mod lobby; /// World server-specific code. pub mod world; +/// Everything packet parsing related. +pub mod packet; + // TODO: make this configurable // See https://ffxiv.consolegameswiki.com/wiki/Servers for a list of possible IDs pub const WORLD_ID: u16 = 63; diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 31a8ea8..8edfc4b 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -7,28 +7,36 @@ use crate::{ ZONE_ID, blowfish::Blowfish, common::timestamp_secs, - encryption::generate_encryption_key, - ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, Server, ServiceAccount}, packet::{ - CompressionType, ConnectionType, PacketSegment, SegmentType, State, parse_packet, - send_packet, + CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, + generate_encryption_key, parse_packet, send_packet, }, }; -use super::client_select_data::ClientSelectData; +use super::{ + client_select_data::ClientSelectData, + ipc::{ + CharacterDetails, Server, ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, + ServiceAccount, + }, +}; +use crate::lobby::ipc::ClientLobbyIpcSegment; pub struct LobbyConnection { pub socket: TcpStream, - pub state: State, + pub state: PacketState, } impl LobbyConnection { - pub async fn parse_packet(&mut self, data: &[u8]) -> (Vec, ConnectionType) { + pub async fn parse_packet( + &mut self, + data: &[u8], + ) -> (Vec>, ConnectionType) { parse_packet(data, &mut self.state).await } - pub async fn send_segment(&mut self, segment: PacketSegment) { + pub async fn send_segment(&mut self, segment: PacketSegment) { send_packet( &mut self.socket, &[segment], @@ -66,7 +74,7 @@ impl LobbyConnection { }] .to_vec(); - let service_account_list = IPCStructData::LobbyServiceAccountList { + let service_account_list = ServerLobbyIpcData::LobbyServiceAccountList { sequence: 0, num_service_accounts: service_accounts.len() as u8, unk1: 3, @@ -74,10 +82,10 @@ impl LobbyConnection { service_accounts: service_accounts.to_vec(), }; - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::LobbyServiceAccountList, + op_code: ServerLobbyIpcType::LobbyServiceAccountList, server_id: 0, timestamp: timestamp_secs(), data: service_account_list, @@ -106,7 +114,7 @@ impl LobbyConnection { // add any empty boys servers.resize(6, Server::default()); - let lobby_server_list = IPCStructData::LobbyServerList { + let lobby_server_list = ServerLobbyIpcData::LobbyServerList { sequence: 0, unk1: 1, offset: 0, @@ -114,10 +122,10 @@ impl LobbyConnection { servers, }; - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::LobbyServerList, + op_code: ServerLobbyIpcType::LobbyServerList, server_id: 0, timestamp: timestamp_secs(), data: lobby_server_list, @@ -133,12 +141,12 @@ impl LobbyConnection { // send them the retainer list { - let lobby_retainer_list = IPCStructData::LobbyRetainerList { unk1: 1 }; + let lobby_retainer_list = ServerLobbyIpcData::LobbyRetainerList { unk1: 1 }; - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::LobbyRetainerList, + op_code: ServerLobbyIpcType::LobbyRetainerList, server_id: 0, timestamp: timestamp_secs(), data: lobby_retainer_list, @@ -215,7 +223,7 @@ impl LobbyConnection { let lobby_character_list = if i == 3 { // On the last packet, add the account-wide information - IPCStructData::LobbyCharacterList { + ServerLobbyIpcData::LobbyCharacterList { sequence, counter: (i * 4) + 1, // TODO: why the + 1 here? num_in_packet: characters_in_packet.len() as u8, @@ -236,7 +244,7 @@ impl LobbyConnection { characters: characters_in_packet, } } else { - IPCStructData::LobbyCharacterList { + ServerLobbyIpcData::LobbyCharacterList { sequence, counter: i * 4, num_in_packet: characters_in_packet.len() as u8, @@ -258,10 +266,10 @@ impl LobbyConnection { } }; - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::LobbyCharacterList, + op_code: ServerLobbyIpcType::LobbyCharacterList, server_id: 0, timestamp: timestamp_secs(), data: lobby_character_list, @@ -282,7 +290,7 @@ impl LobbyConnection { panic!("Missing session id!"); }; - let enter_world = IPCStructData::LobbyEnterWorld { + let enter_world = ServerLobbyIpcData::LobbyEnterWorld { sequence, character_id: 0, content_id: lookup_id, // TODO: shouldn't these be named the same then? @@ -291,10 +299,10 @@ impl LobbyConnection { host: "127.0.0.1".to_string(), }; - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::LobbyEnterWorld, + op_code: ServerLobbyIpcType::LobbyEnterWorld, server_id: 0, timestamp: timestamp_secs(), data: enter_world, @@ -309,7 +317,7 @@ impl LobbyConnection { } pub async fn send_error(&mut self, sequence: u64, error: u32, exd_error: u16) { - let lobby_error = IPCStructData::LobbyError { + let lobby_error = ServerLobbyIpcData::LobbyError { sequence, error, value: 0, @@ -317,10 +325,10 @@ impl LobbyConnection { unk1: 1, }; - let ipc = IPCSegment { + let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, - op_code: IPCOpCode::InitializeChat, + op_code: ServerLobbyIpcType::LobbyError, server_id: 0, timestamp: timestamp_secs(), data: lobby_error, diff --git a/src/lobby/ipc/character_action.rs b/src/lobby/ipc/character_action.rs new file mode 100644 index 0000000..9b2e87e --- /dev/null +++ b/src/lobby/ipc/character_action.rs @@ -0,0 +1,30 @@ +use binrw::binrw; + +#[binrw] +#[derive(Clone, PartialEq, Debug)] +pub enum LobbyCharacterAction { + #[brw(magic = 0x1u8)] + ReserveName, + #[brw(magic = 0x2u8)] + Create, + #[brw(magic = 0x3u8)] + Rename, + #[brw(magic = 0x4u8)] + Delete, + #[brw(magic = 0x5u8)] + Move, + #[brw(magic = 0x6u8)] + RemakeRetainer, + #[brw(magic = 0x7u8)] + RemakeChara, + #[brw(magic = 0x8u8)] + SettingsUploadBegin, + #[brw(magic = 0xCu8)] + SettingsUpload, + #[brw(magic = 0xEu8)] + WorldVisit, + #[brw(magic = 0xFu8)] + DataCenterToken, + #[brw(magic = 0x15u8)] + Request, +} diff --git a/src/lobby/ipc/character_list.rs b/src/lobby/ipc/character_list.rs new file mode 100644 index 0000000..93e99e5 --- /dev/null +++ b/src/lobby/ipc/character_list.rs @@ -0,0 +1,39 @@ +use binrw::binrw; + +use crate::CHAR_NAME_MAX_LENGTH; + +use super::{read_string, write_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], + #[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], +} diff --git a/src/lobby/ipc/client_version_info.rs b/src/lobby/ipc/client_version_info.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lobby/ipc/client_version_info.rs @@ -0,0 +1 @@ + diff --git a/src/lobby/ipc/mod.rs b/src/lobby/ipc/mod.rs new file mode 100644 index 0000000..bec883d --- /dev/null +++ b/src/lobby/ipc/mod.rs @@ -0,0 +1,349 @@ +use binrw::binrw; + +mod character_action; +pub use character_action::LobbyCharacterAction; + +mod character_list; +pub use character_list::CharacterDetails; + +mod client_version_info; + +mod server_list; +pub use server_list::Server; + +mod service_account_list; +pub use service_account_list::ServiceAccount; + +use crate::{ + CHAR_NAME_MAX_LENGTH, + common::{read_string, write_string}, + packet::{IpcSegment, IpcSegmentTrait}, +}; + +pub type ClientLobbyIpcSegment = IpcSegment; + +impl IpcSegmentTrait for ClientLobbyIpcSegment { + fn calc_size(&self) -> u32 { + todo!() + } +} + +// TODO: make generic +impl Default for ClientLobbyIpcSegment { + fn default() -> Self { + Self { + unk1: 0x14, + unk2: 0, + op_code: ClientLobbyIpcType::ClientVersionInfo, + server_id: 0, + timestamp: 0, + data: ClientLobbyIpcData::ClientVersionInfo { + sequence: 0, + session_id: String::new(), + version_info: String::new(), + }, + } + } +} + +pub type ServerLobbyIpcSegment = IpcSegment; + +impl IpcSegmentTrait for ServerLobbyIpcSegment { + fn calc_size(&self) -> u32 { + // 16 is the size of the IPC header + 16 + match self.op_code { + ServerLobbyIpcType::LobbyError => 536, + ServerLobbyIpcType::LobbyServiceAccountList => 24 + (8 * 80), + ServerLobbyIpcType::LobbyCharacterList => 80 + (2 * 1184), + ServerLobbyIpcType::LobbyEnterWorld => 160, + ServerLobbyIpcType::LobbyServerList => 24 + (6 * 84), + ServerLobbyIpcType::LobbyRetainerList => 210, + ServerLobbyIpcType::CharacterCreated => 2568, + } + } +} + +// TODO: make generic +impl Default for ServerLobbyIpcSegment { + fn default() -> Self { + Self { + unk1: 0x14, + unk2: 0, + op_code: ServerLobbyIpcType::LobbyError, + server_id: 0, + timestamp: 0, + data: ServerLobbyIpcData::LobbyError { + sequence: 0, + error: 0, + value: 0, + exd_error_id: 0, + unk1: 0, + }, + } + } +} + +#[binrw] +#[brw(repr = u16)] +#[derive(Clone, PartialEq, Debug)] +pub enum ServerLobbyIpcType { + /// Sent by the server to indicate an lobby error occured + LobbyError = 0x2, + /// 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, + // Assumed what this is, but probably incorrect + CharacterCreated = 0xE, +} + +#[binrw] +#[brw(repr = u16)] +#[derive(Clone, PartialEq, Debug)] +pub enum ClientLobbyIpcType { + /// 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, +} + +#[binrw] +#[br(import(magic: &ClientLobbyIpcType))] +#[derive(Debug, Clone)] +pub enum ClientLobbyIpcData { + // Client->Server IPC + #[br(pre_assert(*magic == ClientLobbyIpcType::ClientVersionInfo))] + ClientVersionInfo { + sequence: u64, + + #[brw(pad_before = 10)] // 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 == ClientLobbyIpcType::RequestCharacterList))] + RequestCharacterList { + #[brw(pad_before = 16)] + sequence: u64, + // TODO: what is in here? + }, + #[br(pre_assert(*magic == ClientLobbyIpcType::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, + #[bw(pad_size_to = 436)] + #[br(count = 436)] + #[br(map = read_string)] + #[bw(map = write_string)] + json: String, + }, + #[br(pre_assert(*magic == ClientLobbyIpcType::RequestEnterWorld))] + RequestEnterWorld { + #[brw(pad_before = 16)] + sequence: u64, + lookup_id: u64, + // TODO: what else is in here? + }, +} + +#[binrw] +#[br(import(_magic: &ServerLobbyIpcType))] +#[derive(Debug, Clone)] +pub enum ServerLobbyIpcData { + 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, + }, + LobbyServerList { + sequence: u64, + unk1: u16, + offset: u16, + #[brw(pad_after = 8)] + num_servers: u32, + #[br(count = 6)] + #[brw(pad_size_to = 504)] + servers: Vec, + }, + LobbyRetainerList { + // TODO: what is in here? + #[brw(pad_before = 7)] + #[brw(pad_after = 202)] + unk1: u8, + }, + 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, + }, + 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, + }, + LobbyError { + sequence: u64, + error: u32, + value: u32, + exd_error_id: u16, + unk1: u16, + }, + NameRejection { + // FIXME: This is opcode 0x2, which is InitializeChat. We need to separate the lobby/zone IPC codes. + unk1: u8, + #[brw(pad_before = 7)] // empty + unk2: u16, + #[brw(pad_before = 6)] // empty + #[brw(pad_after = 516)] // mostly empty + unk3: u32, + }, + CharacterCreated { + #[brw(pad_after = 4)] // empty + unk1: u32, + #[brw(pad_after = 4)] // empty + unk2: u32, + #[brw(pad_before = 32)] // empty + #[brw(pad_after = 1136)] // empty + details: CharacterDetails, + }, +} + +#[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 lobby_ipc_sizes() { + let ipc_types = [ + ( + ServerLobbyIpcType::LobbyServerList, + ServerLobbyIpcData::LobbyServerList { + sequence: 0, + unk1: 0, + offset: 0, + num_servers: 0, + servers: Vec::new(), + }, + ), + ( + ServerLobbyIpcType::LobbyCharacterList, + ServerLobbyIpcData::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(), + }, + ), + ]; + + for (opcode, ipc) in &ipc_types { + let mut cursor = Cursor::new(Vec::new()); + + let ipc_segment = ServerLobbyIpcSegment { + unk1: 0, + unk2: 0, + op_code: opcode.clone(), + 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!", + opcode + ); + } + } +} diff --git a/src/lobby/ipc/server_list.rs b/src/lobby/ipc/server_list.rs new file mode 100644 index 0000000..1af6456 --- /dev/null +++ b/src/lobby/ipc/server_list.rs @@ -0,0 +1,19 @@ +use binrw::binrw; + +use crate::common::{read_string, write_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, +} diff --git a/src/lobby/ipc/service_account_list.rs b/src/lobby/ipc/service_account_list.rs new file mode 100644 index 0000000..f25fe2d --- /dev/null +++ b/src/lobby/ipc/service_account_list.rs @@ -0,0 +1,16 @@ +use binrw::binrw; + +use crate::common::{read_string, write_string}; + +#[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, +} diff --git a/src/lobby/mod.rs b/src/lobby/mod.rs index cf3bc19..40ce1d0 100644 --- a/src/lobby/mod.rs +++ b/src/lobby/mod.rs @@ -1,3 +1,4 @@ pub mod chara_make; mod client_select_data; pub mod connection; +pub mod ipc; diff --git a/src/compression.rs b/src/packet/compression.rs similarity index 84% rename from src/compression.rs rename to src/packet/compression.rs index 48230ae..742cb2e 100644 --- a/src/compression.rs +++ b/src/packet/compression.rs @@ -1,3 +1,4 @@ +use binrw::binrw; use std::fs::write; use std::io::Cursor; @@ -8,12 +9,22 @@ use crate::{ packet::{PacketHeader, PacketSegment}, }; +use super::IpcSegmentTrait; + +#[binrw] +#[brw(repr = u8)] +#[derive(Debug, PartialEq)] +pub enum CompressionType { + Uncompressed = 0, + Oodle = 2, +} + #[binrw::parser(reader, endian)] -pub(crate) fn decompress( +pub(crate) fn decompress( oodle: &mut FFXIVOodle, header: &PacketHeader, encryption_key: Option<&[u8]>, -) -> BinResult> { +) -> BinResult>> { let mut segments = Vec::new(); let size = header.size as usize - std::mem::size_of::(); diff --git a/src/encryption.rs b/src/packet/encryption.rs similarity index 86% rename from src/encryption.rs rename to src/packet/encryption.rs index 1e158bf..618b727 100644 --- a/src/encryption.rs +++ b/src/packet/encryption.rs @@ -1,10 +1,12 @@ use std::fs::write; use std::io::Cursor; -use binrw::{BinRead, BinResult, BinWrite}; +use binrw::BinResult; use crate::blowfish::Blowfish; +use super::IpcSegmentTrait; + const GAME_VERSION: u16 = 7000; pub fn generate_encryption_key(key: &[u8], phrase: &str) -> [u8; 16] { @@ -18,10 +20,10 @@ pub fn generate_encryption_key(key: &[u8], phrase: &str) -> [u8; 16] { } #[binrw::parser(reader, endian)] -pub(crate) fn decrypt(size: u32, encryption_key: Option<&[u8]>) -> BinResult -where - for<'a> T: BinRead = ()> + 'a, -{ +pub(crate) fn decrypt( + size: u32, + encryption_key: Option<&[u8]>, +) -> BinResult { if let Some(encryption_key) = encryption_key { let size = size - (std::mem::size_of::() * 4) as u32; // 16 = header size @@ -43,10 +45,11 @@ where } #[binrw::writer(writer, endian)] -pub(crate) fn encrypt(value: &T, size: u32, encryption_key: Option<&[u8]>) -> BinResult<()> -where - for<'a> T: BinWrite = ()> + 'a, -{ +pub(crate) fn encrypt( + value: &T, + size: u32, + encryption_key: Option<&[u8]>, +) -> BinResult<()> { if let Some(encryption_key) = encryption_key { let size = size - (std::mem::size_of::() * 4) as u32; // 16 = header size diff --git a/src/packet/ipc.rs b/src/packet/ipc.rs new file mode 100644 index 0000000..76e6eec --- /dev/null +++ b/src/packet/ipc.rs @@ -0,0 +1,30 @@ +use binrw::{BinRead, BinWrite, binrw}; + +pub trait IpcSegmentTrait: + for<'a> BinRead = ()> + for<'a> BinWrite = ()> + std::fmt::Debug + 'static +{ + fn calc_size(&self) -> u32; +} + +#[binrw] +#[derive(Debug, Clone)] +pub struct IpcSegment +where + for<'a> OpCode: BinRead = ()> + 'a + std::fmt::Debug, + for<'a> OpCode: BinWrite = ()> + 'a + std::fmt::Debug, + for<'a> Data: BinRead = (&'a OpCode,)> + 'a + std::fmt::Debug, + for<'a> Data: BinWrite = ()> + 'a + std::fmt::Debug, +{ + pub unk1: u8, + pub unk2: u8, + #[br(dbg)] + pub op_code: OpCode, + #[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: Data, +} diff --git a/src/packet/mod.rs b/src/packet/mod.rs new file mode 100644 index 0000000..0906b83 --- /dev/null +++ b/src/packet/mod.rs @@ -0,0 +1,15 @@ +mod packet; +use packet::PacketHeader; +pub use packet::{ + ConnectionType, PacketSegment, PacketState, SegmentType, parse_packet, send_keep_alive, + send_packet, +}; + +mod compression; +pub use compression::CompressionType; + +mod encryption; +pub use encryption::generate_encryption_key; + +mod ipc; +pub use ipc::{IpcSegment, IpcSegmentTrait}; diff --git a/src/packet.rs b/src/packet/packet.rs similarity index 86% rename from src/packet.rs rename to src/packet/packet.rs index 661ba3d..60041c3 100644 --- a/src/packet.rs +++ b/src/packet/packet.rs @@ -7,13 +7,9 @@ use std::{ use binrw::{BinRead, BinWrite, binrw}; use tokio::{io::AsyncWriteExt, net::TcpStream}; -use crate::{ - common::read_string, - compression::decompress, - encryption::{decrypt, encrypt}, - ipc::IPCSegment, - oodle::FFXIVOodle, -}; +use crate::{common::read_string, oodle::FFXIVOodle, packet::encryption::decrypt}; + +use super::{CompressionType, compression::decompress, encryption::encrypt, ipc::IpcSegmentTrait}; #[binrw] #[brw(repr = u16)] @@ -28,7 +24,7 @@ pub enum ConnectionType { #[binrw] #[brw(import(size: u32, encryption_key: Option<&[u8]>))] #[derive(Debug, Clone)] -pub enum SegmentType { +pub enum SegmentType { // Client->Server Packets #[brw(magic = 0x1u32)] InitializeSession { @@ -53,7 +49,7 @@ pub enum SegmentType { Ipc { #[br(parse_with = decrypt, args(size, encryption_key))] #[bw(write_with = encrypt, args(size, encryption_key))] - data: IPCSegment, + data: T, }, #[brw(magic = 0x7u32)] KeepAlive { id: u32, timestamp: u32 }, @@ -75,14 +71,6 @@ pub enum SegmentType { }, } -#[binrw] -#[brw(repr = u8)] -#[derive(Debug, PartialEq)] -pub enum CompressionType { - Uncompressed = 0, - Oodle = 2, -} - #[binrw] #[derive(Debug)] pub struct PacketHeader { @@ -101,7 +89,7 @@ pub struct PacketHeader { #[binrw] #[brw(import(encryption_key: Option<&[u8]>))] #[derive(Debug, Clone)] -pub struct PacketSegment { +pub struct PacketSegment { #[bw(calc = self.calc_size())] #[br(dbg)] pub size: u32, @@ -111,10 +99,10 @@ pub struct PacketSegment { pub target_actor: u32, #[brw(args(size, encryption_key))] #[br(dbg)] - pub segment_type: SegmentType, + pub segment_type: SegmentType, } -impl PacketSegment { +impl PacketSegment { fn calc_size(&self) -> u32 { let header = std::mem::size_of::() * 4; header as u32 @@ -133,13 +121,13 @@ impl PacketSegment { #[binrw] #[brw(import(oodle: &mut FFXIVOodle, encryption_key: Option<&[u8]>))] #[derive(Debug)] -struct Packet { +struct Packet { #[br(dbg)] header: PacketHeader, #[bw(args(encryption_key))] #[br(parse_with = decompress, args(oodle, &header, encryption_key,))] #[br(dbg)] - segments: Vec, + segments: Vec>, } fn dump(msg: &str, data: &[u8]) { @@ -147,10 +135,10 @@ fn dump(msg: &str, data: &[u8]) { panic!("{msg} Dumped to packet.bin."); } -pub async fn send_packet( +pub async fn send_packet( socket: &mut TcpStream, - segments: &[PacketSegment], - state: &mut State, + segments: &[PacketSegment], + state: &mut PacketState, compression_type: CompressionType, ) { let timestamp: u64 = SystemTime::now() @@ -212,14 +200,17 @@ pub async fn send_packet( } // temporary -pub struct State { +pub struct PacketState { pub client_key: Option<[u8; 16]>, pub session_id: Option, pub serverbound_oodle: FFXIVOodle, pub clientbound_oodle: FFXIVOodle, } -pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec, ConnectionType) { +pub async fn parse_packet( + data: &[u8], + state: &mut PacketState, +) -> (Vec>, ConnectionType) { let mut cursor = Cursor::new(data); match Packet::read_le_args( @@ -231,15 +222,6 @@ pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec ) { Ok(packet) => { println!("{:#?}", packet); - - // don't really think this works like I think it does' - /*if packet.header.size as usize != data.len() { - dump( - "Packet size mismatch between what we're given and the header!", - data, - ); - }*/ - (packet.segments, packet.header.connection_type) } Err(err) => { @@ -251,8 +233,13 @@ pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec } } -pub async fn send_keep_alive(socket: &mut TcpStream, state: &mut State, id: u32, timestamp: u32) { - let response_packet = PacketSegment { +pub async fn send_keep_alive( + socket: &mut TcpStream, + state: &mut PacketState, + id: u32, + timestamp: u32, +) { + let response_packet: PacketSegment = PacketSegment { source_actor: 0, target_actor: 0, segment_type: SegmentType::KeepAliveResponse { id, timestamp }, @@ -268,8 +255,9 @@ pub async fn send_keep_alive(socket: &mut TcpStream, state: &mut State, id: u32, #[cfg(test)] mod tests { + // TODO: Restore this test + /* use super::*; - /// Ensure that the packet size as reported matches up with what we write #[test] fn test_packet_sizes() { @@ -303,5 +291,5 @@ mod tests { assert_eq!(buffer.len(), packet_segment.calc_size() as usize); } - } + }*/ } diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 303351b..6e86938 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -1,12 +1,14 @@ use crate::{ CHAR_NAME, CUSTOMIZE_DATA, WORLD_ID, common::timestamp_secs, - ipc::{IPCOpCode, IPCSegment, IPCStructData}, packet::{PacketSegment, SegmentType}, - world::PlayerSpawn, + world::ipc::{PlayerSpawn, ServerZoneIpcData, ServerZoneIpcSegment, ServerZoneIpcType}, }; -use super::{ChatMessage, Position, ZoneConnection}; +use super::{ + ZoneConnection, + ipc::{ChatMessage, Position}, +}; pub struct ChatHandler {} @@ -34,13 +36,13 @@ impl ChatHandler { // send player spawn { - let ipc = IPCSegment { + let ipc = ServerZoneIpcSegment { unk1: 20, unk2: 0, - op_code: IPCOpCode::PlayerSpawn, + op_code: ServerZoneIpcType::PlayerSpawn, server_id: 0, timestamp: timestamp_secs(), - data: IPCStructData::PlayerSpawn(PlayerSpawn { + data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { some_unique_id: 1, content_id: 1, current_world_id: WORLD_ID, diff --git a/src/world/connection.rs b/src/world/connection.rs index ef34c46..1e81f37 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -2,19 +2,24 @@ use tokio::net::TcpStream; use crate::{ common::timestamp_secs, - ipc::{ActorSetPos, IPCOpCode, IPCSegment, IPCStructData}, packet::{ - CompressionType, ConnectionType, PacketSegment, SegmentType, State, parse_packet, + CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, parse_packet, send_packet, }, }; -use super::{InitZone, Position, UpdateClassInfo, Zone}; +use super::{ + Zone, + ipc::{ + ActorSetPos, ClientZoneIpcSegment, InitZone, Position, ServerZoneIpcData, + ServerZoneIpcSegment, ServerZoneIpcType, UpdateClassInfo, + }, +}; pub struct ZoneConnection { pub socket: TcpStream, - pub state: State, + pub state: PacketState, pub player_id: u32, pub zone: Zone, @@ -22,11 +27,14 @@ pub struct ZoneConnection { } impl ZoneConnection { - pub async fn parse_packet(&mut self, data: &[u8]) -> (Vec, ConnectionType) { + pub async fn parse_packet( + &mut self, + data: &[u8], + ) -> (Vec>, ConnectionType) { parse_packet(data, &mut self.state).await } - pub async fn send_segment(&mut self, segment: PacketSegment) { + pub async fn send_segment(&mut self, segment: PacketSegment) { send_packet( &mut self.socket, &[segment], @@ -39,10 +47,10 @@ impl ZoneConnection { pub async fn set_player_position(&mut self, position: Position) { // set pos { - let ipc = IPCSegment { - op_code: IPCOpCode::ActorSetPos, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ActorSetPos, timestamp: timestamp_secs(), - data: IPCStructData::ActorSetPos(ActorSetPos { + data: ServerZoneIpcData::ActorSetPos(ActorSetPos { unk: 0x020fa3b8, position, ..Default::default() @@ -70,10 +78,10 @@ impl ZoneConnection { // Player Class Info { - let ipc = IPCSegment { - op_code: IPCOpCode::UpdateClassInfo, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::UpdateClassInfo, timestamp: timestamp_secs(), - data: IPCStructData::UpdateClassInfo(UpdateClassInfo { + data: ServerZoneIpcData::UpdateClassInfo(UpdateClassInfo { class_id: 35, unknown: 1, synced_level: 90, @@ -93,10 +101,10 @@ impl ZoneConnection { // link shell information { - let ipc = IPCSegment { - op_code: IPCOpCode::LinkShellInformation, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::LinkShellInformation, timestamp: timestamp_secs(), - data: IPCStructData::LinkShellInformation { unk: [0; 456] }, + data: ServerZoneIpcData::LinkShellInformation { unk: [0; 456] }, ..Default::default() }; @@ -112,10 +120,10 @@ impl ZoneConnection { // Init Zone { - let ipc = IPCSegment { - op_code: IPCOpCode::InitZone, + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InitZone, timestamp: timestamp_secs(), - data: IPCStructData::InitZone(InitZone { + data: ServerZoneIpcData::InitZone(InitZone { server_id: 0, zone_id: self.zone.id, weather_id: 1, diff --git a/src/world/actor_control_self.rs b/src/world/ipc/actor_control_self.rs similarity index 100% rename from src/world/actor_control_self.rs rename to src/world/ipc/actor_control_self.rs diff --git a/src/world/chat_message.rs b/src/world/ipc/chat_message.rs similarity index 100% rename from src/world/chat_message.rs rename to src/world/ipc/chat_message.rs diff --git a/src/world/init_zone.rs b/src/world/ipc/init_zone.rs similarity index 100% rename from src/world/init_zone.rs rename to src/world/ipc/init_zone.rs diff --git a/src/world/ipc/mod.rs b/src/world/ipc/mod.rs new file mode 100644 index 0000000..53503af --- /dev/null +++ b/src/world/ipc/mod.rs @@ -0,0 +1,465 @@ +mod chat_message; +use binrw::binrw; +pub use chat_message::ChatMessage; + +mod social_list; +pub use social_list::PlayerEntry; +pub use social_list::SocialList; +pub use social_list::SocialListRequest; +pub use social_list::SocialListRequestType; + +mod player_spawn; +pub use player_spawn::CharacterMode; +pub use player_spawn::PlayerSpawn; + +mod position; +pub use position::Position; + +mod status_effect; +pub use status_effect::StatusEffect; + +mod update_class_info; +pub use update_class_info::UpdateClassInfo; + +mod player_setup; +pub use player_setup::PlayerSetup; + +mod player_stats; +pub use player_stats::PlayerStats; + +mod actor_control_self; +pub use actor_control_self::ActorControlSelf; +pub use actor_control_self::ActorControlType; + +mod init_zone; +pub use init_zone::InitZone; + +use crate::common::read_string; +use crate::common::write_string; +use crate::packet::IpcSegment; +use crate::packet::IpcSegmentTrait; + +pub type ClientZoneIpcSegment = IpcSegment; + +impl IpcSegmentTrait for ClientZoneIpcSegment { + fn calc_size(&self) -> u32 { + todo!() + } +} + +// TODO: make generic +impl Default for ClientZoneIpcSegment { + fn default() -> Self { + Self { + unk1: 0x14, + unk2: 0, + op_code: ClientZoneIpcType::InitRequest, + server_id: 0, + timestamp: 0, + data: ClientZoneIpcData::InitRequest { unk: [0; 105] }, + } + } +} + +pub type ServerZoneIpcSegment = IpcSegment; + +impl IpcSegmentTrait for ServerZoneIpcSegment { + fn calc_size(&self) -> u32 { + // 16 is the size of the IPC header + 16 + match self.op_code { + ServerZoneIpcType::InitializeChat => 8, + ServerZoneIpcType::InitZone => 103, + ServerZoneIpcType::ActorControlSelf => 32, + ServerZoneIpcType::PlayerStats => 224, + ServerZoneIpcType::PlayerSetup => 2784, + ServerZoneIpcType::UpdateClassInfo => 48, + ServerZoneIpcType::PlayerSpawn => 656, + ServerZoneIpcType::InitResponse => 16, + ServerZoneIpcType::LogOutComplete => 8, + ServerZoneIpcType::ActorSetPos => 24, + ServerZoneIpcType::ServerChatMessage => 776, + ServerZoneIpcType::Unk8 => 808, + ServerZoneIpcType::LinkShellInformation => 456, + ServerZoneIpcType::Unk9 => 24, + ServerZoneIpcType::Unk10 => 8, + ServerZoneIpcType::Unk11 => 32, + ServerZoneIpcType::Unk15 => 8, + ServerZoneIpcType::Unk16 => 136, + ServerZoneIpcType::ActorControl => 24, + ServerZoneIpcType::ActorMove => 16, + ServerZoneIpcType::Unk17 => 104, + ServerZoneIpcType::SocialList => 1136, + ServerZoneIpcType::PrepareZoning => 16, + } + } +} + +// TODO: make generic +impl Default for ServerZoneIpcSegment { + fn default() -> Self { + Self { + unk1: 0x14, + unk2: 0, + op_code: ServerZoneIpcType::InitializeChat, + server_id: 0, + timestamp: 0, + data: ServerZoneIpcData::InitializeChat { unk: [0; 8] }, + } + } +} + +// TODO: move to their own files +#[binrw] +#[derive(Debug, Clone, Default)] +pub struct ActorSetPos { + pub unk: u32, + pub layer_id: u32, + pub position: Position, + pub unk3: u32, +} + +#[binrw] +#[brw(repr = u8)] +#[derive(Clone, PartialEq, Debug)] +pub enum GameMasterCommandType { + ChangeTerritory = 0x58, +} + +#[binrw] +#[brw(repr = u16)] +#[derive(Clone, PartialEq, Debug)] +pub enum ServerZoneIpcType { + /// Sent by the server to Initialize something chat-related? + InitializeChat = 0x2, + /// 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 server to spawn the player in + PlayerSpawn = 0x1AB, + /// Sent by the server as response to ZoneInitRequest. + InitResponse = 0x2D0, + // Sent by the server to indicate the log out is complete + LogOutComplete = 0x369, + // Sent by the server to modify the client's position + ActorSetPos = 0x223, + // Sent by the server when they send a chat message + ServerChatMessage = 0x196, + // Unknown, server sends to the client before player spawn + Unk8 = 0x134, + // Unknown, but seems to contain information on cross-world linkshells + LinkShellInformation = 0x234, + // Unknown, server sends to the client before player spawn + Unk9 = 0x189, + // Unknown, server sends to the client before player spawn. + // Seems to the same across two different characters? + Unk10 = 0x110, + // Unknown, server sends this in response to Unk7 + Unk11 = 0x156, + // Sent by the server when it wants the client to... prepare to zone? + PrepareZoning = 0x308, + // Sent by the server??? + Unk15 = 0x28C, + // Sent by the server before init zone??? + Unk16 = 0x3AB, + // Sent by the server + ActorControl = 0x1B9, + // Sent by the server + ActorMove = 0x3D8, + // Sent by the server + Unk17 = 0x2A1, + // Sent by the server in response to SocialListRequest + SocialList = 0x36C, +} + +#[binrw] +#[brw(repr = u16)] +#[derive(Clone, PartialEq, Debug)] +pub enum ClientZoneIpcType { + /// 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 client when they're done loading and they need to be spawned in + FinishLoading = 0x397, // TODO: assumed + // 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 = 0x2E5, + // 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, + // Sent by the client when it requests the friends list and other related info + SocialListRequest = 0x1A1, + // 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 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, + // Unknown, client sends this for ??? + Unk12 = 0x0E9, + // Sent by the client when the character walks into a zone transistion + EnterZoneLine = 0x205, + // Sent by the client after we sent a InitZone in TravelToZone?? + // TODO: Actually, I don't think is real... + Unk13 = 0x2EE, + // Sent by the client for unknown reasons + Unk14 = 0x87, +} + +#[binrw] +#[br(import(_magic: &ServerZoneIpcType))] +#[derive(Debug, Clone)] +pub enum ServerZoneIpcData { + InitializeChat { + unk: [u8; 8], + }, + InitResponse { + unk1: u64, + character_id: u32, + unk2: u32, + }, + InitZone(InitZone), + ActorControlSelf(ActorControlSelf), + PlayerStats(PlayerStats), + PlayerSetup(PlayerSetup), + UpdateClassInfo(UpdateClassInfo), + PlayerSpawn(PlayerSpawn), + LogOutComplete { + // TODO: guessed + unk: [u8; 8], + }, + ActorSetPos(ActorSetPos), + ServerChatMessage { + unk: u8, // channel? + #[brw(pad_after = 775)] + #[br(count = 775)] + #[br(map = read_string)] + #[bw(map = write_string)] + message: String, + }, + Unk8 { + unk: [u8; 808], + }, + LinkShellInformation { + unk: [u8; 456], + }, + Unk9 { + unk: [u8; 24], + }, + Unk10 { + unk: u64, + }, + Unk11 { + timestamp: u32, + #[brw(pad_after = 24)] // empty bytes + unk: u32, + }, + PrepareZoning { + unk: [u32; 4], + }, + Unk15 { + unk: u32, + player_id: u32, + }, + Unk16 { + unk: [u8; 136], + }, + ActorControl { + #[brw(pad_after = 20)] // empty + unk: u32, + }, + ActorMove { + #[brw(pad_after = 4)] // empty + pos: Position, + }, + Unk17 { + unk: [u8; 104], + }, + SocialList(SocialList), +} + +#[binrw] +#[br(import(magic: &ClientZoneIpcType))] +#[derive(Debug, Clone)] +pub enum ClientZoneIpcData { + #[br(pre_assert(*magic == ClientZoneIpcType::InitRequest))] + InitRequest { + // TODO: full of possibly interesting information + #[br(dbg)] + unk: [u8; 105], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::FinishLoading))] + FinishLoading { + // TODO: full of possibly interesting information + unk: [u8; 72], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk1))] + Unk1 { + // TODO: full of possibly interesting information + unk: [u8; 32], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk2))] + Unk2 { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk3))] + Unk3 { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk4))] + Unk4 { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::SetSearchInfoHandler))] + SetSearchInfoHandler { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk5))] + Unk5 { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::SocialListRequest))] + SocialListRequest(SocialListRequest), + #[br(pre_assert(*magic == ClientZoneIpcType::Unk7))] + Unk7 { + // TODO: full of possibly interesting information + timestamp: u32, + #[brw(pad_before = 8)] // empty bytes + #[brw(pad_after = 4)] // empty bytes + unk1: [u8; 16], // something + }, + #[br(pre_assert(*magic == ClientZoneIpcType::UpdatePositionHandler))] + UpdatePositionHandler { + // TODO: full of possibly interesting information + unk: [u8; 24], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::LogOut))] + LogOut { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Disconnected))] + Disconnected { + // TODO: full of possibly interesting information + unk: [u8; 8], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::ChatMessage))] + ChatMessage(ChatMessage), + #[br(pre_assert(*magic == ClientZoneIpcType::GameMasterCommand))] + GameMasterCommand { + // TODO: incomplete + command: GameMasterCommandType, + #[br(pad_before = 3)] // idk, not empty though + arg: u32, + #[br(dbg)] + unk: [u8; 24], + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk12))] + Unk12 { + unk: [u8; 8], // TODO: unknown + }, + #[br(pre_assert(*magic == ClientZoneIpcType::EnterZoneLine))] + EnterZoneLine { + exit_box_id: u32, + position: Position, + #[brw(pad_after = 4)] // empty + landset_index: i32, + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk13))] + Unk13 { + #[br(dbg)] + unk: [u8; 16], // TODO: unknown + }, + #[br(pre_assert(*magic == ClientZoneIpcType::Unk14))] + Unk14 { + #[br(dbg)] + unk: [u8; 8], // TODO: unknown + }, +} + +#[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 world_ipc_sizes() { + let ipc_types = [ + ( + ServerZoneIpcType::ActorControlSelf, + ServerZoneIpcData::ActorControlSelf(ActorControlSelf::default()), + ), + ( + ServerZoneIpcType::InitializeChat, + ServerZoneIpcData::InitializeChat { unk: [0; 8] }, + ), + ( + ServerZoneIpcType::PlayerStats, + ServerZoneIpcData::PlayerStats(PlayerStats::default()), + ), + ( + ServerZoneIpcType::PlayerSetup, + ServerZoneIpcData::PlayerSetup(PlayerSetup::default()), + ), + ( + ServerZoneIpcType::UpdateClassInfo, + ServerZoneIpcData::UpdateClassInfo(UpdateClassInfo::default()), + ), + ( + ServerZoneIpcType::PlayerSpawn, + ServerZoneIpcData::PlayerSpawn(PlayerSpawn::default()), + ), + ( + ServerZoneIpcType::ActorSetPos, + ServerZoneIpcData::ActorSetPos(ActorSetPos::default()), + ), + ]; + + for (opcode, data) in &ipc_types { + let mut cursor = Cursor::new(Vec::new()); + + let ipc_segment = ServerZoneIpcSegment { + unk1: 0, + unk2: 0, + op_code: opcode.clone(), // doesn't matter for this test + server_id: 0, + timestamp: 0, + data: data.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!", + opcode + ); + } + } +} diff --git a/src/world/player_setup.rs b/src/world/ipc/player_setup.rs similarity index 100% rename from src/world/player_setup.rs rename to src/world/ipc/player_setup.rs diff --git a/src/world/player_spawn.rs b/src/world/ipc/player_spawn.rs similarity index 100% rename from src/world/player_spawn.rs rename to src/world/ipc/player_spawn.rs diff --git a/src/world/player_stats.rs b/src/world/ipc/player_stats.rs similarity index 100% rename from src/world/player_stats.rs rename to src/world/ipc/player_stats.rs diff --git a/src/world/position.rs b/src/world/ipc/position.rs similarity index 100% rename from src/world/position.rs rename to src/world/ipc/position.rs diff --git a/src/world/social_list.rs b/src/world/ipc/social_list.rs similarity index 100% rename from src/world/social_list.rs rename to src/world/ipc/social_list.rs diff --git a/src/world/status_effect.rs b/src/world/ipc/status_effect.rs similarity index 100% rename from src/world/status_effect.rs rename to src/world/ipc/status_effect.rs diff --git a/src/world/update_class_info.rs b/src/world/ipc/update_class_info.rs similarity index 100% rename from src/world/update_class_info.rs rename to src/world/ipc/update_class_info.rs diff --git a/src/world/mod.rs b/src/world/mod.rs index 0ec0d6f..20a0b23 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -1,28 +1,4 @@ -mod player_spawn; -pub use player_spawn::CharacterMode; -pub use player_spawn::PlayerSpawn; - -mod position; -pub use position::Position; - -mod status_effect; -pub use status_effect::StatusEffect; - -mod update_class_info; -pub use update_class_info::UpdateClassInfo; - -mod player_setup; -pub use player_setup::PlayerSetup; - -mod player_stats; -pub use player_stats::PlayerStats; - -mod actor_control_self; -pub use actor_control_self::ActorControlSelf; -pub use actor_control_self::ActorControlType; - -mod init_zone; -pub use init_zone::InitZone; +pub mod ipc; mod zone; pub use zone::Zone; @@ -32,12 +8,3 @@ pub use chat_handler::ChatHandler; mod connection; pub use connection::ZoneConnection; - -mod chat_message; -pub use chat_message::ChatMessage; - -mod social_list; -pub use social_list::PlayerEntry; -pub use social_list::SocialList; -pub use social_list::SocialListRequest; -pub use social_list::SocialListRequestType;