[MD]
### 动作语法迁移
- **动作写法整体换新, 大部分你不需要自己动手改。** 首次启动时 CoreLib 会自动把 `plugins/` 下所有 Emaki 插件的配置文件改写成新写法, 只做一次, 之后不再重复。每个被改过的文件旁边都会留一份 `.legacy-backup` 后缀的原文件备份。万一某一行转换不成功, 插件不会拿这行去覆盖你的配置, 而是单独写到一个 `.v2-failed` 文件里、原配置保持不动, 控制台也会列出是哪个文件的哪一行。像 `types/*.yml` 里的 `sendmessage`、`sources/*.yml` 里的 `@chance=5 sendactionbar` 这类通用动作都在自动改写范围内。
- **等级自己的八个动作要你手工改名字**, 自动改写不包括它们。名字规则是去掉 `emaki` 前缀并加下划线分词:
- `emakileveladdexp` → `level_add_exp`
- `emakilevelsetexp` → `level_set_exp`
- `emakilevelremoveexp` → `level_remove_exp`
- `emakileveladdlevel` → `level_add_level`
- `emakilevelsetlevel` → `level_set_level`
- `emakilevelremovelevel` → `level_remove_level`
- `emakilevelreset` → `level_reset`
- `emakilevellevelup` → `level_up`
- 旧名字不再保留。这八个旧名字连自动改写都认不出来, 会被原样留在配置里, 升级后加载时控制台会提示这一行不认识。
- **这八个动作原来的 `target=玩家名` 参数取消了**, 改由前面的来源段指定对谁生效。原先写 `emakileveladdexp type=main amount=10 target=Steve`, 现在写 `player_by_name Steve | level_add_exp type=main amount=10`。不写来源段时默认作用于触发动作的那名玩家, 和以前不填 `target` 的效果一样。
- **`auto_upgrade` 和 `silent` 现在只有 `level_add_exp` 认**。以前这两个参数在加减经验、加减等级上都能写, 现在只对加经验有意义, 写到别的动作上会被当成不认识的参数。
- **动作行里引用等级数据的占位符要加 `var.` 前缀**, 只有你自己改过等级类型或经验来源配置才需要管这条。随包的 `types/`、`sources/` 默认文件已经是新写法, 直接用不会有问题。如果你在自定义配置的 `actions.success`、`actions.failure`、`actions.gain` 里写过 `%old_level%`、`%new_level%`、`%amount%`、`%failure_reason%` 这类占位符, 要改成 `%var.old_level%`、`%var.new_level%`、`%var.amount%`、`%var.failure_reason%`, 否则消息里会原样显示 `%new_level%` 而不是等级数字。动作行里可用的完整清单是 `type`、`type_display_name`、`level`、`old_level`、`new_level`、`exp`、`old_exp`、`new_exp`、`total_exp`、`required_exp`、`progress`、`progress_percent`、`amount`、`reason`、`failure_reason`。
- **公式里的占位符不要动。** 只有动作行 (那些写在 `actions:` 下面的行) 需要加前缀。`exp_formula` 里的 `%result_amount%`、`%mythic_level%`, 以及 `attributes` 属性公式里的 `%level%`, 走的是另一套算式解析, 保持原来的裸写法, 加了前缀反而会算不出来。PlaceholderAPI 那套 `%emakilevel_...%` 占位符同样不受影响, 不用动。
### 经验规则脚本取消
- **原先用 JS 脚本写经验规则和升级钩子的功能整体取消了, 没有自动转换的办法。** 通过 `level.registerExpRule` 调整经验倍率、通过 `level.onLevelUp` 挂升级回调的脚本都不再生效, 请改用配置文件: 倍率走 config.yml 的 `multipliers`, 升级时要做的事走等级类型里的 `actions.success`。随包的两个示例脚本 `scripts/examples/level_exp_rule.js` 和 `scripts/examples/level_status.js` 一并删除。
### 经验与升级修复
- **修复经验被别的插件拦下来时, 每日上限照样被扣掉**。只影响装了会监听等级事件的插件的服务器。原先插件先把这次经验记进当天的上限统计, 再通知其他插件, 其他插件把这次经验拦掉或改小之后, 统计里那笔已经记上了。表现是玩家明明没拿到经验, 当天的上限额度却少了一截。现在改成等其他插件表过态之后, 只按真正入账的数额记统计。
- **修复升级中途出问题时, 补偿记录可能整批丢失**。升级扣费的过程会先记一笔待办, 万一扣了钱却没给等级, 下次启动时插件会照着这笔待办把钱退回来。原先只要待办文件里有一份内容坏掉, 插件读取时就会整批放弃, 所有等着退款退材料的玩家都补不回来了。现在坏掉的那一份会被单独挪到 `data/operation-journal/quarantine/` 留着, 其余的照常恢复, 控制台也会列出这次待恢复的操作。
- **修复升级时写这份待办卡住玩家甚至中断升级**。原先每次升级都在玩家所在的线程上直接读写硬盘, 服务器磁盘慢的时候会拖慢升级, 写入失败还会直接把这次升级打断。现在改成后台写, 写失败只在控制台报错并把这笔待办留到下次恢复, 不再影响玩家当场的操作。
- **修复 `/emakilevel reload` 之后部分玩家的等级数据没刷新**, 只影响 Folia 服务端。重载会给所有在线玩家重新写一遍等级数据, 原先是在执行命令的那个线程上一口气写完, Folia 上碰到不属于当前线程的玩家就会报错跳过。现在会把这些玩家分派回各自的线程处理, 全部都能刷新到。
### 配置清理
- 删掉了七个一直没有实际作用的配置项: `storage.type`、`storage.save_on_quit`、`storage.save_on_shutdown`, 以及 `pdc.sync_on_join`、`pdc.sync_on_exp_change`、`pdc.sync_on_level_change`、`pdc.sync_on_reload`。它们以前就写在配置文件里, 但插件从来没读过, 不管填什么都没效果, 现在直接删了, 你的配置里有也可以放心删掉。`pdc.enabled` 和 `pdc.namespace` 保留不变。
- 配置预检的问题行现在按严重级别整行着色: 致命和错误是红色, 警告是黄色, 提示仍是原来的灰色。
### 开发者相关
- `EmakiLevelApiProvider` 已移除, 直接使用 `EmakiLevelApi` 的静态方法。
- `EmakiLevelApi` 原先直接挂在门面上的静态方法 (`available()`、`type()`、`types()`、`getPlayerData()`、`getLevel()`、`getExp()`、`getTotalExp()`、`getRequiredExp()`、`addExp()`/`removeExp()`/`setExp()`、`addLevel()`/`removeLevel()`/`setLevel()`、`levelUp()` 以及对应的 `*Async()` 变体) 全部移除, 改由四个访问器分层提供: `status()`、`catalog()`、`operations()`、`extensions()`。
- `available()` 由 `status()` 替代 (返回 `ApiStatus`, 提供 `usable()`、`ready()`、版本与插件名字段)。`ready()` 的判据是数据已加载, 而不是组件已创建。
- 只读查询走 `catalog()`: `types()`、`type()`、`level()`、`exp()`、`totalExp()`、`requiredExp()`、`loadPlayerDataAsync()`, 并新增排行榜查询 `top(typeId, limit)`、`topCount(typeId)` 与经验调整预览 `previewAdjustment()`。
- 状态变更走 `operations()`: `addExp()`、`removeExp()`、`setExp()`、`addLevel()`、`removeLevel()`、`setLevel()`、`levelUp()`、`reset()`, 并新增 `syncPlayer()`、`openGui()`、`openTopGui()`。
- 除 `catalog().types()`/`type()` 之外, 上述方法统一返回 `EmakiResult<T>`, 插件未加载或数据未就绪时返回不可用结果而不是抛异常。
- 新增 `extensions().registerExpSource(Plugin, ExpSourceProvider)`, 第三方插件可以注册自己的经验来源, 在 `entity_kill`、`mythic_mob_kill`、`block_place`、`block_break`、`crop_harvest`、`craft_item`、`furnace_extract`、`player_fish`、`entity_tame`、`brew_complete` 这些玩法触发点返回 `ExpSourceGrant` 发经验。按注册者与 provider id 去重, 注册者被禁用时自动清除, 返回的 `ExpSourceRegistration` 句柄可重复关闭。
- 新增可取消事件 `PlayerPreLevelUpEvent`, 在所有检查通过、开始扣费与写入之前触发。取消它会阻止这次升级, 不扣任何费用、不改等级、不发奖励、不执行动作, 运行时按 `event_cancelled` 上报失败。这是唯一受支持的否决升级的方式。事件只在玩家在线且当前线程持有该玩家时触发, 并且更早的检查已失败时不触发, 因此不能当作审计流水使用; 需要确认升级已完成请监听 `PlayerLevelUpEvent`。
- `PlayerExpGainEvent` 被取消时, 失败原因由 `invalid_amount` 改为 `event_cancelled`。
- `EmakiLevelApi.Bridge` 标注 `@ApiStatus.NonExtendable`, 第三方插件不得实现。
- 事件类补齐线程语义说明: 各事件均为同步事件, 仅在运行时持有对应玩家所有权时触发, 监听器需遵守相同的 Paper/Folia 线程约束, 并且要尽快返回、不得阻塞等待其他线程。
### 升级说明
- 需要先把 EmakiCoreLib 升到 `4.7.0`。
- **升级前请备份 `plugins/EmakiLevel/` 整个目录。**
- 通用动作写法由插件自动改写, 但下面三处要你自己动手:
- 等级自己的八个动作要改名 (对照上面的清单)。
- 这八个动作里的 `target=玩家名` 要换成前置的 `player_by_name 玩家名 |` 来源段。
- `auto_upgrade` 与 `silent` 只保留在 `level_add_exp` 上, 写在其他动作上的要删掉。
- 如果你改过等级类型或经验来源配置, 再顺手检查一下动作行里的占位符有没有加 `var.` 前缀。公式里的占位符不用动。
- 用 JS 脚本写经验规则或升级钩子的话, 要改成用 config.yml 的 `multipliers` 与等级类型的 `actions` 实现。
- 玩家等级数据文件结构没有变化, 不需要迁移。命令、别名与权限键也都保持不变。
- `config.yml` 只删了七个无效键, 其余键与默认值不变, 沿用旧配置文件不会出问题。
[/MD]