Skip to content

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:

text
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 SkillRuntimeV2 just 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

text
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:

FieldTypeMeaning
payloadStringRaw payload string (e.g. "fire_wave" or a JSON snippet)
triggerTypeTriggerType enumTrigger type
itemItemStack?The triggering item (may be absent for the command bridge)
itemIdString?Item id
skillHandledBooleanWhether the main chain handled it (filled back)
castResultStringCast result, = CastResult.name (filled back)
castAttemptedBooleanWhether a cast was attempted (filled back)
fallbackInvokedBooleanWhether fallback was taken (filled back)
mythicInvokedBooleanWhether MM was actually called (filled back)
primaryPipelineBooleanWhether 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:

java
@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):

FormatExampleParses 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:

text
QiSkillEventListener → SkillCastPipeline.executePrimary → runtime.executePrimary

After 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:

text
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 combinationMeaning
skillHandled=true + mythicInvoked=true + fallbackInvoked=false✅ Main chain normal, skill cast
mythicInvoked=falseGating didn't pass, MM was never touched (correct — a failed Gate shouldn't EXEC)
fallbackInvoked=trueMain chain didn't handle it, fallback was taken
castAttempted=true but mythicInvoked=falseAttempted 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:

LabelStage
[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:

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(效果层)"

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 notReason
MetaSkill / Condition skill as a QS routing typeBelongs to the MythicMob domain; MM schedules it itself
MythicItem give / item trigger / item skill engineQS is not an Item engine; the payload is just an opaque skill key
Mob ~onSpawn / ~onHit / ~onTimer injectionMob behavior is configured by MM; QS doesn't inject
Bridging the full set of Mythic PlaceholdersQS's own %qinhskills_*% is enough

In other words: QS promises MM just one thingapiHelper.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):

PathEntry point
QI sideQISkillBridge.dispatchViaEvent
QS main chainQiSkillEventListener → executePrimary
QS gatewaySkillEventGateway → callEvent
QS fallbackexecuteFallback (only when "unhandled + not mythicInvoked")

The command bridge is no exception either: /qs cast still goes through SkillEventGateway → QISkillUseEvent into the same chain — which is why the command bridge and the native handler produce identical results.


📋 7. Diagnostics quick reference

Want to confirmWhat to look at
Whether the skill actually castevent flag mythicInvoked
Which station it got stuck atthe last stage label in the trace (stuck at [GATE] = blocked by gating)
Whether fallback was takenfallbackInvoked / [FALLBACK]
Whether it cast twicefallback's double-execution guard skips it; if it still repeats, check whether someone is bypassing
Whether anyone bypassed the architecturethe [BYPASS] warning in the logs
Why gating blocked itenable debug, look at the specific reason at the [GATE] stage (unlock/cooldown/resource…)

Keep reading