1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-23 21:17:45 +00:00

Store various content unlock & clear flags in database

I ended up sticking all of the unlock-relate information in a JSON
object, instead of putting in more columns.
This commit is contained in:
Joshua Goins 2025-07-18 19:25:14 -04:00
parent 7d3deb8685
commit 234e804953
4 changed files with 157 additions and 52 deletions

View file

@ -315,8 +315,18 @@ async fn client_loop(
current_class: current_class as u8,
current_job: connection.player_data.classjob_id,
levels: connection.player_data.classjob_levels.map(|x| x as u16),
unlocks: connection.player_data.unlocks.clone(),
aetherytes: connection.player_data.aetherytes.clone(),
unlocks: connection.player_data.unlocks.unlocks.clone(),
aetherytes: connection.player_data.unlocks.aetherytes.clone(),
unlocked_raids: connection.player_data.unlocks.unlocked_raids.clone().into(),
unlocked_dungeons: connection.player_data.unlocks.unlocked_dungeons.clone(),
unlocked_guildhests: connection.player_data.unlocks.unlocked_guildhests.clone(),
unlocked_trials: connection.player_data.unlocks.unlocked_trials.clone(),
unlocked_pvp: connection.player_data.unlocks.unlocked_pvp.clone(),
cleared_raids: connection.player_data.unlocks.cleared_raids.clone(),
cleared_dungeons: connection.player_data.unlocks.cleared_dungeons.clone(),
cleared_guildhests: connection.player_data.unlocks.cleared_guildhests.clone(),
cleared_trials: connection.player_data.unlocks.cleared_trials.clone(),
cleared_pvp: connection.player_data.unlocks.cleared_pvp.clone(),
..Default::default()
}),
..Default::default()

View file

@ -156,16 +156,46 @@ pub struct PlayerStatus {
#[br(count = 141)]
#[bw(pad_size_to = 141)]
pub unknown948: Vec<u8>,
pub unlocked_raids: [u8; RAID_ARRAY_SIZE],
pub unlocked_dungeons: [u8; DUNGEON_ARRAY_SIZE],
pub unlocked_guildhests: [u8; GUILDHEST_ARRAY_SIZE],
pub unlocked_trials: [u8; TRIAL_ARRAY_SIZE],
pub unlocked_pvp: [u8; PVP_ARRAY_SIZE],
pub cleared_raids: [u8; RAID_ARRAY_SIZE],
pub cleared_dungeons: [u8; DUNGEON_ARRAY_SIZE],
pub cleared_guildhests: [u8; GUILDHEST_ARRAY_SIZE],
pub cleared_trials: [u8; TRIAL_ARRAY_SIZE],
pub cleared_pvp: [u8; PVP_ARRAY_SIZE],
#[br(count = RAID_ARRAY_SIZE)]
#[bw(pad_size_to = RAID_ARRAY_SIZE)]
pub unlocked_raids: Vec<u8>,
#[br(count = DUNGEON_ARRAY_SIZE)]
#[bw(pad_size_to = DUNGEON_ARRAY_SIZE)]
pub unlocked_dungeons: Vec<u8>,
#[br(count = GUILDHEST_ARRAY_SIZE)]
#[bw(pad_size_to = GUILDHEST_ARRAY_SIZE)]
pub unlocked_guildhests: Vec<u8>,
#[br(count = TRIAL_ARRAY_SIZE)]
#[bw(pad_size_to = TRIAL_ARRAY_SIZE)]
pub unlocked_trials: Vec<u8>,
#[br(count = PVP_ARRAY_SIZE)]
#[bw(pad_size_to = PVP_ARRAY_SIZE)]
pub unlocked_pvp: Vec<u8>,
#[br(count = RAID_ARRAY_SIZE)]
#[bw(pad_size_to = RAID_ARRAY_SIZE)]
pub cleared_raids: Vec<u8>,
#[br(count = DUNGEON_ARRAY_SIZE)]
#[bw(pad_size_to = DUNGEON_ARRAY_SIZE)]
pub cleared_dungeons: Vec<u8>,
#[br(count = GUILDHEST_ARRAY_SIZE)]
#[bw(pad_size_to = GUILDHEST_ARRAY_SIZE)]
pub cleared_guildhests: Vec<u8>,
#[br(count = TRIAL_ARRAY_SIZE)]
#[bw(pad_size_to = TRIAL_ARRAY_SIZE)]
pub cleared_trials: Vec<u8>,
#[br(count = PVP_ARRAY_SIZE)]
#[bw(pad_size_to = PVP_ARRAY_SIZE)]
pub cleared_pvp: Vec<u8>,
#[br(count = 16)]
#[bw(pad_size_to = 16)]

View file

@ -6,11 +6,14 @@ use std::{
};
use mlua::Function;
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use crate::{
CLASSJOB_ARRAY_SIZE, COMPLETED_LEVEQUEST_BITMASK_SIZE, COMPLETED_QUEST_BITMASK_SIZE,
ERR_INVENTORY_ADD_FAILED, LogMessageType,
AETHERYTE_UNLOCK_BITMASK_SIZE, CLASSJOB_ARRAY_SIZE, COMPLETED_LEVEQUEST_BITMASK_SIZE,
COMPLETED_QUEST_BITMASK_SIZE, DUNGEON_ARRAY_SIZE, ERR_INVENTORY_ADD_FAILED,
GUILDHEST_ARRAY_SIZE, LogMessageType, PVP_ARRAY_SIZE, RAID_ARRAY_SIZE, TRIAL_ARRAY_SIZE,
UNLOCK_BITMASK_SIZE,
common::{
GameData, INVALID_OBJECT_ID, ItemInfoQuery, ObjectId, ObjectTypeId, Position,
timestamp_secs, value_to_flag_byte_index_value,
@ -57,6 +60,43 @@ pub struct TeleportQuery {
pub aetheryte_id: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnlockData {
pub unlocks: Vec<u8>,
pub aetherytes: Vec<u8>,
pub completed_quests: Vec<u8>,
pub unlocked_raids: Vec<u8>,
pub unlocked_dungeons: Vec<u8>,
pub unlocked_guildhests: Vec<u8>,
pub unlocked_trials: Vec<u8>,
pub unlocked_pvp: Vec<u8>,
pub cleared_raids: Vec<u8>,
pub cleared_dungeons: Vec<u8>,
pub cleared_guildhests: Vec<u8>,
pub cleared_trials: Vec<u8>,
pub cleared_pvp: Vec<u8>,
}
impl Default for UnlockData {
fn default() -> Self {
Self {
unlocks: vec![0x0; UNLOCK_BITMASK_SIZE],
aetherytes: vec![0x0; AETHERYTE_UNLOCK_BITMASK_SIZE],
completed_quests: vec![0x0; COMPLETED_QUEST_BITMASK_SIZE],
unlocked_raids: vec![0x0; RAID_ARRAY_SIZE],
unlocked_dungeons: vec![0x0; DUNGEON_ARRAY_SIZE],
unlocked_guildhests: vec![0x0; GUILDHEST_ARRAY_SIZE],
unlocked_trials: vec![0x0; TRIAL_ARRAY_SIZE],
unlocked_pvp: vec![0x0; PVP_ARRAY_SIZE],
cleared_raids: vec![0x0; RAID_ARRAY_SIZE],
cleared_dungeons: vec![0x0; DUNGEON_ARRAY_SIZE],
cleared_guildhests: vec![0x0; GUILDHEST_ARRAY_SIZE],
cleared_trials: vec![0x0; TRIAL_ARRAY_SIZE],
cleared_pvp: vec![0x0; PVP_ARRAY_SIZE],
}
}
}
#[derive(Debug, Default, Clone)]
pub struct PlayerData {
// Static data
@ -83,15 +123,13 @@ pub struct PlayerData {
pub gm_rank: GameMasterRank,
pub gm_invisible: bool,
pub unlocks: Vec<u8>,
pub aetherytes: Vec<u8>,
pub completed_quests: Vec<u8>,
pub item_sequence: u32,
pub shop_sequence: u32,
/// Store the target actor id for the purpose of chaining cutscenes.
pub target_actorid: ObjectTypeId,
/// The server-side copy of NPC shop buyback lists.
pub buyback_list: BuyBackList,
pub unlocks: UnlockData,
}
/// Various obsfucation-related bits like the seeds and keys for this connection.
@ -835,7 +873,7 @@ impl ZoneConnection {
}
Task::Unlock { id } => {
let (value, index) = value_to_flag_byte_index_value(*id);
self.player_data.unlocks[index as usize] |= value;
self.player_data.unlocks.unlocks[index as usize] |= value;
self.actor_control_self(ActorControlSelf {
category: ActorControlCategory::ToggleUnlock {
@ -851,9 +889,9 @@ impl ZoneConnection {
for i in 1..239 {
let (value, index) = value_to_flag_byte_index_value(i);
if *on {
self.player_data.aetherytes[index as usize] |= value;
self.player_data.unlocks.aetherytes[index as usize] |= value;
} else {
self.player_data.aetherytes[index as usize] ^= value;
self.player_data.unlocks.aetherytes[index as usize] ^= value;
}
/* Unknown if this will make the server panic from a flood of packets.
@ -869,9 +907,9 @@ impl ZoneConnection {
} else {
let (value, index) = value_to_flag_byte_index_value(*id);
if *on {
self.player_data.aetherytes[index as usize] |= value;
self.player_data.unlocks.aetherytes[index as usize] |= value;
} else {
self.player_data.aetherytes[index as usize] ^= value;
self.player_data.unlocks.aetherytes[index as usize] ^= value;
}
self.actor_control_self(ActorControlSelf {
@ -955,7 +993,8 @@ impl ZoneConnection {
}
}
Task::CompleteAllQuests {} => {
self.player_data.completed_quests = vec![0xFF; COMPLETED_QUEST_BITMASK_SIZE];
self.player_data.unlocks.completed_quests =
vec![0xFF; COMPLETED_QUEST_BITMASK_SIZE];
self.send_quest_information().await;
}
Task::UnlockContent { id } => {
@ -1637,7 +1676,7 @@ impl ZoneConnection {
op_code: ServerZoneIpcType::QuestCompleteList,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::QuestCompleteList {
completed_quests: self.player_data.completed_quests.clone(),
completed_quests: self.player_data.unlocks.completed_quests.clone(),
unk2: vec![0xFF; 69],
},
..Default::default()

View file

@ -4,8 +4,7 @@ use rusqlite::Connection;
use serde::Deserialize;
use crate::{
AETHERYTE_UNLOCK_BITMASK_SIZE, CLASSJOB_ARRAY_SIZE, COMPLETED_QUEST_BITMASK_SIZE,
UNLOCK_BITMASK_SIZE,
CLASSJOB_ARRAY_SIZE,
common::{
CustomizeData, GameData, ItemInfoQuery, Position,
workdefinitions::{CharaMake, ClientSelectData, RemakeMode},
@ -14,7 +13,7 @@ use crate::{
ipc::lobby::{CharacterDetails, CharacterFlag},
};
use super::PlayerData;
use super::{PlayerData, connection::UnlockData};
pub struct WorldDatabase {
connection: Mutex<Connection>,
@ -50,7 +49,23 @@ 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, classjob_exp STRING, unlocks STRING, aetherytes STRING, completed_quests 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,
classjob_exp STRING,
unlocks STRING);";
connection.execute(query, ()).unwrap();
}
@ -271,8 +286,8 @@ impl WorldDatabase {
);
// import unlock flags
player_data.unlocks = character.unlock_flags;
player_data.aetherytes = character.unlock_aetherytes;
player_data.unlocks.unlocks = character.unlock_flags;
player_data.unlocks.aetherytes = character.unlock_aetherytes;
self.commit_player_data(&player_data);
@ -290,7 +305,20 @@ impl WorldDatabase {
.unwrap();
stmt = connection
.prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank, classjob_id, classjob_levels, classjob_exp, unlocks, aetherytes, completed_quests 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,
classjob_exp,
unlocks
FROM character_data WHERE content_id = ?1",
)
.unwrap();
let mut player_data: PlayerData = stmt
.query_row((content_id,), |row| {
@ -310,9 +338,7 @@ impl WorldDatabase {
classjob_id: row.get(7)?,
classjob_levels: json_unpack::<[i32; CLASSJOB_ARRAY_SIZE]>(row.get(8)?),
classjob_exp: json_unpack::<[u32; CLASSJOB_ARRAY_SIZE]>(row.get(9)?),
unlocks: json_unpack::<Vec<u8>>(row.get(10)?),
aetherytes: json_unpack::<Vec<u8>>(row.get(11)?),
completed_quests: json_unpack::<Vec<u8>>(row.get(12)?),
unlocks: json_unpack(row.get(10)?),
..Default::default()
})
})
@ -367,9 +393,22 @@ 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, classjob_exp=?9, unlocks=?10, aetherytes=?11, completed_quests=?12 WHERE content_id = ?13")
.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,
classjob_exp=?9,
unlocks=?10
WHERE content_id = ?11",
)
.unwrap();
stmt.execute((
stmt.execute(rusqlite::params![
data.zone_id,
data.position.x,
data.position.y,
@ -380,10 +419,8 @@ impl WorldDatabase {
serde_json::to_string(&data.classjob_levels).unwrap(),
serde_json::to_string(&data.classjob_exp).unwrap(),
serde_json::to_string(&data.unlocks).unwrap(),
serde_json::to_string(&data.aetherytes).unwrap(),
serde_json::to_string(&data.completed_quests).unwrap(),
data.content_id,
))
])
.unwrap();
}
@ -547,15 +584,6 @@ impl WorldDatabase {
let classjob_exp = [0u32; CLASSJOB_ARRAY_SIZE];
// fill out initial unlocks
let unlocks = vec![0u8; UNLOCK_BITMASK_SIZE];
// fill out initial aetherytes
let aetherytes = vec![0u8; AETHERYTE_UNLOCK_BITMASK_SIZE];
// fill out initial completed quests`
let completed_quests = vec![0u8; COMPLETED_QUEST_BITMASK_SIZE];
// insert ids
connection
.execute(
@ -567,7 +595,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, ?9, ?10, ?11, ?12);",
"INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90, ?7, ?8, ?9, ?10);",
(
content_id,
name,
@ -578,9 +606,7 @@ impl WorldDatabase {
chara_make.classjob_id,
serde_json::to_string(&classjob_levels).unwrap(),
serde_json::to_string(&classjob_exp).unwrap(),
serde_json::to_string(&unlocks).unwrap(),
serde_json::to_string(&aetherytes).unwrap(),
serde_json::to_string(&completed_quests).unwrap(),
serde_json::to_string(&UnlockData::default()).unwrap(),
),
)
.unwrap();