1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-25 08:27:44 +00:00

Move lobby character actions to LobbyConnection, support deleting characters

This commit is contained in:
Joshua Goins 2025-03-22 17:32:00 -04:00
parent 5c8998ddb2
commit 107a00aa92
6 changed files with 320 additions and 242 deletions

View file

@ -4,14 +4,11 @@ use kawari::common::custom_ipc::CustomIpcType;
use kawari::common::get_world_name; use kawari::common::get_world_name;
use kawari::config::get_config; use kawari::config::get_config;
use kawari::lobby::LobbyConnection; use kawari::lobby::LobbyConnection;
use kawari::lobby::ipc::{ use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment};
CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData,
ServerLobbyIpcSegment, ServerLobbyIpcType,
};
use kawari::lobby::send_custom_world_packet; use kawari::lobby::send_custom_world_packet;
use kawari::oodle::OodleNetwork; use kawari::oodle::OodleNetwork;
use kawari::packet::ConnectionType; 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::io::AsyncReadExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -43,10 +40,9 @@ async fn main() {
state, state,
session_id: None, session_id: None,
stored_character_creation_name: String::new(), stored_character_creation_name: String::new(),
world_name: world_name.clone(),
}; };
let world_name = world_name.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = [0; 2056]; let mut buf = [0; 2056];
loop { loop {
@ -63,7 +59,7 @@ async fn main() {
for segment in &segments { for segment in &segments {
match &segment.segment_type { match &segment.segment_type {
SegmentType::InitializeEncryption { phrase, key } => { SegmentType::InitializeEncryption { phrase, key } => {
connection.initialize_encryption(phrase, key).await; connection.initialize_encryption(phrase, key).await
} }
SegmentType::Ipc { data } => match &data.data { SegmentType::Ipc { data } => match &data.data {
ClientLobbyIpcData::ClientVersionInfo { ClientLobbyIpcData::ClientVersionInfo {
@ -83,194 +79,10 @@ async fn main() {
//connection.send_error(*sequence, 1012, 13101).await; //connection.send_error(*sequence, 1012, 13101).await;
} }
ClientLobbyIpcData::RequestCharacterList { sequence } => { 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) => { ClientLobbyIpcData::LobbyCharacterAction(character_action) => {
match &character_action.action { connection.handle_character_action(character_action).await
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!(),
}
} }
ClientLobbyIpcData::RequestEnterWorld { ClientLobbyIpcData::RequestEnterWorld {
sequence, sequence,

View file

@ -841,6 +841,37 @@ async fn main() {
.await; .await;
} }
} }
CustomIpcData::DeleteCharacter { content_id } => {
database.delete_character(*content_id);
// send response
{
send_packet::<CustomIpcSegment>(
&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!( _ => panic!(
"The server is recieving a response or unknown custom IPC!" "The server is recieving a response or unknown custom IPC!"
), ),

View file

@ -23,6 +23,8 @@ impl ReadWriteIpcSegment for CustomIpcSegment {
CustomIpcType::NameIsAvailableResponse => 1, CustomIpcType::NameIsAvailableResponse => 1,
CustomIpcType::RequestCharacterList => 4, CustomIpcType::RequestCharacterList => 4,
CustomIpcType::RequestCharacterListRepsonse => 1184 * 8, CustomIpcType::RequestCharacterListRepsonse => 1184 * 8,
CustomIpcType::DeleteCharacter => 4,
CustomIpcType::CharacterDeleted => 1,
} }
} }
} }
@ -48,6 +50,10 @@ pub enum CustomIpcType {
RequestCharacterList = 0x7, RequestCharacterList = 0x7,
/// Response to RequestCharacterList /// Response to RequestCharacterList
RequestCharacterListRepsonse = 0x8, RequestCharacterListRepsonse = 0x8,
/// Request that a character be deleted from the world server
DeleteCharacter = 0x9,
/// Response to DeleteCharacter
CharacterDeleted = 0x10,
} }
#[binrw] #[binrw]
@ -92,6 +98,10 @@ pub enum CustomIpcData {
#[br(count = num_characters)] #[br(count = num_characters)]
characters: Vec<CharacterDetails>, // TODO: maybe chunk this into 4 parts ala the lobby server? characters: Vec<CharacterDetails>, // 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 { impl Default for CustomIpcData {

View file

@ -17,8 +17,9 @@ use crate::{
}; };
use super::ipc::{ use super::ipc::{
CharacterDetails, LobbyCharacterList, LobbyServerList, LobbyServiceAccountList, Server, CharacterDetails, LobbyCharacterAction, LobbyCharacterActionKind, LobbyCharacterList,
ServerLobbyIpcData, ServerLobbyIpcSegment, ServerLobbyIpcType, ServiceAccount, LobbyServerList, LobbyServiceAccountList, Server, ServerLobbyIpcData, ServerLobbyIpcSegment,
ServerLobbyIpcType, ServiceAccount,
}; };
use crate::lobby::ipc::ClientLobbyIpcSegment; use crate::lobby::ipc::ClientLobbyIpcSegment;
@ -31,6 +32,8 @@ pub struct LobbyConnection {
pub state: PacketState, pub state: PacketState,
pub stored_character_creation_name: String, pub stored_character_creation_name: String,
pub world_name: String,
} }
impl LobbyConnection { impl LobbyConnection {
@ -334,6 +337,217 @@ impl LobbyConnection {
}) })
.await; .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. /// Sends a custom IPC packet to the world server, meant for private server-to-server communication.

View file

@ -37,7 +37,7 @@ pub enum LobbyCharacterActionKind {
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub struct LobbyCharacterAction { pub struct LobbyCharacterAction {
pub sequence: u64, pub sequence: u64,
pub character_id: u64, pub content_id: u64,
#[br(pad_before = 8)] #[br(pad_before = 8)]
pub character_index: u8, pub character_index: u8,
pub action: LobbyCharacterActionKind, pub action: LobbyCharacterActionKind,

View file

@ -102,54 +102,55 @@ impl WorldDatabase {
.prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1") .prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1")
.unwrap(); .unwrap();
let (name, chara_make): (String, String) = stmt let result: Result<(String, String), rusqlite::Error> =
.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?))) stmt.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?)));
.unwrap();
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 { let select_data = ClientSelectData {
game_name_unk: "Final Fantasy".to_string(), game_name_unk: "Final Fantasy".to_string(),
current_class: 2, current_class: 2,
class_levels: [5; 30], class_levels: [5; 30],
race: chara_make.customize.race as i32, race: chara_make.customize.race as i32,
subrace: chara_make.customize.subrace as i32, subrace: chara_make.customize.subrace as i32,
gender: chara_make.customize.gender as i32, gender: chara_make.customize.gender as i32,
birth_month: chara_make.birth_month, birth_month: chara_make.birth_month,
birth_day: chara_make.birth_day, birth_day: chara_make.birth_day,
guardian: chara_make.guardian, guardian: chara_make.guardian,
unk8: 0, unk8: 0,
unk9: 0, unk9: 0,
zone_id: ZONE_ID as i32, zone_id: ZONE_ID as i32,
unk11: 0, unk11: 0,
customize: chara_make.customize, customize: chara_make.customize,
unk12: 0, unk12: 0,
unk13: 0, unk13: 0,
unk14: [0; 10], unk14: [0; 10],
unk15: 0, unk15: 0,
unk16: 0, unk16: 0,
legacy_character: 0, legacy_character: 0,
unk18: 0, unk18: 0,
unk19: 0, unk19: 0,
unk20: 0, unk20: 0,
unk21: String::new(), unk21: String::new(),
unk22: 0, unk22: 0,
unk23: 0, unk23: 0,
}; };
characters.push(CharacterDetails { characters.push(CharacterDetails {
actor_id: *actor_id, actor_id: *actor_id,
content_id: *content_id as u64, content_id: *content_id as u64,
index: index as u32, index: index as u32,
unk1: [0; 16], unk1: [0; 16],
origin_server_id: world_id, origin_server_id: world_id,
current_server_id: world_id, current_server_id: world_id,
character_name: name.clone(), character_name: name.clone(),
origin_server_name: world_name.to_string(), origin_server_name: world_name.to_string(),
current_server_name: world_name.to_string(), current_server_name: world_name.to_string(),
character_detail_json: select_data.to_json(), character_detail_json: select_data.to_json(),
unk2: [0; 20], unk2: [0; 20],
}); });
}
} }
characters characters
@ -215,4 +216,14 @@ impl WorldDatabase {
chara_make: CharaMake::from_json(&chara_make_json), 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();
}
} }