From fbe6862c7b4d52516655ff86eceee18f815bf214 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 22 Jun 2025 09:18:14 -0400 Subject: [PATCH] Begin implementing EXP This doesn't actually do things like level you up, but if you use the GM EXP command then it will update the UI. --- USAGE.md | 1 + src/bin/kawari-world.rs | 7 ++++++- src/ipc/zone/mod.rs | 1 + src/ipc/zone/update_class_info.rs | 11 ++++++----- src/world/connection.rs | 14 ++++++++++++-- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/USAGE.md b/USAGE.md index e6f7797..9f00c24 100644 --- a/USAGE.md +++ b/USAGE.md @@ -131,3 +131,4 @@ These GM commands are implemented in the FFXIV protocol, but only some of them a * `//gm aetheryte `: Unlock an Aetheryte. * `//gm speed `: Increases your movement speed by `multiplier`. * `//gm orchestrion `: Unlock an Orchestrion song. +* `//gm exp ` Adds the specified amount of EXP to the current class/job. diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index e586324..6c1bed9 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -279,7 +279,7 @@ async fn client_loop( timestamp: timestamp_secs(), data: ServerZoneIpcData::PlayerStatus(PlayerStatus { content_id: connection.player_data.content_id, - exp: [0; 32], + exp: connection.player_data.classjob_exp, name: chara_details.name, char_id: connection.player_data.actor_id, race: chara_details.chara_make.customize.race, @@ -682,6 +682,11 @@ async fn client_loop( category: ActorControlCategory::LearnTeleport { id, unlocked: on } }).await; } } + GameMasterCommandType::EXP => { + let amount = *arg0; + connection.player_data.set_current_exp(connection.player_data.current_exp() + amount); + connection.update_class_info().await; + } } } ClientZoneIpcData::ZoneJump { diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 7256897..93cde92 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -144,6 +144,7 @@ pub enum GameMasterCommandType { ToggleInvisibility = 0xD, ToggleWireframe = 0x26, ChangeTerritory = 0x58, + EXP = 0x68, Orchestrion = 0x74, GiveItem = 0xC8, Aetheryte = 0x5E, diff --git a/src/ipc/zone/update_class_info.rs b/src/ipc/zone/update_class_info.rs index 23ea1fa..65dd047 100644 --- a/src/ipc/zone/update_class_info.rs +++ b/src/ipc/zone/update_class_info.rs @@ -3,10 +3,11 @@ use binrw::binrw; #[binrw] #[derive(Debug, Clone, Copy, Default)] pub struct UpdateClassInfo { - pub class_id: u16, - pub unknown: u8, - pub is_specialist: u8, - pub synced_level: u16, + pub class_id: u8, + #[brw(pad_before = 1)] + pub current_level: u16, pub class_level: u16, - pub role_actions: [u32; 2], + pub synced_level: u16, + pub current_exp: u32, + pub rested_exp: u32, } diff --git a/src/world/connection.rs b/src/world/connection.rs index 634c13b..384d110 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -59,6 +59,7 @@ pub struct PlayerData { pub classjob_id: u8, pub classjob_levels: [i32; 32], + pub classjob_exp: [u32; 32], pub curr_hp: u32, pub max_hp: u32, pub curr_mp: u16, @@ -84,6 +85,14 @@ impl PlayerData { pub fn set_current_level(&mut self, level: i32) { self.classjob_levels[self.classjob_id as usize] = level; } + + pub fn current_exp(&self) -> u32 { + self.classjob_exp[self.classjob_id as usize] + } + + pub fn set_current_exp(&mut self, exp: u32) { + self.classjob_exp[self.classjob_id as usize] = exp; + } } /// Represents a single connection between an instance of the client and the world server @@ -300,10 +309,11 @@ impl ZoneConnection { op_code: ServerZoneIpcType::UpdateClassInfo, timestamp: timestamp_secs(), data: ServerZoneIpcData::UpdateClassInfo(UpdateClassInfo { - class_id: self.player_data.classjob_id as u16, - unknown: 1, + class_id: self.player_data.classjob_id, synced_level: self.player_data.current_level() as u16, class_level: self.player_data.current_level() as u16, + current_level: self.player_data.current_level() as u16, + current_exp: self.player_data.current_exp(), ..Default::default() }), ..Default::default()