1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-06-30 11:47:45 +00:00

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.
This commit is contained in:
Joshua Goins 2025-06-27 23:01:00 -04:00
parent 1fa861a35a
commit 0b470dc28b
7 changed files with 52 additions and 27 deletions

View file

@ -1,5 +1,5 @@
required_rank = GM_RANK_DEBUG required_rank = GM_RANK_DEBUG
command_sender = "[unlockaction] " command_sender = "[unlock] "
function onCommand(args, player) function onCommand(args, player)
local parts = split(args) local parts = split(args)
@ -14,18 +14,18 @@ function onCommand(args, player)
if parts[1] == "all" then if parts[1] == "all" then
for i = 0, 1000, 1 do for i = 0, 1000, 1 do
player:unlock_action(i) player:unlock(i)
end end
printf(player, "Everything is unlocked!", id) printf(player, "Everything is unlocked!", id)
else else
local id = tonumber(parts[1]) local id = tonumber(parts[1])
if not id then 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 return
end end
player:unlock_action(id) player:unlock(id)
printf(player, "Action %s unlocked!", id) printf(player, "%s unlocked!", id)
end end
end end

View file

@ -293,6 +293,7 @@ async fn client_loop(
current_class: connection.player_data.classjob_id, current_class: connection.player_data.classjob_id,
current_job: connection.player_data.classjob_id, current_job: connection.player_data.classjob_id,
levels: connection.player_data.classjob_levels.map(|x| x as u16), levels: connection.player_data.classjob_levels.map(|x| x as u16),
unlocks: connection.player_data.unlocks.clone(),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()

View file

@ -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 struct Attributes {
pub strength: u32, pub strength: u32,
pub dexterity: u32, pub dexterity: u32,

View file

@ -79,19 +79,18 @@ pub struct PlayerStatus {
#[bw(pad_size_to = 33)] #[bw(pad_size_to = 33)]
pub mount_guide_mask: Vec<u8>, pub mount_guide_mask: Vec<u8>,
pub ornament_mask: [u8; 4], pub ornament_mask: [u8; 4],
#[br(count = 85)] #[br(count = 95)]
#[bw(pad_size_to = 85)] #[bw(pad_size_to = 95)]
pub unknown281: Vec<u8>, pub unknown281: Vec<u8>,
#[br(count = CHAR_NAME_MAX_LENGTH)] #[br(count = CHAR_NAME_MAX_LENGTH)]
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)] #[br(map = read_string)]
#[bw(map = write_string)] #[bw(map = write_string)]
pub name: String, pub name: String,
pub unknown293: [u8; 16], pub unknown293: [u8; 32],
pub unknown2a3: u8,
#[br(count = 64)] #[br(count = 64)]
#[bw(pad_size_to = 64)] #[bw(pad_size_to = 64)]
pub unlock_bitmask: Vec<u8>, pub unlocks: Vec<u8>,
pub aetheryte: [u8; 26], pub aetheryte: [u8; 26],
pub favorite_aetheryte_ids: [u16; 4], pub favorite_aetheryte_ids: [u16; 4],
pub free_aetheryte_id: u16, pub free_aetheryte_id: u16,
@ -159,8 +158,8 @@ pub struct PlayerStatus {
pub cleared_pvp: [u8; 5], pub cleared_pvp: [u8; 5],
// meh, this is where i put all of the new data // meh, this is where i put all of the new data
#[br(count = 216)] #[br(count = 191)]
#[bw(pad_size_to = 216)] #[bw(pad_size_to = 191)]
pub unknown948: Vec<u8>, pub unknown948: Vec<u8>,
} }

View file

@ -10,7 +10,9 @@ use tokio::net::TcpStream;
use crate::{ use crate::{
OBFUSCATION_ENABLED_MODE, 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}, config::{WorldConfig, get_config},
inventory::{ContainerType, Inventory, Item}, inventory::{ContainerType, Inventory, Item},
ipc::{ ipc::{
@ -76,6 +78,8 @@ pub struct PlayerData {
pub teleport_query: TeleportQuery, pub teleport_query: TeleportQuery,
pub gm_rank: GameMasterRank, pub gm_rank: GameMasterRank,
pub gm_invisible: bool, pub gm_invisible: bool,
pub unlocks: Vec<u8>,
} }
impl PlayerData { impl PlayerData {
@ -674,6 +678,18 @@ impl ZoneConnection {
Task::ToggleInvisibility { invisible } => { Task::ToggleInvisibility { invisible } => {
self.toggle_invisibility(*invisible).await; 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(); player.queued_tasks.clear();

View file

@ -47,7 +47,7 @@ impl WorldDatabase {
// Create characters data table // 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(); connection.execute(query, ()).unwrap();
} }
@ -140,7 +140,7 @@ impl WorldDatabase {
.unwrap(); .unwrap();
stmt = connection 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(); .unwrap();
let ( let (
pos_x, pos_x,
@ -152,7 +152,8 @@ impl WorldDatabase {
gm_rank, gm_rank,
classjob_id, classjob_id,
classjob_levels, 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| { .query_row((content_id,), |row| {
Ok(( Ok((
row.get(0)?, row.get(0)?,
@ -164,6 +165,7 @@ impl WorldDatabase {
row.get(6)?, row.get(6)?,
row.get(7)?, row.get(7)?,
row.get(8)?, row.get(8)?,
row.get(9)?,
)) ))
}) })
.unwrap(); .unwrap();
@ -185,6 +187,7 @@ impl WorldDatabase {
gm_rank: GameMasterRank::try_from(gm_rank).unwrap(), gm_rank: GameMasterRank::try_from(gm_rank).unwrap(),
classjob_id: classjob_id as u8, classjob_id: classjob_id as u8,
classjob_levels: serde_json::from_str(&classjob_levels).unwrap(), classjob_levels: serde_json::from_str(&classjob_levels).unwrap(),
unlocks: serde_json::from_str(&unlocks).unwrap(),
..Default::default() ..Default::default()
} }
} }
@ -194,7 +197,7 @@ impl WorldDatabase {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let mut stmt = connection 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(); .unwrap();
stmt.execute(( stmt.execute((
data.zone_id, data.zone_id,
@ -205,6 +208,7 @@ impl WorldDatabase {
serde_json::to_string(&data.inventory).unwrap(), serde_json::to_string(&data.inventory).unwrap(),
data.classjob_id, data.classjob_id,
serde_json::to_string(&data.classjob_levels).unwrap(), serde_json::to_string(&data.classjob_levels).unwrap(),
serde_json::to_string(&data.unlocks).unwrap(),
data.content_id, data.content_id,
)) ))
.unwrap(); .unwrap();
@ -363,6 +367,9 @@ impl WorldDatabase {
let mut classjob_levels = [0i32; 32]; let mut classjob_levels = [0i32; 32];
classjob_levels[chara_make.classjob_id as usize] = 1; // inital level classjob_levels[chara_make.classjob_id as usize] = 1; // inital level
// fill out initial unlocks
let unlocks = vec![0u8; 64];
// insert ids // insert ids
connection connection
.execute( .execute(
@ -374,7 +381,7 @@ impl WorldDatabase {
// insert char data // insert char data
connection connection
.execute( .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, content_id,
name, name,
@ -384,6 +391,7 @@ impl WorldDatabase {
serde_json::to_string(&inventory).unwrap(), serde_json::to_string(&inventory).unwrap(),
chara_make.classjob_id, chara_make.classjob_id,
serde_json::to_string(&classjob_levels).unwrap(), serde_json::to_string(&classjob_levels).unwrap(),
serde_json::to_string(&unlocks).unwrap(),
), ),
) )
.unwrap(); .unwrap();

View file

@ -27,6 +27,7 @@ pub enum Task {
WarpAetheryte { aetheryte_id: u32 }, WarpAetheryte { aetheryte_id: u32 },
ReloadScripts, ReloadScripts,
ToggleInvisibility { invisible: bool }, ToggleInvisibility { invisible: bool },
Unlock { id: u32 },
} }
#[derive(Default)] #[derive(Default)]
@ -135,13 +136,8 @@ impl LuaPlayer {
self.create_segment_self(op_code, data); self.create_segment_self(op_code, data);
} }
fn unlock_action(&mut self, id: u32) { fn unlock(&mut self, id: u32) {
let op_code = ServerZoneIpcType::ActorControlSelf; self.queued_tasks.push(Task::Unlock { id });
let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::ToggleUnlock { id, unlocked: true },
});
self.create_segment_self(op_code, data);
} }
fn set_speed(&mut self, speed: u16) { fn set_speed(&mut self, speed: u16) {
@ -289,8 +285,8 @@ impl UserData for LuaPlayer {
this.unlock_aetheryte(unlock, id); this.unlock_aetheryte(unlock, id);
Ok(()) Ok(())
}); });
methods.add_method_mut("unlock_action", |_, this, action_id: u32| { methods.add_method_mut("unlock", |_, this, action_id: u32| {
this.unlock_action(action_id); this.unlock(action_id);
Ok(()) Ok(())
}); });
methods.add_method_mut("set_speed", |_, this, speed: u16| { methods.add_method_mut("set_speed", |_, this, speed: u16| {