๐พ Data Storage โ
Previous: Diagnostics & ProtocolใยทใNext: Glossary
QS persists each player's skill profile to disk. This chapter covers what PlayerSkillProfile stores, what the on-disk YAML looks like, when it loads/saves, and which state is persisted and which lives only in memory. This document targets QS 1.0.22.
Responsibility boundary in one line: QS owns only the "skill profile" resource seam (
PlayerSkillProfile). Player class, player level, mana pool, CDR, and player attributes all belong to QinhClass.
1. On-disk location โ
plugins/QinhSkills/players/<UUID>.ymlOne file per player, named by UUID. PlayerProfileStore uses a ConcurrentHashMap as an in-memory cache, indexed by UUID.
2. PlayerSkillProfile fields โ
data class PlayerSkillProfile(
val uuid: UUID,
val unlocked: MutableSet<String>, // unlocked skill ids
val levels: MutableMap<String, Int>, // skill โ level
val activeSlots: MutableMap<Int, String>, // slot โ skill id
val cooldownUntil: MutableMap<String, Long>, // skill โ cooldown-until ms
val resources: MutableMap<String, Double>, // resource pools, e.g. mana
val castModeOverrides: MutableMap<String, String>,
val variables: MutableMap<String, String>, // persisted skill variables
)| Field | Meaning | Persisted? |
|---|---|---|
unlocked | Set of unlocked skills | โ |
levels | Per-skill levels (default 1, floor 1) | โ |
activeSlots | Skill slot โ skill id | โ
(on-disk key slots) |
cooldownUntil | Skill โ cooldown-until timestamp | โ
(on-disk key cooldowns, expired entries auto-filtered) |
resources | Resource pools (mana, etc.) | โ |
variables | Persisted variables | โ |
castModeOverrides | Cast mode overrides | โ ๏ธ In-memory (current save doesn't write to disk) |
All skill ids are lowercased inside the profile (
unlock/getLevel/setSlot, etc. all.lowercase()).
3. On-disk YAML structure โ
The real structure PlayerProfileStore.save writes out (note the key names):
unlocked:
- fire_wave
- dash
levels:
fire_wave: 3
dash: 1
slots: # from activeSlots
'1': fire_wave
'2': dash
resources:
mana: 85.0
variables:
some_key: some_value
cooldowns: # from cooldownUntil, only writes still-unexpired entries
fire_wave: 1718600000000Key points:
activeSlotsis written out asslots.<slot>;cooldownUntilis written out ascooldowns.<skill>.- Cooldowns only write still-unexpired entries (
savedoesfilter { it.value > now }), and expired entries are filtered again on load โ so cooldowns are protected against "relog reset" (detailed below). castModeOverridesis currently not in thesavewrite-out list; it's in-memory state.
4. Load and save timing โ
Load โ
- A player's profile is lazy-loaded on first access (
get(uuid)โcomputeIfAbsent { load(uuid) }). - File doesn't exist โ generate a default profile (see below).
Save โ
| Trigger | Notes |
|---|---|
| Player quit | PlayerLifecycleListener saves on quit and unloads from cache |
| API change | After unlock / lock / setLevel / setSlot, save immediately |
unload(uuid) | Saves once before removing from cache |
Changes via
QinhSkillsAPI.unlock/lock/setLevel/setSlotwrite to disk synchronously. Changes made directly to thePlayerSkillProfileobject (not via the API) aren't persisted until the player quits orsaveis called explicitly.
5. Default profile โ
A new player (no file) gets a default profile on first load, seeded with just one resource:
profile.resources["mana"] = config.getDouble("resources.default_mana", 100.0)That is, mana = resources.default_mana by default (100 if unset). All other collections are empty (no unlocks, no level overrides, no cooldowns).
6. Persisted vs in-memory state (important) โ
| State | Stored where | After relog |
|---|---|---|
| Cooldowns (cooldownUntil) | โ on disk | Kept (by timestamp, prevents relog refresh) |
| Unlocks / levels / slots / resources / variables | โ on disk | Kept |
| Charge count (ChargeTracker) | โ in memory | Reset |
| Toggle on/off state (ToggleTracker) | โ in memory | Reset |
Design trade-offs:
- Cooldowns are persisted, specifically to prevent the "relog to refresh cooldowns" exploit.
- Charges / toggles live in memory (
ChargeTracker/ToggleTracker) and zero out on relog. Toggle skills default back to the off state after relog.
7. The resource seam and the QinhClass boundary โ
The mana in resources is a temporary placeholder until QC takes over. Currently QS owns this single resource seam (PlayerSkillProfile.resources):
- Channeling / casting also spends resources through this same seam (it doesn't build a second pool).
- When QinhClass takes over resources, only this one place changes โ the rest of gating, placeholders, and the API don't move.
What QS does not do (all belongs to QinhClass):
- Player class, player level
- The true ownership of the mana / stamina resource pool (QS is just a temporary placeholder)
- Cooldown reduction (CDR) values
- Player attributes
8. Reading/writing profiles programmatically โ
Operate indirectly via QinhSkillsAPI (recommended, auto-saves):
QinhSkillsAPI.unlock(player, "fire_wave") // unlock + save
QinhSkillsAPI.setLevel(player, "fire_wave", 3) // set level + save
QinhSkillsAPI.setSlot(player, 1, "fire_wave") // set slot + save
QinhSkillsAPI.setSlot(player, 1, null) // clear slot + save
val unlocked = QinhSkillsAPI.isUnlocked(player, "fire_wave")These APIs write to disk every time. Watch the I/O frequency when looping over large batches of players.
Further reading โ
- API โ the
QinhSkillsAPImethods that operate on profiles - Placeholders โ profile data exposed through placeholders
- Diagnostics & Protocol โ how to troubleshoot when cooldowns/charges are wrong