From 107a00aa9278b6f35a50987def1695f89a5c4e75 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 22 Mar 2025 17:32:00 -0400 Subject: [PATCH] Move lobby character actions to LobbyConnection, support deleting characters --- src/bin/kawari-lobby.rs | 200 +-------------------------- src/bin/kawari-world.rs | 31 +++++ src/common/custom_ipc.rs | 10 ++ src/lobby/connection.rs | 218 +++++++++++++++++++++++++++++- src/lobby/ipc/character_action.rs | 2 +- src/world/database.rs | 101 ++++++++------ 6 files changed, 320 insertions(+), 242 deletions(-) diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 7ec325a..099c809 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -4,14 +4,11 @@ use kawari::common::custom_ipc::CustomIpcType; use kawari::common::get_world_name; use kawari::config::get_config; use kawari::lobby::LobbyConnection; -use kawari::lobby::ipc::{ - CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData, - ServerLobbyIpcSegment, ServerLobbyIpcType, -}; +use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment}; use kawari::lobby::send_custom_world_packet; use kawari::oodle::OodleNetwork; use kawari::packet::ConnectionType; -use kawari::packet::{PacketSegment, PacketState, SegmentType, send_keep_alive}; +use kawari::packet::{PacketState, SegmentType, send_keep_alive}; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; @@ -43,10 +40,9 @@ async fn main() { state, session_id: None, stored_character_creation_name: String::new(), + world_name: world_name.clone(), }; - let world_name = world_name.clone(); - tokio::spawn(async move { let mut buf = [0; 2056]; loop { @@ -63,7 +59,7 @@ async fn main() { for segment in &segments { match &segment.segment_type { SegmentType::InitializeEncryption { phrase, key } => { - connection.initialize_encryption(phrase, key).await; + connection.initialize_encryption(phrase, key).await } SegmentType::Ipc { data } => match &data.data { ClientLobbyIpcData::ClientVersionInfo { @@ -83,194 +79,10 @@ async fn main() { //connection.send_error(*sequence, 1012, 13101).await; } ClientLobbyIpcData::RequestCharacterList { sequence } => { - tracing::info!("Client is requesting character list..."); - - connection.send_lobby_info(*sequence).await; + connection.send_lobby_info(*sequence).await } ClientLobbyIpcData::LobbyCharacterAction(character_action) => { - match &character_action.action { - LobbyCharacterActionKind::ReserveName => { - tracing::info!( - "Player is requesting {} as a new character name!", - character_action.name - ); - - // 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 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(); - - let ipc = ServerLobbyIpcSegment { - unk1: 0, - unk2: 0, - op_code: ServerLobbyIpcType::CharacterCreated, - server_id: 0, - timestamp: 0, - data: ServerLobbyIpcData::CharacterCreated { - sequence: character_action.sequence + 1, - unk: 0x00010101, - details: CharacterDetails { - character_name: character_action - .name - .clone(), - origin_server_name: world_name.clone(), - current_server_name: world_name.clone(), - ..Default::default() - }, - }, - }; - - connection - .send_segment(PacketSegment { - source_actor: 0x0, - target_actor: 0x0, - segment_type: SegmentType::Ipc { - data: ipc, - }, - }) - .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, // TODO: I screwed this up when translating from the old struct to the new LobbyError - 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 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 - { - let ipc = ServerLobbyIpcSegment { - unk1: 0, - unk2: 0, - op_code: ServerLobbyIpcType::CharacterCreated, - server_id: 0, - timestamp: 0, - data: ServerLobbyIpcData::CharacterCreated { - sequence: character_action.sequence + 1, - unk: 0x00020101, - details: CharacterDetails { - actor_id: our_actor_id, - content_id: our_content_id, - character_name: character_action - .name - .clone(), - origin_server_name: world_name.clone(), - current_server_name: world_name.clone(), - ..Default::default() - }, - }, - }; - - connection - .send_segment(PacketSegment { - source_actor: 0x0, - target_actor: 0x0, - segment_type: SegmentType::Ipc { - data: ipc, - }, - }) - .await; - } - } - LobbyCharacterActionKind::Rename => todo!(), - LobbyCharacterActionKind::Delete => todo!(), - LobbyCharacterActionKind::Move => todo!(), - LobbyCharacterActionKind::RemakeRetainer => todo!(), - LobbyCharacterActionKind::RemakeChara => todo!(), - LobbyCharacterActionKind::SettingsUploadBegin => todo!(), - LobbyCharacterActionKind::SettingsUpload => todo!(), - LobbyCharacterActionKind::WorldVisit => todo!(), - LobbyCharacterActionKind::DataCenterToken => todo!(), - LobbyCharacterActionKind::Request => todo!(), - } + connection.handle_character_action(character_action).await } ClientLobbyIpcData::RequestEnterWorld { sequence, diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 2f59eb7..5911657 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -841,6 +841,37 @@ async fn main() { .await; } } + CustomIpcData::DeleteCharacter { content_id } => { + database.delete_character(*content_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::CharacterDeleted, + server_id: 0, + timestamp: 0, + data: CustomIpcData::CharacterDeleted { + deleted: 1, + }, + }, + }, + }], + ) + .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 3b2d4f7..bb0afdc 100644 --- a/src/common/custom_ipc.rs +++ b/src/common/custom_ipc.rs @@ -23,6 +23,8 @@ impl ReadWriteIpcSegment for CustomIpcSegment { CustomIpcType::NameIsAvailableResponse => 1, CustomIpcType::RequestCharacterList => 4, CustomIpcType::RequestCharacterListRepsonse => 1184 * 8, + CustomIpcType::DeleteCharacter => 4, + CustomIpcType::CharacterDeleted => 1, } } } @@ -48,6 +50,10 @@ pub enum CustomIpcType { RequestCharacterList = 0x7, /// Response to RequestCharacterList RequestCharacterListRepsonse = 0x8, + /// Request that a character be deleted from the world server + DeleteCharacter = 0x9, + /// Response to DeleteCharacter + CharacterDeleted = 0x10, } #[binrw] @@ -92,6 +98,10 @@ pub enum CustomIpcData { #[br(count = num_characters)] characters: Vec, // TODO: maybe chunk this into 4 parts ala the lobby server? }, + #[br(pre_assert(*magic == CustomIpcType::DeleteCharacter))] + DeleteCharacter { content_id: u64 }, + #[br(pre_assert(*magic == CustomIpcType::CharacterDeleted))] + CharacterDeleted { deleted: u8 }, } impl Default for CustomIpcData { diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 8a78e69..d86010b 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -17,8 +17,9 @@ use crate::{ }; use super::ipc::{ - CharacterDetails, LobbyCharacterList, LobbyServerList, LobbyServiceAccountList, Server, - ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, ServiceAccount, + CharacterDetails, LobbyCharacterAction, LobbyCharacterActionKind, LobbyCharacterList, + LobbyServerList, LobbyServiceAccountList, Server, ServerLobbyIpcData, ServerLobbyIpcSegment, + ServerLobbyIpcType, ServiceAccount, }; use crate::lobby::ipc::ClientLobbyIpcSegment; @@ -31,6 +32,8 @@ pub struct LobbyConnection { pub state: PacketState, pub stored_character_creation_name: String, + + pub world_name: String, } impl LobbyConnection { @@ -334,6 +337,217 @@ impl LobbyConnection { }) .await; } + + pub async fn handle_character_action(&mut self, character_action: &LobbyCharacterAction) { + match &character_action.action { + LobbyCharacterActionKind::ReserveName => { + tracing::info!( + "Player is requesting {} as a new character name!", + character_action.name + ); + + // 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 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 { + self.stored_character_creation_name = character_action.name.clone(); + + let ipc = ServerLobbyIpcSegment { + unk1: 0, + unk2: 0, + op_code: ServerLobbyIpcType::CharacterCreated, + server_id: 0, + timestamp: 0, + data: ServerLobbyIpcData::CharacterCreated { + sequence: character_action.sequence + 1, + unk: 0x00010101, + details: CharacterDetails { + character_name: character_action.name.clone(), + origin_server_name: self.world_name.clone(), + current_server_name: self.world_name.clone(), + ..Default::default() + }, + }, + }; + + self.send_segment(PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .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, // TODO: I screwed this up when translating from the old struct to the new LobbyError + exd_error_id: 0, + value: 0, + unk1: 0, + }, + }; + + let response_packet = PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }; + self.send_segment(response_packet).await; + } + } + LobbyCharacterActionKind::Create => { + tracing::info!("Player is creating a new character!"); + + 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: self.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 + { + let ipc = ServerLobbyIpcSegment { + unk1: 0, + unk2: 0, + op_code: ServerLobbyIpcType::CharacterCreated, + server_id: 0, + timestamp: 0, + data: ServerLobbyIpcData::CharacterCreated { + sequence: character_action.sequence + 1, + unk: 0x00020101, + details: CharacterDetails { + actor_id: our_actor_id, + content_id: our_content_id, + character_name: character_action.name.clone(), + origin_server_name: self.world_name.clone(), + current_server_name: self.world_name.clone(), + ..Default::default() + }, + }, + }; + + self.send_segment(PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + } + LobbyCharacterActionKind::Rename => todo!(), + LobbyCharacterActionKind::Delete => { + // tell the world server to yeet this guy + { + let ipc_segment = CustomIpcSegment { + unk1: 0, + unk2: 0, + op_code: CustomIpcType::DeleteCharacter, + server_id: 0, + timestamp: 0, + data: CustomIpcData::DeleteCharacter { + content_id: character_action.content_id, + }, + }; + + let _ = send_custom_world_packet(ipc_segment).await.unwrap(); + + // we intentionally don't care about the response right now, it's not expected to fail + } + + // send a confirmation that the deletion was successful + { + let ipc = ServerLobbyIpcSegment { + unk1: 0, + unk2: 0, + op_code: ServerLobbyIpcType::CharacterCreated, // FIXME: a TERRIBLE name for this packet + server_id: 0, + timestamp: 0, + data: ServerLobbyIpcData::CharacterCreated { + sequence: character_action.sequence + 1, + unk: 0x00040101, // TODO: probably LobbyCharacterAction actually, see create character packet too + details: CharacterDetails { + actor_id: 0, // TODO: fill maybe? + content_id: character_action.content_id, + character_name: character_action.name.clone(), + origin_server_name: self.world_name.clone(), + current_server_name: self.world_name.clone(), + ..Default::default() + }, + }, + }; + + self.send_segment(PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + } + LobbyCharacterActionKind::Move => todo!(), + LobbyCharacterActionKind::RemakeRetainer => todo!(), + LobbyCharacterActionKind::RemakeChara => todo!(), + LobbyCharacterActionKind::SettingsUploadBegin => todo!(), + LobbyCharacterActionKind::SettingsUpload => todo!(), + LobbyCharacterActionKind::WorldVisit => todo!(), + LobbyCharacterActionKind::DataCenterToken => todo!(), + LobbyCharacterActionKind::Request => todo!(), + } + } } /// Sends a custom IPC packet to the world server, meant for private server-to-server communication. diff --git a/src/lobby/ipc/character_action.rs b/src/lobby/ipc/character_action.rs index c4ffacb..0d0bf2e 100644 --- a/src/lobby/ipc/character_action.rs +++ b/src/lobby/ipc/character_action.rs @@ -37,7 +37,7 @@ pub enum LobbyCharacterActionKind { #[derive(Clone, PartialEq, Debug)] pub struct LobbyCharacterAction { pub sequence: u64, - pub character_id: u64, + pub content_id: u64, #[br(pad_before = 8)] pub character_index: u8, pub action: LobbyCharacterActionKind, diff --git a/src/world/database.rs b/src/world/database.rs index 1c378cb..f340830 100644 --- a/src/world/database.rs +++ b/src/world/database.rs @@ -102,54 +102,55 @@ impl WorldDatabase { .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 result: Result<(String, String), rusqlite::Error> = + stmt.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))); - let chara_make = CharaMake::from_json(&chara_make); + if let Ok((name, chara_make)) = result { + 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, - }; + 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], - }); + 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], + }); + } } characters @@ -215,4 +216,14 @@ impl WorldDatabase { chara_make: CharaMake::from_json(&chara_make_json), } } + + /// Deletes a character and all associated data + pub fn delete_character(&self, content_id: u64) { + let connection = self.connection.lock().unwrap(); + + let mut stmt = connection + .prepare("DELETE FROM character_data WHERE content_id = ?1; DELETE FROM characters WHERE content_id = ?1;") + .unwrap(); + stmt.execute((content_id,)).unwrap(); + } }