1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-23 15:47:45 +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 kawari::CONTENT_ID;
use std::time::{SystemTime, UNIX_EPOCH};
use kawari::blowfish::Blowfish;
use kawari::chara_make::CharaMake; use kawari::chara_make::CharaMake;
use kawari::client_select_data::{ClientCustomizeData, ClientSelectData}; use kawari::ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, LobbyCharacterAction};
use kawari::encryption::generate_encryption_key; use kawari::lobby::connection::LobbyConnection;
use kawari::ipc::{
CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, LobbyCharacterAction, Server,
ServiceAccount,
};
use kawari::oodle::FFXIVOodle; use kawari::oodle::FFXIVOodle;
use kawari::packet::{ use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive};
CompressionType, PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet, use tokio::io::AsyncReadExt;
}; use tokio::net::TcpListener;
use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, WORLD_NAME, ZONE_ID};
use tokio::io::{AsyncReadExt, WriteHalf};
use tokio::net::{TcpListener, TcpStream};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -27,29 +17,33 @@ async fn main() {
loop { loop {
let (socket, _) = listener.accept().await.unwrap(); 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, client_key: None,
session_id: None, session_id: None,
clientbound_oodle: FFXIVOodle::new(), clientbound_oodle: FFXIVOodle::new(),
serverbound_oodle: FFXIVOodle::new(), serverbound_oodle: FFXIVOodle::new(),
player_id: None,
}; };
let mut connection = LobbyConnection { socket, state };
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = [0; 2056]; let mut buf = [0; 2056];
loop { 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 { if n != 0 {
tracing::info!("read {} bytes", n); 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 { for segment in &segments {
match &segment.segment_type { match &segment.segment_type {
SegmentType::InitializeEncryption { phrase, key } => { 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 { SegmentType::Ipc { data } => match &data.data {
IPCStructData::ClientVersionInfo { IPCStructData::ClientVersionInfo {
@ -60,23 +54,17 @@ async fn main() {
"Client {session_id} ({version_info}) logging in!" "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 } => { IPCStructData::RequestCharacterList { sequence } => {
tracing::info!("Client is requesting character list..."); tracing::info!("Client is requesting character list...");
send_lobby_info(&mut write, &mut state, *sequence).await; connection.send_lobby_info(*sequence).await;
} }
IPCStructData::LobbyCharacterAction { IPCStructData::LobbyCharacterAction {
character_id, action, name, json, ..
character_index,
action,
world_id,
name,
json,
..
} => { } => {
match action { match action {
LobbyCharacterAction::ReserveName => { LobbyCharacterAction::ReserveName => {
@ -136,17 +124,14 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
.send_segment(PacketSegment {
source_actor: 0x0, source_actor: 0x0,
target_actor: 0x0, target_actor: 0x0,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc {
}; data: ipc,
send_packet( },
&mut write, })
&[response_packet],
&mut state,
CompressionType::Uncompressed,
)
.await; .await;
} }
} }
@ -180,17 +165,14 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
.send_segment(PacketSegment {
source_actor: 0x0, source_actor: 0x0,
target_actor: 0x0, target_actor: 0x0,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc {
}; data: ipc,
send_packet( },
&mut write, })
&[response_packet],
&mut state,
CompressionType::Uncompressed,
)
.await; .await;
} }
} }
@ -212,15 +194,20 @@ async fn main() {
} => { } => {
tracing::info!("Client is joining the world..."); tracing::info!("Client is joining the world...");
send_enter_world(&mut write, &mut state, *sequence, *lookup_id) connection.send_enter_world(*sequence, *lookup_id).await;
.await;
} }
_ => { _ => {
panic!("The server is recieving a IPC response packet!") panic!("The server is recieving a IPC response packet!")
} }
}, },
SegmentType::KeepAlive { id, timestamp } => { 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!") 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::io::Cursor;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use binrw::BinRead; use binrw::BinRead;
use kawari::client_select_data::ClientCustomizeData; use kawari::ipc::{GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData};
use kawari::ipc::{ActorSetPos, GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData};
use kawari::oodle::FFXIVOodle; use kawari::oodle::FFXIVOodle;
use kawari::packet::{ use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive};
CompressionType, PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet,
};
use kawari::world::{ use kawari::world::{
ActorControlSelf, ActorControlType, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position, ActorControlSelf, ActorControlType, ChatHandler, InitZone, PlayerSetup, PlayerSpawn,
UpdateClassInfo, Zone, 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::io::AsyncReadExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -28,41 +24,40 @@ async fn main() {
loop { loop {
let (socket, _) = listener.accept().await.unwrap(); 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, client_key: None,
session_id: None, session_id: None,
clientbound_oodle: FFXIVOodle::new(), clientbound_oodle: FFXIVOodle::new(),
serverbound_oodle: FFXIVOodle::new(), serverbound_oodle: FFXIVOodle::new(),
player_id: None,
}; };
let zone = Zone::load(010); let zone = Zone::load(010);
let mut exit_position = None; let mut exit_position = None;
let mut connection = ZoneConnection {
socket,
state,
player_id: 0,
};
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = [0; 2056]; let mut buf = [0; 2056];
loop { 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 { if n != 0 {
println!("recieved {n} bytes..."); 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 { 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 { match &segment.segment_type {
SegmentType::InitializeSession { player_id } => { SegmentType::InitializeSession { player_id } => {
state.player_id = Some(*player_id); connection.player_id = *player_id;
// We have send THEM a keep alive // We have send THEM a keep alive
{ {
@ -73,20 +68,15 @@ async fn main() {
.try_into() .try_into()
.unwrap(); .unwrap();
let response_packet = PacketSegment { connection
.send_segment(PacketSegment {
source_actor: 0, source_actor: 0,
target_actor: 0, target_actor: 0,
segment_type: SegmentType::KeepAlive { segment_type: SegmentType::KeepAlive {
id: 0xE0037603u32, id: 0xE0037603u32,
timestamp, timestamp,
}, },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -96,19 +86,14 @@ async fn main() {
"Client {player_id} is initializing zone session..." "Client {player_id} is initializing zone session..."
); );
let response_packet = PacketSegment { connection
.send_segment(PacketSegment {
source_actor: 0, source_actor: 0,
target_actor: 0, target_actor: 0,
segment_type: SegmentType::ZoneInitialize { segment_type: SegmentType::ZoneInitialize {
player_id: *player_id, player_id: *player_id,
}, },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
kawari::packet::ConnectionType::Chat => { kawari::packet::ConnectionType::Chat => {
@ -117,19 +102,14 @@ async fn main() {
); );
{ {
let response_packet = PacketSegment { connection
.send_segment(PacketSegment {
source_actor: 0, source_actor: 0,
target_actor: 0, target_actor: 0,
segment_type: SegmentType::ZoneInitialize { segment_type: SegmentType::ZoneInitialize {
player_id: *player_id, player_id: *player_id,
}, },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -143,17 +123,12 @@ async fn main() {
data: IPCStructData::InitializeChat { unk: [0; 8] }, data: IPCStructData::InitializeChat { unk: [0; 8] },
}; };
let response_packet = PacketSegment { connection
.send_segment(PacketSegment {
source_actor: *player_id, source_actor: *player_id,
target_actor: *player_id, target_actor: *player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
} }
@ -179,22 +154,17 @@ async fn main() {
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: IPCStructData::InitResponse { data: IPCStructData::InitResponse {
unk1: 0, unk1: 0,
character_id: state.player_id.unwrap(), character_id: connection.player_id,
unk2: 0, unk2: 0,
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -220,17 +190,12 @@ async fn main() {
), ),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -250,17 +215,12 @@ async fn main() {
}), }),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -281,17 +241,12 @@ async fn main() {
}), }),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -314,17 +269,12 @@ async fn main() {
), ),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -341,17 +291,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -366,17 +311,12 @@ async fn main() {
data: IPCStructData::Unk9 { unk: [0; 24] }, data: IPCStructData::Unk9 { unk: [0; 24] },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -393,17 +333,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -418,17 +353,12 @@ async fn main() {
data: IPCStructData::Unk8 { unk: [0; 808] }, data: IPCStructData::Unk8 { unk: [0; 808] },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -448,17 +378,12 @@ async fn main() {
}), }),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
} }
@ -481,17 +406,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -537,17 +457,12 @@ async fn main() {
}), }),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -564,17 +479,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -621,17 +531,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
} }
@ -652,72 +557,24 @@ async fn main() {
data: IPCStructData::LogOutComplete { unk: [0; 8] }, data: IPCStructData::LogOutComplete { unk: [0; 8] },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
} }
IPCStructData::Disconnected { .. } => { IPCStructData::Disconnected { .. } => {
tracing::info!("Client disconnected!"); tracing::info!("Client disconnected!");
} }
IPCStructData::ChatMessage { message, .. } => { IPCStructData::ChatMessage(chat_message) => {
tracing::info!("Client sent chat message: {message}!"); ChatHandler::handle_chat_message(
&mut connection,
let parts: Vec<&str> = message.split(' ').collect(); chat_message,
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,
) )
.await; .await
}
}
_ => tracing::info!("Unrecognized debug command!"),
}
} }
IPCStructData::GameMasterCommand { command, arg, .. } => { IPCStructData::GameMasterCommand { command, arg, .. } => {
tracing::info!("Got a game master command!"); tracing::info!("Got a game master command!");
@ -739,19 +596,14 @@ async fn main() {
}), }),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { segment_type: SegmentType::Ipc {
data: ipc, data: ipc,
}, },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
} }
@ -800,17 +652,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -827,17 +674,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -869,17 +711,12 @@ async fn main() {
}), }),
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
@ -896,17 +733,12 @@ async fn main() {
}, },
}; };
let response_packet = PacketSegment { connection
source_actor: state.player_id.unwrap(), .send_segment(PacketSegment {
target_actor: state.player_id.unwrap(), source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}; })
send_packet(
&mut write,
&[response_packet],
&mut state,
CompressionType::Oodle,
)
.await; .await;
} }
} }
@ -922,7 +754,13 @@ async fn main() {
} }
} }
SegmentType::KeepAlive { id, timestamp } => { 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 { .. } => { SegmentType::KeepAliveResponse { .. } => {
tracing::info!("Got keep alive response from client... cool..."); tracing::info!("Got keep alive response from client... cool...");

View file

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

View file

@ -1,5 +1,4 @@
use binrw::binrw; use serde_json::Value;
use serde_json::{Value, json};
use crate::client_select_data::ClientCustomizeData; 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 { pub(crate) fn read_bool_from<T: std::convert::From<u8> + std::cmp::PartialEq>(x: T) -> bool {
x == T::from(1u8) x == T::from(1u8)
@ -17,3 +20,12 @@ pub(crate) fn write_string(str: &String) -> Vec<u8> {
let c_string = CString::new(&**str).unwrap(); let c_string = CString::new(&**str).unwrap();
c_string.as_bytes_with_nul().to_vec() 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, CHAR_NAME_MAX_LENGTH,
common::{read_string, write_string}, common::{read_string, write_string},
world::{ world::{
ActorControlSelf, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position, ActorControlSelf, ChatMessage, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
UpdateClassInfo, UpdateClassInfo,
}, },
}; };
@ -342,23 +342,7 @@ pub enum IPCStructData {
unk: [u8; 8], unk: [u8; 8],
}, },
#[br(pre_assert(*magic == IPCOpCode::ChatMessage))] #[br(pre_assert(*magic == IPCOpCode::ChatMessage))]
ChatMessage { ChatMessage(ChatMessage),
// TODO: incomplete
#[brw(pad_before = 4)] // empty
player_id: u32,
#[brw(pad_before = 4)] // empty
timestamp: u32,
#[brw(pad_before = 8)] // NOT empty
channel: u16,
#[br(count = 32)]
#[bw(pad_size_to = 32)]
#[br(map = read_string)]
#[bw(map = write_string)]
message: String,
},
#[br(pre_assert(*magic == IPCOpCode::GameMasterCommand))] #[br(pre_assert(*magic == IPCOpCode::GameMasterCommand))]
GameMasterCommand { GameMasterCommand {
// TODO: incomplete // TODO: incomplete

View file

@ -7,10 +7,12 @@ pub mod blowfish;
pub mod chara_make; pub mod chara_make;
pub mod client_select_data; pub mod client_select_data;
mod common; mod common;
pub use common::timestamp_secs;
mod compression; mod compression;
pub mod config; pub mod config;
pub mod encryption; pub mod encryption;
pub mod ipc; pub mod ipc;
pub mod lobby;
pub mod oodle; pub mod oodle;
pub mod packet; pub mod packet;
pub mod patchlist; 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 binrw::{BinRead, BinWrite, binrw};
use tokio::{ use tokio::{io::AsyncWriteExt, net::TcpStream};
io::{AsyncWriteExt, WriteHalf},
net::TcpStream,
};
use crate::{ use crate::{
common::read_string, common::read_string,
@ -150,7 +147,7 @@ fn dump(msg: &str, data: &[u8]) {
} }
pub async fn send_packet( pub async fn send_packet(
socket: &mut WriteHalf<TcpStream>, socket: &mut TcpStream,
segments: &[PacketSegment], segments: &[PacketSegment],
state: &mut State, state: &mut State,
compression_type: CompressionType, compression_type: CompressionType,
@ -219,7 +216,6 @@ pub struct State {
pub session_id: Option<String>, pub session_id: Option<String>,
pub serverbound_oodle: FFXIVOodle, pub serverbound_oodle: FFXIVOodle,
pub clientbound_oodle: FFXIVOodle, pub clientbound_oodle: FFXIVOodle,
pub player_id: Option<u32>,
} }
pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec<PacketSegment>, ConnectionType) { 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( pub async fn send_keep_alive(socket: &mut TcpStream, state: &mut State, id: u32, timestamp: u32) {
socket: &mut WriteHalf<TcpStream>,
state: &mut State,
id: u32,
timestamp: u32,
) {
let response_packet = PacketSegment { let response_packet = PacketSegment {
source_actor: 0, source_actor: 0,
target_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; mod zone;
pub use zone::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 /// Represents a loaded zone
pub struct Zone { pub struct Zone {