执行链路与事件:从按键到放出技能
前面几页讲了「怎么接」。这一页讲「接好之后,底层到底发生了什么」——从玩家点击物品,到 MythicMobs 放出火焰,中间这条链路逐站走一遍。
读完你会能够:看懂完整调用链、监听 / 取消 QISkillUseEvent、理解 payload 怎么被解析、分清「主链」和「fallback」、以及用 trace 阶段标签和 event flags 诊断问题。
这一页偏开发者 / 深度服主。只想接技能的,前几页够用了。
🛤️ 一、完整执行链路
QS 的运行时是一条单一管线——所有入口(物品 handler、命令桥、API、被动事件)最后都汇进它。下面以「玩家点击 QI 物品」为例,走完全程:
玩家点击物品 (PlayerInteractEvent)
→ QinhItems ActionTriggerListener 捕获按键
→ QinhItems ActionDispatchService 按 handlerId 路由
→ handler "qinhskills:cast" (QS 启动时经 QCL 的 QinhActionHandler 契约
注册到 QI;QS 这侧实现是 QiListener)
→ QISkillBridge.dispatchViaEvent 构造并 callEvent 一个 QISkillUseEvent
(QISkillUseEvent 由 QCL 提供)
→ QS 的 QiSkillEventListener 监听到事件
→ SkillCastPipeline.executePrimary(event) 进入主链
→ SkillRuntimeV2.executePrimary(event) 【这是唯一运行时,下文统称「运行时」】
├─ 输入归一 SkillInputNormalizer
├─ 状态机 SkillStateMachine
├─ 图解析 SkillGraphResolver + 连招 ComboResolver
├─ 执行计划 ExecutionPlanBuilder
├─ 门控 CastGate (解锁/冷却/冷却组/充能/GCD/资源/血祭/条件…)
├─ 执行 MythicExecutor.cast → MythicMobs
└─ 后处理 冷却 / 状态 / 连招 / actionbar
→ event 字段回填 skillHandled / castResult / mythicInvoked / fallbackInvoked📌 注意
SkillRuntimeV2只是类名里带个 2,它就是 QS 唯一的运行时,文档里一律叫它「运行时」。没有别的并行管线、没有什么开关可切。
文本时序图
玩家 QinhItems QISkillUseEvent QinhSkills 运行时 MythicMobs
│ 点击物品 │ │ │ │
├──────────────►│ 捕获(TriggerListener) │ │ │
│ ├─ 路由(DispatchService) │ │ │
│ ├─ handler qinhskills:cast │ │
│ ├─ dispatchViaEvent ─────►│ callEvent │ │
│ │ ├───────────────────────►│ QiSkillEventListener│
│ │ │ ├─ executePrimary │
│ │ │ ├─ 归一/状态/图/连招 │
│ │ │ ├─ 计划 → 门控(Gate) │
│ │ │ ├─ EXEC ─────────────►│ 放粒子/伤害
│ │ │ ├─ 后处理(冷却/状态) │
│ │ │◄── 回填 flags ─────────┤ │
│◄─────────────┤ (放出来了 / 或占位消息 / 或提示) │ │📡 二、QISkillUseEvent:唯一入口事件
这条链路的枢纽是一个 Bukkit 事件 QISkillUseEvent(由 QinhCoreLib 提供)。所有技能释放都经它——这也是「单一入口」原则的体现。
2.1 事件字段
QISkillUseEvent 是 Cancellable(可取消)的,字段:
| 字段 | 类型 | 含义 |
|---|---|---|
payload | String | 原始 payload 串(如 "fire_wave" 或一段 JSON) |
triggerType | TriggerType 枚举 | 触发类型 |
item | ItemStack? | 触发的物品(命令桥时可能无) |
itemId | String? | 物品 id |
skillHandled | Boolean | 主链是否已处理(回填) |
castResult | String | 施放结果,= CastResult.name(回填) |
castAttempted | Boolean | 是否尝试过施放(回填) |
fallbackInvoked | Boolean | 是否走了 fallback(回填) |
mythicInvoked | Boolean | 是否真的调了 MM(回填) |
primaryPipeline | Boolean | 是否走主链 |
「回填」= QS 运行时跑完后把结果写回事件,监听者随后能读到。
2.2 如何监听 / 取消(开发者)
任何插件都能 @EventHandler 监听它,可读结果、可取消:
@EventHandler
public void onSkillUse(QISkillUseEvent event) {
// 读:玩家想放什么
String payload = event.getPayload();
// 取消:比如某区域禁用技能
if (inSafeZone(event)) {
event.setCancelled(true); // 取消 → QS 不会施放
return;
}
// 读结果(在 QS 处理之后的监听里)
if (event.isMythicInvoked()) {
// 技能确实交给 MM 放出来了
}
}⚠️ 字段名以实际 API 为准(开发者 API 有完整签名);这里展示「监听 + 取消 + 读 flag」三种典型用法。
🔤 三、PayloadParser:payload 怎么被解析
事件里的 payload 由 QS 内部的 PayloadParser 解析成「要放哪个技能 + 附带上下文」。支持三种格式(技能 id 一律转小写):
| 格式 | 例子 | 解析出 |
|---|---|---|
| 纯 id | "fire_wave" | skill = fire_wave |
| 带模式 | "fire_wave:RIGHT_CLICK" | skill = fire_wave,模式 RIGHT_CLICK |
| JSON | '{"skill":"demo_slash_charged","source":"qinhitems","context":{"mode":"LEFT_CLICK"}}' | skill = demo_slash_charged,source + context.mode |
JSON 识别的键:skill(必填)、source(来源标记)、context.mode(触发模式)。
命令桥(
qs cast <id>)只产生纯 id 形态——这就是它不支持 JSON 的原因(见 对接其他物品插件)。
🔀 四、主链 vs Fallback(防二次执行)
QS 有两条进运行时的路:主链和 fallback。设计 fallback 是为了「兜底」,但绝不能因此重复施放。
主链(Primary)
正常情况走主链:
QiSkillEventListener → SkillCastPipeline.executePrimary → 运行时.executePrimary跑完回填 skillHandled 等字段。
Fallback(兜底)
如果 QI 已经 callEvent 了,但主链由于某些原因 skillHandled = false(没处理),则:
QiListener.dispatch → SkillCastPipeline.executeFallback → 再跑一次运行时🛡️ 防二次执行
Fallback 不能造成「放两次技能」。所以 executeFallback 进去前会逐项检查,凡是满足以下任一就跳过:
| 已经… | 就跳过 fallback |
|---|---|
skillHandled = true(主链已处理) | ✅ 跳过 |
fallbackInvoked = true(已经 fallback 过) | ✅ 跳过 |
mythicInvoked = true(已经调过 MM) | ✅ 跳过 |
这保证了「Fallback 不得 double EXEC」——同一次按键,技能最多放一次。这是 QS 事件架构的硬契约。
🩺 五、开发者链路诊断
排查「技能没放出来 / 放了两次 / 绕过门控」时,靠这三样:
5.1 event flags(事件字段)
跑完一次,看事件回填的 flag 组合,就能定位卡在哪:
| flag 组合 | 含义 |
|---|---|
skillHandled=true + mythicInvoked=true + fallbackInvoked=false | ✅ 主链正常,技能放出 |
mythicInvoked=false | 门控(Gate)没过,没碰 MM(正确——Gate 失败不该 EXEC) |
fallbackInvoked=true | 主链没处理,走了兜底 |
castAttempted=true 但 mythicInvoked=false | 尝试了但被门控拦下 |
5.2 trace 阶段标签(开 debug: true 时输出)
开启调试后,链路每过一站都打一个标签,照着读就知道走到哪:
| 标签 | 阶段 |
|---|---|
[QI] | QinhItems 捕获 / 路由 |
[EVENT] | QISkillUseEvent 触发 |
[PARSE] | payload 解析 |
[ROUTE] | 路由到技能 |
[GATE] | 门控校验 |
[EXEC] | 调 MM 执行 |
[POST] | 后处理(冷却 / 状态 / 连招 / actionbar) |
[FALLBACK] | 走了兜底路径 |
例:一次正常释放的 trace 是
[QI] → [EVENT] → [PARSE] → [ROUTE] → [GATE pass] → [EXEC] → [POST]。如果停在[GATE]没到[EXEC],说明门控拦下了(解锁/冷却/资源某项没过)——这是正常的拦截,不是 bug。
5.3 [BYPASS] 警告(始终记录)
[BYPASS] 是始终记录(不需开 debug)的警告——它表示「有人试图绕过事件 / 门控直接调 MM」。看到它说明架构被破坏了,应当排查是谁在绕路。
QS 的守卫(
CastPipelineGuard.allowMythicExecute/assertCastServiceAccess)确保所有 MM 调用都经事件,任何旁路都会触发[BYPASS]。
🏛️ 六、架构原则:correct vs forbidden(来自验证数据集)
QS 自带一份事件链路验证数据集 integrations/event_chain_validation.yml,它白纸黑字写下了 QS 的架构边界。两句话最关键:
architecture_principles:
correct_abstraction: "QinhItems → QISkillUseEvent → QinhSkills Engine → MythicMobs.castSkill"
forbidden_abstraction: "QinhSkills = MythicMobs SDK wrapper"
qs_role: "Skill Runtime Bridge(玩家技能运行时)"
mm_role: "Execution black box(效果层)"读法:
- ✅ correct(正确抽象):物品 → 事件 → QS 引擎 → MM 施放。QS 是技能运行时桥,MM 是效果黑盒。
- ❌ forbidden(禁止抽象):把 QS 当成「MythicMobs 的 SDK 包装器」。QS 不该去复刻 MM 的全部能力。
数据集还明确列出了 QS 故意不做的事(这些是架构收口,不是待办功能):
| QS 不做 | 原因 |
|---|---|
| MetaSkill / Condition skill 作为 QS 路由类型 | 属于 MythicMob 域,MM 自己调度 |
| MythicItem give / item trigger / item skill engine | QS 不当 Item 引擎;payload 只是不透明的技能键 |
Mob ~onSpawn / ~onHit / ~onTimer 注入 | Mob 行为由 MM 配置,QS 不注入 |
| Mythic Placeholders 全集桥接 | QS 自有 %qinhskills_*% 即可 |
也就是说:QS 对 MM 只承诺一件事——
apiHelper.castSkill(player, skillId)(玩家主动施放技能)。其余 MM 能力 QS 一概不碰。这正是「好处 3:可插拔执行后端」能成立的前提——接口越窄,越好换后端。
全链路无旁路审计
数据集里的 event_bypass_audit 明确:所有 MM 调用都经事件(all_mythic_calls_via_event: true):
| 路径 | 入口 |
|---|---|
| QI 侧 | QISkillBridge.dispatchViaEvent |
| QS 主链 | QiSkillEventListener → executePrimary |
| QS 网关 | SkillEventGateway → callEvent |
| QS 兜底 | executeFallback(仅「未处理 + 未 mythicInvoked」时) |
连命令桥也不例外:
/qs cast仍然经SkillEventGateway → QISkillUseEvent进同一条链——所以命令桥和原生 handler 效果一致。
📋 七、诊断速查
| 想确认 | 看什么 |
|---|---|
| 技能到底放了没 | event flag mythicInvoked |
| 卡在哪一站 | trace 最后出现的阶段标签([GATE] 停住 = 门控拦下) |
| 是否走了兜底 | fallbackInvoked / [FALLBACK] |
| 有没有放两次 | fallback 防二次执行会跳过;若仍重复,查是否有人旁路 |
| 有没有人绕过架构 | 日志里的 [BYPASS] 警告 |
| 门控为什么拦 | 开 debug,看 [GATE] 阶段的具体原因(解锁/冷却/资源…) |
继续阅读
- 完整 API 签名、占位符、脚本协议 → 开发者 API
- 门控每一项的判定顺序 → 冷却充能GCD与冲突
- 回看四方分工与三种接法 → 对接总览