mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e760d5f07e | ||
|
|
d0b6a7c0f3 | ||
|
|
96b0cf6075 | ||
|
|
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);
|
||||
|
||||
@@ -23,12 +23,13 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Container](docs/API.md#container) | `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
@@ -39,6 +40,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 +96,10 @@ 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_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_STARTED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STOPPED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STARTED_MOVING` | *(none)* |
|
||||
@@ -116,6 +122,13 @@ 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). |
|
||||
| `focus` / `focustarget` | Sticky target set via [`FocusUnit`](docs/API.md#focusunitunit), cleared via [`ClearFocus`](docs/API.md#clearfocus). Same `UnitX` coverage as `nameplateN`. Fires [`PLAYER_FOCUS_CHANGED`](docs/API.md#player_focus_changed-event) on transition. See [Focus](docs/API.md#focus). |
|
||||
|
||||
## Installation
|
||||
|
||||
Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the
|
||||
|
||||
+433
-11
@@ -34,6 +34,7 @@ build instructions.
|
||||
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
|
||||
- [`C_Container.GetContainerItemDurability(containerIndex, slotIndex)`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
|
||||
- [`C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)`](#c_containergetcontaineritemrepaircostcontainerindex-slotindex)
|
||||
- [`C_Container.GetContainerItemCharges(containerIndex, slotIndex)`](#c_containergetcontaineritemchargescontainerindex-slotindex)
|
||||
- [`C_Container.GetContainerNumFreeSlots(bagID)`](#c_containergetcontainernumfreeslotsbagid)
|
||||
- [`C_Container.PlayerHasHearthstone()`](#c_containerplayerhashearthstone)
|
||||
- [`C_Container.UseHearthstone()`](#c_containerusehearthstone)
|
||||
@@ -73,6 +74,8 @@ 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)
|
||||
- [`PLAYER_FOCUS_CHANGED` event](#player_focus_changed-event)
|
||||
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
@@ -92,6 +95,11 @@ build instructions.
|
||||
- [`C_Reputation.SetWatchedFactionByID(factionID)`](#c_reputationsetwatchedfactionbyidfactionid)
|
||||
- [`C_Reputation.GetLastStandingChange()`](#c_reputationgetlaststandingchange)
|
||||
|
||||
- [Focus](#focus)
|
||||
- [`FocusUnit(unit)`](#focusunitunit)
|
||||
- [`ClearFocus()`](#clearfocus)
|
||||
- [Unit token (`focus` / `focustarget`)](#unit-token-focus--focustarget)
|
||||
|
||||
- [FriendList](#friendlist)
|
||||
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
|
||||
- [`C_FriendList.IsWhoQueryPending()`](#c_friendlistiswhoquerypending)
|
||||
@@ -195,6 +203,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)
|
||||
@@ -756,6 +771,62 @@ Useful for "repair only items above N copper" smart-repair logic
|
||||
without scanning tooltips. ClassicAPI addition; modern WoW has no
|
||||
direct equivalent.
|
||||
|
||||
### `C_Container.GetContainerItemCharges(containerIndex, slotIndex)`
|
||||
|
||||
Per-slot equivalent of
|
||||
[`C_Item.GetItemCount`](#c_itemgetitemcountitem-includebank-includecharges)'s
|
||||
`includeCharges=true` mode — returns the total uses available in
|
||||
*this single slot*, where `GetItemCount` totals the same value
|
||||
across every matching slot.
|
||||
|
||||
```
|
||||
uses = C_Container.GetContainerItemCharges(containerIndex, slotIndex)
|
||||
```
|
||||
|
||||
`containerIndex` accepts the same values as
|
||||
[`C_Container.GetContainerItemDurability`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
|
||||
(`0` = backpack, `1..4` = equipped bags, `-1`/`5..11` = bank, etc.).
|
||||
`slotIndex` is 1-based.
|
||||
|
||||
Math is `stack * usesPerItem`, where `usesPerItem` is
|
||||
`abs(SPELL_CHARGES[0])` for items with a negative charges field
|
||||
(consume-on-use: wands, healthstones, mana gems, sapper charges)
|
||||
and `1` for everything else. Worked examples:
|
||||
|
||||
| Slot contents | Stack | Raw charges | Returns |
|
||||
|---|---:|---:|---:|
|
||||
| Wand of Decay at 50 charges | 1 | -50 | **50** |
|
||||
| Wand at 1 charge | 1 | -1 | **1** |
|
||||
| Healthstone | 1 | -1 | **1** |
|
||||
| Stack of 20 water | 20 | -1 | **20** |
|
||||
| Stack of 5 mana potions | 5 | -1 | **5** |
|
||||
| Hearthstone (cooldown-only) | 1 | 0 | **1** |
|
||||
| Any other item in the slot | 1 | 0 | **1** |
|
||||
|
||||
Returns `nil` only when the slot is empty.
|
||||
|
||||
```lua
|
||||
-- "how many charges does my wand have left?"
|
||||
local charges = C_Container.GetContainerItemCharges(0, 1)
|
||||
print("Wand charges remaining: " .. charges)
|
||||
|
||||
-- "how many drinks across the bags?" — same as GetItemCount(water, false, true)
|
||||
local total = 0
|
||||
for bag = 0, 4 do
|
||||
for slot = 1, GetContainerNumSlots(bag) do
|
||||
if C_Container.GetContainerItemID(bag, slot) == waterID then
|
||||
total = total + C_Container.GetContainerItemCharges(bag, slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
ClassicAPI addition; modern WoW has no direct equivalent (modern
|
||||
addons read charges off tooltip text). Useful when you need a
|
||||
per-slot number rather than `GetItemCount`'s rollup — e.g. to
|
||||
display "X charges" on the slot UI for a wand without re-walking
|
||||
every other matching item.
|
||||
|
||||
### `C_Container.GetContainerNumFreeSlots(bagID)`
|
||||
|
||||
Returns the number of empty slots in the given bag, plus the bag's
|
||||
@@ -896,18 +967,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 +1031,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 +1652,93 @@ 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.
|
||||
|
||||
### `PLAYER_FOCUS_CHANGED` event
|
||||
|
||||
Fires whenever the player's focus changes — assignment via
|
||||
[`FocusUnit`](#focusunitunit), clear via [`ClearFocus`](#clearfocus),
|
||||
or a `FocusUnit(token)` call where the token resolves to no GUID
|
||||
(implicit clear). No payload args; the new focus GUID is read off
|
||||
[`UnitGUID("focus")`](#unitguidunit) in the handler.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_FOCUS_CHANGED")
|
||||
f:SetScript("OnEvent", function()
|
||||
local guid = UnitGUID("focus")
|
||||
if guid then
|
||||
print("focusing", UnitName("focus"))
|
||||
else
|
||||
print("focus cleared")
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Identity-checked**: assigning the same GUID twice is a no-op
|
||||
(no event refires), matching 3.3.5's `FUN_0051FF20` behavior.
|
||||
Mirrors modern's documented semantics — "fired whenever the
|
||||
player's focus target is changed, including when the focus target
|
||||
is lost or cleared".
|
||||
|
||||
### `QUEST_ACCEPTED` event
|
||||
|
||||
Fires once per quest the player just accepted, with two payload args:
|
||||
@@ -1983,6 +2155,82 @@ This is a ClassicAPI-only call; modern WoW has no equivalent (modern
|
||||
addons get the factionID from `FACTION_STANDING_CHANGED` directly, so
|
||||
they don't need a separate getter).
|
||||
|
||||
## Focus
|
||||
|
||||
Polyfills modern WoW's focus-target system: a single sticky GUID
|
||||
that addons can pin to a unit and address via the `"focus"` unit
|
||||
token across the entire `UnitX` API surface. Backed by a hook on
|
||||
`FUN_TOKEN_TO_GUID` (shared with the [`nameplateN`](#unit-tokens-nameplaten)
|
||||
tokens — see `unit/TokenExtensions.cpp`).
|
||||
|
||||
State is **session-only** — drops on `/reload`, `/logout`, and zone
|
||||
loads that recreate Lua state. Modern Classic Era behaves the same;
|
||||
addons that want persistence have to re-`FocusUnit` from `SavedVariables`
|
||||
at `ADDON_LOADED`.
|
||||
|
||||
**Auto-clear on despawn.** When the focused unit leaves the client's
|
||||
object table — out of rendering range, full despawn, dies and
|
||||
decays — focus drops and [`PLAYER_FOCUS_CHANGED`](#player_focus_changed-event)
|
||||
fires. The unit re-entering range does NOT auto-refocus. Matches
|
||||
modern WoW's documented behavior. Implementation: per-tick
|
||||
`ObjectByGUID(g_focusGUID)` probe; on null result, `Set(0)`.
|
||||
|
||||
### `FocusUnit(unit)`
|
||||
|
||||
Sets focus to the given unit. Argument is a unit token —
|
||||
`"target"`, `"mouseover"`, `"party1"`, `"nameplate1"`, anything the
|
||||
resolver accepts. Calling with no argument is shorthand for
|
||||
`FocusUnit("target")` (matches modern's `/focus` slash command
|
||||
default).
|
||||
|
||||
```lua
|
||||
FocusUnit("target") -- pin the current target
|
||||
FocusUnit("nameplate1") -- pin the unit behind nameplate1
|
||||
FocusUnit() -- same as FocusUnit("target")
|
||||
```
|
||||
|
||||
Fires [`PLAYER_FOCUS_CHANGED`](#player_focus_changed-event) if the
|
||||
resolved GUID differs from the current focus. No-op (no event) if
|
||||
the same unit is already focused — matches the
|
||||
identity-check-first behavior of 3.3.5's `FUN_0051FF20`.
|
||||
|
||||
Passing a token that doesn't resolve to a unit (e.g. `"target"`
|
||||
with nothing targeted, or `"party5"` solo) clears focus — same as
|
||||
calling `ClearFocus()`.
|
||||
|
||||
### `ClearFocus()`
|
||||
|
||||
Drops the focus. Fires `PLAYER_FOCUS_CHANGED` if there was one;
|
||||
no-op otherwise.
|
||||
|
||||
```lua
|
||||
ClearFocus()
|
||||
```
|
||||
|
||||
### Unit token (`focus` / `focustarget`)
|
||||
|
||||
`"focus"` resolves to whatever GUID `FocusUnit` last stashed.
|
||||
Accepted by every `UnitX` function:
|
||||
|
||||
```lua
|
||||
local name = UnitName("focus") -- nil if no focus
|
||||
local hp = UnitHealth("focus")
|
||||
local _, class = UnitClass("focus")
|
||||
```
|
||||
|
||||
Chains compose through the engine's standard suffix walker:
|
||||
`"focustarget"`, `"focustargettarget"`, etc. — same behavior as
|
||||
`"targettarget"`, mirrored instruction-for-instruction in our hook
|
||||
so addons get the engine semantics they expect.
|
||||
|
||||
Returns `nil` cleanly when no focus is set; doesn't raise the
|
||||
"Unknown unit name" error.
|
||||
|
||||
[`UnitTokenFromGUID`](#unittokenfromguidguid) scans `"focus"` right
|
||||
after `"target"` (matching retail order), so a focused unit's GUID
|
||||
reverse-resolves to `"focus"` only if it isn't already addressable
|
||||
as `"player"` / `"party*"` / `"raid*"` / `"nameplate*"` / `"target"`.
|
||||
|
||||
## FriendList
|
||||
|
||||
### `C_FriendList.SendWhoQueryByName(name)`
|
||||
@@ -4325,6 +4573,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 +6935,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`, `softenemy`,
|
||||
`softfriend`, `softinteract` all post-date vanilla and the engine's
|
||||
resolver doesn't recognize them). `nameplateN` and `focus` are
|
||||
included — we hook the resolver so the engine recognizes both token
|
||||
forms. As with the other positional tokens, a returned
|
||||
`"nameplate3"` 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 → focus
|
||||
→ 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)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
|
||||
// returns the `abs(SPELL_CHARGES[0])` value of the item in the
|
||||
// specified bag slot, or `nil` for empty slots and items that don't
|
||||
// carry a charge concept. Wands return their remaining cast count
|
||||
// (e.g. 50 → 49 → …); single-use items (Healthstone, Mana Gem,
|
||||
// Goblin Sapper Charge) return `1`.
|
||||
//
|
||||
// Modern equivalent: none — modern WoW exposes charges only through
|
||||
// tooltip text scanning. `C_Item.GetItemCount(includeBank, includeCharges)`
|
||||
// totals charges across every matching stack; this is the per-slot
|
||||
// reader.
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Charges.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
namespace Container::Charges {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemCharges(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
return Item::Charges::PushChargesForItem(
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
|
||||
&Script_C_Container_GetContainerItemCharges);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::Charges
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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/>.
|
||||
|
||||
#include "Charges.h"
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Item::Charges {
|
||||
|
||||
int PushChargesForItem(void *L, const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
if (descriptor == nullptr)
|
||||
return 0;
|
||||
const int32_t stack = static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
const int32_t rawCharges = *reinterpret_cast<const int32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_SPELL_CHARGES_0);
|
||||
const int32_t usesPerItem = (rawCharges < 0) ? -rawCharges : 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(stack * usesPerItem));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace Item::Charges
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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>
|
||||
|
||||
namespace Item::Charges {
|
||||
|
||||
// Pushes the total uses available in this single slot — the
|
||||
// per-slot equivalent of `GetItemCount(_, _, includeCharges=true)`.
|
||||
// Returns `1` (pushed one value) on success, `0` (pushes nothing,
|
||||
// caller surfaces as `nil`) when the item is missing or the
|
||||
// descriptor is null.
|
||||
//
|
||||
// Math is `stack * usesPerItem` where `usesPerItem` follows
|
||||
// `Item::Count::GetUsesPerItem`:
|
||||
// - `SPELL_CHARGES[0] >= 0` → `1` (no destroy-on-zero behavior;
|
||||
// stack count alone reflects "how many uses").
|
||||
// - `SPELL_CHARGES[0] < 0` → `abs(value)` (consume-on-use items;
|
||||
// wand at 50 has `-50` so each item contributes 50).
|
||||
//
|
||||
// Worked examples:
|
||||
// - Wand at 50 charges (stack=1, raw=-50): `1 * 50 = 50`
|
||||
// - Hearthstone (stack=1, raw=0): `1 * 1 = 1` (cooldown-
|
||||
// only, no charges concept; we still surface "1 use").
|
||||
// - Water stack of 20 (stack=20, raw=-1): `20 * 1 = 20`
|
||||
// - Equipped sword (stack=1, raw=0): `1 * 1 = 1`
|
||||
//
|
||||
// Caller can `return PushChargesForItem(L, item);` to forward the
|
||||
// count directly.
|
||||
int PushChargesForItem(void *L, const uint8_t *item);
|
||||
|
||||
} // namespace Item::Charges
|
||||
+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 `unit/TokenExtensions.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 `unit/TokenExtensions.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 `unit/TokenExtensions.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,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
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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/>.
|
||||
|
||||
// `FocusUnit(unit)` / `ClearFocus()` / `focus` unit token. Polyfills
|
||||
// the post-vanilla focus-target system on top of the engine's
|
||||
// existing token resolver via the hook in
|
||||
// `unit/TokenExtensions.cpp`.
|
||||
//
|
||||
// Storage: a single 64-bit GUID held in-process. The 3.3.5 engine
|
||||
// keeps the same shape (two globals at `DAT_00BD07D0`/`D4`); we
|
||||
// don't share those because vanilla never reads them. The resolver
|
||||
// hook reads our `Unit::Focus::Get()` directly.
|
||||
//
|
||||
// Setter mirrors 3.3.5's `FUN_0051FF20` minus the post-vanilla
|
||||
// `UnitFlag bit 0x2000` ("focus glow" rendering hint, introduced in
|
||||
// TBC for the default focus frame). Vanilla addons (pfUI) render
|
||||
// their own focus indicator and don't need the hint.
|
||||
//
|
||||
// Auto-clear on despawn: modern WoW fires `PLAYER_FOCUS_CHANGED`
|
||||
// when the focused unit leaves the client's object table (out of
|
||||
// rendering range, despawn) and does NOT auto-refocus when they
|
||||
// come back. We mirror that by probing `ObjectByGUID(g_focusGUID)`
|
||||
// every world tick — when it returns null, we `Set(0)` which fires
|
||||
// the event. Cost is one hash-table lookup per tick while focus is
|
||||
// set; zero when no focus is active.
|
||||
|
||||
#include "Focus.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Focus {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEventName = "PLAYER_FOCUS_CHANGED";
|
||||
const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
|
||||
uint64_t g_focusGUID = 0;
|
||||
|
||||
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);
|
||||
|
||||
uint64_t ResolveTokenGUID(const char *token) {
|
||||
if (token == nullptr || *token == '\0')
|
||||
return 0;
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Per-tick despawn watcher. Probes the engine's object table for
|
||||
// `g_focusGUID`; when it disappears (out of range, fully despawned),
|
||||
// clear focus and fire PLAYER_FOCUS_CHANGED — matches modern's
|
||||
// "leaves render distance → focus drops" behavior. Won't refocus
|
||||
// when the unit comes back, also matching modern.
|
||||
void OnWorldTick() {
|
||||
if (g_focusGUID == 0)
|
||||
return;
|
||||
auto resolve = reinterpret_cast<ResolveByGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
if (resolve(Offsets::OBJ_TYPE_UNIT, "Focus",
|
||||
static_cast<uint32_t>(g_focusGUID),
|
||||
static_cast<uint32_t>(g_focusGUID >> 32),
|
||||
0x172) == nullptr)
|
||||
Set(0);
|
||||
}
|
||||
|
||||
// `FocusUnit(unit)` — sets focus to whatever GUID `unit` currently
|
||||
// resolves to. Modern signature: `FocusUnit(unit)` with the token
|
||||
// argument. Modern also allows `FocusUnit()` (no arg) to mean
|
||||
// `"target"`, matching the `/focus` slash command's default.
|
||||
int __fastcall Script_FocusUnit(void *L) {
|
||||
if (Game::Lua::IsString(L, 1)) {
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
Set(ResolveTokenGUID(token));
|
||||
} else {
|
||||
Set(ResolveTokenGUID("target"));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ClearFocus()` — drops the focus, fires PLAYER_FOCUS_CHANGED.
|
||||
int __fastcall Script_ClearFocus(void *L) {
|
||||
(void)L;
|
||||
Set(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("FocusUnit", &Script_FocusUnit);
|
||||
Game::Lua::RegisterGlobalFunction("ClearFocus", &Script_ClearFocus);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t Get() { return g_focusGUID; }
|
||||
|
||||
void Set(uint64_t guid) {
|
||||
if (guid == g_focusGUID)
|
||||
return; // no-op: same target → no event fire (matches 3.3.5)
|
||||
g_focusGUID = guid;
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
}
|
||||
|
||||
} // namespace Unit::Focus
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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>
|
||||
|
||||
namespace Unit::Focus {
|
||||
|
||||
// Current focus GUID, or `0` when nothing is focused. Read by the
|
||||
// `focus` / `focustarget*` branch of the token resolver in
|
||||
// `unit/TokenExtensions.cpp`.
|
||||
uint64_t Get();
|
||||
|
||||
// Sets focus to `guid` and fires `PLAYER_FOCUS_CHANGED` (no args).
|
||||
// Identity-checks first — assigning the same GUID is a no-op,
|
||||
// matching 3.3.5's `FUN_0051FF20` behavior so the event only fires
|
||||
// on a real transition. Pass `0` to clear focus.
|
||||
void Set(uint64_t guid);
|
||||
|
||||
} // namespace Unit::Focus
|
||||
+27
-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,17 @@ 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`,
|
||||
// `softenemy`, `softfriend`, `softinteract` — those all post-date
|
||||
// 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit name" for
|
||||
// them). `nameplateN` and `focus` are included — we hook the token
|
||||
// resolver so the engine recognizes them (see
|
||||
// `unit/TokenExtensions.cpp` and `unit/Focus.cpp`). Order matches
|
||||
// retail with the dropped tokens skipped in place:
|
||||
//
|
||||
// player → pet → party1..4 → partypet1..4 → raid1..40
|
||||
// → raidpet1..40 → target → npc → mouseover
|
||||
// → raidpet1..40 → nameplate1..N → target → focus
|
||||
// → 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,7 +188,24 @@ 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("focus")) return 1;
|
||||
if (check("npc")) return 1;
|
||||
if (check("mouseover")) return 1;
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// 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 post-vanilla unit
|
||||
// tokens. Hooking the central token→GUID resolver means every
|
||||
// `Script_Unit*` (UnitName, UnitGUID, UnitHealth, UnitClass,
|
||||
// UnitExists, …) gains support for the new token families for free,
|
||||
// since they all funnel here.
|
||||
//
|
||||
// Currently extended families:
|
||||
//
|
||||
// - `nameplate1`..`nameplateN` — index into the ordered list of
|
||||
// visible nameplates maintained in `nameplate/Events.cpp`. See
|
||||
// `Unit tokens (nameplateN)` in docs/API.md.
|
||||
//
|
||||
// - `focus` — single-slot sticky target backed by `Unit::Focus`.
|
||||
// See `unit/Focus.cpp` for the storage + `PLAYER_FOCUS_CHANGED`
|
||||
// event firing.
|
||||
//
|
||||
// Both families compose with the engine's own `target`-suffix walker
|
||||
// (`focustarget`, `nameplate1targettarget`) via the shared
|
||||
// `WalkSuffix` helper, which mirrors the engine's
|
||||
// `LAB_005159d3..0x00515A2C` walker instruction-for-instruction
|
||||
// (SStrCmpI "target" + ObjectByGUID + read UNIT_FIELD_TARGET).
|
||||
//
|
||||
// Resolution order in the hook: focus check first (literal-equal,
|
||||
// short token), then nameplate prefix (longer, requires digit).
|
||||
// Both branches short-circuit; on no-match the original engine
|
||||
// resolver runs unchanged.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "nameplate/Walk.h"
|
||||
#include "unit/Focus.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::TokenExtensions {
|
||||
|
||||
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 kNamePlatePrefix[] = "nameplate";
|
||||
constexpr int kNamePlatePrefixLen = static_cast<int>(sizeof(kNamePlatePrefix) - 1);
|
||||
constexpr const char kFocusPrefix[] = "focus";
|
||||
constexpr int kFocusPrefixLen = static_cast<int>(sizeof(kFocusPrefix) - 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, "ClassicAPI",
|
||||
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);
|
||||
|
||||
// `focus` / `focustarget*`. The 5-char prefix is unique among
|
||||
// unit tokens (no `focuscast` / `focused*` etc. live in vanilla
|
||||
// or modern resolvers), so any match continues as our token.
|
||||
if (sstrcmpi(token, kFocusPrefix, kFocusPrefixLen) == 0) {
|
||||
const uint64_t guid = Unit::Focus::Get();
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
return WalkSuffix(guid, token + kFocusPrefixLen);
|
||||
}
|
||||
|
||||
// `nameplateN` / `nameplate1target*`. Requires a digit after the
|
||||
// prefix; anything else (`nameplate`, `nameplateX`) falls through
|
||||
// to the engine for its standard "Unknown unit name" error.
|
||||
if (sstrcmpi(token, kNamePlatePrefix, kNamePlatePrefixLen) == 0) {
|
||||
const char *p = token + kNamePlatePrefixLen;
|
||||
if (*p >= '0' && *p <= '9') {
|
||||
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;
|
||||
return WalkSuffix(guid, p);
|
||||
}
|
||||
}
|
||||
|
||||
return Original_o(token);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static const Game::HookAutoRegister _hook{
|
||||
Offsets::FUN_TOKEN_TO_GUID,
|
||||
reinterpret_cast<void *>(&Hook_h),
|
||||
reinterpret_cast<void **>(&Original_o)};
|
||||
|
||||
} // namespace Unit::TokenExtensions
|
||||
Reference in New Issue
Block a user