Execution Flow & Events: From Keypress to Cast
Previous: Integrating Other Item Plugins · Next: Developer API
The previous pages covered "how to integrate." This page covers "once integrated, what actually happens under the hood" — walking step by step through the chain from a player clicking an item to MythicMobs producing flames.
By the end you'll be able to: follow the complete call chain, listen to / cancel QISkillUseEvent, understand how the payload is parsed, distinguish the "main chain" from "fallback," and diagnose problems using trace stage labels and event flags.
This page leans toward developers / advanced admins. If you just want to hook up skills, the earlier pages are enough.
🛤️ 1. The complete execution flow
QS's runtime is a single pipeline — all entry points (item handler, command bridge, API, passive events) ultimately funnel into it. Below, using "player clicks a QI item" as the example, walks the whole journey:
Player clicks item (PlayerInteractEvent)
→ QinhItems ActionTriggerListener captures the keypress
→ QinhItems ActionDispatchService routes by handlerId
→ handler "qinhskills:cast" (registered to QI via QCL's QinhActionHandler
contract at QS startup; QS's implementation is QiListener)
→ QISkillBridge.dispatchViaEvent builds and callEvents a QISkillUseEvent
(QISkillUseEvent is provided by QCL)
→ QS's QiSkillEventListener listens for the event
→ SkillCastPipeline.executePrimary(event) enters the main chain
→ SkillRuntimeV2.executePrimary(event) 【this is the one and only runtime, hereafter "the runtime"】
├─ Input normalization SkillInputNormalizer
├─ State machine SkillStateMachine
├─ Graph resolution SkillGraphResolver + combos ComboResolver
├─ Execution plan ExecutionPlanBuilder
├─ Gating CastGate (unlock/cooldown/cooldown group/charges/GCD/resources/blood-sacrifice/conditions…)
├─ Execution MythicExecutor.cast → MythicMobs
└─ Post-processing cooldown / state / combo / actionbar
→ event fields filled back skillHandled / castResult / mythicInvoked / fallbackInvoked📌 Note that
SkillRuntimeV2just has a "2" in the class name — it is QS's one and only runtime, and the docs always call it "the runtime." There's no other parallel pipeline, no switch to flip.
Text sequence diagram
Player QinhItems QISkillUseEvent QinhSkills runtime MythicMobs
│ click item │ │ │ │
├──────────────►│ capture(TriggerListener)│ │ │
│ ├─ route(DispatchService)│ │ │
│ ├─ handler qinhskills:cast │ │
│ ├─ dispatchViaEvent ─────►│ callEvent │ │
│ │ ├───────────────────────►│ QiSkillEventListener│
│ │ │ ├─ executePrimary │
│ │ │ ├─ normalize/state/graph/combo │
│ │ │ ├─ plan → gate(Gate) │
│ │ │ ├─ EXEC ─────────────►│ emit particles/damage
│ │ │ ├─ post-process(cooldown/state) │
│ │ │◄── fill back flags ────┤ │
│◄─────────────┤ (cast / or placeholder message / or notice) │ │📡 2. QISkillUseEvent: the one and only entry event
The hub of this chain is a Bukkit event, QISkillUseEvent (provided by QinhCoreLib). Every skill cast passes through it — this is the embodiment of the "single entry point" principle.
2.1 Event fields
QISkillUseEvent is Cancellable. Fields:
| Field | Type | Meaning |
|---|---|---|
payload | String | Raw payload string (e.g. "fire_wave" or a JSON snippet) |
triggerType | TriggerType enum | Trigger type |
item | ItemStack? | The triggering item (may be absent for the command bridge) |
itemId | String? | Item id |
skillHandled | Boolean | Whether the main chain handled it (filled back) |
castResult | String | Cast result, = CastResult.name (filled back) |
castAttempted | Boolean | Whether a cast was attempted (filled back) |
fallbackInvoked | Boolean | Whether fallback was taken (filled back) |
mythicInvoked | Boolean | Whether MM was actually called (filled back) |
primaryPipeline | Boolean | Whether the main chain was taken |
"Filled back" = after the QS runtime finishes, it writes the results back into the event, which listeners can then read.
2.2 How to listen / cancel (developers)
Any plugin can @EventHandler it, reading results and cancelling:
@EventHandler
public void onSkillUse(QISkillUseEvent event) {
// Read: what skill the player wants to cast
String payload = event.getPayload();
// Cancel: e.g. disable skills in a certain area
if (inSafeZone(event)) {
event.setCancelled(true); // cancel → QS won't cast
return;
}
// Read results (in a listener that runs after QS has processed)
if (event.isMythicInvoked()) {
// the skill was indeed handed off to MM to cast
}
}⚠️ Field names follow the actual API (Developer API has the full signatures); this shows the three typical usages: "listen + cancel + read flag."
🔤 3. PayloadParser: how the payload is parsed
The event's payload is parsed by QS's internal PayloadParser into "which skill to cast + accompanying context." Three formats are supported (the skill id is always lowercased):
| Format | Example | Parses into |
|---|---|---|
| Plain id | "fire_wave" | skill = fire_wave |
| With mode | "fire_wave:RIGHT_CLICK" | skill = fire_wave, mode RIGHT_CLICK |
| JSON | '{"skill":"demo_slash_charged","source":"qinhitems","context":{"mode":"LEFT_CLICK"}}' | skill = demo_slash_charged, source + context.mode |
JSON-recognized keys: skill (required), source (origin marker), context.mode (trigger mode).
The command bridge (
qs cast <id>) only produces the plain id form — that's why it doesn't support JSON (see Integrating Other Item Plugins).
🔀 4. Main chain vs. Fallback (preventing double execution)
QS has two paths into the runtime: the main chain and fallback. Fallback is designed as a "safety net," but it must never cause a double cast.
Main chain (Primary)
Under normal conditions the main chain is taken:
QiSkillEventListener → SkillCastPipeline.executePrimary → runtime.executePrimaryAfter it runs, it fills back skillHandled and the other fields.
Fallback
If QI already callEvented but, for some reason, the main chain leaves skillHandled = false (didn't handle it), then:
QiListener.dispatch → SkillCastPipeline.executeFallback → run the runtime again🛡️ Preventing double execution
Fallback must not cause "casting twice." So before entering, executeFallback checks each item, and skips if any one of the following holds:
| Already… | Skip fallback |
|---|---|
skillHandled = true (main chain handled it) | ✅ Skip |
fallbackInvoked = true (already fell back) | ✅ Skip |
mythicInvoked = true (already called MM) | ✅ Skip |
This guarantees "Fallback must not double EXEC" — for a single keypress, the skill casts at most once. This is a hard contract of QS's event architecture.
🩺 5. Developer flow diagnostics
When troubleshooting "the skill didn't fire / fired twice / bypassed gating," rely on these three things:
5.1 event flags (event fields)
After one run, look at the combination of flags filled back into the event to pinpoint where it got stuck:
| Flag combination | Meaning |
|---|---|
skillHandled=true + mythicInvoked=true + fallbackInvoked=false | ✅ Main chain normal, skill cast |
mythicInvoked=false | Gating didn't pass, MM was never touched (correct — a failed Gate shouldn't EXEC) |
fallbackInvoked=true | Main chain didn't handle it, fallback was taken |
castAttempted=true but mythicInvoked=false | Attempted but blocked by gating |
5.2 trace stage labels (output when debug: true)
With debug enabled, the chain prints a label at each station it passes; read along to know where it got to:
| Label | Stage |
|---|---|
[QI] | QinhItems capture / route |
[EVENT] | QISkillUseEvent fired |
[PARSE] | payload parsing |
[ROUTE] | routing to the skill |
[GATE] | gating validation |
[EXEC] | calling MM to execute |
[POST] | post-processing (cooldown / state / combo / actionbar) |
[FALLBACK] | fallback path taken |
Example: a normal cast's trace is
[QI] → [EVENT] → [PARSE] → [ROUTE] → [GATE pass] → [EXEC] → [POST]. If it stops at[GATE]and never reaches[EXEC], gating blocked it (unlock/cooldown/resource — something didn't pass) — this is a normal block, not a bug.
5.3 [BYPASS] warning (always logged)
[BYPASS] is a warning that is always logged (no need to enable debug) — it means "someone tried to bypass the event / gating and call MM directly." Seeing it means the architecture has been broken, and you should investigate who's taking a shortcut.
QS's guards (
CastPipelineGuard.allowMythicExecute/assertCastServiceAccess) ensure all MM calls go through the event; any bypass triggers[BYPASS].
🏛️ 6. Architecture principles: correct vs. forbidden (from the validation dataset)
QS ships with an event-chain validation dataset, integrations/event_chain_validation.yml, which spells out QS's architectural boundaries in black and white. Two lines matter most:
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(效果层)"How to read it:
- ✅ correct (correct abstraction): item → event → QS engine → MM cast. QS is the skill runtime bridge, MM is the execution black box.
- ❌ forbidden (forbidden abstraction): treating QS as "MythicMobs' SDK wrapper." QS should not replicate all of MM's capabilities.
The dataset also explicitly lists what QS deliberately does not do (these are architectural boundaries, not pending features):
| QS does not | Reason |
|---|---|
| MetaSkill / Condition skill as a QS routing type | Belongs to the MythicMob domain; MM schedules it itself |
| MythicItem give / item trigger / item skill engine | QS is not an Item engine; the payload is just an opaque skill key |
Mob ~onSpawn / ~onHit / ~onTimer injection | Mob behavior is configured by MM; QS doesn't inject |
| Bridging the full set of Mythic Placeholders | QS's own %qinhskills_*% is enough |
In other words: QS promises MM just one thing —
apiHelper.castSkill(player, skillId)(a player actively casting a skill). QS touches none of MM's other capabilities. This is precisely the premise that makes "Benefit 3: a pluggable execution backend" hold — the narrower the interface, the easier it is to swap backends.
Whole-chain no-bypass audit
The dataset's event_bypass_audit makes it explicit: all MM calls go through the event (all_mythic_calls_via_event: true):
| Path | Entry point |
|---|---|
| QI side | QISkillBridge.dispatchViaEvent |
| QS main chain | QiSkillEventListener → executePrimary |
| QS gateway | SkillEventGateway → callEvent |
| QS fallback | executeFallback (only when "unhandled + not mythicInvoked") |
The command bridge is no exception either:
/qs caststill goes throughSkillEventGateway → QISkillUseEventinto the same chain — which is why the command bridge and the native handler produce identical results.
📋 7. Diagnostics quick reference
| Want to confirm | What to look at |
|---|---|
| Whether the skill actually cast | event flag mythicInvoked |
| Which station it got stuck at | the last stage label in the trace (stuck at [GATE] = blocked by gating) |
| Whether fallback was taken | fallbackInvoked / [FALLBACK] |
| Whether it cast twice | fallback's double-execution guard skips it; if it still repeats, check whether someone is bypassing |
| Whether anyone bypassed the architecture | the [BYPASS] warning in the logs |
| Why gating blocked it | enable debug, look at the specific reason at the [GATE] stage (unlock/cooldown/resource…) |
Keep reading
- Full API signatures, placeholders, scripting protocol → Developer API
- The decision order for each gating item → Cooldowns, Charges, GCD & Conflicts
- Revisit the four-way division of labor and the three integration methods → Integration Overview