From 2a1491c8f970a6fbe137cb799cd153f910ca743b Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 20 Jun 2025 19:08:53 -0400 Subject: [PATCH] Make the current classjob persistent, and your levels too This requires another database wipe, or if you're savvy enough a few schema edits. --- src/bin/kawari-world.rs | 2 +- src/world/connection.rs | 22 ++++++++----- src/world/database.rs | 68 ++++++++++++++++++++++++++++------------- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 8a109e8..db4d5ed 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -587,7 +587,7 @@ async fn client_loop( match &command { GameMasterCommandType::SetLevel => { - connection.player_data.level = *arg0 as u8; + connection.player_data.set_current_level(*arg0 as i32); connection.update_class_info().await; } GameMasterCommandType::ChangeWeather => { diff --git a/src/world/connection.rs b/src/world/connection.rs index b8416d3..79b83c7 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -46,7 +46,7 @@ pub struct PlayerData { pub account_id: u32, pub classjob_id: u8, - pub level: u8, + pub classjob_levels: [i32; 32], pub curr_hp: u32, pub max_hp: u32, pub curr_mp: u16, @@ -64,6 +64,16 @@ pub struct PlayerData { pub gm_invisible: bool, } +impl PlayerData { + pub fn current_level(&self) -> i32 { + self.classjob_levels[self.classjob_id as usize] + } + + pub fn set_current_level(&mut self, level: i32) { + self.classjob_levels[self.classjob_id as usize] = level; + } +} + /// Represents a single connection between an instance of the client and the world server pub struct ZoneConnection { pub config: WorldConfig, @@ -137,8 +147,6 @@ impl ZoneConnection { pub async fn initialize(&mut self, actor_id: u32) { // some still hardcoded values - self.player_data.classjob_id = 1; - self.player_data.level = 5; self.player_data.curr_hp = 100; self.player_data.max_hp = 100; self.player_data.curr_mp = 10000; @@ -282,8 +290,8 @@ impl ZoneConnection { data: ServerZoneIpcData::UpdateClassInfo(UpdateClassInfo { class_id: self.player_data.classjob_id as u16, unknown: 1, - synced_level: self.player_data.level as u16, - class_level: self.player_data.level as u16, + synced_level: self.player_data.current_level() as u16, + class_level: self.player_data.current_level() as u16, ..Default::default() }), ..Default::default() @@ -658,7 +666,7 @@ impl ZoneConnection { data: ServerZoneIpcData::StatusEffectList(StatusEffectList { statues: list, classjob_id: self.player_data.classjob_id, - level: self.player_data.level, + level: self.player_data.current_level() as u8, curr_hp: self.player_data.curr_hp, max_hp: self.player_data.max_hp, curr_mp: self.player_data.curr_mp, @@ -779,7 +787,7 @@ impl ZoneConnection { hp_max: self.player_data.max_hp, mp_curr: self.player_data.curr_mp, mp_max: self.player_data.max_mp, - level: self.player_data.level, + level: self.player_data.current_level() as u8, object_kind: ObjectKind::Player(PlayerSubKind::Player), look: chara_details.chara_make.customize, display_flags: DisplayFlag::UNK, diff --git a/src/world/database.rs b/src/world/database.rs index 62454e7..6f1fe83 100644 --- a/src/world/database.rs +++ b/src/world/database.rs @@ -47,7 +47,7 @@ impl WorldDatabase { // Create characters data table { - let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING, city_state INTEGER, zone_id INTEGER, pos_x REAL, pos_y REAL, pos_z REAL, rotation REAL, inventory STRING, remake_mode INTEGER, gm_rank INTEGER);"; + let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING, city_state INTEGER, zone_id INTEGER, pos_x REAL, pos_y REAL, pos_z REAL, rotation REAL, inventory STRING, remake_mode INTEGER, gm_rank INTEGER, classjob_id INTEGER, classjob_levels STRING);"; connection.execute(query, ()).unwrap(); } @@ -142,17 +142,19 @@ impl WorldDatabase { .unwrap(); stmt = connection - .prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank FROM character_data WHERE content_id = ?1") + .prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank, classjob_id, classjob_levels FROM character_data WHERE content_id = ?1") .unwrap(); - let (pos_x, pos_y, pos_z, rotation, zone_id, inventory_json, gm_rank): ( - f32, - f32, - f32, - f32, - u16, - String, - u8, - ) = stmt + let ( + pos_x, + pos_y, + pos_z, + rotation, + zone_id, + inventory_json, + gm_rank, + classjob_id, + classjob_levels, + ): (f32, f32, f32, f32, u16, String, u8, i32, String) = stmt .query_row((content_id,), |row| { Ok(( row.get(0)?, @@ -162,6 +164,8 @@ impl WorldDatabase { row.get(4)?, row.get(5)?, row.get(6)?, + row.get(7)?, + row.get(8)?, )) }) .unwrap(); @@ -181,6 +185,8 @@ impl WorldDatabase { zone_id, inventory, gm_rank: GameMasterRank::try_from(gm_rank).unwrap(), + classjob_id: classjob_id as u8, + classjob_levels: serde_json::from_str(&classjob_levels).unwrap(), ..Default::default() } } @@ -190,7 +196,7 @@ impl WorldDatabase { let connection = self.connection.lock().unwrap(); let mut stmt = connection - .prepare("UPDATE character_data SET zone_id=?1, pos_x=?2, pos_y=?3, pos_z=?4, rotation=?5, inventory=?6 WHERE content_id = ?7") + .prepare("UPDATE character_data SET zone_id=?1, pos_x=?2, pos_y=?3, pos_z=?4, rotation=?5, inventory=?6, classjob_id=?7, classjob_levels=?8 WHERE content_id = ?9") .unwrap(); stmt.execute(( data.zone_id, @@ -199,6 +205,8 @@ impl WorldDatabase { data.position.z, data.rotation, serde_json::to_string(&data.inventory).unwrap(), + data.classjob_id, + serde_json::to_string(&data.classjob_levels).unwrap(), data.content_id, )) .unwrap(); @@ -247,30 +255,41 @@ impl WorldDatabase { for (index, (content_id, actor_id)) in content_actor_ids.iter().enumerate() { let mut stmt = connection .prepare( - "SELECT name, chara_make, zone_id, inventory, remake_mode FROM character_data WHERE content_id = ?1", + "SELECT name, chara_make, zone_id, inventory, remake_mode, classjob_id, classjob_levels FROM character_data WHERE content_id = ?1", ) .unwrap(); - let result: Result<(String, String, u16, String, i32), rusqlite::Error> = stmt - .query_row((content_id,), |row| { + let result: Result<(String, String, u16, String, i32, i32, String), rusqlite::Error> = + stmt.query_row((content_id,), |row| { Ok(( row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, + row.get(5)?, + row.get(6)?, )) }); - if let Ok((name, chara_make, zone_id, inventory_json, remake_mode)) = result { + if let Ok(( + name, + chara_make, + zone_id, + inventory_json, + remake_mode, + classjob_id, + classjob_levels, + )) = result + { let chara_make = CharaMake::from_json(&chara_make); let inventory: Inventory = serde_json::from_str(&inventory_json).unwrap(); let select_data = ClientSelectData { character_name: name.clone(), - current_class: 2, - class_levels: [5; 32], + current_class: classjob_id, + class_levels: serde_json::from_str(&classjob_levels).unwrap(), race: chara_make.customize.race as i32, subrace: chara_make.customize.subrace as i32, gender: chara_make.customize.gender as i32, @@ -331,7 +350,7 @@ impl WorldDatabase { &self, service_account_id: u32, name: &str, - chara_make: &str, + chara_make_str: &str, city_state: u8, zone_id: u16, inventory: Inventory, @@ -341,6 +360,11 @@ impl WorldDatabase { let connection = self.connection.lock().unwrap(); + // fill out the initial classjob + let chara_make = CharaMake::from_json(chara_make_str); + let mut classjob_levels = [0i32; 32]; + classjob_levels[chara_make.classjob_id as usize] = 1; // inital level + // insert ids connection .execute( @@ -352,14 +376,16 @@ impl WorldDatabase { // insert char data connection .execute( - "INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90);", + "INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90, ?7, ?8);", ( content_id, name, - chara_make, + chara_make_str, city_state, zone_id, serde_json::to_string(&inventory).unwrap(), + chara_make.classjob_id, + serde_json::to_string(&classjob_levels).unwrap(), ), ) .unwrap();