1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-26 16:37:46 +00:00

Show the actual character list on the lobby screen

This doesn't do any actual account checking yet, but it works pretty well.
This commit is contained in:
Joshua Goins 2025-03-21 21:26:32 -04:00
parent 7b77c19008
commit 4b67b22c9f
6 changed files with 239 additions and 125 deletions

View file

@ -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<CustomIpcSegment> {
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<CustomIpcSegment> = 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::<CustomIpcSegment>(&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!"),
}
}

View file

@ -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<Mutex<Connection>>, content_id: u64) -> u32 {
stmt.query_row((content_id,), |row| row.get(0)).unwrap()
}
fn get_character_list(
connection: &Arc<Mutex<Connection>>,
service_account_id: u32,
) -> Vec<CharacterDetails> {
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::<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::RequestCharacterListRepsonse,
server_id: 0,
timestamp: 0,
data: CustomIpcData::RequestCharacterListRepsonse {
characters
},
},
},
}],
)
.await;
}
}
_ => panic!(
"The server is recieving a response or unknown custom IPC!"
),

View file

@ -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<CharacterDetails>, // TODO: maybe chunk this into 4 parts ala the lobby server?
},
}
impl Default for CustomIpcData {

View file

@ -68,30 +68,30 @@ impl CustomizeData {
Self {
race: json[0].as_str().unwrap().parse::<u8>().unwrap(),
gender: json[1].as_str().unwrap().parse::<u8>().unwrap(),
height: json[2].as_str().unwrap().parse::<u8>().unwrap(),
subrace: json[3].as_str().unwrap().parse::<u8>().unwrap(),
face: json[4].as_str().unwrap().parse::<u8>().unwrap(),
hair: json[5].as_str().unwrap().parse::<u8>().unwrap(),
enable_highlights: json[6].as_str().unwrap().parse::<u8>().unwrap(),
skin_tone: json[7].as_str().unwrap().parse::<u8>().unwrap(),
right_eye_color: json[8].as_str().unwrap().parse::<u8>().unwrap(),
hair_tone: json[9].as_str().unwrap().parse::<u8>().unwrap(),
highlights: json[10].as_str().unwrap().parse::<u8>().unwrap(),
facial_features: json[11].as_str().unwrap().parse::<u8>().unwrap(),
facial_feature_color: json[12].as_str().unwrap().parse::<u8>().unwrap(),
eyebrows: json[13].as_str().unwrap().parse::<u8>().unwrap(),
left_eye_color: json[14].as_str().unwrap().parse::<u8>().unwrap(),
eyes: json[15].as_str().unwrap().parse::<u8>().unwrap(),
nose: json[16].as_str().unwrap().parse::<u8>().unwrap(),
jaw: json[17].as_str().unwrap().parse::<u8>().unwrap(),
mouth: json[18].as_str().unwrap().parse::<u8>().unwrap(),
lips_tone_fur_pattern: json[19].as_str().unwrap().parse::<u8>().unwrap(),
race_feature_size: json[20].as_str().unwrap().parse::<u8>().unwrap(),
race_feature_type: json[21].as_str().unwrap().parse::<u8>().unwrap(),
bust: json[22].as_str().unwrap().parse::<u8>().unwrap(),
face_paint: json[23].as_str().unwrap().parse::<u8>().unwrap(),
face_paint_color: json[24].as_str().unwrap().parse::<u8>().unwrap(),
age: 1,
age: json[2].as_str().unwrap().parse::<u8>().unwrap(),
height: json[3].as_str().unwrap().parse::<u8>().unwrap(),
subrace: json[4].as_str().unwrap().parse::<u8>().unwrap(),
face: json[5].as_str().unwrap().parse::<u8>().unwrap(),
hair: json[6].as_str().unwrap().parse::<u8>().unwrap(),
enable_highlights: json[7].as_str().unwrap().parse::<u8>().unwrap(),
skin_tone: json[8].as_str().unwrap().parse::<u8>().unwrap(),
right_eye_color: json[9].as_str().unwrap().parse::<u8>().unwrap(),
hair_tone: json[10].as_str().unwrap().parse::<u8>().unwrap(),
highlights: json[11].as_str().unwrap().parse::<u8>().unwrap(),
facial_features: json[12].as_str().unwrap().parse::<u8>().unwrap(),
facial_feature_color: json[13].as_str().unwrap().parse::<u8>().unwrap(),
eyebrows: json[14].as_str().unwrap().parse::<u8>().unwrap(),
left_eye_color: json[15].as_str().unwrap().parse::<u8>().unwrap(),
eyes: json[16].as_str().unwrap().parse::<u8>().unwrap(),
nose: json[17].as_str().unwrap().parse::<u8>().unwrap(),
jaw: json[18].as_str().unwrap().parse::<u8>().unwrap(),
mouth: json[19].as_str().unwrap().parse::<u8>().unwrap(),
lips_tone_fur_pattern: json[20].as_str().unwrap().parse::<u8>().unwrap(),
race_feature_size: json[21].as_str().unwrap().parse::<u8>().unwrap(),
race_feature_type: json[22].as_str().unwrap().parse::<u8>().unwrap(),
bust: json[23].as_str().unwrap().parse::<u8>().unwrap(),
face_paint: json[24].as_str().unwrap().parse::<u8>().unwrap(),
face_paint_color: json[25].as_str().unwrap().parse::<u8>().unwrap(),
}
}
}

View file

@ -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<CustomIpcSegment> {
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<CustomIpcSegment> = 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::<CustomIpcSegment>(&buf[..n], &mut packet_state).await;
match &segments[0].segment_type {
SegmentType::CustomIpc { data } => Some(data.clone()),
_ => None,
}
}

View file

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