diff --git a/.gitignore b/.gitignore index d6a661b..aae89fc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea/ config.json *.bin +*.db diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index b7101bc..dd1fe57 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -1,15 +1,61 @@ -use kawari::lobby::CharaMake; +use kawari::common::custom_ipc::CustomIpcData; +use kawari::common::custom_ipc::CustomIpcSegment; +use kawari::common::custom_ipc::CustomIpcType; use kawari::lobby::LobbyConnection; use kawari::lobby::ipc::{ CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, }; 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() { @@ -32,6 +78,7 @@ async fn main() { socket, state, session_id: None, + stored_character_creation_name: String::new(), }; tokio::spawn(async move { @@ -82,37 +129,37 @@ async fn main() { character_action.name ); - // reject - /*{ - let ipc = IPCSegment { - unk1: 0, - unk2: 0, - op_code: IPCOpCode::InitializeChat, // wrong but technically right - server_id: 0, - timestamp: 0, - data: ClientLobbyIpcType::NameRejection { - unk1: 0x03, - unk2: 0x0bdb, - unk3: 0x000132cc, - }, - }; + // check with the world server if the name is available + let name_request = CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::CheckNameIsAvailable, + server_id: 0, + timestamp: 0, + data: CustomIpcData::CheckNameIsAvailable { + name: character_action.name.clone(), + }, + }; - 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; - }*/ + let name_response = + send_custom_world_packet(name_request) + .await + .expect("Failed to get name request packet!"); + let CustomIpcData::NameIsAvailableResponse { free } = + &name_response.data + else { + panic!("Unexpedted custom IPC type!") + }; + + tracing::info!("Is name free? {free}"); + + // TODO: use read_bool_as + let free: bool = *free == 1u8; + + if free { + connection.stored_character_creation_name = + character_action.name.clone(); - // accept - { let ipc = ServerLobbyIpcSegment { unk1: 0, unk2: 0, @@ -145,13 +192,75 @@ async fn main() { }, }) .await; + } else { + let ipc = ServerLobbyIpcSegment { + unk1: 0, + unk2: 0, + op_code: ServerLobbyIpcType::LobbyError, + server_id: 0, + timestamp: 0, + data: ServerLobbyIpcData::LobbyError { + sequence: 0x03, + error: 0x0bdb, + exd_error_id: 0, + value: 0, + unk1: 0, + }, + }; + + let response_packet = PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }; + connection.send_segment(response_packet).await; } } LobbyCharacterActionKind::Create => { tracing::info!("Player is creating a new character!"); - let chara_make = - CharaMake::from_json(&character_action.json); + let our_actor_id; + let our_content_id; + + // tell the world server to create this character + { + let ipc_segment = CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::RequestCreateCharacter, + server_id: 0, + timestamp: 0, + data: CustomIpcData::RequestCreateCharacter { + name: connection + .stored_character_creation_name + .clone(), // TODO: worth double-checking, but AFAIK we have to store it this way? + chara_make_json: character_action + .json + .clone(), + }, + }; + + let response_segment = + send_custom_world_packet(ipc_segment) + .await + .unwrap(); + match &response_segment.data { + CustomIpcData::CharacterCreated { + actor_id, + content_id, + } => { + our_actor_id = *actor_id; + our_content_id = *content_id; + } + _ => panic!( + "Unexpected custom IPC packet type here!" + ), + } + } + + tracing::info!( + "Got new player info from world server: {our_content_id} {our_actor_id}" + ); // a slightly different character created packet now { @@ -165,8 +274,8 @@ async fn main() { sequence: character_action.sequence + 1, unk: 0x00020101, details: CharacterDetails { - id: 0x07369f3a, // notice that we give them an id now - content_id: CONTENT_ID, + actor_id: our_actor_id, + content_id: our_content_id, character_name: character_action .name .clone(), @@ -204,11 +313,41 @@ async fn main() { } ClientLobbyIpcData::RequestEnterWorld { sequence, - lookup_id, + content_id, } => { - tracing::info!("Client is joining the world..."); + tracing::info!("Client is joining the world with {content_id}"); - connection.send_enter_world(*sequence, *lookup_id).await; + let our_actor_id; + + // find the actor id for this content id + // NOTE: This is NOT the ideal solution. I theorize the lobby server has it's own records with this information. + { + let ipc_segment = CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::GetActorId, + server_id: 0, + timestamp: 0, + data: CustomIpcData::GetActorId { + content_id: *content_id, + }, + }; + + 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!" + ), + } + } + + connection + .send_enter_world(*sequence, *content_id, our_actor_id) + .await; } }, SegmentType::KeepAlive { id, timestamp } => { diff --git a/src/bin/kawari-login.rs b/src/bin/kawari-login.rs index faecbe1..dc1177a 100644 --- a/src/bin/kawari-login.rs +++ b/src/bin/kawari-login.rs @@ -35,6 +35,8 @@ impl LoginServerState { fn add_user(&self, username: &str, password: &str) { let connection = self.connection.lock().unwrap(); + tracing::info!("Adding user with username {username}"); + let query = "INSERT INTO users VALUES (?1, ?2);"; connection .execute(query, (username, password)) @@ -45,6 +47,8 @@ impl LoginServerState { fn login_user(&self, username: &str, password: &str) -> Result { let selected_row: Result<(String, String), rusqlite::Error>; + tracing::info!("Finding user with username {username}"); + { let connection = self.connection.lock().unwrap(); @@ -200,17 +204,17 @@ async fn check_session( } fn setup_state() -> LoginServerState { - let connection = Connection::open_in_memory().expect("Failed to open database!"); + let connection = Connection::open("login.db").expect("Failed to open database!"); // Create users table { - let query = "CREATE TABLE users (username TEXT PRIMARY KEY, password TEXT);"; + let query = "CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);"; connection.execute(query, ()).unwrap(); } // Create active sessions table { - let query = "CREATE TABLE sessions (username TEXT PRIMARY KEY, sid TEXT);"; + let query = "CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);"; connection.execute(query, ()).unwrap(); } diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 7e4c574..17f8e14 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,6 +1,11 @@ +use std::sync::{Arc, Mutex}; + +use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}; use kawari::config::get_config; +use kawari::lobby::CharaMake; use kawari::oodle::OodleNetwork; use kawari::packet::{ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive}; +use kawari::world::PlayerData; use kawari::world::ipc::{ ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData, ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, StatusEffect, @@ -12,15 +17,130 @@ use kawari::world::{ Position, SocialList, }, }; -use kawari::{ - CHAR_NAME, CITY_STATE, CONTENT_ID, CUSTOMIZE_DATA, DEITY, NAMEDAY_DAY, NAMEDAY_MONTH, WORLD_ID, - ZONE_ID, common::timestamp_secs, -}; +use kawari::{CHAR_NAME, CITY_STATE, CONTENT_ID, WORLD_ID, ZONE_ID, common::timestamp_secs}; use physis::common::{Language, Platform}; use physis::gamedata::GameData; +use rusqlite::Connection; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; +fn setup_db() -> Arc> { + let connection = Connection::open("world.db").expect("Failed to open database!"); + + // Create characters table + { + let query = "CREATE TABLE IF NOT EXISTS characters (content_id INTEGER PRIMARY KEY, service_account_id INTEGER, actor_id INTEGER);"; + connection.execute(query, ()).unwrap(); + } + + // Create characters data table + { + let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING);"; + connection.execute(query, ()).unwrap(); + } + + Arc::new(Mutex::new(connection)) +} + +fn find_player_data(connection: &Arc>, actor_id: u32) -> PlayerData { + let connection = connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT content_id, service_account_id FROM characters WHERE actor_id = ?1") + .unwrap(); + let (content_id, account_id) = stmt + .query_row((actor_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap(); + + PlayerData { + actor_id, + content_id, + account_id, + } +} + +// TODO: from/to sql int + +fn find_actor_id(connection: &Arc>, content_id: u64) -> u32 { + let connection = connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT actor_id FROM characters WHERE content_id = ?1") + .unwrap(); + + stmt.query_row((content_id,), |row| row.get(0)).unwrap() +} + +fn generate_content_id() -> u32 { + rand::random() +} + +fn generate_actor_id() -> u32 { + rand::random() +} + +/// Gives (content_id, actor_id) +fn create_player_data( + connection: &Arc>, + name: &str, + chara_make: &str, +) -> (u64, u32) { + let content_id = generate_content_id(); + let actor_id = generate_actor_id(); + + let connection = connection.lock().unwrap(); + + // insert ids + connection + .execute( + "INSERT INTO characters VALUES (?1, ?2, ?3);", + (content_id, 0x1, actor_id), + ) + .unwrap(); + + // insert char data + connection + .execute( + "INSERT INTO character_data VALUES (?1, ?2, ?3);", + (content_id, name, chara_make), + ) + .unwrap(); + + (content_id as u64, actor_id) +} + +/// Checks if `name` is in the character data table +fn check_is_name_free(connection: &Arc>, name: &str) -> bool { + let connection = connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT content_id FROM character_data WHERE name = ?1") + .unwrap(); + + !stmt.exists((name,)).unwrap() +} + +struct CharacterData { + name: String, + chara_make: CharaMake, // probably not the ideal way to store this? +} + +fn find_chara_make(connection: &Arc>, content_id: u64) -> CharacterData { + let connection = connection.lock().unwrap(); + + let mut stmt = connection + .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") + .unwrap(); + let (name, chara_make_json): (String, String) = stmt + .query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap(); + + CharacterData { + name, + chara_make: CharaMake::from_json(&chara_make_json), + } +} + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); @@ -29,9 +149,13 @@ async fn main() { tracing::info!("World server started on 127.0.0.1:7100"); + let db_connection = setup_db(); + loop { let (socket, _) = listener.accept().await.unwrap(); + let db_connection = db_connection.clone(); + let state = PacketState { client_key: None, clientbound_oodle: OodleNetwork::new(), @@ -43,7 +167,7 @@ async fn main() { let mut connection = ZoneConnection { socket, state, - player_id: 0, + player_data: PlayerData::default(), spawn_index: 0, zone: Zone::load(ZONE_ID), }; @@ -61,8 +185,16 @@ async fn main() { let (segments, connection_type) = connection.parse_packet(&buf[..n]).await; for segment in &segments { match &segment.segment_type { - SegmentType::InitializeSession { player_id } => { - connection.player_id = *player_id; + SegmentType::InitializeSession { actor_id } => { + tracing::info!("actor id to parse: {actor_id}"); + + // collect actor data + connection.player_data = find_player_data( + &db_connection, + actor_id.parse::().unwrap(), + ); + + println!("player data: {:#?}", connection.player_data); // We have send THEM a keep alive { @@ -81,7 +213,7 @@ async fn main() { match connection_type { kawari::packet::ConnectionType::Zone => { tracing::info!( - "Client {player_id} is initializing zone session..." + "Client {actor_id} is initializing zone session..." ); connection @@ -89,7 +221,7 @@ async fn main() { source_actor: 0, target_actor: 0, segment_type: SegmentType::ZoneInitialize { - player_id: *player_id, + player_id: connection.player_data.actor_id, timestamp: timestamp_secs(), }, }) @@ -97,7 +229,7 @@ async fn main() { } kawari::packet::ConnectionType::Chat => { tracing::info!( - "Client {player_id} is initializing chat session..." + "Client {actor_id} is initializing chat session..." ); { @@ -106,7 +238,7 @@ async fn main() { source_actor: 0, target_actor: 0, segment_type: SegmentType::ZoneInitialize { - player_id: *player_id, + player_id: connection.player_data.actor_id, timestamp: timestamp_secs(), }, }) @@ -125,8 +257,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: *player_id, - target_actor: *player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -151,7 +283,7 @@ async fn main() { timestamp: timestamp_secs(), data: ServerZoneIpcData::InitResponse { unk1: 0, - character_id: connection.player_id, + character_id: connection.player_data.actor_id, unk2: 0, }, ..Default::default() @@ -159,8 +291,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -188,8 +320,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -211,8 +343,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -220,22 +352,37 @@ async fn main() { // Player Setup { + let chara_details = find_chara_make( + &db_connection, + connection.player_data.content_id, + ); + let ipc = ServerZoneIpcSegment { op_code: ServerZoneIpcType::PlayerSetup, timestamp: timestamp_secs(), data: ServerZoneIpcData::PlayerSetup(PlayerSetup { - content_id: CONTENT_ID, + content_id: connection.player_data.content_id, exp: [10000; 32], levels: [100; 32], - name: CHAR_NAME.to_string(), - char_id: connection.player_id, - race: CUSTOMIZE_DATA.race, - gender: CUSTOMIZE_DATA.gender, - tribe: CUSTOMIZE_DATA.subrace, + name: chara_details.name, + char_id: connection.player_data.actor_id, + race: chara_details.chara_make.customize.race, + gender: chara_details + .chara_make + .customize + .gender, + tribe: chara_details + .chara_make + .customize + .subrace, city_state: CITY_STATE, - nameday_month: NAMEDAY_MONTH, - nameday_day: NAMEDAY_DAY, - deity: DEITY, + nameday_month: chara_details + .chara_make + .birth_month + as u8, + nameday_day: chara_details.chara_make.birth_day + as u8, + deity: chara_details.chara_make.guardian as u8, ..Default::default() }), ..Default::default() @@ -243,8 +390,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -266,8 +413,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -278,6 +425,11 @@ async fn main() { "Client has finished loading... spawning in!" ); + let chara_details = find_chara_make( + &db_connection, + connection.player_data.content_id, + ); + // send player spawn { let ipc = ServerZoneIpcSegment { @@ -290,14 +442,14 @@ async fn main() { home_world_id: WORLD_ID, title: 1, class_job: 35, - name: CHAR_NAME.to_string(), + name: chara_details.name, hp_curr: 100, hp_max: 100, mp_curr: 100, mp_max: 100, object_kind: ObjectKind::Player, gm_rank: 3, - look: CUSTOMIZE_DATA, + look: chara_details.chara_make.customize, fc_tag: "LOCAL".to_string(), subtype: 4, models: [ @@ -323,8 +475,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -343,8 +495,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -401,8 +553,12 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection + .player_data + .actor_id, + target_actor: connection + .player_data + .actor_id, segment_type: SegmentType::Ipc { data: ipc, }, @@ -425,8 +581,12 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection + .player_data + .actor_id, + target_actor: connection + .player_data + .actor_id, segment_type: SegmentType::Ipc { data: ipc, }, @@ -454,8 +614,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -480,8 +640,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -560,8 +720,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -580,8 +740,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -614,9 +774,8 @@ async fn main() { .read_excel_sheet("Action", &exh, Language::English, 0) .unwrap(); - let action_row = &exd - .read_row(&exh, request.action_id as u32) - .unwrap()[0]; + let action_row = + &exd.read_row(&exh, request.action_id).unwrap()[0]; println!("Found action: {:#?}", action_row); @@ -631,7 +790,9 @@ async fn main() { effect_id: 50, param: 0, duration: 50.0, - source_actor_id: connection.player_id, + source_actor_id: connection + .player_data + .actor_id, }; 30], ..Default::default() @@ -642,8 +803,8 @@ async fn main() { connection .send_segment(PacketSegment { - source_actor: connection.player_id, - target_actor: connection.player_id, + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -664,6 +825,108 @@ async fn main() { SegmentType::KeepAliveResponse { .. } => { tracing::info!("Got keep alive response from client... cool..."); } + SegmentType::CustomIpc { data } => { + match &data.data { + CustomIpcData::RequestCreateCharacter { + name, + chara_make_json, + } => { + tracing::info!( + "creating character from: {name} {chara_make_json}" + ); + + let (content_id, actor_id) = create_player_data( + &db_connection, + name, + chara_make_json, + ); + + tracing::info!( + "Created new player: {content_id} {actor_id}" + ); + + // send them the new actor and content id + { + connection + .send_segment(PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::CustomIpc { + data: CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: + CustomIpcType::CharacterCreated, + server_id: 0, + timestamp: 0, + data: CustomIpcData::CharacterCreated { + actor_id, + content_id, + }, + }, + }, + }) + .await; + } + } + CustomIpcData::GetActorId { content_id } => { + let actor_id = find_actor_id(&db_connection, *content_id); + + tracing::info!("We found an actor id: {actor_id}"); + + // send them the actor id + { + connection + .send_segment(PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::CustomIpc { + data: CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::ActorIdFound, + server_id: 0, + timestamp: 0, + data: CustomIpcData::ActorIdFound { + actor_id, + }, + }, + }, + }) + .await; + } + } + CustomIpcData::CheckNameIsAvailable { name } => { + let is_name_free = check_is_name_free(&db_connection, name); + let is_name_free = if is_name_free { 1 } else { 0 }; + + // send response + { + connection + .send_segment(PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::CustomIpc { + data: CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::NameIsAvailableResponse, + server_id: 0, + timestamp: 0, + data: CustomIpcData::NameIsAvailableResponse { + free: is_name_free, + }, + }, + }, + }) + .await; + } + } + _ => panic!( + "The server is recieving a response or unknown custom IPC!" + ), + } + } _ => { panic!("The server is recieving a response or unknown packet!") } diff --git a/src/common/custom_ipc.rs b/src/common/custom_ipc.rs new file mode 100644 index 0000000..fedec15 --- /dev/null +++ b/src/common/custom_ipc.rs @@ -0,0 +1,88 @@ +use binrw::binrw; + +use crate::{ + CHAR_NAME_MAX_LENGTH, + common::read_string, + packet::{IpcSegment, ReadWriteIpcSegment}, +}; + +use super::write_string; + +pub type CustomIpcSegment = IpcSegment; + +impl ReadWriteIpcSegment for CustomIpcSegment { + fn calc_size(&self) -> u32 { + // 16 is the size of the IPC header + 16 + match self.op_code { + CustomIpcType::RequestCreateCharacter => 1024 + CHAR_NAME_MAX_LENGTH as u32, + CustomIpcType::CharacterCreated => 12, + CustomIpcType::GetActorId => 8, + CustomIpcType::ActorIdFound => 4, + CustomIpcType::CheckNameIsAvailable => CHAR_NAME_MAX_LENGTH as u32, + CustomIpcType::NameIsAvailableResponse => 1, + } + } +} + +#[binrw] +#[brw(repr = u16)] +#[derive(Default, Clone, PartialEq, Debug)] +pub enum CustomIpcType { + #[default] + /// Request the world server to create a character + RequestCreateCharacter = 0x1, + /// Response from the world server when the character is created + CharacterCreated = 0x2, + /// Request the actor id from the content id of a character + GetActorId = 0x3, + /// Response from the world server when the actor id is found + ActorIdFound = 0x4, + /// Check if a name is available on the world server + CheckNameIsAvailable = 0x5, + /// Response to CheckNameIsAvailable + NameIsAvailableResponse = 0x6, +} + +#[binrw] +#[br(import(magic: &CustomIpcType))] +#[derive(Debug, Clone)] +pub enum CustomIpcData { + #[br(pre_assert(*magic == CustomIpcType::RequestCreateCharacter))] + RequestCreateCharacter { + #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] + #[br(count = CHAR_NAME_MAX_LENGTH)] + #[br(map = read_string)] + #[bw(map = write_string)] + name: String, + #[bw(pad_size_to = 1024)] + #[br(count = 1024)] + #[br(map = read_string)] + #[bw(map = write_string)] + chara_make_json: String, + }, + #[br(pre_assert(*magic == CustomIpcType::CharacterCreated))] + CharacterCreated { actor_id: u32, content_id: u64 }, + #[br(pre_assert(*magic == CustomIpcType::GetActorId))] + GetActorId { content_id: u64 }, + #[br(pre_assert(*magic == CustomIpcType::ActorIdFound))] + ActorIdFound { actor_id: u32 }, + #[br(pre_assert(*magic == CustomIpcType::CheckNameIsAvailable))] + CheckNameIsAvailable { + #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] + #[br(count = CHAR_NAME_MAX_LENGTH)] + #[br(map = read_string)] + #[bw(map = write_string)] + name: String, + }, + #[br(pre_assert(*magic == CustomIpcType::NameIsAvailableResponse))] + NameIsAvailableResponse { free: u8 }, +} + +impl Default for CustomIpcData { + fn default() -> CustomIpcData { + CustomIpcData::RequestCreateCharacter { + chara_make_json: String::new(), + name: String::new(), + } + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 70bfc4b..57bedf4 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -6,6 +6,8 @@ use std::{ mod customize_data; pub use customize_data::CustomizeData; +pub mod custom_ipc; + pub(crate) fn read_string(byte_stream: Vec) -> String { let str = String::from_utf8(byte_stream).unwrap(); str.trim_matches(char::from(0)).to_string() // trim \0 from the end of strings diff --git a/src/lobby/chara_make.rs b/src/lobby/chara_make.rs index b953343..550f06e 100644 --- a/src/lobby/chara_make.rs +++ b/src/lobby/chara_make.rs @@ -1,4 +1,4 @@ -use serde_json::Value; +use serde_json::{Value, json}; use crate::common::CustomizeData; @@ -28,6 +28,26 @@ impl CharaMake { unk6: content[6].as_str().unwrap().parse::().unwrap(), } } + + pub fn to_json(&self) -> String { + let content = json!([ + self.customize.to_json(), + self.unk1, + self.guardian, + self.birth_month, + self.classjob, + self.birth_day, + self.unk6, + ]); + + let obj = json!({ + "content": content, + "classname": "CharaMake", + "classid": 118, + }); + + serde_json::to_string(&obj).unwrap() + } } #[cfg(test)] diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 13529e3..6c90a40 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -29,6 +29,8 @@ pub struct LobbyConnection { pub session_id: Option, pub state: PacketState, + + pub stored_character_creation_name: String, } impl LobbyConnection { @@ -209,7 +211,7 @@ impl LobbyConnection { }; let mut characters = vec![CharacterDetails { - id: 0, + actor_id: 0, content_id: CONTENT_ID, index: 0, unk1: [0; 16], @@ -295,16 +297,16 @@ impl LobbyConnection { } /// Send the host information for the world server to the client. - pub async fn send_enter_world(&mut self, sequence: u64, lookup_id: u64) { + pub async fn send_enter_world(&mut self, sequence: u64, content_id: u64, actor_id: u32) { let Some(session_id) = &self.session_id else { panic!("Missing session id!"); }; let enter_world = ServerLobbyIpcData::LobbyEnterWorld { sequence, - character_id: 0, - content_id: lookup_id, // TODO: shouldn't these be named the same then? - session_id: session_id.clone(), + actor_id, + content_id, + token: String::new(), port: 7100, host: "127.0.0.1".to_string(), }; diff --git a/src/lobby/ipc/character_list.rs b/src/lobby/ipc/character_list.rs index 9ebaaf1..30cd378 100644 --- a/src/lobby/ipc/character_list.rs +++ b/src/lobby/ipc/character_list.rs @@ -8,7 +8,7 @@ use super::{read_string, write_string}; #[derive(Debug, Clone, Default)] pub struct CharacterDetails { #[brw(pad_after = 4)] - pub id: u32, + pub actor_id: u32, pub content_id: u64, #[brw(pad_after = 4)] pub index: u32, diff --git a/src/lobby/ipc/mod.rs b/src/lobby/ipc/mod.rs index aede873..062f063 100644 --- a/src/lobby/ipc/mod.rs +++ b/src/lobby/ipc/mod.rs @@ -144,9 +144,8 @@ pub enum ClientLobbyIpcData { LobbyCharacterAction(LobbyCharacterAction), #[br(pre_assert(*magic == ClientLobbyIpcType::RequestEnterWorld))] RequestEnterWorld { - #[brw(pad_before = 16)] sequence: u64, - lookup_id: u64, + content_id: u64, // TODO: what else is in here? }, } @@ -166,7 +165,7 @@ pub enum ServerLobbyIpcData { LobbyCharacterList(LobbyCharacterList), LobbyEnterWorld { sequence: u64, - character_id: u32, + actor_id: u32, #[brw(pad_before = 4)] content_id: u64, #[brw(pad_before = 4)] @@ -174,7 +173,7 @@ pub enum ServerLobbyIpcData { #[br(count = 66)] #[br(map = read_string)] #[bw(map = write_string)] - session_id: String, + token: String, // WHAT IS THIS FOR?? port: u16, #[brw(pad_after = 16)] // garbage? #[br(count = 48)] @@ -232,9 +231,9 @@ mod tests { ServerLobbyIpcType::LobbyEnterWorld, ServerLobbyIpcData::LobbyEnterWorld { sequence: 0, - character_id: 0, + actor_id: 0, content_id: 0, - session_id: String::new(), + token: String::new(), port: 0, host: String::new(), }, diff --git a/src/packet/packet.rs b/src/packet/packet.rs index e66c0a8..1f0ed81 100644 --- a/src/packet/packet.rs +++ b/src/packet/packet.rs @@ -4,7 +4,7 @@ use binrw::{BinRead, BinWrite, binrw}; use tokio::{io::AsyncWriteExt, net::TcpStream}; use crate::{ - common::{read_string, timestamp_msecs}, + common::{custom_ipc::CustomIpcSegment, read_string, timestamp_msecs, write_string}, oodle::OodleNetwork, packet::{compression::compress, encryption::decrypt}, }; @@ -30,9 +30,12 @@ pub enum SegmentType { // Client->Server Packets #[brw(magic = 0x1u32)] InitializeSession { - #[brw(pad_before = 4)] - #[brw(pad_after = 48)] // TODO: probably not empty? - player_id: u32, + #[brw(pad_before = 4)] // empty + #[brw(pad_size_to = 36)] + #[br(count = 36)] + #[br(map = read_string)] + #[bw(map = write_string)] + actor_id: String, // square enix in their infinite wisdom has this as a STRING REPRESENTATION of an integer. what }, #[brw(magic = 0x9u32)] InitializeEncryption { @@ -71,6 +74,10 @@ pub enum SegmentType { #[brw(pad_after = 32)] timestamp: u32, }, + + // Custom Packets + #[brw(magic = 0xAAAAu32)] + CustomIpc { data: CustomIpcSegment }, } #[binrw] @@ -112,6 +119,7 @@ impl PacketSegment { SegmentType::KeepAliveResponse { .. } => 0x8, SegmentType::ZoneInitialize { .. } => 40, SegmentType::InitializeSession { .. } => todo!(), + SegmentType::CustomIpc { data } => data.calc_size(), } } } diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 5c78c3b..df43bcd 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -60,7 +60,7 @@ impl ChatHandler { connection .send_segment(PacketSegment { source_actor: 0x106ad804, - target_actor: connection.player_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -115,7 +115,7 @@ impl ChatHandler { connection .send_segment(PacketSegment { source_actor: 0x106ad804, - target_actor: connection.player_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -138,7 +138,7 @@ impl ChatHandler { connection .send_segment(PacketSegment { source_actor: 0x106ad804, - target_actor: connection.player_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -163,7 +163,7 @@ impl ChatHandler { spawn_index: connection.get_free_spawn_index(), bnpc_base: 13498, bnpc_name: 10261, - spawner_id: connection.player_id, + spawner_id: connection.player_data.actor_id, parent_actor_id: INVALID_OBJECT_ID, // TODO: make default? object_kind: ObjectKind::BattleNpc, level: 1, @@ -189,7 +189,7 @@ impl ChatHandler { connection .send_segment(PacketSegment { source_actor: 0x106ad804, - target_actor: connection.player_id, + target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; diff --git a/src/world/connection.rs b/src/world/connection.rs index 49c8a0c..d6dbf35 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -16,12 +16,19 @@ use super::{ }, }; +#[derive(Debug, Default)] +pub struct PlayerData { + pub actor_id: u32, + pub content_id: u64, + pub account_id: u32, +} + /// Represents a single connection between an instance of the client and the world server pub struct ZoneConnection { pub socket: TcpStream, pub state: PacketState, - pub player_id: u32, + pub player_data: PlayerData, pub zone: Zone, pub spawn_index: u8, @@ -61,8 +68,8 @@ impl ZoneConnection { }; self.send_segment(PacketSegment { - source_actor: self.player_id, - target_actor: self.player_id, + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -88,8 +95,8 @@ impl ZoneConnection { }; self.send_segment(PacketSegment { - source_actor: self.player_id, - target_actor: self.player_id, + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -105,8 +112,8 @@ impl ZoneConnection { }; self.send_segment(PacketSegment { - source_actor: self.player_id, - target_actor: self.player_id, + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -129,8 +136,8 @@ impl ZoneConnection { }; self.send_segment(PacketSegment { - source_actor: self.player_id, - target_actor: self.player_id, + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; @@ -149,8 +156,8 @@ impl ZoneConnection { }; self.send_segment(PacketSegment { - source_actor: self.player_id, - target_actor: self.player_id, + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, segment_type: SegmentType::Ipc { data: ipc }, }) .await; diff --git a/src/world/ipc/npc_spawn.rs b/src/world/ipc/npc_spawn.rs index 0d1c4c1..eb6f78a 100644 --- a/src/world/ipc/npc_spawn.rs +++ b/src/world/ipc/npc_spawn.rs @@ -16,6 +16,8 @@ mod tests { use binrw::BinRead; + use crate::world::ipc::{CharacterMode, ObjectKind}; + use super::*; #[test] diff --git a/src/world/ipc/player_spawn.rs b/src/world/ipc/player_spawn.rs index a10ad99..4cfcb08 100644 --- a/src/world/ipc/player_spawn.rs +++ b/src/world/ipc/player_spawn.rs @@ -23,7 +23,7 @@ mod tests { use binrw::BinRead; - use crate::world::ipc::CharacterMode; + use crate::world::ipc::{CharacterMode, ObjectKind}; use super::*; diff --git a/src/world/mod.rs b/src/world/mod.rs index 20a0b23..8056f71 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -7,4 +7,4 @@ mod chat_handler; pub use chat_handler::ChatHandler; mod connection; -pub use connection::ZoneConnection; +pub use connection::{PlayerData, ZoneConnection};