1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-06-08 09:27:45 +00:00

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.
This commit is contained in:
Joshua Goins 2025-03-16 17:43:29 -04:00
parent 726d351f8b
commit f5d75301b2
31 changed files with 1217 additions and 983 deletions

View file

@ -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::<ServerLobbyIpcSegment>(
&mut connection.socket,
&mut connection.state,
*id,

View file

@ -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::<ServerZoneIpcSegment>(
&mut connection.socket,
&mut connection.state,
*id,

View file

@ -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<ServiceAccount>,
},
#[br(pre_assert(false))]
LobbyServerList {
sequence: u64,
unk1: u16,
offset: u16,
#[brw(pad_after = 8)]
num_servers: u32,
#[br(count = 6)]
#[brw(pad_size_to = 504)]
servers: Vec<Server>,
},
#[br(pre_assert(false))]
LobbyRetainerList {
// TODO: what is in here?
#[brw(pad_before = 7)]
#[brw(pad_after = 202)]
unk1: u8,
},
#[br(pre_assert(false))]
LobbyCharacterList {
sequence: u64,
counter: u8,
#[brw(pad_after = 2)]
num_in_packet: u8,
unk1: u8,
unk2: u8,
unk3: u8,
/// Set to 128 if legacy character
unk4: u8,
unk5: [u32; 7],
unk6: u8,
veteran_rank: u8,
#[brw(pad_after = 1)]
unk7: u8,
days_subscribed: u32,
remaining_days: u32,
days_to_next_rank: u32,
max_characters_on_world: u16,
unk8: u16,
#[brw(pad_after = 12)]
entitled_expansion: u32,
#[br(count = 2)]
#[brw(pad_size_to = 2368)]
characters: Vec<CharacterDetails>,
},
#[br(pre_assert(false))]
LobbyEnterWorld {
sequence: u64,
character_id: u32,
#[brw(pad_before = 4)]
content_id: u64,
#[brw(pad_before = 4)]
#[bw(pad_size_to = 66)]
#[br(count = 66)]
#[br(map = read_string)]
#[bw(map = write_string)]
session_id: String,
port: u16,
#[brw(pad_after = 16)]
#[br(count = 48)]
#[br(map = read_string)]
#[bw(map = write_string)]
host: String,
},
#[br(pre_assert(false))]
InitializeChat { unk: [u8; 8] },
#[br(pre_assert(false))]
InitResponse {
unk1: u64,
character_id: u32,
unk2: u32,
},
#[br(pre_assert(false))]
InitZone(InitZone),
#[br(pre_assert(false))]
ActorControlSelf(ActorControlSelf),
#[br(pre_assert(false))]
PlayerStats(PlayerStats),
#[br(pre_assert(false))]
PlayerSetup(PlayerSetup),
#[br(pre_assert(false))]
UpdateClassInfo(UpdateClassInfo),
#[br(pre_assert(false))]
PlayerSpawn(PlayerSpawn),
#[br(pre_assert(false))]
LogOutComplete {
// TODO: guessed
unk: [u8; 8],
},
#[br(pre_assert(false))]
ActorSetPos(ActorSetPos),
#[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
);
}
}
}

View file

@ -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;

View file

@ -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<PacketSegment>, ConnectionType) {
pub async fn parse_packet(
&mut self,
data: &[u8],
) -> (Vec<PacketSegment<ClientLobbyIpcSegment>>, 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<ServerLobbyIpcSegment>) {
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,

View file

@ -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,
}

View file

@ -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],
}

View file

@ -0,0 +1 @@

349
src/lobby/ipc/mod.rs Normal file
View file

@ -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<ClientLobbyIpcType, ClientLobbyIpcData>;
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<ServerLobbyIpcType, ServerLobbyIpcData>;
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<ServiceAccount>,
},
LobbyServerList {
sequence: u64,
unk1: u16,
offset: u16,
#[brw(pad_after = 8)]
num_servers: u32,
#[br(count = 6)]
#[brw(pad_size_to = 504)]
servers: Vec<Server>,
},
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<CharacterDetails>,
},
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
);
}
}
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -1,3 +1,4 @@
pub mod chara_make;
mod client_select_data;
pub mod connection;
pub mod ipc;

View file

@ -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<T: IpcSegmentTrait>(
oodle: &mut FFXIVOodle,
header: &PacketHeader,
encryption_key: Option<&[u8]>,
) -> BinResult<Vec<PacketSegment>> {
) -> BinResult<Vec<PacketSegment<T>>> {
let mut segments = Vec::new();
let size = header.size as usize - std::mem::size_of::<PacketHeader>();

View file

@ -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<T>(size: u32, encryption_key: Option<&[u8]>) -> BinResult<T>
where
for<'a> T: BinRead<Args<'a> = ()> + 'a,
{
pub(crate) fn decrypt<T: IpcSegmentTrait>(
size: u32,
encryption_key: Option<&[u8]>,
) -> BinResult<T> {
if let Some(encryption_key) = encryption_key {
let size = size - (std::mem::size_of::<u32>() * 4) as u32; // 16 = header size
@ -43,10 +45,11 @@ where
}
#[binrw::writer(writer, endian)]
pub(crate) fn encrypt<T>(value: &T, size: u32, encryption_key: Option<&[u8]>) -> BinResult<()>
where
for<'a> T: BinWrite<Args<'a> = ()> + 'a,
{
pub(crate) fn encrypt<T: IpcSegmentTrait>(
value: &T,
size: u32,
encryption_key: Option<&[u8]>,
) -> BinResult<()> {
if let Some(encryption_key) = encryption_key {
let size = size - (std::mem::size_of::<u32>() * 4) as u32; // 16 = header size

30
src/packet/ipc.rs Normal file
View file

@ -0,0 +1,30 @@
use binrw::{BinRead, BinWrite, binrw};
pub trait IpcSegmentTrait:
for<'a> BinRead<Args<'a> = ()> + for<'a> BinWrite<Args<'a> = ()> + std::fmt::Debug + 'static
{
fn calc_size(&self) -> u32;
}
#[binrw]
#[derive(Debug, Clone)]
pub struct IpcSegment<OpCode, Data>
where
for<'a> OpCode: BinRead<Args<'a> = ()> + 'a + std::fmt::Debug,
for<'a> OpCode: BinWrite<Args<'a> = ()> + 'a + std::fmt::Debug,
for<'a> Data: BinRead<Args<'a> = (&'a OpCode,)> + 'a + std::fmt::Debug,
for<'a> Data: BinWrite<Args<'a> = ()> + '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,
}

15
src/packet/mod.rs Normal file
View file

@ -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};

View file

@ -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<T: IpcSegmentTrait> {
// 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<T: IpcSegmentTrait> {
#[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<T>,
}
impl PacketSegment {
impl<T: IpcSegmentTrait> PacketSegment<T> {
fn calc_size(&self) -> u32 {
let header = std::mem::size_of::<u32>() * 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<T: IpcSegmentTrait> {
#[br(dbg)]
header: PacketHeader,
#[bw(args(encryption_key))]
#[br(parse_with = decompress, args(oodle, &header, encryption_key,))]
#[br(dbg)]
segments: Vec<PacketSegment>,
segments: Vec<PacketSegment<T>>,
}
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<T: IpcSegmentTrait>(
socket: &mut TcpStream,
segments: &[PacketSegment],
state: &mut State,
segments: &[PacketSegment<T>],
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<String>,
pub serverbound_oodle: FFXIVOodle,
pub clientbound_oodle: FFXIVOodle,
}
pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec<PacketSegment>, ConnectionType) {
pub async fn parse_packet<T: IpcSegmentTrait>(
data: &[u8],
state: &mut PacketState,
) -> (Vec<PacketSegment<T>>, 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<PacketSegment>
) {
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<PacketSegment>
}
}
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<T: IpcSegmentTrait>(
socket: &mut TcpStream,
state: &mut PacketState,
id: u32,
timestamp: u32,
) {
let response_packet: PacketSegment<T> = 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);
}
}
}*/
}

View file

@ -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,

View file

@ -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<PacketSegment>, ConnectionType) {
pub async fn parse_packet(
&mut self,
data: &[u8],
) -> (Vec<PacketSegment<ClientZoneIpcSegment>>, 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<ServerZoneIpcSegment>) {
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,

465
src/world/ipc/mod.rs Normal file
View file

@ -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<ClientZoneIpcType, ClientZoneIpcData>;
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<ServerZoneIpcType, ServerZoneIpcData>;
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
);
}
}
}

View file

@ -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;