1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-21 15:07:45 +00:00

Start implementing some world IPC

This doesn't work yet, but whatever it's a start.
This commit is contained in:
Joshua Goins 2025-03-10 21:31:21 -04:00
parent e3886e69da
commit 660e12c597
9 changed files with 835 additions and 69 deletions

View file

@ -18,6 +18,13 @@ name = "kawari-patch"
[[bin]] [[bin]]
name = "kawari-web" name = "kawari-web"
[[bin]]
name = "kawari-lobby"
[[bin]]
name = "kawari-world"
required-features = ["oodle"]
[profile.release] [profile.release]
lto = true lto = true
strip = true strip = true
@ -25,6 +32,10 @@ opt-level = "z"
codegen-units = 1 codegen-units = 1
panic = "abort" panic = "abort"
[features]
default = ["oodle"]
oodle = []
[build-dependencies] [build-dependencies]
cc = "1.0" cc = "1.0"

View file

@ -4,9 +4,11 @@ use std::time::{SystemTime, UNIX_EPOCH};
use kawari::client_select_data::{ClientCustomizeData, ClientSelectData}; use kawari::client_select_data::{ClientCustomizeData, ClientSelectData};
use kawari::encryption::{blowfish_encode, generate_encryption_key}; use kawari::encryption::{blowfish_encode, generate_encryption_key};
use kawari::ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, Server, ServiceAccount}; use kawari::ipc::{CharacterDetails, IPCOpCode, IPCSegment, IPCStructData, Server, ServiceAccount};
use kawari::oodle::FFXIVOodle;
use kawari::packet::{ use kawari::packet::{
PacketSegment, SegmentType, State, parse_packet, send_keep_alive, send_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::io::{AsyncReadExt, WriteHalf};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
@ -25,6 +27,8 @@ async fn main() {
let mut state = State { let mut state = State {
client_key: None, client_key: None,
session_id: None, session_id: None,
oodle: FFXIVOodle::new(),
player_id: None,
}; };
tokio::spawn(async move { tokio::spawn(async move {
@ -33,7 +37,7 @@ async fn main() {
let n = read.read(&mut buf).await.expect("Failed to read data!"); let n = read.read(&mut buf).await.expect("Failed to read data!");
if n != 0 { 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 { for segment in &segments {
match &segment.segment_type { match &segment.segment_type {
SegmentType::InitializeEncryption { phrase, key } => { SegmentType::InitializeEncryption { phrase, key } => {
@ -50,12 +54,12 @@ async fn main() {
state.session_id = Some(session_id.clone()); 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 } => { IPCStructData::RequestCharacterList { sequence } => {
tracing::info!("Client is requesting character list..."); 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 { IPCStructData::LobbyCharacterAction {
sequence, sequence,
@ -79,7 +83,7 @@ async fn main() {
} => { } => {
tracing::info!("Client is joining the world..."); 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; .await;
} }
_ => { _ => {
@ -87,7 +91,7 @@ async fn main() {
} }
}, },
SegmentType::KeepAlive { id, timestamp } => { 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!") panic!("The server is recieving a response packet!")
@ -125,7 +129,7 @@ async fn initialize_encryption(
send_packet(socket, &[response_packet], state).await; send_packet(socket, &[response_packet], state).await;
} }
async fn send_account_list(socket: &mut WriteHalf<TcpStream>, state: &State) { async fn send_account_list(socket: &mut WriteHalf<TcpStream>, state: &mut State) {
let timestamp: u32 = SystemTime::now() let timestamp: u32 = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!") .expect("Failed to get UNIX timestamp!")
@ -167,12 +171,7 @@ async fn send_account_list(socket: &mut WriteHalf<TcpStream>, state: &State) {
send_packet(socket, &[response_packet], state).await; send_packet(socket, &[response_packet], state).await;
} }
// TODO: make this configurable async fn send_lobby_info(socket: &mut WriteHalf<TcpStream>, state: &mut State, sequence: u64) {
// 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<TcpStream>, state: &State, sequence: u64) {
let timestamp: u32 = SystemTime::now() let timestamp: u32 = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Failed to get UNIX timestamp!") .expect("Failed to get UNIX timestamp!")
@ -256,7 +255,7 @@ async fn send_lobby_info(socket: &mut WriteHalf<TcpStream>, state: &State, seque
guardian: 2, guardian: 2,
unk8: 0, unk8: 0,
unk9: 0, unk9: 0,
zone_id: 1000, zone_id: ZONE_ID as i32,
unk11: 0, unk11: 0,
customize: ClientCustomizeData { customize: ClientCustomizeData {
race: 3, race: 3,
@ -301,7 +300,7 @@ async fn send_lobby_info(socket: &mut WriteHalf<TcpStream>, state: &State, seque
let mut characters = vec![CharacterDetails { let mut characters = vec![CharacterDetails {
id: 0, id: 0,
content_id: 11111111111111111, content_id: CONTENT_ID,
index: 0, index: 0,
unk1: [0; 16], unk1: [0; 16],
origin_server_id: WORLD_ID, origin_server_id: WORLD_ID,
@ -387,7 +386,7 @@ async fn send_lobby_info(socket: &mut WriteHalf<TcpStream>, state: &State, seque
async fn send_enter_world( async fn send_enter_world(
socket: &mut WriteHalf<TcpStream>, socket: &mut WriteHalf<TcpStream>,
state: &State, state: &mut State,
sequence: u64, sequence: u64,
lookup_id: u64, lookup_id: u64,
) { ) {

View file

@ -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::io::AsyncReadExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -17,6 +24,8 @@ async fn main() {
let mut state = State { let mut state = State {
client_key: None, client_key: None,
session_id: None, session_id: None,
oodle: FFXIVOodle::new(),
player_id: None,
}; };
tokio::spawn(async move { tokio::spawn(async move {
@ -25,14 +34,435 @@ async fn main() {
let n = read.read(&mut buf).await.expect("Failed to read data!"); let n = read.read(&mut buf).await.expect("Failed to read data!");
if n != 0 { 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 { for segment in &segments {
match &segment.segment_type { 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 } => { 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 } => { 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!") panic!("The server is recieving a response or unknown packet!")

View file

@ -1,14 +1,16 @@
use std::fs::write;
use std::io::Cursor; use std::io::Cursor;
use binrw::{BinRead, BinResult}; use binrw::{BinRead, BinResult};
use crate::{ use crate::{
oodle::FFXIVOodle, oodle::{FFXIVOodle, Oodle},
packet::{PacketHeader, PacketSegment}, packet::{PacketHeader, PacketSegment},
}; };
#[binrw::parser(reader, endian)] #[binrw::parser(reader, endian)]
pub(crate) fn decompress( pub(crate) fn decompress(
oodle: &mut FFXIVOodle,
header: &PacketHeader, header: &PacketHeader,
encryption_key: Option<&[u8]>, encryption_key: Option<&[u8]>,
) -> BinResult<Vec<PacketSegment>> { ) -> BinResult<Vec<PacketSegment>> {
@ -16,16 +18,23 @@ pub(crate) fn decompress(
let size = header.size as usize - std::mem::size_of::<PacketHeader>(); let size = header.size as usize - std::mem::size_of::<PacketHeader>();
println!(
"known packet size: {} but decompressing {} bytes",
header.size, size
);
let mut data = vec![0; 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 { let data = match header.compressed {
crate::packet::CompressionType::Uncompressed => data, crate::packet::CompressionType::Uncompressed => data,
crate::packet::CompressionType::Oodle => { crate::packet::CompressionType::Oodle => oodle.decode(data, header.oodle_decompressed_size),
FFXIVOodle::new().decode(data, header.oodle_decompressed_size)
}
}; };
write("decompressed.bin", &data).unwrap();
let mut cursor = Cursor::new(&data); let mut cursor = Cursor::new(&data);
for _ in 0..header.segment_count { for _ in 0..header.segment_count {

View file

@ -36,17 +36,15 @@ pub(crate) fn decrypt<T>(size: u32, encryption_key: Option<&[u8]>) -> BinResult<
where where
for<'a> T: BinRead<Args<'a> = ()> + 'a, for<'a> T: BinRead<Args<'a> = ()> + 'a,
{ {
let Some(encryption_key) = encryption_key else { if let Some(encryption_key) = encryption_key {
panic!("This segment type is encrypted and no key was provided!");
};
let size = size - (std::mem::size_of::<u32>() * 4) as u32; // 16 = header size let size = size - (std::mem::size_of::<u32>() * 4) as u32; // 16 = header size
let mut data = vec![0; size as usize]; let mut data = vec![0; size as usize];
reader.read_exact(&mut data)?; reader.read_exact(&mut data)?;
unsafe { unsafe {
let decryption_result = blowfish_decode(encryption_key.as_ptr(), 16, data.as_ptr(), size); 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); let decrypted_data = slice::from_raw_parts(decryption_result, size as usize);
write("decrypted.bin", decrypted_data).unwrap(); write("decrypted.bin", decrypted_data).unwrap();
@ -54,6 +52,11 @@ where
let mut cursor = Cursor::new(&decrypted_data); let mut cursor = Cursor::new(&decrypted_data);
T::read_options(&mut cursor, endian, ()) T::read_options(&mut cursor, endian, ())
} }
} else {
tracing::info!("NOTE: Not decrypting this IPC packet since no key was provided!");
T::read_options(reader, endian, ())
}
} }
#[binrw::writer(writer, endian)] #[binrw::writer(writer, endian)]
@ -61,10 +64,7 @@ pub(crate) fn encrypt<T>(value: &T, size: u32, encryption_key: Option<&[u8]>) ->
where where
for<'a> T: BinWrite<Args<'a> = ()> + 'a, for<'a> T: BinWrite<Args<'a> = ()> + 'a,
{ {
let Some(encryption_key) = encryption_key else { if let Some(encryption_key) = encryption_key {
panic!("This segment type needs to be encrypted and no key was provided!");
};
let size = size - (std::mem::size_of::<u32>() * 4) as u32; // 16 = header size let size = size - (std::mem::size_of::<u32>() * 4) as u32; // 16 = header size
let mut cursor = Cursor::new(Vec::new()); let mut cursor = Cursor::new(Vec::new());
@ -85,6 +85,11 @@ where
Ok(()) Ok(())
} }
} else {
tracing::info!("NOTE: Not encrypting this IPC packet since no key was provided!");
value.write_options(writer, endian, ())
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -2,10 +2,14 @@ use binrw::binrw;
use crate::common::{read_string, write_string}; use crate::common::{read_string, write_string};
// NOTE: See https://github.com/karashiiro/FFXIVOpcodes/blob/master/FFXIVOpcodes/Ipcs.cs for opcodes
#[binrw] #[binrw]
#[brw(repr = u16)] #[brw(repr = u16)]
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum IPCOpCode { 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. /// Sent by the client when it requests the character list in the lobby.
RequestCharacterList = 0x3, RequestCharacterList = 0x3,
/// Sent by the client when it requests to enter a world. /// Sent by the client when it requests to enter a world.
@ -24,6 +28,21 @@ pub enum IPCOpCode {
LobbyServerList = 0x15, LobbyServerList = 0x15,
/// Sent by the server to inform the client of their retainers. /// Sent by the server to inform the client of their retainers.
LobbyRetainerList = 0x17, 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] #[binrw]
@ -97,6 +116,21 @@ pub enum LobbyCharacterAction {
Request = 0x15, 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] #[binrw]
#[br(import(magic: &IPCOpCode))] #[br(import(magic: &IPCOpCode))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -144,9 +178,17 @@ pub enum IPCStructData {
lookup_id: u64, lookup_id: u64,
// TODO: what else is in here? // 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 // Server->Client IPC
#[br(pre_assert(false))]
LobbyServiceAccountList { LobbyServiceAccountList {
#[br(dbg)]
sequence: u64, sequence: u64,
#[brw(pad_before = 1)] #[brw(pad_before = 1)]
num_service_accounts: u8, num_service_accounts: u8,
@ -156,6 +198,7 @@ pub enum IPCStructData {
#[br(count = 8)] #[br(count = 8)]
service_accounts: Vec<ServiceAccount>, service_accounts: Vec<ServiceAccount>,
}, },
#[br(pre_assert(false))]
LobbyServerList { LobbyServerList {
sequence: u64, sequence: u64,
unk1: u16, unk1: u16,
@ -165,12 +208,14 @@ pub enum IPCStructData {
#[br(count = 6)] #[br(count = 6)]
servers: Vec<Server>, servers: Vec<Server>,
}, },
#[br(pre_assert(false))]
LobbyRetainerList { LobbyRetainerList {
// TODO: what is in here? // TODO: what is in here?
#[brw(pad_before = 7)] #[brw(pad_before = 7)]
#[brw(pad_after = 202)] #[brw(pad_after = 202)]
unk1: u8, unk1: u8,
}, },
#[br(pre_assert(false))]
LobbyCharacterList { LobbyCharacterList {
sequence: u64, sequence: u64,
counter: u8, counter: u8,
@ -196,6 +241,7 @@ pub enum IPCStructData {
#[br(count = 2)] #[br(count = 2)]
characters: Vec<CharacterDetails>, characters: Vec<CharacterDetails>,
}, },
#[br(pre_assert(false))]
LobbyEnterWorld { LobbyEnterWorld {
sequence: u64, sequence: u64,
character_id: u32, character_id: u32,
@ -214,6 +260,219 @@ pub enum IPCStructData {
#[bw(map = write_string)] #[bw(map = write_string)]
host: 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] #[binrw]
@ -221,9 +480,12 @@ pub enum IPCStructData {
pub struct IPCSegment { pub struct IPCSegment {
pub unk1: u8, pub unk1: u8,
pub unk2: u8, pub unk2: u8,
#[br(dbg)]
pub op_code: IPCOpCode, pub op_code: IPCOpCode,
#[brw(pad_before = 2)] // empty #[brw(pad_before = 2)] // empty
#[br(dbg)]
pub server_id: u16, pub server_id: u16,
#[br(dbg)]
pub timestamp: u32, pub timestamp: u32,
#[brw(pad_before = 4)] #[brw(pad_before = 4)]
#[br(args(&op_code))] #[br(args(&op_code))]
@ -244,6 +506,14 @@ impl IPCSegment {
IPCStructData::LobbyCharacterAction { .. } => todo!(), IPCStructData::LobbyCharacterAction { .. } => todo!(),
IPCStructData::LobbyEnterWorld { .. } => 160, IPCStructData::LobbyEnterWorld { .. } => 160,
IPCStructData::RequestEnterWorld { .. } => todo!(), 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,
} }
} }
} }

View file

@ -8,10 +8,19 @@ mod compression;
pub mod config; pub mod config;
pub mod encryption; pub mod encryption;
pub mod ipc; pub mod ipc;
mod oodle; pub mod oodle;
pub mod packet; pub mod packet;
pub mod patchlist; 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 { pub fn generate_sid() -> String {
let random_id: String = rand::thread_rng() let random_id: String = rand::thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)

View file

@ -43,6 +43,10 @@ pub struct FFXIVOodle {
window: Vec<u8>, window: Vec<u8>,
} }
pub trait Oodle {
fn decode(&mut self, input: Vec<u8>, decompressed_size: u32) -> Vec<u8>;
}
impl FFXIVOodle { impl FFXIVOodle {
pub fn new() -> FFXIVOodle { pub fn new() -> FFXIVOodle {
let htbits: i32 = 17; let htbits: i32 = 17;
@ -74,8 +78,10 @@ impl FFXIVOodle {
} }
} }
} }
}
pub fn decode(&mut self, input: Vec<u8>, decompressed_size: u32) -> Vec<u8> { impl Oodle for FFXIVOodle {
fn decode(&mut self, input: Vec<u8>, decompressed_size: u32) -> Vec<u8> {
unsafe { unsafe {
let mut out_buf: Vec<u8> = vec![0u8; decompressed_size.try_into().unwrap()]; let mut out_buf: Vec<u8> = vec![0u8; decompressed_size.try_into().unwrap()];
let mut in_buf = input.to_vec(); let mut in_buf = input.to_vec();

View file

@ -15,12 +15,13 @@ use crate::{
compression::decompress, compression::decompress,
encryption::{decrypt, encrypt}, encryption::{decrypt, encrypt},
ipc::IPCSegment, ipc::IPCSegment,
oodle::FFXIVOodle,
}; };
#[binrw] #[binrw]
#[brw(repr = u16)] #[brw(repr = u16)]
#[derive(Debug)] #[derive(Debug)]
enum ConnectionType { pub enum ConnectionType {
None = 0x0, None = 0x0,
Zone = 0x1, Zone = 0x1,
Chat = 0x2, Chat = 0x2,
@ -32,6 +33,12 @@ enum ConnectionType {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SegmentType { pub enum SegmentType {
// Client->Server Packets // 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)] #[brw(magic = 0x9u32)]
InitializeEncryption { InitializeEncryption {
#[brw(pad_before = 36)] // empty #[brw(pad_before = 36)] // empty
@ -53,13 +60,18 @@ pub enum SegmentType {
KeepAlive { id: u32, timestamp: u32 }, KeepAlive { id: u32, timestamp: u32 },
// Server->Client Packets // Server->Client Packets
#[brw(magic = 0x0Au32)] #[brw(magic = 0xAu32)]
InitializationEncryptionResponse { InitializationEncryptionResponse {
#[br(count = 0x280)] #[br(count = 0x280)]
data: Vec<u8>, data: Vec<u8>,
}, },
#[brw(magic = 0x08u32)] #[brw(magic = 0x8u32)]
KeepAliveResponse { id: u32, timestamp: u32 }, KeepAliveResponse { id: u32, timestamp: u32 },
#[brw(magic = 0x2u32)]
ZoneInitialize {
#[brw(pad_after = 36)]
player_id: u32,
},
} }
#[binrw] #[binrw]
@ -90,10 +102,14 @@ pub struct PacketHeader {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PacketSegment { pub struct PacketSegment {
#[bw(calc = self.calc_size())] #[bw(calc = self.calc_size())]
#[br(dbg)]
pub size: u32, pub size: u32,
#[br(dbg)]
pub source_actor: u32, pub source_actor: u32,
#[br(dbg)]
pub target_actor: u32, pub target_actor: u32,
#[brw(args(size, encryption_key))] #[brw(args(size, encryption_key))]
#[br(dbg)]
pub segment_type: SegmentType, pub segment_type: SegmentType,
} }
@ -105,20 +121,23 @@ impl PacketSegment {
SegmentType::InitializeEncryption { .. } => 616, SegmentType::InitializeEncryption { .. } => 616,
SegmentType::InitializationEncryptionResponse { .. } => 640, SegmentType::InitializationEncryptionResponse { .. } => 640,
SegmentType::Ipc { data } => data.calc_size(), SegmentType::Ipc { data } => data.calc_size(),
SegmentType::KeepAlive { .. } => todo!(), SegmentType::KeepAlive { .. } => 0x8,
SegmentType::KeepAliveResponse { .. } => 0x8, SegmentType::KeepAliveResponse { .. } => 0x8,
SegmentType::ZoneInitialize { .. } => 40,
SegmentType::InitializeSession { .. } => todo!(),
} }
} }
} }
#[binrw] #[binrw]
#[brw(import(encryption_key: Option<&[u8]>))] #[brw(import(oodle: &mut FFXIVOodle, encryption_key: Option<&[u8]>))]
#[derive(Debug)] #[derive(Debug)]
struct Packet { struct Packet {
#[br(dbg)] #[br(dbg)]
header: PacketHeader, header: PacketHeader,
#[bw(args(encryption_key))] #[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<PacketSegment>, segments: Vec<PacketSegment>,
} }
@ -130,7 +149,7 @@ fn dump(msg: &str, data: &[u8]) {
pub async fn send_packet( pub async fn send_packet(
socket: &mut WriteHalf<TcpStream>, socket: &mut WriteHalf<TcpStream>,
segments: &[PacketSegment], segments: &[PacketSegment],
state: &State, state: &mut State,
) { ) {
let timestamp: u64 = SystemTime::now() let timestamp: u64 = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -166,7 +185,10 @@ pub async fn send_packet(
packet packet
.write_le_args( .write_le_args(
&mut cursor, &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(); .unwrap();
@ -185,14 +207,19 @@ pub async fn send_packet(
pub struct State { pub struct State {
pub client_key: Option<[u8; 16]>, pub client_key: Option<[u8; 16]>,
pub session_id: Option<String>, pub session_id: Option<String>,
pub oodle: FFXIVOodle,
pub player_id: Option<u32>,
} }
pub async fn parse_packet(data: &[u8], state: &mut State) -> Vec<PacketSegment> { pub async fn parse_packet(data: &[u8], state: &mut State) -> (Vec<PacketSegment>, ConnectionType) {
let mut cursor = Cursor::new(data); let mut cursor = Cursor::new(data);
match Packet::read_le_args( match Packet::read_le_args(
&mut cursor, &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) => { Ok(packet) => {
println!("{:#?}", packet); println!("{:#?}", packet);
@ -204,20 +231,20 @@ pub async fn parse_packet(data: &[u8], state: &mut State) -> Vec<PacketSegment>
); );
} }
packet.segments (packet.segments, packet.header.connection_type)
} }
Err(err) => { Err(err) => {
println!("{err}"); println!("{err}");
dump("Failed to parse packet!", data); dump("Failed to parse packet!", data);
Vec::new() (Vec::new(), ConnectionType::None)
} }
} }
} }
pub async fn send_keep_alive( pub async fn send_keep_alive(
socket: &mut WriteHalf<TcpStream>, socket: &mut WriteHalf<TcpStream>,
state: &State, state: &mut State,
id: u32, id: u32,
timestamp: u32, timestamp: u32,
) { ) {