diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index b8cdeb7..081f660 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -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,18 +124,15 @@ async fn main() { }, }; - let response_packet = PacketSegment { - source_actor: 0x0, - target_actor: 0x0, - segment_type: SegmentType::Ipc { data: ipc }, - }; - send_packet( - &mut write, - &[response_packet], - &mut state, - CompressionType::Uncompressed, - ) - .await; + connection + .send_segment(PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { + data: ipc, + }, + }) + .await; } } LobbyCharacterAction::Create => { @@ -180,18 +165,15 @@ async fn main() { }, }; - let response_packet = PacketSegment { - source_actor: 0x0, - target_actor: 0x0, - segment_type: SegmentType::Ipc { data: ipc }, - }; - send_packet( - &mut write, - &[response_packet], - &mut state, - CompressionType::Uncompressed, - ) - .await; + connection + .send_segment(PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { + data: ipc, + }, + }) + .await; } } LobbyCharacterAction::Rename => todo!(), @@ -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, - 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, 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, 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, - 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; -} diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index c054519..a5106ac 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -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,21 +68,16 @@ async fn main() { .try_into() .unwrap(); - let response_packet = 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; + connection + .send_segment(PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::KeepAlive { + id: 0xE0037603u32, + timestamp, + }, + }) + .await; } match connection_type { @@ -96,20 +86,15 @@ async fn main() { "Client {player_id} is initializing zone session..." ); - let response_packet = 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; + connection + .send_segment(PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::ZoneInitialize { + player_id: *player_id, + }, + }) + .await; } kawari::packet::ConnectionType::Chat => { tracing::info!( @@ -117,20 +102,15 @@ async fn main() { ); { - let response_packet = 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; + connection + .send_segment(PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::ZoneInitialize { + player_id: *player_id, + }, + }) + .await; } { @@ -143,18 +123,13 @@ async fn main() { data: IPCStructData::InitializeChat { unk: [0; 8] }, }; - let response_packet = 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; + connection + .send_segment(PacketSegment { + source_actor: *player_id, + target_actor: *player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } } _ => panic!( @@ -179,23 +154,18 @@ 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(), - segment_type: SegmentType::Ipc { data: ipc }, - }; - send_packet( - &mut write, - &[response_packet], - &mut state, - CompressionType::Oodle, - ) - .await; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // Control Data @@ -220,18 +190,13 @@ async fn main() { ), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // Stats @@ -250,18 +215,13 @@ async fn main() { }), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // Player Setup @@ -281,18 +241,13 @@ async fn main() { }), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // Player Class Info @@ -314,18 +269,13 @@ async fn main() { ), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // unk10 @@ -341,18 +291,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // unk9 @@ -366,18 +311,13 @@ 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(), - segment_type: SegmentType::Ipc { data: ipc }, - }; - send_packet( - &mut write, - &[response_packet], - &mut state, - CompressionType::Oodle, - ) - .await; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // link shell information @@ -393,18 +333,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // unk8 @@ -418,18 +353,13 @@ 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(), - segment_type: SegmentType::Ipc { data: ipc }, - }; - send_packet( - &mut write, - &[response_packet], - &mut state, - CompressionType::Oodle, - ) - .await; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // Init Zone @@ -448,18 +378,13 @@ async fn main() { }), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } } IPCStructData::FinishLoading { .. } => { @@ -481,18 +406,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // send player spawn @@ -537,18 +457,13 @@ async fn main() { }), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // fade in? @@ -564,18 +479,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // wipe any exit position so it isn't accidentally reused @@ -621,18 +531,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } } IPCStructData::UpdatePositionHandler { .. } => { @@ -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(), - segment_type: SegmentType::Ipc { data: ipc }, - }; - send_packet( - &mut write, - &[response_packet], - &mut state, - CompressionType::Oodle, - ) - .await; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .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::().unwrap(); - let pos_y = parts[2].parse::().unwrap(); - let pos_z = parts[3].parse::().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; - } - } - _ => tracing::info!("Unrecognized debug command!"), - } + IPCStructData::ChatMessage(chat_message) => { + ChatHandler::handle_chat_message( + &mut connection, + chat_message, + ) + .await } IPCStructData::GameMasterCommand { command, arg, .. } => { tracing::info!("Got a game master command!"); @@ -739,20 +596,15 @@ async fn main() { }), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { + data: ipc, + }, + }) + .await; } } } @@ -800,18 +652,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // fade out? x2 @@ -827,18 +674,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } tracing::info!( @@ -869,18 +711,13 @@ async fn main() { }), }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } // idk @@ -896,18 +733,13 @@ async fn main() { }, }; - 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; + connection + .send_segment(PacketSegment { + source_actor: connection.player_id, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; } } IPCStructData::Unk13 { .. } => { @@ -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..."); diff --git a/src/blowfish/mod.rs b/src/blowfish/mod.rs index 1d99eea..a3aaa5f 100644 --- a/src/blowfish/mod.rs +++ b/src/blowfish/mod.rs @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: 2025 Joshua Goins // 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); } diff --git a/src/chara_make.rs b/src/chara_make.rs index eb28c36..52908fa 100644 --- a/src/chara_make.rs +++ b/src/chara_make.rs @@ -1,5 +1,4 @@ -use binrw::binrw; -use serde_json::{Value, json}; +use serde_json::Value; use crate::client_select_data::ClientCustomizeData; diff --git a/src/common.rs b/src/common.rs index c229d79..ba18a01 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,7 @@ -use std::ffi::CString; +use std::{ + ffi::CString, + time::{SystemTime, UNIX_EPOCH}, +}; pub(crate) fn read_bool_from + std::cmp::PartialEq>(x: T) -> bool { x == T::from(1u8) @@ -17,3 +20,12 @@ pub(crate) fn write_string(str: &String) -> Vec { 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() +} diff --git a/src/ipc.rs b/src/ipc.rs index eb62417..8fd9c83 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 388f5c4..36e3e95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs new file mode 100644 index 0000000..51438b6 --- /dev/null +++ b/src/lobby/connection.rs @@ -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, 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; + } +} diff --git a/src/lobby/mod.rs b/src/lobby/mod.rs new file mode 100644 index 0000000..b3b606b --- /dev/null +++ b/src/lobby/mod.rs @@ -0,0 +1 @@ +pub mod connection; diff --git a/src/packet.rs b/src/packet.rs index 5bc47d2..99c6946 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -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, + socket: &mut TcpStream, segments: &[PacketSegment], state: &mut State, compression_type: CompressionType, @@ -219,7 +216,6 @@ pub struct State { pub session_id: Option, pub serverbound_oodle: FFXIVOodle, pub clientbound_oodle: FFXIVOodle, - pub player_id: Option, } pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec, ConnectionType) { @@ -254,12 +250,7 @@ pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec } } -pub async fn send_keep_alive( - socket: &mut WriteHalf, - 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, diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs new file mode 100644 index 0000000..70f013a --- /dev/null +++ b/src/world/chat_handler.rs @@ -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::().unwrap(); + let pos_y = parts[2].parse::().unwrap(); + let pos_z = parts[3].parse::().unwrap(); + + connection + .set_player_position(Position { + x: pos_x, + y: pos_y, + z: pos_z, + }) + .await; + } + _ => tracing::info!("Unrecognized debug command!"), + } + } +} diff --git a/src/world/chat_message.rs b/src/world/chat_message.rs new file mode 100644 index 0000000..6d46fa1 --- /dev/null +++ b/src/world/chat_message.rs @@ -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, +} diff --git a/src/world/connection.rs b/src/world/connection.rs new file mode 100644 index 0000000..bc7e063 --- /dev/null +++ b/src/world/connection.rs @@ -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, 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; + } + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs index 2eb6044..bc276f4 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -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; diff --git a/src/world/zone.rs b/src/world/zone.rs index c363e66..4b49461 100644 --- a/src/world/zone.rs +++ b/src/world/zone.rs @@ -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 {