diff --git a/Cargo.toml b/Cargo.toml index 7f58075..9583f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,13 @@ name = "kawari-patch" [[bin]] name = "kawari-web" +[[bin]] +name = "kawari-lobby" + +[[bin]] +name = "kawari-world" +required-features = ["oodle"] + [profile.release] lto = true strip = true @@ -25,6 +32,10 @@ opt-level = "z" codegen-units = 1 panic = "abort" +[features] +default = ["oodle"] +oodle = [] + [build-dependencies] cc = "1.0" diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 8eb5ded..d49ae1f 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -4,9 +4,11 @@ use std::time::{SystemTime, UNIX_EPOCH}; use kawari::client_select_data::{ClientCustomizeData, ClientSelectData}; use kawari::encryption::{blowfish_encode, generate_encryption_key}; use kawari::ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, Server, ServiceAccount}; +use kawari::oodle::FFXIVOodle; use kawari::packet::{ PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet, }; +use kawari::{CONTENT_ID, WORLD_ID, WORLD_NAME, ZONE_ID}; use tokio::io::{AsyncReadExt, WriteHalf}; use tokio::net::{TcpListener, TcpStream}; @@ -25,6 +27,8 @@ async fn main() { let mut state = State { client_key: None, session_id: None, + oodle: FFXIVOodle::new(), + player_id: None, }; tokio::spawn(async move { @@ -33,7 +37,7 @@ async fn main() { let n = read.read(&mut buf).await.expect("Failed to read data!"); if n != 0 { - let segments = parse_packet(&buf[..n], &mut state).await; + let (segments, _) = parse_packet(&buf[..n], &mut state).await; for segment in &segments { match &segment.segment_type { SegmentType::InitializeEncryption { phrase, key } => { @@ -50,12 +54,12 @@ async fn main() { state.session_id = Some(session_id.clone()); - send_account_list(&mut write, &state).await; + send_account_list(&mut write, &mut state).await; } IPCStructData::RequestCharacterList { sequence } => { tracing::info!("Client is requesting character list..."); - send_lobby_info(&mut write, &state, *sequence).await; + send_lobby_info(&mut write, &mut state, *sequence).await; } IPCStructData::LobbyCharacterAction { sequence, @@ -79,7 +83,7 @@ async fn main() { } => { tracing::info!("Client is joining the world..."); - send_enter_world(&mut write, &state, *sequence, *lookup_id) + send_enter_world(&mut write, &mut state, *sequence, *lookup_id) .await; } _ => { @@ -87,7 +91,7 @@ async fn main() { } }, SegmentType::KeepAlive { id, timestamp } => { - send_keep_alive(&mut write, &state, *id, *timestamp).await + send_keep_alive(&mut write, &mut state, *id, *timestamp).await } _ => { panic!("The server is recieving a response packet!") @@ -125,7 +129,7 @@ async fn initialize_encryption( send_packet(socket, &[response_packet], state).await; } -async fn send_account_list(socket: &mut WriteHalf, state: &State) { +async fn send_account_list(socket: &mut WriteHalf, state: &mut State) { let timestamp: u32 = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Failed to get UNIX timestamp!") @@ -167,12 +171,7 @@ async fn send_account_list(socket: &mut WriteHalf, state: &State) { send_packet(socket, &[response_packet], state).await; } -// TODO: make this configurable -// See https://ffxiv.consolegameswiki.com/wiki/Servers for a list of possible IDs -const WORLD_ID: u16 = 63; -const WORLD_NAME: &str = "KAWARI"; - -async fn send_lobby_info(socket: &mut WriteHalf, state: &State, sequence: u64) { +async fn send_lobby_info(socket: &mut WriteHalf, state: &mut State, sequence: u64) { let timestamp: u32 = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Failed to get UNIX timestamp!") @@ -256,7 +255,7 @@ async fn send_lobby_info(socket: &mut WriteHalf, state: &State, seque guardian: 2, unk8: 0, unk9: 0, - zone_id: 1000, + zone_id: ZONE_ID as i32, unk11: 0, customize: ClientCustomizeData { race: 3, @@ -301,7 +300,7 @@ async fn send_lobby_info(socket: &mut WriteHalf, state: &State, seque let mut characters = vec![CharacterDetails { id: 0, - content_id: 11111111111111111, + content_id: CONTENT_ID, index: 0, unk1: [0; 16], origin_server_id: WORLD_ID, @@ -387,7 +386,7 @@ async fn send_lobby_info(socket: &mut WriteHalf, state: &State, seque async fn send_enter_world( socket: &mut WriteHalf, - state: &State, + state: &mut State, sequence: u64, lookup_id: u64, ) { diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index b485142..0aac8e6 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,4 +1,11 @@ -use kawari::packet::{SegmentType, State, parse_packet, send_keep_alive}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use kawari::ipc::{ActorControlType, IPCOpCode, IPCSegment, IPCStructData, Position}; +use kawari::oodle::FFXIVOodle; +use kawari::packet::{ + PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_packet, +}; +use kawari::{CONTENT_ID, WORLD_ID, ZONE_ID}; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; @@ -17,6 +24,8 @@ async fn main() { let mut state = State { client_key: None, session_id: None, + oodle: FFXIVOodle::new(), + player_id: None, }; tokio::spawn(async move { @@ -25,14 +34,435 @@ async fn main() { let n = read.read(&mut buf).await.expect("Failed to read data!"); if n != 0 { - let segments = parse_packet(&buf[..n], &mut state).await; + println!("recieved {n} bytes..."); + let (segments, connection_type) = parse_packet(&buf[..n], &mut state).await; for segment in &segments { match &segment.segment_type { + SegmentType::InitializeSession { player_id } => { + state.player_id = Some(*player_id); + + // We have send THEM a keep alive + { + let timestamp: u32 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Failed to get UNIX timestamp!") + .as_secs() + .try_into() + .unwrap(); + + let response_packet = PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::KeepAlive { + id: 0xE0037603u32, + timestamp, + }, + }; + send_packet(&mut write, &[response_packet], &mut state).await; + } + + match connection_type { + kawari::packet::ConnectionType::Zone => { + tracing::info!( + "Client {player_id} is initializing zone session..." + ); + + let response_packet = PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::ZoneInitialize { + player_id: *player_id, + }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + kawari::packet::ConnectionType::Chat => { + tracing::info!( + "Client {player_id} is initializing chat session..." + ); + + { + let response_packet = PacketSegment { + source_actor: 0, + target_actor: 0, + segment_type: SegmentType::ZoneInitialize { + player_id: *player_id, + }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::InitializeChat, + server_id: 0, + timestamp: 0, + data: IPCStructData::InitializeChat { + unk: [0; 24], + }, + }; + + let response_packet = PacketSegment { + source_actor: *player_id, + target_actor: *player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + } + _ => panic!( + "The client is trying to initialize the wrong connection?!" + ), + } + } SegmentType::Ipc { data } => { - panic!("The server is recieving a IPC response or unknown packet!") + match &data.data { + IPCStructData::InitRequest { .. } => { + tracing::info!( + "Client is now requesting zone information. Sending!" + ); + + let timestamp_secs = || { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Failed to get UNIX timestamp!") + .as_secs() + .try_into() + .unwrap() + }; + + // IPC Init(?) + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::InitResponse, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::InitResponse { + unk1: 0, + character_id: state.player_id.unwrap(), + unk2: 0, + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + + // Control Data + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::ActorControlSelf, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::ActorControlSelf { + category: ActorControlType::SetCharaGearParamUI, + param1: 1, + param2: 1, + param3: 0, + param4: 0, + param5: 0, + param6: 0, + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + + // Stats + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::PlayerStats, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::PlayerStats { + strength: 1, + dexterity: 0, + vitality: 0, + intelligence: 0, + mind: 0, + piety: 0, + hp: 100, + mp: 100, + tp: 0, + gp: 0, + cp: 0, + delay: 0, + tenacity: 0, + attack_power: 0, + defense: 0, + direct_hit_rate: 0, + evasion: 0, + magic_defense: 0, + critical_hit: 0, + attack_magic_potency: 0, + healing_magic_potency: 0, + elemental_bonus: 0, + determination: 0, + skill_speed: 0, + spell_speed: 0, + haste: 0, + craftmanship: 0, + control: 0, + gathering: 0, + perception: 0, + unk1: [0; 26], + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + + // Player Setup + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::PlayerSetup, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::PlayerSetup { + content_id: CONTENT_ID, + crest: 0, + unknown10: 0, + char_id: 0, + rested_exp: 0, + companion_current_exp: 0, + unknown1c: 0, + fish_caught: 0, + use_bait_catalog_id: 0, + unknown28: 0, + unknown_pvp2c: 0, + unknown2e: 0, + pvp_frontline_overall_campaigns: 0, + unknown_timestamp34: 0, + unknown_timestamp38: 0, + unknown3c: 0, + unknown40: 0, + unknown44: 0, + companion_time_passed: 0.0, + unknown4c: 0, + unknown50: 0, + unknown_pvp52: [0; 4], + pvp_series_exp: 0, + player_commendations: 0, + unknown64: [0; 8], + pvp_rival_wings_total_matches: 0, + pvp_rival_wings_total_victories: 0, + pvp_rival_wings_weekly_matches: 0, + pvp_rival_wings_weekly_victories: 0, + max_level: 0, + expansion: 0, + unknown76: 0, + unknown77: 0, + unknown78: 0, + race: 0, + tribe: 0, + gender: 0, + current_job: 0, + current_class: 0, + deity: 0, + nameday_month: 0, + nameday_day: 0, + city_state: 0, + homepoint: 0, + unknown8d: [0; 3], + companion_rank: 0, + companion_stars: 0, + companion_sp: 0, + companion_unk93: 0, + companion_color: 0, + companion_fav_feed: 0, + fav_aetheryte_count: 0, + unknown97: [0; 5], + sightseeing21_to_80_unlock: 0, + sightseeing_heavensward_unlock: 0, + unknown9e: [0; 26], + exp: [10000; 32], + pvp_total_exp: 0, + unknown_pvp124: 0, + pvp_exp: 0, + pvp_frontline_overall_ranks: [0; 3], + unknown138: 0, + levels: [100; 32], + unknown194: [0; 218], + companion_name: [0; 21], + companion_def_rank: 0, + companion_att_rank: 0, + companion_heal_rank: 0, + mount_guide_mask: [0; 33], + ornament_mask: [0; 4], + unknown281: [0; 23], + name: "KAWARI".to_string(), + unknown293: [0; 16], + unknown2a3: 0, + unlock_bitmask: [0; 64], + aetheryte: [0; 26], + favorite_aetheryte_ids: [0; 4], + free_aetheryte_id: 0, + ps_plus_free_aetheryte_id: 0, + discovery: [0; 480], + howto: [0; 36], + unknown554: [0; 4], + minions: [0; 60], + chocobo_taxi_mask: [0; 12], + watched_cutscenes: [0; 159], + companion_barding_mask: [0; 12], + companion_equipped_head: 0, + companion_equipped_body: 0, + companion_equipped_legs: 0, + unknown_mask: [0; 287], + pose: [0; 7], + unknown6df: [0; 3], + challenge_log_complete: [0; 13], + secret_recipe_book_mask: [0; 12], + unknown_mask6f7: [0; 29], + relic_completion: [0; 12], + sightseeing_mask: [0; 37], + hunting_mark_mask: [0; 102], + triple_triad_cards: [0; 45], + unknown895: 0, + unknown7d7: [0; 15], + unknown7d8: 0, + unknown7e6: [0; 49], + regional_folklore_mask: [0; 6], + orchestrion_mask: [0; 87], + hall_of_novice_completion: [0; 3], + anima_completion: [0; 11], + unknown85e: [0; 41], + unlocked_raids: [0; 28], + unlocked_dungeons: [0; 18], + unlocked_guildhests: [0; 10], + unlocked_trials: [0; 12], + unlocked_pvp: [0; 5], + cleared_raids: [0; 28], + cleared_dungeons: [0; 18], + cleared_guildhests: [0; 10], + cleared_trials: [0; 12], + cleared_pvp: [0; 5], + unknown948: [0; 15], + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + + // Player Class Info + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::UpdateClassInfo, + server_id: 69, // lol + timestamp: timestamp_secs(), + data: IPCStructData::UpdateClassInfo { + class_id: 35, + unknown: 1, + is_specialist: 0, + synced_level: 90, + class_level: 90, + role_actions: [0; 10], + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + + // Init Zone + { + let ipc = IPCSegment { + unk1: 0, + unk2: 0, + op_code: IPCOpCode::InitZone, + server_id: 0, + timestamp: timestamp_secs(), + data: IPCStructData::InitZone { + server_id: WORLD_ID, + zone_id: ZONE_ID, + zone_index: 0, + content_finder_condition_id: 0, + layer_set_id: 0, + layout_id: 0, + weather_id: 1, + unk_bitmask1: 0x10, + unk_bitmask2: 0, + unk1: 0, + unk2: 0, + festival_id: 0, + additional_festival_id: 0, + unk3: 0, + unk4: 0, + unk5: 0, + unk6: [0; 4], + unk7: [0; 3], + position: Position { + x: 0.0, + y: 0.0, + z: 0.0, + }, + unk8: [0; 4], + unk9: 0, + }, + }; + + let response_packet = PacketSegment { + source_actor: state.player_id.unwrap(), + target_actor: state.player_id.unwrap(), + segment_type: SegmentType::Ipc { data: ipc }, + }; + send_packet(&mut write, &[response_packet], &mut state) + .await; + } + } + _ => panic!( + "The server is recieving a IPC response or unknown packet!" + ), + } } SegmentType::KeepAlive { id, timestamp } => { - send_keep_alive(&mut write, &state, *id, *timestamp).await + send_keep_alive(&mut write, &mut state, *id, *timestamp).await + } + SegmentType::KeepAliveResponse { .. } => { + tracing::info!("Got keep alive response from client... cool..."); } _ => { panic!("The server is recieving a response or unknown packet!") diff --git a/src/compression.rs b/src/compression.rs index c6467b5..4254db4 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -1,14 +1,16 @@ +use std::fs::write; use std::io::Cursor; use binrw::{BinRead, BinResult}; use crate::{ - oodle::FFXIVOodle, + oodle::{FFXIVOodle, Oodle}, packet::{PacketHeader, PacketSegment}, }; #[binrw::parser(reader, endian)] pub(crate) fn decompress( + oodle: &mut FFXIVOodle, header: &PacketHeader, encryption_key: Option<&[u8]>, ) -> BinResult> { @@ -16,16 +18,23 @@ pub(crate) fn decompress( let size = header.size as usize - std::mem::size_of::(); + println!( + "known packet size: {} but decompressing {} bytes", + header.size, size + ); + let mut data = vec![0; size]; - reader.read_exact(&mut data)?; + reader.read_exact(&mut data).unwrap(); + + write("compressed.bin", &data).unwrap(); let data = match header.compressed { crate::packet::CompressionType::Uncompressed => data, - crate::packet::CompressionType::Oodle => { - FFXIVOodle::new().decode(data, header.oodle_decompressed_size) - } + crate::packet::CompressionType::Oodle => oodle.decode(data, header.oodle_decompressed_size), }; + write("decompressed.bin", &data).unwrap(); + let mut cursor = Cursor::new(&data); for _ in 0..header.segment_count { diff --git a/src/encryption.rs b/src/encryption.rs index 1ba051c..eb8acb9 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -36,23 +36,26 @@ pub(crate) fn decrypt(size: u32, encryption_key: Option<&[u8]>) -> BinResult< where for<'a> T: BinRead = ()> + 'a, { - let Some(encryption_key) = encryption_key else { - panic!("This segment type is encrypted and no key was provided!"); - }; + if let Some(encryption_key) = encryption_key { + let size = size - (std::mem::size_of::() * 4) as u32; // 16 = header size - let size = size - (std::mem::size_of::() * 4) as u32; // 16 = header size + let mut data = vec![0; size as usize]; + reader.read_exact(&mut data)?; - let mut data = vec![0; size as usize]; - reader.read_exact(&mut data)?; + unsafe { + let decryption_result = + blowfish_decode(encryption_key.as_ptr(), 16, data.as_ptr(), size); + let decrypted_data = slice::from_raw_parts(decryption_result, size as usize); - unsafe { - let decryption_result = blowfish_decode(encryption_key.as_ptr(), 16, data.as_ptr(), size); - let decrypted_data = slice::from_raw_parts(decryption_result, size as usize); + write("decrypted.bin", decrypted_data).unwrap(); - write("decrypted.bin", decrypted_data).unwrap(); + let mut cursor = Cursor::new(&decrypted_data); + T::read_options(&mut cursor, endian, ()) + } + } else { + tracing::info!("NOTE: Not decrypting this IPC packet since no key was provided!"); - let mut cursor = Cursor::new(&decrypted_data); - T::read_options(&mut cursor, endian, ()) + T::read_options(reader, endian, ()) } } @@ -61,29 +64,31 @@ pub(crate) fn encrypt(value: &T, size: u32, encryption_key: Option<&[u8]>) -> where for<'a> T: BinWrite = ()> + 'a, { - let Some(encryption_key) = encryption_key else { - panic!("This segment type needs to be encrypted and no key was provided!"); - }; + if let Some(encryption_key) = encryption_key { + let size = size - (std::mem::size_of::() * 4) as u32; // 16 = header size - let size = size - (std::mem::size_of::() * 4) as u32; // 16 = header size + let mut cursor = Cursor::new(Vec::new()); + value.write_options(&mut cursor, endian, ())?; - let mut cursor = Cursor::new(Vec::new()); - value.write_options(&mut cursor, endian, ())?; + let mut buffer = cursor.into_inner(); + buffer.resize(size as usize, 0); - let mut buffer = cursor.into_inner(); - buffer.resize(size as usize, 0); + unsafe { + let encoded = blowfish_encode( + encryption_key.as_ptr(), + 16, + buffer.as_ptr(), + buffer.len() as u32, + ); + let encoded_data = slice::from_raw_parts(encoded, size as usize); + writer.write_all(encoded_data)?; - unsafe { - let encoded = blowfish_encode( - encryption_key.as_ptr(), - 16, - buffer.as_ptr(), - buffer.len() as u32, - ); - let encoded_data = slice::from_raw_parts(encoded, size as usize); - writer.write_all(encoded_data)?; + Ok(()) + } + } else { + tracing::info!("NOTE: Not encrypting this IPC packet since no key was provided!"); - Ok(()) + value.write_options(writer, endian, ()) } } diff --git a/src/ipc.rs b/src/ipc.rs index 322107b..aded2d2 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -2,10 +2,14 @@ use binrw::binrw; use crate::common::{read_string, write_string}; +// NOTE: See https://github.com/karashiiro/FFXIVOpcodes/blob/master/FFXIVOpcodes/Ipcs.cs for opcodes + #[binrw] #[brw(repr = u16)] #[derive(Clone, PartialEq, Debug)] pub enum IPCOpCode { + /// Sent by the server to Initialize something chat-related? + InitializeChat = 0x2, /// Sent by the client when it requests the character list in the lobby. RequestCharacterList = 0x3, /// Sent by the client when it requests to enter a world. @@ -24,6 +28,21 @@ pub enum IPCOpCode { LobbyServerList = 0x15, /// Sent by the server to inform the client of their retainers. LobbyRetainerList = 0x17, + + /// Sent by the client when they successfully initialize with the server, and they need several bits of information (e.g. what zone to load) + InitRequest = 0x2ED, + /// Sent by the server as response to ZoneInitRequest. + InitResponse = 280, // TODO: probably wrong! + /// Sent by the server that tells the client which zone to load + InitZone = 0x0311, + /// Sent by the server for... something + ActorControlSelf = 0x018C, + /// Sent by the server containing character stats + PlayerStats = 0x01FA, + /// Sent by the server to setup the player on the client + PlayerSetup = 0x006B, + // Sent by the server to setup class info + UpdateClassInfo = 0x006A, } #[binrw] @@ -97,6 +116,21 @@ pub enum LobbyCharacterAction { Request = 0x15, } +#[binrw] +#[derive(Debug, Clone, Default)] +pub struct Position { + pub x: f32, + pub y: f32, + pub z: f32, +} + +#[binrw] +#[derive(Debug, Eq, PartialEq, Clone)] +#[brw(repr = u16)] +pub enum ActorControlType { + SetCharaGearParamUI = 0x260, +} + #[binrw] #[br(import(magic: &IPCOpCode))] #[derive(Debug, Clone)] @@ -144,9 +178,17 @@ pub enum IPCStructData { lookup_id: u64, // TODO: what else is in here? }, + #[br(pre_assert(*magic == IPCOpCode::InitRequest))] + InitRequest { + // TODO: full of possibly interesting information + #[br(dbg)] + unk: [u8; 105], + }, // Server->Client IPC + #[br(pre_assert(false))] LobbyServiceAccountList { + #[br(dbg)] sequence: u64, #[brw(pad_before = 1)] num_service_accounts: u8, @@ -156,6 +198,7 @@ pub enum IPCStructData { #[br(count = 8)] service_accounts: Vec, }, + #[br(pre_assert(false))] LobbyServerList { sequence: u64, unk1: u16, @@ -165,12 +208,14 @@ pub enum IPCStructData { #[br(count = 6)] servers: Vec, }, + #[br(pre_assert(false))] LobbyRetainerList { // TODO: what is in here? #[brw(pad_before = 7)] #[brw(pad_after = 202)] unk1: u8, }, + #[br(pre_assert(false))] LobbyCharacterList { sequence: u64, counter: u8, @@ -196,6 +241,7 @@ pub enum IPCStructData { #[br(count = 2)] characters: Vec, }, + #[br(pre_assert(false))] LobbyEnterWorld { sequence: u64, character_id: u32, @@ -214,6 +260,219 @@ pub enum IPCStructData { #[bw(map = write_string)] host: String, }, + #[br(pre_assert(false))] + InitializeChat { unk: [u8; 24] }, + #[br(pre_assert(false))] + InitResponse { + unk1: u64, + character_id: u32, + unk2: u32, + }, + #[br(pre_assert(false))] + InitZone { + server_id: u16, + zone_id: u16, + zone_index: u16, + content_finder_condition_id: u16, + layer_set_id: u32, + layout_id: u32, + weather_id: u32, + unk_bitmask1: u8, + unk_bitmask2: u8, + unk1: u8, + unk2: u32, + festival_id: u16, + additional_festival_id: u16, + unk3: u32, + unk4: u32, + unk5: u32, + unk6: [u32; 4], + unk7: [u32; 3], + position: Position, + unk8: [u32; 4], + unk9: u32, + }, + #[br(pre_assert(false))] + ActorControlSelf { + #[brw(pad_after = 2)] + category: ActorControlType, + param1: u32, + param2: u32, + param3: u32, + param4: u32, + param5: u32, + #[brw(pad_after = 4)] + param6: u32, + }, + #[br(pre_assert(false))] + PlayerStats { + strength: u32, + dexterity: u32, + vitality: u32, + intelligence: u32, + mind: u32, + piety: u32, + hp: u32, + mp: u32, + tp: u32, + gp: u32, + cp: u32, + delay: u32, + tenacity: u32, + attack_power: u32, + defense: u32, + direct_hit_rate: u32, + evasion: u32, + magic_defense: u32, + critical_hit: u32, + attack_magic_potency: u32, + healing_magic_potency: u32, + elemental_bonus: u32, + determination: u32, + skill_speed: u32, + spell_speed: u32, + haste: u32, + craftmanship: u32, + control: u32, + gathering: u32, + perception: u32, + unk1: [u32; 26], + }, + #[br(pre_assert(false))] + PlayerSetup { + content_id: u64, + crest: u64, + unknown10: u64, + char_id: u32, + rested_exp: u32, + companion_current_exp: u32, + unknown1c: u32, + fish_caught: u32, + use_bait_catalog_id: u32, + unknown28: u32, + unknown_pvp2c: u16, + unknown2e: u16, + pvp_frontline_overall_campaigns: u32, + unknown_timestamp34: u32, + unknown_timestamp38: u32, + unknown3c: u32, + unknown40: u32, + unknown44: u32, + companion_time_passed: f32, + unknown4c: u32, + unknown50: u16, + unknown_pvp52: [u16; 4], + pvp_series_exp: u16, + player_commendations: u16, + unknown64: [u16; 8], + pvp_rival_wings_total_matches: u16, + pvp_rival_wings_total_victories: u16, + pvp_rival_wings_weekly_matches: u16, + pvp_rival_wings_weekly_victories: u16, + max_level: u8, + expansion: u8, + unknown76: u8, + unknown77: u8, + unknown78: u8, + race: u8, + tribe: u8, + gender: u8, + current_job: u8, + current_class: u8, + deity: u8, + nameday_month: u8, + nameday_day: u8, + city_state: u8, + homepoint: u8, + unknown8d: [u8; 3], + companion_rank: u8, + companion_stars: u8, + companion_sp: u8, + companion_unk93: u8, + companion_color: u8, + companion_fav_feed: u8, + fav_aetheryte_count: u8, + unknown97: [u8; 5], + sightseeing21_to_80_unlock: u8, + sightseeing_heavensward_unlock: u8, + unknown9e: [u8; 26], + exp: [u32; 32], + pvp_total_exp: u32, + unknown_pvp124: u32, + pvp_exp: u32, + pvp_frontline_overall_ranks: [u32; 3], + unknown138: u32, + levels: [u16; 32], + unknown194: [u8; 218], + companion_name: [u8; 21], + companion_def_rank: u8, + companion_att_rank: u8, + companion_heal_rank: u8, + mount_guide_mask: [u8; 33], + ornament_mask: [u8; 4], + unknown281: [u8; 23], + #[br(count = 32)] + #[bw(pad_size_to = 32)] + #[br(map = read_string)] + #[bw(map = write_string)] + name: String, + unknown293: [u8; 16], + unknown2a3: u8, + unlock_bitmask: [u8; 64], + aetheryte: [u8; 26], + favorite_aetheryte_ids: [u16; 4], + free_aetheryte_id: u16, + ps_plus_free_aetheryte_id: u16, + discovery: [u8; 480], + howto: [u8; 36], + unknown554: [u8; 4], + minions: [u8; 60], + chocobo_taxi_mask: [u8; 12], + watched_cutscenes: [u8; 159], + companion_barding_mask: [u8; 12], + companion_equipped_head: u8, + companion_equipped_body: u8, + companion_equipped_legs: u8, + unknown_mask: [u8; 287], + pose: [u8; 7], + unknown6df: [u8; 3], + challenge_log_complete: [u8; 13], + secret_recipe_book_mask: [u8; 12], + unknown_mask6f7: [u8; 29], + relic_completion: [u8; 12], + sightseeing_mask: [u8; 37], + hunting_mark_mask: [u8; 102], + triple_triad_cards: [u8; 45], + unknown895: u8, + unknown7d7: [u8; 15], + unknown7d8: u8, + unknown7e6: [u8; 49], + regional_folklore_mask: [u8; 6], + orchestrion_mask: [u8; 87], + hall_of_novice_completion: [u8; 3], + anima_completion: [u8; 11], + unknown85e: [u8; 41], + unlocked_raids: [u8; 28], + unlocked_dungeons: [u8; 18], + unlocked_guildhests: [u8; 10], + unlocked_trials: [u8; 12], + unlocked_pvp: [u8; 5], + cleared_raids: [u8; 28], + cleared_dungeons: [u8; 18], + cleared_guildhests: [u8; 10], + cleared_trials: [u8; 12], + cleared_pvp: [u8; 5], + unknown948: [u8; 15], + }, + #[br(pre_assert(false))] + UpdateClassInfo { + class_id: u16, + unknown: u8, + is_specialist: u8, + synced_level: u16, + class_level: u16, + role_actions: [u32; 10], + }, } #[binrw] @@ -221,9 +480,12 @@ pub enum IPCStructData { pub struct IPCSegment { pub unk1: u8, pub unk2: u8, + #[br(dbg)] pub op_code: IPCOpCode, #[brw(pad_before = 2)] // empty + #[br(dbg)] pub server_id: u16, + #[br(dbg)] pub timestamp: u32, #[brw(pad_before = 4)] #[br(args(&op_code))] @@ -244,6 +506,14 @@ impl IPCSegment { IPCStructData::LobbyCharacterAction { .. } => todo!(), IPCStructData::LobbyEnterWorld { .. } => 160, IPCStructData::RequestEnterWorld { .. } => todo!(), + IPCStructData::InitializeChat { .. } => 24, + IPCStructData::InitRequest { .. } => todo!(), + IPCStructData::InitResponse { .. } => 16, + IPCStructData::InitZone { .. } => 103, + IPCStructData::ActorControlSelf { .. } => 32, + IPCStructData::PlayerStats { .. } => 228, + IPCStructData::PlayerSetup { .. } => 2544, + IPCStructData::UpdateClassInfo { .. } => 48, } } } diff --git a/src/lib.rs b/src/lib.rs index 43bc553..b8bc00a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,10 +8,19 @@ mod compression; pub mod config; pub mod encryption; pub mod ipc; -mod oodle; +pub mod oodle; pub mod packet; pub mod patchlist; +// TODO: make this configurable +// See https://ffxiv.consolegameswiki.com/wiki/Servers for a list of possible IDs +pub const WORLD_ID: u16 = 63; +pub const WORLD_NAME: &str = "KAWARI"; + +pub const ZONE_ID: u16 = 1255; + +pub const CONTENT_ID: u64 = 11111111111111111; + pub fn generate_sid() -> String { let random_id: String = rand::thread_rng() .sample_iter(&Alphanumeric) diff --git a/src/oodle.rs b/src/oodle.rs index eee05a5..7c5d9c6 100644 --- a/src/oodle.rs +++ b/src/oodle.rs @@ -43,6 +43,10 @@ pub struct FFXIVOodle { window: Vec, } +pub trait Oodle { + fn decode(&mut self, input: Vec, decompressed_size: u32) -> Vec; +} + impl FFXIVOodle { pub fn new() -> FFXIVOodle { let htbits: i32 = 17; @@ -74,8 +78,10 @@ impl FFXIVOodle { } } } +} - pub fn decode(&mut self, input: Vec, decompressed_size: u32) -> Vec { +impl Oodle for FFXIVOodle { + fn decode(&mut self, input: Vec, decompressed_size: u32) -> Vec { unsafe { let mut out_buf: Vec = vec![0u8; decompressed_size.try_into().unwrap()]; let mut in_buf = input.to_vec(); diff --git a/src/packet.rs b/src/packet.rs index 70415b6..0a31a1c 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -15,12 +15,13 @@ use crate::{ compression::decompress, encryption::{decrypt, encrypt}, ipc::IPCSegment, + oodle::FFXIVOodle, }; #[binrw] #[brw(repr = u16)] #[derive(Debug)] -enum ConnectionType { +pub enum ConnectionType { None = 0x0, Zone = 0x1, Chat = 0x2, @@ -32,6 +33,12 @@ enum ConnectionType { #[derive(Debug, Clone)] pub enum SegmentType { // Client->Server Packets + #[brw(magic = 0x1u32)] + InitializeSession { + #[brw(pad_before = 4)] + #[brw(pad_after = 48)] // TODO: probably not empty? + player_id: u32, + }, #[brw(magic = 0x9u32)] InitializeEncryption { #[brw(pad_before = 36)] // empty @@ -53,13 +60,18 @@ pub enum SegmentType { KeepAlive { id: u32, timestamp: u32 }, // Server->Client Packets - #[brw(magic = 0x0Au32)] + #[brw(magic = 0xAu32)] InitializationEncryptionResponse { #[br(count = 0x280)] data: Vec, }, - #[brw(magic = 0x08u32)] + #[brw(magic = 0x8u32)] KeepAliveResponse { id: u32, timestamp: u32 }, + #[brw(magic = 0x2u32)] + ZoneInitialize { + #[brw(pad_after = 36)] + player_id: u32, + }, } #[binrw] @@ -90,10 +102,14 @@ pub struct PacketHeader { #[derive(Debug, Clone)] pub struct PacketSegment { #[bw(calc = self.calc_size())] + #[br(dbg)] pub size: u32, + #[br(dbg)] pub source_actor: u32, + #[br(dbg)] pub target_actor: u32, #[brw(args(size, encryption_key))] + #[br(dbg)] pub segment_type: SegmentType, } @@ -105,20 +121,23 @@ impl PacketSegment { SegmentType::InitializeEncryption { .. } => 616, SegmentType::InitializationEncryptionResponse { .. } => 640, SegmentType::Ipc { data } => data.calc_size(), - SegmentType::KeepAlive { .. } => todo!(), + SegmentType::KeepAlive { .. } => 0x8, SegmentType::KeepAliveResponse { .. } => 0x8, + SegmentType::ZoneInitialize { .. } => 40, + SegmentType::InitializeSession { .. } => todo!(), } } } #[binrw] -#[brw(import(encryption_key: Option<&[u8]>))] +#[brw(import(oodle: &mut FFXIVOodle, encryption_key: Option<&[u8]>))] #[derive(Debug)] struct Packet { #[br(dbg)] header: PacketHeader, #[bw(args(encryption_key))] - #[br(parse_with = decompress, args(&header, encryption_key,))] + #[br(parse_with = decompress, args(oodle, &header, encryption_key,))] + #[br(dbg)] segments: Vec, } @@ -130,7 +149,7 @@ fn dump(msg: &str, data: &[u8]) { pub async fn send_packet( socket: &mut WriteHalf, segments: &[PacketSegment], - state: &State, + state: &mut State, ) { let timestamp: u64 = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -166,7 +185,10 @@ pub async fn send_packet( packet .write_le_args( &mut cursor, - (state.client_key.as_ref().map(|s: &[u8; 16]| s.as_slice()),), + ( + &mut state.oodle, + state.client_key.as_ref().map(|s: &[u8; 16]| s.as_slice()), + ), ) .unwrap(); @@ -185,14 +207,19 @@ pub async fn send_packet( pub struct State { pub client_key: Option<[u8; 16]>, pub session_id: Option, + pub oodle: FFXIVOodle, + pub player_id: Option, } -pub async fn parse_packet(data: &[u8], state: &mut State) -> Vec { +pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec, ConnectionType) { let mut cursor = Cursor::new(data); match Packet::read_le_args( &mut cursor, - (state.client_key.as_ref().map(|s: &[u8; 16]| s.as_slice()),), + ( + &mut state.oodle, + state.client_key.as_ref().map(|s: &[u8; 16]| s.as_slice()), + ), ) { Ok(packet) => { println!("{:#?}", packet); @@ -204,20 +231,20 @@ pub async fn parse_packet(data: &[u8], state: &mut State) -> Vec ); } - packet.segments + (packet.segments, packet.header.connection_type) } Err(err) => { println!("{err}"); dump("Failed to parse packet!", data); - Vec::new() + (Vec::new(), ConnectionType::None) } } } pub async fn send_keep_alive( socket: &mut WriteHalf, - state: &State, + state: &mut State, id: u32, timestamp: u32, ) {