mirror of
https://github.com/redstrate/Kawari.git
synced 2025-04-21 15:07:45 +00:00
Implement basic character persistence, World <-> Lobby server communication
This is unfortunately lumped into one big commit, and is very hacky and WIP but does indeed work! Since the Lobby and World server are two separate servers, it uses it's own custom IPC packets (reusing the same packet structures as regular game ones.) The characters you create in the Lobby server are now saved in the World server, but this is not yet reflected in the Lobby screen.
This commit is contained in:
parent
f4536f2cb7
commit
0900d0b94e
16 changed files with 667 additions and 132 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
||||||
.idea/
|
.idea/
|
||||||
config.json
|
config.json
|
||||||
*.bin
|
*.bin
|
||||||
|
*.db
|
||||||
|
|
|
@ -1,15 +1,61 @@
|
||||||
use kawari::lobby::CharaMake;
|
use kawari::common::custom_ipc::CustomIpcData;
|
||||||
|
use kawari::common::custom_ipc::CustomIpcSegment;
|
||||||
|
use kawari::common::custom_ipc::CustomIpcType;
|
||||||
use kawari::lobby::LobbyConnection;
|
use kawari::lobby::LobbyConnection;
|
||||||
use kawari::lobby::ipc::{
|
use kawari::lobby::ipc::{
|
||||||
CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData,
|
CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData,
|
||||||
ServerLobbyIpcSegment, ServerLobbyIpcType,
|
ServerLobbyIpcSegment, ServerLobbyIpcType,
|
||||||
};
|
};
|
||||||
use kawari::oodle::OodleNetwork;
|
use kawari::oodle::OodleNetwork;
|
||||||
|
use kawari::packet::CompressionType;
|
||||||
use kawari::packet::ConnectionType;
|
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::packet::{PacketSegment, PacketState, SegmentType, send_keep_alive};
|
||||||
use kawari::{CONTENT_ID, WORLD_NAME};
|
use kawari::{CONTENT_ID, WORLD_NAME};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::net::TcpListener;
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -32,6 +78,7 @@ async fn main() {
|
||||||
socket,
|
socket,
|
||||||
state,
|
state,
|
||||||
session_id: None,
|
session_id: None,
|
||||||
|
stored_character_creation_name: String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
@ -82,37 +129,37 @@ async fn main() {
|
||||||
character_action.name
|
character_action.name
|
||||||
);
|
);
|
||||||
|
|
||||||
// reject
|
// check with the world server if the name is available
|
||||||
/*{
|
let name_request = CustomIpcSegment {
|
||||||
let ipc = IPCSegment {
|
unk1: 0,
|
||||||
unk1: 0,
|
unk2: 0,
|
||||||
unk2: 0,
|
op_code: CustomIpcType::CheckNameIsAvailable,
|
||||||
op_code: IPCOpCode::InitializeChat, // wrong but technically right
|
server_id: 0,
|
||||||
server_id: 0,
|
timestamp: 0,
|
||||||
timestamp: 0,
|
data: CustomIpcData::CheckNameIsAvailable {
|
||||||
data: ClientLobbyIpcType::NameRejection {
|
name: character_action.name.clone(),
|
||||||
unk1: 0x03,
|
},
|
||||||
unk2: 0x0bdb,
|
};
|
||||||
unk3: 0x000132cc,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let response_packet = PacketSegment {
|
let name_response =
|
||||||
source_actor: 0x0,
|
send_custom_world_packet(name_request)
|
||||||
target_actor: 0x0,
|
.await
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
.expect("Failed to get name request packet!");
|
||||||
};
|
let CustomIpcData::NameIsAvailableResponse { free } =
|
||||||
send_packet(
|
&name_response.data
|
||||||
&mut write,
|
else {
|
||||||
&[response_packet],
|
panic!("Unexpedted custom IPC type!")
|
||||||
&mut state,
|
};
|
||||||
CompressionType::Uncompressed,
|
|
||||||
)
|
tracing::info!("Is name free? {free}");
|
||||||
.await;
|
|
||||||
}*/
|
// TODO: use read_bool_as
|
||||||
|
let free: bool = *free == 1u8;
|
||||||
|
|
||||||
|
if free {
|
||||||
|
connection.stored_character_creation_name =
|
||||||
|
character_action.name.clone();
|
||||||
|
|
||||||
// accept
|
|
||||||
{
|
|
||||||
let ipc = ServerLobbyIpcSegment {
|
let ipc = ServerLobbyIpcSegment {
|
||||||
unk1: 0,
|
unk1: 0,
|
||||||
unk2: 0,
|
unk2: 0,
|
||||||
|
@ -145,13 +192,75 @@ async fn main() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.await;
|
.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,
|
||||||
|
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 => {
|
LobbyCharacterActionKind::Create => {
|
||||||
tracing::info!("Player is creating a new character!");
|
tracing::info!("Player is creating a new character!");
|
||||||
|
|
||||||
let chara_make =
|
let our_actor_id;
|
||||||
CharaMake::from_json(&character_action.json);
|
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
|
// a slightly different character created packet now
|
||||||
{
|
{
|
||||||
|
@ -165,8 +274,8 @@ async fn main() {
|
||||||
sequence: character_action.sequence + 1,
|
sequence: character_action.sequence + 1,
|
||||||
unk: 0x00020101,
|
unk: 0x00020101,
|
||||||
details: CharacterDetails {
|
details: CharacterDetails {
|
||||||
id: 0x07369f3a, // notice that we give them an id now
|
actor_id: our_actor_id,
|
||||||
content_id: CONTENT_ID,
|
content_id: our_content_id,
|
||||||
character_name: character_action
|
character_name: character_action
|
||||||
.name
|
.name
|
||||||
.clone(),
|
.clone(),
|
||||||
|
@ -204,11 +313,41 @@ async fn main() {
|
||||||
}
|
}
|
||||||
ClientLobbyIpcData::RequestEnterWorld {
|
ClientLobbyIpcData::RequestEnterWorld {
|
||||||
sequence,
|
sequence,
|
||||||
lookup_id,
|
content_id,
|
||||||
} => {
|
} => {
|
||||||
tracing::info!("Client is joining the world...");
|
tracing::info!("Client is joining the world with {content_id}");
|
||||||
|
|
||||||
connection.send_enter_world(*sequence, *lookup_id).await;
|
let our_actor_id;
|
||||||
|
|
||||||
|
// find the actor id for this content id
|
||||||
|
// NOTE: This is NOT the ideal solution. I theorize the lobby server has it's own records with this information.
|
||||||
|
{
|
||||||
|
let ipc_segment = CustomIpcSegment {
|
||||||
|
unk1: 0,
|
||||||
|
unk2: 0,
|
||||||
|
op_code: CustomIpcType::GetActorId,
|
||||||
|
server_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
data: CustomIpcData::GetActorId {
|
||||||
|
content_id: *content_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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!"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection
|
||||||
|
.send_enter_world(*sequence, *content_id, our_actor_id)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SegmentType::KeepAlive { id, timestamp } => {
|
SegmentType::KeepAlive { id, timestamp } => {
|
||||||
|
|
|
@ -35,6 +35,8 @@ impl LoginServerState {
|
||||||
fn add_user(&self, username: &str, password: &str) {
|
fn add_user(&self, username: &str, password: &str) {
|
||||||
let connection = self.connection.lock().unwrap();
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
tracing::info!("Adding user with username {username}");
|
||||||
|
|
||||||
let query = "INSERT INTO users VALUES (?1, ?2);";
|
let query = "INSERT INTO users VALUES (?1, ?2);";
|
||||||
connection
|
connection
|
||||||
.execute(query, (username, password))
|
.execute(query, (username, password))
|
||||||
|
@ -45,6 +47,8 @@ impl LoginServerState {
|
||||||
fn login_user(&self, username: &str, password: &str) -> Result<String, LoginError> {
|
fn login_user(&self, username: &str, password: &str) -> Result<String, LoginError> {
|
||||||
let selected_row: Result<(String, String), rusqlite::Error>;
|
let selected_row: Result<(String, String), rusqlite::Error>;
|
||||||
|
|
||||||
|
tracing::info!("Finding user with username {username}");
|
||||||
|
|
||||||
{
|
{
|
||||||
let connection = self.connection.lock().unwrap();
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
@ -200,17 +204,17 @@ async fn check_session(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_state() -> LoginServerState {
|
fn setup_state() -> LoginServerState {
|
||||||
let connection = Connection::open_in_memory().expect("Failed to open database!");
|
let connection = Connection::open("login.db").expect("Failed to open database!");
|
||||||
|
|
||||||
// Create users table
|
// Create users table
|
||||||
{
|
{
|
||||||
let query = "CREATE TABLE users (username TEXT PRIMARY KEY, password TEXT);";
|
let query = "CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);";
|
||||||
connection.execute(query, ()).unwrap();
|
connection.execute(query, ()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create active sessions table
|
// Create active sessions table
|
||||||
{
|
{
|
||||||
let query = "CREATE TABLE sessions (username TEXT PRIMARY KEY, sid TEXT);";
|
let query = "CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);";
|
||||||
connection.execute(query, ()).unwrap();
|
connection.execute(query, ()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
|
||||||
use kawari::config::get_config;
|
use kawari::config::get_config;
|
||||||
|
use kawari::lobby::CharaMake;
|
||||||
use kawari::oodle::OodleNetwork;
|
use kawari::oodle::OodleNetwork;
|
||||||
use kawari::packet::{ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive};
|
use kawari::packet::{ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive};
|
||||||
|
use kawari::world::PlayerData;
|
||||||
use kawari::world::ipc::{
|
use kawari::world::ipc::{
|
||||||
ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData,
|
ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData,
|
||||||
ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, StatusEffect,
|
ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, StatusEffect,
|
||||||
|
@ -12,15 +17,130 @@ use kawari::world::{
|
||||||
Position, SocialList,
|
Position, SocialList,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use kawari::{
|
use kawari::{CHAR_NAME, CITY_STATE, CONTENT_ID, WORLD_ID, ZONE_ID, common::timestamp_secs};
|
||||||
CHAR_NAME, CITY_STATE, CONTENT_ID, CUSTOMIZE_DATA, DEITY, NAMEDAY_DAY, NAMEDAY_MONTH, WORLD_ID,
|
|
||||||
ZONE_ID, common::timestamp_secs,
|
|
||||||
};
|
|
||||||
use physis::common::{Language, Platform};
|
use physis::common::{Language, Platform};
|
||||||
use physis::gamedata::GameData;
|
use physis::gamedata::GameData;
|
||||||
|
use rusqlite::Connection;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
fn setup_db() -> Arc<Mutex<Connection>> {
|
||||||
|
let connection = Connection::open("world.db").expect("Failed to open database!");
|
||||||
|
|
||||||
|
// Create characters table
|
||||||
|
{
|
||||||
|
let query = "CREATE TABLE IF NOT EXISTS characters (content_id INTEGER PRIMARY KEY, service_account_id INTEGER, actor_id INTEGER);";
|
||||||
|
connection.execute(query, ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create characters data table
|
||||||
|
{
|
||||||
|
let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING);";
|
||||||
|
connection.execute(query, ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(connection))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_player_data(connection: &Arc<Mutex<Connection>>, actor_id: u32) -> PlayerData {
|
||||||
|
let connection = connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT content_id, service_account_id FROM characters WHERE actor_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
let (content_id, account_id) = stmt
|
||||||
|
.query_row((actor_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
PlayerData {
|
||||||
|
actor_id,
|
||||||
|
content_id,
|
||||||
|
account_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: from/to sql int
|
||||||
|
|
||||||
|
fn find_actor_id(connection: &Arc<Mutex<Connection>>, content_id: u64) -> u32 {
|
||||||
|
let connection = connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT actor_id FROM characters WHERE content_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
stmt.query_row((content_id,), |row| row.get(0)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_content_id() -> u32 {
|
||||||
|
rand::random()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_actor_id() -> u32 {
|
||||||
|
rand::random()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives (content_id, actor_id)
|
||||||
|
fn create_player_data(
|
||||||
|
connection: &Arc<Mutex<Connection>>,
|
||||||
|
name: &str,
|
||||||
|
chara_make: &str,
|
||||||
|
) -> (u64, u32) {
|
||||||
|
let content_id = generate_content_id();
|
||||||
|
let actor_id = generate_actor_id();
|
||||||
|
|
||||||
|
let connection = connection.lock().unwrap();
|
||||||
|
|
||||||
|
// insert ids
|
||||||
|
connection
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO characters VALUES (?1, ?2, ?3);",
|
||||||
|
(content_id, 0x1, actor_id),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// insert char data
|
||||||
|
connection
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO character_data VALUES (?1, ?2, ?3);",
|
||||||
|
(content_id, name, chara_make),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(content_id as u64, actor_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if `name` is in the character data table
|
||||||
|
fn check_is_name_free(connection: &Arc<Mutex<Connection>>, name: &str) -> bool {
|
||||||
|
let connection = connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT content_id FROM character_data WHERE name = ?1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
!stmt.exists((name,)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CharacterData {
|
||||||
|
name: String,
|
||||||
|
chara_make: CharaMake, // probably not the ideal way to store this?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_chara_make(connection: &Arc<Mutex<Connection>>, content_id: u64) -> CharacterData {
|
||||||
|
let connection = connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
let (name, chara_make_json): (String, String) = stmt
|
||||||
|
.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
CharacterData {
|
||||||
|
name,
|
||||||
|
chara_make: CharaMake::from_json(&chara_make_json),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
@ -29,9 +149,13 @@ async fn main() {
|
||||||
|
|
||||||
tracing::info!("World server started on 127.0.0.1:7100");
|
tracing::info!("World server started on 127.0.0.1:7100");
|
||||||
|
|
||||||
|
let db_connection = setup_db();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (socket, _) = listener.accept().await.unwrap();
|
let (socket, _) = listener.accept().await.unwrap();
|
||||||
|
|
||||||
|
let db_connection = db_connection.clone();
|
||||||
|
|
||||||
let state = PacketState {
|
let state = PacketState {
|
||||||
client_key: None,
|
client_key: None,
|
||||||
clientbound_oodle: OodleNetwork::new(),
|
clientbound_oodle: OodleNetwork::new(),
|
||||||
|
@ -43,7 +167,7 @@ async fn main() {
|
||||||
let mut connection = ZoneConnection {
|
let mut connection = ZoneConnection {
|
||||||
socket,
|
socket,
|
||||||
state,
|
state,
|
||||||
player_id: 0,
|
player_data: PlayerData::default(),
|
||||||
spawn_index: 0,
|
spawn_index: 0,
|
||||||
zone: Zone::load(ZONE_ID),
|
zone: Zone::load(ZONE_ID),
|
||||||
};
|
};
|
||||||
|
@ -61,8 +185,16 @@ async fn main() {
|
||||||
let (segments, connection_type) = connection.parse_packet(&buf[..n]).await;
|
let (segments, connection_type) = connection.parse_packet(&buf[..n]).await;
|
||||||
for segment in &segments {
|
for segment in &segments {
|
||||||
match &segment.segment_type {
|
match &segment.segment_type {
|
||||||
SegmentType::InitializeSession { player_id } => {
|
SegmentType::InitializeSession { actor_id } => {
|
||||||
connection.player_id = *player_id;
|
tracing::info!("actor id to parse: {actor_id}");
|
||||||
|
|
||||||
|
// collect actor data
|
||||||
|
connection.player_data = find_player_data(
|
||||||
|
&db_connection,
|
||||||
|
actor_id.parse::<u32>().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("player data: {:#?}", connection.player_data);
|
||||||
|
|
||||||
// We have send THEM a keep alive
|
// We have send THEM a keep alive
|
||||||
{
|
{
|
||||||
|
@ -81,7 +213,7 @@ async fn main() {
|
||||||
match connection_type {
|
match connection_type {
|
||||||
kawari::packet::ConnectionType::Zone => {
|
kawari::packet::ConnectionType::Zone => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Client {player_id} is initializing zone session..."
|
"Client {actor_id} is initializing zone session..."
|
||||||
);
|
);
|
||||||
|
|
||||||
connection
|
connection
|
||||||
|
@ -89,7 +221,7 @@ async fn main() {
|
||||||
source_actor: 0,
|
source_actor: 0,
|
||||||
target_actor: 0,
|
target_actor: 0,
|
||||||
segment_type: SegmentType::ZoneInitialize {
|
segment_type: SegmentType::ZoneInitialize {
|
||||||
player_id: *player_id,
|
player_id: connection.player_data.actor_id,
|
||||||
timestamp: timestamp_secs(),
|
timestamp: timestamp_secs(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -97,7 +229,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
kawari::packet::ConnectionType::Chat => {
|
kawari::packet::ConnectionType::Chat => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Client {player_id} is initializing chat session..."
|
"Client {actor_id} is initializing chat session..."
|
||||||
);
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -106,7 +238,7 @@ async fn main() {
|
||||||
source_actor: 0,
|
source_actor: 0,
|
||||||
target_actor: 0,
|
target_actor: 0,
|
||||||
segment_type: SegmentType::ZoneInitialize {
|
segment_type: SegmentType::ZoneInitialize {
|
||||||
player_id: *player_id,
|
player_id: connection.player_data.actor_id,
|
||||||
timestamp: timestamp_secs(),
|
timestamp: timestamp_secs(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -125,8 +257,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: *player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: *player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -151,7 +283,7 @@ async fn main() {
|
||||||
timestamp: timestamp_secs(),
|
timestamp: timestamp_secs(),
|
||||||
data: ServerZoneIpcData::InitResponse {
|
data: ServerZoneIpcData::InitResponse {
|
||||||
unk1: 0,
|
unk1: 0,
|
||||||
character_id: connection.player_id,
|
character_id: connection.player_data.actor_id,
|
||||||
unk2: 0,
|
unk2: 0,
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -159,8 +291,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -188,8 +320,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -211,8 +343,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -220,22 +352,37 @@ async fn main() {
|
||||||
|
|
||||||
// Player Setup
|
// Player Setup
|
||||||
{
|
{
|
||||||
|
let chara_details = find_chara_make(
|
||||||
|
&db_connection,
|
||||||
|
connection.player_data.content_id,
|
||||||
|
);
|
||||||
|
|
||||||
let ipc = ServerZoneIpcSegment {
|
let ipc = ServerZoneIpcSegment {
|
||||||
op_code: ServerZoneIpcType::PlayerSetup,
|
op_code: ServerZoneIpcType::PlayerSetup,
|
||||||
timestamp: timestamp_secs(),
|
timestamp: timestamp_secs(),
|
||||||
data: ServerZoneIpcData::PlayerSetup(PlayerSetup {
|
data: ServerZoneIpcData::PlayerSetup(PlayerSetup {
|
||||||
content_id: CONTENT_ID,
|
content_id: connection.player_data.content_id,
|
||||||
exp: [10000; 32],
|
exp: [10000; 32],
|
||||||
levels: [100; 32],
|
levels: [100; 32],
|
||||||
name: CHAR_NAME.to_string(),
|
name: chara_details.name,
|
||||||
char_id: connection.player_id,
|
char_id: connection.player_data.actor_id,
|
||||||
race: CUSTOMIZE_DATA.race,
|
race: chara_details.chara_make.customize.race,
|
||||||
gender: CUSTOMIZE_DATA.gender,
|
gender: chara_details
|
||||||
tribe: CUSTOMIZE_DATA.subrace,
|
.chara_make
|
||||||
|
.customize
|
||||||
|
.gender,
|
||||||
|
tribe: chara_details
|
||||||
|
.chara_make
|
||||||
|
.customize
|
||||||
|
.subrace,
|
||||||
city_state: CITY_STATE,
|
city_state: CITY_STATE,
|
||||||
nameday_month: NAMEDAY_MONTH,
|
nameday_month: chara_details
|
||||||
nameday_day: NAMEDAY_DAY,
|
.chara_make
|
||||||
deity: DEITY,
|
.birth_month
|
||||||
|
as u8,
|
||||||
|
nameday_day: chara_details.chara_make.birth_day
|
||||||
|
as u8,
|
||||||
|
deity: chara_details.chara_make.guardian as u8,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -243,8 +390,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -266,8 +413,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -278,6 +425,11 @@ async fn main() {
|
||||||
"Client has finished loading... spawning in!"
|
"Client has finished loading... spawning in!"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let chara_details = find_chara_make(
|
||||||
|
&db_connection,
|
||||||
|
connection.player_data.content_id,
|
||||||
|
);
|
||||||
|
|
||||||
// send player spawn
|
// send player spawn
|
||||||
{
|
{
|
||||||
let ipc = ServerZoneIpcSegment {
|
let ipc = ServerZoneIpcSegment {
|
||||||
|
@ -290,14 +442,14 @@ async fn main() {
|
||||||
home_world_id: WORLD_ID,
|
home_world_id: WORLD_ID,
|
||||||
title: 1,
|
title: 1,
|
||||||
class_job: 35,
|
class_job: 35,
|
||||||
name: CHAR_NAME.to_string(),
|
name: chara_details.name,
|
||||||
hp_curr: 100,
|
hp_curr: 100,
|
||||||
hp_max: 100,
|
hp_max: 100,
|
||||||
mp_curr: 100,
|
mp_curr: 100,
|
||||||
mp_max: 100,
|
mp_max: 100,
|
||||||
object_kind: ObjectKind::Player,
|
object_kind: ObjectKind::Player,
|
||||||
gm_rank: 3,
|
gm_rank: 3,
|
||||||
look: CUSTOMIZE_DATA,
|
look: chara_details.chara_make.customize,
|
||||||
fc_tag: "LOCAL".to_string(),
|
fc_tag: "LOCAL".to_string(),
|
||||||
subtype: 4,
|
subtype: 4,
|
||||||
models: [
|
models: [
|
||||||
|
@ -323,8 +475,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -343,8 +495,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -401,8 +553,12 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection
|
||||||
target_actor: connection.player_id,
|
.player_data
|
||||||
|
.actor_id,
|
||||||
|
target_actor: connection
|
||||||
|
.player_data
|
||||||
|
.actor_id,
|
||||||
segment_type: SegmentType::Ipc {
|
segment_type: SegmentType::Ipc {
|
||||||
data: ipc,
|
data: ipc,
|
||||||
},
|
},
|
||||||
|
@ -425,8 +581,12 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection
|
||||||
target_actor: connection.player_id,
|
.player_data
|
||||||
|
.actor_id,
|
||||||
|
target_actor: connection
|
||||||
|
.player_data
|
||||||
|
.actor_id,
|
||||||
segment_type: SegmentType::Ipc {
|
segment_type: SegmentType::Ipc {
|
||||||
data: ipc,
|
data: ipc,
|
||||||
},
|
},
|
||||||
|
@ -454,8 +614,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -480,8 +640,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -560,8 +720,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -580,8 +740,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -614,9 +774,8 @@ async fn main() {
|
||||||
.read_excel_sheet("Action", &exh, Language::English, 0)
|
.read_excel_sheet("Action", &exh, Language::English, 0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let action_row = &exd
|
let action_row =
|
||||||
.read_row(&exh, request.action_id as u32)
|
&exd.read_row(&exh, request.action_id).unwrap()[0];
|
||||||
.unwrap()[0];
|
|
||||||
|
|
||||||
println!("Found action: {:#?}", action_row);
|
println!("Found action: {:#?}", action_row);
|
||||||
|
|
||||||
|
@ -631,7 +790,9 @@ async fn main() {
|
||||||
effect_id: 50,
|
effect_id: 50,
|
||||||
param: 0,
|
param: 0,
|
||||||
duration: 50.0,
|
duration: 50.0,
|
||||||
source_actor_id: connection.player_id,
|
source_actor_id: connection
|
||||||
|
.player_data
|
||||||
|
.actor_id,
|
||||||
};
|
};
|
||||||
30],
|
30],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -642,8 +803,8 @@ async fn main() {
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: connection.player_id,
|
source_actor: connection.player_data.actor_id,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -664,6 +825,108 @@ async fn main() {
|
||||||
SegmentType::KeepAliveResponse { .. } => {
|
SegmentType::KeepAliveResponse { .. } => {
|
||||||
tracing::info!("Got keep alive response from client... cool...");
|
tracing::info!("Got keep alive response from client... cool...");
|
||||||
}
|
}
|
||||||
|
SegmentType::CustomIpc { data } => {
|
||||||
|
match &data.data {
|
||||||
|
CustomIpcData::RequestCreateCharacter {
|
||||||
|
name,
|
||||||
|
chara_make_json,
|
||||||
|
} => {
|
||||||
|
tracing::info!(
|
||||||
|
"creating character from: {name} {chara_make_json}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (content_id, actor_id) = create_player_data(
|
||||||
|
&db_connection,
|
||||||
|
name,
|
||||||
|
chara_make_json,
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Created new player: {content_id} {actor_id}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// send them the new actor and content id
|
||||||
|
{
|
||||||
|
connection
|
||||||
|
.send_segment(PacketSegment {
|
||||||
|
source_actor: 0,
|
||||||
|
target_actor: 0,
|
||||||
|
segment_type: SegmentType::CustomIpc {
|
||||||
|
data: CustomIpcSegment {
|
||||||
|
unk1: 0,
|
||||||
|
unk2: 0,
|
||||||
|
op_code:
|
||||||
|
CustomIpcType::CharacterCreated,
|
||||||
|
server_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
data: CustomIpcData::CharacterCreated {
|
||||||
|
actor_id,
|
||||||
|
content_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CustomIpcData::GetActorId { content_id } => {
|
||||||
|
let actor_id = find_actor_id(&db_connection, *content_id);
|
||||||
|
|
||||||
|
tracing::info!("We found an actor id: {actor_id}");
|
||||||
|
|
||||||
|
// send them the actor id
|
||||||
|
{
|
||||||
|
connection
|
||||||
|
.send_segment(PacketSegment {
|
||||||
|
source_actor: 0,
|
||||||
|
target_actor: 0,
|
||||||
|
segment_type: SegmentType::CustomIpc {
|
||||||
|
data: CustomIpcSegment {
|
||||||
|
unk1: 0,
|
||||||
|
unk2: 0,
|
||||||
|
op_code: CustomIpcType::ActorIdFound,
|
||||||
|
server_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
data: CustomIpcData::ActorIdFound {
|
||||||
|
actor_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CustomIpcData::CheckNameIsAvailable { name } => {
|
||||||
|
let is_name_free = check_is_name_free(&db_connection, name);
|
||||||
|
let is_name_free = if is_name_free { 1 } else { 0 };
|
||||||
|
|
||||||
|
// send response
|
||||||
|
{
|
||||||
|
connection
|
||||||
|
.send_segment(PacketSegment {
|
||||||
|
source_actor: 0,
|
||||||
|
target_actor: 0,
|
||||||
|
segment_type: SegmentType::CustomIpc {
|
||||||
|
data: CustomIpcSegment {
|
||||||
|
unk1: 0,
|
||||||
|
unk2: 0,
|
||||||
|
op_code: CustomIpcType::NameIsAvailableResponse,
|
||||||
|
server_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
data: CustomIpcData::NameIsAvailableResponse {
|
||||||
|
free: is_name_free,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!(
|
||||||
|
"The server is recieving a response or unknown custom IPC!"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("The server is recieving a response or unknown packet!")
|
panic!("The server is recieving a response or unknown packet!")
|
||||||
}
|
}
|
||||||
|
|
88
src/common/custom_ipc.rs
Normal file
88
src/common/custom_ipc.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
use binrw::binrw;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CHAR_NAME_MAX_LENGTH,
|
||||||
|
common::read_string,
|
||||||
|
packet::{IpcSegment, ReadWriteIpcSegment},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::write_string;
|
||||||
|
|
||||||
|
pub type CustomIpcSegment = IpcSegment<CustomIpcType, CustomIpcData>;
|
||||||
|
|
||||||
|
impl ReadWriteIpcSegment for CustomIpcSegment {
|
||||||
|
fn calc_size(&self) -> u32 {
|
||||||
|
// 16 is the size of the IPC header
|
||||||
|
16 + match self.op_code {
|
||||||
|
CustomIpcType::RequestCreateCharacter => 1024 + CHAR_NAME_MAX_LENGTH as u32,
|
||||||
|
CustomIpcType::CharacterCreated => 12,
|
||||||
|
CustomIpcType::GetActorId => 8,
|
||||||
|
CustomIpcType::ActorIdFound => 4,
|
||||||
|
CustomIpcType::CheckNameIsAvailable => CHAR_NAME_MAX_LENGTH as u32,
|
||||||
|
CustomIpcType::NameIsAvailableResponse => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(repr = u16)]
|
||||||
|
#[derive(Default, Clone, PartialEq, Debug)]
|
||||||
|
pub enum CustomIpcType {
|
||||||
|
#[default]
|
||||||
|
/// Request the world server to create a character
|
||||||
|
RequestCreateCharacter = 0x1,
|
||||||
|
/// Response from the world server when the character is created
|
||||||
|
CharacterCreated = 0x2,
|
||||||
|
/// Request the actor id from the content id of a character
|
||||||
|
GetActorId = 0x3,
|
||||||
|
/// Response from the world server when the actor id is found
|
||||||
|
ActorIdFound = 0x4,
|
||||||
|
/// Check if a name is available on the world server
|
||||||
|
CheckNameIsAvailable = 0x5,
|
||||||
|
/// Response to CheckNameIsAvailable
|
||||||
|
NameIsAvailableResponse = 0x6,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[br(import(magic: &CustomIpcType))]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CustomIpcData {
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::RequestCreateCharacter))]
|
||||||
|
RequestCreateCharacter {
|
||||||
|
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
|
||||||
|
#[br(count = CHAR_NAME_MAX_LENGTH)]
|
||||||
|
#[br(map = read_string)]
|
||||||
|
#[bw(map = write_string)]
|
||||||
|
name: String,
|
||||||
|
#[bw(pad_size_to = 1024)]
|
||||||
|
#[br(count = 1024)]
|
||||||
|
#[br(map = read_string)]
|
||||||
|
#[bw(map = write_string)]
|
||||||
|
chara_make_json: String,
|
||||||
|
},
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::CharacterCreated))]
|
||||||
|
CharacterCreated { actor_id: u32, content_id: u64 },
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::GetActorId))]
|
||||||
|
GetActorId { content_id: u64 },
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::ActorIdFound))]
|
||||||
|
ActorIdFound { actor_id: u32 },
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::CheckNameIsAvailable))]
|
||||||
|
CheckNameIsAvailable {
|
||||||
|
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
|
||||||
|
#[br(count = CHAR_NAME_MAX_LENGTH)]
|
||||||
|
#[br(map = read_string)]
|
||||||
|
#[bw(map = write_string)]
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::NameIsAvailableResponse))]
|
||||||
|
NameIsAvailableResponse { free: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CustomIpcData {
|
||||||
|
fn default() -> CustomIpcData {
|
||||||
|
CustomIpcData::RequestCreateCharacter {
|
||||||
|
chara_make_json: String::new(),
|
||||||
|
name: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ use std::{
|
||||||
mod customize_data;
|
mod customize_data;
|
||||||
pub use customize_data::CustomizeData;
|
pub use customize_data::CustomizeData;
|
||||||
|
|
||||||
|
pub mod custom_ipc;
|
||||||
|
|
||||||
pub(crate) fn read_string(byte_stream: Vec<u8>) -> String {
|
pub(crate) fn read_string(byte_stream: Vec<u8>) -> String {
|
||||||
let str = String::from_utf8(byte_stream).unwrap();
|
let str = String::from_utf8(byte_stream).unwrap();
|
||||||
str.trim_matches(char::from(0)).to_string() // trim \0 from the end of strings
|
str.trim_matches(char::from(0)).to_string() // trim \0 from the end of strings
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use serde_json::Value;
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::common::CustomizeData;
|
use crate::common::CustomizeData;
|
||||||
|
|
||||||
|
@ -28,6 +28,26 @@ impl CharaMake {
|
||||||
unk6: content[6].as_str().unwrap().parse::<i32>().unwrap(),
|
unk6: content[6].as_str().unwrap().parse::<i32>().unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> String {
|
||||||
|
let content = json!([
|
||||||
|
self.customize.to_json(),
|
||||||
|
self.unk1,
|
||||||
|
self.guardian,
|
||||||
|
self.birth_month,
|
||||||
|
self.classjob,
|
||||||
|
self.birth_day,
|
||||||
|
self.unk6,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let obj = json!({
|
||||||
|
"content": content,
|
||||||
|
"classname": "CharaMake",
|
||||||
|
"classid": 118,
|
||||||
|
});
|
||||||
|
|
||||||
|
serde_json::to_string(&obj).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -29,6 +29,8 @@ pub struct LobbyConnection {
|
||||||
pub session_id: Option<String>,
|
pub session_id: Option<String>,
|
||||||
|
|
||||||
pub state: PacketState,
|
pub state: PacketState,
|
||||||
|
|
||||||
|
pub stored_character_creation_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LobbyConnection {
|
impl LobbyConnection {
|
||||||
|
@ -209,7 +211,7 @@ impl LobbyConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut characters = vec![CharacterDetails {
|
let mut characters = vec![CharacterDetails {
|
||||||
id: 0,
|
actor_id: 0,
|
||||||
content_id: CONTENT_ID,
|
content_id: CONTENT_ID,
|
||||||
index: 0,
|
index: 0,
|
||||||
unk1: [0; 16],
|
unk1: [0; 16],
|
||||||
|
@ -295,16 +297,16 @@ impl LobbyConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send the host information for the world server to the client.
|
/// Send the host information for the world server to the client.
|
||||||
pub async fn send_enter_world(&mut self, sequence: u64, lookup_id: u64) {
|
pub async fn send_enter_world(&mut self, sequence: u64, content_id: u64, actor_id: u32) {
|
||||||
let Some(session_id) = &self.session_id else {
|
let Some(session_id) = &self.session_id else {
|
||||||
panic!("Missing session id!");
|
panic!("Missing session id!");
|
||||||
};
|
};
|
||||||
|
|
||||||
let enter_world = ServerLobbyIpcData::LobbyEnterWorld {
|
let enter_world = ServerLobbyIpcData::LobbyEnterWorld {
|
||||||
sequence,
|
sequence,
|
||||||
character_id: 0,
|
actor_id,
|
||||||
content_id: lookup_id, // TODO: shouldn't these be named the same then?
|
content_id,
|
||||||
session_id: session_id.clone(),
|
token: String::new(),
|
||||||
port: 7100,
|
port: 7100,
|
||||||
host: "127.0.0.1".to_string(),
|
host: "127.0.0.1".to_string(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ use super::{read_string, write_string};
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct CharacterDetails {
|
pub struct CharacterDetails {
|
||||||
#[brw(pad_after = 4)]
|
#[brw(pad_after = 4)]
|
||||||
pub id: u32,
|
pub actor_id: u32,
|
||||||
pub content_id: u64,
|
pub content_id: u64,
|
||||||
#[brw(pad_after = 4)]
|
#[brw(pad_after = 4)]
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
|
|
|
@ -144,9 +144,8 @@ pub enum ClientLobbyIpcData {
|
||||||
LobbyCharacterAction(LobbyCharacterAction),
|
LobbyCharacterAction(LobbyCharacterAction),
|
||||||
#[br(pre_assert(*magic == ClientLobbyIpcType::RequestEnterWorld))]
|
#[br(pre_assert(*magic == ClientLobbyIpcType::RequestEnterWorld))]
|
||||||
RequestEnterWorld {
|
RequestEnterWorld {
|
||||||
#[brw(pad_before = 16)]
|
|
||||||
sequence: u64,
|
sequence: u64,
|
||||||
lookup_id: u64,
|
content_id: u64,
|
||||||
// TODO: what else is in here?
|
// TODO: what else is in here?
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -166,7 +165,7 @@ pub enum ServerLobbyIpcData {
|
||||||
LobbyCharacterList(LobbyCharacterList),
|
LobbyCharacterList(LobbyCharacterList),
|
||||||
LobbyEnterWorld {
|
LobbyEnterWorld {
|
||||||
sequence: u64,
|
sequence: u64,
|
||||||
character_id: u32,
|
actor_id: u32,
|
||||||
#[brw(pad_before = 4)]
|
#[brw(pad_before = 4)]
|
||||||
content_id: u64,
|
content_id: u64,
|
||||||
#[brw(pad_before = 4)]
|
#[brw(pad_before = 4)]
|
||||||
|
@ -174,7 +173,7 @@ pub enum ServerLobbyIpcData {
|
||||||
#[br(count = 66)]
|
#[br(count = 66)]
|
||||||
#[br(map = read_string)]
|
#[br(map = read_string)]
|
||||||
#[bw(map = write_string)]
|
#[bw(map = write_string)]
|
||||||
session_id: String,
|
token: String, // WHAT IS THIS FOR??
|
||||||
port: u16,
|
port: u16,
|
||||||
#[brw(pad_after = 16)] // garbage?
|
#[brw(pad_after = 16)] // garbage?
|
||||||
#[br(count = 48)]
|
#[br(count = 48)]
|
||||||
|
@ -232,9 +231,9 @@ mod tests {
|
||||||
ServerLobbyIpcType::LobbyEnterWorld,
|
ServerLobbyIpcType::LobbyEnterWorld,
|
||||||
ServerLobbyIpcData::LobbyEnterWorld {
|
ServerLobbyIpcData::LobbyEnterWorld {
|
||||||
sequence: 0,
|
sequence: 0,
|
||||||
character_id: 0,
|
actor_id: 0,
|
||||||
content_id: 0,
|
content_id: 0,
|
||||||
session_id: String::new(),
|
token: String::new(),
|
||||||
port: 0,
|
port: 0,
|
||||||
host: String::new(),
|
host: String::new(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@ use binrw::{BinRead, BinWrite, binrw};
|
||||||
use tokio::{io::AsyncWriteExt, net::TcpStream};
|
use tokio::{io::AsyncWriteExt, net::TcpStream};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{read_string, timestamp_msecs},
|
common::{custom_ipc::CustomIpcSegment, read_string, timestamp_msecs, write_string},
|
||||||
oodle::OodleNetwork,
|
oodle::OodleNetwork,
|
||||||
packet::{compression::compress, encryption::decrypt},
|
packet::{compression::compress, encryption::decrypt},
|
||||||
};
|
};
|
||||||
|
@ -30,9 +30,12 @@ pub enum SegmentType<T: ReadWriteIpcSegment> {
|
||||||
// Client->Server Packets
|
// Client->Server Packets
|
||||||
#[brw(magic = 0x1u32)]
|
#[brw(magic = 0x1u32)]
|
||||||
InitializeSession {
|
InitializeSession {
|
||||||
#[brw(pad_before = 4)]
|
#[brw(pad_before = 4)] // empty
|
||||||
#[brw(pad_after = 48)] // TODO: probably not empty?
|
#[brw(pad_size_to = 36)]
|
||||||
player_id: u32,
|
#[br(count = 36)]
|
||||||
|
#[br(map = read_string)]
|
||||||
|
#[bw(map = write_string)]
|
||||||
|
actor_id: String, // square enix in their infinite wisdom has this as a STRING REPRESENTATION of an integer. what
|
||||||
},
|
},
|
||||||
#[brw(magic = 0x9u32)]
|
#[brw(magic = 0x9u32)]
|
||||||
InitializeEncryption {
|
InitializeEncryption {
|
||||||
|
@ -71,6 +74,10 @@ pub enum SegmentType<T: ReadWriteIpcSegment> {
|
||||||
#[brw(pad_after = 32)]
|
#[brw(pad_after = 32)]
|
||||||
timestamp: u32,
|
timestamp: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Custom Packets
|
||||||
|
#[brw(magic = 0xAAAAu32)]
|
||||||
|
CustomIpc { data: CustomIpcSegment },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[binrw]
|
#[binrw]
|
||||||
|
@ -112,6 +119,7 @@ impl<T: ReadWriteIpcSegment> PacketSegment<T> {
|
||||||
SegmentType::KeepAliveResponse { .. } => 0x8,
|
SegmentType::KeepAliveResponse { .. } => 0x8,
|
||||||
SegmentType::ZoneInitialize { .. } => 40,
|
SegmentType::ZoneInitialize { .. } => 40,
|
||||||
SegmentType::InitializeSession { .. } => todo!(),
|
SegmentType::InitializeSession { .. } => todo!(),
|
||||||
|
SegmentType::CustomIpc { data } => data.calc_size(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ impl ChatHandler {
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: 0x106ad804,
|
source_actor: 0x106ad804,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -115,7 +115,7 @@ impl ChatHandler {
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: 0x106ad804,
|
source_actor: 0x106ad804,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -138,7 +138,7 @@ impl ChatHandler {
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: 0x106ad804,
|
source_actor: 0x106ad804,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -163,7 +163,7 @@ impl ChatHandler {
|
||||||
spawn_index: connection.get_free_spawn_index(),
|
spawn_index: connection.get_free_spawn_index(),
|
||||||
bnpc_base: 13498,
|
bnpc_base: 13498,
|
||||||
bnpc_name: 10261,
|
bnpc_name: 10261,
|
||||||
spawner_id: connection.player_id,
|
spawner_id: connection.player_data.actor_id,
|
||||||
parent_actor_id: INVALID_OBJECT_ID, // TODO: make default?
|
parent_actor_id: INVALID_OBJECT_ID, // TODO: make default?
|
||||||
object_kind: ObjectKind::BattleNpc,
|
object_kind: ObjectKind::BattleNpc,
|
||||||
level: 1,
|
level: 1,
|
||||||
|
@ -189,7 +189,7 @@ impl ChatHandler {
|
||||||
connection
|
connection
|
||||||
.send_segment(PacketSegment {
|
.send_segment(PacketSegment {
|
||||||
source_actor: 0x106ad804,
|
source_actor: 0x106ad804,
|
||||||
target_actor: connection.player_id,
|
target_actor: connection.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -16,12 +16,19 @@ use super::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct PlayerData {
|
||||||
|
pub actor_id: u32,
|
||||||
|
pub content_id: u64,
|
||||||
|
pub account_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a single connection between an instance of the client and the world server
|
/// Represents a single connection between an instance of the client and the world server
|
||||||
pub struct ZoneConnection {
|
pub struct ZoneConnection {
|
||||||
pub socket: TcpStream,
|
pub socket: TcpStream,
|
||||||
|
|
||||||
pub state: PacketState,
|
pub state: PacketState,
|
||||||
pub player_id: u32,
|
pub player_data: PlayerData,
|
||||||
|
|
||||||
pub zone: Zone,
|
pub zone: Zone,
|
||||||
pub spawn_index: u8,
|
pub spawn_index: u8,
|
||||||
|
@ -61,8 +68,8 @@ impl ZoneConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.send_segment(PacketSegment {
|
self.send_segment(PacketSegment {
|
||||||
source_actor: self.player_id,
|
source_actor: self.player_data.actor_id,
|
||||||
target_actor: self.player_id,
|
target_actor: self.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -88,8 +95,8 @@ impl ZoneConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.send_segment(PacketSegment {
|
self.send_segment(PacketSegment {
|
||||||
source_actor: self.player_id,
|
source_actor: self.player_data.actor_id,
|
||||||
target_actor: self.player_id,
|
target_actor: self.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -105,8 +112,8 @@ impl ZoneConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.send_segment(PacketSegment {
|
self.send_segment(PacketSegment {
|
||||||
source_actor: self.player_id,
|
source_actor: self.player_data.actor_id,
|
||||||
target_actor: self.player_id,
|
target_actor: self.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -129,8 +136,8 @@ impl ZoneConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.send_segment(PacketSegment {
|
self.send_segment(PacketSegment {
|
||||||
source_actor: self.player_id,
|
source_actor: self.player_data.actor_id,
|
||||||
target_actor: self.player_id,
|
target_actor: self.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -149,8 +156,8 @@ impl ZoneConnection {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.send_segment(PacketSegment {
|
self.send_segment(PacketSegment {
|
||||||
source_actor: self.player_id,
|
source_actor: self.player_data.actor_id,
|
||||||
target_actor: self.player_id,
|
target_actor: self.player_data.actor_id,
|
||||||
segment_type: SegmentType::Ipc { data: ipc },
|
segment_type: SegmentType::Ipc { data: ipc },
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -16,6 +16,8 @@ mod tests {
|
||||||
|
|
||||||
use binrw::BinRead;
|
use binrw::BinRead;
|
||||||
|
|
||||||
|
use crate::world::ipc::{CharacterMode, ObjectKind};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -23,7 +23,7 @@ mod tests {
|
||||||
|
|
||||||
use binrw::BinRead;
|
use binrw::BinRead;
|
||||||
|
|
||||||
use crate::world::ipc::CharacterMode;
|
use crate::world::ipc::{CharacterMode, ObjectKind};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,4 @@ mod chat_handler;
|
||||||
pub use chat_handler::ChatHandler;
|
pub use chat_handler::ChatHandler;
|
||||||
|
|
||||||
mod connection;
|
mod connection;
|
||||||
pub use connection::ZoneConnection;
|
pub use connection::{PlayerData, ZoneConnection};
|
||||||
|
|
Loading…
Add table
Reference in a new issue