diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 1da21ab..c360de1 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -2,6 +2,7 @@ use std::cmp::min; use std::time::{SystemTime, UNIX_EPOCH}; use kawari::blowfish::Blowfish; +use kawari::chara_make::CharaMake; use kawari::client_select_data::{ClientCustomizeData, ClientSelectData}; use kawari::encryption::generate_encryption_key; use kawari::ipc::{ @@ -74,6 +75,7 @@ async fn main() { action, world_id, name, + json, .. } => { match action { @@ -83,7 +85,7 @@ async fn main() { ); // reject - { + /*{ let ipc = IPCSegment { unk1: 0, unk2: 0, @@ -97,6 +99,87 @@ async fn main() { }, }; + let response_packet = PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Uncompressed, + ) + .await; + }*/ + + // accept + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::CharacterCreated, + server_id: 0, + timestamp: 0, + data: IPCStructData::CharacterCreated { + unk1: 0x4, + unk2: 0x00010101, + details: CharacterDetails { + content_id: CONTENT_ID, + character_name: name.clone(), + origin_server_name: "KAWARI" + .to_string(), + current_server_name: "KAWARI" + .to_string(), + ..Default::default() + }, + }, + }; + + let response_packet = PacketSegment { + source_actor: 0x0, + target_actor: 0x0, + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet( + &mut write, + &[response_packet], + &mut state, + CompressionType::Uncompressed, + ) + .await; + } + } + LobbyCharacterAction::Create => { + tracing::info!("Player is creating a new character!"); + + let chara_make = CharaMake::from_json(json); + println!("charamake: {:#?}", chara_make); + + // a slightly different character created packet now + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::CharacterCreated, + server_id: 0, + timestamp: 0, + data: IPCStructData::CharacterCreated { + unk1: 0x5, + unk2: 0x00020101, + details: CharacterDetails { + id: 0x07369f3a, // notice that we give them an id now + content_id: CONTENT_ID, + character_name: name.clone(), + origin_server_name: "KAWARI" + .to_string(), + current_server_name: "KAWARI" + .to_string(), + ..Default::default() + }, + }, + }; + let response_packet = PacketSegment { source_actor: 0x0, target_actor: 0x0, @@ -111,7 +194,6 @@ async fn main() { .await; } } - LobbyCharacterAction::Create => todo!(), LobbyCharacterAction::Rename => todo!(), LobbyCharacterAction::Delete => todo!(), LobbyCharacterAction::Move => todo!(), diff --git a/src/chara_make.rs b/src/chara_make.rs new file mode 100644 index 0000000..eb28c36 --- /dev/null +++ b/src/chara_make.rs @@ -0,0 +1,48 @@ +use binrw::binrw; +use serde_json::{Value, json}; + +use crate::client_select_data::ClientCustomizeData; + +#[derive(Debug)] +pub struct CharaMake { + pub customize: ClientCustomizeData, + pub unk1: i32, // always 1? + pub guardian: i32, + pub birth_month: i32, + pub classjob: i32, + pub birth_day: i32, + pub unk6: i32, // always 1? +} + +impl CharaMake { + pub fn from_json(json: &str) -> Self { + let v: Value = serde_json::from_str(json).unwrap(); + let content = &v["content"]; + + Self { + customize: ClientCustomizeData::from_json(&content[0]), + unk1: content[1].as_str().unwrap().parse::().unwrap(), + guardian: content[2].as_str().unwrap().parse::().unwrap(), + birth_month: content[3].as_str().unwrap().parse::().unwrap(), + classjob: content[4].as_str().unwrap().parse::().unwrap(), + birth_day: content[5].as_str().unwrap().parse::().unwrap(), + unk6: content[6].as_str().unwrap().parse::().unwrap(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_charamake() { + let json = "{\"classid\":118,\"classname\":\"CharaMake\",\"content\":[[\"1\",\"0\",\"1\",\"50\",\"1\",\"5\",\"161\",\"0\",\"3\",\"30\",\"103\",\"0\",\"0\",\"0\",\"1\",\"30\",\"4\",\"5\",\"2\",\"128\",\"35\",\"50\",\"0\",\"0\",\"0\",\"0\"],\"1\",\"1\",\"1\",\"1\",\"1\",\"1\"]}"; + + let chara_make = CharaMake::from_json(json); + assert_eq!(chara_make.customize.gender, 0); + assert_eq!(chara_make.classjob, 1); + + // TODO: add more asserts + } +} diff --git a/src/client_select_data.rs b/src/client_select_data.rs index 6ea1e15..ed32fdc 100644 --- a/src/client_select_data.rs +++ b/src/client_select_data.rs @@ -60,6 +60,36 @@ impl ClientCustomizeData { self.face_paint_color.to_string(), ]) } + + pub fn from_json(json: &Value) -> Self { + Self { + race: json[0].as_str().unwrap().parse::().unwrap(), + gender: json[1].as_str().unwrap().parse::().unwrap(), + height: json[2].as_str().unwrap().parse::().unwrap(), + subrace: json[3].as_str().unwrap().parse::().unwrap(), + face: json[4].as_str().unwrap().parse::().unwrap(), + hair: json[5].as_str().unwrap().parse::().unwrap(), + enable_highlights: json[6].as_str().unwrap().parse::().unwrap(), + skin_tone: json[7].as_str().unwrap().parse::().unwrap(), + right_eye_color: json[8].as_str().unwrap().parse::().unwrap(), + hair_tone: json[9].as_str().unwrap().parse::().unwrap(), + highlights: json[10].as_str().unwrap().parse::().unwrap(), + facial_features: json[11].as_str().unwrap().parse::().unwrap(), + facial_feature_color: json[12].as_str().unwrap().parse::().unwrap(), + eyebrows: json[13].as_str().unwrap().parse::().unwrap(), + left_eye_color: json[14].as_str().unwrap().parse::().unwrap(), + eyes: json[15].as_str().unwrap().parse::().unwrap(), + nose: json[16].as_str().unwrap().parse::().unwrap(), + jaw: json[17].as_str().unwrap().parse::().unwrap(), + mouth: json[18].as_str().unwrap().parse::().unwrap(), + lips_tone_fur_pattern: json[19].as_str().unwrap().parse::().unwrap(), + race_feature_size: json[20].as_str().unwrap().parse::().unwrap(), + race_feature_type: json[21].as_str().unwrap().parse::().unwrap(), + bust: json[22].as_str().unwrap().parse::().unwrap(), + face_paint: json[23].as_str().unwrap().parse::().unwrap(), + face_paint_color: json[24].as_str().unwrap().parse::().unwrap(), + } + } } /// See https://github.com/aers/FFXIVClientStructs/blob/main/FFXIVClientStructs/FFXIV/Application/Network/WorkDefinitions/ClientSelectData.cs pub struct ClientSelectData { diff --git a/src/ipc.rs b/src/ipc.rs index b9837f6..36f6eb2 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -96,6 +96,8 @@ pub enum IPCOpCode { Unk10 = 0x110, // Unknown, server sends this in response to Unk7 Unk11 = 0x156, + // Assumed what this is, but probably incorrect + CharacterCreated = 0xE, } #[binrw] @@ -493,6 +495,16 @@ pub enum IPCStructData { #[brw(pad_after = 516)] // mostly empty unk3: u32, }, + #[br(pre_assert(false))] + CharacterCreated { + #[brw(pad_after = 4)] // empty + unk1: u32, + #[brw(pad_after = 4)] // empty + unk2: u32, + #[brw(pad_before = 32)] // empty + #[brw(pad_after = 1136)] // empty + details: CharacterDetails, + }, } #[binrw] @@ -558,6 +570,7 @@ impl IPCSegment { IPCStructData::Unk10 { .. } => 8, IPCStructData::Unk11 { .. } => 32, IPCStructData::NameRejection { .. } => 536, + IPCStructData::CharacterCreated { .. } => 2568, } } } diff --git a/src/lib.rs b/src/lib.rs index fcba3be..3d6a4db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use rand::Rng; use rand::distributions::Alphanumeric; pub mod blowfish; +pub mod chara_make; pub mod client_select_data; mod common; mod compression;