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:
parent
e3886e69da
commit
660e12c597
9 changed files with 835 additions and 69 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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!")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
270
src/ipc.rs
270
src/ipc.rs
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -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)
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue