Skip to content

C_NamePlate API + nameplateN unit tokens#1

Merged
brues-code merged 15 commits into
masterfrom
nameplates
May 26, 2026
Merged

C_NamePlate API + nameplateN unit tokens#1
brues-code merged 15 commits into
masterfrom
nameplates

Conversation

@brues-code
Copy link
Copy Markdown
Owner

@brues-code brues-code commented May 26, 2026

Summary

Vanilla 1.12.1 backport of the modern C_NamePlate.* API, including the nameplateN unit-token family that makes every Script_Unit* accept nameplate-anchored tokens transparently.

C_NamePlate.* functions (docs/API.md#nameplate):

  • C_NamePlate.GetNamePlates() — table of nameplate Frames
  • C_NamePlate.GetNamePlateGUIDs() — universal enumerator (catches default vanilla plates that aren't Lua-registered)
  • C_NamePlate.GetNamePlateForUnit(token) — token → frame, works for distant party/raid members
  • C_NamePlate.GetNamePlateForGUID(guidString) — GUID → frame

Unit tokens (docs/API.md#unit-tokens-nameplaten):

  • nameplate1..nameplateN accepted by every UnitX function — UnitName, UnitGUID, UnitClass, UnitHealth, UnitExists, UnitIsPlayer, etc. (~30 functions)
  • Suffix chains compose: nameplate1target, nameplate1targettarget
  • Creation-order semantics matching modern WoW; OOB returns nil cleanly (no "Unknown unit name" error)

Events (payloads match modern WoW exactly):

  • NAME_PLATE_CREATED — first sighting of a nameplate frame pointer, arg1 is 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_REMOVEDarg1 is 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

  • Enumeration: walks the local-player-anchored object hash table for TYPEMASK_UNIT entries with non-null unit + 0xE60.
  • Frame wrappers delegate to the engine's 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 to nameplate + 0x08. Every push thereafter — ours via rawgeti, 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 = styledButton during NAME_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).
  • Events: per-frame poll + diff (subscribed via Tick::WorldTick), not engine hooks — the engine's transient hide/reshow cycle (~7 callers of FUN_00608A10) is absorbed because we compare snapshots, not raw +0xE60 writes.
  • nameplateN tokens: single hook on FUN_TOKEN_TO_GUID (0x00515970). SStrCmpI prefix-gates "nameplate"; non-nameplate tokens fall straight through to the unmodified resolver. Suffix walker mirrors the engine's own LAB_005159d3 walker instruction-for-instruction (SStrCmpI "target" + ObjectByGUID + read UNIT_FIELD_TARGET at m_objectFields + 0x28).
  • /reload safety: diff state (g_seenPlates, g_lastTickPlates, g_orderedGUIDs) is cleared in FrameScript_Initialize_h ahead 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_objectFields is a pointer at obj + 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 redundant add esp, 12 on top of the callee's own cleanup, drifting ESP +12 per call and crashing somewhere deep in lua_pushnil once L lands in .text.
  • An earlier attempt kept our own wrapper alongside the engine's, but pfUI's CreateFrame("Button", "name", parent) inside NAME_PLATE_CREATED makes the engine register the parent itself — diverging from our wrapper and orphaning addon fields. Single-source-of-truth via ScriptRegister is the only stable shape.
  • LUA_REF_REF / LUA_REF_UNREF offset 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() work
  • GetNamePlateGUIDs() returns GUIDs for both pfUI and default vanilla plates
  • GetNamePlateForUnit("target") matches GetNamePlateForGUID(UnitGUID("target"))
  • Events fire only on real transitions; event volume reasonable during movement (10-15/sec, matches modern WoW)
  • UnitName("nameplate1"), UnitClass("nameplate1") resolve correctly
  • UnitName("nameplate1target") resolves via suffix walker (mirrors engine's own targettarget behavior)
  • UnitName("nameplate99") returns nil silently (no "Unknown unit name" error)
  • NAME_PLATE_UNIT_ADDED arg1 is the "nameplateN" token, UnitName(arg1) resolves to the unit's name in the handler
  • NAME_PLATE_UNIT_REMOVED arg1 still resolves to the leaving unit during the handler
  • With pfUI: plate.nameplate decoration survives across pool recycle — same engine CGNamePlateFrame reused for a new unit returns the same wrapper-table identity, with the decoration intact
  • Without pfUI: ({plate:GetRegions()})[3]:GetText() still returns the vanilla name string (the engine's bare wrapper has the framescript metatable and methods work)

brues-code added 13 commits May 26, 2026 02:10
- 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.
@brues-code brues-code changed the title C_NamePlate API surface C_NamePlate API + nameplateN unit tokens May 26, 2026
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.
@brues-code brues-code merged commit da8454c into master May 26, 2026
@brues-code brues-code deleted the nameplates branch May 26, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant