@@ -131,6 +131,7 @@ build instructions.
131131 - [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
132132 - [`UPDATE_MOUSEOVER_UNIT` event (loss-fire fix)](#update_mouseover_unit-event-loss-fire-fix)
133133 - [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
134+ - [`UNIT_SPELLCAST_*` events](#unit_spellcast_-events)
134135
135136- [Expansion](#expansion)
136137 - [`GetClassicExpansionLevel()`](#getclassicexpansionlevel)
@@ -461,7 +462,6 @@ build instructions.
461462 - [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
462463 - [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
463464 - [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
464- - [`UNIT_SPELLCAST_*` events (player)](#unit_spellcast_-events-player)
465465 - [`C_Spell.GetSpellLevelInfo(spellID)`](#c_spellgetspelllevelinfospellid)
466466 - [`GetSpellRequiredTargetLevel(spellID)`](#getspellrequiredtargetlevelspellid)
467467
@@ -3244,6 +3244,131 @@ The cached "last form" sentinel uses `-1` for "player descriptor not
32443244yet resolvable" so transient resolution failures during early login
32453245don't get misread as leaving a form.
32463246
3247+ ### `UNIT_SPELLCAST_*` events
3248+
3249+ Backport of the TBC+ cast/channel events to 1.12, for the **local player and
3250+ other units**. Ported cast-bar / rotation addons (anything written against
3251+ the modern signature) register these instead of vanilla's arg-less
3252+ `SPELLCAST_*` events and read `unit, castGUID, spellID` directly. Thirteen
3253+ events are provided; six also fire for non-player units:
3254+
3255+ | Event | Fires when | Units | Args |
3256+ |-------|-----------|-------|------|
3257+ | `UNIT_SPELLCAST_SENT` | `CMSG_CAST_SPELL` leaves the client (earliest point) | player | `unit, target, castGUID, spellID, spellName, rank` |
3258+ | `UNIT_SPELLCAST_START` | a cast-time spell begins | all | `unit, castGUID, spellID, spellName, rank` |
3259+ | `UNIT_SPELLCAST_STOP` | a cast-time spell ends (any reason) | all | same |
3260+ | `UNIT_SPELLCAST_DELAYED` | pushback extends the cast | player | same |
3261+ | `UNIT_SPELLCAST_SUCCEEDED` | the spell goes off (`SMSG_SPELL_GO`) — incl. instants | all | same |
3262+ | `UNIT_SPELLCAST_INTERRUPTED` | a started **cast** is interrupted (kick, movement, LoS) — never for channels | all | same |
3263+ | `UNIT_SPELLCAST_FAILED` | a cast is rejected before it starts (range, mana, cooldown) with an error shown | player | same |
3264+ | `UNIT_SPELLCAST_FAILED_QUIET` | a cast fails with **no** error shown (spammy retry, reticle cancel, …) | player | same |
3265+ | `UNIT_SPELLCAST_CHANNEL_START` | a channel begins | all | same |
3266+ | `UNIT_SPELLCAST_CHANNEL_UPDATE` | pushback shortens a channel | player | same |
3267+ | `UNIT_SPELLCAST_CHANNEL_STOP` | a channel ends | all | same |
3268+ | `UNIT_SPELLCAST_RETICLE_TARGET` | a ground-target reticle appears (AoE placement — Blizzard, Flare, …) | player | `unit, "", spellID, spellName, rank` |
3269+ | `UNIT_SPELLCAST_RETICLE_CLEAR` | the reticle is placed or cancelled | player | `unit, "", spellID, spellName, rank` |
3270+
3271+ `unit` (arg1) is the token of the casting unit — `"player"` for your own
3272+ casts, or a unit token (`"target"`, `"focus"`, `"party3"`, `"nameplate2"`,
3273+ `"pet"`, `"mouseover"`, …) for another unit. `spellName` / `rank` are
3274+ ClassicAPI tail extensions (modern stops at `spellID`); addons reading only
3275+ the first three positional args are unaffected.
3276+
3277+ **Per-token fan-out.** A caster GUID can map to several tokens at once (your
3278+ `target` is also `party2` and `nameplate1`). Like retail, the event fires
3279+ **once per token** currently pointing at the caster, so a `target`-frame
3280+ cast bar, a `party2` frame, and a nameplate cast bar each get their own
3281+ event with the same castGUID. Tokens are resolved fresh at fire time (they
3282+ shift frame-to-frame). If you run **SuperWoW**, its raw-GUID token (`"0x…"`)
3283+ is deliberately filtered out — only standard tokens are fanned out.
3284+
3285+ **Non-player limits.** Only the six events above fire for other units, and
3286+ they're **best-effort** — driven purely by the packets an observer receives:
3287+ - `SENT` never fires (only your own outgoing casts are visible).
3288+ - `DELAYED` / `CHANNEL_UPDATE` never fire (pushback is sent only to the
3289+ caster, so another unit's bar can't stretch/shrink from damage).
3290+ - `FAILED` never fires (a pre-cast requirement failure is client-local).
3291+ - A remote cast/channel only has timing from the moment its
3292+ `SMSG_SPELL_START` was observed; casters who were already casting when
3293+ they came into range have no start time.
3294+
3295+ **Channels never fire `INTERRUPTED`.** Retail emits only `CHANNEL_STOP` when a
3296+ channel ends, whether it completed or was cut short (verified against retail),
3297+ so ClassicAPI matches that for both the player and other units. `INTERRUPTED`
3298+ is a cast-only event.
3299+
3300+ **Reticle events** fire for ground-targeted (AoE) spells only, always for the
3301+ player: `RETICLE_TARGET` when the placement reticle comes up, `RETICLE_CLEAR`
3302+ when it's placed or cancelled. There's no cast yet, so the castGUID slot
3303+ (arg2) is empty — retail pushes `nil` there, but the engine's event
3304+ dispatcher can't emit a `nil` mid-argument-list, so ClassicAPI pushes `""`
3305+ instead. `unit` (arg1) and `spellID` (arg3) are exact; arg2 is the only
3306+ difference and is inconsequential for a reticle.
3307+
3308+ **castGUID.** A synthesized string in the modern shape
3309+ `Cast-<type>-<serverID>-<instanceID>-<zoneUID>-<spellID>-<castUID>`. Vanilla
3310+ can't know server / instance / zone, so those three fields are `0`; the
3311+ load-bearing parts are the `spellID` (field 6, which addons `strsplit("-")`
3312+ out) and a unique-per-cast `castUID` (field 7). **Every event of one cast
3313+ carries the same castGUID**, so `SENT` → `START`/`CHANNEL_START` →
3314+ `SUCCEEDED` → `STOP`/`CHANNEL_STOP` all pair up — including across the caster
3315+ and observers (they converge on the same value), and a chained same-spell
3316+ recast gets its own castUID. The `type` and `castUID` follow the
3317+ [spell-cast-GUID spec](https://warcraft.wiki.gg/wiki/GUID#Cast):
3318+ - **Type 3** (real casts — the common case): `castUID` is time-based — the
3319+ low 23 bits are the cast's UNIX-epoch second, the higher bits a per-second
3320+ counter.
3321+ - **Type 2** (`UNIT_SPELLCAST_FAILED` — a local-only cast that never reached
3322+ the server): `castUID` is a plain locally-incrementing integer.
3323+
3324+ **Ordering** matches modern:
3325+
3326+ - Cast-time spell: `SENT → START → SUCCEEDED → STOP`.
3327+ - Channel: `SENT → CHANNEL_START → SUCCEEDED → CHANNEL_STOP` (CHANNEL_START
3328+ before SUCCEEDED, as on retail).
3329+ - Instant: `SENT → SUCCEEDED`.
3330+
3331+ **INTERRUPTED vs FAILED vs FAILED_QUIET** follow modern's split: a spell that
3332+ never started (out of range, not enough mana, on cooldown, LoS to a target)
3333+ fires `FAILED`, except for a fixed whitelist of "quiet" `SpellCastResult`
3334+ codes that fire `FAILED_QUIET` instead — `SPELL_IN_PROGRESS` (casting while
3335+ already casting / a spell-queue rejection), `DONT_REPORT` (fake fails, a
3336+ cancelled ground reticle), and `CHARMED`. That whitelist mirrors the 3.3.5
3337+ client's own unit-spellcast dispatch, mapped to vanilla's `SpellCastResult`
3338+ enum. A spell that was *already casting* and gets stopped (an enemy kick,
3339+ moving to cancel, breaking LoS mid-cast) fires `INTERRUPTED`. Holding
3340+ the cast key while running fires `INTERRUPTED` repeatedly (once per retry),
3341+ each reusing the interrupted cast's castGUID — matching retail.
3342+
3343+ **Channel pushback (player).** Taking damage while channeling shortens the
3344+ channel in vanilla; `CHANNEL_UPDATE` fires on each hit and
3345+ [`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)'s
3346+ `endTimeMs` re-anchors to the server's new remaining time, so cast bars
3347+ shrink correctly. (The event carries no time — like retail it's a "re-read
3348+ now" trigger; timing is read back from `UnitChannelInfo`.)
3349+
3350+ Every fire is gated on whether any frame is registered for that event, so
3351+ the whole system costs one pointer-compare per state transition when no
3352+ addon uses it (no arg synthesis, no DBC lookups, no per-token fan-out).
3353+
3354+ ```lua
3355+ local f = CreateFrame("Frame")
3356+ for _, e in ipairs({
3357+ "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP",
3358+ "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_START",
3359+ }) do f:RegisterEvent(e) end
3360+ f:SetScript("OnEvent", function()
3361+ -- vanilla passes event/arg1/... as globals, not function params
3362+ if arg1 == "target" then print(event, arg3) end -- arg3 = spellID
3363+ end)
3364+ ```
3365+
3366+ > **Additive to the vanilla `SPELLCAST_*` events.** The engine's own arg-less
3367+ > `SPELLCAST_START` / `SPELLCAST_CHANNEL_UPDATE` / … still fire as before;
3368+ > these `UNIT_`-prefixed events are the modern layer on top. The empowered-cast
3369+ > events (`UNIT_SPELLCAST_EMPOWER_*`, a Dragonflight addition) are not
3370+ > implemented — vanilla has no empowered casts.
3371+
32473372## Expansion
32483373
32493374Helpers shipped by modern Classic Era / Cata Classic for addons that
@@ -11486,131 +11611,6 @@ otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
1148611611**`nil` times**. The player path is unchanged (full timing). Same
1148711612placeholder fields as `C_Spell.UnitCastingInfo`.
1148811613
11489- ### `UNIT_SPELLCAST_*` events
11490-
11491- Backport of the TBC+ cast/channel events to 1.12, for the **local player and
11492- other units**. Ported cast-bar / rotation addons (anything written against
11493- the modern signature) register these instead of vanilla's arg-less
11494- `SPELLCAST_*` events and read `unit, castGUID, spellID` directly. Thirteen
11495- events are provided; six also fire for non-player units:
11496-
11497- | Event | Fires when | Units | Args |
11498- |-------|-----------|-------|------|
11499- | `UNIT_SPELLCAST_SENT` | `CMSG_CAST_SPELL` leaves the client (earliest point) | player | `unit, target, castGUID, spellID, spellName, rank` |
11500- | `UNIT_SPELLCAST_START` | a cast-time spell begins | all | `unit, castGUID, spellID, spellName, rank` |
11501- | `UNIT_SPELLCAST_STOP` | a cast-time spell ends (any reason) | all | same |
11502- | `UNIT_SPELLCAST_DELAYED` | pushback extends the cast | player | same |
11503- | `UNIT_SPELLCAST_SUCCEEDED` | the spell goes off (`SMSG_SPELL_GO`) — incl. instants | all | same |
11504- | `UNIT_SPELLCAST_INTERRUPTED` | a started **cast** is interrupted (kick, movement, LoS) — never for channels | all | same |
11505- | `UNIT_SPELLCAST_FAILED` | a cast is rejected before it starts (range, mana, cooldown) with an error shown | player | same |
11506- | `UNIT_SPELLCAST_FAILED_QUIET` | a cast fails with **no** error shown (spammy retry, reticle cancel, …) | player | same |
11507- | `UNIT_SPELLCAST_CHANNEL_START` | a channel begins | all | same |
11508- | `UNIT_SPELLCAST_CHANNEL_UPDATE` | pushback shortens a channel | player | same |
11509- | `UNIT_SPELLCAST_CHANNEL_STOP` | a channel ends | all | same |
11510- | `UNIT_SPELLCAST_RETICLE_TARGET` | a ground-target reticle appears (AoE placement — Blizzard, Flare, …) | player | `unit, "", spellID, spellName, rank` |
11511- | `UNIT_SPELLCAST_RETICLE_CLEAR` | the reticle is placed or cancelled | player | `unit, "", spellID, spellName, rank` |
11512-
11513- `unit` (arg1) is the token of the casting unit — `"player"` for your own
11514- casts, or a unit token (`"target"`, `"focus"`, `"party3"`, `"nameplate2"`,
11515- `"pet"`, `"mouseover"`, …) for another unit. `spellName` / `rank` are
11516- ClassicAPI tail extensions (modern stops at `spellID`); addons reading only
11517- the first three positional args are unaffected.
11518-
11519- **Per-token fan-out.** A caster GUID can map to several tokens at once (your
11520- `target` is also `party2` and `nameplate1`). Like retail, the event fires
11521- **once per token** currently pointing at the caster, so a `target`-frame
11522- cast bar, a `party2` frame, and a nameplate cast bar each get their own
11523- event with the same castGUID. Tokens are resolved fresh at fire time (they
11524- shift frame-to-frame). If you run **SuperWoW**, its raw-GUID token (`"0x…"`)
11525- is deliberately filtered out — only standard tokens are fanned out.
11526-
11527- **Non-player limits.** Only the six events above fire for other units, and
11528- they're **best-effort** — driven purely by the packets an observer receives:
11529- - `SENT` never fires (only your own outgoing casts are visible).
11530- - `DELAYED` / `CHANNEL_UPDATE` never fire (pushback is sent only to the
11531- caster, so another unit's bar can't stretch/shrink from damage).
11532- - `FAILED` never fires (a pre-cast requirement failure is client-local).
11533- - A remote cast/channel only has timing from the moment its
11534- `SMSG_SPELL_START` was observed; casters who were already casting when
11535- they came into range have no start time.
11536-
11537- **Channels never fire `INTERRUPTED`.** Retail emits only `CHANNEL_STOP` when a
11538- channel ends, whether it completed or was cut short (verified against retail),
11539- so ClassicAPI matches that for both the player and other units. `INTERRUPTED`
11540- is a cast-only event.
11541-
11542- **Reticle events** fire for ground-targeted (AoE) spells only, always for the
11543- player: `RETICLE_TARGET` when the placement reticle comes up, `RETICLE_CLEAR`
11544- when it's placed or cancelled. There's no cast yet, so the castGUID slot
11545- (arg2) is empty — retail pushes `nil` there, but the engine's event
11546- dispatcher can't emit a `nil` mid-argument-list, so ClassicAPI pushes `""`
11547- instead. `unit` (arg1) and `spellID` (arg3) are exact; arg2 is the only
11548- difference and is inconsequential for a reticle.
11549-
11550- **castGUID.** A synthesized string in the modern shape
11551- `Cast-<type>-<serverID>-<instanceID>-<zoneUID>-<spellID>-<castUID>`. Vanilla
11552- can't know server / instance / zone, so those three fields are `0`; the
11553- load-bearing parts are the `spellID` (field 6, which addons `strsplit("-")`
11554- out) and a unique-per-cast `castUID` (field 7). **Every event of one cast
11555- carries the same castGUID**, so `SENT` → `START`/`CHANNEL_START` →
11556- `SUCCEEDED` → `STOP`/`CHANNEL_STOP` all pair up — including across the caster
11557- and observers (they converge on the same value), and a chained same-spell
11558- recast gets its own castUID. The `type` and `castUID` follow the
11559- [spell-cast-GUID spec](https://warcraft.wiki.gg/wiki/GUID#Cast):
11560- - **Type 3** (real casts — the common case): `castUID` is time-based — the
11561- low 23 bits are the cast's UNIX-epoch second, the higher bits a per-second
11562- counter.
11563- - **Type 2** (`UNIT_SPELLCAST_FAILED` — a local-only cast that never reached
11564- the server): `castUID` is a plain locally-incrementing integer.
11565-
11566- **Ordering** matches modern:
11567-
11568- - Cast-time spell: `SENT → START → SUCCEEDED → STOP`.
11569- - Channel: `SENT → CHANNEL_START → SUCCEEDED → CHANNEL_STOP` (CHANNEL_START
11570- before SUCCEEDED, as on retail).
11571- - Instant: `SENT → SUCCEEDED`.
11572-
11573- **INTERRUPTED vs FAILED vs FAILED_QUIET** follow modern's split: a spell that
11574- never started (out of range, not enough mana, on cooldown, LoS to a target)
11575- fires `FAILED`, except for a fixed whitelist of "quiet" `SpellCastResult`
11576- codes that fire `FAILED_QUIET` instead — `SPELL_IN_PROGRESS` (casting while
11577- already casting / a spell-queue rejection), `DONT_REPORT` (fake fails, a
11578- cancelled ground reticle), and `CHARMED`. That whitelist mirrors the 3.3.5
11579- client's own unit-spellcast dispatch, mapped to vanilla's `SpellCastResult`
11580- enum. A spell that was *already casting* and gets stopped (an enemy kick,
11581- moving to cancel, breaking LoS mid-cast) fires `INTERRUPTED`. Holding
11582- the cast key while running fires `INTERRUPTED` repeatedly (once per retry),
11583- each reusing the interrupted cast's castGUID — matching retail.
11584-
11585- **Channel pushback (player).** Taking damage while channeling shortens the
11586- channel in vanilla; `CHANNEL_UPDATE` fires on each hit and
11587- [`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)'s
11588- `endTimeMs` re-anchors to the server's new remaining time, so cast bars
11589- shrink correctly. (The event carries no time — like retail it's a "re-read
11590- now" trigger; timing is read back from `UnitChannelInfo`.)
11591-
11592- Every fire is gated on whether any frame is registered for that event, so
11593- the whole system costs one pointer-compare per state transition when no
11594- addon uses it (no arg synthesis, no DBC lookups, no per-token fan-out).
11595-
11596- ```lua
11597- local f = CreateFrame("Frame")
11598- for _, e in ipairs({
11599- "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_STOP",
11600- "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_START",
11601- }) do f:RegisterEvent(e) end
11602- f:SetScript("OnEvent", function()
11603- -- vanilla passes event/arg1/... as globals, not function params
11604- if arg1 == "target" then print(event, arg3) end -- arg3 = spellID
11605- end)
11606- ```
11607-
11608- > **Additive to the vanilla `SPELLCAST_*` events.** The engine's own arg-less
11609- > `SPELLCAST_START` / `SPELLCAST_CHANNEL_UPDATE` / … still fire as before;
11610- > these `UNIT_`-prefixed events are the modern layer on top. The empowered-cast
11611- > events (`UNIT_SPELLCAST_EMPOWER_*`, a Dragonflight addition) are not
11612- > implemented — vanilla has no empowered casts.
11613-
1161411614### `C_Spell.GetSpellLevelInfo(spellID)`
1161511615
1161611616Returns the raw `Spell.dbc` level fields for a spell:
0 commit comments