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::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,

View file

@ -841,6 +841,37 @@ async fn main() {
.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!(
"The server is recieving a response or unknown custom IPC!"
),

View file

@ -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<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 {

View file

@ -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.

View file

@ -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,

View file

@ -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();
}
}