diff --git a/README.md b/README.md index 2c6bb86..9ff713e 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.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` | @@ -94,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` | `nameplateFrame` | +| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") | +| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") | | `PLAYER_STARTED_LOOKING` | *(none)* | | `PLAYER_STOPPED_LOOKING` | *(none)* | | `PLAYER_STARTED_MOVING` | *(none)* | @@ -116,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 5defd74..69edc24 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) @@ -195,6 +196,13 @@ build instructions. - [`C_MerchantFrame.IsMerchantItemRefundable(slot)`](#c_merchantframeismerchantitemrefundableslot) - [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled) +- [NamePlate](#nameplate) + - [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates) + - [`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) - [`C_PlayerCache.GetPlayerInfoByName(name)`](#c_playercachegetplayerinfobynamename) @@ -1567,6 +1575,66 @@ 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. 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` | `"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") +f:RegisterEvent("NAME_PLATE_CREATED") +f:RegisterEvent("NAME_PLATE_UNIT_ADDED") +f:RegisterEvent("NAME_PLATE_UNIT_REMOVED") +f:SetScript("OnEvent", function() + if event == "NAME_PLATE_CREATED" then + -- arg1 = the nameplate Frame itself + arg1:SetAlpha(0.8) + elseif event == "NAME_PLATE_UNIT_ADDED" then + -- arg1 = "nameplate1" / "nameplate2" / ... + local name = UnitName(arg1) + local plate = C_NamePlate.GetNamePlateForUnit(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 `GetNamePlateForUnit(arg1)`. + +> **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** + +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: @@ -4325,6 +4393,174 @@ 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. +## 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. + +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()` + +Returns a 1-based table of nameplate `Frame` objects — one per +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() +for i, plate in ipairs(plates) do + print(i, plate:GetName(), plate:GetWidth()) +end +``` + +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.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.GetNamePlateForGUID(guidString)` + +Same as `GetNamePlateForUnit` but takes the `"0xHHHHHHHHHHHHHHHH"` +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 = "nameplateN" token; convert to stable GUID for storage + local guid = UnitGUID(arg1) + platesByGuid[guid] = C_NamePlate.GetNamePlateForGUID(guid) +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 +`"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", ... } +``` + +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. + +### 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 @@ -6519,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 diff --git a/src/DllMain.cpp b/src/DllMain.cpp index 2546312..afeb8f8 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,15 @@ static bool __fastcall FrameScript_Initialize_h() { // table is rebuilt at a fresh allocation; the old slots are stale. Event::Custom::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 // re-initializes Lua state in both cases), giving us a clean diff --git a/src/Offsets.h b/src/Offsets.h index 104c382..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 @@ -281,6 +305,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 @@ -2089,6 +2135,22 @@ 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. 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. 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, LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset) diff --git a/src/nameplate/Events.cpp b/src/nameplate/Events.cpp new file mode 100644 index 0000000..9ab123f --- /dev/null +++ b/src/nameplate/Events.cpp @@ -0,0 +1,253 @@ +// 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. +// +// 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 "nameplate/Walk.h" +#include "tick/WorldTick.h" + +#include +#include +#include +#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. +// +// 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; + +// 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; + +// 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; + 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`. +// `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 + 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 (with the Frame as arg1) for never-before-seen + // 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()) { + 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. + // 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()) { + auto it = std::find(g_orderedGUIDs.begin(), g_orderedGUIDs.end(), + kv.first); + 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); + } + } + + g_lastTickPlates.swap(g_currentTickPlates); +} + +} // namespace + +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. +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/Info.cpp b/src/nameplate/Info.cpp new file mode 100644 index 0000000..20a3e55 --- /dev/null +++ b/src/nameplate/Info.cpp @@ -0,0 +1,265 @@ +// 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.GetNamePlates()` — returns nameplate Frame objects, +// 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, 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 +// 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 "nameplate/Walk.h" + +#include + +namespace NamePlate::Info { + +namespace { + +using NamePlate::Walk::ForEachNamePlatedUnit; +using NamePlate::Walk::kOffUnitNamePlate; + +using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n); +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); + +} // namespace + +static int __fastcall Script_GetNamePlates(void *L) { + Game::Lua::NewTable(L); + int nextIndex = 1; + ForEachNamePlatedUnit( + [L, &nextIndex](const uint8_t *, const uint8_t *nameplate, + const uint8_t *) { + Game::Lua::PushNumber(L, static_cast(nextIndex++)); + // 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; +} + +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; +} + +// 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). +// +// 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. +// +// Why delegate instead of building our own wrapper: +// +// - **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. +// +// - **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)); + + int refKey = *reinterpret_cast( + static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); + if (refKey <= 0) { + scriptRegister(nameplate, nullptr, nullptr); + refKey = *reinterpret_cast( + static_cast(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF); + } + + rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey); + if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) + return; + Game::Lua::SetTop(L, -2); + + // 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 +// 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. +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)); + PushNamePlateForGUID(L, tokenToGuid(token)); + return 1; +} + +// `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; + } + 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; + } + PushNamePlateForGUID(L, guid); + 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); + Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateForGUID", + &Script_GetNamePlateForGUID); +} + +static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +} // namespace NamePlate::Info 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 new file mode 100644 index 0000000..bddb1d3 --- /dev/null +++ b/src/nameplate/Walk.h @@ -0,0 +1,121 @@ +// 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::Info { + +// Pushes the nameplate Frame at `nameplate` onto the Lua stack. +// 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); + +} // 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); + +// 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 { + +// `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; +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