31 Commits
Author SHA1 Message Date
Brues c00eb24a8d fix(event): refuse glue's event table and re-validate cached claim slots
CGGameUI::Initialize (0x0048FE00) calls RegisterScriptFunctions — where
our LoadScriptFunctions hook enables claim writes — and only then
SetScriptEventTable(0xBE1198, 0x225), which builds the in-game table.
Between the two the live table at [0xCEEF68] is still glue's 26-entry
one. A third-party DLL that registers a frame from its own
LoadScriptFunctions hook fires our Frame::RegisterEvent (0x00702140)
hook in that window: Grow enlarged the glue table (26 -> 142), the
reservations claimed slots 26..125 in it, the engine freed that buffer
and built the real table, and every cached slot stayed non-negative —
never retried, invisible to IsEventValid, and a Fire would have
signalled whatever engine event sat at that id.

Two guards, both read off the engine's own build:

- GameTableInstalled: the in-game build always passes 0x225 names and
  the other DLLs' SetEventCount clamps only enlarge, so a table smaller
  than EVENT_NAME_TABLE_STATIC_COUNT is glue's. Claim and Grow refuse it.
- Stale: each reservation keeps the SStrDup'd pointer it wrote; when the
  live entry at its slot no longer holds that pointer (or name), the
  table was rebuilt behind us and every slot is forgotten and re-claimed
  before the original RegisterEvent populates any chain.

Fixes #134
2026-11-09 11:05:32 -06:00
Brues b3b950f2d8 feat(spell): spell alerts and proc glows (API only)
Adds C_SpellActivationOverlay.IsSpellOverlayed (and the global
IsSpellOverlayed), Enum.ScreenLocationType, and the four
SPELL_ACTIVATION_OVERLAY_* events. Nothing is drawn: an action-bar or
UI addon draws the alerts and the glows from these.

Glows come from the engine's own reactive-spell list. AddSpellToBook
puts every learned spell whose record has AttributesEx4 0x200 on the
list at 0x00B71150 (count 0x00B7114C), one 8-byte {spellID, usable}
entry per name. FireUsableEvents (0x004B31C0) retests the list with
the engine's usable check, stores each answer, then signals
SPELL_UPDATE_USABLE (0x107). The module subscribes to that signal
through Event::SignalHook and reads the stored answers, so the verdict
is the engine's (rage, target health, aura states, and the hidden
combo point behind Overpower) and no new hook is needed. A usable entry
glows every spellbook spell with the same name, which is the list's own
identity rule, so a bar slot with any rank glows. On stock 1.12.1 this
is Execute, Overpower, Revenge, Riposte, Counterattack, Mongoose Bite
and Hammer of Wrath; Turtle's Spell.dbc changes the set by itself.

Alerts come from two Blizzard rows whose aura exists unchanged in 1.12:
Shadow Trance (Nightfall art, glows Shadow Bolt through its class mask
via Spell::Lookup::IsFitToFamily) and druid Clearcasting
(Natures_Grace art). An Aura::Source observer reconciles on the
player's aura add/remove, rescanning the descriptor so counts can't
drift. The texture is a path; 1.12 has no file data IDs.

On world entry, HIDE fires with no spell ID and every active alert
fires SHOW again, so a /reload redraws them. Glows already lit before a
/reload do not re-fire; a button reads IsSpellOverlayed when it draws.
Logging out clears the state without events.

Event::EnteringWorld gains an observer list, because it claims
PLAYER_ENTERING_WORLD in the signal chain and later subscribers never
see it. Event::Custom's reservation cap goes from 96 to 128: the four
new events filled the old cap exactly, and a name past the cap is
dropped silently.
2026-11-01 13:35:12 -06:00
Brues 1e1463cf9a feat(unit): UNIT_IN_RANGE_UPDATE and UNIT_DISTANCE_CHECK_UPDATE
UNIT_IN_RANGE_UPDATE (unitTarget, isInRange). Nothing in the client
reacts to a distance: the player moves every frame while it moves, and
other units in view are moved a little each frame between their movement
packets. So a world-tick pass checks the units, only while a frame is
registered for the event. The pass walks the event's subscriber chain
(Event::Custom::ForEachListener, which FireValues now uses too): a frame
registered with RegisterUnitEvent adds its units
(Frame::UnitEvent::Units), and a plain registration adds pet and each
party and raid member and their pets. A token fires when
Unit::Range::InRange changes for it. UnitInRange now calls the same
function. A filter unit that is not a unit token is dropped after one
protected IsUnitToken check, since the engine's resolver raises a Lua
error for it.

UNIT_DISTANCE_CHECK_UPDATE (unitTarget, isInDistance) is true while the
client holds a group member's object. UpdateRaidMemberRecord (0x004BADC0)
has four E8 callers, which are exactly a player object entering the
object table (CGPlayer_C::PostInit 0x005DEC43, PostUpdate on reactivation
0x005DECAF) and leaving it (the release slot 0x005E9ACF, ~CGPlayer_C
0x005DD6AB for an object never released). The calls point at a site that
notes a party or raid member's GUID (IsRaidMemberOrPet, not the active
player), and the next world tick fires for each of its tokens with
whether the object table holds it then. The release slot runs before the
manager moves the object to the released table, so a fire in place would
still show the member to UnitIsVisible. No loaded DLL references the
function or the sites.
2026-10-31 04:54:35 -05:00
Brues 26c201b95b fix(event): a named unit field gets a watch-copy slot first
Every unit-field watcher keeps a mirror of its bytes in the object's
watch copy, at the slot UnitWatchedIndex (0x0047DB20) finds for the field
in the table at 0x00839E48. A field not in the table gets 0xB6, past the
unit's 170-slot copy. UNIT_MOD_CAST_SPEED (139), named for
UNIT_SPELL_HASTE, is such a field, so every watcher on it wrote the
field over memory past the copy: a pet's cast-speed update on its
owner's logoff put a 1.0f over a list link, and ClntObjMgrRelease
crashed on the next SMSG_DESTROY_OBJECT (ERROR #132 at 0x00464A02).

The table holds 166 fields and one spare entry (0xB6, the copy's last
slot, which no field asks for); UnitWatchedIndex is its only reader and
nothing writes it. EngineName now writes a field without a slot into
that spare before naming its slot, and refuses the name once the spare
is taken. Field 139 takes it, so UNIT_SPELL_HASTE keeps the engine's
watcher for every unit.
2026-10-31 03:47:17 -05:00
Brues e2a34bd18a feat(unit): UNIT_TARGET fires when a unit's target changes
1.12.1 already turns unit-field changes into events. FUN_0051bbb0
registers a watcher on unit field i for each named slot i of the static
event-name array at 0x00BE1198, and the watcher (FUN_0051bd50) signals
event i once per token of the unit through the broadcast FUN_00515e50.
The field index is the event id. UNIT_FIELD_TARGET is index 10, and the
watcher's size selector still gives it the 8 bytes of a GUID, but slot
10 has no name, so the engine never watches the field.

Event::Custom::EngineName names such a slot. It writes the name into the
static array at load, before any world init or unit watch. World init
copies the array into the live event table (0x0048FEA1), and the
engine's own watchers then fire the event for every token, with focus,
nameplateN, markN and focustarget included. The entry joins the
reservation list with a fixed slot, so it is documented, kept across
/reload, and not counted by Grow.

Slot 10 is in the glue band. Its one by-number signal is the glue
realm-list rebuild (0x0046E73D), which only CGlueMgr::Idle reaches.
2026-10-31 02:25:04 -05:00
Brues e47e881ae1 fix(event): FireValues applies RegisterUnitEvent filters
FireValues walks the subscriber chain itself and runs each OnEvent
through RunSlotWithValues, so it never reached the runner where
Frame::UnitEvent's gate sits. RegisterUnitEvent("UNIT_AURA_DELTA", unit)
fired for every unit's deltas.

Frame::UnitEvent::Accepts shares the gate's match. FireValues asks it
for each frame when the first value is a string, the same rule the
engine path uses for a %s arg1.
2026-10-31 00:21:18 -05:00
Brues 1a2f806f16 fix(event): skip the slots the engine fires by number
A NULL name in the event table did not mean the slot was free. The
engine fires events by id, and two NULL-named in-game ids still fire:
the glue band (ids 0..25, including ADDON_LIST_UPDATE at 6, which the
SMSG_ADDON_INFO listener fires whichever table is installed) and chat
type 89's id 540. Our low walk put the 5th custom event on slot 6.

Claimable() now requires an id past GLUE_EVENT_COUNT that is not in the
engine's chat-type id table, read live. Both the claim walk and Grow's
headroom count use it. Verified against benilla-re's fire-site census
and this exe's own name initializer; a live dump puts our 77 events at
31..110 with nothing below 26.
2026-10-26 17:33:12 -05:00
Brues 6c5269e3e6 feat(profiling): add per-addon memory and CPU usage
Backport UpdateAddOnMemoryUsage / GetAddOnMemoryUsage and the script
profiling family (scriptProfile, GetFunctionCPUUsage, UpdateAddOnCPUUsage,
GetAddOnCPUUsage, GetFrameCPUUsage, GetEventCPUUsage, GetScriptCPUUsage,
ResetCPUUsage), mirroring 3.3.5's engine mechanism.

- Gc::Hooks owns the GC lifecycle hooks (luaC_link, newlstr,
  luaS_newudata, freeobj); Gc::Tag stamps each new object with the
  engine's context word (3.3.5's lua_tainted rule) and sums sizes with
  the engine's own free sizes.
- lua_gctag pins: the addon load window (two call patches in
  FUN_ADDON_LOAD_ONE) and template application (engine single-name
  sites + Xml::Inherits lists).
- Profile::Cpu keeps 3.3.5's per-closure Profile records through Lua's
  own call/return hook, installed only when scriptProfile is 1; tail
  calls, error unwinds and coroutines handled.
- ScriptArgs observers time every handler run; signals counted at each
  dispatcher (call-site redirect inside FUN_FIRE_EVENT, not a MinHook).
- Frame::ScriptSlots enumerates a frame's script slots without creating
  them, for GetFrameCPUUsage.
- Add the unordered_dense submodule (v5.3.2); share ResolveRecord via
  addons/Registry; fix the AddOnEntry inline-name comments.
2026-10-23 15:31:39 -05:00
Brues 718e5a31fe refactor(addons): drop dead embed code and duplicate offsets
- Stop writing DefaultState on the embedded entry: the entry
  constructor (0x5205b9) already stores 1, and the enable-resolver hook
  forces it on.
- Ask the original exists check for the dev marker instead of reading
  and freeing the whole file.
- Remove FUN_STORM_SMEMALLOC/SMEMFREE, which duplicated
  FUN_STORM_SMEM_ALLOC/FREE; Event::Custom uses the main names.
2026-10-21 15:45:44 -05:00
brues-code 62cabb2acb Add the combat log: COMBAT_LOG_EVENT, filters and kept entries (#108)
* feat(combatlog): add COMBAT_LOG_EVENT and CombatLogGetCurrentEventInfo

Port 4.3.4's combat log events (UnitCombatLog_C.cpp) in its argument
layout: timestamp, subevent, hideCaster, source and dest GUID, name,
flags and raid flags, then the spell prefix and the subevent suffix.

- Swing, range, spell and periodic damage, misses, heals, energize,
  drain and leech, damage shields, split damage and environmental
  damage, read from the combat log messages through
  Net::PacketDispatch.
- Spell misses from the one function every miss goes through, so a
  missile's miss is sent when it arrives.
- Object flags and raid flags from the fields 4.3.4 reads. Names follow
  the 1.12 combat log namer's steps.
- CombatLogGetCurrentEventInfo returns the values only while the events
  are being sent, as on Classic Era.
- Event::Custom::FireValues sends an event with Lua values, so booleans
  and nils land where 4.3.4 puts them.
- COMBATLOG_OBJECT_* constants in the addon.

Overkill, overhealing and healing absorbs are 0: the server does not
send them.

* feat(combatlog): add casts, auras, dispels, interrupts and deaths

Port 4.3.4's layer 2 combat log subevents: SPELL_CAST_START/SUCCESS/
FAILED, SPELL_AURA_APPLIED/REMOVED and their _DOSE forms, SPELL_DISPEL,
SPELL_DISPEL_FAILED, SPELL_INTERRUPT, SPELL_EXTRA_ATTACKS, SPELL_DRAIN,
SPELL_LEECH, SPELL_RESURRECT, SPELL_DURABILITY_DAMAGE(_ALL),
SPELL_INSTAKILL, UNIT_DIED, UNIT_DESTROYED and PARTY_KILL, plus proc
resists as SPELL_MISSED.

Casts come from SPELL_START / SPELL_GO and the cast-fail log line, effects
from SPELLLOGEXECUTE as the 1.12 client reads it, auras from the engine's
aura handlers through a new Aura::Source change observer that carries the
bound caster, and deaths from the death presentation.

* feat(combatlog): add filters, kept entries and CombatLog_Object_IsA

Port 4.3.4's UnitCombatLog_C_Scripts surface: CombatLogAddFilter /
ResetFilter, the kept-entry functions (GetNumEntries, SetCurrentEntry,
GetCurrentEntry, AdvanceEntry, ClearEntries), the retention time with its
combatLogRetentionTime CVar, and CombatLog_Object_IsA. COMBAT_LOG_EVENT now
fires only for entries that match the filters. Entries and filters clear on
the return to the login screen. Adds the COMBATLOG_FILTER_* masks from
4.3.4's Constants.lua.

* docs(combatlog): move the combat log events into the Events section

* fix(combatlog): bound the execute-log loop and the retention time

- Stop an execute log's target loop at an effect that carries nothing,
  so a huge target count cannot spin.
- Clamp the retention time to what the wrap-safe age test can measure.
- Bound GetCurrentEntry's index by the entry count.
- List ENCHANT_APPLIED and ENCHANT_REMOVED among the subevents that do
  not fire, and note why effect 59 casts log SPELL_CAST_SUCCESS.
2026-10-20 23:01:29 -05:00
Brues 6812771a89 fix(event): raise the reservation cap past the 64-slot overflow
MAX_RESERVED was 64 while the codebase carries 65 AutoReserve
instantiations: 60 at file scope plus the 5 lazy function-local ones in
voicechat/Tts.cpp, which construct on first registration and only when
VanillaTTS is absent.

The constructor silently returns once g_reservedCount hits the cap, and
the file-scope 60 all run at static init, so the TTS five land last and
the 65th -- VOICE_CHAT_TTS_VOICES_UPDATE -- was dropped: Slot() stayed
-1 and Fire() no-opped, so the event never fired on any client without
VanillaTTS.

Bump to 96 and record the incident; the comment's "~40 reservations"
figure was stale from the previous overflow at 32.
2026-09-16 20:28:25 -05:00
Brues 108a91a843 feat(api): describe the API in the code, browse it with /classicapi
docs/API.md is 18.7k lines and its anchors are signature slugs, so editing a
signature breaks the 281 internal and 88 README links into it. Blizzard
solved the same problem by shipping the API as data plus a browser addon,
and warcraft.wiki.gg renders those same tables. One set of descriptors can
feed all three.

Each registration now carries one next to it: typed arguments and returns,
nilability, defaults, and a sentence. Events, enumerations and returned table
shapes are described the same way. No Script_* body changed.

Game.h gains namespace Game::Doc (Field with Req/Opt/Vararg, FieldList,
Function, Method, Structure, Event) and a trailing descriptor argument on all
six Game::Lua::Register* plus Event::Custom::AutoReserve.

api/Documentation.cpp records every registration on the first pass of each
Lua state and seals it, so /reload re-registers but records nothing and the
environment comes from which pass saw the name. Export is lazy: nothing is
materialized until C_APIDocumentation.GetSystem asks for one system, because
eager export would build thousands of tables per reload for a slash command.
_classicapi_UndocumentedAPI lists what still has no descriptor.

AddOns/!!!ClassicAPI/APIDocumentation/ ports Blizzard_APIDocumentation's eight
files. Named /classicapi, not /api: the browser holds only what ClassicAPI
adds, so /api would have implied the whole 1.12 surface. Two deliberate
differences from the retail source, both noted in the file headers: a system
search also matches its namespace, since that is the name people type; and an
undocumented function says so, because an empty signature otherwise reads as
a real one.

LinkUtil gains the SetItemRef to LinkUtil.ProcessLink dispatch. That handler
registry was dead code; a new link type now registers a handler instead of
stacking another SetItemRef override. Also defines assertsafe there, which
was undefined and only mattered once the function went live.

Documented: the C_Spell and C_SpellBook surface, the spell globals, both
spellbook enumerations, the 13 UNIT_SPELLCAST_* events, GameTooltip as the
widget-method example and C_Glue as the glue and mixed-environment example.
66 of 692 function registrations. tools/New-ApiDocSkeletons.ps1 emits
paste-ready skeletons to make the rest of the sweep tractable.

Verified in game: the browser lists, searches, and prints detail; links
render and click through; right-click copies and shift-click dumps.
2026-09-12 23:20:46 -05:00
Brues 0c8468cf2f refactor(reload): auto-register per-/reload cleanup; fix Time::Timer stale refs
Modules that keep reload-fragile file-static state (Lua registry refs,
frame/object-pointer-keyed maps, cached event-slot indices) each expose a
PrepareForReload() that DllMain called from the FrameScript_Initialize hook.
That list was hand-maintained, so a new module could silently forget to wire
itself in -- the failure mode behind the Frame::Attributes stale-ref bug and
Tooltip::SetEvents issue #33.

Add Game::ReloadAutoRegister + Game::RunReloadCleanups(), mirroring the
existing ModuleAutoRegister / HookAutoRegister / WorldTick::AutoSubscribe
patterns. Each module self-registers with one file-scope line after its
PrepareForReload:

    static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};

DllMain's 11-call manual list collapses to a single RunReloadCleanups(); a
new module can no longer be forgotten. Player::NameCache::Flush stays an
explicit call -- it's a save-before-teardown, not a stale-state clear, and
also runs on the glue-return path.

A sweep of all file-static state found one uncovered module: Time::Timer
pins C_Timer.After/NewTimer/NewTicker callbacks in the Lua registry but never
released them on /reload -- a surviving/stale ref could fire a dead closure.
Add Time::Timer::PrepareForReload (release each pending timer's registry
callback, clear the queue) and self-register it.

- Game.{h,cpp}: ReloadAutoRegister + RunReloadCleanups.
- DllMain.cpp: collapse the manual list to RunReloadCleanups(); drop 6
  now-dead includes.
- 11 existing reload modules + Time::Timer: add the self-register line.

Behavior-preserving (same cleanups, before the Lua teardown); auto-register
order is unspecified across TUs but every callback only clears its own state.
2026-08-25 18:37:44 -05:00
Brues 7f52afeef4 perf(event): fire custom events through O(1) AutoReserve::Slot()
Every custom-event fire resolved its name to a slot via
Event::Custom::Lookup - a linear strcmp scan over the ~46-entry
reservation table, on every single fire. The slot itself was already
cached per table build; only the name->reservation hop was repeated.
On a client that is purely CPU-bound, that is wasted work on paths
that burst exactly when frames are scarce (nameplate churn, cast spam,
loot rolls, mouse/modifier input).

AutoReserve now remembers its reservation index (the ctor's dedup
resolves duplicate names to the same entry) and exposes Slot() - two
loads. Reload safety is unchanged and stays centralized: Slot() reads
through the shared table PrepareForReload/RetryClaims maintain, so the
value can never go stale. Lookup is deleted; LookupByName remains for
engine-defined names.

All fire sites converted to the instance handle:
- CastEvents: the k* name constants become the AutoReserve instances,
  so all 19 fire sites stay textually identical; the Fire/FireReticle/
  FireRemote helpers take the reservation by reference.
- equipmentset: reservations move into Set.h as C++17 inline consts so
  the Data and Api TUs share one handle.
- Cache::QueryLoad::Register takes the AutoReserve pointer instead of
  an event-name string (creature/gameobject callers updated).
- Tts keeps its lazy VanillaTTS-stand-down reservations via
  null-guarded pointers set at registration.
- GlobalMouse/InputEvents/lossofcontrol instances moved above their
  fire sites (static-init list order is irrelevant).
2026-08-24 10:11:10 -05:00
Brues c54556f6a1 refactor: centralize engine field/global access via Game::Read/Ref/Ptr
Replace the raw `*reinterpret_cast<const T *>(base + Offsets::OFF_X)`
idiom (and its double-cast variant when base wasn't already a byte
pointer) with three typed accessors in Game.h:

  Game::Read<T>(base, offset)  field value read
  Game::Ref<T>(base, offset)   writable field lvalue
  Game::Ptr<T>(base, offset)   pointer to a field (arrays / out-params)
  Game::Read<T>(address) / Ref<T>(address)  fixed-VA globals

Pure mechanical refactor, zero behavior change: ~330 cast sites across
84 files collapse to 651 helper calls. Offsets stay in Offsets.h per the
single-source-of-truth rule — the helpers only remove the cast noise and
fold away the inner `reinterpret_cast<uint8_t*>(obj)` base casts.

Left as raw casts by design: FUN_* function-pointer casts, void*/char*
VA handles passed as arguments (caches, method registries), subobject-
anchor pointers fed to __thiscall helpers, `void *volatile` player-ptr
readiness gates (converting would strip volatile), and VA-as-array-base
captures that are subscripted later.
2026-08-19 11:01:19 -05:00
brues-code 29e3dab24a UNIT_SPELLCAST_* event family + GUID unit tokens (#11)
* spell: backport UNIT_SPELLCAST_* events for the local player

Adds the TBC+ cast/channel events (START, STOP, DELAYED, SUCCEEDED,
INTERRUPTED, FAILED, SENT, CHANNEL_START, CHANNEL_UPDATE, CHANNEL_STOP)
for the player, so cast-bar / rotation addons written against the modern
signature work on 1.12. Each carries (unit, castGUID, spellID, spellName,
rank) -- SENT inserts `target` at arg2 -- with a synthesized modern-shape
castGUID (Cast-3-0-0-0-<spellID>-<castUID>) shared across all of a cast's
events so START pairs with STOP. arg1 is always "player" (player-only phase).

The events are derived, not independently tracked: Spell::Cast already
hooks every cast/channel path for UnitCastingInfo/UnitChannelInfo, so
Spell::CastEvents reuses that state.
  - START/STOP/DELAYED/CHANNEL_START/STOP: polled from Cast's OnWorldTick
    against the previous snapshot.
  - SUCCEEDED: fired from the SMSG_SPELL_GO hook (covers instants too).
    Deferred for channels so it lands after CHANNEL_START (modern order).
  - INTERRUPTED/FAILED: split by result code in a Spell_C_SpellFailed
    co-hook -- a started cast that aborts (kick/move/LoS) fires INTERRUPTED
    (repeated per retry, reusing the cast guid); a pre-cast rejection fires
    FAILED.
  - SENT: minted from the CMSG_CAST_SPELL send, threading its castGUID
    forward so all of a cast's events share one guid.
  - CHANNEL_UPDATE: from a new MSG_CHANNEL_UPDATE co-hook, which also
    re-anchors g_channel.endMs to the server's remaining time so channel
    pushback (which shortens vanilla channels) shows in UnitChannelInfo.

Every fire is gated on Event::Custom::HasListeners (new), so the system
costs one pointer-compare per transition when no addon registers.

Supporting changes:
  - Net::SendObserver: shared co-hook on the NetClient send funnel with an
    AutoSubscribe list, so CastEvents (SENT) and ComboDuration (combo-point
    capture) both watch outgoing packets without owning the hook. Extracted
    from ComboDuration's private hook.
  - Cast.cpp: player channel now self-expires at its computed endMs in the
    poll (was cleared only by the ~1s-lagged +0x228 field, so CHANNEL_STOP
    trailed the real end by ~1s); wrap-safe delta compare so a long-uptime
    ms tick past 2^31 can't misfire it.

Player-only; remote-unit fan-out, FAILED_QUIET, and empowered casts are
not implemented.

* spell: extend UNIT_SPELLCAST_* to non-player units (phase 2)

Fires the six observable cast/channel events -- START, STOP,
CHANNEL_START, CHANNEL_STOP, SUCCEEDED, INTERRUPTED -- for other units,
not just the player. Each is fanned out once per unit token currently
mapping to the caster (target / focus / nameplateN / party / raid / pet /
mouseover), so a target frame, a party frame, and a nameplate cast bar
all get their own event with a shared castGUID -- matching retail.

Token fan-out reuses the engine's own GUID->token reverse map
(FUN_00515c50, nampower's unfilled GetNamesFromGUID) for native tokens,
plus focus/nameplate which the engine can't see, via
Unit::Identity::TokensForGUID. SuperWoW's raw-GUID token ("0x...") is
filtered out so only standard tokens fan out.

Remote events are derived from Spell::Cast's existing packet hooks:
  - START / CHANNEL_START from HandleSpellStart (SMSG_SPELL_START)
  - SUCCEEDED from the SPELL_GO hook (Aura::Source)
  - STOP / CHANNEL_STOP at the computed end, or on abort
  - INTERRUPTED (casts only) from HandleCastAborted (ClearCastingSpell /
    failure packets)
A per-caster state machine (PollRemote, from OnWorldTick) fires them in
modern order; a channel's SPELL_GO cast->channel ClearCastingSpell is
filtered so it doesn't self-interrupt at its own start.

Not fired for remotes (data is caster-only / client-local): SENT,
DELAYED, FAILED, CHANNEL_UPDATE.

Also in this change:
  - castGUID follows type rule from https://warcraft.wiki.gg/wiki/GUID#Cast : real casts are Type 3 with a
    time-based castUID (low 23 bits = UNIX second, high bits = per-second
    counter, matching retail's structure), threaded across a cast's events
    (player and remote converge on the same value); FAILED is Type 2 with
    a flat incrementing integer.
  - Channels never fire INTERRUPTED (player or remote) -- retail emits
    only CHANNEL_STOP whether a channel completes or is cut short.

arg1 is the caster's unit token ("player" or another token); the
spellName / rank tail extensions are unchanged.

* spell: return the castGUID as UnitCastingInfo's castID

UnitCastingInfo / CastingInfo now return the cast's castGUID as castID
(return 7) instead of nil -- the same string the UNIT_SPELLCAST_* events
carry, so an addon can correlate the polled info with the events. It's
pulled from Spell::CastEvents (the single source of the castUID) via a new
CurrentCastGuid accessor, and works for the player and remote units. The
timing stays sourced from Spell::Cast -- only the castGUID crosses over,
so there's no duplication of cast state.

UnitChannelInfo is left retail-exact (no castID; channels don't carry
one), even though our channel events do include the shared castGUID.

Also corrects the spell-cast-GUID spec attribution in comments/docs to
warcraft.wiki.gg/wiki/GUID#Cast (it was wrongly credited to Wowhead).

* spell: add UNIT_SPELLCAST_RETICLE_TARGET / _CLEAR events

Backports the ground-target reticle events for the player:
RETICLE_TARGET when an AoE placement reticle comes up (Blizzard, Flare,
Rain of Fire, ...), RETICLE_CLEAR when it's cancelled. Args are
(unit, "", spellID, spellName, rank); arg1 is always "player".

Driven by polling the engine's targeting-flag global (0x00CECAC0 -- the
predicate behind SpellIsTargeting) from OnWorldTick: 0->nonzero fires
RETICLE_TARGET with the pending spellID (0x00CEAC58, written by
Spell_C_CastSpell); nonzero->0 fires RETICLE_CLEAR. CLEAR is suppressed
on placement (a CMSG_CAST_SPELL for the reticle spell went out, seen via
the shared send observer), so it fires only on a genuine cancel --
matching retail, which emits no CLEAR when the spell is placed/cast.

The castGUID slot (arg2) is "" rather than retail's nil: there's no cast
to identify yet, and the engine's event dispatcher can't emit a nil
mid-argument-list (a NULL %s corrupts the args after it -- the nil trick
only survives as the last arg). unit (arg1) and spellID (arg3) are exact.

* spell: fire UNIT_SPELLCAST_FAILED_QUIET for the quiet result whitelist

Vanilla's Spell_C_SpellFailed dropped SPELL_FAILED_DONT_REPORT silently.
Modern surfaces suppressed (no-error-text) failures as
UNIT_SPELLCAST_FAILED_QUIET rather than FAILED.

Verified against the 3.3.5 client's unit-spellcast dispatch (FUN_007fecc0),
which selects the QUIET event for a fixed 3-code whitelist, mapped to 1.12's
SpellCastResult enum (FUN_006e23e0):
  CHARMED (0x14) . DONT_REPORT (0x17) . SPELL_IN_PROGRESS (0x61)

SPELL_IN_PROGRESS (casting while already casting / a nampower spell-queue
rejection) is the common case that previously mis-fired as a loud FAILED.

* unit: accept raw GUID literals as unit tokens

Fold a "0x<hex>" branch into the existing FUN_TOKEN_TO_GUID hook so every
Unit* function (UnitName, UnitHealth, UnitClass, UnitExists, ...) accepts a
64-bit GUID literal in place of a token. The parsed GUID goes straight to
the engine's own object-manager lookup (the path "player" takes), so an
unloaded/out-of-range GUID resolves to nil like any absent unit, no error.
Reuses the existing suffix walker, so "0x...target" composes.

Gated on SuperWoW's absence (GetModuleHandleA("SuperWoWhook.dll"), cached):
SuperWoW already provides GUID input via its own resolver hook, so when it
is loaded we defer to avoid two layers parsing the same string. Behaves
identically either way. Input direction only.
2026-07-31 21:38:44 -05:00
Brues a3f224876f event: add a correctly-timed on-demand table-grow valve
Re-introduces the event-table grow valve (removed in 538beda as
mis-timed) at the correct point. Event::Custom::Grow runs from
RetryClaims -- the frame:RegisterEvent hook -- on the first in-game
registration, which is AFTER RebuildEventTable (and SuperWoW/nampower's
clamp-to-700 that co-hooks it) but BEFORE any chain is populated. So
every chain is still an empty self-sentinel and the buffer can be moved
safely. A one-shot latch (reset per reload) runs it at most once per
build, and a hard "abort if any chain is live" scan backs the timing.

Same realloc logic the old EnsureCapacity used -- SMemAlloc a new
buffer, copy name pointers, re-init empty-sentinel anchors, swap the
{cap,count,base} globals, SMemFree the old -- bypassing SetEventCount's
destructive =700 clamp. Consumers read base fresh; the engine's reload
teardown frees our buffer + names as its own.

Dormant in practice (~200 low gaps vs ~46 reservations). Validated via a
new _classicapi_ForceEventGrow() diagnostic that forces a grow on the
next /reload: table grew 700->716, all events (ours, engine,
SuperWoW/nampower, VMT) survived with names intact, dispatch worked on
the grown table, and a following /reload cleanly freed the buffer.
2026-07-23 21:27:29 -05:00
Brues 538beda499 event: remove the mis-timed EnsureCapacity table-grow valve
EnsureCapacity was meant to grow the event table when the engine's ~200
low NULL gaps ran short of our ~46 reservations, by reallocating the
entry buffer via Storm and swapping the base/count globals. But it was
called from LoadScriptFunctions_h, which runs BEFORE RebuildEventTable
inside FUN_0048FBF0 -- so it always operated on the previous cycle's
still-populated table, tripped its own "abort if any chain is live"
guard, and never actually grew anything. Mis-timed dead code doing risky
buffer surgery on the shared event table.

Claim-NULL into the engine's low gaps is the sole mechanism now; it
always suffices, and TryClaim logs once if it somehow doesn't. Drop the
function, its Storm alloc/free helpers, and the now-inaccurate offset
comments.
2026-07-23 21:06:36 -05:00
Brues af01a4b408 event: claim NULL slots low-first + on-demand table expansion
Reworks custom-event slot reservation to be coordination-free and
durable without depending on DLL load/hook order.

- Claim NULL-name slots walking LOW->high instead of from the tail.
  SuperWoW/nampower own the high range (fixed IDs 549..655) and
  destructively clamp the table to 700, and VanillaMinimapTracking also
  walks the tail, so the top is contested; the engine's low gaps (~200,
  incl. a ~64-slot hole at 42..106) are stable and uncontested. Verified
  in-game: our events now land at 1..64, VMT alone at 699 -- clean
  separation, independent of the clamp.

- Add Event::Custom::EnsureCapacity: on-demand grow run once from
  LoadScriptFunctions_h (after module registration so all reservations
  incl. lazy TTS are counted, before any frame registers). If NULL slots
  < reserved, it reallocates the entry buffer via Storm and swaps the
  {cap,count,base} globals directly, bypassing SetEventCount's clamp --
  so it's load/hook-order independent. Defensive: reads fresh, verifies
  all chains empty before moving the buffer (aborts otherwise), frees
  only the old buffer (names are shared), publishes base before count.
  Dormant in the common case (low gaps dwarf our count).

- Drops the SetEventCount co-hook (Capacity.cpp) entirely -- nothing new
  on a SuperWoW/nampower-contended function. Keeps the one-shot
  table-full log as the backstop.

Adds Storm SMemAlloc/SMemFree + VAR_EVENT_TABLE_CAP offsets.
2026-07-23 18:13:41 -05:00
Brues 2bd33288c9 event: raise custom-event reservation cap (32 -> 64)
AutoReserve silently drops any reservation past MAX_RESERVED, and the
codebase crossed 32 file-scope reservations, so a shifting subset of custom
events never claimed a table slot -> Lookup returned -1 and Fire no-op'd.
Adding the three AUCTION_MULTISELL_* events moved the overflow onto
ITEM_DATA_LOAD_RESULT / GET_ITEM_INFO_RECEIVED, which stopped firing.

Raise the cap to 64 (well above the ~45 total incl. the lazy TTS events) and
add a comment warning to keep it above the AutoReserve count. Verified via
_classicapi_DumpAllEvents: all custom events now claim slots, item events
included.
2026-07-19 18:29:20 -05:00
Brues d9102bae3b Relicense source headers from LGPL to GPL v3
The repo was internally inconsistent: the LICENSE file is GPL v3 but every
src/ header carried the LGPL v3 notice and the README said "LGPL v3 or
later". LGPL was also the wrong fit — this is an injected mod plus a
bundled addon, not a library other code links against, so LGPL's linking
exception buys nothing.

It's also required: src/voicechat/Tts.cpp is ported from awesome_wotlk's
VoiceChat.cpp, which is GPL v3, and a derivative of GPL code cannot be
relicensed under more permissive terms. Dependencies stay compatible —
MinHook (BSD-2) and the nampower-mirrored packet shapes (BSD) are
GPL-compatible, and SAPI is covered by the system-library exception.

Swept "GNU Lesser General Public License" -> "GNU General Public License"
across all 257 src/ headers and changed the README license line to match
the GPL v3 LICENSE file. Comment-only; no build impact.
2026-06-29 16:38:16 -05:00
Brues 598c66894e Polyfill UNIT_FACTION fire-coverage on 5 paths the 1.12 engine skips
3.3.5+ fires UNIT_FACTION("player") on rep-state changes that vanilla
silently drops. Hook each path's inner setter / SMSG handler and fire
the event post-original via the engine's printf-style dispatcher:

- FactionToggleAtWar — fire if rep slot's flag byte changed
- SetFactionInactive / SetFactionActive — same shape
- SMSG_SET_FACTION_STANDINGS — fire if any of 64 flag bytes changed
  (catches AT_WAR side-effect flips on standing threshold crossings)
- SMSG_SET_FACTION_ATWAR — unconditional after byte write
- SMSG_INITIALIZE_FACTIONS — fire after initial sync at login

Hoist the event-table strcmp walk into Event::Custom::LookupByName
(sibling to Lookup, which only consults our reservation cache). Replaces
duplicated walks in event/Util.cpp::IsKnownEventName and the polyfill.
2026-06-05 20:50:51 -05:00
Brues 60147989bb Collapse Event::Custom::Fire_* into one variadic template
The six per-arity helpers (Fire_None / Fire_D / Fire_DD / Fire_DDD /
Fire_SD / Fire_S) all wrapped the same FUN_FIRE_EVENT engine call
with the same eventID<0 early-out — only the format string and arg
list differed. Collapsing into a single template Fire(eventID, fmt,
args...) keeps the printf-style format at the call site and frees us
from adding a new per-arity overload for each new event shape.
2026-05-16 22:05:07 -05:00
Brues 96a4edaaa1 Fixed _TURNING and added GLOBAL_MOUSE_ 2026-05-14 12:52:22 -05:00
Brues 6113ac24d4 EQUIPMENT_SWAP_PENDING event 2026-05-11 18:36:22 -05:00
Brues 3a83375bd8 update todo 2026-05-11 16:06:07 -05:00
Brues 3149298293 C_EquipmentSet 2026-05-11 15:13:08 -05:00
Brues 3782dc272a FACTION_STANDING_CHANGED event 2026-05-11 10:21:46 -05:00
Brues b8e3fe9b0e IsModifierKeyDown and all variants 2026-05-10 18:06:58 -05:00
Brues 879b3fedb6 Utilize Storm allocator 2026-05-08 11:46:03 -05:00
Brues 6e566fbc11 ITEM_DATA_LOAD_RESULT 2026-05-07 20:58:16 -05:00