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
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

View file

@ -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()

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 strength: u32,
pub dexterity: u32,

View file

@ -79,19 +79,18 @@ pub struct PlayerStatus {
#[bw(pad_size_to = 33)]
pub mount_guide_mask: Vec<u8>,
pub ornament_mask: [u8; 4],
#[br(count = 85)]
#[bw(pad_size_to = 85)]
#[br(count = 95)]
#[bw(pad_size_to = 95)]
pub unknown281: Vec<u8>,
#[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<u8>,
pub unlocks: Vec<u8>,
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<u8>,
}

View file

@ -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<u8>,
}
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();

View file

@ -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();

View file

@ -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| {