C_NamePlate API + nameplateN unit tokens#1
Merged
Merged
Conversation
- 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.
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.
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.
- 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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Vanilla 1.12.1 backport of the modern
C_NamePlate.*API, including thenameplateNunit-token family that makes everyScript_Unit*accept nameplate-anchored tokens transparently.C_NamePlate.*functions (docs/API.md#nameplate):C_NamePlate.GetNamePlates()— table of nameplate FramesC_NamePlate.GetNamePlateGUIDs()— universal enumerator (catches default vanilla plates that aren't Lua-registered)C_NamePlate.GetNamePlateForUnit(token)— token → frame, works for distant party/raid membersC_NamePlate.GetNamePlateForGUID(guidString)— GUID → frameUnit tokens (docs/API.md#unit-tokens-nameplaten):
nameplate1..nameplateNaccepted by everyUnitXfunction —UnitName,UnitGUID,UnitClass,UnitHealth,UnitExists,UnitIsPlayer, etc. (~30 functions)nameplate1target,nameplate1targettargetEvents (payloads match modern WoW exactly):
NAME_PLATE_CREATED— first sighting of a nameplate frame pointer,arg1is the Frame itself (achieved via pre-set_G.arg1+ empty format trick on the engine's printf-style dispatcher)NAME_PLATE_UNIT_ADDED/NAME_PLATE_UNIT_REMOVED—arg1is the"nameplateN"unit token, formatted from the plate's index in the ordered list at fire time. For_REMOVED, the token is computed before the slot shifts out so the handler can still resolve it to the leaving unit.Implementation notes
TYPEMASK_UNITentries with non-nullunit + 0xE60.FrameScript_Object::ScriptRegister(0x00701BD0). First push for a never-seen nameplate calls it once, the engine builds a{[0] = lightuserdata}table with the framescript metatable,luaL_refs it into the registry, and writes the refkey tonameplate + 0x08. Every push thereafter — ours viarawgeti, addons'CreateFrame(parent)extraction, the engine's own internal pushers — funnels through the same registry slot and returns the same Lua table. Addon-set fields (pfUI:plate.nameplate = styledButtonduringNAME_PLATE_CREATED) survive every roundtrip because there's a single source of truth. The refcount-pinning caveat in CLAUDE.md is benign for pool-managed nameplates (never destroyed during a session).Tick::WorldTick), not engine hooks — the engine's transient hide/reshow cycle (~7 callers ofFUN_00608A10) is absorbed because we compare snapshots, not raw+0xE60writes.nameplateNtokens: single hook onFUN_TOKEN_TO_GUID(0x00515970). SStrCmpI prefix-gates"nameplate"; non-nameplate tokens fall straight through to the unmodified resolver. Suffix walker mirrors the engine's ownLAB_005159d3walker instruction-for-instruction (SStrCmpI"target"+ObjectByGUID+ read UNIT_FIELD_TARGET atm_objectFields + 0x28)./reloadsafety: diff state (g_seenPlates,g_lastTickPlates,g_orderedGUIDs) is cleared inFrameScript_Initialize_hahead of the engine's Lua teardown so currently-visible plates refire CREATED + UNIT_ADDED for the reloaded UI. The engine handles its own wrapper registry teardown — nothing per-module to invalidate.Pitfalls hit (documented in code comments)
m_objectFieldsis a pointer atobj + 0x110, not an inline byte offset — two deref's, not one. Mistaking this read function-pointer bytes as GUIDs.SStrCmpI(0x0064A4C0) is__stdcall, not__cdecl— declaring it cdecl makes MSVC emit a redundantadd esp, 12on top of the callee's own cleanup, drifting ESP +12 per call and crashing somewhere deep inlua_pushnilonceLlands in.text.CreateFrame("Button", "name", parent)insideNAME_PLATE_CREATEDmakes the engine register the parent itself — diverging from our wrapper and orphaning addon fields. Single-source-of-truth viaScriptRegisteris the only stable shape.LUA_REF_REF/LUA_REF_UNREFoffset provenance verified via Ghidra (canonical luaL_ref/unref shapes with FREELIST_REF chaining + LUA_NOREF/REFNIL sentinels).Test plan
GetNamePlates()returns Frame tables; methods like:GetWidth()workGetNamePlateGUIDs()returns GUIDs for both pfUI and default vanilla platesGetNamePlateForUnit("target")matchesGetNamePlateForGUID(UnitGUID("target"))UnitName("nameplate1"),UnitClass("nameplate1")resolve correctlyUnitName("nameplate1target")resolves via suffix walker (mirrors engine's owntargettargetbehavior)UnitName("nameplate99")returns nil silently (no "Unknown unit name" error)NAME_PLATE_UNIT_ADDEDarg1is the"nameplateN"token,UnitName(arg1)resolves to the unit's name in the handlerNAME_PLATE_UNIT_REMOVEDarg1still resolves to the leaving unit during the handlerplate.nameplatedecoration survives across pool recycle — same engineCGNamePlateFramereused for a new unit returns the same wrapper-table identity, with the decoration intact({plate:GetRegions()})[3]:GetText()still returns the vanilla name string (the engine's bare wrapper has the framescript metatable and methods work)