Compare commits

..
2 Commits
Author SHA1 Message Date
Brues be6dc200fe fix(addon): restore MouseIsOver's stock 1/nil return contract
The embedded addon replaces FrameXML's MouseIsOver with a wrapper over
the native region:IsMouseOver(), which returns a boolean. Stock 1.12's
MouseIsOver (UIParent.lua:1718) returns 1 on hit and nil otherwise, and
vanilla-era addons compare against that literal - Quiver gates every
widget click on `MouseIsOver(f) == 1`, which `true` silently fails,
deadening its entire config dialog (buttons, checkboxes, icon clicks).

Return 1/nil from the wrapper. 1 is truthy, so modern-style
`if MouseIsOver(f) then` callers are unaffected.
2026-08-24 10:37:39 -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
31 changed files with 176 additions and 184 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
function MouseIsOver(region, topOffset, bottomOffset, leftOffset, rightOffset)
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset);
if region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset) then
return 1;
end
end
+6 -9
View File
@@ -68,12 +68,9 @@ namespace AuctionHouse::PostItem {
namespace {
constexpr const char *kEvtStart = "AUCTION_MULTISELL_START";
constexpr const char *kEvtUpdate = "AUCTION_MULTISELL_UPDATE";
constexpr const char *kEvtFailure = "AUCTION_MULTISELL_FAILURE";
const Event::Custom::AutoReserve _r1{kEvtStart};
const Event::Custom::AutoReserve _r2{kEvtUpdate};
const Event::Custom::AutoReserve _r3{kEvtFailure};
const Event::Custom::AutoReserve _evtStart{"AUCTION_MULTISELL_START"};
const Event::Custom::AutoReserve _evtUpdate{"AUCTION_MULTISELL_UPDATE"};
const Event::Custom::AutoReserve _evtFailure{"AUCTION_MULTISELL_FAILURE"};
// Generous per-step abort bound. A split or a post is one server round-trip
// (~tens of frames); 600 frames (~10s at 60fps) means "something's wrong"
@@ -206,7 +203,7 @@ bool FindFreeGeneralSlot(void *L, int *outBag, int *outSlot) {
void Finish(bool success) {
g_job.active = false;
if (!success)
Event::Custom::Fire(Event::Custom::Lookup(kEvtFailure), "");
Event::Custom::Fire(_evtFailure.Slot(), "");
// success needs no event: the last PostWait already fired
// AUCTION_MULTISELL_UPDATE(total, total).
}
@@ -279,7 +276,7 @@ void Tick() {
// Post confirmed once the server pulls the item out of the slot.
if (w.item == nullptr || w.guid != g_job.postGuid) {
++g_job.postedStacks;
Event::Custom::Fire(Event::Custom::Lookup(kEvtUpdate), "%d%d",
Event::Custom::Fire(_evtUpdate.Slot(), "%d%d",
g_job.postedStacks, g_job.totalStacks);
g_job.phase = Phase::Idle;
g_job.waitTicks = 0;
@@ -400,7 +397,7 @@ int __fastcall Script_C_AuctionHouse_PostItem(void *L) {
g_job.runTime = runTime;
g_job.phase = Phase::Idle;
Event::Custom::Fire(Event::Custom::Lookup(kEvtStart), "");
Event::Custom::Fire(_evtStart.Slot(), "");
Game::Lua::PushBool(L, true);
return 1;
}
+1 -1
View File
@@ -143,7 +143,7 @@ void OnWorldTick() {
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
return;
g_pending = false;
const int slot = Event::Custom::Lookup(kEventName);
const int slot = _reserve.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "");
// Notify C++ subscribers of the bag-change frame (see UpdateDelayed.h).
+7 -7
View File
@@ -61,8 +61,8 @@ constexpr int kMaxWaitTicks = 1200; // give up and fire failure after this many
struct Registered {
void *cache;
const char *eventName;
uintptr_t getRecord; // this cache class's _GetRecord
const Event::Custom::AutoReserve *reserve; // completion event's reservation
uintptr_t getRecord; // this cache class's _GetRecord
Pending pending[kMaxPending];
int pendingCount;
};
@@ -79,8 +79,7 @@ Registered *Find(void *cache) {
}
void Fire(const Registered *r, uint32_t id, bool success) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(r->eventName),
static_cast<int>(id), success);
Event::Custom::FireIdSuccess(r->reserve->Slot(), static_cast<int>(id), success);
}
int FindPending(const Registered *r, uint32_t id) {
@@ -176,15 +175,16 @@ const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr) {
if (cacheInstance == nullptr || eventName == nullptr || getRecordAddr == 0)
void Register(void *cacheInstance, const Event::Custom::AutoReserve *reserve,
uintptr_t getRecordAddr) {
if (cacheInstance == nullptr || reserve == nullptr || getRecordAddr == 0)
return;
if (Find(cacheInstance) != nullptr) // idempotent (safe across /reload)
return;
if (g_cacheCount >= kMaxCaches)
return;
g_caches[g_cacheCount].cache = cacheInstance;
g_caches[g_cacheCount].eventName = eventName;
g_caches[g_cacheCount].reserve = reserve;
g_caches[g_cacheCount].getRecord = getRecordAddr;
g_caches[g_cacheCount].pendingCount = 0;
++g_cacheCount;
+14 -10
View File
@@ -15,6 +15,10 @@
#include <cstdint>
namespace Event::Custom {
struct AutoReserve;
}
// Shared async-load machinery for the client query caches that aren't
// item/quest — creature, gameobject, and (potentially) the other
// SMSG_*_QUERY caches. Modeled on `Item::Data`'s pending/timeout/event
@@ -30,10 +34,9 @@
// functions, so hooking creature's and gameobject's separately is fine);
// item/quest have their own parsers and aren't touched.
//
// static const char *kEvent = "CREATURE_DATA_LOAD_RESULT";
// static const Event::Custom::AutoReserve _r{kEvent}; // reserve the event
// static const Event::Custom::AutoReserve _r{"CREATURE_DATA_LOAD_RESULT"};
// ...
// Cache::QueryLoad::Register(cacheInstance, kEvent,
// Cache::QueryLoad::Register(cacheInstance, &_r,
// Offsets::FUN_CREATURE_GET_RECORD); // at init
// Cache::QueryLoad::RequestLoad(cacheInstance, id); // fire a load
//
@@ -44,13 +47,14 @@ namespace Cache::QueryLoad {
// Register a query cache for async-load tracking. `cacheInstance` is the
// cache global (e.g. `Offsets::VAR_CREATURE_CACHE` cast to void*);
// `eventName` is a Custom event the caller has reserved via
// `Event::Custom::AutoReserve`, fired `(id, success)` on completion;
// `getRecordAddr` is the cache class's `_GetRecord` (e.g.
// `Offsets::FUN_CREATURE_GET_RECORD`). `cacheInstance`/`eventName` must
// outlive the process (pass static values). Idempotent — calling again
// for the same cache is a no-op (safe across /reload).
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr);
// `reserve` is the caller's `Event::Custom::AutoReserve` for the
// completion event, fired `(id, success)`; `getRecordAddr` is the cache
// class's `_GetRecord` (e.g. `Offsets::FUN_CREATURE_GET_RECORD`).
// `cacheInstance`/`reserve` must outlive the process (pass statics).
// Idempotent — calling again for the same cache is a no-op (safe across
// /reload).
void Register(void *cacheInstance, const Event::Custom::AutoReserve *reserve,
uintptr_t getRecordAddr);
// True if `id` is already in the cache (pure peek, no network query).
bool IsCached(void *cacheInstance, uint32_t id);
+1 -1
View File
@@ -361,7 +361,7 @@ void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeIDs",
&Script_GetCreatureTypeIDs);
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
&_reserveLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -6
View File
@@ -386,7 +386,7 @@ int __fastcall Script_UseEquipmentSet(void *L) {
const uint32_t setID = ArgSetID(L, 1);
const Set *s = Data::FindByID(setID);
if (s == nullptr) {
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
const int evt = kEvtSwapFinished.Slot();
if (evt >= 0)
Event::Custom::Fire(evt, "%d%d", 0, static_cast<int>(setID));
Game::Lua::PushBoolean(L, 0);
@@ -396,7 +396,7 @@ int __fastcall Script_UseEquipmentSet(void *L) {
// Fire PENDING right after the set-exists check, before any of
// the swap work. Addon UI can use this to gate swap-in-progress
// visuals.
const int pendingEvt = Event::Custom::Lookup(kSwapPendingEvent);
const int pendingEvt = kEvtSwapPending.Slot();
if (pendingEvt >= 0)
Event::Custom::Fire(pendingEvt, "%d", static_cast<int>(setID));
@@ -569,7 +569,7 @@ int __fastcall Script_UseEquipmentSet(void *L) {
virtualPaperdoll[targetSlot - 1] = 0;
}
const int evt = Event::Custom::Lookup(kSwapFinishedEvent);
const int evt = kEvtSwapFinished.Slot();
if (evt >= 0)
Event::Custom::Fire(evt, "%d%d", 1, static_cast<int>(setID));
@@ -620,8 +620,5 @@ static void RegisterLuaFunctions() {
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
static const Event::Custom::AutoReserve _reserve{kEventName};
static const Event::Custom::AutoReserve _reservePending{kSwapPendingEvent};
static const Event::Custom::AutoReserve _reserveFinished{kSwapFinishedEvent};
} // namespace EquipmentSet::Api
+1 -1
View File
@@ -75,7 +75,7 @@ uint32_t NextAvailableID() {
}
void FireChanged() {
const int slot = Event::Custom::Lookup(kEventName);
const int slot = kEvtSetsChanged.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "");
}
+8 -5
View File
@@ -13,6 +13,8 @@
#pragma once
#include "event/Custom.h"
#include <cstdint>
#include <string>
@@ -109,8 +111,9 @@ constexpr int LOC_MISSING = -1;
// coincidence, not load-bearing).
constexpr int LOC_IGNORED = 1;
// Custom event names. All three are reserved at module-init time
// via `Event::Custom::AutoReserve` in `Api.cpp`.
// Custom event reservations, shared by the Data and Api TUs (C++17 inline
// variables — one instance each; the AutoReserve ctor dedups by name anyway).
// Fire via `kEvt*.Slot()`.
//
// EQUIPMENT_SETS_CHANGED — no payload; fires on any mutation
// (create / save / modify / delete /
@@ -130,8 +133,8 @@ constexpr int LOC_IGNORED = 1;
// pickup→equip pair actually completed,
// matching modern's "we dispatched"
// definition of success).
constexpr const char *kEventName = "EQUIPMENT_SETS_CHANGED";
constexpr const char *kSwapPendingEvent = "EQUIPMENT_SWAP_PENDING";
constexpr const char *kSwapFinishedEvent = "EQUIPMENT_SWAP_FINISHED";
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
} // namespace EquipmentSet
+10 -10
View File
@@ -244,25 +244,25 @@ void Grow() {
} // namespace
AutoReserve::AutoReserve(const char *name) {
if (name == nullptr || g_reservedCount >= MAX_RESERVED)
if (name == nullptr)
return;
// Dedup: a name reserved twice resolves both instances to the one entry.
for (int i = 0; i < g_reservedCount; ++i) {
if (std::strcmp(g_reserved[i].name, name) == 0)
if (std::strcmp(g_reserved[i].name, name) == 0) {
index_ = i;
return;
}
}
if (g_reservedCount >= MAX_RESERVED)
return; // overflow — index_ stays -1, Slot() reports unclaimed
g_reserved[g_reservedCount].name = name;
g_reserved[g_reservedCount].slot = -1;
index_ = g_reservedCount;
++g_reservedCount;
}
int Lookup(const char *name) {
if (name == nullptr)
return -1;
for (int i = 0; i < g_reservedCount; ++i) {
if (std::strcmp(g_reserved[i].name, name) == 0)
return g_reserved[i].slot;
}
return -1;
int AutoReserve::Slot() const {
return index_ >= 0 ? g_reserved[index_].slot : -1;
}
int LookupByName(const char *name) {
+22 -17
View File
@@ -37,31 +37,36 @@ namespace Event::Custom {
// DLL operate on the table independently of the others.
//
// The name pointer must outlive the engine (a string literal does).
// Same name reserved twice is deduped; reserving more than `MAX_RESERVED`
// names total (see Custom.cpp) silently drops the overflow — keep that cap
// above the codebase-wide `AutoReserve` count.
// Same name reserved twice is deduped (both instances resolve to the same
// reservation); reserving more than `MAX_RESERVED` names total (see
// Custom.cpp) silently drops the overflow — keep that cap above the
// codebase-wide `AutoReserve` count.
//
// Fire through the instance: `Fire(_r.Slot(), …)`. `Slot()` is the O(1)
// read of the reservation's current slot id — it resolves through the
// shared reservation table, so reload invalidation (`PrepareForReload`)
// and re-claiming (`RetryClaims`) stay centralized and the value is never
// stale. Returns -1 while unclaimed (before the first Lua-side
// `RegisterEvent` triggers `RetryClaims`), which `Fire` treats as a no-op.
// Do NOT cache the return across frames — read it at fire time (the read
// is two loads; there is nothing to save).
struct AutoReserve {
explicit AutoReserve(const char *name);
};
int Slot() const;
// Returns the slot id currently assigned to `name`, or -1 if not yet
// claimed (e.g. before the first Lua-side `RegisterEvent` has
// triggered `RetryClaims`). Slot indices may change across `/reload`,
// so call this at fire time rather than caching the value.
//
// Only consults our reservation cache — for engine-defined events
// (UNIT_FACTION, BAG_UPDATE, etc.) use `LookupByName`.
int Lookup(const char *name);
private:
int index_ = -1; // into the reservation table; -1 = dropped (overflow)
};
// Walks the live engine event table at `[VAR_EVENT_TABLE_BASE_PTR]`
// looking for an entry whose name strcmps equal to `name`. Returns
// the slot index, or -1 if no entry matches.
//
// Works for both engine-defined events AND our `AutoReserve`-claimed
// custom events (both populate the same table). Slightly slower than
// `Lookup` (O(table size) vs O(reservations)) — prefer `Lookup` for
// names we've reserved. Use this for engine-defined events we want to
// fire ourselves (e.g., polyfilling missing event dispatches).
// custom events (both populate the same table). O(table size) — for
// names we've reserved, prefer the O(1) `AutoReserve::Slot()`. Use this
// for engine-defined events we want to fire ourselves (e.g., polyfilling
// missing event dispatches).
int LookupByName(const char *name);
// True iff at least one frame is currently registered for the event in
@@ -88,7 +93,7 @@ bool HasListeners(int slot);
// Fire(slot, "%s%d", keyName, down); // MODIFIER_STATE_CHANGED(key, down)
//
// No-op for `eventID < 0`, which lets callers cheaply guard on the
// `Lookup()` result without an explicit if.
// `Slot()` result without an explicit if.
template <typename... Args>
inline void Fire(int eventID, const char *format, Args... args) {
if (eventID < 0)
+1 -1
View File
@@ -98,7 +98,7 @@ void __fastcall FireNotify_h(int factionID, int delta) {
// any addon relying on the chat event firing first would expect.
FireNotify_o(factionID, delta);
const int slot = Event::Custom::Lookup(kEventName);
const int slot = _reserve.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "%d%d%d", factionID, newStanding, delta);
+1 -1
View File
@@ -107,7 +107,7 @@ void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_GameObjectInfo", "RequestLoadGameObjectByID",
&Script_RequestLoadGameObjectByID);
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE),
kGameObjectDataLoadResult, Offsets::FUN_GAMEOBJECT_GET_RECORD);
&_reserveLoadResult, Offsets::FUN_GAMEOBJECT_GET_RECORD);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -5
View File
@@ -64,8 +64,8 @@ namespace Input::GlobalMouse {
namespace {
constexpr const char *kDownEvent = "GLOBAL_MOUSE_DOWN";
constexpr const char *kUpEvent = "GLOBAL_MOUSE_UP";
const Event::Custom::AutoReserve _reserveDown{"GLOBAL_MOUSE_DOWN"};
const Event::Custom::AutoReserve _reserveUp{"GLOBAL_MOUSE_UP"};
// Held-state bitmap — bit `i` set means the button with 1-based
// `IsMouseButtonDown` ID `i+1` is currently held. Modern API IDs:
@@ -154,7 +154,7 @@ void ProcessMouseMessage(UINT msg, WPARAM wParam) {
else g_mouseButtonMask &= ~bit;
}
const int slot = Event::Custom::Lookup(down ? kDownEvent : kUpEvent);
const int slot = (down ? _reserveDown : _reserveUp).Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "%s", name);
}
@@ -241,8 +241,6 @@ void RegisterLuaFunctions() {
InstallHook();
}
const Event::Custom::AutoReserve _reserveDown{kDownEvent};
const Event::Custom::AutoReserve _reserveUp{kUpEvent};
const Tick::WorldTick::AutoSubscribe _evict{&EvictTick};
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+3 -5
View File
@@ -102,11 +102,9 @@ void SetBit(int bitIdx, bool down) {
if (down) g_modifierMask |= bit;
else g_modifierMask &= ~bit;
// Slot was assigned when the engine populated its event table; our
// hook on `RebuildEventTable` recorded the index. Lookup at fire
// time rather than caching because `/reload` re-runs the rebuild
// and the index may shift.
const int slot = Event::Custom::Lookup(kModifierStateChanged);
// Read the slot at fire time (never cache it) — `/reload` rebuilds the
// event table and the reservation re-claims, which may shift the index.
const int slot = _reserveModifierStateChanged.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "%s%d", kKeyName[bitIdx], static_cast<int>(down));
}
+4 -4
View File
@@ -58,13 +58,13 @@ static const Event::Custom::AutoReserve _reserveItemDataLoadResult{kItemDataLoad
static const Event::Custom::AutoReserve _reserveGetItemInfoReceived{kGetItemInfoReceived};
static void FireGetItemInfoReceived(int itemID, bool success) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kGetItemInfoReceived),
itemID, success);
Event::Custom::FireIdSuccess(_reserveGetItemInfoReceived.Slot(), itemID,
success);
}
static void FireItemDataLoadResult(int itemID, bool success) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kItemDataLoadResult),
itemID, success);
Event::Custom::FireIdSuccess(_reserveItemDataLoadResult.Slot(), itemID,
success);
}
// Item-load callbacks are __stdcall, 2 args / 8 bytes — matching the
+1 -1
View File
@@ -318,7 +318,7 @@ void AppendNonBagOwned(uint64_t *out, int *count) {
}
void FireChanged() {
Event::Custom::Fire(Event::Custom::Lookup(kEvtNewItems), "");
Event::Custom::Fire(_r.Slot(), "");
}
// Login-baseline handler. Runs every frame but does no work once the
+4 -5
View File
@@ -344,7 +344,7 @@ StartFn_t g_origStart = nullptr;
// structurally: a new item appears or the ring evicts an old one (shifting
// every index).
void FireFullUpdate() {
Event::Custom::Fire(Event::Custom::Lookup(kFullUpdate), "");
Event::Custom::Fire(_evFullUpdate.Slot(), "");
}
// START_ROLL: let the original build the store node, then read the item's
@@ -373,8 +373,7 @@ void __fastcall Roll_h(void *msg, void *ctx, int a3) {
if (structural)
FireFullUpdate();
if (itemIdx > 0)
Event::Custom::Fire(Event::Custom::Lookup(kRollChanged), "%d%d", itemIdx,
playerIdx);
Event::Custom::Fire(_evRollChanged.Slot(), "%d%d", itemIdx, playerIdx);
}
void __fastcall Won_h(void *ctx, void *msg, int a3) {
@@ -388,7 +387,7 @@ void __fastcall Won_h(void *ctx, void *msg, int a3) {
if (structural)
FireFullUpdate();
if (itemIdx > 0)
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
Event::Custom::Fire(_evRollComplete.Slot(), "%d", itemIdx);
}
void __fastcall Passed_h(void *ctx, void *msg) {
@@ -402,7 +401,7 @@ void __fastcall Passed_h(void *ctx, void *msg) {
if (structural)
FireFullUpdate();
if (itemIdx > 0)
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
Event::Custom::Fire(_evRollComplete.Slot(), "%d", itemIdx);
}
const Game::HookAutoRegister _hookRoll{
+1 -1
View File
@@ -267,7 +267,7 @@ void Complete() {
s_scan.currentGuid = 0;
s_scan.suppressEvents = false;
s_scan.lootMode = false;
Event::Custom::Fire(Event::Custom::Lookup(kEventCompleted), "");
Event::Custom::Fire(_reserve.Slot(), "");
}
void TryStartNext() {
+4 -6
View File
@@ -223,8 +223,8 @@ int BuildList(LocEntry *out, int maxOut) {
// lesson). The scan is bounded (16 debuff slots) and early-outs when idle, so
// the per-frame cost is negligible.
constexpr const char *kEventAdded = "LOSS_OF_CONTROL_ADDED";
constexpr const char *kEventUpdate = "LOSS_OF_CONTROL_UPDATE";
const Event::Custom::AutoReserve _reserveAdded{"LOSS_OF_CONTROL_ADDED"};
const Event::Custom::AutoReserve _reserveUpdate{"LOSS_OF_CONTROL_UPDATE"};
// Identity of an active effect for frame-to-frame diffing: a CC is keyed by
// spellID, a school interrupt by its school mask (spellID 0).
@@ -255,7 +255,7 @@ void OnWorldTick() {
for (int i = 0; i < n; ++i) {
if (!ContainsKey(g_prev, g_prevCount, cur[i])) {
// A newly-applied effect -> LOSS_OF_CONTROL_ADDED(eventIndex).
Event::Custom::Fire(Event::Custom::Lookup(kEventAdded), "%d", i + 1);
Event::Custom::Fire(_reserveAdded.Slot(), "%d", i + 1);
changed = true;
}
}
@@ -269,15 +269,13 @@ void OnWorldTick() {
// forward-compatible with per-unit tracking and matches modern WoW, whose
// LOSS_OF_CONTROL_UPDATE carries the unit token.
if (changed)
Event::Custom::Fire(Event::Custom::Lookup(kEventUpdate), "%s", "player");
Event::Custom::Fire(_reserveUpdate.Slot(), "%s", "player");
for (int i = 0; i < n; ++i)
g_prev[i] = cur[i];
g_prevCount = n;
}
static const Event::Custom::AutoReserve _reserveAdded{kEventAdded};
static const Event::Custom::AutoReserve _reserveUpdate{kEventUpdate};
static const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
// `C_LossOfControl.GetActiveLossOfControlDataCount()` -> number.
+11 -15
View File
@@ -59,13 +59,9 @@ namespace NamePlate::Events {
namespace {
constexpr const char *kEventCreated = "NAME_PLATE_CREATED";
constexpr const char *kEventUnitAdded = "NAME_PLATE_UNIT_ADDED";
constexpr const char *kEventUnitRemoved = "NAME_PLATE_UNIT_REMOVED";
const Event::Custom::AutoReserve _r1{kEventCreated};
const Event::Custom::AutoReserve _r2{kEventUnitAdded};
const Event::Custom::AutoReserve _r3{kEventUnitRemoved};
const Event::Custom::AutoReserve _evtCreated{"NAME_PLATE_CREATED"};
const Event::Custom::AutoReserve _evtUnitAdded{"NAME_PLATE_UNIT_ADDED"};
const Event::Custom::AutoReserve _evtUnitRemoved{"NAME_PLATE_UNIT_REMOVED"};
// Previous tick's snapshot — GUID → nameplate-frame pointer for each
// nameplated unit. Compared against the next tick's walk to compute
@@ -112,14 +108,14 @@ int AssignSlot(uint64_t guid) {
return static_cast<int>(g_slots.size() - 1);
}
// Fire `eventName` with a pre-formatted string as `arg1`. The engine
// Fire `event` with a pre-formatted string as `arg1`. The engine
// dispatcher's `%s` format code pushes the C string into `_G.arg1`
// as a Lua string — no escaping concerns for our own input
// (`"nameplateN"`).
void FireWithString(const char *eventName, const char *value) {
void FireWithString(const Event::Custom::AutoReserve &event, const char *value) {
if (value == nullptr)
return;
const int slot = Event::Custom::Lookup(eventName);
const int slot = event.Slot();
if (slot < 0)
return;
Event::Custom::Fire(slot, "%s", value);
@@ -172,10 +168,10 @@ using LuaRefRef_t = int(__fastcall *)(void *L, int t);
using LuaRefUnref_t = void(__fastcall *)(void *L, int t, int ref);
using LuaRawGetI_t = void(__fastcall *)(void *L, int t, int n);
void FireWithFrame(const char *eventName, void *frame) {
void FireWithFrame(const Event::Custom::AutoReserve &event, void *frame) {
if (frame == nullptr)
return;
const int slot = Event::Custom::Lookup(eventName);
const int slot = event.Slot();
if (slot < 0)
return;
@@ -229,7 +225,7 @@ void OnWorldTick() {
// event handler.
for (const auto &kv : g_currentTickPlates) {
if (g_seenPlates.insert(kv.second).second)
FireWithFrame(kEventCreated, const_cast<void *>(kv.second));
FireWithFrame(_evtCreated, const_cast<void *>(kv.second));
if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) {
const int slot = AssignSlot(kv.first);
// Watch this unit's fields so UNIT_HEALTH/UNIT_AURA/… fire with
@@ -237,7 +233,7 @@ void OnWorldTick() {
// target/party/raid tokens, not nameplates).
Unit::TokenObserver::Register(kv.first, &NamePlateFieldCb);
char tokenBuf[24];
FireWithString(kEventUnitAdded,
FireWithString(_evtUnitAdded,
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, slot + 1));
}
}
@@ -254,7 +250,7 @@ void OnWorldTick() {
continue;
const int oneBased = static_cast<int>(it - g_slots.begin()) + 1;
char tokenBuf[24];
FireWithString(kEventUnitRemoved,
FireWithString(_evtUnitRemoved,
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased));
Unit::TokenObserver::Unregister(kv.first, &NamePlateFieldCb);
*it = 0; // free the slot (no shift of survivors)
+2 -3
View File
@@ -109,8 +109,7 @@ int __fastcall EquipSlotChanged_cb(uint32_t fieldOffset, uint32_t /*size*/,
const int slot0 =
static_cast<int>(fieldOffset - Offsets::OFF_DESC_PLAYER_EQUIP_FIRST) >> 3;
if (slot0 >= 0 && slot0 < Offsets::DESC_PLAYER_EQUIP_SLOTS) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kEvtEquipmentChanged),
slot0 + 1,
Event::Custom::FireIdSuccess(_r.Slot(), slot0 + 1,
SlotHasItem(guidLo, guidHi, slot0));
// Gear swap changes item +healing and Spirit/Armor — invalidate the
// GetSpellBonusHealing cache.
@@ -225,7 +224,7 @@ void __fastcall AlertsRecompute_h() {
return;
}
if (changed)
Event::Custom::Fire(Event::Custom::Lookup(kEvtDurability), "");
Event::Custom::Fire(_r2.Slot(), "");
}
static const Game::HookAutoRegister _alertsHook{
+1 -2
View File
@@ -68,8 +68,7 @@ void __fastcall BindPointUpdateHandler_h(void *packetBuffer) {
static_cast<uintptr_t>(Offsets::VAR_IN_WORLD)) == 0)
return;
const int slot = Event::Custom::Lookup(kEventName);
Event::Custom::Fire(slot, "");
Event::Custom::Fire(_reserve.Slot(), "");
}
} // namespace
+12 -18
View File
@@ -43,12 +43,12 @@ namespace Player::InputEvents {
namespace {
constexpr const char *kStartedMovingEvent = "PLAYER_STARTED_MOVING";
constexpr const char *kStoppedMovingEvent = "PLAYER_STOPPED_MOVING";
constexpr const char *kStartedLookingEvent = "PLAYER_STARTED_LOOKING";
constexpr const char *kStoppedLookingEvent = "PLAYER_STOPPED_LOOKING";
constexpr const char *kStartedTurningEvent = "PLAYER_STARTED_TURNING";
constexpr const char *kStoppedTurningEvent = "PLAYER_STOPPED_TURNING";
const Event::Custom::AutoReserve _reserveStartedMoving{"PLAYER_STARTED_MOVING"};
const Event::Custom::AutoReserve _reserveStoppedMoving{"PLAYER_STOPPED_MOVING"};
const Event::Custom::AutoReserve _reserveStartedLooking{"PLAYER_STARTED_LOOKING"};
const Event::Custom::AutoReserve _reserveStoppedLooking{"PLAYER_STOPPED_LOOKING"};
const Event::Custom::AutoReserve _reserveStartedTurning{"PLAYER_STARTED_TURNING"};
const Event::Custom::AutoReserve _reserveStoppedTurning{"PLAYER_STOPPED_TURNING"};
// Below this per-frame delta, treat the yaw as stable. Picks up
// real rotation (smallest user-perceptible RMB-drag is ~0.5°/frame
@@ -85,12 +85,12 @@ const uint8_t *Camera() {
gameState, Offsets::OFF_GAME_STATE_CAMERA_PTR);
}
void FireTransition(bool now, bool &prev, const char *startedName,
const char *stoppedName) {
void FireTransition(bool now, bool &prev, const Event::Custom::AutoReserve &started,
const Event::Custom::AutoReserve &stopped) {
if (now == prev)
return;
prev = now;
const int slot = Event::Custom::Lookup(now ? startedName : stoppedName);
const int slot = (now ? started : stopped).Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "");
}
@@ -172,19 +172,13 @@ void OnWorldTick() {
g_havePrevCameraYaw = true;
}
FireTransition(moving, g_wasMoving, kStartedMovingEvent, kStoppedMovingEvent);
FireTransition(turning, g_wasTurning, kStartedTurningEvent, kStoppedTurningEvent);
FireTransition(looking, g_wasLooking, kStartedLookingEvent, kStoppedLookingEvent);
FireTransition(moving, g_wasMoving, _reserveStartedMoving, _reserveStoppedMoving);
FireTransition(turning, g_wasTurning, _reserveStartedTurning, _reserveStoppedTurning);
FireTransition(looking, g_wasLooking, _reserveStartedLooking, _reserveStoppedLooking);
}
} // namespace
static const Event::Custom::AutoReserve _reserveStartedMoving{kStartedMovingEvent};
static const Event::Custom::AutoReserve _reserveStoppedMoving{kStoppedMovingEvent};
static const Event::Custom::AutoReserve _reserveStartedLooking{kStartedLookingEvent};
static const Event::Custom::AutoReserve _reserveStoppedLooking{kStoppedLookingEvent};
static const Event::Custom::AutoReserve _reserveStartedTurning{kStartedTurningEvent};
static const Event::Custom::AutoReserve _reserveStoppedTurning{kStoppedTurningEvent};
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace Player::InputEvents
+3 -3
View File
@@ -33,8 +33,8 @@ static const Event::Custom::AutoReserve _reserveQuestDataLoadResult{kQuestDataLo
static void __stdcall QuestLoadCallback(void *userData, int success) {
const auto questID =
static_cast<int>(reinterpret_cast<uintptr_t>(userData));
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
questID, success != 0);
Event::Custom::FireIdSuccess(_reserveQuestDataLoadResult.Slot(), questID,
success != 0);
}
static void RequestAndMaybeNotify(uint32_t questID) {
@@ -45,7 +45,7 @@ static void RequestAndMaybeNotify(uint32_t questID) {
// invoking our callback. Fire the event ourselves so addons get the
// notification regardless of cache state, matching modern semantics.
if (wasCached) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(kQuestDataLoadResult),
Event::Custom::FireIdSuccess(_reserveQuestDataLoadResult.Slot(),
static_cast<int>(questID), true);
}
}
+2 -2
View File
@@ -157,7 +157,7 @@ void __fastcall QuestLogRebuild_h(int param_1) {
// (login or post-reload). Modern WoW's QUEST_ACCEPTED doesn't
// fire on initial sync either; we match that.
if (newCount == 1) {
const int slot = Event::Custom::Lookup(kAcceptedEventName);
const int slot = _reserveAccepted.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "%d%d", firstNewIdx, firstNewID);
}
@@ -168,7 +168,7 @@ void __fastcall QuestLogRebuild_h(int param_1) {
// all but one of the previous character's quests (1 removal, N
// additions in the same rebuild).
if (removedCount == 1 && newCount <= 1) {
const int slot = Event::Custom::Lookup(kRemovedEventName);
const int slot = _reserveRemoved.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "%d", firstRemovedID);
}
+1 -2
View File
@@ -77,8 +77,7 @@ int __fastcall Handler_h(void *thisPlayer, void *edx, void *packetStream) {
const int result = Handler_o(thisPlayer, edx, packetStream);
if (questID > 0) {
const int slot = Event::Custom::Lookup(kEventName);
Event::Custom::Fire(slot, "%u%u%u",
Event::Custom::Fire(_reserve.Slot(), "%u%u%u",
questID, xpReward, moneyReward);
}
+23 -35
View File
@@ -31,33 +31,20 @@ namespace Spell::CastEvents {
namespace {
constexpr const char *kStart = "UNIT_SPELLCAST_START";
constexpr const char *kStop = "UNIT_SPELLCAST_STOP";
constexpr const char *kDelayed = "UNIT_SPELLCAST_DELAYED";
constexpr const char *kChannelStart = "UNIT_SPELLCAST_CHANNEL_START";
constexpr const char *kChannelStop = "UNIT_SPELLCAST_CHANNEL_STOP";
constexpr const char *kChannelUpdate = "UNIT_SPELLCAST_CHANNEL_UPDATE";
constexpr const char *kSucceeded = "UNIT_SPELLCAST_SUCCEEDED";
constexpr const char *kInterrupted = "UNIT_SPELLCAST_INTERRUPTED";
constexpr const char *kFailed = "UNIT_SPELLCAST_FAILED";
constexpr const char *kFailedQuiet = "UNIT_SPELLCAST_FAILED_QUIET";
constexpr const char *kSent = "UNIT_SPELLCAST_SENT";
constexpr const char *kReticleTarget = "UNIT_SPELLCAST_RETICLE_TARGET";
constexpr const char *kReticleClear = "UNIT_SPELLCAST_RETICLE_CLEAR";
const Event::Custom::AutoReserve _rStart{kStart};
const Event::Custom::AutoReserve _rStop{kStop};
const Event::Custom::AutoReserve _rDelayed{kDelayed};
const Event::Custom::AutoReserve _rChannelStart{kChannelStart};
const Event::Custom::AutoReserve _rChannelStop{kChannelStop};
const Event::Custom::AutoReserve _rChannelUpdate{kChannelUpdate};
const Event::Custom::AutoReserve _rSucceeded{kSucceeded};
const Event::Custom::AutoReserve _rInterrupted{kInterrupted};
const Event::Custom::AutoReserve _rFailed{kFailed};
const Event::Custom::AutoReserve _rFailedQuiet{kFailedQuiet};
const Event::Custom::AutoReserve _rSent{kSent};
const Event::Custom::AutoReserve _rReticleTarget{kReticleTarget};
const Event::Custom::AutoReserve _rReticleClear{kReticleClear};
// The reservations double as the fire handles (`k*.Slot()` — O(1)).
const Event::Custom::AutoReserve kStart{"UNIT_SPELLCAST_START"};
const Event::Custom::AutoReserve kStop{"UNIT_SPELLCAST_STOP"};
const Event::Custom::AutoReserve kDelayed{"UNIT_SPELLCAST_DELAYED"};
const Event::Custom::AutoReserve kChannelStart{"UNIT_SPELLCAST_CHANNEL_START"};
const Event::Custom::AutoReserve kChannelStop{"UNIT_SPELLCAST_CHANNEL_STOP"};
const Event::Custom::AutoReserve kChannelUpdate{"UNIT_SPELLCAST_CHANNEL_UPDATE"};
const Event::Custom::AutoReserve kSucceeded{"UNIT_SPELLCAST_SUCCEEDED"};
const Event::Custom::AutoReserve kInterrupted{"UNIT_SPELLCAST_INTERRUPTED"};
const Event::Custom::AutoReserve kFailed{"UNIT_SPELLCAST_FAILED"};
const Event::Custom::AutoReserve kFailedQuiet{"UNIT_SPELLCAST_FAILED_QUIET"};
const Event::Custom::AutoReserve kSent{"UNIT_SPELLCAST_SENT"};
const Event::Custom::AutoReserve kReticleTarget{"UNIT_SPELLCAST_RETICLE_TARGET"};
const Event::Custom::AutoReserve kReticleClear{"UNIT_SPELLCAST_RETICLE_CLEAR"};
// Autoshot spams client-side failures while ramping — nampower filters it
// out of its failure event, so do we.
@@ -108,8 +95,9 @@ void BuildCastGuid(char *out, size_t n, int type, int spellID, int castUID) {
// rank)`. Gated on a listener so an unwatched event does no DBC lookups or
// string formatting. A null name/rank pushes `nil` through the dispatcher's
// `lua_pushstring(NULL) → pushnil` tail-jump, which is fine.
void Fire(const char *eventName, int spellID, int guidNum, int type = 3) {
const int slot = Event::Custom::Lookup(eventName);
void Fire(const Event::Custom::AutoReserve &event, int spellID, int guidNum,
int type = 3) {
const int slot = event.Slot();
if (!Event::Custom::HasListeners(slot))
return;
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
@@ -127,7 +115,7 @@ void Fire(const char *eventName, int spellID, int guidNum, int type = 3) {
// inserted at arg2. `target` is a unit token (e.g. "target") resolved from
// the cast packet, or "" for self / no-target casts.
void FireSent(int spellID, int guidNum, const char *target) {
const int slot = Event::Custom::Lookup(kSent);
const int slot = kSent.Slot();
if (!Event::Custom::HasListeners(slot))
return;
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
@@ -148,8 +136,8 @@ void FireSent(int spellID, int guidNum, const char *target) {
// the load-bearing fields — are exact; only arg2 differs (`""` vs `nil`),
// which is inconsequential for a reticle (no cast to identify). spellName /
// rank are our usual extension tail.
void FireReticle(const char *eventName, int spellID) {
const int slot = Event::Custom::Lookup(eventName);
void FireReticle(const Event::Custom::AutoReserve &event, int spellID) {
const int slot = event.Slot();
if (!Event::Custom::HasListeners(slot))
return;
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
@@ -382,9 +370,9 @@ const Net::SendObserver::AutoSubscribe _sendSub{&OnSend};
// the payload is fanned out per token (`target` / `focus` / `nameplateN` /
// `party` / `raid` / `mouseover`). Same `(unit, castGUID, spellID, spellName,
// rank)` shape as the player events, just with a non-"player" unit.
void FireRemote(const char *eventName, uint64_t casterGuid, int spellID,
int guidNum) {
const int slot = Event::Custom::Lookup(eventName);
void FireRemote(const Event::Custom::AutoReserve &event, uint64_t casterGuid,
int spellID, int guidNum) {
const int slot = event.Slot();
if (!Event::Custom::HasListeners(slot))
return;
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
+1 -1
View File
@@ -284,7 +284,7 @@ void Set(uint64_t guid) {
TokenObserver::Register(guid, &FocusFieldCb);
g_focusObserved = true;
}
const int slot = Event::Custom::Lookup(kEventName);
const int slot = _reserve.Slot();
if (slot >= 0)
Event::Custom::Fire(slot, "");
}
+1 -2
View File
@@ -173,8 +173,7 @@ void __fastcall BonusActionBarUpdate_h() {
if (current < 0 || current == s_lastForm)
return;
s_lastForm = current;
const int slot = Event::Custom::Lookup(kEventName);
Event::Custom::Fire(slot, "");
Event::Custom::Fire(_reserve.Slot(), "");
}
} // namespace
+21 -4
View File
@@ -108,6 +108,19 @@ constexpr const char *kEvtPlaybackStarted = "VOICE_CHAT_TTS_PLAYBACK_STARTED";
constexpr const char *kEvtSpeakTextUpdate = "VOICE_CHAT_TTS_SPEAK_TEXT_UPDATE";
constexpr const char *kEvtVoicesUpdate = "VOICE_CHAT_TTS_VOICES_UPDATE";
// The lazily-created reservations, set by RegisterLuaFunctions. Null while
// standing down for VanillaTTS — but the fire helpers only run from our own
// registered surface, so they never see null in that mode; SlotOf's guard is
// belt-and-suspenders (`Fire(-1)` no-ops).
const Event::Custom::AutoReserve *g_evtPlaybackFailed = nullptr;
const Event::Custom::AutoReserve *g_evtPlaybackFinished = nullptr;
const Event::Custom::AutoReserve *g_evtPlaybackStarted = nullptr;
const Event::Custom::AutoReserve *g_evtVoicesUpdate = nullptr;
int SlotOf(const Event::Custom::AutoReserve *r) {
return r != nullptr ? r->Slot() : -1;
}
// ---------------------------------------------------------------------------
// CVar names + handles (registered in RegisterLuaFunctions)
// ---------------------------------------------------------------------------
@@ -186,18 +199,18 @@ std::wstring Utf8ToWide(const char *s) {
// Event fire helpers (engine dispatcher via Event::Custom)
// ---------------------------------------------------------------------------
void FireVoicesUpdate() {
Event::Custom::Fire(Event::Custom::Lookup(kEvtVoicesUpdate), "");
Event::Custom::Fire(SlotOf(g_evtVoicesUpdate), "");
}
void FirePlaybackStarted(int numConsumers, int utteranceID, int durationMS, int dest) {
Event::Custom::Fire(Event::Custom::Lookup(kEvtPlaybackStarted), "%d%d%d%d",
Event::Custom::Fire(SlotOf(g_evtPlaybackStarted), "%d%d%d%d",
numConsumers, utteranceID, durationMS, dest);
}
void FirePlaybackFinished(int numConsumers, int utteranceID, int dest) {
Event::Custom::Fire(Event::Custom::Lookup(kEvtPlaybackFinished), "%d%d%d",
Event::Custom::Fire(SlotOf(g_evtPlaybackFinished), "%d%d%d",
numConsumers, utteranceID, dest);
}
void FirePlaybackFailed(const char *status, int utteranceID, int dest) {
Event::Custom::Fire(Event::Custom::Lookup(kEvtPlaybackFailed), "%s%d%d",
Event::Custom::Fire(SlotOf(g_evtPlaybackFailed), "%s%d%d",
status, utteranceID, dest);
}
@@ -565,6 +578,10 @@ void RegisterLuaFunctions() {
static const Event::Custom::AutoReserve _r3{kEvtPlaybackStarted};
static const Event::Custom::AutoReserve _r4{kEvtSpeakTextUpdate};
static const Event::Custom::AutoReserve _r5{kEvtVoicesUpdate};
g_evtPlaybackFailed = &_r1;
g_evtPlaybackFinished = &_r2;
g_evtPlaybackStarted = &_r3;
g_evtVoicesUpdate = &_r5;
g_cvarVoice = CVar::Factory::Register(kCVarVoice, "0", 0, &OnVoiceChanged);
g_cvarSpeed = CVar::Factory::Register(kCVarSpeed, "0", 0, &OnSpeedChanged);