Skip to content

执行链路与事件:从按键到放出技能

上一页:对接其他物品插件 · 下一页:开发者 API

前面几页讲了「怎么接」。这一页讲「接好之后,底层到底发生了什么」——从玩家点击物品,到 MythicMobs 放出火焰,中间这条链路逐站走一遍。

读完你会能够:看懂完整调用链、监听 / 取消 QISkillUseEvent、理解 payload 怎么被解析、分清「主链」和「fallback」、以及用 trace 阶段标签和 event flags 诊断问题。

这一页偏开发者 / 深度服主。只想接技能的,前几页够用了。


🛤️ 一、完整执行链路

QS 的运行时是一条单一管线——所有入口(物品 handler、命令桥、API、被动事件)最后都汇进它。下面以「玩家点击 QI 物品」为例,走完全程:

text
玩家点击物品 (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 唯一的运行时,文档里一律叫它「运行时」。没有别的并行管线、没有什么开关可切。

文本时序图

text
玩家          QinhItems              QISkillUseEvent          QinhSkills 运行时        MythicMobs
 │  点击物品     │                        │                        │                     │
 ├──────────────►│ 捕获(TriggerListener)  │                        │                     │
 │              ├─ 路由(DispatchService) │                        │                     │
 │              ├─ handler qinhskills:cast                         │                     │
 │              ├─ dispatchViaEvent ─────►│ callEvent              │                     │
 │              │                        ├───────────────────────►│ QiSkillEventListener│
 │              │                        │                        ├─ executePrimary     │
 │              │                        │                        ├─ 归一/状态/图/连招   │
 │              │                        │                        ├─ 计划 → 门控(Gate)   │
 │              │                        │                        ├─ EXEC ─────────────►│ 放粒子/伤害
 │              │                        │                        ├─ 后处理(冷却/状态)   │
 │              │                        │◄── 回填 flags ─────────┤                     │
 │◄─────────────┤  (放出来了 / 或占位消息 / 或提示)                 │                     │

📡 二、QISkillUseEvent:唯一入口事件

这条链路的枢纽是一个 Bukkit 事件 QISkillUseEvent(由 QinhCoreLib 提供)。所有技能释放都经它——这也是「单一入口」原则的体现。

2.1 事件字段

QISkillUseEventCancellable(可取消)的,字段:

字段类型含义
payloadString原始 payload 串(如 "fire_wave" 或一段 JSON)
triggerTypeTriggerType 枚举触发类型
itemItemStack?触发的物品(命令桥时可能无)
itemIdString?物品 id
skillHandledBoolean主链是否已处理(回填
castResultString施放结果,= CastResult.name回填
castAttemptedBoolean是否尝试过施放(回填
fallbackInvokedBoolean是否走了 fallback(回填
mythicInvokedBoolean是否真的调了 MM(回填
primaryPipelineBoolean是否走主链

「回填」= QS 运行时跑完后把结果写回事件,监听者随后能读到。

2.2 如何监听 / 取消(开发者)

任何插件都能 @EventHandler 监听它,可读结果、可取消

java
@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_chargedsource + context.mode

JSON 识别的键:skill必填)、source(来源标记)、context.mode(触发模式)。

命令桥(qs cast <id>)只产生纯 id 形态——这就是它不支持 JSON 的原因(见 对接其他物品插件)。


🔀 四、主链 vs Fallback(防二次执行)

QS 有两条进运行时的路:主链fallback。设计 fallback 是为了「兜底」,但绝不能因此重复施放

主链(Primary)

正常情况走主链:

text
QiSkillEventListener → SkillCastPipeline.executePrimary → 运行时.executePrimary

跑完回填 skillHandled 等字段。

Fallback(兜底)

如果 QI 已经 callEvent 了,但主链由于某些原因 skillHandled = false(没处理),则:

text
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=truemythicInvoked=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 的架构边界。两句话最关键:

yaml
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 engineQS 不当 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] 阶段的具体原因(解锁/冷却/资源…)

继续阅读