1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-23 07:37:46 +00:00

Create dedicated Connection implementations to handle future server work

The current situation of throw-every-piece-of-logic-into-one-file for each
server isn't working out. So now there is a new ZoneConnection/LobbyConnection
struct that will be delegating tasks to their own Handlers (for example, there
could be a ChatHandler.)

I'm not sure how well this architecture will scale, but it's better than what
we have right now.
This commit is contained in:
Joshua Goins 2025-03-15 20:36:39 -04:00
parent 059becf55f
commit f372f3173d
15 changed files with 721 additions and 795 deletions

View file

@ -1,21 +1,11 @@
use std::cmp::min;
use std::time::{SystemTime, UNIX_EPOCH};
use kawari::blowfish::Blowfish;
use kawari::CONTENT_ID;
use kawari::chara_make::CharaMake;
use kawari::client_select_data::{ClientCustomizeData, ClientSelectData};
use kawari::encryption::generate_encryption_key;
use kawari::ipc::{
CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, LobbyCharacterAction, Server,
ServiceAccount,
};
use kawari::ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, LobbyCharacterAction};
use kawari::lobby::connection::LobbyConnection;
use kawari::oodle::FFXIVOodle;
use kawari::packet::{
CompressionType, PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet,
};
use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, WORLD_NAME, ZONE_ID};
use tokio::io::{AsyncReadExt, WriteHalf};
use tokio::net::{TcpListener, TcpStream};
use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive};
use tokio::io::AsyncReadExt;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
@ -27,29 +17,33 @@ async fn main() {
loop {
let (socket, _) = listener.accept().await.unwrap();
let (mut read, mut write) = tokio::io::split(socket);
let mut state = State {
let state = State {
client_key: None,
session_id: None,
clientbound_oodle: FFXIVOodle::new(),
serverbound_oodle: FFXIVOodle::new(),
player_id: None,
};
let mut connection = LobbyConnection { socket, state };
tokio::spawn(async move {
let mut buf = [0; 2056];
loop {
let n = read.read(&mut buf).await.expect("Failed to read data!");
let n = connection
.socket
.read(&mut buf)
.await
.expect("Failed to read data!");
if n != 0 {
tracing::info!("read {} bytes", n);
let (segments, _) = parse_packet(&buf[..n], &mut state).await;
let (segments, _) = connection.parse_packet(&buf[..n]).await;
for segment in &segments {
match &segment.segment_type {
SegmentType::InitializeEncryption { phrase, key } => {
initialize_encryption(&mut write, &mut state, phrase, key).await;
connection.initialize_encryption(phrase, key).await;
}
SegmentType::Ipc { data } => match &data.data {
IPCStructData::ClientVersionInfo {
@ -60,23 +54,17 @@ async fn main() {
"Client {session_id} ({version_info}) logging in!"
);
state.session_id = Some(session_id.clone());
connection.state.session_id = Some(session_id.clone());
send_account_list(&mut write, &mut state).await;
connection.send_account_list().await;
}
IPCStructData::RequestCharacterList { sequence } => {
tracing::info!("Client is requesting character list...");
send_lobby_info(&mut write, &mut state, *sequence).await;
connection.send_lobby_info(*sequence).await;
}
IPCStructData::LobbyCharacterAction {
character_id,
character_index,
action,
world_id,
name,
json,
..
action, name, json, ..
} => {
match action {
LobbyCharacterAction::ReserveName => {
@ -136,17 +124,14 @@ async fn main() {
},
};
let response_packet = PacketSegment {
connection
.send_segment(PacketSegment {
source_actor: 0x0,
target_actor: 0x0,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Uncompressed,
)
segment_type: SegmentType::Ipc {
data: ipc,
},
})
.await;
}
}
@ -180,17 +165,14 @@ async fn main() {
},
};
let response_packet = PacketSegment {
connection
.send_segment(PacketSegment {
source_actor: 0x0,
target_actor: 0x0,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Uncompressed,
)
segment_type: SegmentType::Ipc {
data: ipc,
},
})
.await;
}
}
@ -212,15 +194,20 @@ async fn main() {
} => {
tracing::info!("Client is joining the world...");
send_enter_world(&mut write, &mut state, *sequence, *lookup_id)
.await;
connection.send_enter_world(*sequence, *lookup_id).await;
}
_ => {
panic!("The server is recieving a IPC response packet!")
}
},
SegmentType::KeepAlive { id, timestamp } => {
send_keep_alive(&mut write, &mut state, *id, *timestamp).await
send_keep_alive(
&mut connection.socket,
&mut connection.state,
*id,
*timestamp,
)
.await
}
_ => {
panic!("The server is recieving a response packet!")
@ -232,322 +219,3 @@ async fn main() {
});
}
}
async fn initialize_encryption(
socket: &mut WriteHalf<TcpStream>,
state: &mut State,
phrase: &str,
key: &[u8; 4],
) {
// Generate an encryption key for this client
state.client_key = Some(generate_encryption_key(key, phrase));
let mut data = 0xE0003C2Au32.to_le_bytes().to_vec();
data.resize(0x280, 0);
let blowfish = Blowfish::new(&state.client_key.unwrap());
blowfish.encrypt(&mut data);
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::InitializationEncryptionResponse { data },
};
send_packet(
socket,
&[response_packet],
state,
CompressionType::Uncompressed,
)
.await;
}
async fn send_account_list(socket: &mut WriteHalf<TcpStream>, state: &mut State) {
let timestamp: u32 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!")
.as_secs()
.try_into()
.unwrap();
// send the client the service account list
let service_accounts = [ServiceAccount {
id: 0x002E4A2B,
unk1: 0,
index: 0,
name: "FINAL FANTASY XIV".to_string(),
}]
.to_vec();
let service_account_list = IPCStructData::LobbyServiceAccountList {
sequence: 0,
num_service_accounts: service_accounts.len() as u8,
unk1: 3,
unk2: 0x99,
service_accounts: service_accounts.to_vec(),
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyServiceAccountList,
server_id: 0,
timestamp,
data: service_account_list,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
socket,
&[response_packet],
state,
CompressionType::Uncompressed,
)
.await;
}
async fn send_lobby_info(socket: &mut WriteHalf<TcpStream>, state: &mut State, sequence: u64) {
let timestamp: u32 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!")
.as_secs()
.try_into()
.unwrap();
let mut packets = Vec::new();
// send them the server list
{
let mut servers = [Server {
id: WORLD_ID,
index: 0,
flags: 0,
icon: 0,
name: WORLD_NAME.to_string(),
}]
.to_vec();
// add any empty boys
servers.resize(6, Server::default());
let lobby_server_list = IPCStructData::LobbyServerList {
sequence: 0,
unk1: 1,
offset: 0,
num_servers: 1,
servers,
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyServerList,
server_id: 0,
timestamp,
data: lobby_server_list,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
packets.push(response_packet);
}
// send them the retainer list
{
let lobby_retainer_list = IPCStructData::LobbyRetainerList { unk1: 1 };
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyRetainerList,
server_id: 0,
timestamp,
data: lobby_retainer_list,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
packets.push(response_packet);
}
send_packet(socket, &packets, state, CompressionType::Uncompressed).await;
// now send them the character list
{
let select_data = ClientSelectData {
game_name_unk: "Final Fantasy".to_string(),
current_class: 2,
class_levels: [5; 30],
race: 0,
subrace: 0,
gender: 0,
birth_month: 5,
birth_day: 5,
guardian: 2,
unk8: 0,
unk9: 0,
zone_id: ZONE_ID as i32,
unk11: 0,
customize: CUSTOMIZE_DATA,
unk12: 0,
unk13: 0,
unk14: [0; 10],
unk15: 0,
unk16: 0,
legacy_character: 0,
unk18: 0,
unk19: 0,
unk20: 0,
unk21: String::new(),
unk22: 0,
unk23: 0,
};
let mut characters = vec![CharacterDetails {
id: 0,
content_id: CONTENT_ID,
index: 0,
unk1: [0; 16],
origin_server_id: WORLD_ID,
current_server_id: WORLD_ID,
character_name: CHAR_NAME.to_string(),
origin_server_name: WORLD_NAME.to_string(),
current_server_name: WORLD_NAME.to_string(),
character_detail_json: select_data.to_json(),
unk2: [0; 20],
}];
for i in 0..4 {
let mut characters_in_packet = Vec::new();
for _ in 0..min(characters.len(), 2) {
characters_in_packet.push(characters.swap_remove(0));
}
// add any empty boys
characters_in_packet.resize(2, CharacterDetails::default());
let lobby_character_list = if i == 3 {
// On the last packet, add the account-wide information
IPCStructData::LobbyCharacterList {
sequence,
counter: (i * 4) + 1, // TODO: why the + 1 here?
num_in_packet: characters_in_packet.len() as u8,
unk1: 0,
unk2: 0,
unk3: 0,
unk4: 128,
unk5: [0; 7],
unk6: 0,
veteran_rank: 0,
unk7: 0,
days_subscribed: 5,
remaining_days: 5,
days_to_next_rank: 0,
unk8: 8,
max_characters_on_world: 2,
entitled_expansion: 4,
characters: characters_in_packet,
}
} else {
IPCStructData::LobbyCharacterList {
sequence,
counter: i * 4,
num_in_packet: characters_in_packet.len() as u8,
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: characters_in_packet,
}
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyCharacterList,
server_id: 0,
timestamp,
data: lobby_character_list,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
socket,
&[response_packet],
state,
CompressionType::Uncompressed,
)
.await;
}
}
}
async fn send_enter_world(
socket: &mut WriteHalf<TcpStream>,
state: &mut State,
sequence: u64,
lookup_id: u64,
) {
let Some(session_id) = &state.session_id else {
panic!("Missing session id!");
};
let timestamp: u32 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!")
.as_secs()
.try_into()
.unwrap();
let enter_world = IPCStructData::LobbyEnterWorld {
sequence,
character_id: 0,
content_id: lookup_id, // TODO: shouldn't these be named the same then?
session_id: session_id.clone(),
port: 7100,
host: "127.0.0.1".to_string(),
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyEnterWorld,
server_id: 0,
timestamp,
data: enter_world,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
socket,
&[response_packet],
state,
CompressionType::Uncompressed,
)
.await;
}

View file

@ -1,20 +1,16 @@
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use binrw::BinRead;
use kawari::client_select_data::ClientCustomizeData;
use kawari::ipc::{ActorSetPos, GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData};
use kawari::ipc::{GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData};
use kawari::oodle::FFXIVOodle;
use kawari::packet::{
CompressionType, PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet,
};
use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive};
use kawari::world::{
ActorControlSelf, ActorControlType, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
UpdateClassInfo, Zone,
ActorControlSelf, ActorControlType, ChatHandler, InitZone, PlayerSetup, PlayerSpawn,
PlayerStats, Position, UpdateClassInfo, Zone, ZoneConnection,
};
use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, ZONE_ID};
use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, ZONE_ID, timestamp_secs};
use tokio::io::AsyncReadExt;
use tokio::net::TcpListener;
@ -28,41 +24,40 @@ async fn main() {
loop {
let (socket, _) = listener.accept().await.unwrap();
let (mut read, mut write) = tokio::io::split(socket);
let mut state = State {
let state = State {
client_key: None,
session_id: None,
clientbound_oodle: FFXIVOodle::new(),
serverbound_oodle: FFXIVOodle::new(),
player_id: None,
};
let zone = Zone::load(010);
let mut exit_position = None;
let mut connection = ZoneConnection {
socket,
state,
player_id: 0,
};
tokio::spawn(async move {
let mut buf = [0; 2056];
loop {
let n = read.read(&mut buf).await.expect("Failed to read data!");
let n = connection
.socket
.read(&mut buf)
.await
.expect("Failed to read data!");
if n != 0 {
println!("recieved {n} bytes...");
let (segments, connection_type) = parse_packet(&buf[..n], &mut state).await;
let (segments, connection_type) = connection.parse_packet(&buf[..n]).await;
for segment in &segments {
let timestamp_secs = || {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!")
.as_secs()
.try_into()
.unwrap()
};
match &segment.segment_type {
SegmentType::InitializeSession { player_id } => {
state.player_id = Some(*player_id);
connection.player_id = *player_id;
// We have send THEM a keep alive
{
@ -73,20 +68,15 @@ async fn main() {
.try_into()
.unwrap();
let response_packet = PacketSegment {
connection
.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::KeepAlive {
id: 0xE0037603u32,
timestamp,
},
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -96,19 +86,14 @@ async fn main() {
"Client {player_id} is initializing zone session..."
);
let response_packet = PacketSegment {
connection
.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::ZoneInitialize {
player_id: *player_id,
},
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
kawari::packet::ConnectionType::Chat => {
@ -117,19 +102,14 @@ async fn main() {
);
{
let response_packet = PacketSegment {
connection
.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::ZoneInitialize {
player_id: *player_id,
},
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -143,17 +123,12 @@ async fn main() {
data: IPCStructData::InitializeChat { unk: [0; 8] },
};
let response_packet = PacketSegment {
connection
.send_segment(PacketSegment {
source_actor: *player_id,
target_actor: *player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
}
@ -179,22 +154,17 @@ async fn main() {
timestamp: timestamp_secs(),
data: IPCStructData::InitResponse {
unk1: 0,
character_id: state.player_id.unwrap(),
character_id: connection.player_id,
unk2: 0,
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -220,17 +190,12 @@ async fn main() {
),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -250,17 +215,12 @@ async fn main() {
}),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -281,17 +241,12 @@ async fn main() {
}),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -314,17 +269,12 @@ async fn main() {
),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -341,17 +291,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -366,17 +311,12 @@ async fn main() {
data: IPCStructData::Unk9 { unk: [0; 24] },
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -393,17 +333,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -418,17 +353,12 @@ async fn main() {
data: IPCStructData::Unk8 { unk: [0; 808] },
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -448,17 +378,12 @@ async fn main() {
}),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
}
@ -481,17 +406,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -537,17 +457,12 @@ async fn main() {
}),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -564,17 +479,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -621,17 +531,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
}
@ -652,72 +557,24 @@ async fn main() {
data: IPCStructData::LogOutComplete { unk: [0; 8] },
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
}
IPCStructData::Disconnected { .. } => {
tracing::info!("Client disconnected!");
}
IPCStructData::ChatMessage { message, .. } => {
tracing::info!("Client sent chat message: {message}!");
let parts: Vec<&str> = message.split(' ').collect();
match parts[0] {
"!setpos" => {
let pos_x = parts[1].parse::<f32>().unwrap();
let pos_y = parts[2].parse::<f32>().unwrap();
let pos_z = parts[3].parse::<f32>().unwrap();
// set pos
{
let ipc = IPCSegment {
unk1: 14,
unk2: 0,
op_code: IPCOpCode::ActorSetPos,
server_id: WORLD_ID,
timestamp: timestamp_secs(),
data: IPCStructData::ActorSetPos(
ActorSetPos {
unk: 0x020fa3b8,
position: Position {
x: pos_x,
y: pos_y,
z: pos_z,
},
..Default::default()
},
),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
segment_type: SegmentType::Ipc {
data: ipc,
},
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
IPCStructData::ChatMessage(chat_message) => {
ChatHandler::handle_chat_message(
&mut connection,
chat_message,
)
.await;
}
}
_ => tracing::info!("Unrecognized debug command!"),
}
.await
}
IPCStructData::GameMasterCommand { command, arg, .. } => {
tracing::info!("Got a game master command!");
@ -739,19 +596,14 @@ async fn main() {
}),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc {
data: ipc,
},
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
}
@ -800,17 +652,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -827,17 +674,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -869,17 +711,12 @@ async fn main() {
}),
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
@ -896,17 +733,12 @@ async fn main() {
},
};
let response_packet = PacketSegment {
source_actor: state.player_id.unwrap(),
target_actor: state.player_id.unwrap(),
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
})
.await;
}
}
@ -922,7 +754,13 @@ async fn main() {
}
}
SegmentType::KeepAlive { id, timestamp } => {
send_keep_alive(&mut write, &mut state, *id, *timestamp).await
send_keep_alive(
&mut connection.socket,
&mut connection.state,
*id,
*timestamp,
)
.await
}
SegmentType::KeepAliveResponse { .. } => {
tracing::info!("Got keep alive response from client... cool...");

View file

@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
use std::io::{Cursor, Write};
mod constants;
use constants::{BLOWFISH_P, BLOWFISH_S};
@ -93,29 +91,28 @@ impl Blowfish {
/// Calculates the F-function for `x`.
fn f(&self, x: u32) -> u32 {
let [a, b, c, d] = x.to_le_bytes();
return ((self.s[0][d as usize].wrapping_add(self.s[1][c as usize]))
^ (self.s[2][b as usize]))
.wrapping_add(self.s[3][a as usize]);
((self.s[0][d as usize].wrapping_add(self.s[1][c as usize])) ^ (self.s[2][b as usize]))
.wrapping_add(self.s[3][a as usize])
}
fn encrypt_pair(&self, xl: &mut u32, xr: &mut u32) {
for i in 0..ROUNDS {
*xl ^= self.p[i as usize];
*xr = self.f(*xl) ^ *xr;
*xl ^= self.p[i];
*xr ^= self.f(*xl);
(*xl, *xr) = (*xr, *xl);
}
(*xl, *xr) = (*xr, *xl);
*xr ^= self.p[ROUNDS as usize];
*xl ^= self.p[ROUNDS as usize + 1];
*xr ^= self.p[ROUNDS];
*xl ^= self.p[ROUNDS + 1];
}
fn decrypt_pair(&self, xl: &mut u32, xr: &mut u32) {
for i in (2..ROUNDS + 2).rev() {
*xl ^= self.p[i as usize];
*xr = self.f(*xl) ^ *xr;
*xl ^= self.p[i];
*xr ^= self.f(*xl);
(*xl, *xr) = (*xr, *xl);
}

View file

@ -1,5 +1,4 @@
use binrw::binrw;
use serde_json::{Value, json};
use serde_json::Value;
use crate::client_select_data::ClientCustomizeData;

View file

@ -1,4 +1,7 @@
use std::ffi::CString;
use std::{
ffi::CString,
time::{SystemTime, UNIX_EPOCH},
};
pub(crate) fn read_bool_from<T: std::convert::From<u8> + std::cmp::PartialEq>(x: T) -> bool {
x == T::from(1u8)
@ -17,3 +20,12 @@ pub(crate) fn write_string(str: &String) -> Vec<u8> {
let c_string = CString::new(&**str).unwrap();
c_string.as_bytes_with_nul().to_vec()
}
pub fn timestamp_secs() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!")
.as_secs()
.try_into()
.unwrap()
}

View file

@ -4,7 +4,7 @@ use crate::{
CHAR_NAME_MAX_LENGTH,
common::{read_string, write_string},
world::{
ActorControlSelf, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
ActorControlSelf, ChatMessage, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
UpdateClassInfo,
},
};
@ -342,23 +342,7 @@ pub enum IPCStructData {
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::ChatMessage))]
ChatMessage {
// TODO: incomplete
#[brw(pad_before = 4)] // empty
player_id: u32,
#[brw(pad_before = 4)] // empty
timestamp: u32,
#[brw(pad_before = 8)] // NOT empty
channel: u16,
#[br(count = 32)]
#[bw(pad_size_to = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
message: String,
},
ChatMessage(ChatMessage),
#[br(pre_assert(*magic == IPCOpCode::GameMasterCommand))]
GameMasterCommand {
// TODO: incomplete

View file

@ -7,10 +7,12 @@ pub mod blowfish;
pub mod chara_make;
pub mod client_select_data;
mod common;
pub use common::timestamp_secs;
mod compression;
pub mod config;
pub mod encryption;
pub mod ipc;
pub mod lobby;
pub mod oodle;
pub mod packet;
pub mod patchlist;

308
src/lobby/connection.rs Normal file
View file

@ -0,0 +1,308 @@
use std::cmp::min;
use tokio::net::TcpStream;
use crate::{
CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, WORLD_NAME, ZONE_ID,
blowfish::Blowfish,
client_select_data::ClientSelectData,
common::timestamp_secs,
encryption::generate_encryption_key,
ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, Server, ServiceAccount},
packet::{
CompressionType, ConnectionType, PacketSegment, SegmentType, State, parse_packet,
send_packet,
},
};
pub struct LobbyConnection {
pub socket: TcpStream,
pub state: State,
}
impl LobbyConnection {
pub async fn parse_packet(&mut self, data: &[u8]) -> (Vec<PacketSegment>, ConnectionType) {
parse_packet(data, &mut self.state).await
}
pub async fn send_segment(&mut self, segment: PacketSegment) {
send_packet(
&mut self.socket,
&[segment],
&mut self.state,
CompressionType::Uncompressed,
)
.await;
}
pub async fn initialize_encryption(&mut self, phrase: &str, key: &[u8; 4]) {
// Generate an encryption key for this client
self.state.client_key = Some(generate_encryption_key(key, phrase));
let mut data = 0xE0003C2Au32.to_le_bytes().to_vec();
data.resize(0x280, 0);
let blowfish = Blowfish::new(&self.state.client_key.unwrap());
blowfish.encrypt(&mut data);
self.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::InitializationEncryptionResponse { data },
})
.await;
}
pub async fn send_account_list(&mut self) {
// send the client the service account list
let service_accounts = [ServiceAccount {
id: 0x002E4A2B,
unk1: 0,
index: 0,
name: "FINAL FANTASY XIV".to_string(),
}]
.to_vec();
let service_account_list = IPCStructData::LobbyServiceAccountList {
sequence: 0,
num_service_accounts: service_accounts.len() as u8,
unk1: 3,
unk2: 0x99,
service_accounts: service_accounts.to_vec(),
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyServiceAccountList,
server_id: 0,
timestamp: timestamp_secs(),
data: service_account_list,
};
self.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
pub async fn send_lobby_info(&mut self, sequence: u64) {
let mut packets = Vec::new();
// send them the server list
{
let mut servers = [Server {
id: WORLD_ID,
index: 0,
flags: 0,
icon: 0,
name: WORLD_NAME.to_string(),
}]
.to_vec();
// add any empty boys
servers.resize(6, Server::default());
let lobby_server_list = IPCStructData::LobbyServerList {
sequence: 0,
unk1: 1,
offset: 0,
num_servers: 1,
servers,
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyServerList,
server_id: 0,
timestamp: timestamp_secs(),
data: lobby_server_list,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
packets.push(response_packet);
}
// send them the retainer list
{
let lobby_retainer_list = IPCStructData::LobbyRetainerList { unk1: 1 };
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyRetainerList,
server_id: 0,
timestamp: timestamp_secs(),
data: lobby_retainer_list,
};
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
};
packets.push(response_packet);
}
send_packet(
&mut self.socket,
&packets,
&mut self.state,
CompressionType::Uncompressed,
)
.await;
// now send them the character list
{
let select_data = ClientSelectData {
game_name_unk: "Final Fantasy".to_string(),
current_class: 2,
class_levels: [5; 30],
race: 0,
subrace: 0,
gender: 0,
birth_month: 5,
birth_day: 5,
guardian: 2,
unk8: 0,
unk9: 0,
zone_id: ZONE_ID as i32,
unk11: 0,
customize: CUSTOMIZE_DATA,
unk12: 0,
unk13: 0,
unk14: [0; 10],
unk15: 0,
unk16: 0,
legacy_character: 0,
unk18: 0,
unk19: 0,
unk20: 0,
unk21: String::new(),
unk22: 0,
unk23: 0,
};
let mut characters = vec![CharacterDetails {
id: 0,
content_id: CONTENT_ID,
index: 0,
unk1: [0; 16],
origin_server_id: WORLD_ID,
current_server_id: WORLD_ID,
character_name: CHAR_NAME.to_string(),
origin_server_name: WORLD_NAME.to_string(),
current_server_name: WORLD_NAME.to_string(),
character_detail_json: select_data.to_json(),
unk2: [0; 20],
}];
for i in 0..4 {
let mut characters_in_packet = Vec::new();
for _ in 0..min(characters.len(), 2) {
characters_in_packet.push(characters.swap_remove(0));
}
// add any empty boys
characters_in_packet.resize(2, CharacterDetails::default());
let lobby_character_list = if i == 3 {
// On the last packet, add the account-wide information
IPCStructData::LobbyCharacterList {
sequence,
counter: (i * 4) + 1, // TODO: why the + 1 here?
num_in_packet: characters_in_packet.len() as u8,
unk1: 0,
unk2: 0,
unk3: 0,
unk4: 128,
unk5: [0; 7],
unk6: 0,
veteran_rank: 0,
unk7: 0,
days_subscribed: 5,
remaining_days: 5,
days_to_next_rank: 0,
unk8: 8,
max_characters_on_world: 2,
entitled_expansion: 4,
characters: characters_in_packet,
}
} else {
IPCStructData::LobbyCharacterList {
sequence,
counter: i * 4,
num_in_packet: characters_in_packet.len() as u8,
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: characters_in_packet,
}
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyCharacterList,
server_id: 0,
timestamp: timestamp_secs(),
data: lobby_character_list,
};
self.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
}
}
pub async fn send_enter_world(&mut self, sequence: u64, lookup_id: u64) {
let Some(session_id) = &self.state.session_id else {
panic!("Missing session id!");
};
let enter_world = IPCStructData::LobbyEnterWorld {
sequence,
character_id: 0,
content_id: lookup_id, // TODO: shouldn't these be named the same then?
session_id: session_id.clone(),
port: 7100,
host: "127.0.0.1".to_string(),
};
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LobbyEnterWorld,
server_id: 0,
timestamp: timestamp_secs(),
data: enter_world,
};
self.send_segment(PacketSegment {
source_actor: 0,
target_actor: 0,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
}

1
src/lobby/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod connection;

View file

@ -5,10 +5,7 @@ use std::{
};
use binrw::{BinRead, BinWrite, binrw};
use tokio::{
io::{AsyncWriteExt, WriteHalf},
net::TcpStream,
};
use tokio::{io::AsyncWriteExt, net::TcpStream};
use crate::{
common::read_string,
@ -150,7 +147,7 @@ fn dump(msg: &str, data: &[u8]) {
}
pub async fn send_packet(
socket: &mut WriteHalf<TcpStream>,
socket: &mut TcpStream,
segments: &[PacketSegment],
state: &mut State,
compression_type: CompressionType,
@ -219,7 +216,6 @@ pub struct State {
pub session_id: Option<String>,
pub serverbound_oodle: FFXIVOodle,
pub clientbound_oodle: FFXIVOodle,
pub player_id: Option<u32>,
}
pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec<PacketSegment>, ConnectionType) {
@ -254,12 +250,7 @@ pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec<PacketSegment>
}
}
pub async fn send_keep_alive(
socket: &mut WriteHalf<TcpStream>,
state: &mut State,
id: u32,
timestamp: u32,
) {
pub async fn send_keep_alive(socket: &mut TcpStream, state: &mut State, id: u32, timestamp: u32) {
let response_packet = PacketSegment {
source_actor: 0,
target_actor: 0,

27
src/world/chat_handler.rs Normal file
View file

@ -0,0 +1,27 @@
use super::{ChatMessage, Position, ZoneConnection};
pub struct ChatHandler {}
impl ChatHandler {
pub async fn handle_chat_message(connection: &mut ZoneConnection, chat_message: &ChatMessage) {
tracing::info!("Client sent chat message: {}!", chat_message.message);
let parts: Vec<&str> = chat_message.message.split(' ').collect();
match parts[0] {
"!setpos" => {
let pos_x = parts[1].parse::<f32>().unwrap();
let pos_y = parts[2].parse::<f32>().unwrap();
let pos_z = parts[3].parse::<f32>().unwrap();
connection
.set_player_position(Position {
x: pos_x,
y: pos_y,
z: pos_z,
})
.await;
}
_ => tracing::info!("Unrecognized debug command!"),
}
}
}

23
src/world/chat_message.rs Normal file
View file

@ -0,0 +1,23 @@
use binrw::binrw;
use crate::common::{read_string, write_string};
#[binrw]
#[derive(Debug, Clone, Default)]
pub struct ChatMessage {
// TODO: incomplete
#[brw(pad_before = 4)] // empty
pub player_id: u32,
#[brw(pad_before = 4)] // empty
pub timestamp: u32,
#[brw(pad_before = 8)] // NOT empty
pub channel: u16,
#[br(count = 32)]
#[bw(pad_size_to = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub message: String,
}

67
src/world/connection.rs Normal file
View file

@ -0,0 +1,67 @@
use tokio::net::TcpStream;
use crate::{
WORLD_ID,
common::timestamp_secs,
ipc::{ActorSetPos, IPCOpCode, IPCSegment, IPCStructData},
packet::{
CompressionType, ConnectionType, PacketSegment, SegmentType, State, parse_packet,
send_packet,
},
};
use super::Position;
pub struct ZoneConnection {
pub socket: TcpStream,
pub state: State,
pub player_id: u32,
}
impl ZoneConnection {
pub async fn parse_packet(&mut self, data: &[u8]) -> (Vec<PacketSegment>, ConnectionType) {
parse_packet(data, &mut self.state).await
}
pub async fn send_segment(&mut self, segment: PacketSegment) {
send_packet(
&mut self.socket,
&[segment],
&mut self.state,
CompressionType::Oodle,
)
.await;
}
pub async fn set_player_position(&mut self, position: Position) {
// set pos
{
let ipc = IPCSegment {
unk1: 14,
unk2: 0,
op_code: IPCOpCode::ActorSetPos,
server_id: WORLD_ID,
timestamp: timestamp_secs(),
data: IPCStructData::ActorSetPos(ActorSetPos {
unk: 0x020fa3b8,
position,
..Default::default()
}),
};
let response_packet = PacketSegment {
source_actor: self.player_id,
target_actor: self.player_id,
segment_type: SegmentType::Ipc { data: ipc },
};
send_packet(
&mut self.socket,
&[response_packet],
&mut self.state,
CompressionType::Oodle,
)
.await;
}
}
}

View file

@ -25,3 +25,12 @@ pub use init_zone::InitZone;
mod zone;
pub use zone::Zone;
mod chat_handler;
pub use chat_handler::ChatHandler;
mod connection;
pub use connection::ZoneConnection;
mod chat_message;
pub use chat_message::ChatMessage;

View file

@ -6,7 +6,7 @@ use physis::{
},
};
use crate::{config::get_config, world::Position};
use crate::config::get_config;
/// Represents a loaded zone
pub struct Zone {