mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c14c6947 | ||
|
|
a0df98750b | ||
|
|
da8454c7c2 | ||
|
|
d5a45c73cb |
@@ -27,3 +27,4 @@ Util\TimedCallback.lua
|
||||
Util\EquipmentManager.lua
|
||||
Util\PlayerUtil.lua
|
||||
Util\SlashCommandsRegistry.lua
|
||||
Util\LinkUtil.lua
|
||||
|
||||
@@ -62,7 +62,10 @@ LinkTypes = {
|
||||
LinkUtil = {};
|
||||
|
||||
function LinkUtil.FormatLink(linkType, linkDisplayText, ...)
|
||||
local linkFormatTable = { string.format("|H%s", linkType), ... };
|
||||
local linkFormatTable = { string.format("|H%s", linkType) };
|
||||
for i = 1, arg.n do
|
||||
linkFormatTable[i + 1] = arg[i];
|
||||
end
|
||||
local returnLink = table.concat(linkFormatTable, ":");
|
||||
if linkDisplayText then
|
||||
return returnLink .. string.format("|h%s|h", linkDisplayText);
|
||||
|
||||
@@ -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
|
||||
|
||||
+267
-11
@@ -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)
|
||||
@@ -896,18 +904,29 @@ C_Container.SwapItems(1, 3, 0, 7)
|
||||
C_Container.SwapItems(0, 1, 0, 2)
|
||||
```
|
||||
|
||||
Bag IDs match the rest of `C_Container.*`: `0` is the backpack, `1..4`
|
||||
are the equipped bags. Slots are 1-based. The engine accepts both
|
||||
directions:
|
||||
Bag IDs match the rest of `C_Container.*`:
|
||||
|
||||
- Same container (e.g. backpack ↔ backpack, bag1 ↔ bag1) sends opcode
|
||||
`0x10D` `CMSG_SWAP_INV_ITEM` with the two linear slot bytes.
|
||||
- Cross-container (e.g. backpack ↔ bag1, bag2 ↔ bag3) sends opcode
|
||||
`0x10C` `CMSG_SWAP_ITEM` with `(srcBag, srcSlot, dstBag, dstSlot)`.
|
||||
- `0` — backpack
|
||||
- `1..4` — equipped bags
|
||||
- `-1` — main bank (24 slots; **requires the bank window to be open**)
|
||||
- `5..10` — equipped bank bags (**requires the bank window to be open**)
|
||||
|
||||
Slots are 1-based. The engine accepts both directions:
|
||||
|
||||
- Same container (e.g. backpack ↔ backpack, bag1 ↔ bag1, bank ↔ bank)
|
||||
sends opcode `0x10D` `CMSG_SWAP_INV_ITEM` with the two linear slot bytes.
|
||||
- Cross-container (e.g. backpack ↔ bag1, bag2 ↔ bank, bank ↔ bank bag)
|
||||
sends opcode `0x10C` `CMSG_SWAP_ITEM` with `(srcBag, srcSlot, dstBag, dstSlot)`.
|
||||
|
||||
Destination empty becomes a move; destination occupied becomes an
|
||||
atomic swap.
|
||||
|
||||
> **Bank slots require the bank window to be open.** The engine
|
||||
> doesn't sync bank-side `CGContainer`s or bank inventory entries
|
||||
> before the first bank interaction, so any bank `EncodeBagSlot`
|
||||
> path returns false until then. Same constraint as
|
||||
> `C_Container.GetContainerItemInfo` on bank IDs.
|
||||
|
||||
> **ClassicAPI-only.** Modern Classic Era has no direct swap-two-slots
|
||||
> call — addons there drive the cursor with two `PickupContainerItem`
|
||||
> calls in sequence. This single-call form bypasses the cursor
|
||||
@@ -949,6 +968,9 @@ consolidate partial stacks — `SwapItems` swaps the whole slots, which
|
||||
isn't what you want when both slots hold the same item but one has a
|
||||
partial stack.
|
||||
|
||||
Bank IDs (`-1`, `5..10`) work here too with the same bank-window
|
||||
constraint as `SwapItems`.
|
||||
|
||||
> **vs. the modern equivalent.** Modern Classic Era has no direct
|
||||
> one-call move — addons there string `SplitContainerItem(bag, slot,
|
||||
> count)` + `PickupContainerItem(dstBag, dstSlot)` together to drive
|
||||
@@ -1567,6 +1589,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 +4407,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,13 +6769,19 @@ 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
|
||||
included between `raidpetN` and `target` — we hook the resolver so
|
||||
the engine recognizes the token form (see
|
||||
[Unit tokens](#unit-tokens-nameplaten)). As with the other
|
||||
positional tokens, the returned `"nameplate3"`-style result is only
|
||||
valid at that instant; the slot may shift to a different unit
|
||||
between calls, so re-verify with `UnitGUID(token)` before reusing.
|
||||
|
||||
```
|
||||
player → pet → party1..4 → partypet1..4 → raid1..40
|
||||
→ raidpet1..40 → target → npc → mouseover
|
||||
→ raidpet1..40 → nameplate1..N → target → npc → mouseover
|
||||
```
|
||||
|
||||
```lua
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+45
-2
@@ -64,12 +64,34 @@ constexpr int FIRST_BAG_CONTAINER_LINEAR = 19;
|
||||
// player invMgr — i.e. 1-based bag-0 slot S maps to linear 22+S.
|
||||
constexpr int BACKPACK_LINEAR_OFFSET = 22;
|
||||
|
||||
// Main bank contents (bagID -1) live at linear slots 39..62 in the
|
||||
// player invMgr — i.e. 1-based bank slot S maps to linear 38+S.
|
||||
// Same encoding shape as the backpack (container is the player
|
||||
// itself), different base offset. Range valid only when the bank
|
||||
// window has been opened in the current session.
|
||||
constexpr int BANK_MAIN_LINEAR_OFFSET = 38;
|
||||
constexpr int BANK_MAIN_NUM_SLOTS = 24;
|
||||
|
||||
// Bank bag containers occupy 1-based equipment slots 64..69 (linear
|
||||
// 63..68). Addressed by bagID 5..10 — bagID 5 = first bank bag.
|
||||
// Requires bank window open; otherwise the engine's CGContainer for
|
||||
// the bag is unsynced and `ResolveEquipmentSlot` returns null.
|
||||
constexpr int FIRST_BANK_BAG_EQ_SLOT = 64;
|
||||
constexpr int FIRST_BANK_BAG_ID = 5;
|
||||
constexpr int LAST_BANK_BAG_ID = 10;
|
||||
|
||||
// (bagID, 1-based slot) → (containerGUID, engine linear slot). Returns
|
||||
// false for unequipped bags or out-of-range slots; does NOT validate
|
||||
// that the slot is occupied (caller checks separately).
|
||||
//
|
||||
// bagID 0 → container = player, linear = BACKPACK_LINEAR_OFFSET + slot
|
||||
// bagID 1..4 → container = equipped bag's CGContainer, linear = slot - 1
|
||||
// bagID 0 → container = player, linear = 22 + slot (backpack)
|
||||
// bagID 1..4 → container = equipped bag's CGContainer, linear = slot - 1
|
||||
// bagID -1 → container = player, linear = 38 + slot (bank main)
|
||||
// bagID 5..10 → container = bank bag's CGContainer, linear = slot - 1
|
||||
//
|
||||
// Bank-side encodings only resolve once the bank window has been
|
||||
// opened in the current session (the engine doesn't sync the bank
|
||||
// CGContainers or bank-slot inventory entries before then).
|
||||
bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
uint32_t *containerLo, uint32_t *containerHi,
|
||||
uint32_t *linearSlot) {
|
||||
@@ -94,6 +116,27 @@ bool EncodeBagSlot(int bagID, int slotInBag,
|
||||
*linearSlot = static_cast<uint32_t>(slotInBag - 1);
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
if (slotInBag > BANK_MAIN_NUM_SLOTS)
|
||||
return false;
|
||||
void *player = ResolvePlayer();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
if (!ReadGuid(player, containerLo, containerHi))
|
||||
return false;
|
||||
*linearSlot = static_cast<uint32_t>(BANK_MAIN_LINEAR_OFFSET + slotInBag);
|
||||
return true;
|
||||
}
|
||||
if (bagID >= FIRST_BANK_BAG_ID && bagID <= LAST_BANK_BAG_ID) {
|
||||
const uint8_t *bag = Item::Location::ResolveEquipmentSlot(
|
||||
FIRST_BANK_BAG_EQ_SLOT + (bagID - FIRST_BANK_BAG_ID));
|
||||
if (bag == nullptr)
|
||||
return false;
|
||||
if (!ReadGuid(bag, containerLo, containerHi))
|
||||
return false;
|
||||
*linearSlot = static_cast<uint32_t>(slotInBag - 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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<N>` 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 <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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<uint64_t, const void *> 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<uint64_t, const void *> 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<const void *> 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<uint64_t> 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<N>` 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<LuaRefRef_t>(
|
||||
static_cast<uintptr_t>(Offsets::LUA_REF_REF));
|
||||
auto refUnref = reinterpret_cast<LuaRefUnref_t>(
|
||||
static_cast<uintptr_t>(Offsets::LUA_REF_UNREF));
|
||||
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
|
||||
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<const uint64_t *>(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<void *>(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<int>(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<int>(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<size_t>(oneBased - 1);
|
||||
if (idx >= g_orderedGUIDs.size())
|
||||
return 0;
|
||||
return g_orderedGUIDs[idx];
|
||||
}
|
||||
|
||||
} // namespace NamePlate::Events
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `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 <cstdint>
|
||||
|
||||
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<double>(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<uint8_t *>(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<const uint64_t *>(instance);
|
||||
if (guid == 0)
|
||||
return;
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushNumber(L, static_cast<double>(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<LuaRawGetI_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
|
||||
auto scriptRegister = reinterpret_cast<ScriptRegister_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER));
|
||||
|
||||
int refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
if (refKey <= 0) {
|
||||
scriptRegister(nameplate, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(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<int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REFCOUNT) = 0;
|
||||
scriptRegister(nameplate, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(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<ResolveByGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
auto *unit = static_cast<uint8_t *>(
|
||||
resolve(Offsets::OBJ_TYPE_UNIT, "NamePlate",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32),
|
||||
0x172));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return;
|
||||
}
|
||||
auto *nameplate = *reinterpret_cast<uint8_t *const *>(
|
||||
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<TokenToGUID_t>(
|
||||
static_cast<uintptr_t>(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
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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 <cstdint>
|
||||
|
||||
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<int>(sizeof(kPrefix) - 1);
|
||||
constexpr const char kSuffixTarget[] = "target";
|
||||
constexpr int kSuffixTargetLen = static_cast<int>(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<ResolveByGUID_t>(
|
||||
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(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<uint8_t *>(
|
||||
resolve(Offsets::OBJ_TYPE_UNIT, "nameplate",
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(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<const uint8_t *const *>(
|
||||
obj + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
if (fields == nullptr)
|
||||
return 0;
|
||||
guid = *reinterpret_cast<const uint64_t *>(
|
||||
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<SStrCmpI_t>(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<void *>(&Hook_h),
|
||||
reinterpret_cast<void **>(&Original_o)};
|
||||
|
||||
} // namespace NamePlate::TokenResolver
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// 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 <typename F>
|
||||
int ForEachNamePlatedUnit(F &&emit) {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(kLocalPlayerGlobal);
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
auto *buckets = *reinterpret_cast<uint8_t *const *>(
|
||||
player + kOffPlayerBucketArray);
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
|
||||
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<const uint32_t *>(
|
||||
bucket + kBucketLinkOffsetField);
|
||||
uintptr_t entry = *reinterpret_cast<const uintptr_t *>(
|
||||
bucket + kBucketChainHeadField);
|
||||
|
||||
while (entry != 0 && (entry & 1) == 0) {
|
||||
auto *obj = reinterpret_cast<const uint8_t *>(entry);
|
||||
auto *instance = *reinterpret_cast<const uint8_t *const *>(
|
||||
obj + kOffEntryInstanceBlock);
|
||||
if (instance != nullptr) {
|
||||
const uint32_t typeMask = *reinterpret_cast<const uint32_t *>(
|
||||
instance + kOffInstanceTypeMask);
|
||||
if ((typeMask & kTypeMaskUnit) != 0) {
|
||||
const auto *nameplate = *reinterpret_cast<const uint8_t *const *>(
|
||||
obj + kOffUnitNamePlate);
|
||||
if (nameplate != nullptr) {
|
||||
emit(obj, nameplate, instance);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
entry = *reinterpret_cast<const uintptr_t *>(
|
||||
obj + linkOffset + 4);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace NamePlate::Walk
|
||||
+24
-5
@@ -14,6 +14,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "nameplate/Walk.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -105,13 +106,15 @@ static int __fastcall Script_UnitGUID(void *L) {
|
||||
// nil if none of the known tokens point at it.
|
||||
//
|
||||
// The search walks the modern retail token order with post-vanilla
|
||||
// tokens dropped (no `vehicle`, `nameplateN`, `arenaN`, `arenapetN`,
|
||||
// `bossN`, `focus`, `softenemy`, `softfriend`, `softinteract` —
|
||||
// those all post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise
|
||||
// "Unknown unit name" for them). Order:
|
||||
// tokens dropped (no `vehicle`, `arenaN`, `arenapetN`, `bossN`,
|
||||
// `focus`, `softenemy`, `softfriend`, `softinteract` — those all
|
||||
// post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit
|
||||
// name" for them). `nameplateN` is included — we hook the token
|
||||
// resolver so the engine recognizes it (see `nameplate/TokenResolver.cpp`).
|
||||
// Order:
|
||||
//
|
||||
// player → pet → party1..4 → partypet1..4 → raid1..40
|
||||
// → raidpet1..40 → target → npc → mouseover
|
||||
// → raidpet1..40 → nameplate1..N → target → npc → mouseover
|
||||
//
|
||||
// The result is inherently unstable — multiple tokens can map to the
|
||||
// same GUID at once (your target IS your mouseover IS your party1), and
|
||||
@@ -183,6 +186,22 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
|
||||
if (check(buf)) return 1;
|
||||
}
|
||||
|
||||
// Visible nameplates. The ordered list (see
|
||||
// `NamePlate::Events::g_orderedGUIDs`) is dense — `GetGUIDByIndex`
|
||||
// returns 0 once we step past the populated count, terminating
|
||||
// the loop. Skips the token-resolver hook + SStrCmpI cost per
|
||||
// index by comparing GUIDs directly.
|
||||
for (int i = 1; ; ++i) {
|
||||
const uint64_t plateGuid = NamePlate::Events::GetGUIDByIndex(i);
|
||||
if (plateGuid == 0)
|
||||
break;
|
||||
if (plateGuid == target) {
|
||||
std::snprintf(buf, sizeof buf, "nameplate%d", i);
|
||||
Game::Lua::PushString(L, buf);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (check("target")) return 1;
|
||||
if (check("npc")) return 1;
|
||||
if (check("mouseover")) return 1;
|
||||
|
||||
Reference in New Issue
Block a user