Skip to content

Commit 58e7e1a

Browse files
committed
frame: add Frame:IsEventRegistered
Vanilla has RegisterEvent / UnregisterEvent as frame methods but not the query IsEventRegistered (added in a later expansion). There's no engine Script_* to delegate to, so we replicate the membership check already embedded in Frame::RegisterEvent (FUN_00702140): find the event by name in the live event table (SStrCmpI), then walk that entry's subscriber chain (head@+0xC, next@+4, frame@+8, low-bit/null sentinel) for this frame. self is resolved via Game::Lua::ResolveObject, yielding the same CFrameScriptObject* the chain stores. Registered on the Frame method registry (VAR_FRAME_METHOD_REGISTRY, alongside RegisterEvent) so it resolves on any frame. Pure read, no new hooks. Returns a boolean, matching the 3.3.5-era contract; verified in-game (true for a registered event, false otherwise). Documented in docs/API.md and README.md.
1 parent 5339c97 commit 58e7e1a

4 files changed

Lines changed: 87 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
3333
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
3434
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
3535
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
36-
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript` |
36+
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered` |
3737
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
3838
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
3939
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |

docs/API.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ build instructions.
137137
- [`frame:SetShown(shown)`](#framesetshownshown)
138138
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
139139
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
140+
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
140141

141142
- [FriendList](#friendlist)
142143
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
@@ -3143,6 +3144,22 @@ button:HookScript("OnEnter", function(self)
31433144
end)
31443145
```
31453146

3147+
### `frame:IsEventRegistered(event)`
3148+
3149+
Vanilla ships `RegisterEvent` / `UnregisterEvent` but not the query.
3150+
Returns `true` if the frame is currently registered for `event`,
3151+
`false` otherwise (including for an unknown event name). Reuses the
3152+
engine's own subscriber-chain membership check — the same walk
3153+
`RegisterEvent` performs before appending — so the answer is exactly
3154+
what the event dispatcher sees. Works on any frame.
3155+
3156+
```lua
3157+
local f = CreateFrame("Frame")
3158+
f:RegisterEvent("PLAYER_LOGIN")
3159+
f:IsEventRegistered("PLAYER_LOGIN") -- true
3160+
f:IsEventRegistered("PLAYER_LOGOUT") -- false
3161+
```
3162+
31463163
## FriendList
31473164

31483165
### `C_FriendList.SendWhoQueryByName(name)`

src/Offsets.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3463,6 +3463,15 @@ enum Offsets {
34633463
OFF_EVENT_ENTRY_NAME = 0x00,
34643464
OFF_EVENT_ENTRY_HEAD = 0x0C,
34653465

3466+
// Subscriber-chain node layout — the linked list hanging off each
3467+
// entry's +0x0C head, derived from Frame::RegisterEvent
3468+
// (FUN_00702140): each node is `{ ?, next@+0x04, frame@+0x08 }`, where
3469+
// `frame` is the CFrameScriptObject* subscriber. A node value with its
3470+
// low bit set (or null) is the end/empty sentinel. Walked by
3471+
// Frame:IsEventRegistered to test membership.
3472+
OFF_EVENT_NODE_NEXT = 0x04,
3473+
OFF_EVENT_NODE_FRAME = 0x08,
3474+
34663475
// `Frame::RegisterEvent` — the C++ helper called by the Lua
34673476
// `frame:RegisterEvent(eventName)` method (`Script_RegisterEvent` at
34683477
// `0x00774A40`). `__thiscall` with `(this=frame, eventName)`. Walks

src/frame/Modern.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,65 @@ int __fastcall Script_HookScript(void *L) {
186186
return 0;
187187
}
188188

189+
// ---- IsEventRegistered (Frame) ---------------------------------------------
190+
191+
// `frame:IsEventRegistered("event")` — vanilla has RegisterEvent /
192+
// UnregisterEvent but not the query. There's no engine Script_* to
193+
// delegate to, so we replicate the membership check that lives inside
194+
// Frame::RegisterEvent (FUN_00702140): find the event by name in the live
195+
// event table, then walk that entry's subscriber chain for this frame.
196+
// The frame pointer we compare against is the same CFrameScriptObject*
197+
// the engine stores (Game::Lua::ResolveObject resolves self exactly as
198+
// Script_RegisterEvent does).
199+
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
200+
201+
bool FrameHasEvent(void *frame, const char *eventName) {
202+
const uint32_t count = *reinterpret_cast<const uint32_t *>(
203+
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
204+
auto *base = *reinterpret_cast<const uint8_t *const *>(
205+
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_BASE_PTR));
206+
if (count == 0 || base == nullptr)
207+
return false;
208+
209+
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(
210+
static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
211+
212+
for (uint32_t i = 0; i < count; ++i) {
213+
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
214+
const char *name = *reinterpret_cast<const char *const *>(
215+
entry + Offsets::OFF_EVENT_ENTRY_NAME);
216+
if (name == nullptr)
217+
continue;
218+
if (sstrcmpi(name, eventName, 0x7FFFFFFF) != 0)
219+
continue;
220+
// Names are unique, so this is the one entry — walk its chain.
221+
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
222+
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
223+
while ((node & 1) == 0 && node != 0) {
224+
if (*reinterpret_cast<void *const *>(
225+
node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
226+
return true;
227+
node = *reinterpret_cast<const uintptr_t *>(
228+
node + Offsets::OFF_EVENT_NODE_NEXT);
229+
}
230+
return false;
231+
}
232+
return false;
233+
}
234+
235+
int __fastcall Script_IsEventRegistered(void *L) {
236+
if (!Game::Lua::IsString(L, 2)) {
237+
Game::Lua::Error(L, "Usage: frame:IsEventRegistered(\"event\")");
238+
return 0;
239+
}
240+
void *frame = Game::Lua::ResolveObject(L, 1);
241+
const char *eventName = Game::Lua::ToString(L, 2);
242+
const bool registered =
243+
frame != nullptr && eventName != nullptr && FrameHasEvent(frame, eventName);
244+
Game::Lua::PushBool(L, registered);
245+
return 1;
246+
}
247+
189248
// ---- SetPoint one-arg form (Script_SetPoint co-hook) -----------------------
190249

191250
// Vanilla's Script_SetPoint accepts every modern form except the bare
@@ -222,6 +281,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
222281
Offsets::FUN_SCRIPT_FRAME_HIDE>},
223282
{"SetResizeBounds", &Script_SetResizeBounds},
224283
{"HookScript", &Script_HookScript},
284+
{"IsEventRegistered", &Script_IsEventRegistered},
225285
};
226286

227287
const Game::Lua::FrameMethodEntry g_textureMethods[] = {

0 commit comments

Comments
 (0)