From 8e0f99640a899216ab83b15351e45b73fd754d1b Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 00:59:07 -0500 Subject: [PATCH 01/15] C_NamePlate.GetNamePlateGUIDs --- README.md | 1 + docs/API.md | 30 ++++++++++- src/nameplate/Info.cpp | 118 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/nameplate/Info.cpp diff --git a/README.md b/README.md index 2c6bb86..ea3cacf 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**. | [Macros](docs/API.md#macros) | `GetMacroSpell` | | [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` | | [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` | +| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateGUIDs` | | [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` | | [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` | | [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` | diff --git a/docs/API.md b/docs/API.md index 5defd74..0b9857a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -195,6 +195,9 @@ build instructions. - [`C_MerchantFrame.IsMerchantItemRefundable(slot)`](#c_merchantframeismerchantitemrefundableslot) - [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled) +- [NamePlate](#nameplate) + - [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids) + - [NameCache](#namecache) - [`GetPlayerInfoByGUID(guid)`](#getplayerinfobyguidguid) - [`C_PlayerCache.GetPlayerInfoByName(name)`](#c_playercachegetplayerinfobynamename) @@ -4325,7 +4328,32 @@ disable the sell-all-junk button; vanilla has no such setting, so the feature is always on. Function exists so retail addons that gate `SellAllJunkItems` on this don't no-op silently. -## NameCache +## NamePlate + +### `C_NamePlate.GetNamePlateGUIDs()` + +Returns a 1-based table of GUID strings (modern `"0x..."` format) — +one per CGUnit that currently has an allocated nameplate frame. +Empty table when nameplates are toggled off (`V` key) or no units +in nameplate range are visible. + +```lua +/dump C_NamePlate.GetNamePlateGUIDs() +-- { "0xF13000C36C26FD02", "0xF130000009276912", ... } +``` + +Walks the local-player-anchored object hash table for `TYPEMASK_UNIT` +entries, filters by `*(unit + 0xE60) != nullptr` (the per-unit +nameplate-frame pointer the engine assigns in `FUN_006086E0`'s "show +nameplate" path). Returns the GUIDs of matching units in hash-bucket +order — order isn't stable across calls. + +**Named differently from modern WoW's `C_NamePlate.GetNamePlates`** — +the modern call returns nameplate `Frame` objects, not GUIDs. We +ship the GUID primitive; surfacing the frames would need additional +engine hooks. Modern also provides `"nameplateN"` unit tokens and +`NAME_PLATE_UNIT_ADDED` / `REMOVED` events — both unimplemented +here (multi-session scope). GUID-keyed cache of player names and classes. The engine itself maintains an in-memory `NameCache` at `0x00C0E228`, populated by diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp new file mode 100644 index 0000000..0561852 --- /dev/null +++ b/src/nameplate/Info.cpp @@ -0,0 +1,118 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU Lesser General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along with +// ClassicAPI. If not, see . + +// `C_NamePlate.GetNamePlateGUIDs()` — enumerates GUIDs of currently- +// visible units that have an allocated nameplate frame. +// +// Named differently from modern `C_NamePlate.GetNamePlates` because +// the modern call returns nameplate `Frame` objects, not GUIDs. This +// backport ships only the GUID primitive; surfacing the frames would +// require additional engine hooks. +// +// Vanilla 1.12 stores each unit's nameplate pointer at `CGUnit + 0xE60` +// (verified via `FUN_006086E0`'s "ensure nameplate exists" path). The +// nameplate also caches the unit's GUID at `+0x4E8` for back-lookup. +// There's no central "active nameplates" list — the engine maintains +// per-unit pointers updated by per-unit state-change handlers. +// +// To enumerate, we walk the local-player-anchored object hash table +// (`player + 0x1C` = bucket array, `player + 0x24` = mask). Each +// bucket header stores the link-field offset at byte 0 and the +// chain-head pointer at byte 8 — Storm's intrusive-hash pattern. +// Filter by `TYPEMASK_UNIT` (`flags & 0x08` at `*(entry+8) + 8`) and +// check `+0xE60` for a non-null nameplate pointer. + +#include "Game.h" +#include "Offsets.h" +#include "guid/Guid.h" + +#include + +namespace NamePlate::Info { + +namespace { + +constexpr uintptr_t kLocalPlayerGlobal = 0x00B41414; +constexpr int kOffPlayerBucketArray = 0x1C; +constexpr int kOffPlayerBucketMask = 0x24; +constexpr int kBucketStride = 12; +constexpr int kBucketLinkOffsetField = 0; // byte 0: link-field offset within entry +constexpr int kBucketChainHeadField = 8; // byte 8: chain head pointer +constexpr int kOffEntryInstanceBlock = 0x08; +constexpr int kOffInstanceTypeMask = 0x08; +constexpr uint32_t kTypeMaskUnit = 0x08; +constexpr int kOffUnitNamePlate = 0xE60; + +} // namespace + +static int __fastcall Script_GetNamePlateGUIDs(void *L) { + Game::Lua::NewTable(L); + + auto *player = *reinterpret_cast(kLocalPlayerGlobal); + if (player == nullptr) + return 1; + + auto *buckets = *reinterpret_cast( + player + kOffPlayerBucketArray); + const uint32_t mask = *reinterpret_cast( + player + kOffPlayerBucketMask); + if (buckets == nullptr || mask == 0xFFFFFFFFu) + return 1; + + int nextIndex = 1; + for (uint32_t b = 0; b <= mask; ++b) { + const uint8_t *bucket = buckets + b * kBucketStride; + const uint32_t linkOffset = *reinterpret_cast( + bucket + kBucketLinkOffsetField); + uintptr_t entry = *reinterpret_cast( + bucket + kBucketChainHeadField); + + while (entry != 0 && (entry & 1) == 0) { + auto *obj = reinterpret_cast(entry); + auto *instance = *reinterpret_cast( + obj + kOffEntryInstanceBlock); + if (instance != nullptr) { + const uint32_t typeMask = *reinterpret_cast( + instance + kOffInstanceTypeMask); + if ((typeMask & kTypeMaskUnit) != 0) { + const auto *nameplate = *reinterpret_cast( + obj + kOffUnitNamePlate); + if (nameplate != nullptr) { + const uint64_t guid = *reinterpret_cast( + instance); + if (guid != 0) { + char buf[Guid::STRING_SIZE]; + Game::Lua::PushNumber(L, static_cast(nextIndex++)); + Game::Lua::PushString(L, + Guid::FormatAsString(guid, buf, sizeof buf)); + Game::Lua::SetTable(L, -3); + } + } + } + } + + entry = *reinterpret_cast( + obj + linkOffset + 4); + } + } + return 1; +} + +static void RegisterLuaFunctions() { + Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateGUIDs", + &Script_GetNamePlateGUIDs); +} + +static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +} // namespace NamePlate::Info From 2ec0767b3564abfea21f9d45d209dee243542d5a Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 01:02:00 -0500 Subject: [PATCH 02/15] fixed NameCache header --- docs/API.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/API.md b/docs/API.md index 0b9857a..e4efe86 100644 --- a/docs/API.md +++ b/docs/API.md @@ -4355,6 +4355,8 @@ engine hooks. Modern also provides `"nameplateN"` unit tokens and `NAME_PLATE_UNIT_ADDED` / `REMOVED` events — both unimplemented here (multi-session scope). +## NameCache + GUID-keyed cache of player names and classes. The engine itself maintains an in-memory `NameCache` at `0x00C0E228`, populated by `SMSG_NAME_QUERY_RESPONSE` — but vanilla doesn't expose it to Lua, From e0adc32f2f8230e23df5db39cf83d60869c0930a Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 01:06:07 -0500 Subject: [PATCH 03/15] C_NamePlate.GetNamePlates --- README.md | 2 +- docs/API.md | 57 ++++++++++++++++++++-------- src/nameplate/Info.cpp | 86 ++++++++++++++++++++++++++++++------------ 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index ea3cacf..062868e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**. | [Macros](docs/API.md#macros) | `GetMacroSpell` | | [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` | | [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` | -| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateGUIDs` | +| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlates`, `C_NamePlate.GetNamePlateGUIDs` | | [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` | | [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` | | [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` | diff --git a/docs/API.md b/docs/API.md index e4efe86..5cf6c54 100644 --- a/docs/API.md +++ b/docs/API.md @@ -196,6 +196,7 @@ build instructions. - [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled) - [NamePlate](#nameplate) + - [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates) - [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids) - [NameCache](#namecache) @@ -4330,30 +4331,54 @@ gate `SellAllJunkItems` on this don't no-op silently. ## NamePlate +Modern `C_NamePlate.*` returns nameplate `Frame` objects keyed off +unit data. Vanilla 1.12 doesn't ship the API at all — but the +underlying data (per-unit nameplate pointer at `CGUnit + 0xE60`) +exists. We enumerate visible units via the local-player-anchored +object hash table, filter by `TYPEMASK_UNIT`, and return matches. + +> **Scope vs. modern.** Modern API also provides `"nameplateN"` unit +> tokens and `NAME_PLATE_UNIT_ADDED` / `REMOVED` events. Both +> deferred — would require hooking the unit-token resolver and a +> per-frame polling loop. The functions documented below give +> addons enough to walk plates per call. + +### `C_NamePlate.GetNamePlates()` + +Returns a 1-based table of nameplate `Frame` objects — one per +CGUnit that currently has an allocated nameplate. The frames are +real Lua tables with methods (`:GetName()`, `:GetWidth()`, +`:SetAlpha()`, etc.); decorations added by other addons (pfUI's +nameplate skin, healthbar overlays, etc.) are visible on the +returned tables as expected. + +```lua +local plates = C_NamePlate.GetNamePlates() +for i, plate in ipairs(plates) do + print(i, plate:GetName(), plate:GetWidth()) +end +``` + +Pushes `registry[plate + 0x08]` for each frame — the per-CFrame Lua +registry ref-key the engine populates on frame creation. Empty +table when nameplates are toggled off (`V` key) or no units are in +nameplate range. + ### `C_NamePlate.GetNamePlateGUIDs()` -Returns a 1-based table of GUID strings (modern `"0x..."` format) — -one per CGUnit that currently has an allocated nameplate frame. -Empty table when nameplates are toggled off (`V` key) or no units -in nameplate range are visible. +Companion to `GetNamePlates()` — returns the GUID strings of the +same set of units in modern `"0xHHHHHHHHHHHHHHHH"` format. Useful +when an addon only needs GUIDs (raid-target tracking, threat +coloring) and doesn't want to walk the frame list to read each +plate's stored guid. ```lua /dump C_NamePlate.GetNamePlateGUIDs() -- { "0xF13000C36C26FD02", "0xF130000009276912", ... } ``` -Walks the local-player-anchored object hash table for `TYPEMASK_UNIT` -entries, filters by `*(unit + 0xE60) != nullptr` (the per-unit -nameplate-frame pointer the engine assigns in `FUN_006086E0`'s "show -nameplate" path). Returns the GUIDs of matching units in hash-bucket -order — order isn't stable across calls. - -**Named differently from modern WoW's `C_NamePlate.GetNamePlates`** — -the modern call returns nameplate `Frame` objects, not GUIDs. We -ship the GUID primitive; surfacing the frames would need additional -engine hooks. Modern also provides `"nameplateN"` unit tokens and -`NAME_PLATE_UNIT_ADDED` / `REMOVED` events — both unimplemented -here (multi-session scope). +Same enumeration as `GetNamePlates`; the two return parallel lists +in the same hash-bucket order (which isn't stable across calls). ## NameCache diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 0561852..1111381 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -11,13 +11,17 @@ // You should have received a copy of the GNU Lesser General Public License along with // ClassicAPI. If not, see . -// `C_NamePlate.GetNamePlateGUIDs()` — enumerates GUIDs of currently- -// visible units that have an allocated nameplate frame. +// `C_NamePlate.GetNamePlates()` — returns nameplate Frame objects, +// matching modern WoW's signature. Each CGNamePlateFrame is a CFrame +// subclass that stores its Lua-registry ref-key at the standard +// `OFF_COBJECT_LUA_REGISTRY_REF = +0x08`; pushing +// `registry[refKey]` yields the Lua table that addons can call +// `:Show()`, `:GetWidth()`, etc. on. // -// Named differently from modern `C_NamePlate.GetNamePlates` because -// the modern call returns nameplate `Frame` objects, not GUIDs. This -// backport ships only the GUID primitive; surfacing the frames would -// require additional engine hooks. +// `C_NamePlate.GetNamePlateGUIDs()` — companion returning the GUID +// strings of the same set of units. Faster than walking +// `GetNamePlates()` + reading guid back, useful when an addon only +// needs the GUIDs (raid-target tracking, threat coloring, etc.). // // Vanilla 1.12 stores each unit's nameplate pointer at `CGUnit + 0xE60` // (verified via `FUN_006086E0`'s "ensure nameplate exists" path). The @@ -53,23 +57,24 @@ constexpr int kOffInstanceTypeMask = 0x08; constexpr uint32_t kTypeMaskUnit = 0x08; constexpr int kOffUnitNamePlate = 0xE60; -} // namespace - -static int __fastcall Script_GetNamePlateGUIDs(void *L) { - Game::Lua::NewTable(L); +using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); +// Walk visible units with allocated nameplates, invoking `emit(unit, +// nameplate, instance)` for each. Returns the number of emissions. +template +int ForEachNamePlatedUnit(F &&emit) { auto *player = *reinterpret_cast(kLocalPlayerGlobal); if (player == nullptr) - return 1; + return 0; auto *buckets = *reinterpret_cast( player + kOffPlayerBucketArray); const uint32_t mask = *reinterpret_cast( player + kOffPlayerBucketMask); if (buckets == nullptr || mask == 0xFFFFFFFFu) - return 1; + return 0; - int nextIndex = 1; + int count = 0; for (uint32_t b = 0; b <= mask; ++b) { const uint8_t *bucket = buckets + b * kBucketStride; const uint32_t linkOffset = *reinterpret_cast( @@ -85,30 +90,63 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) { const uint32_t typeMask = *reinterpret_cast( instance + kOffInstanceTypeMask); if ((typeMask & kTypeMaskUnit) != 0) { - const auto *nameplate = *reinterpret_cast( + const auto *nameplate = *reinterpret_cast( obj + kOffUnitNamePlate); if (nameplate != nullptr) { - const uint64_t guid = *reinterpret_cast( - instance); - if (guid != 0) { - char buf[Guid::STRING_SIZE]; - Game::Lua::PushNumber(L, static_cast(nextIndex++)); - Game::Lua::PushString(L, - Guid::FormatAsString(guid, buf, sizeof buf)); - Game::Lua::SetTable(L, -3); - } + emit(obj, nameplate, instance); + ++count; } } } - entry = *reinterpret_cast( obj + linkOffset + 4); } } + return count; +} + +} // namespace + +static int __fastcall Script_GetNamePlates(void *L) { + Game::Lua::NewTable(L); + auto rawgeti = reinterpret_cast( + Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); + int nextIndex = 1; + ForEachNamePlatedUnit( + [L, rawgeti, &nextIndex](const uint8_t *, const uint8_t *nameplate, + const uint8_t *) { + const int refKey = *reinterpret_cast( + nameplate + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); + if (refKey == 0) + return; + Game::Lua::PushNumber(L, static_cast(nextIndex++)); + rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + Game::Lua::SetTable(L, -3); + }); + return 1; +} + +static int __fastcall Script_GetNamePlateGUIDs(void *L) { + Game::Lua::NewTable(L); + int nextIndex = 1; + ForEachNamePlatedUnit( + [L, &nextIndex](const uint8_t *, const uint8_t *, + const uint8_t *instance) { + const uint64_t guid = *reinterpret_cast(instance); + if (guid == 0) + return; + char buf[Guid::STRING_SIZE]; + Game::Lua::PushNumber(L, static_cast(nextIndex++)); + Game::Lua::PushString(L, + Guid::FormatAsString(guid, buf, sizeof buf)); + Game::Lua::SetTable(L, -3); + }); return 1; } static void RegisterLuaFunctions() { + Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlates", + &Script_GetNamePlates); Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateGUIDs", &Script_GetNamePlateGUIDs); } From 7861b50012519a484f20756135f57dd217d47d0c Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 01:39:09 -0500 Subject: [PATCH 04/15] GetNamePlates will show blizzard nameplates as well --- docs/API.md | 69 ++++++++++++++++++++++++++++++--------- src/nameplate/Info.cpp | 73 +++++++++++++++++++++++++++++++++++------- 2 files changed, 114 insertions(+), 28 deletions(-) diff --git a/docs/API.md b/docs/API.md index 5cf6c54..fec8ee0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -4346,11 +4346,10 @@ object hash table, filter by `TYPEMASK_UNIT`, and return matches. ### `C_NamePlate.GetNamePlates()` Returns a 1-based table of nameplate `Frame` objects — one per -CGUnit that currently has an allocated nameplate. The frames are -real Lua tables with methods (`:GetName()`, `:GetWidth()`, -`:SetAlpha()`, etc.); decorations added by other addons (pfUI's -nameplate skin, healthbar overlays, etc.) are visible on the -returned tables as expected. +CGUnit that currently has an allocated **Lua-registered** nameplate. +The frames are real Lua tables with methods (`:GetName()`, +`:GetWidth()`, `:SetAlpha()`, etc.) and any addon-added decorations +on them. ```lua local plates = C_NamePlate.GetNamePlates() @@ -4359,26 +4358,64 @@ for i, plate in ipairs(plates) do end ``` -Pushes `registry[plate + 0x08]` for each frame — the per-CFrame Lua -registry ref-key the engine populates on frame creation. Empty -table when nameplates are toggled off (`V` key) or no units are in -nameplate range. +Two kinds of plates can show up: + +- **Addon-created plates** (pfUI, TidyPlates, NamePlateMod, etc.): + registered with Lua via `CreateFrame`, so each has a real + registry ref. We push `registry[plate + 0x08]`. Identity is + stable across calls — caching is safe while the frame is alive. + +- **Default vanilla plates**: created internally by the engine + without ever calling `CreateFrame`. Their `+0x08` field holds the + sentinel `LUA_NOREF` (`-2`), not a real registry key. We build a + fresh wrapper table per call (`{[0] = lightuserdata(plate)}` with + the global `__framescript_meta` metatable) so addons get the + same method surface. The wrapper isn't cached engine-side, so + identity isn't stable across calls — don't compare wrappers, and + don't store them across the unit going out of range (the + underlying frame may be freed). Call `GetNamePlates()` fresh + each time you need plates. + +### Reading region content from a default nameplate + +Vanilla plates have six child regions in stable positions. Walk them +with `:GetRegions()`: + +```lua +local plates = C_NamePlate.GetNamePlates() +for _, plate in ipairs(plates) do + local regions = {plate:GetRegions()} + local name = regions[3]:GetText() -- e.g. "Joseph Dalton" + local level = tonumber(regions[4]:GetText()) -- e.g. 60 + -- regions[1], [2], [5], [6] are textures (border, healthbar, + -- glow, raid-icon — order depends on the engine's draw order) +end +``` + +Lua 5.0 has no `select()`, so collect into a table via +`{plate:GetRegions()}` and index. Addon-created plates have +different region layouts — those frames inherit whatever shape the +addon built, not this one. ### `C_NamePlate.GetNamePlateGUIDs()` -Companion to `GetNamePlates()` — returns the GUID strings of the -same set of units in modern `"0xHHHHHHHHHHHHHHHH"` format. Useful -when an addon only needs GUIDs (raid-target tracking, threat -coloring) and doesn't want to walk the frame list to read each -plate's stored guid. +Returns a 1-based table of GUID strings (modern +`"0xHHHHHHHHHHHHHHHH"` format) — one per CGUnit with an allocated +nameplate, **regardless** of whether the frame has been registered +with Lua. Catches default vanilla nameplates that +[`GetNamePlates`](#c_nameplategetnameplates) can't surface as +frames. ```lua /dump C_NamePlate.GetNamePlateGUIDs() -- { "0xF13000C36C26FD02", "0xF130000009276912", ... } ``` -Same enumeration as `GetNamePlates`; the two return parallel lists -in the same hash-bucket order (which isn't stable across calls). +Walks the local-player-anchored object hash table for `TYPEMASK_UNIT` +entries, filters by `*(unit + 0xE60) != nullptr`. The per-unit +nameplate pointer is set by `FUN_006086E0`'s "show nameplate" path +regardless of which nameplate system rendered it. Order follows +hash-bucket iteration and isn't stable across calls. ## NameCache diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 1111381..e799068 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -12,16 +12,32 @@ // ClassicAPI. If not, see . // `C_NamePlate.GetNamePlates()` — returns nameplate Frame objects, -// matching modern WoW's signature. Each CGNamePlateFrame is a CFrame -// subclass that stores its Lua-registry ref-key at the standard -// `OFF_COBJECT_LUA_REGISTRY_REF = +0x08`; pushing -// `registry[refKey]` yields the Lua table that addons can call -// `:Show()`, `:GetWidth()`, etc. on. +// matching modern WoW's signature. Two paths because vanilla has two +// kinds of nameplates: +// +// 1. **Addon-created** (pfUI, TidyPlates, etc.) — already registered +// with Lua via `CreateFrame`. Their Lua-registry ref-key sits at +// `+0x08`; we push `registry[refKey]` to return the cached +// wrapper. Identity stable across calls. +// +// 2. **Default vanilla nameplates** — created internally without ever +// calling `CreateFrame`, so `+0x08` is 0. We build a fresh wrapper +// table per call: `{[0] = lightuserdata(frame)}` with the global +// `__framescript_meta` metatable. Methods work +// (`:GetWidth()` / `:GetAlpha()` / etc.) but the wrapper isn't +// cached engine-side — calling `GetNamePlates()` again returns a +// different table for the same frame. Don't compare wrappers by +// identity, and don't cache them across the unit going out of +// range (the underlying frame may be freed). +// +// We deliberately don't call the engine's frame-registration helper +// (`FUN_00701BD0`) for the unregistered case — it increments a Lua +// refcount on the frame that's never decremented, pinning the frame +// in memory. // // `C_NamePlate.GetNamePlateGUIDs()` — companion returning the GUID -// strings of the same set of units. Faster than walking -// `GetNamePlates()` + reading guid back, useful when an addon only -// needs the GUIDs (raid-target tracking, threat coloring, etc.). +// strings of the same set of units, regardless of registration +// state. Cheapest enumeration when an addon only needs the GUIDs. // // Vanilla 1.12 stores each unit's nameplate pointer at `CGUnit + 0xE60` // (verified via `FUN_006086E0`'s "ensure nameplate exists" path). The @@ -58,6 +74,33 @@ constexpr uint32_t kTypeMaskUnit = 0x08; constexpr int kOffUnitNamePlate = 0xE60; using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); +using LuaPushLightUserdata_t = void(__fastcall *)(void *L, void *p); +using LuaSetMetatable_t = int(__fastcall *)(void *L, int idx); + +constexpr uintptr_t kFunLuaPushLightUserdata = 0x006F3A20; +constexpr uintptr_t kFunLuaSetMetatable = 0x006F4020; +constexpr int kLuaGlobalsIndex = -10001; +constexpr const char *kFrameMetatableGlobal = "__framescript_meta"; + +// Build a fresh frame wrapper on the Lua stack: `{[0] = frame}` with +// `_G["__framescript_meta"]` as metatable. Same shape the engine's +// frame-registration helper builds, minus the registry-cache step +// (which we skip to avoid pinning the frame's refcount). +void PushFreshFrameWrapper(void *L, void *frame) { + auto pushLight = reinterpret_cast( + kFunLuaPushLightUserdata); + auto setMetatable = reinterpret_cast( + kFunLuaSetMetatable); + + Game::Lua::NewTable(L); + Game::Lua::PushNumber(L, 0); + pushLight(L, frame); + Game::Lua::RawSet(L, -3); + + Game::Lua::PushString(L, kFrameMetatableGlobal); + Game::Lua::GetTable(L, kLuaGlobalsIndex); + setMetatable(L, -2); +} // Walk visible units with allocated nameplates, invoking `emit(unit, // nameplate, instance)` for each. Returns the number of emissions. @@ -115,12 +158,18 @@ static int __fastcall Script_GetNamePlates(void *L) { ForEachNamePlatedUnit( [L, rawgeti, &nextIndex](const uint8_t *, const uint8_t *nameplate, const uint8_t *) { + Game::Lua::PushNumber(L, static_cast(nextIndex++)); + // The engine initializes `OFF_COBJECT_LUA_REGISTRY_REF` to + // `LUA_NOREF` (`-2`) for internally-created frames; a real + // refkey is always a positive integer from `luaL_ref`. + // Treat anything `<= 0` as unregistered. const int refKey = *reinterpret_cast( nameplate + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); - if (refKey == 0) - return; - Game::Lua::PushNumber(L, static_cast(nextIndex++)); - rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + if (refKey > 0) { + rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + } else { + PushFreshFrameWrapper(L, const_cast(nameplate)); + } Game::Lua::SetTable(L, -3); }); return 1; From 26bae33b814b15c3f73f30c7c6194cc2e65ed7d1 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 01:43:07 -0500 Subject: [PATCH 05/15] C_NamePlate.GetNamePlateForUnit --- README.md | 2 +- docs/API.md | 20 +++++++++++++ src/nameplate/Info.cpp | 68 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 062868e..31931e5 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**. | [Macros](docs/API.md#macros) | `GetMacroSpell` | | [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` | | [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` | -| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlates`, `C_NamePlate.GetNamePlateGUIDs` | +| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` | | [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` | | [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` | | [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` | diff --git a/docs/API.md b/docs/API.md index fec8ee0..59e60fc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -198,6 +198,7 @@ build instructions. - [NamePlate](#nameplate) - [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates) - [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids) + - [`C_NamePlate.GetNamePlateForUnit(unitToken)`](#c_nameplategetnameplateforunitunittoken) - [NameCache](#namecache) - [`GetPlayerInfoByGUID(guid)`](#getplayerinfobyguidguid) @@ -4397,6 +4398,25 @@ Lua 5.0 has no `select()`, so collect into a table via different region layouts — those frames inherit whatever shape the addon built, not this one. +### `C_NamePlate.GetNamePlateForUnit(unitToken)` + +Returns the nameplate Frame for a single unit (resolved via the +engine's token-to-GUID path, so out-of-range party/raid members +work too), or `nil` if the unit has no allocated nameplate. + +```lua +local plate = C_NamePlate.GetNamePlateForUnit("target") +if plate then + local regions = {plate:GetRegions()} + print("targeting:", regions[3]:GetText()) -- e.g. "Santora" +end +``` + +Same registered-vs-fresh-wrapper behavior as `GetNamePlates()` — +addon-created plates return their cached wrapper, default vanilla +plates get a fresh per-call wrapper. Don't cache the result across +the unit going out of range. + ### `C_NamePlate.GetNamePlateGUIDs()` Returns a 1-based table of GUID strings (modern diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index e799068..1d146f3 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -76,6 +76,10 @@ constexpr int kOffUnitNamePlate = 0xE60; using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); using LuaPushLightUserdata_t = void(__fastcall *)(void *L, void *p); using LuaSetMetatable_t = int(__fastcall *)(void *L, int idx); +using TokenToGUID_t = uint64_t(__fastcall *)(const char *token); +using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName, + uint32_t guidLo, uint32_t guidHi, + int priority); constexpr uintptr_t kFunLuaPushLightUserdata = 0x006F3A20; constexpr uintptr_t kFunLuaSetMetatable = 0x006F4020; @@ -193,11 +197,75 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) { return 1; } +// Pushes a nameplate Frame onto the stack — registered or fresh +// wrapper depending on whether the engine assigned a real refKey. +static void PushNamePlateFrame(void *L, void *nameplate) { + const int refKey = *reinterpret_cast( + static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); + if (refKey > 0) { + auto rawgeti = reinterpret_cast( + Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); + rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + } else { + PushFreshFrameWrapper(L, nameplate); + } +} + +// `C_NamePlate.GetNamePlateForUnit(unitToken)` — returns the +// nameplate Frame for the given unit, or `nil` if the unit has no +// nameplate (out of range, hidden, etc.). Resolves the token to a +// GUID via the engine's `FUN_TOKEN_TO_GUID` so distant party/raid +// members can be queried, then looks up the CGUnit via the object +// hash and reads its `+0xE60` nameplate pointer. +static int __fastcall Script_GetNamePlateForUnit(void *L) { + if (!Game::Lua::IsString(L, 1)) { + Game::Lua::PushNil(L); + return 1; + } + const char *token = Game::Lua::ToString(L, 1); + if (token == nullptr) { + Game::Lua::PushNil(L); + return 1; + } + + auto tokenToGuid = reinterpret_cast( + static_cast(Offsets::FUN_TOKEN_TO_GUID)); + const uint64_t guid = tokenToGuid(token); + if (guid == 0) { + Game::Lua::PushNil(L); + return 1; + } + + auto resolve = reinterpret_cast( + static_cast(Offsets::FUN_OBJECT_RESOLVE_BY_GUID)); + auto *unit = static_cast( + resolve(Offsets::OBJ_TYPE_UNIT, "NamePlate", + static_cast(guid), + static_cast(guid >> 32), + 0x172)); + if (unit == nullptr) { + Game::Lua::PushNil(L); + return 1; + } + + auto *nameplate = *reinterpret_cast( + unit + kOffUnitNamePlate); + if (nameplate == nullptr) { + Game::Lua::PushNil(L); + return 1; + } + + PushNamePlateFrame(L, nameplate); + return 1; +} + static void RegisterLuaFunctions() { Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlates", &Script_GetNamePlates); Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateGUIDs", &Script_GetNamePlateGUIDs); + Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateForUnit", + &Script_GetNamePlateForUnit); } static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; From c17d4d2de5e0c441a312704f2beacea221ef0447 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 02:17:34 -0500 Subject: [PATCH 06/15] nameplate events --- README.md | 3 ++ docs/API.md | 51 ++++++++++++++++++ src/nameplate/Events.cpp | 114 +++++++++++++++++++++++++++++++++++++++ src/nameplate/Info.cpp | 59 ++------------------ src/nameplate/Walk.h | 83 ++++++++++++++++++++++++++++ 5 files changed, 254 insertions(+), 56 deletions(-) create mode 100644 src/nameplate/Events.cpp create mode 100644 src/nameplate/Walk.h diff --git a/README.md b/README.md index 31931e5..e32a5bd 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ functions, just behavior the stock 1.12 engine didn't have. See the | `HEARTHSTONE_BOUND` | *(none)* | | `ITEM_DATA_LOAD_RESULT` | `itemID, success` | | `MODIFIER_STATE_CHANGED` | `keyName, down` | +| `NAME_PLATE_CREATED` | `unitGUID` | +| `NAME_PLATE_UNIT_ADDED` | `unitGUID` | +| `NAME_PLATE_UNIT_REMOVED` | `unitGUID` | | `PLAYER_STARTED_LOOKING` | *(none)* | | `PLAYER_STOPPED_LOOKING` | *(none)* | | `PLAYER_STARTED_MOVING` | *(none)* | diff --git a/docs/API.md b/docs/API.md index 59e60fc..66859b0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -73,6 +73,7 @@ build instructions. - [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event) - [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event) - [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event) + - [`NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events) - [`QUEST_ACCEPTED` event](#quest_accepted-event) - [`QUEST_TURNED_IN` event](#quest_turned_in-event) - [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event) @@ -1572,6 +1573,56 @@ renderer-state changes that recreate WoW's main window (e.g. toggling vertical sync), where an `SetWindowLongPtr`-style `WNDPROC` subclass would be left dangling. +### `NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events + +Fire when nameplate state actually changes. All three carry a single +payload — the **unit GUID string** (modern `"0xHHHHHHHHHHHHHHHH"` +format). + +| Event | When it fires | +|-------|---------------| +| `NAME_PLATE_CREATED` | First time we surface a particular `CGNamePlateFrame` pointer. Same frame re-used for a later unit (pool recycle) does NOT refire. | +| `NAME_PLATE_UNIT_ADDED` | Unit gets a visible nameplate (entered nameplate range, became hostile, etc.). | +| `NAME_PLATE_UNIT_REMOVED` | Unit's nameplate is gone (left range, despawned, etc.). | + +```lua +local f = CreateFrame("Frame") +f:RegisterEvent("NAME_PLATE_UNIT_ADDED") +f:RegisterEvent("NAME_PLATE_UNIT_REMOVED") +f:SetScript("OnEvent", function() + -- arg1 = unit GUID string ("0xF13000C36C26FD02", etc.) + if event == "NAME_PLATE_UNIT_ADDED" then + -- skin the nameplate, register tracking, etc. + end +end) +``` + +> **Modern divergence.** Retail passes `"nameplateN"` unit tokens to +> ADDED/REMOVED and the nameplate `Frame` to CREATED. We don't expose +> nameplate tokens (the engine's resolver isn't extensible without +> hooking it), and the event dispatcher can't push a Frame as +> payload — so all three events use the GUID string. Addons that +> need the Frame call +> [`C_NamePlate.GetNamePlateForUnit`](#c_nameplategetnameplateforunitunittoken) +> reactively, using the GUID via a custom token-resolution path or +> matching against `GetNamePlateGUIDs()`. + +**Implementation notes** + +Detected by per-frame polling, not engine hooks. Each world tick we +walk the object hash for nameplated units and diff against the +previous tick's snapshot. Modern WoW also synthesizes these via +diffing (the underlying engine has no event for "plate state +changed"). The cost is ~20-50µs/frame even in busy raids — well +below noise. + +The diff approach absorbs the engine's transient hide/reshow cycle: +vanilla has ~7 code paths that briefly zero `unit + 0xE60` (z-order +rebuilds, anchor changes, flag-change re-eval) and the next frame's +show path re-allocates from the pool. Those transient zeroes never +become events because the unit appears in both the previous and +current tick's snapshot. + ### `QUEST_ACCEPTED` event Fires once per quest the player just accepted, with two payload args: diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp new file mode 100644 index 0000000..ddec6e7 --- /dev/null +++ b/src/nameplate/Events.cpp @@ -0,0 +1,114 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU Lesser General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along with +// ClassicAPI. If not, see . + +// Nameplate lifecycle events: NAME_PLATE_CREATED, NAME_PLATE_UNIT_ADDED, +// NAME_PLATE_UNIT_REMOVED. +// +// Per-frame poll: walk visible CGUnits with allocated nameplates, diff +// against last frame's snapshot, fire events on real transitions only. +// The engine's internal hide/show cycle (z-order rebuilds, anchor +// changes, ~7 callers of `FUN_00608A10` that transiently zero `+0xE60`) +// is absorbed because we only compare with the previous *frame's* +// state, not every transient `unit + 0xE60` write. +// +// Events fire with the unit's GUID string as payload — vanilla has no +// `"nameplateN"` tokens (engine resolver isn't extensible) and the +// event dispatcher's format codes don't include a "push frame" type. +// Addons that need the frame call +// `C_NamePlate.GetNamePlateForUnit()` +// reactively. + +#include "Game.h" +#include "Offsets.h" +#include "event/Custom.h" +#include "guid/Guid.h" +#include "nameplate/Walk.h" +#include "tick/WorldTick.h" + +#include +#include +#include + +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}; + +// Previous tick's snapshot — GUID → nameplate-frame pointer for each +// nameplated unit. Compared against the next tick's walk to compute +// ADDED/REMOVED diffs. +std::unordered_map g_lastTickPlates; + +// Scratch map reused each tick, swapped into `g_lastTickPlates` at +// the end. File-static so we don't pay the constructor/destructor +// cycle every frame — `clear()` keeps the existing bucket capacity. +std::unordered_map g_currentTickPlates; + +// Frame pointers we've ever surfaced as nameplate plates. First +// sighting fires NAME_PLATE_CREATED; same pointer reappearing (pool +// reuse) doesn't refire. +std::unordered_set g_seenPlates; + +void FireWithGUID(const char *eventName, uint64_t guid) { + if (guid == 0) + return; + const int slot = Event::Custom::Lookup(eventName); + if (slot < 0) + return; + char buf[Guid::STRING_SIZE]; + Guid::FormatAsString(guid, buf, sizeof buf); + Event::Custom::Fire(slot, "%s", buf); +} + +void OnWorldTick() { + g_currentTickPlates.clear(); + g_currentTickPlates.reserve(64); // typical visible-nameplate ceiling + NamePlate::Walk::ForEachNamePlatedUnit( + [](const uint8_t *, const uint8_t *nameplate, + const uint8_t *instance) { + const uint64_t guid = *reinterpret_cast(instance); + if (guid == 0) + return; + g_currentTickPlates.emplace(guid, nameplate); + }); + + // Fire CREATED for never-before-seen frame pointers, ADDED for + // GUIDs not in last tick's snapshot. + for (const auto &kv : g_currentTickPlates) { + if (g_seenPlates.insert(kv.second).second) + FireWithGUID(kEventCreated, kv.first); + if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) + FireWithGUID(kEventUnitAdded, kv.first); + } + + // Fire REMOVED for GUIDs in last tick's snapshot but not current. + for (const auto &kv : g_lastTickPlates) { + if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) + FireWithGUID(kEventUnitRemoved, kv.first); + } + + g_lastTickPlates.swap(g_currentTickPlates); +} + +} // namespace + +static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick}; + +} // namespace NamePlate::Events diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 1d146f3..2362cd4 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -55,6 +55,7 @@ #include "Game.h" #include "Offsets.h" #include "guid/Guid.h" +#include "nameplate/Walk.h" #include @@ -62,16 +63,8 @@ namespace NamePlate::Info { namespace { -constexpr uintptr_t kLocalPlayerGlobal = 0x00B41414; -constexpr int kOffPlayerBucketArray = 0x1C; -constexpr int kOffPlayerBucketMask = 0x24; -constexpr int kBucketStride = 12; -constexpr int kBucketLinkOffsetField = 0; // byte 0: link-field offset within entry -constexpr int kBucketChainHeadField = 8; // byte 8: chain head pointer -constexpr int kOffEntryInstanceBlock = 0x08; -constexpr int kOffInstanceTypeMask = 0x08; -constexpr uint32_t kTypeMaskUnit = 0x08; -constexpr int kOffUnitNamePlate = 0xE60; +using NamePlate::Walk::ForEachNamePlatedUnit; +using NamePlate::Walk::kOffUnitNamePlate; using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); using LuaPushLightUserdata_t = void(__fastcall *)(void *L, void *p); @@ -106,52 +99,6 @@ void PushFreshFrameWrapper(void *L, void *frame) { setMetatable(L, -2); } -// Walk visible units with allocated nameplates, invoking `emit(unit, -// nameplate, instance)` for each. Returns the number of emissions. -template -int ForEachNamePlatedUnit(F &&emit) { - auto *player = *reinterpret_cast(kLocalPlayerGlobal); - if (player == nullptr) - return 0; - - auto *buckets = *reinterpret_cast( - player + kOffPlayerBucketArray); - const uint32_t mask = *reinterpret_cast( - player + kOffPlayerBucketMask); - if (buckets == nullptr || mask == 0xFFFFFFFFu) - return 0; - - int count = 0; - for (uint32_t b = 0; b <= mask; ++b) { - const uint8_t *bucket = buckets + b * kBucketStride; - const uint32_t linkOffset = *reinterpret_cast( - bucket + kBucketLinkOffsetField); - uintptr_t entry = *reinterpret_cast( - bucket + kBucketChainHeadField); - - while (entry != 0 && (entry & 1) == 0) { - auto *obj = reinterpret_cast(entry); - auto *instance = *reinterpret_cast( - obj + kOffEntryInstanceBlock); - if (instance != nullptr) { - const uint32_t typeMask = *reinterpret_cast( - instance + kOffInstanceTypeMask); - if ((typeMask & kTypeMaskUnit) != 0) { - const auto *nameplate = *reinterpret_cast( - obj + kOffUnitNamePlate); - if (nameplate != nullptr) { - emit(obj, nameplate, instance); - ++count; - } - } - } - entry = *reinterpret_cast( - obj + linkOffset + 4); - } - } - return count; -} - } // namespace static int __fastcall Script_GetNamePlates(void *L) { diff --git a/src/nameplate/Walk.h b/src/nameplate/Walk.h new file mode 100644 index 0000000..6a92d66 --- /dev/null +++ b/src/nameplate/Walk.h @@ -0,0 +1,83 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU Lesser General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +#include + +// Shared nameplate enumeration — walks the local-player-anchored +// object hash table for visible `TYPEMASK_UNIT` entries and invokes +// `emit(unit, nameplate, instance)` for each unit that currently has +// an allocated nameplate (`unit + 0xE60` non-null). +// +// Header-only template — used by both the Lua-facing accessors in +// `nameplate/Info.cpp` and the per-tick differ in `nameplate/Events.cpp`. + +namespace NamePlate::Walk { + +constexpr uintptr_t kLocalPlayerGlobal = 0x00B41414; +constexpr int kOffPlayerBucketArray = 0x1C; +constexpr int kOffPlayerBucketMask = 0x24; +constexpr int kBucketStride = 12; +constexpr int kBucketLinkOffsetField = 0; +constexpr int kBucketChainHeadField = 8; +constexpr int kOffEntryInstanceBlock = 0x08; +constexpr int kOffInstanceTypeMask = 0x08; +constexpr uint32_t kTypeMaskUnit = 0x08; +constexpr int kOffUnitNamePlate = 0xE60; + +template +int ForEachNamePlatedUnit(F &&emit) { + auto *player = *reinterpret_cast(kLocalPlayerGlobal); + if (player == nullptr) + return 0; + + auto *buckets = *reinterpret_cast( + player + kOffPlayerBucketArray); + const uint32_t mask = *reinterpret_cast( + player + kOffPlayerBucketMask); + if (buckets == nullptr || mask == 0xFFFFFFFFu) + return 0; + + int count = 0; + for (uint32_t b = 0; b <= mask; ++b) { + const uint8_t *bucket = buckets + b * kBucketStride; + const uint32_t linkOffset = *reinterpret_cast( + bucket + kBucketLinkOffsetField); + uintptr_t entry = *reinterpret_cast( + bucket + kBucketChainHeadField); + + while (entry != 0 && (entry & 1) == 0) { + auto *obj = reinterpret_cast(entry); + auto *instance = *reinterpret_cast( + obj + kOffEntryInstanceBlock); + if (instance != nullptr) { + const uint32_t typeMask = *reinterpret_cast( + instance + kOffInstanceTypeMask); + if ((typeMask & kTypeMaskUnit) != 0) { + const auto *nameplate = *reinterpret_cast( + obj + kOffUnitNamePlate); + if (nameplate != nullptr) { + emit(obj, nameplate, instance); + ++count; + } + } + } + entry = *reinterpret_cast( + obj + linkOffset + 4); + } + } + return count; +} + +} // namespace NamePlate::Walk From 4140d1b6cbba47eab8d2079ba10f11804a307606 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 02:24:02 -0500 Subject: [PATCH 07/15] C_NamePlate.GetNamePlateForGUID --- README.md | 2 +- docs/API.md | 23 +++++++++++++++ src/nameplate/Info.cpp | 66 ++++++++++++++++++++++++++++-------------- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e32a5bd..fdc9060 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**. | [Macros](docs/API.md#macros) | `GetMacroSpell` | | [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` | | [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` | -| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` | +| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` | | [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` | | [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` | | [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` | diff --git a/docs/API.md b/docs/API.md index 66859b0..bfc8535 100644 --- a/docs/API.md +++ b/docs/API.md @@ -200,6 +200,7 @@ build instructions. - [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates) - [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids) - [`C_NamePlate.GetNamePlateForUnit(unitToken)`](#c_nameplategetnameplateforunitunittoken) + - [`C_NamePlate.GetNamePlateForGUID(guidString)`](#c_nameplategetnameplateforguidguidstring) - [NameCache](#namecache) - [`GetPlayerInfoByGUID(guid)`](#getplayerinfobyguidguid) @@ -4468,6 +4469,28 @@ addon-created plates return their cached wrapper, default vanilla plates get a fresh per-call wrapper. Don't cache the result across the unit going out of range. +### `C_NamePlate.GetNamePlateForGUID(guidString)` + +Same as `GetNamePlateForUnit` but takes the `"0xHHHHHHHHHHHHHHHH"` +GUID-string form. Designed to pair with the +[NAME_PLATE_UNIT_ADDED / REMOVED](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events) +events, whose payload is the unit GUID rather than a token. + +```lua +local f = CreateFrame("Frame") +f:RegisterEvent("NAME_PLATE_UNIT_ADDED") +f:SetScript("OnEvent", function() + -- arg1 = GUID string + local plate = C_NamePlate.GetNamePlateForGUID(arg1) + if plate then + -- ... skin / style plate ... + end +end) +``` + +Returns `nil` if the GUID doesn't parse, doesn't resolve to a +visible CGUnit, or the unit has no allocated nameplate. + ### `C_NamePlate.GetNamePlateGUIDs()` Returns a 1-based table of GUID strings (modern diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 2362cd4..7a8b1e5 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -158,12 +158,39 @@ static void PushNamePlateFrame(void *L, void *nameplate) { } } +// GUID → nameplate Frame pushed on stack. Pushes nil if the GUID +// doesn't resolve to a visible CGUnit, or the unit has no allocated +// nameplate. +static void PushNamePlateForGUID(void *L, uint64_t guid) { + if (guid == 0) { + Game::Lua::PushNil(L); + return; + } + auto resolve = reinterpret_cast( + static_cast(Offsets::FUN_OBJECT_RESOLVE_BY_GUID)); + auto *unit = static_cast( + resolve(Offsets::OBJ_TYPE_UNIT, "NamePlate", + static_cast(guid), + static_cast(guid >> 32), + 0x172)); + if (unit == nullptr) { + Game::Lua::PushNil(L); + return; + } + auto *nameplate = *reinterpret_cast( + unit + kOffUnitNamePlate); + if (nameplate == nullptr) { + Game::Lua::PushNil(L); + return; + } + PushNamePlateFrame(L, nameplate); +} + // `C_NamePlate.GetNamePlateForUnit(unitToken)` — returns the // nameplate Frame for the given unit, or `nil` if the unit has no // nameplate (out of range, hidden, etc.). Resolves the token to a // GUID via the engine's `FUN_TOKEN_TO_GUID` so distant party/raid -// members can be queried, then looks up the CGUnit via the object -// hash and reads its `+0xE60` nameplate pointer. +// members can be queried. static int __fastcall Script_GetNamePlateForUnit(void *L) { if (!Game::Lua::IsString(L, 1)) { Game::Lua::PushNil(L); @@ -174,35 +201,28 @@ static int __fastcall Script_GetNamePlateForUnit(void *L) { Game::Lua::PushNil(L); return 1; } - auto tokenToGuid = reinterpret_cast( static_cast(Offsets::FUN_TOKEN_TO_GUID)); - const uint64_t guid = tokenToGuid(token); - if (guid == 0) { - Game::Lua::PushNil(L); - return 1; - } + PushNamePlateForGUID(L, tokenToGuid(token)); + return 1; +} - auto resolve = reinterpret_cast( - static_cast(Offsets::FUN_OBJECT_RESOLVE_BY_GUID)); - auto *unit = static_cast( - resolve(Offsets::OBJ_TYPE_UNIT, "NamePlate", - static_cast(guid), - static_cast(guid >> 32), - 0x172)); - if (unit == nullptr) { +// `C_NamePlate.GetNamePlateForGUID(guidString)` — same as +// `GetNamePlateForUnit` but takes a `"0xHHHHHHHHHHHHHHHH"` GUID +// string. Useful for handling the `NAME_PLATE_UNIT_ADDED` / +// `_REMOVED` events whose payload is a GUID, not a unit token. +static int __fastcall Script_GetNamePlateForGUID(void *L) { + if (!Game::Lua::IsString(L, 1)) { Game::Lua::PushNil(L); return 1; } - - auto *nameplate = *reinterpret_cast( - unit + kOffUnitNamePlate); - if (nameplate == nullptr) { + const char *guidStr = Game::Lua::ToString(L, 1); + uint64_t guid = 0; + if (guidStr == nullptr || !Guid::Parse(guidStr, &guid)) { Game::Lua::PushNil(L); return 1; } - - PushNamePlateFrame(L, nameplate); + PushNamePlateForGUID(L, guid); return 1; } @@ -213,6 +233,8 @@ static void RegisterLuaFunctions() { &Script_GetNamePlateGUIDs); Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateForUnit", &Script_GetNamePlateForUnit); + Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateForGUID", + &Script_GetNamePlateForGUID); } static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; From 3b01601cb79188c500f91ba2d44157a59766b64e Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 12:22:48 -0500 Subject: [PATCH 08/15] NAME_PLATE_CREATED now sends frame as argument --- README.md | 2 +- docs/API.md | 45 ++++++++++++++---------- src/Offsets.h | 8 +++++ src/nameplate/Events.cpp | 75 +++++++++++++++++++++++++++++++++++----- src/nameplate/Info.cpp | 7 ++-- src/nameplate/Walk.h | 10 ++++++ 6 files changed, 115 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index fdc9060..7de4d4f 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the | `HEARTHSTONE_BOUND` | *(none)* | | `ITEM_DATA_LOAD_RESULT` | `itemID, success` | | `MODIFIER_STATE_CHANGED` | `keyName, down` | -| `NAME_PLATE_CREATED` | `unitGUID` | +| `NAME_PLATE_CREATED` | `nameplateFrame` | | `NAME_PLATE_UNIT_ADDED` | `unitGUID` | | `NAME_PLATE_UNIT_REMOVED` | `unitGUID` | | `PLAYER_STARTED_LOOKING` | *(none)* | diff --git a/docs/API.md b/docs/API.md index bfc8535..2de6dee 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1576,37 +1576,44 @@ would be left dangling. ### `NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events -Fire when nameplate state actually changes. All three carry a single -payload — the **unit GUID string** (modern `"0xHHHHHHHHHHHHHHHH"` -format). +Fire when nameplate state actually changes. Payloads: -| Event | When it fires | -|-------|---------------| -| `NAME_PLATE_CREATED` | First time we surface a particular `CGNamePlateFrame` pointer. Same frame re-used for a later unit (pool recycle) does NOT refire. | -| `NAME_PLATE_UNIT_ADDED` | Unit gets a visible nameplate (entered nameplate range, became hostile, etc.). | -| `NAME_PLATE_UNIT_REMOVED` | Unit's nameplate is gone (left range, despawned, etc.). | +| Event | `arg1` | Notes | +|-------|--------|-------| +| `NAME_PLATE_CREATED` | nameplate **Frame** | Matches modern WoW. Fires once per unique `CGNamePlateFrame` pointer — same frame re-used via pool recycle does NOT refire. | +| `NAME_PLATE_UNIT_ADDED` | unit **GUID string** | Modern uses a `"nameplateN"` token; vanilla's token resolver isn't extensible. Pass `arg1` to [`GetNamePlateForGUID`](#c_nameplategetnameplateforguidguidstring) for the frame. | +| `NAME_PLATE_UNIT_REMOVED` | unit **GUID string** | Same as above. | ```lua local f = CreateFrame("Frame") +f:RegisterEvent("NAME_PLATE_CREATED") f:RegisterEvent("NAME_PLATE_UNIT_ADDED") f:RegisterEvent("NAME_PLATE_UNIT_REMOVED") f:SetScript("OnEvent", function() - -- arg1 = unit GUID string ("0xF13000C36C26FD02", etc.) - if event == "NAME_PLATE_UNIT_ADDED" then - -- skin the nameplate, register tracking, etc. + if event == "NAME_PLATE_CREATED" then + -- arg1 = the nameplate Frame itself + arg1:SetAlpha(0.8) + elseif event == "NAME_PLATE_UNIT_ADDED" then + -- arg1 = unit GUID string ("0xF13000C36C26FD02", etc.) + local plate = C_NamePlate.GetNamePlateForGUID(arg1) + -- ... style based on the unit ... end end) ``` +> **CREATED timing with nameplate addons (pfUI / TidyPlates / etc).** +> The event fires when the *engine* allocates the underlying +> `CGNamePlateFrame`. Nameplate-mod addons typically decorate the +> frame on their own per-frame update — so `arg1` at `CREATED` time +> is a bare frame with no addon-side decorations yet. For +> unit-specific work after the addon has decorated, use +> `NAME_PLATE_UNIT_ADDED` (fires next tick at the latest) or fetch +> the current frame on-demand via `GetNamePlateForGUID`. + > **Modern divergence.** Retail passes `"nameplateN"` unit tokens to -> ADDED/REMOVED and the nameplate `Frame` to CREATED. We don't expose -> nameplate tokens (the engine's resolver isn't extensible without -> hooking it), and the event dispatcher can't push a Frame as -> payload — so all three events use the GUID string. Addons that -> need the Frame call -> [`C_NamePlate.GetNamePlateForUnit`](#c_nameplategetnameplateforunitunittoken) -> reactively, using the GUID via a custom token-resolution path or -> matching against `GetNamePlateGUIDs()`. +> `ADDED`/`REMOVED`. Vanilla's token resolver isn't extensible, so +> we ship the GUID string instead and let addons round-trip through +> `GetNamePlateForGUID` to reach the frame. **Implementation notes** diff --git a/src/Offsets.h b/src/Offsets.h index 104c382..0f33f08 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -2089,6 +2089,14 @@ enum Offsets { // to L->top → `add [ecx+8], 0x10` incr_top). `0x6F32B0` is // `lua_replace` (single TValue copy + decr_top). LUA_PUSH_VALUE = 0x6F3350, + // `luaL_ref(L, t)` — pops the top, stores it in the table at `t` + // at a freshly-allocated integer key, returns the key. Use with + // `LUA_REGISTRY_INDEX` to stash Lua values across C-side scopes; + // pair with `LUA_REF_UNREF` to release. + LUA_REF_REF = 0x6F5310, + // `luaL_unref(L, t, ref)` — releases a ref previously returned + // by `luaL_ref`, freeing the slot for future allocations. + LUA_REF_UNREF = 0x6F5400, LUA_PUSH_CCLOSURE = 0x6F3920, LUA_NEW_TABLE = 0x6F3C90, LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset) diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp index ddec6e7..4bde3e1 100644 --- a/src/nameplate/Events.cpp +++ b/src/nameplate/Events.cpp @@ -21,12 +21,18 @@ // is absorbed because we only compare with the previous *frame's* // state, not every transient `unit + 0xE60` write. // -// Events fire with the unit's GUID string as payload — vanilla has no -// `"nameplateN"` tokens (engine resolver isn't extensible) and the -// event dispatcher's format codes don't include a "push frame" type. -// Addons that need the frame call -// `C_NamePlate.GetNamePlateForUnit()` -// reactively. +// Payload shape: +// - `NAME_PLATE_CREATED` — `arg1` is the nameplate **Frame** (matches +// modern WoW). The engine's printf-style dispatcher +// (`FUN_FIRE_EVENT`) only knows `%s`/`%d`/`%u`/`%f` format codes +// with no "push Lua value" option, so we route this event through +// a pre-set path: save current `_G.arg1`, set it to the frame, fire +// with empty format (dispatcher leaves `_G.arg` alone when no +// codes are parsed), restore. +// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the unit GUID +// string. Modern uses a `"nameplateN"` token; vanilla's token +// resolver isn't extensible. Addons call +// `C_NamePlate.GetNamePlateForGUID(arg1)` to get the frame. #include "Game.h" #include "Offsets.h" @@ -77,6 +83,56 @@ void FireWithGUID(const char *eventName, uint64_t guid) { Event::Custom::Fire(slot, "%s", buf); } +// Fire `eventName` with the nameplate `Frame` set as `_G.arg1`. +// `FUN_FIRE_EVENT`'s format-string parser only handles primitive +// types, but it only mutates `_G.arg` for codes it actually +// parses. With an empty format we pre-set `_G.arg1` and the +// dispatcher leaves it alone. Restore the previous value after +// fire so we don't leak our frame into unrelated global state. +// +// Lua-stack-clean: stack depth on entry == stack depth on exit. +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) { + if (frame == nullptr) + return; + const int slot = Event::Custom::Lookup(eventName); + if (slot < 0) + return; + + void *L = Game::Lua::State(); + if (L == nullptr) + return; + + auto refRef = reinterpret_cast( + static_cast(Offsets::LUA_REF_REF)); + auto refUnref = reinterpret_cast( + static_cast(Offsets::LUA_REF_UNREF)); + auto rawgeti = reinterpret_cast( + Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); + + // Save current `_G.arg1` to the registry. + Game::Lua::PushString(L, "arg1"); + Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); + const int savedRef = refRef(L, Game::Lua::REGISTRY_INDEX); + + // Set `_G.arg1 = frame`. + Game::Lua::PushString(L, "arg1"); + NamePlate::Info::PushNamePlateFrame(L, frame); + Game::Lua::SetTable(L, Game::Lua::GLOBALS_INDEX); + + // Fire — empty format, so the dispatcher doesn't touch `arg1`. + Event::Custom::Fire(slot, ""); + + // Restore previous `_G.arg1` from the saved registry ref. + Game::Lua::PushString(L, "arg1"); + rawgeti(L, Game::Lua::REGISTRY_INDEX, savedRef); + Game::Lua::SetTable(L, Game::Lua::GLOBALS_INDEX); + refUnref(L, Game::Lua::REGISTRY_INDEX, savedRef); +} + void OnWorldTick() { g_currentTickPlates.clear(); g_currentTickPlates.reserve(64); // typical visible-nameplate ceiling @@ -89,11 +145,12 @@ void OnWorldTick() { g_currentTickPlates.emplace(guid, nameplate); }); - // Fire CREATED for never-before-seen frame pointers, ADDED for - // GUIDs not in last tick's snapshot. + // Fire CREATED (with the Frame as arg1) for never-before-seen + // frame pointers; ADDED (with GUID string) for GUIDs not in last + // tick's snapshot. for (const auto &kv : g_currentTickPlates) { if (g_seenPlates.insert(kv.second).second) - FireWithGUID(kEventCreated, kv.first); + FireWithFrame(kEventCreated, const_cast(kv.second)); if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) FireWithGUID(kEventUnitAdded, kv.first); } diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 7a8b1e5..8c61738 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -144,9 +144,10 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) { return 1; } -// Pushes a nameplate Frame onto the stack — registered or fresh -// wrapper depending on whether the engine assigned a real refKey. -static void PushNamePlateFrame(void *L, void *nameplate) { +// Exported via `nameplate/Walk.h` so `Events.cpp` can reuse the same +// frame-push path. Internal callers (Script_GetNamePlates etc.) call +// it through the unqualified name (they live in the same namespace). +void PushNamePlateFrame(void *L, void *nameplate) { const int refKey = *reinterpret_cast( static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); if (refKey > 0) { diff --git a/src/nameplate/Walk.h b/src/nameplate/Walk.h index 6a92d66..2d78612 100644 --- a/src/nameplate/Walk.h +++ b/src/nameplate/Walk.h @@ -23,6 +23,16 @@ // Header-only template — used by both the Lua-facing accessors in // `nameplate/Info.cpp` and the per-tick differ in `nameplate/Events.cpp`. +namespace NamePlate::Info { + +// Pushes the nameplate Frame at `nameplate` onto the Lua stack — +// either the cached registry-wrapper (addon-registered plates) or a +// fresh per-call wrapper (default vanilla plates). Defined in +// `Info.cpp`. +void PushNamePlateFrame(void *L, void *nameplate); + +} // namespace NamePlate::Info + namespace NamePlate::Walk { constexpr uintptr_t kLocalPlayerGlobal = 0x00B41414; From 6145962f3e28280e095a664cfa22fede92b08c9c Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 12:53:48 -0500 Subject: [PATCH 09/15] document verification provenance for nameplate-adjacent offsets - Offsets.h: annotate LUA_REF_REF / LUA_REF_UNREF with their Ghidra decompile shapes (FREELIST_REF chain + LUA_REFNIL/NOREF sentinels) so the next maintainer doesn't have to re-derive whether these are really luaL_ref/unref. - Walk.h: warn that the 0x00B41414 player global overlays multiple sub-objects; reading +0x1C/+0x24 here is safe, feeding it to the inventory path crashes. --- src/Offsets.h | 12 ++++++++++-- src/nameplate/Walk.h | 9 +++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Offsets.h b/src/Offsets.h index 0f33f08..e4042e2 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -2092,10 +2092,18 @@ enum Offsets { // `luaL_ref(L, t)` — pops the top, stores it in the table at `t` // at a freshly-allocated integer key, returns the key. Use with // `LUA_REGISTRY_INDEX` to stash Lua values across C-side scopes; - // pair with `LUA_REF_UNREF` to release. + // pair with `LUA_REF_UNREF` to release. Verified by decompiling + // `FUN_006f5310`: classic luaL_ref shape — nil-top early-out + // returning `LUA_REFNIL = -1`, FREELIST_REF chain pop (pushvalue + + // tonumber on table[0]), else objlen-based new slot, finally + // table[ref] = popped value. LUA_REF_REF = 0x6F5310, // `luaL_unref(L, t, ref)` — releases a ref previously returned - // by `luaL_ref`, freeing the slot for future allocations. + // by `luaL_ref`, freeing the slot for future allocations. Verified + // by decompiling `FUN_006f5400`: `ref >= 0` guard (skips + // `LUA_NOREF = -2` / `LUA_REFNIL = -1`), reads current FREELIST_REF + // head, writes it to `table[ref]`, then updates FREELIST_REF = ref + // — the canonical freelist-link operation. LUA_REF_UNREF = 0x6F5400, LUA_PUSH_CCLOSURE = 0x6F3920, LUA_NEW_TABLE = 0x6F3C90, diff --git a/src/nameplate/Walk.h b/src/nameplate/Walk.h index 2d78612..704217a 100644 --- a/src/nameplate/Walk.h +++ b/src/nameplate/Walk.h @@ -35,6 +35,15 @@ void PushNamePlateFrame(void *L, void *nameplate); namespace NamePlate::Walk { +// `0x00B41414` is a player-related global that overlays multiple +// sub-objects at different offsets. We read the object-hash-table +// fields at `+0x1C` (bucket array) and `+0x24` (bucket mask) — that +// sub-struct is safe to walk for any unit the engine knows about. +// **Do not feed this pointer to inventory routines** (`GetItemBySlot` +// etc.); the CGPlayer_C inventory manager lives at a different +// offset (`+0x1D38`) and is sourced via `ResolveUnitToken("player")`, +// not via this global. See CLAUDE.md "Resolving an item-location" +// for the inventory-crash story. constexpr uintptr_t kLocalPlayerGlobal = 0x00B41414; constexpr int kOffPlayerBucketArray = 0x1C; constexpr int kOffPlayerBucketMask = 0x24; From f96ef0f3698df08a911f864964ec599edf8e6ebb Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 13:10:56 -0500 Subject: [PATCH 10/15] harden nameplate frame push against stale registry refs PushNamePlateFrame now type-checks the rawgeti'd value; on a stale slot (post-/reload windows, freed slot, etc.) it pops and falls back to a fresh wrapper instead of returning whatever the registry happens to hold at that index in the new Lua state. Script_GetNamePlates now routes through the same helper so it inherits the guard. Also document the bound on g_seenPlates: pool reuse via DAT_00c4d920 caps it at the session's peak simultaneous-plate count. --- src/nameplate/Events.cpp | 8 ++++++++ src/nameplate/Info.cpp | 38 +++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp index 4bde3e1..f98136e 100644 --- a/src/nameplate/Events.cpp +++ b/src/nameplate/Events.cpp @@ -70,6 +70,14 @@ std::unordered_map g_currentTickPlates; // Frame pointers we've ever surfaced as nameplate plates. First // sighting fires NAME_PLATE_CREATED; same pointer reappearing (pool // reuse) doesn't refire. +// +// Bounded by the engine's CGNamePlateFrame freelist high-water mark: +// `FUN_006087F0` first checks the global freelist head at +// `DAT_00c4d920` and recycles any waiting frame before falling back +// to `SMemAlloc(0x518)`. So the set grows only up to the peak +// simultaneous-visible-plate count for the session — typically +// <80 even in AV-scale scenes (matching the `reserve(64)` below) — +// then tops out as pool reuse covers all subsequent shows. std::unordered_set g_seenPlates; void FireWithGUID(const char *eventName, uint64_t guid) { diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 8c61738..127bfcd 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -103,24 +103,16 @@ void PushFreshFrameWrapper(void *L, void *frame) { static int __fastcall Script_GetNamePlates(void *L) { Game::Lua::NewTable(L); - auto rawgeti = reinterpret_cast( - Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); int nextIndex = 1; ForEachNamePlatedUnit( - [L, rawgeti, &nextIndex](const uint8_t *, const uint8_t *nameplate, - const uint8_t *) { + [L, &nextIndex](const uint8_t *, const uint8_t *nameplate, + const uint8_t *) { Game::Lua::PushNumber(L, static_cast(nextIndex++)); - // The engine initializes `OFF_COBJECT_LUA_REGISTRY_REF` to - // `LUA_NOREF` (`-2`) for internally-created frames; a real - // refkey is always a positive integer from `luaL_ref`. - // Treat anything `<= 0` as unregistered. - const int refKey = *reinterpret_cast( - nameplate + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); - if (refKey > 0) { - rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); - } else { - PushFreshFrameWrapper(L, const_cast(nameplate)); - } + // Shared helper validates refkey freshness (defensive vs. + // stale-across-reload) and falls back to a fresh wrapper + // if the registry slot doesn't actually hold the frame + // table any more. + PushNamePlateFrame(L, const_cast(nameplate)); Game::Lua::SetTable(L, -3); }); return 1; @@ -147,6 +139,16 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) { // Exported via `nameplate/Walk.h` so `Events.cpp` can reuse the same // frame-push path. Internal callers (Script_GetNamePlates etc.) call // it through the unqualified name (they live in the same namespace). +// +// Defensive against stale refkeys: on `/reload` the C++ frame +// persists but the Lua registry is rebuilt at a fresh state. The +// engine's per-frame teardown should release each refkey via +// `luaL_unref`, but we'd rather not rely on it for every frame in +// every reload path — if the rawgeti'd value isn't actually a +// frame-wrapper table (nil, freed registry slot, anything), pop it +// and synthesise a fresh wrapper. Same fallback the +// "unregistered frame" path uses, so the caller can't observe a +// non-table being pushed. void PushNamePlateFrame(void *L, void *nameplate) { const int refKey = *reinterpret_cast( static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); @@ -154,9 +156,11 @@ void PushNamePlateFrame(void *L, void *nameplate) { auto rawgeti = reinterpret_cast( Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); - } else { - PushFreshFrameWrapper(L, nameplate); + if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) + return; + Game::Lua::SetTop(L, -2); // pop the stale value } + PushFreshFrameWrapper(L, nameplate); } // GUID → nameplate Frame pushed on stack. Pushes nil if the GUID From 4b37a53ea91cafb44d2b7d93759e698d87d0f56e Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 13:45:35 -0500 Subject: [PATCH 11/15] support nameplateN unit tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hook FUN_TOKEN_TO_GUID so every Script_Unit* (UnitName, UnitGUID, UnitHealth, UnitClass, UnitExists, ...) accepts `nameplateN` tokens and their `target`-suffix chains (`nameplate1target`, `nameplate1targettarget`) — modern WoW semantics on the vanilla engine. Hook is gated by an SStrCmpI prefix check; non-nameplate tokens fall straight through to the original resolver. - Ordered GUID list is maintained alongside the existing NAME_PLATE_UNIT_ADDED / _REMOVED diff. Append on ADDED, erase on REMOVED — creation-order, stable for a plate's lifetime, modern semantics. - Suffix walker mirrors the engine's own LAB_005159d3 walker instruction-for-instruction: SStrCmpI "target" + ObjectByGUID + read UNIT_FIELD_TARGET (m_objectFields + 0x28). - SStrCmpI typedef is __stdcall, not __cdecl. The function ends with ret 0xc; declaring it cdecl makes MSVC emit a redundant add esp, 12 on top of the callee cleanup, drifting ESP +12 per call and crashing somewhere deep in Lua once L gets read from a poisoned stack slot. - m_objectFields is a *pointer* at obj+0x110, not an inline byte offset. Two dereferences (mov eax, [obj+0x110]; mov edi, [eax+0x28]), not one — earlier version mistook the inline-vs-pointer shape and read instruction bytes as GUIDs. --- src/Offsets.h | 22 +++++ src/nameplate/Events.cpp | 38 ++++++++- src/nameplate/TokenResolver.cpp | 146 ++++++++++++++++++++++++++++++++ src/nameplate/Walk.h | 11 +++ 4 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/nameplate/TokenResolver.cpp diff --git a/src/Offsets.h b/src/Offsets.h index e4042e2..28121a1 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -281,6 +281,28 @@ enum Offsets { // input it's the right primitive. FUN_TOKEN_TO_GUID = 0x00515970, + // `SStrCmpI(a, b, n)` — Storm's case-insensitive memcmp-style + // comparator. **`int __stdcall(const char *a, const char *b, int n)`** + // — the function ends with `ret 0xc`, so the callee pops the + // 3-arg stack frame. Declaring it as `__cdecl` and calling makes + // MSVC emit a redundant `add esp, 12` post-call, drifting ESP + // upward by 12 per call and corrupting the caller's stack frame + // — manifested as a deep-Lua crash whose `L` pointer landed in + // `.text`. Returns 0 when the first `n` characters match + // (ignoring case) or both strings end before `n`. + FUN_SSTR_CMP_I = 0x0064A4C0, + + // UNIT_FIELD_TARGET within `m_objectFields` — 64-bit GUID at byte + // offsets +0x28 (lo) / +0x2C (hi). Verified by disassembling + // `FUN_TOKEN_TO_GUID`'s suffix walker at `0x00515A1C-A2C`: + // `mov eax, [obj + 0x110]; mov edi, [eax + 0x28]; mov ebx, [eax + 0x2c]` + // — reads the target GUID to chain `targettarget`-style tokens. + // Distinct from the higher field offsets in the + // `OFF_UNIT_FIELD_*` block; UNIT_FIELD_TARGET is one of the few + // 1.12 offsets that matches the CMaNGOS-documented vanilla + // layout. + OFF_UNIT_FIELD_TARGET = 0x28, + // Party / raid roster counts and the party GUID array referenced // in the `FUN_TOKEN_TO_GUID` dispatch comment above. Used by // `UnitTokenFromGUID` to cap its candidate iteration — solo diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp index f98136e..3428625 100644 --- a/src/nameplate/Events.cpp +++ b/src/nameplate/Events.cpp @@ -41,9 +41,11 @@ #include "nameplate/Walk.h" #include "tick/WorldTick.h" +#include #include #include #include +#include namespace NamePlate::Events { @@ -80,6 +82,14 @@ std::unordered_map g_currentTickPlates; // then tops out as pool reuse covers all subsequent shows. std::unordered_set g_seenPlates; +// Ordered list of currently-visible nameplate GUIDs, in +// creation-order. Append on UNIT_ADDED, erase on UNIT_REMOVED. Backs +// the `nameplateN` unit-token resolver in `TokenResolver.cpp`. Order +// matches modern WoW semantics: stable for the lifetime of each +// plate, gaps when middle plates vanish (until the next REMOVED +// shifts later entries down). +std::vector g_orderedGUIDs; + void FireWithGUID(const char *eventName, uint64_t guid) { if (guid == 0) return; @@ -155,18 +165,28 @@ void OnWorldTick() { // Fire CREATED (with the Frame as arg1) for never-before-seen // frame pointers; ADDED (with GUID string) for GUIDs not in last - // tick's snapshot. + // tick's snapshot. New ADDED entries also get appended to the + // ordered GUID list that backs `nameplateN` token resolution. for (const auto &kv : g_currentTickPlates) { if (g_seenPlates.insert(kv.second).second) FireWithFrame(kEventCreated, const_cast(kv.second)); - if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) + if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) { FireWithGUID(kEventUnitAdded, kv.first); + g_orderedGUIDs.push_back(kv.first); + } } // Fire REMOVED for GUIDs in last tick's snapshot but not current. + // Same GUIDs are removed from the ordered list so later plates + // shift down — matches modern semantics. for (const auto &kv : g_lastTickPlates) { - if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) + if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) { FireWithGUID(kEventUnitRemoved, kv.first); + auto it = std::find(g_orderedGUIDs.begin(), g_orderedGUIDs.end(), + kv.first); + if (it != g_orderedGUIDs.end()) + g_orderedGUIDs.erase(it); + } } g_lastTickPlates.swap(g_currentTickPlates); @@ -176,4 +196,16 @@ void OnWorldTick() { static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick}; +// Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver +// in `TokenResolver.cpp` can map an index to a GUID without seeing +// the internal vector. +uint64_t GetGUIDByIndex(int oneBased) { + if (oneBased <= 0) + return 0; + const size_t idx = static_cast(oneBased - 1); + if (idx >= g_orderedGUIDs.size()) + return 0; + return g_orderedGUIDs[idx]; +} + } // namespace NamePlate::Events diff --git a/src/nameplate/TokenResolver.cpp b/src/nameplate/TokenResolver.cpp new file mode 100644 index 0000000..ac886df --- /dev/null +++ b/src/nameplate/TokenResolver.cpp @@ -0,0 +1,146 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU Lesser General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along with +// ClassicAPI. If not, see . + +// Extend `FUN_TOKEN_TO_GUID` (`0x00515970`) with modern WoW's +// `nameplateN` unit-token family. Hooking the central token→GUID +// resolver means every `Script_Unit*` (UnitName, UnitGUID, UnitHealth, +// UnitClass, UnitExists, …) gains nameplate-token support for free, +// since they all funnel here. +// +// Resolution: +// +// 1. SStrCmpI the input against `"nameplate"` (9 chars). Non-match → +// fall through to the original resolver and behave exactly as +// before (engine handles `player` / `target` / `partyN` / etc., +// or raises "Unknown unit name"). +// 2. Parse trailing digits — `nameplate1`, `nameplate12`, ... — into +// a 1-based index `N`. +// 3. Look up `N` in `NamePlate::Events`' ordered GUID list. Out of +// range returns 0 quietly, which the engine treats as "no unit" +// and Lua callers see as `nil` — matches modern behavior, no +// "Unknown unit name" error. +// 4. If the token continues past the digits (`nameplate1target`, +// `nameplate1targettarget`, …) mirror the engine's own suffix +// walker at `LAB_005159d3`: each `"target"` chunk advances the +// GUID by reading `m_objectFields + OFF_UNIT_FIELD_TARGET` +// (UNIT_FIELD_TARGET) via `ObjectByGUID`. +// +// The engine's walker is **inside** the function we hook, so a hook +// that returns early at the prefix-parse step would skip it +// entirely. Replicating the walker here keeps `nameplate1target` +// working without rewriting the resolver. The walker's logic is +// short (one `SStrCmpI` + one `ObjectByGUID` + one field-read per +// iteration) and matches the engine instruction-for-instruction — +// verified against the disassembly of `0x005159D3..0x00515A2C`. + +#include "Game.h" +#include "Offsets.h" +#include "nameplate/Walk.h" + +#include + +namespace NamePlate::TokenResolver { + +namespace { + +using TokenToGUID_t = uint64_t(__fastcall *)(const char *token); +// `__stdcall`, not `__cdecl` — `FUN_00064A4C0` ends with `ret 0xc`, +// callee cleans 3-arg stack frame. Calling it through a cdecl typedef +// makes MSVC emit `add esp, 12` after the call on top of the callee's +// own cleanup, drifting ESP +12 per call and corrupting the caller's +// frame. +using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n); +using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName, + uint32_t guidLo, uint32_t guidHi, + int priority); + +constexpr const char kPrefix[] = "nameplate"; +constexpr int kPrefixLen = static_cast(sizeof(kPrefix) - 1); +constexpr const char kSuffixTarget[] = "target"; +constexpr int kSuffixTargetLen = static_cast(sizeof(kSuffixTarget) - 1); + +// Engine's own priority value for the suffix walker's ObjectByGUID +// calls (verified at `0x005159FE: PUSH 0x6E`). Different from the +// `0x172` we use elsewhere — matching the engine here so the +// resolver's lookup path is identical to the native `targettarget` +// walker. +constexpr int kResolvePriority = 0x6e; + +TokenToGUID_t Original_o = nullptr; + +uint64_t WalkSuffix(uint64_t guid, const char *suffix) { + auto resolve = reinterpret_cast( + Offsets::FUN_OBJECT_RESOLVE_BY_GUID); + auto sstrcmpi = reinterpret_cast(Offsets::FUN_SSTR_CMP_I); + while (guid != 0 && *suffix != '\0') { + if (sstrcmpi(suffix, kSuffixTarget, kSuffixTargetLen) != 0) + return 0; // unknown suffix component — modern returns nil + suffix += kSuffixTargetLen; + auto *obj = static_cast( + resolve(Offsets::OBJ_TYPE_UNIT, "nameplate", + static_cast(guid), + static_cast(guid >> 32), + kResolvePriority)); + if (obj == nullptr) + return 0; + // `m_objectFields` is a *pointer* stored at `obj + 0x110`, + // not an inline array. Engine's walker does + // `mov eax, [obj+0x110]; mov edi, [eax+0x28]` — two + // dereferences. Earlier version of this code missed the + // first one and read instruction bytes interpreted as GUIDs. + auto *fields = *reinterpret_cast( + obj + Offsets::OFF_CGUNIT_OBJECT_FIELDS); + if (fields == nullptr) + return 0; + guid = *reinterpret_cast( + fields + Offsets::OFF_UNIT_FIELD_TARGET); + } + return guid; +} + +uint64_t __fastcall Hook_h(const char *token) { + if (token == nullptr || *token == '\0') + return Original_o(token); + + auto sstrcmpi = reinterpret_cast(Offsets::FUN_SSTR_CMP_I); + if (sstrcmpi(token, kPrefix, kPrefixLen) != 0) + return Original_o(token); + + // Must have at least one digit after `"nameplate"`; otherwise + // it's `"nameplate"` (no number) or `"nameplateX"` — fall + // through to the original so the engine raises its + // "Unknown unit name" error consistently. + const char *p = token + kPrefixLen; + if (*p < '0' || *p > '9') + return Original_o(token); + + int index = 0; + while (*p >= '0' && *p <= '9') { + index = index * 10 + (*p - '0'); + ++p; + } + + const uint64_t guid = NamePlate::Events::GetGUIDByIndex(index); + if (guid == 0) + return 0; // OOB / no plate at index — modern returns nil + return WalkSuffix(guid, p); +} + +} // namespace + +static const Game::HookAutoRegister _hook{ + Offsets::FUN_TOKEN_TO_GUID, + reinterpret_cast(&Hook_h), + reinterpret_cast(&Original_o)}; + +} // namespace NamePlate::TokenResolver diff --git a/src/nameplate/Walk.h b/src/nameplate/Walk.h index 704217a..06dc082 100644 --- a/src/nameplate/Walk.h +++ b/src/nameplate/Walk.h @@ -33,6 +33,17 @@ void PushNamePlateFrame(void *L, void *nameplate); } // namespace NamePlate::Info +namespace NamePlate::Events { + +// Returns the GUID currently bound to `nameplateN` (1-based, matching +// the modern Lua token form). `0` means no nameplate at that index. +// Backed by the per-tick UNIT_ADDED / UNIT_REMOVED diff in +// `Events.cpp` — order is creation-order (append on ADDED, erase on +// REMOVED), stable within a frame. +uint64_t GetGUIDByIndex(int oneBased); + +} // namespace NamePlate::Events + namespace NamePlate::Walk { // `0x00B41414` is a player-related global that overlays multiple From 7d7ecafc6a137028c7075cc23d22c20ba3f01505 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 13:48:43 -0500 Subject: [PATCH 12/15] document nameplateN unit-token support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API.md NamePlate section: replace the "scope vs. modern (deferred)" callout with a pointer to the new Unit tokens subsection; update the events table and "modern divergence" callout to explain why the GUID payload remains (engine dispatcher can't carry Lua values through %s/%d/%u/%f format codes) even though the token form now works. - New "Unit tokens (`nameplateN`)" subsection covering the resolved ~30 UnitX functions, creation-order semantics, suffix chains (`nameplate1target`, `targettarget`), and the SStrCmpI prefix-gate implementation detail. - UnitTokenFromGUID's "post-1.12 omitted" list: nameplateN is no longer engine-unsupported (we hook the resolver) but it's still not used as a reverse-lookup key — indices can be reassigned during a session, so it'd be unstable. - README: new Unit tokens table row pointing to the API.md anchor. --- README.md | 6 +++++ docs/API.md | 73 +++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7de4d4f..492fdbd 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,12 @@ functions, just behavior the stock 1.12 engine didn't have. See the | Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` | | Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` | +### Unit tokens + +| Token | Resolves to | +|-------|-------------| +| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). | + ## Installation Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the diff --git a/docs/API.md b/docs/API.md index 2de6dee..ced665c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -201,6 +201,7 @@ build instructions. - [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids) - [`C_NamePlate.GetNamePlateForUnit(unitToken)`](#c_nameplategetnameplateforunitunittoken) - [`C_NamePlate.GetNamePlateForGUID(guidString)`](#c_nameplategetnameplateforguidguidstring) + - [Unit tokens (`nameplateN`)](#unit-tokens-nameplaten) - [NameCache](#namecache) - [`GetPlayerInfoByGUID(guid)`](#getplayerinfobyguidguid) @@ -1581,7 +1582,7 @@ Fire when nameplate state actually changes. Payloads: | Event | `arg1` | Notes | |-------|--------|-------| | `NAME_PLATE_CREATED` | nameplate **Frame** | Matches modern WoW. Fires once per unique `CGNamePlateFrame` pointer — same frame re-used via pool recycle does NOT refire. | -| `NAME_PLATE_UNIT_ADDED` | unit **GUID string** | Modern uses a `"nameplateN"` token; vanilla's token resolver isn't extensible. Pass `arg1` to [`GetNamePlateForGUID`](#c_nameplategetnameplateforguidguidstring) for the frame. | +| `NAME_PLATE_UNIT_ADDED` | unit **GUID string** | Modern passes a `"nameplateN"` unit token; we ship the GUID string instead because event payloads can't carry Lua values through the engine's printf-style dispatcher. The `nameplateN` token itself **does** work for `UnitName` / `UnitGUID` / etc. — see [Unit tokens](#unit-tokens-nameplaten). | | `NAME_PLATE_UNIT_REMOVED` | unit **GUID string** | Same as above. | ```lua @@ -1610,10 +1611,14 @@ end) > `NAME_PLATE_UNIT_ADDED` (fires next tick at the latest) or fetch > the current frame on-demand via `GetNamePlateForGUID`. -> **Modern divergence.** Retail passes `"nameplateN"` unit tokens to -> `ADDED`/`REMOVED`. Vanilla's token resolver isn't extensible, so -> we ship the GUID string instead and let addons round-trip through -> `GetNamePlateForGUID` to reach the frame. +> **Modern divergence.** Retail passes `"nameplateN"` unit tokens as +> the event payload. The vanilla engine's event dispatcher only +> accepts `%s`/`%d`/`%u`/`%f` format codes — no path for "push the +> token string and let `UnitX` resolve it lazily" — so we ship the +> GUID string and addons can either round-trip via +> `GetNamePlateForGUID` for the frame, or pass the matching +> `"nameplate1"`-style token to any `UnitX` function. See +> [Unit tokens](#unit-tokens-nameplaten). **Implementation notes** @@ -4397,11 +4402,10 @@ underlying data (per-unit nameplate pointer at `CGUnit + 0xE60`) exists. We enumerate visible units via the local-player-anchored object hash table, filter by `TYPEMASK_UNIT`, and return matches. -> **Scope vs. modern.** Modern API also provides `"nameplateN"` unit -> tokens and `NAME_PLATE_UNIT_ADDED` / `REMOVED` events. Both -> deferred — would require hooking the unit-token resolver and a -> per-frame polling loop. The functions documented below give -> addons enough to walk plates per call. +Modern's `"nameplateN"` unit-token family is also supported — see +[Unit tokens](#unit-tokens-nameplaten) below. `NAME_PLATE_UNIT_ADDED` +/ `_REMOVED` / `_CREATED` events fire via a per-tick visible-plate +diff (see [the events section](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)). ### `C_NamePlate.GetNamePlates()` @@ -4518,6 +4522,45 @@ nameplate pointer is set by `FUN_006086E0`'s "show nameplate" path regardless of which nameplate system rendered it. Order follows hash-bucket iteration and isn't stable across calls. +### Unit tokens (`nameplateN`) + +`"nameplate1"`, `"nameplate2"`, … work as unit tokens against every +`UnitX` function: `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, +`UnitHealthMax`, `UnitLevel`, `UnitFaction`, `UnitReaction`, +`UnitExists`, `UnitIsPlayer`, `UnitIsEnemy`, `UnitIsDead`, etc. — +~30 functions for free. + +```lua +for i = 1, 40 do + if not UnitExists("nameplate" .. i) then break end + print(i, UnitName("nameplate" .. i), UnitClass("nameplate" .. i)) +end +``` + +Ordering is **creation-order** — each new plate appends to the end of +the list and stays at its index until the unit goes out of range or +the plate is removed, at which point later indices shift down. +Stable for the lifetime of a single plate; no mid-frame reordering. +Same semantics as modern WoW. + +Token chains work too — `"nameplate1target"`, `"nameplate1targettarget"`, +etc. — by mirroring the engine's own `targettarget`-style suffix +walker (read `UNIT_FIELD_TARGET` off `m_objectFields`, loop). Other +suffixes (`pet`, `master`) aren't supported by the vanilla engine's +own walker either, so they don't compose. + +Out-of-range indices return `nil` cleanly without raising "Unknown +unit name" — `UnitExists("nameplate99")` just returns `false`. + +**Implementation note.** We hook `FUN_TOKEN_TO_GUID` (the central +token→GUID resolver) so the entire `Script_Unit*` surface gains the +new token form transparently. The hook is gated by an `SStrCmpI` +prefix check against `"nameplate"`; non-nameplate tokens +(`"player"`, `"target"`, `"partyN"`, etc.) fall straight through to +the unmodified resolver. The ordered list is maintained alongside +the existing `NAME_PLATE_UNIT_ADDED` / `_REMOVED` diff in the +per-tick nameplate walker. + ## NameCache GUID-keyed cache of player names and classes. The engine itself @@ -6712,9 +6755,13 @@ known unit tokens and return the first one currently mapped to that GUID, or `nil` if none of them point at it. The search order matches modern retail with post-1.12 tokens -omitted (`vehicle`, `nameplateN`, `arenaN`, `arenapetN`, `bossN`, -`focus`, `softenemy`, `softfriend`, `softinteract` all post-date -vanilla and the engine's resolver doesn't recognize them): +omitted (`vehicle`, `arenaN`, `arenapetN`, `bossN`, `focus`, +`softenemy`, `softfriend`, `softinteract` all post-date vanilla and +the engine's resolver doesn't recognize them). `nameplateN` is +recognized by the resolver (we hook it — see +[Unit tokens](#unit-tokens-nameplaten)) but **not** searched here — +two GUIDs can share a nameplate index over the lifetime of a +session, so it's not a stable reverse-lookup key: ``` player → pet → party1..4 → partypet1..4 → raid1..40 From b75437f5fb45dc18570249ecda1fc3c2b46e2746 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 13:58:22 -0500 Subject: [PATCH 13/15] return stable nameplate wrapper across calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PushNamePlateFrame was building a fresh Lua table on every call for unregistered (default vanilla) nameplates — so fields addons set on the wrapper they received in NAME_PLATE_CREATED were lost on the next GetNamePlateForUnit lookup. pfUI specifically writes `plate.nameplate = styledButton` on the CREATED-delivered table to hand its overlay back through our API, and that field was disappearing. Fix: pin the first wrapper for each nameplate pointer in the Lua registry via luaL_ref, cache the refkey in g_wrapperRefkeys, and rawgeti on every subsequent push. Same table identity for the life of the engine plate. Bound by the CGNamePlateFrame pool size (~80). Cleared on /reload via a new NamePlate::Info::PrepareForReload() wired into FrameScript_Initialize_h next to the existing Event::Custom and NameCache reload hooks — stale refkeys would otherwise point into the freed registry. --- src/DllMain.cpp | 5 +++ src/nameplate/Info.cpp | 83 ++++++++++++++++++++++++++++++++++-------- src/nameplate/Walk.h | 16 ++++++-- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/DllMain.cpp b/src/DllMain.cpp index 2546312..0d5ac41 100644 --- a/src/DllMain.cpp +++ b/src/DllMain.cpp @@ -16,6 +16,7 @@ #include "MinHook.h" #include "Offsets.h" #include "event/Custom.h" +#include "nameplate/Walk.h" #include "player/NameCache.h" static Game::FrameScript_Initialize_t FrameScript_Initialize_o = nullptr; @@ -37,6 +38,10 @@ static bool __fastcall FrameScript_Initialize_h() { // table is rebuilt at a fresh allocation; the old slots are stale. Event::Custom::PrepareForReload(); + // Drop the nameplate wrapper-cache refkeys for the same reason: + // the Lua registry that holds them is about to be freed. + NamePlate::Info::PrepareForReload(); + // Persist the name cache before the engine starts tearing down. // This hook fires on both `/reload` and `/logout` (the engine // re-initializes Lua state in both cases), giving us a clean diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 127bfcd..6f10c6d 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -58,6 +58,7 @@ #include "nameplate/Walk.h" #include +#include namespace NamePlate::Info { @@ -69,6 +70,7 @@ using NamePlate::Walk::kOffUnitNamePlate; using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); using LuaPushLightUserdata_t = void(__fastcall *)(void *L, void *p); using LuaSetMetatable_t = int(__fastcall *)(void *L, int idx); +using LuaRefRef_t = int(__fastcall *)(void *L, int t); using TokenToGUID_t = uint64_t(__fastcall *)(const char *token); using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName, uint32_t guidLo, uint32_t guidHi, @@ -79,6 +81,18 @@ constexpr uintptr_t kFunLuaSetMetatable = 0x006F4020; constexpr int kLuaGlobalsIndex = -10001; constexpr const char *kFrameMetatableGlobal = "__framescript_meta"; +// Wrapper-table identity cache for default vanilla nameplates. +// Without this, every `PushNamePlateFrame` call for an unregistered +// plate produces a *different* Lua table from `PushFreshFrameWrapper` +// — so addon-set fields on the wrapper (pfUI sets `plate.nameplate = +// styledButton` on the table it gets in `NAME_PLATE_CREATED`) don't +// survive a later `GetNamePlateForUnit('nameplateN')` call. We pin +// the first wrapper in the Lua registry via `luaL_ref` and reuse +// that refkey for every subsequent push for the same nameplate +// pointer. Bound by the engine's CGNamePlateFrame freelist +// high-water mark (<80 typical, see `Events.cpp` g_seenPlates). +std::unordered_map g_wrapperRefkeys; + // Build a fresh frame wrapper on the Lua stack: `{[0] = frame}` with // `_G["__framescript_meta"]` as metatable. Same shape the engine's // frame-registration helper builds, minus the registry-cache step @@ -140,27 +154,66 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) { // frame-push path. Internal callers (Script_GetNamePlates etc.) call // it through the unqualified name (they live in the same namespace). // -// Defensive against stale refkeys: on `/reload` the C++ frame -// persists but the Lua registry is rebuilt at a fresh state. The -// engine's per-frame teardown should release each refkey via -// `luaL_unref`, but we'd rather not rely on it for every frame in -// every reload path — if the rawgeti'd value isn't actually a -// frame-wrapper table (nil, freed registry slot, anything), pop it -// and synthesise a fresh wrapper. Same fallback the -// "unregistered frame" path uses, so the caller can't observe a -// non-table being pushed. +// Path 1 — engine-registered (`refKey > 0`): use the engine's +// per-frame registry slot. Real refkeys come from addons that +// built their own plate via `CreateFrame`; default vanilla plates +// never hit this path (their `+0x08` is `LUA_NOREF`). +// +// Path 2 — our cached wrapper: for unregistered plates we keep one +// wrapper table per nameplate pointer pinned in the Lua registry, +// so every call returns the *same* table. Without this, addons +// like pfUI that stash `plate.nameplate = decorated` on the +// wrapper received via `NAME_PLATE_CREATED` see their field +// missing on the next `GetNamePlateForUnit` call (different +// wrapper table, fresh metatable, no carried fields). +// +// Path 3 — build a wrapper, pin it via `luaL_ref`, leave it on the +// stack. First time we see this nameplate. +// +// Both lookup paths validate the rawgeti'd value is actually a +// table before returning — guards against stale registry slots +// across `/reload`, where the Lua state rebuilds but our caches +// persist. `PrepareForReload()` (below) clears the cache up front, +// but if it's somehow missed, the type check still keeps callers +// from observing a non-table. void PushNamePlateFrame(void *L, void *nameplate) { - const int refKey = *reinterpret_cast( + auto rawgeti = reinterpret_cast( + Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); + + const int engineRefKey = *reinterpret_cast( static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); - if (refKey > 0) { - auto rawgeti = reinterpret_cast( - Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); - rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + if (engineRefKey > 0) { + rawgeti(L, Game::Lua::REGISTRY_INDEX, engineRefKey); + if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) + return; + Game::Lua::SetTop(L, -2); // stale; fall through + } + + auto cached = g_wrapperRefkeys.find(nameplate); + if (cached != g_wrapperRefkeys.end()) { + rawgeti(L, Game::Lua::REGISTRY_INDEX, cached->second); if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) return; - Game::Lua::SetTop(L, -2); // pop the stale value + Game::Lua::SetTop(L, -2); + g_wrapperRefkeys.erase(cached); // registry slot is dead, rebuild } + PushFreshFrameWrapper(L, nameplate); + // Duplicate so `luaL_ref` can consume one copy while the other + // stays on the stack as our return value. + Game::Lua::PushValue(L, -1); + auto refRef = reinterpret_cast( + static_cast(Offsets::LUA_REF_REF)); + const int newRef = refRef(L, Game::Lua::REGISTRY_INDEX); + g_wrapperRefkeys[nameplate] = newRef; +} + +// Called from `FrameScript_Initialize_h` ahead of the engine's Lua +// teardown so we don't leak refkeys into a destroyed registry. The +// nameplate frames themselves persist across `/reload`, but the +// registry slots they pointed to are about to be freed. +void PrepareForReload() { + g_wrapperRefkeys.clear(); } // GUID → nameplate Frame pushed on stack. Pushes nil if the GUID diff --git a/src/nameplate/Walk.h b/src/nameplate/Walk.h index 06dc082..ec9bff7 100644 --- a/src/nameplate/Walk.h +++ b/src/nameplate/Walk.h @@ -25,12 +25,20 @@ namespace NamePlate::Info { -// Pushes the nameplate Frame at `nameplate` onto the Lua stack — -// either the cached registry-wrapper (addon-registered plates) or a -// fresh per-call wrapper (default vanilla plates). Defined in -// `Info.cpp`. +// Pushes the nameplate Frame at `nameplate` onto the Lua stack. +// Returns a stable wrapper table — the same table for the same +// nameplate pointer across every call — so addon-set fields on the +// wrapper (pfUI: `plate.nameplate`) survive between +// `NAME_PLATE_CREATED` and a later `GetNamePlateFor*` lookup. +// Defined in `Info.cpp`. void PushNamePlateFrame(void *L, void *nameplate); +// Clears the wrapper-cache map. Called from `FrameScript_Initialize` +// before the engine tears down the Lua registry on `/reload`; the +// next push for each plate then builds a fresh wrapper pinned in +// the freshly-rebuilt registry. +void PrepareForReload(); + } // namespace NamePlate::Info namespace NamePlate::Events { From 74f640af7ae47f5addc3706b6d3de9ecc8f65528 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 14:22:55 -0500 Subject: [PATCH 14/15] NAME_PLATE_UNIT events now fire token --- README.md | 4 +-- docs/API.md | 42 +++++++++++----------- src/nameplate/Events.cpp | 78 ++++++++++++++++++++++++++-------------- 3 files changed, 74 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 492fdbd..9ff713e 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,8 @@ functions, just behavior the stock 1.12 engine didn't have. See the | `ITEM_DATA_LOAD_RESULT` | `itemID, success` | | `MODIFIER_STATE_CHANGED` | `keyName, down` | | `NAME_PLATE_CREATED` | `nameplateFrame` | -| `NAME_PLATE_UNIT_ADDED` | `unitGUID` | -| `NAME_PLATE_UNIT_REMOVED` | `unitGUID` | +| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") | +| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") | | `PLAYER_STARTED_LOOKING` | *(none)* | | `PLAYER_STOPPED_LOOKING` | *(none)* | | `PLAYER_STARTED_MOVING` | *(none)* | diff --git a/docs/API.md b/docs/API.md index ced665c..69edc24 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1582,8 +1582,8 @@ Fire when nameplate state actually changes. Payloads: | Event | `arg1` | Notes | |-------|--------|-------| | `NAME_PLATE_CREATED` | nameplate **Frame** | Matches modern WoW. Fires once per unique `CGNamePlateFrame` pointer — same frame re-used via pool recycle does NOT refire. | -| `NAME_PLATE_UNIT_ADDED` | unit **GUID string** | Modern passes a `"nameplateN"` unit token; we ship the GUID string instead because event payloads can't carry Lua values through the engine's printf-style dispatcher. The `nameplateN` token itself **does** work for `UnitName` / `UnitGUID` / etc. — see [Unit tokens](#unit-tokens-nameplaten). | -| `NAME_PLATE_UNIT_REMOVED` | unit **GUID string** | Same as above. | +| `NAME_PLATE_UNIT_ADDED` | `"nameplateN"` **unit token** | Matches modern WoW. Pass straight to `UnitName` / `UnitGUID` / `UnitClass` / etc., or to [`GetNamePlateForUnit`](#c_nameplategetnameplateforunitunittoken) for the frame. The token is positional — see [Unit tokens](#unit-tokens-nameplaten) for ordering semantics. | +| `NAME_PLATE_UNIT_REMOVED` | `"nameplateN"` **unit token** | Same as above. Computed from the plate's slot *before* it shifts out of the ordered list, so the token still resolves to the leaving unit during the event handler. | ```lua local f = CreateFrame("Frame") @@ -1595,8 +1595,9 @@ f:SetScript("OnEvent", function() -- arg1 = the nameplate Frame itself arg1:SetAlpha(0.8) elseif event == "NAME_PLATE_UNIT_ADDED" then - -- arg1 = unit GUID string ("0xF13000C36C26FD02", etc.) - local plate = C_NamePlate.GetNamePlateForGUID(arg1) + -- arg1 = "nameplate1" / "nameplate2" / ... + local name = UnitName(arg1) + local plate = C_NamePlate.GetNamePlateForUnit(arg1) -- ... style based on the unit ... end end) @@ -1609,16 +1610,14 @@ end) > is a bare frame with no addon-side decorations yet. For > unit-specific work after the addon has decorated, use > `NAME_PLATE_UNIT_ADDED` (fires next tick at the latest) or fetch -> the current frame on-demand via `GetNamePlateForGUID`. +> the current frame on-demand via `GetNamePlateForUnit(arg1)`. -> **Modern divergence.** Retail passes `"nameplateN"` unit tokens as -> the event payload. The vanilla engine's event dispatcher only -> accepts `%s`/`%d`/`%u`/`%f` format codes — no path for "push the -> token string and let `UnitX` resolve it lazily" — so we ship the -> GUID string and addons can either round-trip via -> `GetNamePlateForGUID` for the frame, or pass the matching -> `"nameplate1"`-style token to any `UnitX` function. See -> [Unit tokens](#unit-tokens-nameplaten). +> **Token stability gotcha.** Like modern WoW, the `arg1` token is +> positional — `"nameplate3"` today may resolve to a different unit +> after the slot vacates and shifts. If you need a per-unit hash key +> for cross-event bookkeeping, call `UnitGUID(arg1)` and store the +> GUID instead. See [Unit tokens](#unit-tokens-nameplaten) for the +> ordering rules. **Implementation notes** @@ -4483,19 +4482,20 @@ the unit going out of range. ### `C_NamePlate.GetNamePlateForGUID(guidString)` Same as `GetNamePlateForUnit` but takes the `"0xHHHHHHHHHHHHHHHH"` -GUID-string form. Designed to pair with the -[NAME_PLATE_UNIT_ADDED / REMOVED](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events) -events, whose payload is the unit GUID rather than a token. +GUID-string form. Useful when you've stored a unit GUID across +events (e.g., converted the positional `"nameplateN"` token to a +GUID via `UnitGUID(arg1)` at `NAME_PLATE_UNIT_ADDED` time) and need +the frame later. ```lua +local platesByGuid = {} + local f = CreateFrame("Frame") f:RegisterEvent("NAME_PLATE_UNIT_ADDED") f:SetScript("OnEvent", function() - -- arg1 = GUID string - local plate = C_NamePlate.GetNamePlateForGUID(arg1) - if plate then - -- ... skin / style plate ... - end + -- arg1 = "nameplateN" token; convert to stable GUID for storage + local guid = UnitGUID(arg1) + platesByGuid[guid] = C_NamePlate.GetNamePlateForGUID(guid) end) ``` diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp index 3428625..531528c 100644 --- a/src/nameplate/Events.cpp +++ b/src/nameplate/Events.cpp @@ -21,28 +21,30 @@ // is absorbed because we only compare with the previous *frame's* // state, not every transient `unit + 0xE60` write. // -// Payload shape: -// - `NAME_PLATE_CREATED` — `arg1` is the nameplate **Frame** (matches -// modern WoW). The engine's printf-style dispatcher -// (`FUN_FIRE_EVENT`) only knows `%s`/`%d`/`%u`/`%f` format codes -// with no "push Lua value" option, so we route this event through -// a pre-set path: save current `_G.arg1`, set it to the frame, fire -// with empty format (dispatcher leaves `_G.arg` alone when no -// codes are parsed), restore. -// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the unit GUID -// string. Modern uses a `"nameplateN"` token; vanilla's token -// resolver isn't extensible. Addons call -// `C_NamePlate.GetNamePlateForGUID(arg1)` to get the frame. +// Payload shape (matches modern WoW exactly): +// - `NAME_PLATE_CREATED` — `arg1` is the nameplate **Frame**. The +// engine's printf-style dispatcher (`FUN_FIRE_EVENT`) only knows +// `%s`/`%d`/`%u`/`%f` format codes with no "push Lua value" option, +// so we route this event through a pre-set path: save current +// `_G.arg1`, set it to the frame, fire with empty format +// (dispatcher leaves `_G.arg` alone when no codes are parsed), +// restore. +// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the +// `"nameplateN"` unit token (formatted from the plate's index in +// `g_orderedGUIDs` at fire time). The token resolves to the unit +// via the `nameplateN`-aware token resolver in `TokenResolver.cpp` +// — addons can pass it straight to `UnitName`, `UnitGUID`, etc., +// or to `GetNamePlateForUnit` for the frame. #include "Game.h" #include "Offsets.h" #include "event/Custom.h" -#include "guid/Guid.h" #include "nameplate/Walk.h" #include "tick/WorldTick.h" #include #include +#include #include #include #include @@ -90,15 +92,26 @@ std::unordered_set g_seenPlates; // shifts later entries down). std::vector g_orderedGUIDs; -void FireWithGUID(const char *eventName, uint64_t guid) { - if (guid == 0) +// Fire `eventName` 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) { + if (value == nullptr) return; const int slot = Event::Custom::Lookup(eventName); if (slot < 0) return; - char buf[Guid::STRING_SIZE]; - Guid::FormatAsString(guid, buf, sizeof buf); - Event::Custom::Fire(slot, "%s", buf); + Event::Custom::Fire(slot, "%s", value); +} + +// Format a 1-based nameplate index as `"nameplateN"` for ADDED / +// REMOVED event payloads. Buffer should be at least 24 bytes — 9 +// for the prefix, up to 10 digits for the index, room for null. +// Returns `buf` for convenient inline use. +const char *FormatNamePlateToken(char *buf, size_t bufSize, int oneBasedIndex) { + std::snprintf(buf, bufSize, "nameplate%d", oneBasedIndex); + return buf; } // Fire `eventName` with the nameplate `Frame` set as `_G.arg1`. @@ -164,28 +177,39 @@ void OnWorldTick() { }); // Fire CREATED (with the Frame as arg1) for never-before-seen - // frame pointers; ADDED (with GUID string) for GUIDs not in last - // tick's snapshot. New ADDED entries also get appended to the - // ordered GUID list that backs `nameplateN` token resolution. + // frame pointers; ADDED (with `"nameplateN"` token as arg1) for + // GUIDs not in last tick's snapshot. New entries are appended to + // the ordered list *before* firing so the token resolves to the + // newly-added plate during the event handler. for (const auto &kv : g_currentTickPlates) { if (g_seenPlates.insert(kv.second).second) FireWithFrame(kEventCreated, const_cast(kv.second)); if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) { - FireWithGUID(kEventUnitAdded, kv.first); g_orderedGUIDs.push_back(kv.first); + char tokenBuf[24]; + FireWithString(kEventUnitAdded, + FormatNamePlateToken(tokenBuf, sizeof tokenBuf, + static_cast(g_orderedGUIDs.size()))); } } // Fire REMOVED for GUIDs in last tick's snapshot but not current. - // Same GUIDs are removed from the ordered list so later plates - // shift down — matches modern semantics. + // We compute the token from the position *before* erasing so the + // event payload reflects the slot the unit just vacated; the + // handler can still resolve the token to the unit via + // `g_orderedGUIDs[slot]` during dispatch. Later plates shift down + // when we erase — matches modern semantics. for (const auto &kv : g_lastTickPlates) { if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) { - FireWithGUID(kEventUnitRemoved, kv.first); auto it = std::find(g_orderedGUIDs.begin(), g_orderedGUIDs.end(), kv.first); - if (it != g_orderedGUIDs.end()) - g_orderedGUIDs.erase(it); + if (it == g_orderedGUIDs.end()) + continue; + const int oneBased = static_cast(it - g_orderedGUIDs.begin()) + 1; + char tokenBuf[24]; + FireWithString(kEventUnitRemoved, + FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased)); + g_orderedGUIDs.erase(it); } } From 0490ea8a63288071cabe5a2c8d2eaf9886563855 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Tue, 26 May 2026 14:53:16 -0500 Subject: [PATCH 15/15] nameplate events: ship "nameplateN" tokens, delegate wrappers to engine Two intertwined changes that together make nameplate-mod addons (pfUI) survive pool recycle. 1. NAME_PLATE_UNIT_ADDED / _REMOVED arg1 is now the "nameplateN" unit token, matching modern WoW exactly. For REMOVED we compute the token before erasing from g_orderedGUIDs so the handler can still resolve the leaving unit. Addons that want a stable hash key call UnitGUID(arg1) to convert. 2. PushNamePlateFrame now delegates to the engine's own FrameScript_Object::ScriptRegister (0x00701BD0) instead of maintaining our own luaL_ref'd wrapper cache. Per-pointer the engine builds a wrapper table once, refs it into the registry, and stores the refkey at nameplate+0x08 - every code path (ours, addons' CreateFrame(parent) extraction, the engine's own internal pushers) then funnels through rawgeti(REGISTRY, nameplate+0x08) and gets the same Lua table. The prior approach kept our own wrapper alongside the engine's once pfUI's CreateFrame inside NAME_PLATE_CREATED caused the engine to register the underlying frame itself - fields set on our wrapper (plate.nameplate = decoratedButton) disappeared from the API surface even though they still existed on the orphaned table. Single-source-of-truth fixes it. The refcount-pinning caveat CLAUDE.md warned about is benign for pool-managed nameplates (never destroyed during a session). Also clear seenPlates / lastTickPlates / orderedGUIDs on FrameScript_Initialize so currently-visible plates refire CREATED + UNIT_ADDED after /reload - re-presents them to the freshly reloaded UI, matching modern WoW. --- src/DllMain.cpp | 11 ++- src/Offsets.h | 24 +++++++ src/nameplate/Events.cpp | 18 +++++ src/nameplate/Info.cpp | 145 +++++++++++++++------------------------ src/nameplate/Walk.h | 22 +++--- 5 files changed, 116 insertions(+), 104 deletions(-) diff --git a/src/DllMain.cpp b/src/DllMain.cpp index 0d5ac41..afeb8f8 100644 --- a/src/DllMain.cpp +++ b/src/DllMain.cpp @@ -38,9 +38,14 @@ static bool __fastcall FrameScript_Initialize_h() { // table is rebuilt at a fresh allocation; the old slots are stale. Event::Custom::PrepareForReload(); - // Drop the nameplate wrapper-cache refkeys for the same reason: - // the Lua registry that holds them is about to be freed. - NamePlate::Info::PrepareForReload(); + // Clear the nameplate diff state so currently-visible plates + // refire CREATED and UNIT_ADDED on the next tick — re-presents + // them to the freshly reloaded UI, matching modern WoW. The + // wrappers themselves now live in the engine's own registry + // (via `FrameScript_Object::ScriptRegister`), which the engine + // tears down and rebuilds across the Lua reset — no per-module + // cache to clear here. + NamePlate::Events::PrepareForReload(); // Persist the name cache before the engine starts tearing down. // This hook fires on both `/reload` and `/logout` (the engine diff --git a/src/Offsets.h b/src/Offsets.h index 28121a1..70db8bd 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -200,6 +200,30 @@ enum Offsets { // Registers a single global Lua function. __fastcall(name, func). FUN_FRAMESCRIPT_REGISTER_FUNCTION = 0x00704120, + // `FrameScript_Object::ScriptRegister(this, name)` — `__thiscall`, + // `this` = a `CFrameScriptObject *`. On first call (when `this+0x04` + // is zero) builds a Lua wrapper table `{[0] = lightuserdata(this)}` + // with `_G["__framescript_meta"]` as metatable, `luaL_ref`s it into + // the registry, stores the refkey at `this+0x08`. Always increments + // `this+0x04` (the Lua-side refcount). Optional `name` argument + // installs `_G[name] = wrapper` for engine-named frames. + // + // We call this in `PushNamePlateFrame` so the engine and our own + // C_NamePlate getters operate on the **same** wrapper table — + // every push through `lua_rawgeti(REGISTRY, this+0x08)` (the + // canonical engine path) lands on the same Lua object pfUI + // received in `NAME_PLATE_CREATED`, so addon-set fields + // (`plate.nameplate = decoratedButton`) survive engine-side + // re-fetches. Earlier note in `Info.cpp` warned about pinning the + // refcount; for pool-managed nameplates the engine never + // un-registers them anyway, so the pin is benign. + FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER = 0x00701BD0, + + // `this+0x04` Lua refcount, incremented by `ScriptRegister`. We + // read it as a "has the engine ever exposed this CObject to Lua" + // probe — equivalent to checking `this+0x08 > 0` but more direct. + OFF_COBJECT_LUA_REFCOUNT = 0x04, + // Direct cvar lookup — `__fastcall(const char *name) → CVar* | NULL`. // Hash-table by-name lookup over the CVar registry; same call // `Script_GetCVar` makes internally before the engine wraps the diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp index 531528c..9ab123f 100644 --- a/src/nameplate/Events.cpp +++ b/src/nameplate/Events.cpp @@ -220,6 +220,24 @@ void OnWorldTick() { static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick}; +// Called from `FrameScript_Initialize_h` ahead of the engine's Lua +// teardown. Clears the diff state alongside `NamePlate::Info`'s +// wrapper-cache reset so that on the first post-reload tick every +// currently-visible plate refires `NAME_PLATE_CREATED` and +// `NAME_PLATE_UNIT_ADDED`. Without this, addons that decorate via +// CREATED (pfUI: builds its overlay button per-pointer) never see +// the existing plates after a `/reload` — `g_seenPlates` would +// suppress every refire, and the freshly-built wrapper would lack +// the addon's `.nameplate` field. +// +// `g_orderedGUIDs` is also cleared so the post-reload token indices +// start at `nameplate1` again, matching the order plates re-fire in. +void PrepareForReload() { + g_seenPlates.clear(); + g_lastTickPlates.clear(); + g_orderedGUIDs.clear(); +} + // Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver // in `TokenResolver.cpp` can map an index to a GUID without seeing // the internal vector. diff --git a/src/nameplate/Info.cpp b/src/nameplate/Info.cpp index 6f10c6d..20a3e55 100644 --- a/src/nameplate/Info.cpp +++ b/src/nameplate/Info.cpp @@ -58,7 +58,6 @@ #include "nameplate/Walk.h" #include -#include namespace NamePlate::Info { @@ -68,51 +67,13 @@ using NamePlate::Walk::ForEachNamePlatedUnit; using NamePlate::Walk::kOffUnitNamePlate; using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); -using LuaPushLightUserdata_t = void(__fastcall *)(void *L, void *p); -using LuaSetMetatable_t = int(__fastcall *)(void *L, int idx); -using LuaRefRef_t = int(__fastcall *)(void *L, int t); +using ScriptRegister_t = void(__fastcall *)(void *this_, void *edx_unused, + const char *name); using TokenToGUID_t = uint64_t(__fastcall *)(const char *token); using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName, uint32_t guidLo, uint32_t guidHi, int priority); -constexpr uintptr_t kFunLuaPushLightUserdata = 0x006F3A20; -constexpr uintptr_t kFunLuaSetMetatable = 0x006F4020; -constexpr int kLuaGlobalsIndex = -10001; -constexpr const char *kFrameMetatableGlobal = "__framescript_meta"; - -// Wrapper-table identity cache for default vanilla nameplates. -// Without this, every `PushNamePlateFrame` call for an unregistered -// plate produces a *different* Lua table from `PushFreshFrameWrapper` -// — so addon-set fields on the wrapper (pfUI sets `plate.nameplate = -// styledButton` on the table it gets in `NAME_PLATE_CREATED`) don't -// survive a later `GetNamePlateForUnit('nameplateN')` call. We pin -// the first wrapper in the Lua registry via `luaL_ref` and reuse -// that refkey for every subsequent push for the same nameplate -// pointer. Bound by the engine's CGNamePlateFrame freelist -// high-water mark (<80 typical, see `Events.cpp` g_seenPlates). -std::unordered_map g_wrapperRefkeys; - -// Build a fresh frame wrapper on the Lua stack: `{[0] = frame}` with -// `_G["__framescript_meta"]` as metatable. Same shape the engine's -// frame-registration helper builds, minus the registry-cache step -// (which we skip to avoid pinning the frame's refcount). -void PushFreshFrameWrapper(void *L, void *frame) { - auto pushLight = reinterpret_cast( - kFunLuaPushLightUserdata); - auto setMetatable = reinterpret_cast( - kFunLuaSetMetatable); - - Game::Lua::NewTable(L); - Game::Lua::PushNumber(L, 0); - pushLight(L, frame); - Game::Lua::RawSet(L, -3); - - Game::Lua::PushString(L, kFrameMetatableGlobal); - Game::Lua::GetTable(L, kLuaGlobalsIndex); - setMetatable(L, -2); -} - } // namespace static int __fastcall Script_GetNamePlates(void *L) { @@ -154,66 +115,70 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) { // frame-push path. Internal callers (Script_GetNamePlates etc.) call // it through the unqualified name (they live in the same namespace). // -// Path 1 — engine-registered (`refKey > 0`): use the engine's -// per-frame registry slot. Real refkeys come from addons that -// built their own plate via `CreateFrame`; default vanilla plates -// never hit this path (their `+0x08` is `LUA_NOREF`). +// Delegate wrapper construction to the engine's own +// `FrameScript_Object::ScriptRegister`. First call for a never-seen +// nameplate builds a `{[0] = lightuserdata}` table with the +// framescript metatable, `luaL_ref`s it into the registry, and +// writes the refkey to `nameplate + 0x08` (`OFF_COBJECT_LUA_REGISTRY_REF`). +// Subsequent calls find a populated refkey and just rawgeti. // -// Path 2 — our cached wrapper: for unregistered plates we keep one -// wrapper table per nameplate pointer pinned in the Lua registry, -// so every call returns the *same* table. Without this, addons -// like pfUI that stash `plate.nameplate = decorated` on the -// wrapper received via `NAME_PLATE_CREATED` see their field -// missing on the next `GetNamePlateForUnit` call (different -// wrapper table, fresh metatable, no carried fields). +// Why delegate instead of building our own wrapper: // -// Path 3 — build a wrapper, pin it via `luaL_ref`, leave it on the -// stack. First time we see this nameplate. +// - **Single source of truth.** Every code path that pushes this +// frame — ours via this function, addons' `CreateFrame("Type", +// "name", plate)` extracting the parent, the engine's own internal +// pushers — funnels through `rawgeti(REGISTRY, plate+0x08)` and +// gets the same Lua table. Earlier "build our own wrapper + +// `luaL_ref` it" approach kept a side cache, but pfUI's +// `CreateFrame("Button", "name", parent)` inside `NAME_PLATE_CREATED` +// ends up causing the engine to register the parent itself — +// the engine's wrapper diverges from ours, and later +// `GetNamePlateForUnit` calls return the engine's bare wrapper +// instead of our decorated one. Addon fields on the wrapper +// (`plate.nameplate = styledButton`) disappear from the API's +// perspective even though they still exist on the orphaned +// table. // -// Both lookup paths validate the rawgeti'd value is actually a -// table before returning — guards against stale registry slots -// across `/reload`, where the Lua state rebuilds but our caches -// persist. `PrepareForReload()` (below) clears the cache up front, -// but if it's somehow missed, the type check still keeps callers -// from observing a non-table. +// - **Refcount-pinning is benign here.** `ScriptRegister` +// unconditionally increments `nameplate + 0x04`, which normally +// keeps the engine from GC-ing the CObject. Nameplates are +// pool-managed and never destroyed during a session, so pinning +// doesn't actually leak anything. We call `ScriptRegister` at +// most once per nameplate pointer (gated by `refKey <= 0`), so +// the refcount tops out at one increment per pool slot. +// +// Defensive fallback: if the registry slot exists but isn't a table +// any more (some other code freed it), we re-register. This shouldn't +// happen in practice — engines only unref on frame destruction — +// but a stale type check keeps callers from ever seeing a non-table. void PushNamePlateFrame(void *L, void *nameplate) { auto rawgeti = reinterpret_cast( Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT); + auto scriptRegister = reinterpret_cast( + static_cast(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)); - const int engineRefKey = *reinterpret_cast( + int refKey = *reinterpret_cast( static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); - if (engineRefKey > 0) { - rawgeti(L, Game::Lua::REGISTRY_INDEX, engineRefKey); - if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) - return; - Game::Lua::SetTop(L, -2); // stale; fall through - } - - auto cached = g_wrapperRefkeys.find(nameplate); - if (cached != g_wrapperRefkeys.end()) { - rawgeti(L, Game::Lua::REGISTRY_INDEX, cached->second); - if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) - return; - Game::Lua::SetTop(L, -2); - g_wrapperRefkeys.erase(cached); // registry slot is dead, rebuild + if (refKey <= 0) { + scriptRegister(nameplate, nullptr, nullptr); + refKey = *reinterpret_cast( + static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); } - PushFreshFrameWrapper(L, nameplate); - // Duplicate so `luaL_ref` can consume one copy while the other - // stays on the stack as our return value. - Game::Lua::PushValue(L, -1); - auto refRef = reinterpret_cast( - static_cast(Offsets::LUA_REF_REF)); - const int newRef = refRef(L, Game::Lua::REGISTRY_INDEX); - g_wrapperRefkeys[nameplate] = newRef; -} + rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) + return; + Game::Lua::SetTop(L, -2); -// Called from `FrameScript_Initialize_h` ahead of the engine's Lua -// teardown so we don't leak refkeys into a destroyed registry. The -// nameplate frames themselves persist across `/reload`, but the -// registry slots they pointed to are about to be freed. -void PrepareForReload() { - g_wrapperRefkeys.clear(); + // Slot got freed somewhere — re-register to rebuild a fresh + // wrapper and slot. Zero the refcount so `ScriptRegister` + // re-enters its build branch. + *reinterpret_cast( + static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REFCOUNT) = 0; + scriptRegister(nameplate, nullptr, nullptr); + refKey = *reinterpret_cast( + static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); + rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); } // GUID → nameplate Frame pushed on stack. Pushes nil if the GUID diff --git a/src/nameplate/Walk.h b/src/nameplate/Walk.h index ec9bff7..bddb1d3 100644 --- a/src/nameplate/Walk.h +++ b/src/nameplate/Walk.h @@ -26,19 +26,12 @@ namespace NamePlate::Info { // Pushes the nameplate Frame at `nameplate` onto the Lua stack. -// Returns a stable wrapper table — the same table for the same -// nameplate pointer across every call — so addon-set fields on the -// wrapper (pfUI: `plate.nameplate`) survive between -// `NAME_PLATE_CREATED` and a later `GetNamePlateFor*` lookup. -// Defined in `Info.cpp`. +// Returns the engine's canonical wrapper table — the same one +// `rawgeti(REGISTRY, nameplate + 0x08)` resolves to anywhere else +// in the engine — so addon-set fields (pfUI: `plate.nameplate`) +// survive every roundtrip through our API. Defined in `Info.cpp`. void PushNamePlateFrame(void *L, void *nameplate); -// Clears the wrapper-cache map. Called from `FrameScript_Initialize` -// before the engine tears down the Lua registry on `/reload`; the -// next push for each plate then builds a fresh wrapper pinned in -// the freshly-rebuilt registry. -void PrepareForReload(); - } // namespace NamePlate::Info namespace NamePlate::Events { @@ -50,6 +43,13 @@ namespace NamePlate::Events { // REMOVED), stable within a frame. uint64_t GetGUIDByIndex(int oneBased); +// Clears the per-tick diff state (seen pointers, last tick's +// snapshot, ordered GUID list) so the post-`/reload` tick refires +// CREATED + UNIT_ADDED for every currently-visible plate. Called +// from `FrameScript_Initialize_h` alongside the wrapper-cache reset +// in `NamePlate::Info::PrepareForReload`. +void PrepareForReload(); + } // namespace NamePlate::Events namespace NamePlate::Walk {