From 0b470dc28bd46685e7ea316f0448c15d09ffddae Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 27 Jun 2025 23:01:00 -0400 Subject: [PATCH] Make unlocks persistent, fix player name in chat The PlayerStatus struct shifted around recently, so I fixed the offsets yet again. Unlocks should be persistent now, but this as usual requires a database wipe, sorry! I also included some refactors of the !unlock debug command that still had references to the old unlock_action API. --- resources/scripts/commands/debug/Unlock.lua | 10 +++++----- src/bin/kawari-world.rs | 1 + src/common/mod.rs | 5 +++++ src/ipc/zone/player_setup.rs | 13 ++++++------- src/world/connection.rs | 18 +++++++++++++++++- src/world/database.rs | 18 +++++++++++++----- src/world/lua.rs | 14 +++++--------- 7 files changed, 52 insertions(+), 27 deletions(-) diff --git a/resources/scripts/commands/debug/Unlock.lua b/resources/scripts/commands/debug/Unlock.lua index 356bc65..39154dd 100644 --- a/resources/scripts/commands/debug/Unlock.lua +++ b/resources/scripts/commands/debug/Unlock.lua @@ -1,5 +1,5 @@ required_rank = GM_RANK_DEBUG -command_sender = "[unlockaction] " +command_sender = "[unlock] " function onCommand(args, player) local parts = split(args) @@ -14,18 +14,18 @@ function onCommand(args, player) if parts[1] == "all" then for i = 0, 1000, 1 do - player:unlock_action(i) + player:unlock(i) end printf(player, "Everything is unlocked!", id) else local id = tonumber(parts[1]) if not id then - printf(player, "Error parsing action id! Make sure the id is an integer."..usage) + printf(player, "Error parsing unlock id! Make sure the id is an integer."..usage) return end - player:unlock_action(id) - printf(player, "Action %s unlocked!", id) + player:unlock(id) + printf(player, "%s unlocked!", id) end end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index f37a642..ef04d39 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -293,6 +293,7 @@ async fn client_loop( current_class: connection.player_data.classjob_id, current_job: connection.player_data.classjob_id, levels: connection.player_data.classjob_levels.map(|x| x as u16), + unlocks: connection.player_data.unlocks.clone(), ..Default::default() }), ..Default::default() diff --git a/src/common/mod.rs b/src/common/mod.rs index b011d40..4d223d9 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -147,6 +147,11 @@ pub fn determine_initial_starting_zone(citystate_id: u8) -> u16 { } } +pub fn value_to_flag_byte_index_value(in_value: u32) -> (u8, u16) { + let bit_index = in_value % 8; + (1 << bit_index, (in_value / 8) as u16) +} + pub struct Attributes { pub strength: u32, pub dexterity: u32, diff --git a/src/ipc/zone/player_setup.rs b/src/ipc/zone/player_setup.rs index 16b384a..5b2b324 100644 --- a/src/ipc/zone/player_setup.rs +++ b/src/ipc/zone/player_setup.rs @@ -79,19 +79,18 @@ pub struct PlayerStatus { #[bw(pad_size_to = 33)] pub mount_guide_mask: Vec, pub ornament_mask: [u8; 4], - #[br(count = 85)] - #[bw(pad_size_to = 85)] + #[br(count = 95)] + #[bw(pad_size_to = 95)] pub unknown281: Vec, #[br(count = CHAR_NAME_MAX_LENGTH)] #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] #[br(map = read_string)] #[bw(map = write_string)] pub name: String, - pub unknown293: [u8; 16], - pub unknown2a3: u8, + pub unknown293: [u8; 32], #[br(count = 64)] #[bw(pad_size_to = 64)] - pub unlock_bitmask: Vec, + pub unlocks: Vec, pub aetheryte: [u8; 26], pub favorite_aetheryte_ids: [u16; 4], pub free_aetheryte_id: u16, @@ -159,8 +158,8 @@ pub struct PlayerStatus { pub cleared_pvp: [u8; 5], // meh, this is where i put all of the new data - #[br(count = 216)] - #[bw(pad_size_to = 216)] + #[br(count = 191)] + #[bw(pad_size_to = 191)] pub unknown948: Vec, } diff --git a/src/world/connection.rs b/src/world/connection.rs index 548fbc8..32cc870 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -10,7 +10,9 @@ use tokio::net::TcpStream; use crate::{ OBFUSCATION_ENABLED_MODE, - common::{GameData, ObjectId, ObjectTypeId, Position, timestamp_secs}, + common::{ + GameData, ObjectId, ObjectTypeId, Position, timestamp_secs, value_to_flag_byte_index_value, + }, config::{WorldConfig, get_config}, inventory::{ContainerType, Inventory, Item}, ipc::{ @@ -76,6 +78,8 @@ pub struct PlayerData { pub teleport_query: TeleportQuery, pub gm_rank: GameMasterRank, pub gm_invisible: bool, + + pub unlocks: Vec, } impl PlayerData { @@ -674,6 +678,18 @@ impl ZoneConnection { Task::ToggleInvisibility { invisible } => { self.toggle_invisibility(*invisible).await; } + Task::Unlock { id } => { + let (value, index) = value_to_flag_byte_index_value(*id); + self.player_data.unlocks[index as usize] |= value; + + self.actor_control_self(ActorControlSelf { + category: ActorControlCategory::ToggleUnlock { + id: *id, + unlocked: true, + }, + }) + .await; + } } } player.queued_tasks.clear(); diff --git a/src/world/database.rs b/src/world/database.rs index 44bf7a5..ae33fc9 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, classjob_id INTEGER, classjob_levels STRING);"; + 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, unlocks STRING);"; connection.execute(query, ()).unwrap(); } @@ -140,7 +140,7 @@ impl WorldDatabase { .unwrap(); stmt = connection - .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") + .prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank, classjob_id, classjob_levels, unlocks FROM character_data WHERE content_id = ?1") .unwrap(); let ( pos_x, @@ -152,7 +152,8 @@ impl WorldDatabase { gm_rank, classjob_id, classjob_levels, - ): (f32, f32, f32, f32, u16, String, u8, i32, String) = stmt + unlocks, + ): (f32, f32, f32, f32, u16, String, u8, i32, String, String) = stmt .query_row((content_id,), |row| { Ok(( row.get(0)?, @@ -164,6 +165,7 @@ impl WorldDatabase { row.get(6)?, row.get(7)?, row.get(8)?, + row.get(9)?, )) }) .unwrap(); @@ -185,6 +187,7 @@ impl WorldDatabase { gm_rank: GameMasterRank::try_from(gm_rank).unwrap(), classjob_id: classjob_id as u8, classjob_levels: serde_json::from_str(&classjob_levels).unwrap(), + unlocks: serde_json::from_str(&unlocks).unwrap(), ..Default::default() } } @@ -194,7 +197,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, classjob_id=?7, classjob_levels=?8 WHERE content_id = ?9") + .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, unlocks=?9 WHERE content_id = ?10") .unwrap(); stmt.execute(( data.zone_id, @@ -205,6 +208,7 @@ impl WorldDatabase { serde_json::to_string(&data.inventory).unwrap(), data.classjob_id, serde_json::to_string(&data.classjob_levels).unwrap(), + serde_json::to_string(&data.unlocks).unwrap(), data.content_id, )) .unwrap(); @@ -363,6 +367,9 @@ impl WorldDatabase { let mut classjob_levels = [0i32; 32]; classjob_levels[chara_make.classjob_id as usize] = 1; // inital level + // fill out initial unlocks + let unlocks = vec![0u8; 64]; + // insert ids connection .execute( @@ -374,7 +381,7 @@ 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, ?7, ?8);", + "INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90, ?7, ?8, ?9);", ( content_id, name, @@ -384,6 +391,7 @@ impl WorldDatabase { serde_json::to_string(&inventory).unwrap(), chara_make.classjob_id, serde_json::to_string(&classjob_levels).unwrap(), + serde_json::to_string(&unlocks).unwrap(), ), ) .unwrap(); diff --git a/src/world/lua.rs b/src/world/lua.rs index feff455..c73dfa3 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -27,6 +27,7 @@ pub enum Task { WarpAetheryte { aetheryte_id: u32 }, ReloadScripts, ToggleInvisibility { invisible: bool }, + Unlock { id: u32 }, } #[derive(Default)] @@ -135,13 +136,8 @@ impl LuaPlayer { self.create_segment_self(op_code, data); } - fn unlock_action(&mut self, id: u32) { - let op_code = ServerZoneIpcType::ActorControlSelf; - let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf { - category: ActorControlCategory::ToggleUnlock { id, unlocked: true }, - }); - - self.create_segment_self(op_code, data); + fn unlock(&mut self, id: u32) { + self.queued_tasks.push(Task::Unlock { id }); } fn set_speed(&mut self, speed: u16) { @@ -289,8 +285,8 @@ impl UserData for LuaPlayer { this.unlock_aetheryte(unlock, id); Ok(()) }); - methods.add_method_mut("unlock_action", |_, this, action_id: u32| { - this.unlock_action(action_id); + methods.add_method_mut("unlock", |_, this, action_id: u32| { + this.unlock(action_id); Ok(()) }); methods.add_method_mut("set_speed", |_, this, speed: u16| {