Skip to content

๐Ÿ’พ 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>.yml

One file per player, named by UUID. PlayerProfileStore uses a ConcurrentHashMap as an in-memory cache, indexed by UUID.


2. PlayerSkillProfile fields โ€‹

kotlin
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
)
FieldMeaningPersisted?
unlockedSet of unlocked skillsโœ…
levelsPer-skill levels (default 1, floor 1)โœ…
activeSlotsSkill slot โ†’ skill idโœ… (on-disk key slots)
cooldownUntilSkill โ†’ cooldown-until timestampโœ… (on-disk key cooldowns, expired entries auto-filtered)
resourcesResource pools (mana, etc.)โœ…
variablesPersisted variablesโœ…
castModeOverridesCast 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):

yaml
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: 1718600000000

Key points:

  • activeSlots is written out as slots.<slot>; cooldownUntil is written out as cooldowns.<skill>.
  • Cooldowns only write still-unexpired entries (save does filter { it.value > now }), and expired entries are filtered again on load โ€” so cooldowns are protected against "relog reset" (detailed below).
  • castModeOverrides is currently not in the save write-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 โ€‹

TriggerNotes
Player quitPlayerLifecycleListener saves on quit and unloads from cache
API changeAfter unlock / lock / setLevel / setSlot, save immediately
unload(uuid)Saves once before removing from cache

Changes via QinhSkillsAPI.unlock/lock/setLevel/setSlot write to disk synchronously. Changes made directly to the PlayerSkillProfile object (not via the API) aren't persisted until the player quits or save is called explicitly.


5. Default profile โ€‹

A new player (no file) gets a default profile on first load, seeded with just one resource:

kotlin
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) โ€‹

StateStored whereAfter relog
Cooldowns (cooldownUntil)โœ… on diskKept (by timestamp, prevents relog refresh)
Unlocks / levels / slots / resources / variablesโœ… on diskKept
Charge count (ChargeTracker)โŒ in memoryReset
Toggle on/off state (ToggleTracker)โŒ in memoryReset

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

kotlin
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 QinhSkillsAPI methods that operate on profiles
  • Placeholders โ€” profile data exposed through placeholders
  • Diagnostics & Protocol โ€” how to troubleshoot when cooldowns/charges are wrong