diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 08518d0..1f3552e 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -6,56 +6,13 @@ use kawari::lobby::ipc::{ CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, }; +use kawari::lobby::send_custom_world_packet; use kawari::oodle::OodleNetwork; -use kawari::packet::CompressionType; use kawari::packet::ConnectionType; -use kawari::packet::parse_packet; -use kawari::packet::send_packet; use kawari::packet::{PacketSegment, PacketState, SegmentType, send_keep_alive}; use kawari::{CONTENT_ID, WORLD_NAME}; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; -use tokio::net::TcpStream; - -/// Sends a custom IPC packet to the world server, meant for private server-to-server communication. -/// Returns the first custom IPC segment returned. -async fn send_custom_world_packet(segment: CustomIpcSegment) -> Option { - let mut stream = TcpStream::connect("127.0.0.1:7100").await.unwrap(); - - let mut packet_state = PacketState { - client_key: None, - serverbound_oodle: OodleNetwork::new(), - clientbound_oodle: OodleNetwork::new(), - }; - - let segment: PacketSegment = PacketSegment { - source_actor: 0, - target_actor: 0, - segment_type: SegmentType::CustomIpc { data: segment }, - }; - - send_packet( - &mut stream, - &mut packet_state, - ConnectionType::None, - CompressionType::Uncompressed, - &[segment], - ) - .await; - - // read response - let mut buf = [0; 2056]; - let n = stream.read(&mut buf).await.expect("Failed to read data!"); - - println!("Got {n} bytes of response!"); - - let (segments, _) = parse_packet::(&buf[..n], &mut packet_state).await; - - match &segments[0].segment_type { - SegmentType::CustomIpc { data } => Some(data.clone()), - _ => None, - } -} #[tokio::main] async fn main() { @@ -333,15 +290,14 @@ async fn main() { }, }; - let response_segment = send_custom_world_packet(ipc_segment).await.unwrap(); + let response_segment = + send_custom_world_packet(ipc_segment).await.unwrap(); match &response_segment.data { CustomIpcData::ActorIdFound { actor_id } => { our_actor_id = *actor_id; } - _ => panic!( - "Unexpected custom IPC packet type here!" - ), + _ => panic!("Unexpected custom IPC packet type here!"), } } diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 17f8e14..c642279 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,10 +1,15 @@ use std::sync::{Arc, Mutex}; +use kawari::WORLD_NAME; use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}; use kawari::config::get_config; -use kawari::lobby::CharaMake; +use kawari::lobby::ipc::CharacterDetails; +use kawari::lobby::{CharaMake, ClientSelectData}; use kawari::oodle::OodleNetwork; -use kawari::packet::{ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive}; +use kawari::packet::{ + CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive, + send_packet, +}; use kawari::world::PlayerData; use kawari::world::ipc::{ ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData, @@ -71,6 +76,91 @@ fn find_actor_id(connection: &Arc>, content_id: u64) -> u32 { stmt.query_row((content_id,), |row| row.get(0)).unwrap() } +fn get_character_list( + connection: &Arc>, + service_account_id: u32, +) -> Vec { + let connection = connection.lock().unwrap(); + + let content_actor_ids: Vec<(u32, u32)>; + + // find the content ids associated with the service account + { + let mut stmt = connection + .prepare("SELECT content_id, actor_id FROM characters WHERE service_account_id = ?1") + .unwrap(); + + content_actor_ids = stmt + .query_map((service_account_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap() + .map(|x| x.unwrap()) + .collect(); + } + + let mut characters = Vec::new(); + + for (index, (content_id, actor_id)) in content_actor_ids.iter().enumerate() { + dbg!(content_id); + + let mut stmt = connection + .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") + .unwrap(); + + let (name, chara_make): (String, String) = stmt + .query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap(); + + let chara_make = CharaMake::from_json(&chara_make); + + let select_data = ClientSelectData { + game_name_unk: "Final Fantasy".to_string(), + current_class: 2, + class_levels: [5; 30], + race: chara_make.customize.race as i32, + subrace: chara_make.customize.subrace as i32, + gender: chara_make.customize.gender as i32, + birth_month: chara_make.birth_month, + birth_day: chara_make.birth_day, + guardian: chara_make.guardian, + unk8: 0, + unk9: 0, + zone_id: ZONE_ID as i32, + unk11: 0, + customize: chara_make.customize, + 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, + }; + + characters.push(CharacterDetails { + actor_id: *actor_id, + content_id: *content_id as u64, + index: index as u32, + unk1: [0; 16], + origin_server_id: WORLD_ID, + current_server_id: WORLD_ID, + character_name: name.clone(), + origin_server_name: WORLD_NAME.to_string(), + current_server_name: WORLD_NAME.to_string(), + character_detail_json: select_data.to_json(), + unk2: [0; 20], + }); + } + + dbg!(&characters); + + characters +} + fn generate_content_id() -> u32 { rand::random() } @@ -922,6 +1012,37 @@ async fn main() { .await; } } + CustomIpcData::RequestCharacterList { service_account_id } => { + let characters = + get_character_list(&db_connection, *service_account_id); + + // send response + { + send_packet::( + &mut connection.socket, + &mut connection.state, + ConnectionType::None, + CompressionType::Uncompressed, + &[PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::CustomIpc { + data: CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::RequestCharacterListRepsonse, + server_id: 0, + timestamp: 0, + data: CustomIpcData::RequestCharacterListRepsonse { + characters + }, + }, + }, + }], + ) + .await; + } + } _ => panic!( "The server is recieving a response or unknown custom IPC!" ), diff --git a/src/common/custom_ipc.rs b/src/common/custom_ipc.rs index fedec15..3b2d4f7 100644 --- a/src/common/custom_ipc.rs +++ b/src/common/custom_ipc.rs @@ -3,6 +3,7 @@ use binrw::binrw; use crate::{ CHAR_NAME_MAX_LENGTH, common::read_string, + lobby::ipc::CharacterDetails, packet::{IpcSegment, ReadWriteIpcSegment}, }; @@ -20,6 +21,8 @@ impl ReadWriteIpcSegment for CustomIpcSegment { CustomIpcType::ActorIdFound => 4, CustomIpcType::CheckNameIsAvailable => CHAR_NAME_MAX_LENGTH as u32, CustomIpcType::NameIsAvailableResponse => 1, + CustomIpcType::RequestCharacterList => 4, + CustomIpcType::RequestCharacterListRepsonse => 1184 * 8, } } } @@ -41,6 +44,10 @@ pub enum CustomIpcType { CheckNameIsAvailable = 0x5, /// Response to CheckNameIsAvailable NameIsAvailableResponse = 0x6, + /// Request the character list from the world server + RequestCharacterList = 0x7, + /// Response to RequestCharacterList + RequestCharacterListRepsonse = 0x8, } #[binrw] @@ -76,6 +83,15 @@ pub enum CustomIpcData { }, #[br(pre_assert(*magic == CustomIpcType::NameIsAvailableResponse))] NameIsAvailableResponse { free: u8 }, + #[br(pre_assert(*magic == CustomIpcType::RequestCharacterList))] + RequestCharacterList { service_account_id: u32 }, + #[br(pre_assert(*magic == CustomIpcType::RequestCharacterListRepsonse))] + RequestCharacterListRepsonse { + #[bw(calc = characters.len() as u8)] + num_characters: u8, + #[br(count = num_characters)] + characters: Vec, // TODO: maybe chunk this into 4 parts ala the lobby server? + }, } impl Default for CustomIpcData { diff --git a/src/common/customize_data.rs b/src/common/customize_data.rs index 33f51a7..76ac0da 100644 --- a/src/common/customize_data.rs +++ b/src/common/customize_data.rs @@ -68,30 +68,30 @@ impl CustomizeData { Self { race: json[0].as_str().unwrap().parse::().unwrap(), gender: json[1].as_str().unwrap().parse::().unwrap(), - height: json[2].as_str().unwrap().parse::().unwrap(), - subrace: json[3].as_str().unwrap().parse::().unwrap(), - face: json[4].as_str().unwrap().parse::().unwrap(), - hair: json[5].as_str().unwrap().parse::().unwrap(), - enable_highlights: json[6].as_str().unwrap().parse::().unwrap(), - skin_tone: json[7].as_str().unwrap().parse::().unwrap(), - right_eye_color: json[8].as_str().unwrap().parse::().unwrap(), - hair_tone: json[9].as_str().unwrap().parse::().unwrap(), - highlights: json[10].as_str().unwrap().parse::().unwrap(), - facial_features: json[11].as_str().unwrap().parse::().unwrap(), - facial_feature_color: json[12].as_str().unwrap().parse::().unwrap(), - eyebrows: json[13].as_str().unwrap().parse::().unwrap(), - left_eye_color: json[14].as_str().unwrap().parse::().unwrap(), - eyes: json[15].as_str().unwrap().parse::().unwrap(), - nose: json[16].as_str().unwrap().parse::().unwrap(), - jaw: json[17].as_str().unwrap().parse::().unwrap(), - mouth: json[18].as_str().unwrap().parse::().unwrap(), - lips_tone_fur_pattern: json[19].as_str().unwrap().parse::().unwrap(), - race_feature_size: json[20].as_str().unwrap().parse::().unwrap(), - race_feature_type: json[21].as_str().unwrap().parse::().unwrap(), - bust: json[22].as_str().unwrap().parse::().unwrap(), - face_paint: json[23].as_str().unwrap().parse::().unwrap(), - face_paint_color: json[24].as_str().unwrap().parse::().unwrap(), - age: 1, + age: json[2].as_str().unwrap().parse::().unwrap(), + height: json[3].as_str().unwrap().parse::().unwrap(), + subrace: json[4].as_str().unwrap().parse::().unwrap(), + face: json[5].as_str().unwrap().parse::().unwrap(), + hair: json[6].as_str().unwrap().parse::().unwrap(), + enable_highlights: json[7].as_str().unwrap().parse::().unwrap(), + skin_tone: json[8].as_str().unwrap().parse::().unwrap(), + right_eye_color: json[9].as_str().unwrap().parse::().unwrap(), + hair_tone: json[10].as_str().unwrap().parse::().unwrap(), + highlights: json[11].as_str().unwrap().parse::().unwrap(), + facial_features: json[12].as_str().unwrap().parse::().unwrap(), + facial_feature_color: json[13].as_str().unwrap().parse::().unwrap(), + eyebrows: json[14].as_str().unwrap().parse::().unwrap(), + left_eye_color: json[15].as_str().unwrap().parse::().unwrap(), + eyes: json[16].as_str().unwrap().parse::().unwrap(), + nose: json[17].as_str().unwrap().parse::().unwrap(), + jaw: json[18].as_str().unwrap().parse::().unwrap(), + mouth: json[19].as_str().unwrap().parse::().unwrap(), + lips_tone_fur_pattern: json[20].as_str().unwrap().parse::().unwrap(), + race_feature_size: json[21].as_str().unwrap().parse::().unwrap(), + race_feature_type: json[22].as_str().unwrap().parse::().unwrap(), + bust: json[23].as_str().unwrap().parse::().unwrap(), + face_paint: json[24].as_str().unwrap().parse::().unwrap(), + face_paint_color: json[25].as_str().unwrap().parse::().unwrap(), } } } diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 6c90a40..439e07c 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -1,24 +1,24 @@ use std::cmp::min; -use tokio::net::TcpStream; +use tokio::{io::AsyncReadExt, net::TcpStream}; use crate::{ - CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, DEITY, NAMEDAY_DAY, NAMEDAY_MONTH, WORLD_ID, WORLD_NAME, - ZONE_ID, + WORLD_ID, WORLD_NAME, blowfish::Blowfish, - common::timestamp_secs, + common::{ + custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}, + timestamp_secs, + }, + oodle::OodleNetwork, packet::{ CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, generate_encryption_key, parse_packet, send_packet, }, }; -use super::{ - client_select_data::ClientSelectData, - ipc::{ - CharacterDetails, LobbyCharacterList, LobbyServerList, LobbyServiceAccountList, Server, - ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, ServiceAccount, - }, +use super::ipc::{ + CharacterDetails, LobbyCharacterList, LobbyServerList, LobbyServiceAccountList, Server, + ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, ServiceAccount, }; use crate::lobby::ipc::ClientLobbyIpcSegment; @@ -181,48 +181,28 @@ impl LobbyConnection { // 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: CUSTOMIZE_DATA.race as i32, - subrace: CUSTOMIZE_DATA.subrace as i32, - gender: CUSTOMIZE_DATA.gender as i32, - birth_month: NAMEDAY_MONTH as i32, - birth_day: NAMEDAY_DAY as i32, - guardian: DEITY as i32, - 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 charlist_request = CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::RequestCharacterList, + server_id: 0, + timestamp: 0, + data: CustomIpcData::RequestCharacterList { + service_account_id: 0x1, // TODO: placeholder + }, }; - let mut characters = vec![CharacterDetails { - actor_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], - }]; + let name_response = send_custom_world_packet(charlist_request) + .await + .expect("Failed to get name request packet!"); + let CustomIpcData::RequestCharacterListRepsonse { characters } = &name_response.data + else { + panic!("Unexpedted custom IPC type!") + }; + + let mut characters = characters.to_vec(); + + dbg!(&characters); for i in 0..4 { let mut characters_in_packet = Vec::new(); @@ -355,3 +335,43 @@ impl LobbyConnection { .await; } } + +/// Sends a custom IPC packet to the world server, meant for private server-to-server communication. +/// Returns the first custom IPC segment returned. +pub async fn send_custom_world_packet(segment: CustomIpcSegment) -> Option { + let mut stream = TcpStream::connect("127.0.0.1:7100").await.unwrap(); + + let mut packet_state = PacketState { + client_key: None, + serverbound_oodle: OodleNetwork::new(), + clientbound_oodle: OodleNetwork::new(), + }; + + let segment: PacketSegment = PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::CustomIpc { data: segment }, + }; + + send_packet( + &mut stream, + &mut packet_state, + ConnectionType::None, + CompressionType::Uncompressed, + &[segment], + ) + .await; + + // read response + let mut buf = [0; 10024]; // TODO: this large buffer is just working around these packets not being compressed, but they really should be! + let n = stream.read(&mut buf).await.expect("Failed to read data!"); + + println!("Got {n} bytes of response!"); + + let (segments, _) = parse_packet::(&buf[..n], &mut packet_state).await; + + match &segments[0].segment_type { + SegmentType::CustomIpc { data } => Some(data.clone()), + _ => None, + } +} diff --git a/src/lobby/mod.rs b/src/lobby/mod.rs index 16729f2..36bd454 100644 --- a/src/lobby/mod.rs +++ b/src/lobby/mod.rs @@ -2,9 +2,10 @@ mod chara_make; pub use chara_make::CharaMake; mod client_select_data; +pub use client_select_data::ClientSelectData; mod connection; -pub use connection::LobbyConnection; +pub use connection::{LobbyConnection, send_custom_world_packet}; /// The IPC packets for the Lobby connection. pub mod ipc;