💾 数据存储
QS 把每个玩家的技能档案持久化到磁盘。本章讲 PlayerSkillProfile 存了什么、落盘的 YAML 长什么样、何时加载/保存、以及哪些状态持久化、哪些只在内存。本文档对应 QS 1.0.22。
一句话职责边界:QS 只拥有"技能档案"这一条资源缝(
PlayerSkillProfile)。玩家职业、玩家等级、法力池、CDR、玩家属性都归 QinhClass。
1. 落盘位置
plugins/QinhSkills/players/<UUID>.yml每个玩家一个文件,按 UUID 命名。PlayerProfileStore 用 ConcurrentHashMap 做内存缓存,按 UUID 索引。
2. PlayerSkillProfile 字段
kotlin
data class PlayerSkillProfile(
val uuid: UUID,
val unlocked: MutableSet<String>, // 已解锁技能 id
val levels: MutableMap<String, Int>, // 技能 → 等级
val activeSlots: MutableMap<Int, String>, // 槽位 → 技能 id
val cooldownUntil: MutableMap<String, Long>, // 技能 → 冷却截止 ms
val resources: MutableMap<String, Double>, // 资源池,如 mana
val castModeOverrides: MutableMap<String, String>,
val variables: MutableMap<String, String>, // 持久化技能变量
)| 字段 | 含义 | 持久化? |
|---|---|---|
unlocked | 已解锁技能集合 | ✅ |
levels | 各技能等级(默认 1,下限 1) | ✅ |
activeSlots | 技能槽 → 技能 id | ✅(落盘键 slots) |
cooldownUntil | 技能 → 冷却截止时间戳 | ✅(落盘键 cooldowns,过期自动过滤) |
resources | 资源池(mana 等) | ✅ |
variables | 持久化变量 | ✅ |
castModeOverrides | 施法模式覆盖 | ⚠️ 内存(当前 save 未写盘) |
所有技能 id 在档案内部统一小写(
unlock/getLevel/setSlot等都.lowercase())。
3. 落盘 YAML 结构
PlayerProfileStore.save 写出的真实结构(注意键名):
yaml
unlocked:
- fire_wave
- dash
levels:
fire_wave: 3
dash: 1
slots: # 来自 activeSlots
'1': fire_wave
'2': dash
resources:
mana: 85.0
variables:
some_key: some_value
cooldowns: # 来自 cooldownUntil,仅写仍未过期的
fire_wave: 1718600000000要点:
activeSlots落盘成slots.<槽位>;cooldownUntil落盘成cooldowns.<技能>。- 冷却只写仍未过期的项(
save时filter { it.value > now }),加载时再次过滤过期项 —— 因此冷却可防"重登重置"(下面详述)。 castModeOverrides当前不在save写出列表中,属内存态。
4. 加载与保存时机
加载
- 玩家档案在首次访问时懒加载(
get(uuid)→computeIfAbsent { load(uuid) })。 - 文件不存在 → 生成默认档案(见下)。
保存
| 触发 | 说明 |
|---|---|
| 玩家退出 | PlayerLifecycleListener 在 quit 时保存并卸载缓存 |
| API 变更 | unlock / lock / setLevel / setSlot 调用后立即 save |
unload(uuid) | 从缓存移除前保存一次 |
经
QinhSkillsAPI.unlock/lock/setLevel/setSlot改动会同步写盘。直接改PlayerSkillProfile对象(不经 API)的变更,要等到玩家退出或显式save才落盘。
5. 默认档案
新玩家(无文件)首次加载得到默认档案,只种一个资源:
kotlin
profile.resources["mana"] = config.getDouble("resources.default_mana", 100.0)即默认 mana = resources.default_mana(缺省 100)。其余集合为空(无解锁、无等级覆盖、无冷却)。
6. 持久化 vs 内存态(重要)
| 状态 | 存哪 | 重登后 |
|---|---|---|
| 冷却(cooldownUntil) | ✅ 落盘 | 保留(按时间戳,防重登刷新) |
| 解锁 / 等级 / 槽位 / 资源 / 变量 | ✅ 落盘 | 保留 |
| 充能层数(ChargeTracker) | ❌ 内存 | 重置 |
| toggle 开关态(ToggleTracker) | ❌ 内存 | 重置 |
设计取舍:
- 冷却落盘,专门防玩家"重登刷新冷却"的刷子行为。
- 充能 / toggle 在内存(
ChargeTracker/ToggleTracker),重登归零。toggle 技能重登后默认回到关闭态。
7. 资源缝与 QinhClass 边界
resources 里的 mana 是 QC 接管前的临时占位。当前 QS 拥有这唯一一条资源缝(PlayerSkillProfile.resources):
- 吟唱 / 释放扣资源也走这同一条缝(不另造池子)。
- QinhClass 接管资源时,只改这一处 —— 其余门控、占位符、API 不动。
QS 不做(全归 QinhClass):
- 玩家职业、玩家等级
- 法力 / 耐力资源池的真正归属(QS 只是临时占位)
- 冷却缩减(CDR)数值
- 玩家属性
8. 程序化读写档案
通过 QinhSkillsAPI 间接操作(推荐,自动写盘):
kotlin
QinhSkillsAPI.unlock(player, "fire_wave") // 解锁 + 保存
QinhSkillsAPI.setLevel(player, "fire_wave", 3) // 设等级 + 保存
QinhSkillsAPI.setSlot(player, 1, "fire_wave") // 设槽 + 保存
QinhSkillsAPI.setSlot(player, 1, null) // 清槽 + 保存
val unlocked = QinhSkillsAPI.isUnlocked(player, "fire_wave")这些 API 每次都会写盘。对大批量玩家循环调用时注意 I/O 频次。