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:
parent
7b77c19008
commit
4b67b22c9f
6 changed files with 239 additions and 125 deletions
|
@ -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!"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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!"
|
||||
),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue