Compare commits

..
15 Commits
Author SHA1 Message Date
Brues ec73652b67 2nd arg "canOpen" for IsItemOpenable
removed IsItemOpenableByID
2026-05-28 23:26:04 -05:00
Brues 4c798ba58a turtle wow injects a bad version of print 2026-05-28 16:01:17 -05:00
brues-code fbe81b7f01 Simplify API documentation for combat check
Removed redundant explanation for UnitAffectingCombat.
2026-05-28 15:30:44 -05:00
brues-code 1c0e20cca8 Update CharacterOrder to CharacterList in README 2026-05-28 15:21:40 -05:00
Brues 1242f1dd9f Openable: return nil for uncached items 2026-05-28 14:37:43 -05:00
Brues de0c41dd1d IsItemOpenable 2026-05-28 14:33:41 -05:00
Brues 3e22e2c087 useless comment 2026-05-28 14:33:23 -05:00
Brues beb9dec6e1 max out CLASSICAPI_VERSION_VALUE for dev build 2026-05-28 00:52:17 -05:00
Brues e760d5f07e Drop focus when out of render distance 2026-05-27 23:44:22 -05:00
Brues d0b6a7c0f3 C_Container.GetContainerItemCharges 2026-05-27 23:43:28 -05:00
brues-codeandBrues 96b0cf6075 FocusUnit / ClearFocus + focus unit token (#2)
* FocusUnit

* UnitTokenFromGUID: place focus after target, not after mouseover

Retail's documented scan order is
  player -> vehicle -> pet -> party -> partypet -> raid -> raidpet
        -> nameplate -> arena -> arenapet -> boss -> target -> focus
        -> npc -> mouseover -> softenemy -> softfriend -> softinteract

Earlier commit placed focus at the very end. Moves it into the
target-adjacent slot so a focused unit reverse-resolves to "focus"
the moment it isn't also "target", matching retail.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-05-27 19:34:12 -05:00
Brues c0c14c6947 C_Container.SwapItems / MoveItem: accept bank slot IDs
Extend EncodeBagSlot with two new branches matching vanilla's bagID
conventions:

- bagID = -1  -> main bank (24 slots at linear 39..62, container =
  player GUID; same shape as backpack with a +38 offset instead of
  +22).
- bagID = 5..10 -> equipped bank bags (containers at equipment slots
  64..69; same shape as equipped bags with the index range shifted).

Both paths require the bank window to be open in the current
session — the engine doesn't sync bank-side CGContainers or bank
inventory entries before then, so ResolveEquipmentSlot returns null
and EncodeBagSlot bails cleanly. Same constraint
GetContainerItemInfo has on bank IDs.
2026-05-26 19:11:55 -05:00
Brues a0df98750b UnitTokenFromGUID: scan nameplateN between raidpet and target
Now that the engine recognizes `nameplateN` tokens (via the resolver
hook in nameplate/TokenResolver.cpp), they belong in the reverse-
lookup walk too. Iterates the dense ordered list directly via
NamePlate::Events::GetGUIDByIndex — bails on the first zero return,
skipping the token-resolver + SStrCmpI cost per index.
2026-05-26 16:32:08 -05:00
brues-codeandBrues da8454c7c2 C_NamePlate API + nameplateN unit tokens (#1)
* C_NamePlate.GetNamePlateGUIDs

* fixed NameCache header

* C_NamePlate.GetNamePlates

* GetNamePlates will show blizzard nameplates as well

* C_NamePlate.GetNamePlateForUnit

* nameplate events

* C_NamePlate.GetNamePlateForGUID

* NAME_PLATE_CREATED now sends frame as argument

* document verification provenance for nameplate-adjacent offsets

- Offsets.h: annotate LUA_REF_REF / LUA_REF_UNREF with their Ghidra
  decompile shapes (FREELIST_REF chain + LUA_REFNIL/NOREF sentinels)
  so the next maintainer doesn't have to re-derive whether these are
  really luaL_ref/unref.
- Walk.h: warn that the 0x00B41414 player global overlays multiple
  sub-objects; reading +0x1C/+0x24 here is safe, feeding it to the
  inventory path crashes.

* harden nameplate frame push against stale registry refs

PushNamePlateFrame now type-checks the rawgeti'd value; on a stale
slot (post-/reload windows, freed slot, etc.) it pops and falls back
to a fresh wrapper instead of returning whatever the registry happens
to hold at that index in the new Lua state. Script_GetNamePlates now
routes through the same helper so it inherits the guard.

Also document the bound on g_seenPlates: pool reuse via DAT_00c4d920
caps it at the session's peak simultaneous-plate count.

* support nameplateN unit tokens

Hook FUN_TOKEN_TO_GUID so every Script_Unit* (UnitName, UnitGUID,
UnitHealth, UnitClass, UnitExists, ...) accepts `nameplateN` tokens
and their `target`-suffix chains (`nameplate1target`,
`nameplate1targettarget`) — modern WoW semantics on the vanilla
engine. Hook is gated by an SStrCmpI prefix check; non-nameplate
tokens fall straight through to the original resolver.

- Ordered GUID list is maintained alongside the existing
  NAME_PLATE_UNIT_ADDED / _REMOVED diff. Append on ADDED, erase on
  REMOVED — creation-order, stable for a plate's lifetime, modern
  semantics.
- Suffix walker mirrors the engine's own LAB_005159d3 walker
  instruction-for-instruction: SStrCmpI "target" + ObjectByGUID +
  read UNIT_FIELD_TARGET (m_objectFields + 0x28).
- SStrCmpI typedef is __stdcall, not __cdecl. The function ends with
  ret 0xc; declaring it cdecl makes MSVC emit a redundant add esp, 12
  on top of the callee cleanup, drifting ESP +12 per call and
  crashing somewhere deep in Lua once L gets read from a poisoned
  stack slot.
- m_objectFields is a *pointer* at obj+0x110, not an inline byte
  offset. Two dereferences (mov eax, [obj+0x110]; mov edi, [eax+0x28]),
  not one — earlier version mistook the inline-vs-pointer shape and
  read instruction bytes as GUIDs.

* document nameplateN unit-token support

- API.md NamePlate section: replace the "scope vs. modern (deferred)"
  callout with a pointer to the new Unit tokens subsection; update
  the events table and "modern divergence" callout to explain why
  the GUID payload remains (engine dispatcher can't carry Lua values
  through %s/%d/%u/%f format codes) even though the token form now
  works.
- New "Unit tokens (`nameplateN`)" subsection covering the resolved
  ~30 UnitX functions, creation-order semantics, suffix chains
  (`nameplate1target`, `targettarget`), and the SStrCmpI prefix-gate
  implementation detail.
- UnitTokenFromGUID's "post-1.12 omitted" list: nameplateN is no
  longer engine-unsupported (we hook the resolver) but it's still
  not used as a reverse-lookup key — indices can be reassigned
  during a session, so it'd be unstable.
- README: new Unit tokens table row pointing to the API.md anchor.

* return stable nameplate wrapper across calls

PushNamePlateFrame was building a fresh Lua table on every call for
unregistered (default vanilla) nameplates — so fields addons set on
the wrapper they received in NAME_PLATE_CREATED were lost on the
next GetNamePlateForUnit lookup. pfUI specifically writes
`plate.nameplate = styledButton` on the CREATED-delivered table to
hand its overlay back through our API, and that field was
disappearing.

Fix: pin the first wrapper for each nameplate pointer in the Lua
registry via luaL_ref, cache the refkey in g_wrapperRefkeys, and
rawgeti on every subsequent push. Same table identity for the life
of the engine plate. Bound by the CGNamePlateFrame pool size (~80).

Cleared on /reload via a new NamePlate::Info::PrepareForReload()
wired into FrameScript_Initialize_h next to the existing
Event::Custom and NameCache reload hooks — stale refkeys would
otherwise point into the freed registry.

* NAME_PLATE_UNIT events now fire token

* nameplate events: ship "nameplateN" tokens, delegate wrappers to engine

Two intertwined changes that together make nameplate-mod addons
(pfUI) survive pool recycle.

1. NAME_PLATE_UNIT_ADDED / _REMOVED arg1 is now the "nameplateN"
   unit token, matching modern WoW exactly. For REMOVED we compute
   the token before erasing from g_orderedGUIDs so the handler can
   still resolve the leaving unit. Addons that want a stable hash
   key call UnitGUID(arg1) to convert.

2. PushNamePlateFrame now delegates to the engine's own
   FrameScript_Object::ScriptRegister (0x00701BD0) instead of
   maintaining our own luaL_ref'd wrapper cache. Per-pointer the
   engine builds a wrapper table once, refs it into the registry,
   and stores the refkey at nameplate+0x08 - every code path
   (ours, addons' CreateFrame(parent) extraction, the engine's own
   internal pushers) then funnels through
   rawgeti(REGISTRY, nameplate+0x08) and gets the same Lua table.

The prior approach kept our own wrapper alongside the engine's
once pfUI's CreateFrame inside NAME_PLATE_CREATED caused the
engine to register the underlying frame itself - fields set on
our wrapper (plate.nameplate = decoratedButton) disappeared from
the API surface even though they still existed on the orphaned
table. Single-source-of-truth fixes it. The refcount-pinning
caveat CLAUDE.md warned about is benign for pool-managed
nameplates (never destroyed during a session).

Also clear seenPlates / lastTickPlates / orderedGUIDs on
FrameScript_Initialize so currently-visible plates refire CREATED
+ UNIT_ADDED after /reload - re-presents them to the freshly
reloaded UI, matching modern WoW.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-05-26 16:23:09 -05:00
Brues d5a45c73cb Addon: LinkUtil 2026-05-26 02:10:41 -05:00
23 changed files with 2017 additions and 39 deletions
+1
View File
@@ -27,3 +27,4 @@ Util\TimedCallback.lua
Util\EquipmentManager.lua
Util\PlayerUtil.lua
Util\SlashCommandsRegistry.lua
Util\LinkUtil.lua
+1 -1
View File
@@ -110,7 +110,7 @@ PLAYER_FACTION_COLORS =
{
[0] = PLAYER_FACTION_COLOR_HORDE,
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
};-- Quest
};
WOW_PROJECT_MAINLINE = 1;
WOW_PROJECT_CLASSIC = 2;
+4 -1
View File
@@ -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);
-6
View File
@@ -17,12 +17,6 @@
-- Secure-Templates system, so the protective wrappers in 3.3.5
-- reduce to a plain pcall.
--
-- Skipping if print is already defined keeps this file safe to load
-- alongside any other DLL or addon that might inject its own print.
if type(print) == "function" then
return
end
----------------------------------------------------------------------------
-- tostringall: convert every vararg to a string, preserving count.
+6 -1
View File
@@ -27,7 +27,12 @@ set(TINYCBOR_SOURCES
add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
set(CLASSICAPI_VERSION_VALUE 1)
# Dev-build sentinel — represents "newer than every plausible
# release". Encoded as v9999.99.99 = 9999 * 10000 + 99 * 100 + 99 =
# 99,999,999. Picked so addon-side feature gates of the form
# `CLASSIC_API_VERSION >= X` always pass during local development;
# CI release builds overwrite it via `-DCLASSICAPI_TAG=vX.Y.Z`.
set(CLASSICAPI_VERSION_VALUE 99999999)
set(CLASSICAPI_TAG
""
+16 -3
View File
@@ -23,22 +23,24 @@ 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.IsContainerItemOpenable`, `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` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [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` |
@@ -64,7 +66,7 @@ the glue state because GlueXML had no way to reach them otherwise.
| Group | Calls |
|-------|-------|
| CharacterOrder | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
@@ -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
+524 -20
View File
@@ -34,7 +34,9 @@ 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.IsContainerItemOpenable(containerIndex, slotIndex)`](#c_containeriscontaineritemopenablecontainerindex-slotindex)
- [`C_Container.PlayerHasHearthstone()`](#c_containerplayerhashearthstone)
- [`C_Container.UseHearthstone()`](#c_containerusehearthstone)
- [`C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)`](#c_containerswapitemssrcbag-srcslot-dstbag-dstslot)
@@ -73,6 +75,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 +96,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)
@@ -170,6 +179,7 @@ build instructions.
- [`C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`](#c_itemgetitemuniquenessitemlocation--c_itemgetitemuniquenessbyiditem)
- [`C_Item.GetItemLink(itemLocation)`](#c_itemgetitemlinkitemlocation)
- [`C_Item.GetItemInventoryType(itemLocation)` / `C_Item.GetItemInventoryTypeByID(item)`](#c_itemgetiteminventorytypeitemlocation--c_itemgetiteminventorytypebyiditem)
- [`C_Item.IsItemOpenable(itemLocation)`](#c_itemisitemopenableitemlocation)
- [`C_Item.IsLocked(itemLocation)`](#c_itemislockeditemlocation)
- [`C_Item.LockItem(itemLocation)`](#c_itemlockitemitemlocation)
- [`C_Item.LockItemByGUID(itemGUID)`](#c_itemlockitembyguiditemguid)
@@ -195,6 +205,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)
@@ -633,10 +650,6 @@ if UnitAffectingCombat("player") then
end
```
Equivalent to `UnitAffectingCombat("player")` but faster — reads the
`UNIT_FLAG_IN_COMBAT` bit directly off the player CGUnit's
m_objectFields, no token-resolution roundtrip.
## Container
### `C_Container.GetContainerItemID(bagIndex, slotIndex)`
@@ -756,6 +769,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
@@ -809,6 +878,27 @@ internally and counts slots that resolve to a null `CGItem *` (i.e.,
empty). Cross-checked in-game against a manual
`C_Container.GetContainerItemID` walk; counts match.
### `C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`
Positional-arg wrapper for [`C_Item.IsItemOpenable`](#c_itemisitemopenableitemlocation--c_itemisitemopenablebyiditem)
against a bag/slot pair. Same `(isOpenable, canOpen)` tuple — see
the linked section for full semantics. Both returns are `nil` for
empty slots or items whose data hasn't been cached yet.
```lua
-- Sweep main backpack for openables
for slot = 1, GetContainerNumSlots(0) do
if C_Container.IsContainerItemOpenable(0, slot) then
-- ...
end
end
```
Same cache-warming caveat as the underlying `C_Item.IsItemOpenable` —
a freshly-seen item may report `false` for one call while the
`SMSG_ITEM_QUERY_SINGLE` round-trips. Bag-resident items are almost
always already cached.
### `C_Container.PlayerHasHearthstone()`
Returns the itemID of the hearthstone if one is in the player's bags,
@@ -896,18 +986,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 +1050,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 +1671,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 +2174,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)`
@@ -2470,14 +2737,28 @@ lines; re-saving a set repopulates itemIDs.
### `CLASSIC_API_VERSION`
Defined once FrameScript has booted. Addons can use this to detect that
the DLL is loaded and which version is in use. The value is
`X*10000 + Y*100 + Z` for a tag of `vX.Y.Z` passed to CMake at configure
time via `-DCLASSICAPI_TAG=vX.Y.Z`. Unset versions resolve to `0`.
Defined once FrameScript has booted. Addons can use this to detect
that the DLL is loaded and which version is in use. The value is
`X*10000 + Y*100 + Z` for a tag of `vX.Y.Z` passed to CMake at
configure time via `-DCLASSICAPI_TAG=vX.Y.Z`.
Untagged builds (local dev, CI without a release tag) get a
**sentinel value of `99999999`** — encoded as `v9999.99.99`, chosen
to be higher than every plausible real release so addon-side
feature gates like `CLASSIC_API_VERSION >= 10200` don't reject the
dev build.
```lua
if CLASSIC_API_VERSION and CLASSIC_API_VERSION >= 10200 then
-- ClassicAPI v1.2.0 or newer is loaded
-- ClassicAPI v1.2.0 or newer is loaded (or untagged dev build)
end
```
If you specifically want to detect a dev build:
```lua
if CLASSIC_API_VERSION == 99999999 then
-- running against a locally-built, untagged DLL
end
```
@@ -3808,6 +4089,55 @@ if t == 1 then -- head
end
```
### `C_Item.IsItemOpenable(itemLocation)`
Returns two values: `(isOpenable, canOpen)`.
- **`isOpenable`** — `true` if the item type is right-click-openable
(sack, clam, simple chest, quest box, lockbox, etc. — anything
whose tooltip *could* show `<Right Click to Open>`). Intrinsic to
the item type. From `ItemStats.Flags & 0x4`.
- **`canOpen`** — `true` if the player can right-click *this specific
instance* and trigger the open action right now. For unlocked-by-
default items (clams, sacks) this matches `isOpenable`. For items
with a lock (lockboxes), it's only true after the lock has been
removed (rogue Pick Lock, key item, etc.) — so a priest looking at
a fresh lockbox sees `(true, false)`, while a rogue who just
picked it sees `(true, true)`.
Both values are `nil` when the item data isn't cached yet (background
`SMSG_ITEM_QUERY_SINGLE` fired) or when the location is empty /
invalid. Lets callers distinguish "data unknown" from "definitely
not openable".
There's no `ByID` variant — the `canOpen` check depends on a specific
instance's `ITEM_FIELD_FLAGS`, which you can only have via an
itemLocation. If you only have an itemID, use
`C_Item.GetItemInfoInstant` to inspect the item type instead.
```lua
-- Loop a bag and gather every openable the player can act on now
for slot = 1, GetContainerNumSlots(0) do
local _, canOpen = C_Container.IsContainerItemOpenable(0, slot)
if canOpen then
-- pickup + use; this slot is good to open
end
end
-- Show a different hint depending on the lock state
local isOpenable, canOpen = C_Item.IsItemOpenable({bagID = 0, slotIndex = 1})
if isOpenable and not canOpen then
print("This item is locked.")
end
```
Implementation: `isOpenable` reads `ItemStats.Flags & 0x4` from the
client-side ItemSparse cache. `canOpen` adds a check that either
`ItemStats.LockID == 0` (no lock) or the CGItem instance's
`ITEM_FIELD_FLAGS & 0x4` (UNLOCKED) bit is set. Same conditions the
engine's tooltip builder uses at `0x0052E323` to gate the
`ITEM_OPENABLE` line.
### `C_Item.IsLocked(itemLocation)`
Returns `true` if the item is in the client-side "in-transaction"
@@ -4325,6 +4655,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 +7017,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
+10
View File
@@ -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
+96
View File
@@ -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
@@ -847,6 +893,14 @@ enum Offsets {
// descriptor bytes for a worn-and-bound item.
OFF_DESCRIPTOR_FLAGS = 0x3C,
ITEM_FLAG_SOULBOUND = 0x01,
// Bit 2 — instance is "unlocked", i.e. the lock referenced by
// `ItemStats.LockID` has been opened (rogue Pick Lock, key item,
// etc.). For unlockable items, the engine gates the
// `<Right Click to Open>` tooltip on this bit: a fresh lockbox
// has it clear (priest sees no openable affordance), a picked
// lockbox has it set. Items with `LockID == 0` (clams, simple
// sacks) never need the bit.
ITEM_FLAG_UNLOCKED = 0x04,
// Client-side "in-transaction" lock — set when the engine puts an
// item on the cursor / mail-attaches it / trade-attaches it, cleared
@@ -1014,7 +1068,33 @@ enum Offsets {
OFF_ITEMSTATS_NAME = 0x08,
OFF_ITEMSTATS_DISPLAY_INFO_ID = 0x18,
OFF_ITEMSTATS_QUALITY = 0x1C, // u32 — 0=Poor … 5=Legendary
// `m_flags` — item-class flags bitfield. Bits we read:
// `ITEMSTATS_FLAG_OPENABLE` (`0x4`, bit 2): freely right-click-
// openable container (sack, clam, simple chest, quest box).
// The tooltip line `<Right Click to Open>` (`ITEM_OPENABLE`
// string) renders for items with this bit set, gated on the
// player's lock skill when the item also carries a non-zero
// LockID. Same bit modern WoW's `C_Item.IsItemOpenable`
// surfaces.
// Other documented bits (`0x1` = BIND_ON_PICKUP, `0x2` = CONJURED,
// `0x10` = LOOTABLE, `0x200` = WRAPPER, etc.) live in the same
// field but we don't read them here. Add named constants as
// consumers materialize.
// Reader at `0x0052E323` (the tooltip-builder's openable branch in
// `FUN_0052B650`) does `(ItemStats[+0x20] & 0x4)` against this
// field; verified by checking the field offset matches the
// 5-DWORD-per-row layout `puVar4[8]` resolves to in the decompile.
OFF_ITEMSTATS_FLAGS = 0x20,
ITEMSTATS_FLAG_OPENABLE = 0x4,
OFF_ITEMSTATS_SELL_PRICE = 0x28,
// `m_lockID` — references `Lock.dbc`. Zero means "no lock"; any
// nonzero value gates the openable interaction on the player
// successfully unlocking the item (instance flag bit 2 in
// `ITEM_FIELD_FLAGS`). Same field the tooltip builder reads as
// `ItemStats[+0x1AC]` (puVar4[0x6b]) when deciding whether the
// `<Right Click to Open>` line should require the lock to be
// picked first.
OFF_ITEMSTATS_LOCK_ID = 0x1AC,
OFF_ITEMSTATS_INVENTORY_TYPE = 0x2C,
OFF_ITEMSTATS_ITEM_LEVEL = 0x38, // u32 — base ilvl from ItemSparse
// `m_stackable` — max stack size for this item type (1 for non-
@@ -2089,6 +2169,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)
+55
View File
@@ -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
+49
View File
@@ -0,0 +1,49 @@
// 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.IsContainerItemOpenable(bag, slot)` — positional-arg
// wrapper around `Item::Openable::PushIsItemOpenable`. Same chain
// `C_Container.GetContainerItemDurability` and friends use:
// `ResolveBag` → `GetItemBySlot`. Returns the
// `(isOpenable, canOpen)` tuple — see [[item/Openable.h]].
#include "Game.h"
#include "item/Location.h"
#include "item/Openable.h"
namespace Container::Openable {
namespace {
int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L,
"Usage: C_Container.IsContainerItemOpenable(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::Openable::PushIsItemOpenable(
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::Openable
+39
View File
@@ -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
+44
View File
@@ -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
+113
View File
@@ -0,0 +1,113 @@
// 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_Item.IsItemOpenable(itemLocation)` — backport of the modern WoW
// accessor that says "right-clicking this item triggers an open/loot
// interaction" (clams, sacks, simple chests, quest boxes, lockboxes).
// Returns two values: `(isOpenable, canOpen)` — the first is the
// item-type-intrinsic openable flag, the second is whether the
// player can right-click *this specific instance* right now (lock
// is gone or never existed).
//
// Vanilla 1.12's tooltip builder gates the `<Right Click to Open>`
// string (`ITEM_OPENABLE`) on `ItemStats.Flags & 0x4` AND
// (`LockID == 0` OR `ITEM_FIELD_FLAGS & 0x4`). We mirror both checks
// — see `Offsets::ITEMSTATS_FLAG_OPENABLE`, `ITEMSTATS_LOCK_ID`, and
// `ITEM_FLAG_UNLOCKED`.
#include "Game.h"
#include "Offsets.h"
#include "item/Data.h"
#include "item/ID.h"
#include "item/Location.h"
#include "item/Openable.h"
#include <cstdint>
namespace Item::Openable {
namespace {
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
const uint8_t *PeekItemRecord(uint32_t itemID) {
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
const uint64_t zeroGuid = 0;
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
} // namespace
namespace {
// Reads `ITEM_FIELD_FLAGS` from the instance descriptor at +0x114.
// Defaults to 0 if `cgItem` is null or the descriptor pointer hasn't
// been populated yet.
uint32_t InstanceFlags(const uint8_t *cgItem) {
if (cgItem == nullptr)
return 0;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 0;
return *reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_FLAGS);
}
} // namespace
int PushIsItemOpenable(void *L, const uint8_t *cgItem) {
const int itemID = Item::ID::FromCGItem(cgItem);
if (itemID <= 0)
return 0;
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr) {
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
return 0;
}
const uint32_t typeFlags = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_FLAGS);
const uint32_t lockID = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_LOCK_ID);
const bool isOpenable = (typeFlags & Offsets::ITEMSTATS_FLAG_OPENABLE) != 0;
const uint32_t instFlags = InstanceFlags(cgItem);
const bool canOpen = isOpenable && (lockID == 0 ||
(instFlags & Offsets::ITEM_FLAG_UNLOCKED) != 0);
Game::Lua::PushBool(L, isOpenable);
Game::Lua::PushBool(L, canOpen);
return 2;
}
namespace {
int __fastcall Script_C_Item_IsItemOpenable(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::Error(L, "Usage: C_Item.IsItemOpenable(itemLocation)");
return 0;
}
return PushIsItemOpenable(L, Item::Location::Resolve(L, 1));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "IsItemOpenable",
&Script_C_Item_IsItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Item::Openable
+37
View File
@@ -0,0 +1,37 @@
// 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::Openable {
// Pushes two values for the openable check on a player-owned item
// (`isOpenable, canOpen`), returning 2. The two semantics:
//
// `isOpenable` — bool. The item type carries the openable flag
// (`ItemStats.Flags & 0x4` — clams, sacks, lockboxes, etc.).
// `canOpen` — bool. The player can right-click this specific
// instance and trigger the open action right now. True for
// openable items with no lock (`LockID == 0`) or whose instance
// has been unlocked (`ITEM_FIELD_FLAGS & 0x4` — set after a
// rogue picks the lock, a key is used, etc.). False for fresh
// lockboxes a non-rogue is looking at.
//
// `cgItem` may be null (empty slot), in which case nothing is pushed
// and 0 is returned (Lua sees nil for both returns). Uncached itemID
// also returns 0 plus a background cache warm.
int PushIsItemOpenable(void *L, const uint8_t *cgItem);
} // namespace Item::Openable
+45 -2
View File
@@ -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;
}
+253
View File
@@ -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
+265
View File
@@ -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
+121
View File
@@ -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
+127
View File
@@ -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
+31
View File
@@ -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
View File
@@ -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;
+153
View File
@@ -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