Compare commits

...
16 Commits
Author SHA1 Message Date
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
Brues a51225cb84 ranged auto-attack: detect via SPELL_ATTR_AUTO_REPEAT bit
Drop the hardcoded {75, 5019} ID list in favor of testing
Spell.dbc.Attributes bit 1. Auto Shot (Attr=0x00050012) and Shoot
wand (Attr=0x00000012) share that bit; on-next-swing Heroic Strike
and other ranged spells don't. Future-proofs against private
servers adding new auto-repeating spells.
2026-05-26 00:34:11 -05:00
Brues 6969fa77d1 create spellbook section 2026-05-26 00:24:10 -05:00
Brues 74b3765683 auto attack check spells 2026-05-26 00:19:28 -05:00
Brues 10df3f11c2 C_Spell.SpellHasRange 2026-05-26 00:12:47 -05:00
Brues e2d7b0f555 C_Spell.IsSelfBuff 2026-05-26 00:05:43 -05:00
Brues 209a57a8d6 C_Spell.GetSpellSubtext 2026-05-26 00:02:04 -05:00
Brues acb0456610 C_Spell.GetSchoolString 2026-05-25 23:58:10 -05:00
Brues e47f3aacb2 C_Spell.DoesSpellExist 2026-05-25 23:49:29 -05:00
Brues d718530a7a GetItemUniquenessByID 2026-05-25 23:42:10 -05:00
Brues 55d4e1856f GetStackCount/GetItemUniqueness 2026-05-25 23:37:39 -05:00
Brues 68771506a8 IsInRaid/IsInGroup 2026-05-25 23:37:14 -05:00
Brues d0dc8c1965 generate notes for new releases 2026-05-25 15:13:04 -05:00
22 changed files with 2337 additions and 145 deletions
+1
View File
@@ -45,3 +45,4 @@ jobs:
ClassicAPI-Bundle-${{ github.ref_name }}.zip
ClassicAPI-AddOnOnly-${{ github.ref_name }}.zip
ClassicAPI-${{ github.ref_name }}/ClassicAPI.dll
generate_release_notes: true
+1
View File
@@ -27,3 +27,4 @@ Util\TimedCallback.lua
Util\EquipmentManager.lua
Util\PlayerUtil.lua
Util\SlashCommandsRegistry.lua
Util\LinkUtil.lua
+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);
+14 -3
View File
@@ -35,14 +35,16 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [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.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.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.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellTexture`, `C_Spell.IsCurrentSpell`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `CancelSpellByName`, `CastSpellNoToggle`, `FindSpellBookSlotByID`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsMounted`, `IsStealthed`, `IsSwimming` |
| [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` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsMounted`, `IsStealthed`, `IsSwimming` |
| [Table](docs/API.md#table) | `table.wipe` |
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
@@ -93,6 +95,9 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| `HEARTHSTONE_BOUND` | *(none)* |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
| `NAME_PLATE_CREATED` | `nameplateFrame` |
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
| `PLAYER_STARTED_LOOKING` | *(none)* |
| `PLAYER_STOPPED_LOOKING` | *(none)* |
| `PLAYER_STARTED_MOVING` | *(none)* |
@@ -115,6 +120,12 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` |
| Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` |
### Unit tokens
| Token | Resolves to |
|-------|-------------|
| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). |
## Installation
Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the
+639 -134
View File
@@ -73,6 +73,7 @@ build instructions.
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
- [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event)
- [`NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
@@ -166,6 +167,8 @@ build instructions.
- [`C_Item.GetCurrentItemLevel(itemLocation)` / `C_Item.GetDetailedItemLevelInfo(item)`](#c_itemgetcurrentitemlevelitemlocation--c_itemgetdetaileditemlevelinfoitem)
- [`GetAverageItemLevel()`](#getaverageitemlevel)
- [`C_Item.GetItemMaxStackSize(itemLocation)` / `C_Item.GetItemMaxStackSizeByID(item)`](#c_itemgetitemmaxstacksizeitemlocation--c_itemgetitemmaxstacksizebyiditem)
- [`C_Item.GetStackCount(itemLocation)`](#c_itemgetstackcountitemlocation)
- [`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.IsLocked(itemLocation)`](#c_itemislockeditemlocation)
@@ -193,6 +196,13 @@ build instructions.
- [`C_MerchantFrame.IsMerchantItemRefundable(slot)`](#c_merchantframeismerchantitemrefundableslot)
- [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled)
- [NamePlate](#nameplate)
- [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates)
- [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids)
- [`C_NamePlate.GetNamePlateForUnit(unitToken)`](#c_nameplategetnameplateforunitunittoken)
- [`C_NamePlate.GetNamePlateForGUID(guidString)`](#c_nameplategetnameplateforguidguidstring)
- [Unit tokens (`nameplateN`)](#unit-tokens-nameplaten)
- [NameCache](#namecache)
- [`GetPlayerInfoByGUID(guid)`](#getplayerinfobyguidguid)
- [`C_PlayerCache.GetPlayerInfoByName(name)`](#c_playercachegetplayerinfobynamename)
@@ -214,15 +224,17 @@ build instructions.
- [`GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])`](#getquestlogleaderboardidobjectiveindex--questindex)
- [Spell](#spell)
- [`C_Spell.DoesSpellExist(spellID)`](#c_spelldoesspellexistspellid)
- [`C_Spell.GetSchoolString(schoolMask)`](#c_spellgetschoolstringschoolmask)
- [`GetSpellInfo(spellID)` / `GetSpellInfo(slot, bookType)`](#getspellinfospellid--getspellinfoslot-booktype)
- [`C_Spell.GetSpellInfo(spellID)`](#c_spellgetspellinfospellid)
- [`C_Spell.GetSpellName(spellID)`](#c_spellgetspellnamespellid)
- [`C_Spell.GetSpellTexture(spellID)`](#c_spellgetspelltexturespellid)
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
- [`GetSpellLink(spellID)` / `GetSpellLink(slot, bookType)`](#getspelllinkspellid--getspelllinkslot-booktype)
- [`C_Spell.GetSpellLink(spellID)`](#c_spellgetspelllinkspellid)
- [`C_Spell.GetSpellDescription(spellID)`](#c_spellgetspelldescriptionspellid)
- [`C_Spell.GetSpellReagents(spellID)`](#c_spellgetspellreagentsspellid)
- [`C_Spell.GetSpellSubtext(spellIdentifier)`](#c_spellgetspellsubtextspellidentifier)
- [`IsPassiveSpell(spellID)` / `IsPassiveSpell(slot, bookType)`](#ispassivespellspellid--ispassivespellslot-booktype)
- [`C_Spell.IsSpellPassive(spellID)`](#c_spellisspellpassivespellid)
- [`IsPlayerSpell(spellID)`](#isplayerspellspellid)
@@ -231,15 +243,24 @@ build instructions.
- [`C_Spell.IsSpellUsable(spellID)`](#c_spellisspellusablespellid)
- [`C_Spell.GetSpellCooldown(spellIdentifier)`](#c_spellgetspellcooldownspellidentifier)
- [`C_Spell.IsCurrentSpell(spellIdentifier)`](#c_spelliscurrentspellspellidentifier)
- [`C_Spell.IsSelfBuff(spellID)`](#c_spellisselfbuffspellid)
- [`C_Spell.SpellHasRange(spellIdentifier)` / `SpellHasRange(slot, bookType)`](#c_spellspellhasrangespellidentifier--spellhasrangeslot-booktype)
- [`C_Spell.IsAutoAttackSpell(spellID)`](#c_spellisautoattackspellspellid)
- [`C_Spell.IsRangedAutoAttackSpell(spellID)`](#c_spellisrangedautoattackspellspellid)
- [`IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`](#isharmfulspellspell--ishelpfulspellspell)
- [`C_Spell.IsSpellHarmful(spellID)` / `C_Spell.IsSpellHelpful(spellID)`](#c_spellisspellharmfulspellid--c_spellisspellhelpfulspellid)
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
- [`C_SpellBook.GetSpellSkillLine(spellID)`](#c_spellbookgetspellskilllinespellid)
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
- [SpellBook](#spellbook)
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
- [`C_SpellBook.GetSpellSkillLine(spellID)`](#c_spellbookgetspellskilllinespellid)
- [`C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)`](#c_spellbookisautoattackspellbookitemslot-booktype)
- [`C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)`](#c_spellbookisrangedautoattackspellbookitemslot-booktype)
- [State](#state)
- [`IsMounted()`](#ismounted)
- [`Dismount()`](#dismount)
@@ -247,6 +268,8 @@ build instructions.
- [`IsFalling()`](#isfalling)
- [`IsSwimming()`](#isswimming)
- [`IsAssistingRitual()`](#isassistingritual)
- [`IsInGroup()`](#isingroup)
- [`IsInRaid()`](#isinraid)
- [`GetMirrorTimerInfo(index)` / `GetMirrorTimerProgress(label)`](#getmirrortimerinfoindex--getmirrortimerprogresslabel)
- [`GetShapeshiftFormID()`](#getshapeshiftformid)
- [`CancelShapeshiftForm()`](#cancelshapeshiftform)
@@ -881,18 +904,29 @@ C_Container.SwapItems(1, 3, 0, 7)
C_Container.SwapItems(0, 1, 0, 2)
```
Bag IDs match the rest of `C_Container.*`: `0` is the backpack, `1..4`
are the equipped bags. Slots are 1-based. The engine accepts both
directions:
Bag IDs match the rest of `C_Container.*`:
- Same container (e.g. backpack ↔ backpack, bag1 ↔ bag1) sends opcode
`0x10D` `CMSG_SWAP_INV_ITEM` with the two linear slot bytes.
- Cross-container (e.g. backpack ↔ bag1, bag2 ↔ bag3) sends opcode
`0x10C` `CMSG_SWAP_ITEM` with `(srcBag, srcSlot, dstBag, dstSlot)`.
- `0` — backpack
- `1..4` — equipped bags
- `-1` — main bank (24 slots; **requires the bank window to be open**)
- `5..10` — equipped bank bags (**requires the bank window to be open**)
Slots are 1-based. The engine accepts both directions:
- Same container (e.g. backpack ↔ backpack, bag1 ↔ bag1, bank ↔ bank)
sends opcode `0x10D` `CMSG_SWAP_INV_ITEM` with the two linear slot bytes.
- Cross-container (e.g. backpack ↔ bag1, bag2 ↔ bank, bank ↔ bank bag)
sends opcode `0x10C` `CMSG_SWAP_ITEM` with `(srcBag, srcSlot, dstBag, dstSlot)`.
Destination empty becomes a move; destination occupied becomes an
atomic swap.
> **Bank slots require the bank window to be open.** The engine
> doesn't sync bank-side `CGContainer`s or bank inventory entries
> before the first bank interaction, so any bank `EncodeBagSlot`
> path returns false until then. Same constraint as
> `C_Container.GetContainerItemInfo` on bank IDs.
> **ClassicAPI-only.** Modern Classic Era has no direct swap-two-slots
> call — addons there drive the cursor with two `PickupContainerItem`
> calls in sequence. This single-call form bypasses the cursor
@@ -934,6 +968,9 @@ consolidate partial stacks — `SwapItems` swaps the whole slots, which
isn't what you want when both slots hold the same item but one has a
partial stack.
Bank IDs (`-1`, `5..10`) work here too with the same bank-window
constraint as `SwapItems`.
> **vs. the modern equivalent.** Modern Classic Era has no direct
> one-call move — addons there string `SplitContainerItem(bag, slot,
> count)` + `PickupContainerItem(dstBag, dstSlot)` together to drive
@@ -1552,6 +1589,66 @@ renderer-state changes that recreate WoW's main window (e.g. toggling
vertical sync), where an `SetWindowLongPtr`-style `WNDPROC` subclass
would be left dangling.
### `NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events
Fire when nameplate state actually changes. Payloads:
| Event | `arg1` | Notes |
|-------|--------|-------|
| `NAME_PLATE_CREATED` | nameplate **Frame** | Matches modern WoW. Fires once per unique `CGNamePlateFrame` pointer — same frame re-used via pool recycle does NOT refire. |
| `NAME_PLATE_UNIT_ADDED` | `"nameplateN"` **unit token** | Matches modern WoW. Pass straight to `UnitName` / `UnitGUID` / `UnitClass` / etc., or to [`GetNamePlateForUnit`](#c_nameplategetnameplateforunitunittoken) for the frame. The token is positional — see [Unit tokens](#unit-tokens-nameplaten) for ordering semantics. |
| `NAME_PLATE_UNIT_REMOVED` | `"nameplateN"` **unit token** | Same as above. Computed from the plate's slot *before* it shifts out of the ordered list, so the token still resolves to the leaving unit during the event handler. |
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("NAME_PLATE_CREATED")
f:RegisterEvent("NAME_PLATE_UNIT_ADDED")
f:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
f:SetScript("OnEvent", function()
if event == "NAME_PLATE_CREATED" then
-- arg1 = the nameplate Frame itself
arg1:SetAlpha(0.8)
elseif event == "NAME_PLATE_UNIT_ADDED" then
-- arg1 = "nameplate1" / "nameplate2" / ...
local name = UnitName(arg1)
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
-- ... style based on the unit ...
end
end)
```
> **CREATED timing with nameplate addons (pfUI / TidyPlates / etc).**
> The event fires when the *engine* allocates the underlying
> `CGNamePlateFrame`. Nameplate-mod addons typically decorate the
> frame on their own per-frame update — so `arg1` at `CREATED` time
> is a bare frame with no addon-side decorations yet. For
> unit-specific work after the addon has decorated, use
> `NAME_PLATE_UNIT_ADDED` (fires next tick at the latest) or fetch
> the current frame on-demand via `GetNamePlateForUnit(arg1)`.
> **Token stability gotcha.** Like modern WoW, the `arg1` token is
> positional — `"nameplate3"` today may resolve to a different unit
> after the slot vacates and shifts. If you need a per-unit hash key
> for cross-event bookkeeping, call `UnitGUID(arg1)` and store the
> GUID instead. See [Unit tokens](#unit-tokens-nameplaten) for the
> ordering rules.
**Implementation notes**
Detected by per-frame polling, not engine hooks. Each world tick we
walk the object hash for nameplated units and diff against the
previous tick's snapshot. Modern WoW also synthesizes these via
diffing (the underlying engine has no event for "plate state
changed"). The cost is ~20-50µs/frame even in busy raids — well
below noise.
The diff approach absorbs the engine's transient hide/reshow cycle:
vanilla has ~7 code paths that briefly zero `unit + 0xE60` (z-order
rebuilds, anchor changes, flag-change re-eval) and the next frame's
show path re-allocates from the pool. Those transient zeroes never
become events because the unit appears in both the previous and
current tick's snapshot.
### `QUEST_ACCEPTED` event
Fires once per quest the player just accepted, with two payload args:
@@ -3698,6 +3795,50 @@ Single `uint32` read at cache record `+0x60` (`m_stackable`). By-ID
form fires a background cache fill on miss and returns nil; re-call
after `GET_ITEM_INFO_RECEIVED`.
### `C_Item.GetStackCount(itemLocation)`
Returns the **current** stack count in a specific slot — distinct
from `C_Item.GetItemCount(item)`, which sums every stack of that
itemID across the player's inventory. Useful when an addon needs to
know "this specific stack has 13/20", not "I have 47 total".
```lua
local n = C_Item.GetStackCount({bagID = 0, slotIndex = 1})
-- 13 (the bag's first slot has 13 of whatever item)
```
Reads `ITEM_FIELD_STACK_COUNT` directly off the item's
`m_objectFields` — same field `GetContainerItemInfo` returns as
`itemCount`. Returns `0` for empty / unresolvable locations.
### `C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`
Returns `(maxAllowed, category, equippedCount)` — the item's
uniqueness cap, category name, and current equipped-of-category
count.
| Field | Vanilla source |
|-------|----------------|
| `maxAllowed` | `ItemStats_C.m_maxCount` — `0` = unlimited, `1` = unique ("Unique" tag in tooltip), higher = inventory cap |
| `category` | Always `nil` — vanilla's client has no unique-equipped category system (added in TBC). |
| `equippedCount` | Always `0` — without categories there's nothing meaningful to count against. |
```lua
local max = C_Item.GetItemUniqueness({equipmentSlotIndex = 13})
-- max = 1 for typical trinkets (Unique)
local max = C_Item.GetItemUniquenessByID(2943)
-- max = 1 (Eye of Paleth — "Unique")
```
The by-ID form skips item-location resolution and reads `ItemStats_C`
directly. Fires a background cache fill on miss and returns nil;
re-call after `GET_ITEM_INFO_RECEIVED`.
Modern WoW returns the same 3-tuple but with the category half
populated for unique-equipped categories (e.g. "Brewfest Mug",
"Heart of Azeroth"). On this backport, only `maxAllowed` carries
information.
### `C_Item.GetItemLink(itemLocation)`
Returns the fully-decorated per-instance hyperlink for the item at
@@ -4266,6 +4407,174 @@ disable the sell-all-junk button; vanilla has no such setting, so
the feature is always on. Function exists so retail addons that
gate `SellAllJunkItems` on this don't no-op silently.
## NamePlate
Modern `C_NamePlate.*` returns nameplate `Frame` objects keyed off
unit data. Vanilla 1.12 doesn't ship the API at all — but the
underlying data (per-unit nameplate pointer at `CGUnit + 0xE60`)
exists. We enumerate visible units via the local-player-anchored
object hash table, filter by `TYPEMASK_UNIT`, and return matches.
Modern's `"nameplateN"` unit-token family is also supported — see
[Unit tokens](#unit-tokens-nameplaten) below. `NAME_PLATE_UNIT_ADDED`
/ `_REMOVED` / `_CREATED` events fire via a per-tick visible-plate
diff (see [the events section](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)).
### `C_NamePlate.GetNamePlates()`
Returns a 1-based table of nameplate `Frame` objects — one per
CGUnit that currently has an allocated **Lua-registered** nameplate.
The frames are real Lua tables with methods (`:GetName()`,
`:GetWidth()`, `:SetAlpha()`, etc.) and any addon-added decorations
on them.
```lua
local plates = C_NamePlate.GetNamePlates()
for i, plate in ipairs(plates) do
print(i, plate:GetName(), plate:GetWidth())
end
```
Two kinds of plates can show up:
- **Addon-created plates** (pfUI, TidyPlates, NamePlateMod, etc.):
registered with Lua via `CreateFrame`, so each has a real
registry ref. We push `registry[plate + 0x08]`. Identity is
stable across calls — caching is safe while the frame is alive.
- **Default vanilla plates**: created internally by the engine
without ever calling `CreateFrame`. Their `+0x08` field holds the
sentinel `LUA_NOREF` (`-2`), not a real registry key. We build a
fresh wrapper table per call (`{[0] = lightuserdata(plate)}` with
the global `__framescript_meta` metatable) so addons get the
same method surface. The wrapper isn't cached engine-side, so
identity isn't stable across calls — don't compare wrappers, and
don't store them across the unit going out of range (the
underlying frame may be freed). Call `GetNamePlates()` fresh
each time you need plates.
### Reading region content from a default nameplate
Vanilla plates have six child regions in stable positions. Walk them
with `:GetRegions()`:
```lua
local plates = C_NamePlate.GetNamePlates()
for _, plate in ipairs(plates) do
local regions = {plate:GetRegions()}
local name = regions[3]:GetText() -- e.g. "Joseph Dalton"
local level = tonumber(regions[4]:GetText()) -- e.g. 60
-- regions[1], [2], [5], [6] are textures (border, healthbar,
-- glow, raid-icon — order depends on the engine's draw order)
end
```
Lua 5.0 has no `select()`, so collect into a table via
`{plate:GetRegions()}` and index. Addon-created plates have
different region layouts — those frames inherit whatever shape the
addon built, not this one.
### `C_NamePlate.GetNamePlateForUnit(unitToken)`
Returns the nameplate Frame for a single unit (resolved via the
engine's token-to-GUID path, so out-of-range party/raid members
work too), or `nil` if the unit has no allocated nameplate.
```lua
local plate = C_NamePlate.GetNamePlateForUnit("target")
if plate then
local regions = {plate:GetRegions()}
print("targeting:", regions[3]:GetText()) -- e.g. "Santora"
end
```
Same registered-vs-fresh-wrapper behavior as `GetNamePlates()` —
addon-created plates return their cached wrapper, default vanilla
plates get a fresh per-call wrapper. Don't cache the result across
the unit going out of range.
### `C_NamePlate.GetNamePlateForGUID(guidString)`
Same as `GetNamePlateForUnit` but takes the `"0xHHHHHHHHHHHHHHHH"`
GUID-string form. Useful when you've stored a unit GUID across
events (e.g., converted the positional `"nameplateN"` token to a
GUID via `UnitGUID(arg1)` at `NAME_PLATE_UNIT_ADDED` time) and need
the frame later.
```lua
local platesByGuid = {}
local f = CreateFrame("Frame")
f:RegisterEvent("NAME_PLATE_UNIT_ADDED")
f:SetScript("OnEvent", function()
-- arg1 = "nameplateN" token; convert to stable GUID for storage
local guid = UnitGUID(arg1)
platesByGuid[guid] = C_NamePlate.GetNamePlateForGUID(guid)
end)
```
Returns `nil` if the GUID doesn't parse, doesn't resolve to a
visible CGUnit, or the unit has no allocated nameplate.
### `C_NamePlate.GetNamePlateGUIDs()`
Returns a 1-based table of GUID strings (modern
`"0xHHHHHHHHHHHHHHHH"` format) — one per CGUnit with an allocated
nameplate, **regardless** of whether the frame has been registered
with Lua. Catches default vanilla nameplates that
[`GetNamePlates`](#c_nameplategetnameplates) can't surface as
frames.
```lua
/dump C_NamePlate.GetNamePlateGUIDs()
-- { "0xF13000C36C26FD02", "0xF130000009276912", ... }
```
Walks the local-player-anchored object hash table for `TYPEMASK_UNIT`
entries, filters by `*(unit + 0xE60) != nullptr`. The per-unit
nameplate pointer is set by `FUN_006086E0`'s "show nameplate" path
regardless of which nameplate system rendered it. Order follows
hash-bucket iteration and isn't stable across calls.
### Unit tokens (`nameplateN`)
`"nameplate1"`, `"nameplate2"`, … work as unit tokens against every
`UnitX` function: `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`,
`UnitHealthMax`, `UnitLevel`, `UnitFaction`, `UnitReaction`,
`UnitExists`, `UnitIsPlayer`, `UnitIsEnemy`, `UnitIsDead`, etc. —
~30 functions for free.
```lua
for i = 1, 40 do
if not UnitExists("nameplate" .. i) then break end
print(i, UnitName("nameplate" .. i), UnitClass("nameplate" .. i))
end
```
Ordering is **creation-order** — each new plate appends to the end of
the list and stays at its index until the unit goes out of range or
the plate is removed, at which point later indices shift down.
Stable for the lifetime of a single plate; no mid-frame reordering.
Same semantics as modern WoW.
Token chains work too — `"nameplate1target"`, `"nameplate1targettarget"`,
etc. — by mirroring the engine's own `targettarget`-style suffix
walker (read `UNIT_FIELD_TARGET` off `m_objectFields`, loop). Other
suffixes (`pet`, `master`) aren't supported by the vanilla engine's
own walker either, so they don't compose.
Out-of-range indices return `nil` cleanly without raising "Unknown
unit name" — `UnitExists("nameplate99")` just returns `false`.
**Implementation note.** We hook `FUN_TOKEN_TO_GUID` (the central
token→GUID resolver) so the entire `Script_Unit*` surface gains the
new token form transparently. The hook is gated by an `SStrCmpI`
prefix check against `"nameplate"`; non-nameplate tokens
(`"player"`, `"target"`, `"partyN"`, etc.) fall straight through to
the unmodified resolver. The ordered list is maintained alongside
the existing `NAME_PLATE_UNIT_ADDED` / `_REMOVED` diff in the
per-tick nameplate walker.
## NameCache
GUID-keyed cache of player names and classes. The engine itself
@@ -4856,6 +5165,50 @@ A separate function keeps the existing call wire-compatible.
> `(slot, bookType)` overload — only the `C_Spell.*` namespace is
> broadened.
### `C_Spell.DoesSpellExist(spellID)`
Returns `true` if `spellID` resolves to a populated `Spell.dbc`
record, `false` otherwise. Cheap pre-flight check for any of the
spellID-keyed `C_Spell.*` calls — if this returns `false`, the rest
of the family will return nil/empty.
```lua
if C_Spell.DoesSpellExist(133) then
local info = C_Spell.GetSpellInfo(133) -- safe
end
```
### `C_Spell.GetSchoolString(schoolMask)`
Returns the localized name for a damage-school bitmask. Resolves
single-bit masks via the engine's `SPELL_SCHOOL<n>_CAP` global
strings (`SPELL_SCHOOL0_CAP` = "Physical", `SPELL_SCHOOL2_CAP` =
"Fire", etc.).
```lua
C_Spell.GetSchoolString(4) -- "Fire"
C_Spell.GetSchoolString(32) -- "Shadow"
```
Vanilla spells are single-school — the multi-school combos
("Frostfire", "Shadowflame", "Spellstorm", etc.) were TBC-and-later
additions. Any multi-bit mask returns `"Unknown"`, matching the
modern API's documented fallback for unnamed combinations.
> Lua 5.0 doesn't support hex literals (`0x04` is a syntax error);
> pass mask values as decimals, or via `tonumber("0x04", 16)` if you
> want to keep hex notation in source.
| Decimal | Hex | School |
|--------:|-----|--------|
| `1` | `0x01` | Physical |
| `2` | `0x02` | Holy |
| `4` | `0x04` | Fire |
| `8` | `0x08` | Nature |
| `16` | `0x10` | Frost |
| `32` | `0x20` | Shadow |
| `64` | `0x40` | Arcane |
### `GetSpellInfo(spellID)` / `GetSpellInfo(slot, bookType)`
Returns the same nine values as 3.3.5's `GetSpellInfo`, **plus a 10th
@@ -4966,36 +5319,6 @@ local path = C_Spell.GetSpellTexture(133)
-- path = "Interface\\Icons\\Spell_Fire_FlameBolt"
```
### `FindSpellBookSlotByID(spellID)`
Inverse of 1.12's `GetSpellName(slot, bookType)`. Given a spellID,
returns the 1-based slot it occupies in the player or pet spellbook,
along with the matching bookType so the result feeds directly into
slot-and-bookType APIs (`GetSpellName`, `GetSpellTexture`,
`GameTooltip:SetSpell`, etc.).
```
slot, bookType = FindSpellBookSlotByID(spellID)
```
- Returns `(slot, "spell")` if the spell is in the player spellbook.
- Returns `(slot, "pet")` if it's only in the pet spellbook.
- Returns `nil` if the spellID isn't currently in either book.
Player book is searched first, so if a spell somehow appeared in both
(unusual but possible for special pet-shared abilities), the player
slot wins.
```lua
local slot, book = FindSpellBookSlotByID(133)
if slot then
GameTooltip:SetSpell(slot, book) -- shows full caster-scaled tooltip
end
```
Equivalent to the legacy function of the same name introduced in 3.0
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
### `GetSpellLink(spellID)` / `GetSpellLink(slot, bookType)`
Returns the chat-style spell hyperlink and the spellID:
@@ -5096,6 +5419,23 @@ documented in some emulator sources places these at `+0x110` /
`+0x130`, which is wrong for 1.12.1). Iteration stops at the first
empty slot, matching how the engine walks its own reagent loop.
### `C_Spell.GetSpellSubtext(spellIdentifier)`
Returns the localized "Rank N" / "Passive" / "Racial Passive" /
similar string that appears below the spell's name in the
spellbook. Read directly from `Spell.dbc.Rank[9]` at record `+0x204`
(same locale array shape as `Name[9]` two fields prior).
Accepts the modern `SpellIdentifier` shape — `spellID`, name, name
with rank (`"Fireball(Rank 3)"`), or `|Hspell:N|h` hyperlink. Returns
`nil` for unrecognized input or spells with no rank text.
```lua
C_Spell.GetSpellSubtext(133) -- "Rank 1" (lowest-rank Fireball)
C_Spell.GetSpellSubtext(25306) -- "Rank 12" (max-rank Fireball)
C_Spell.GetSpellSubtext("Frost Armor") -- "Rank 1"
```
### `IsPassiveSpell(spellID)` / `IsPassiveSpell(slot, bookType)`
Returns `true` if the spell is passive (no cast bar — applies its effect
@@ -5307,6 +5647,78 @@ on this. Reads three engine slots and returns true on any match:
covers channeled abilities (Fishing, Drain Soul, Ritual of
Summoning, etc.).
### `C_Spell.IsSelfBuff(spellID)`
Returns `true` if every active effect on the spell targets only the
caster — every effect's implicit target (A and B) must be either
`TARGET_NONE` (0) or `TARGET_SELF` (1).
```lua
C_Spell.IsSelfBuff(1006) -- Inner Fire → true
C_Spell.IsSelfBuff(7302) -- Frost Armor → true
C_Spell.IsSelfBuff(1459) -- Arcane Intellect → false (targets friendly)
C_Spell.IsSelfBuff(133) -- Fireball → false (targets enemy)
```
Walks `Spell.dbc.EffectImplicitTargetA[3]` and
`EffectImplicitTargetB[3]`. Spells with no active effects (a
degenerate case for normal content) return `false`.
### `C_Spell.SpellHasRange(spellIdentifier)` / `SpellHasRange(slot, bookType)`
Returns `true` if the spell has a non-zero min or max range — i.e.
it's a targeted ability you cast at a unit / location rather than a
self-cast (which has range `0`).
```lua
C_Spell.SpellHasRange(133) -- Fireball → true (40yd)
C_Spell.SpellHasRange(1006) -- Inner Fire → false (self buff)
SpellHasRange(10, "spell") -- 10th spellbook slot in player book
```
Looks up `Spell.dbc.RangeIndex` (`+0x90`) → `SpellRange.dbc` row,
then tests `minRange > 0 or maxRange > 0`. Vanilla 1.12 doesn't ship
this function at all; 3.3.5+ added it as the `(spellIdentifier)`
form only. We expose both the modern namespaced form (any
`SpellIdentifier`) and a positional `SpellHasRange(slot, bookType)`
matching the dual-signature shape used elsewhere in this backport
(`GetSpellInfo`, `GetSpellLink`, etc.). The `bookType` argument
follows the same convention as `GetSpellName(slot, bookType)`:
`"spell"` (or any non-pet value) → player book, `"pet"` → pet book.
### `C_Spell.IsAutoAttackSpell(spellID)`
Returns `true` if the spell is the melee auto-attack — spell ID
`6603` ("Auto Attack"), used by every class.
```lua
C_Spell.IsAutoAttackSpell(6603) -- true
C_Spell.IsAutoAttackSpell(133) -- false
```
See [`C_SpellBook.IsAutoAttackSpellBookItem`](#c_spellbookisautoattackspellbookitemslot-booktype)
for the spellbook-slot variant.
### `C_Spell.IsRangedAutoAttackSpell(spellID)`
Returns `true` if the spell is one of the two ranged auto-attacks —
spell `75` (Hunter "Auto Shot") or spell `5019` ("Shoot" wand).
```lua
C_Spell.IsRangedAutoAttackSpell(75) -- true (Auto Shot)
C_Spell.IsRangedAutoAttackSpell(5019) -- true (Shoot wand)
C_Spell.IsRangedAutoAttackSpell(6603) -- false (melee)
```
Tests `Spell.dbc.Attributes & 0x02` (the `SPELL_ATTR_AUTO_REPEAT`
flag) — verified empirically against in-game spell data. Auto Shot
(`0x00050012`) and Shoot wand (`0x00000012`) share bit 1; Heroic
Strike (`0x00050014`) and other ranged abilities don't. Naturally
covers any future auto-repeating spell a private server might add.
See [`C_SpellBook.IsRangedAutoAttackSpellBookItem`](#c_spellbookisrangedautoattackspellbookitemslot-booktype)
for the spellbook-slot variant.
### `C_Item.GetWeaponEnchantInfo()`
Returns the modern 12-tuple matching WotLK+'s extended
@@ -5395,95 +5807,6 @@ C_Spell.IsSpellHelpful(2061) -- true (Flash Heal)
Equivalent to `C_Spell.IsSpellHarmful` / `C_Spell.IsSpellHelpful`
introduced in 11.x.
### `C_SpellBook.GetSpellLevelLearned(spellID)`
Returns the level at which a spell becomes available — the
`BaseLevel` field in `Spell.dbc` (record `+0x70`). Direct read off
the cached record; no class/race filtering.
```lua
C_SpellBook.GetSpellLevelLearned(133) -- Fireball rank 1 → 4
C_SpellBook.GetSpellLevelLearned(25306) -- Fireball rank 12 → 60
C_SpellBook.GetSpellLevelLearned(2061) -- Flash Heal rank 1 → 20
```
Returns `0` for invalid spellIDs, spells with no level requirement
(most non-class utility spells), or records the engine hasn't
cached. Matches modern semantics — unknown / utility spells
return 0 rather than nil.
### `C_SpellBook.GetCurrentLevelSpells([level])`
Returns a 1-based table of spellIDs the player's class/race can
learn at the given level. Without arguments, defaults to the
player's current level.
```lua
C_SpellBook.GetCurrentLevelSpells() -- spells trainable at your level
C_SpellBook.GetCurrentLevelSpells(20) -- preview what's available at 20
C_SpellBook.GetCurrentLevelSpells(60) -- preview at 60
```
Walks `SkillLineAbility.dbc` filtering by the player's class bit
(`1 << (classID - 1)`) and race bit, respecting both the include
masks (entries with mask = 0 are "all classes"/"all races", which
also pass) and the exclude masks. For each surviving entry, looks
up the spell's `BaseLevel` and includes it if it matches the
queried level.
> **Vanilla is trainer-driven.** Modern `GetCurrentLevelSpells`
> (added in 5.x when trainers were removed) returns *auto-learned*
> spells. Vanilla 1.12 requires visiting a trainer to actually
> learn most class spells. We return the closest available analog:
> **what's *trainable* at this level**. Useful for "what's new
> this level" UI panels and level-preview tooling ported from MoP+.
Class/race come from the local player — there's no
`(class, race, level)` form because vanilla doesn't expose a
clean class-string→classID lookup. Returns an empty table at
character select / pre-login (no CGPlayer yet) and for levels
where no class/race spells match.
### `C_SpellBook.GetSpellSkillLine(spellID)`
Returns `(name, skillID)` — the SkillLine.dbc row that the given spell
belongs to. The name is the user-facing skill/tab string in the engine's
current locale: spellbook tab names for class spells (`"Protection"`,
`"Fire"`, `"Holy"`), profession headers (`"Tailoring"`, `"Engineering"`),
weapon-skill rows (`"Swords"`, `"Bows"`), etc.
```lua
C_SpellBook.GetSpellSkillLine(1671) -- "Protection", 26 (Shield Bash)
C_SpellBook.GetSpellSkillLine(133) -- "Fire", 8 (Fireball)
C_SpellBook.GetSpellSkillLine(2050) -- "Holy", 56 (Lesser Heal)
C_SpellBook.GetSpellSkillLine(2480) -- "Bows", 45 (Shoot Bow)
C_SpellBook.GetSpellSkillLine(3273) -- "First Aid", 129 (Anesthetic)
```
Walks `SkillLineAbility.dbc` for any row whose `spellID` matches, then
reads the localized `Name[locale]` field at offset `+0x0C` of the
referenced `SkillLine.dbc` record. For spells with multiple SLA rows
(race-locked variants, Turtle WoW's faction-specific entries) the
lookup prefers a row whose class/race masks match the local player —
so the result reflects "what tab is this spell in *for me*" when one
exists. Falls back to the first matching row otherwise, so the
function still answers for spells the local player can't actually use
(inspecting other classes, parsing combat-log entries from unfamiliar
specs).
Returns `(nil, nil)` for:
- non-numeric or non-positive input
- spellIDs with no `SkillLineAbility.dbc` row (most temporary auras,
proc-only spells, item-on-use effects, GM-debug spells)
- rows whose `skillID` doesn't resolve in `SkillLine.dbc`
Vanilla's `GetSpellTabInfo(tabIndex)` enumerates spellbook tabs by
1-based index — it can't answer "what tab is *this spellID* in".
Addons that need a spellID→tab mapping have historically had to walk
every tab's slot range and string-match against `GetSpellName`. This
function reads it directly off the DBC for any spellID, including
spells the player hasn't learned.
### `GetSpellSchool(spellID)`
Returns `(schoolID, schoolName)` for any spell — known to the player
@@ -5622,6 +5945,155 @@ pass the plain name. Multi-rank buffs match on first-found; if you
have multiple ranks of the same buff active (unusual but possible with
e.g. paladin blessings before talent merge), the first slot wins.
## SpellBook
Functions keyed on the player or pet **spellbook** (slot indices,
learn requirements, skill-tab membership). The underlying `Spell.dbc`
data is exposed through the [Spell](#spell) section; what lives here
is the spellbook-as-a-collection layer.
### `FindSpellBookSlotByID(spellID)`
Inverse of 1.12's `GetSpellName(slot, bookType)`. Given a spellID,
returns the 1-based slot it occupies in the player or pet spellbook,
along with the matching bookType so the result feeds directly into
slot-and-bookType APIs (`GetSpellName`, `GetSpellTexture`,
`GameTooltip:SetSpell`, etc.).
```
slot, bookType = FindSpellBookSlotByID(spellID)
```
- Returns `(slot, "spell")` if the spell is in the player spellbook.
- Returns `(slot, "pet")` if it's only in the pet spellbook.
- Returns `nil` if the spellID isn't currently in either book.
Player book is searched first, so if a spell somehow appeared in both
(unusual but possible for special pet-shared abilities), the player
slot wins.
```lua
local slot, book = FindSpellBookSlotByID(133)
if slot then
GameTooltip:SetSpell(slot, book) -- shows full caster-scaled tooltip
end
```
Equivalent to the legacy function of the same name introduced in 3.0
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
### `C_SpellBook.GetSpellLevelLearned(spellID)`
Returns the level at which a spell becomes available — the
`BaseLevel` field in `Spell.dbc` (record `+0x70`). Direct read off
the cached record; no class/race filtering.
```lua
C_SpellBook.GetSpellLevelLearned(133) -- Fireball rank 1 → 4
C_SpellBook.GetSpellLevelLearned(25306) -- Fireball rank 12 → 60
C_SpellBook.GetSpellLevelLearned(2061) -- Flash Heal rank 1 → 20
```
Returns `0` for invalid spellIDs, spells with no level requirement
(most non-class utility spells), or records the engine hasn't
cached. Matches modern semantics — unknown / utility spells
return 0 rather than nil.
### `C_SpellBook.GetCurrentLevelSpells([level])`
Returns a 1-based table of spellIDs the player's class/race can
learn at the given level. Without arguments, defaults to the
player's current level.
```lua
C_SpellBook.GetCurrentLevelSpells() -- spells trainable at your level
C_SpellBook.GetCurrentLevelSpells(20) -- preview what's available at 20
C_SpellBook.GetCurrentLevelSpells(60) -- preview at 60
```
Walks `SkillLineAbility.dbc` filtering by the player's class bit
(`1 << (classID - 1)`) and race bit, respecting both the include
masks (entries with mask = 0 are "all classes"/"all races", which
also pass) and the exclude masks. For each surviving entry, looks
up the spell's `BaseLevel` and includes it if it matches the
queried level.
> **Vanilla is trainer-driven.** Modern `GetCurrentLevelSpells`
> (added in 5.x when trainers were removed) returns *auto-learned*
> spells. Vanilla 1.12 requires visiting a trainer to actually
> learn most class spells. We return the closest available analog:
> **what's *trainable* at this level**. Useful for "what's new
> this level" UI panels and level-preview tooling ported from MoP+.
Class/race come from the local player — there's no
`(class, race, level)` form because vanilla doesn't expose a
clean class-string→classID lookup. Returns an empty table at
character select / pre-login (no CGPlayer yet) and for levels
where no class/race spells match.
### `C_SpellBook.GetSpellSkillLine(spellID)`
Returns `(name, skillID)` — the SkillLine.dbc row that the given spell
belongs to. The name is the user-facing skill/tab string in the engine's
current locale: spellbook tab names for class spells (`"Protection"`,
`"Fire"`, `"Holy"`), profession headers (`"Tailoring"`, `"Engineering"`),
weapon-skill rows (`"Swords"`, `"Bows"`), etc.
```lua
C_SpellBook.GetSpellSkillLine(1671) -- "Protection", 26 (Shield Bash)
C_SpellBook.GetSpellSkillLine(133) -- "Fire", 8 (Fireball)
C_SpellBook.GetSpellSkillLine(2050) -- "Holy", 56 (Lesser Heal)
C_SpellBook.GetSpellSkillLine(2480) -- "Bows", 45 (Shoot Bow)
C_SpellBook.GetSpellSkillLine(3273) -- "First Aid", 129 (Anesthetic)
```
Walks `SkillLineAbility.dbc` for any row whose `spellID` matches, then
reads the localized `Name[locale]` field at offset `+0x0C` of the
referenced `SkillLine.dbc` record. For spells with multiple SLA rows
(race-locked variants, Turtle WoW's faction-specific entries) the
lookup prefers a row whose class/race masks match the local player —
so the result reflects "what tab is this spell in *for me*" when one
exists. Falls back to the first matching row otherwise, so the
function still answers for spells the local player can't actually use
(inspecting other classes, parsing combat-log entries from unfamiliar
specs).
Returns `(nil, nil)` for:
- non-numeric or non-positive input
- spellIDs with no `SkillLineAbility.dbc` row (most temporary auras,
proc-only spells, item-on-use effects, GM-debug spells)
- rows whose `skillID` doesn't resolve in `SkillLine.dbc`
Vanilla's `GetSpellTabInfo(tabIndex)` enumerates spellbook tabs by
1-based index — it can't answer "what tab is *this spellID* in".
Addons that need a spellID→tab mapping have historically had to walk
every tab's slot range and string-match against `GetSpellName`. This
function reads it directly off the DBC for any spellID, including
spells the player hasn't learned.
### `C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)`
Spellbook-slot variant of
[`C_Spell.IsAutoAttackSpell`](#c_spellisautoattackspellspellid). Resolves
the spellbook slot to its spellID first, then tests `== 6603` (melee
auto-attack).
```lua
C_SpellBook.IsAutoAttackSpellBookItem(1, "spell")
```
### `C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)`
Spellbook-slot variant of
[`C_Spell.IsRangedAutoAttackSpell`](#c_spellisrangedautoattackspellspellid).
Resolves the slot to a spellID, returns `true` if it's `75` (Auto
Shot) or `5019` (Shoot wand).
```lua
C_SpellBook.IsRangedAutoAttackSpellBookItem(10, "spell")
-- true if slot 10 is your wand Shoot
```
## State
Player movement / visibility state queries that modern WoW exposes
@@ -5760,6 +6232,33 @@ object, not in the broadcast descriptor, so the function can't
report state for `target` / `party*` / etc. Returns `false` when
called before the player object is initialized (login screen).
### `IsInGroup()`
Returns `true` if the player is in a party or a raid, `false`
otherwise. Modern shortcut over stock 1.12's
`GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0`.
Accepts an optional `groupType` argument (modern
`Enum.PartyCategory.Home` / `Instance`). Vanilla has no LFG / LFD
instance-group concept, so the argument is accepted and ignored.
```lua
if IsInGroup() then
SendChatMessage("ready check", "PARTY")
end
```
### `IsInRaid()`
Returns `true` if the player is in a raid (specifically — parties
return `false`), `false` otherwise. Same `groupType` argument story
as `IsInGroup`.
```lua
local channel = IsInRaid() and "RAID" or "PARTY"
SendChatMessage("ready check", channel)
```
### `GetMirrorTimerInfo(index)` / `GetMirrorTimerProgress(label)`
Modern (3.0+) readers for the BREATH (drowning) / EXHAUSTION
@@ -6270,13 +6769,19 @@ known unit tokens and return the first one currently mapped to that
GUID, or `nil` if none of them point at it.
The search order matches modern retail with post-1.12 tokens
omitted (`vehicle`, `nameplateN`, `arenaN`, `arenapetN`, `bossN`,
`focus`, `softenemy`, `softfriend`, `softinteract` all post-date
vanilla and the engine's resolver doesn't recognize them):
omitted (`vehicle`, `arenaN`, `arenapetN`, `bossN`, `focus`,
`softenemy`, `softfriend`, `softinteract` all post-date vanilla and
the engine's resolver doesn't recognize them). `nameplateN` is
included between `raidpetN` and `target` — we hook the resolver so
the engine recognizes the token form (see
[Unit tokens](#unit-tokens-nameplaten)). As with the other
positional tokens, the returned `"nameplate3"`-style result is only
valid at that instant; the slot may shift to a different unit
between calls, so re-verify with `UnitGUID(token)` before reusing.
```
player → pet → party1..4 → partypet1..4 → raid1..40
→ raidpet1..40 → target → npc → mouseover
→ raidpet1..40 → nameplate1..N → target → npc → mouseover
```
```lua
+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
+70
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
@@ -1082,6 +1128,14 @@ enum Offsets {
ITEMSTATS_SPELL_SLOT_COUNT = 5,
ITEM_SPELLTRIGGER_ON_USE = 0,
// `ItemStats_C.m_maxCount` — max number of this item the player can
// hold in inventory at once. `0` = unlimited, `1` = unique, higher
// values are caps like "5x bandage type". Vanilla 1.12 doesn't have
// modern "unique-equipped category" semantics; this is the only
// uniqueness signal the client has. Derived by summing struct
// members in VanillaHelpers's `ItemStats_C` definition.
OFF_ITEMSTATS_MAX_COUNT = 0x5C,
// `Spell.dbc` `Name[9]` field at record +0x1E0 (9-locale string
// array, indexed by `[VAR_LOCALE_INDEX]`). Used by `GetItemSpell`
// to push the spell name half of its return.
@@ -2081,6 +2135,22 @@ enum Offsets {
// to L->top → `add [ecx+8], 0x10` incr_top). `0x6F32B0` is
// `lua_replace` (single TValue copy + decr_top).
LUA_PUSH_VALUE = 0x6F3350,
// `luaL_ref(L, t)` — pops the top, stores it in the table at `t`
// at a freshly-allocated integer key, returns the key. Use with
// `LUA_REGISTRY_INDEX` to stash Lua values across C-side scopes;
// pair with `LUA_REF_UNREF` to release. Verified by decompiling
// `FUN_006f5310`: classic luaL_ref shape — nil-top early-out
// returning `LUA_REFNIL = -1`, FREELIST_REF chain pop (pushvalue +
// tonumber on table[0]), else objlen-based new slot, finally
// table[ref] = popped value.
LUA_REF_REF = 0x6F5310,
// `luaL_unref(L, t, ref)` — releases a ref previously returned
// by `luaL_ref`, freeing the slot for future allocations. Verified
// by decompiling `FUN_006f5400`: `ref >= 0` guard (skips
// `LUA_NOREF = -2` / `LUA_REFNIL = -1`), reads current FREELIST_REF
// head, writes it to `table[ref]`, then updates FREELIST_REF = ref
// — the canonical freelist-link operation.
LUA_REF_UNREF = 0x6F5400,
LUA_PUSH_CCLOSURE = 0x6F3920,
LUA_NEW_TABLE = 0x6F3C90,
LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset)
+63
View File
@@ -0,0 +1,63 @@
// 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 "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Group::Info {
namespace {
bool InRaid() {
return *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_COUNT)) > 0;
}
bool InParty() {
auto *guids = reinterpret_cast<const uint64_t *>(
static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
if (guids[i] != 0)
return true;
}
return false;
}
} // namespace
// `IsInRaid()` — true iff the local player is in a raid group.
// Modern signature takes an optional `groupType` (Home / Instance);
// vanilla has no LFG/LFD instance-group concept, so we accept and
// ignore the argument.
static int __fastcall Script_IsInRaid(void *L) {
Game::Lua::PushBool(L, InRaid());
return 1;
}
// `IsInGroup()` — true iff the local player is in a party OR a raid.
// Same `groupType` argument story as `IsInRaid`.
static int __fastcall Script_IsInGroup(void *L) {
Game::Lua::PushBool(L, InRaid() || InParty());
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("IsInRaid", &Script_IsInRaid);
Game::Lua::RegisterGlobalFunction("IsInGroup", &Script_IsInGroup);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Group::Info
+58
View File
@@ -0,0 +1,58 @@
// 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.GetStackCount(itemLocation)` — current stack count for the
// specific item slot. Reads `ITEM_FIELD_STACK_COUNT` directly off the
// CGItem's m_objectFields descriptor — same field
// `Script_GetContainerItemInfo` returns as `itemCount`. Distinct from
// `C_Item.GetItemCount(itemID)`, which sums every stack of `itemID`
// across the player's inventory.
#include "Game.h"
#include "Offsets.h"
#include "item/Location.h"
#include <cstdint>
namespace Item::StackCount {
static int __fastcall Script_GetStackCount(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::PushNumber(L, 0.0);
return 1;
}
auto *item = Item::Location::Resolve(L, 1);
if (item == nullptr) {
Game::Lua::PushNumber(L, 0.0);
return 1;
}
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
item + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr) {
Game::Lua::PushNumber(L, 0.0);
return 1;
}
const int count = *reinterpret_cast<const int *>(
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
Game::Lua::PushNumber(L, static_cast<double>(count));
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetStackCount",
&Script_GetStackCount);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::StackCount
+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;
}
+112
View File
@@ -0,0 +1,112 @@
// 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.GetItemUniqueness(itemLocation)` — uniqueness cap. Reads
// `ItemStats_C.m_maxCount` from the item's cached record; that's the
// only uniqueness signal vanilla's client carries. `1` = unique
// ("Held In Off-hand" trinkets, special quest items, etc.); `0` =
// unlimited; higher values cap the inventory count.
//
// Modern WoW returns a 3-tuple `(maxEquipped, category, equippedCount)`
// keyed off the post-TBC category system (e.g. "Unique-Equipped:
// Brewfest Mug"). Vanilla has no category system, so we surface
// `m_maxCount` as the first return and leave the category nil and
// equipped count `0` — matching modern's "field present but inapplicable"
// convention.
#include "Game.h"
#include "Offsets.h"
#include "item/Data.h"
#include "item/ID.h"
#include "item/Location.h"
#include <cstdint>
namespace Item::Uniqueness {
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>(
static_cast<uintptr_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD));
auto *cache = reinterpret_cast<void *>(
static_cast<uintptr_t>(Offsets::VAR_ITEMDB_CACHE));
const uint64_t zeroGuid = 0;
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
} // namespace
// Pushes the 3-tuple `(maxAllowed, nil, 0)` derived from the
// ItemStats_C record, or nil + early-return on cache miss.
int PushFromItemID(void *L, uint32_t itemID) {
const uint8_t *record = PeekItemRecord(itemID);
if (record == nullptr) {
Item::Data::WarmCache(itemID);
Game::Lua::PushNil(L);
return 1;
}
const int maxCount = *reinterpret_cast<const int *>(
record + Offsets::OFF_ITEMSTATS_MAX_COUNT);
Game::Lua::PushNumber(L, static_cast<double>(maxCount));
Game::Lua::PushNil(L); // category (vanilla has none)
Game::Lua::PushNumber(L, 0.0); // equippedCount (not tracked)
return 3;
}
static int __fastcall Script_GetItemUniqueness(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
auto *item = Item::Location::Resolve(L, 1);
if (item == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
const int itemID = Item::ID::FromCGItem(item);
if (itemID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
return PushFromItemID(L, static_cast<uint32_t>(itemID));
}
static int __fastcall Script_GetItemUniquenessByID(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const int itemID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (itemID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
return PushFromItemID(L, static_cast<uint32_t>(itemID));
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetItemUniqueness",
&Script_GetItemUniqueness);
Game::Lua::RegisterTableFunction("C_Item", "GetItemUniquenessByID",
&Script_GetItemUniquenessByID);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::Uniqueness
+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 `TokenResolver.cpp`
// — addons can pass it straight to `UnitName`, `UnitGUID`, etc.,
// or to `GetNamePlateForUnit` for the frame.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "nameplate/Walk.h"
#include "tick/WorldTick.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace NamePlate::Events {
namespace {
constexpr const char *kEventCreated = "NAME_PLATE_CREATED";
constexpr const char *kEventUnitAdded = "NAME_PLATE_UNIT_ADDED";
constexpr const char *kEventUnitRemoved = "NAME_PLATE_UNIT_REMOVED";
const Event::Custom::AutoReserve _r1{kEventCreated};
const Event::Custom::AutoReserve _r2{kEventUnitAdded};
const Event::Custom::AutoReserve _r3{kEventUnitRemoved};
// Previous tick's snapshot — GUID → nameplate-frame pointer for each
// nameplated unit. Compared against the next tick's walk to compute
// ADDED/REMOVED diffs.
std::unordered_map<uint64_t, const void *> g_lastTickPlates;
// Scratch map reused each tick, swapped into `g_lastTickPlates` at
// the end. File-static so we don't pay the constructor/destructor
// cycle every frame — `clear()` keeps the existing bucket capacity.
std::unordered_map<uint64_t, const void *> g_currentTickPlates;
// Frame pointers we've ever surfaced as nameplate plates. First
// sighting fires NAME_PLATE_CREATED; same pointer reappearing (pool
// reuse) doesn't refire.
//
// Bounded by the engine's CGNamePlateFrame freelist high-water mark:
// `FUN_006087F0` first checks the global freelist head at
// `DAT_00c4d920` and recycles any waiting frame before falling back
// to `SMemAlloc(0x518)`. So the set grows only up to the peak
// simultaneous-visible-plate count for the session — typically
// <80 even in AV-scale scenes (matching the `reserve(64)` below) —
// then tops out as pool reuse covers all subsequent shows.
std::unordered_set<const void *> g_seenPlates;
// Ordered list of currently-visible nameplate GUIDs, in
// creation-order. Append on UNIT_ADDED, erase on UNIT_REMOVED. Backs
// the `nameplateN` unit-token resolver in `TokenResolver.cpp`. Order
// matches modern WoW semantics: stable for the lifetime of each
// plate, gaps when middle plates vanish (until the next REMOVED
// shifts later entries down).
std::vector<uint64_t> g_orderedGUIDs;
// Fire `eventName` with a pre-formatted string as `arg1`. The engine
// dispatcher's `%s` format code pushes the C string into `_G.arg1`
// as a Lua string — no escaping concerns for our own input
// (`"nameplateN"`).
void FireWithString(const char *eventName, const char *value) {
if (value == nullptr)
return;
const int slot = Event::Custom::Lookup(eventName);
if (slot < 0)
return;
Event::Custom::Fire(slot, "%s", value);
}
// Format a 1-based nameplate index as `"nameplateN"` for ADDED /
// REMOVED event payloads. Buffer should be at least 24 bytes — 9
// for the prefix, up to 10 digits for the index, room for null.
// Returns `buf` for convenient inline use.
const char *FormatNamePlateToken(char *buf, size_t bufSize, int oneBasedIndex) {
std::snprintf(buf, bufSize, "nameplate%d", oneBasedIndex);
return buf;
}
// Fire `eventName` with the nameplate `Frame` set as `_G.arg1`.
// `FUN_FIRE_EVENT`'s format-string parser only handles primitive
// types, but it only mutates `_G.arg<N>` for codes it actually
// parses. With an empty format we pre-set `_G.arg1` and the
// dispatcher leaves it alone. Restore the previous value after
// fire so we don't leak our frame into unrelated global state.
//
// Lua-stack-clean: stack depth on entry == stack depth on exit.
using LuaRefRef_t = int(__fastcall *)(void *L, int t);
using LuaRefUnref_t = void(__fastcall *)(void *L, int t, int ref);
using LuaRawGetI_t = void(__fastcall *)(void *L, int t, int n);
void FireWithFrame(const char *eventName, void *frame) {
if (frame == nullptr)
return;
const int slot = Event::Custom::Lookup(eventName);
if (slot < 0)
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
auto refRef = reinterpret_cast<LuaRefRef_t>(
static_cast<uintptr_t>(Offsets::LUA_REF_REF));
auto refUnref = reinterpret_cast<LuaRefUnref_t>(
static_cast<uintptr_t>(Offsets::LUA_REF_UNREF));
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
// Save current `_G.arg1` to the registry.
Game::Lua::PushString(L, "arg1");
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
const int savedRef = refRef(L, Game::Lua::REGISTRY_INDEX);
// Set `_G.arg1 = frame`.
Game::Lua::PushString(L, "arg1");
NamePlate::Info::PushNamePlateFrame(L, frame);
Game::Lua::SetTable(L, Game::Lua::GLOBALS_INDEX);
// Fire — empty format, so the dispatcher doesn't touch `arg1`.
Event::Custom::Fire(slot, "");
// Restore previous `_G.arg1` from the saved registry ref.
Game::Lua::PushString(L, "arg1");
rawgeti(L, Game::Lua::REGISTRY_INDEX, savedRef);
Game::Lua::SetTable(L, Game::Lua::GLOBALS_INDEX);
refUnref(L, Game::Lua::REGISTRY_INDEX, savedRef);
}
void OnWorldTick() {
g_currentTickPlates.clear();
g_currentTickPlates.reserve(64); // typical visible-nameplate ceiling
NamePlate::Walk::ForEachNamePlatedUnit(
[](const uint8_t *, const uint8_t *nameplate,
const uint8_t *instance) {
const uint64_t guid = *reinterpret_cast<const uint64_t *>(instance);
if (guid == 0)
return;
g_currentTickPlates.emplace(guid, nameplate);
});
// Fire CREATED (with the Frame as arg1) for never-before-seen
// frame pointers; ADDED (with `"nameplateN"` token as arg1) for
// GUIDs not in last tick's snapshot. New entries are appended to
// the ordered list *before* firing so the token resolves to the
// newly-added plate during the event handler.
for (const auto &kv : g_currentTickPlates) {
if (g_seenPlates.insert(kv.second).second)
FireWithFrame(kEventCreated, const_cast<void *>(kv.second));
if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) {
g_orderedGUIDs.push_back(kv.first);
char tokenBuf[24];
FireWithString(kEventUnitAdded,
FormatNamePlateToken(tokenBuf, sizeof tokenBuf,
static_cast<int>(g_orderedGUIDs.size())));
}
}
// Fire REMOVED for GUIDs in last tick's snapshot but not current.
// We compute the token from the position *before* erasing so the
// event payload reflects the slot the unit just vacated; the
// handler can still resolve the token to the unit via
// `g_orderedGUIDs[slot]` during dispatch. Later plates shift down
// when we erase — matches modern semantics.
for (const auto &kv : g_lastTickPlates) {
if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) {
auto it = std::find(g_orderedGUIDs.begin(), g_orderedGUIDs.end(),
kv.first);
if (it == g_orderedGUIDs.end())
continue;
const int oneBased = static_cast<int>(it - g_orderedGUIDs.begin()) + 1;
char tokenBuf[24];
FireWithString(kEventUnitRemoved,
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased));
g_orderedGUIDs.erase(it);
}
}
g_lastTickPlates.swap(g_currentTickPlates);
}
} // namespace
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
// Called from `FrameScript_Initialize_h` ahead of the engine's Lua
// teardown. Clears the diff state alongside `NamePlate::Info`'s
// wrapper-cache reset so that on the first post-reload tick every
// currently-visible plate refires `NAME_PLATE_CREATED` and
// `NAME_PLATE_UNIT_ADDED`. Without this, addons that decorate via
// CREATED (pfUI: builds its overlay button per-pointer) never see
// the existing plates after a `/reload` — `g_seenPlates` would
// suppress every refire, and the freshly-built wrapper would lack
// the addon's `.nameplate` field.
//
// `g_orderedGUIDs` is also cleared so the post-reload token indices
// start at `nameplate1` again, matching the order plates re-fire in.
void PrepareForReload() {
g_seenPlates.clear();
g_lastTickPlates.clear();
g_orderedGUIDs.clear();
}
// Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver
// in `TokenResolver.cpp` can map an index to a GUID without seeing
// the internal vector.
uint64_t GetGUIDByIndex(int oneBased) {
if (oneBased <= 0)
return 0;
const size_t idx = static_cast<size_t>(oneBased - 1);
if (idx >= g_orderedGUIDs.size())
return 0;
return g_orderedGUIDs[idx];
}
} // namespace NamePlate::Events
+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
+146
View File
@@ -0,0 +1,146 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Extend `FUN_TOKEN_TO_GUID` (`0x00515970`) with modern WoW's
// `nameplateN` unit-token family. Hooking the central token→GUID
// resolver means every `Script_Unit*` (UnitName, UnitGUID, UnitHealth,
// UnitClass, UnitExists, …) gains nameplate-token support for free,
// since they all funnel here.
//
// Resolution:
//
// 1. SStrCmpI the input against `"nameplate"` (9 chars). Non-match →
// fall through to the original resolver and behave exactly as
// before (engine handles `player` / `target` / `partyN` / etc.,
// or raises "Unknown unit name").
// 2. Parse trailing digits — `nameplate1`, `nameplate12`, ... — into
// a 1-based index `N`.
// 3. Look up `N` in `NamePlate::Events`' ordered GUID list. Out of
// range returns 0 quietly, which the engine treats as "no unit"
// and Lua callers see as `nil` — matches modern behavior, no
// "Unknown unit name" error.
// 4. If the token continues past the digits (`nameplate1target`,
// `nameplate1targettarget`, …) mirror the engine's own suffix
// walker at `LAB_005159d3`: each `"target"` chunk advances the
// GUID by reading `m_objectFields + OFF_UNIT_FIELD_TARGET`
// (UNIT_FIELD_TARGET) via `ObjectByGUID`.
//
// The engine's walker is **inside** the function we hook, so a hook
// that returns early at the prefix-parse step would skip it
// entirely. Replicating the walker here keeps `nameplate1target`
// working without rewriting the resolver. The walker's logic is
// short (one `SStrCmpI` + one `ObjectByGUID` + one field-read per
// iteration) and matches the engine instruction-for-instruction —
// verified against the disassembly of `0x005159D3..0x00515A2C`.
#include "Game.h"
#include "Offsets.h"
#include "nameplate/Walk.h"
#include <cstdint>
namespace NamePlate::TokenResolver {
namespace {
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
// `__stdcall`, not `__cdecl` — `FUN_00064A4C0` ends with `ret 0xc`,
// callee cleans 3-arg stack frame. Calling it through a cdecl typedef
// makes MSVC emit `add esp, 12` after the call on top of the callee's
// own cleanup, drifting ESP +12 per call and corrupting the caller's
// frame.
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
constexpr const char kPrefix[] = "nameplate";
constexpr int kPrefixLen = static_cast<int>(sizeof(kPrefix) - 1);
constexpr const char kSuffixTarget[] = "target";
constexpr int kSuffixTargetLen = static_cast<int>(sizeof(kSuffixTarget) - 1);
// Engine's own priority value for the suffix walker's ObjectByGUID
// calls (verified at `0x005159FE: PUSH 0x6E`). Different from the
// `0x172` we use elsewhere — matching the engine here so the
// resolver's lookup path is identical to the native `targettarget`
// walker.
constexpr int kResolvePriority = 0x6e;
TokenToGUID_t Original_o = nullptr;
uint64_t WalkSuffix(uint64_t guid, const char *suffix) {
auto resolve = reinterpret_cast<ResolveByGUID_t>(
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
while (guid != 0 && *suffix != '\0') {
if (sstrcmpi(suffix, kSuffixTarget, kSuffixTargetLen) != 0)
return 0; // unknown suffix component — modern returns nil
suffix += kSuffixTargetLen;
auto *obj = static_cast<uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "nameplate",
static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32),
kResolvePriority));
if (obj == nullptr)
return 0;
// `m_objectFields` is a *pointer* stored at `obj + 0x110`,
// not an inline array. Engine's walker does
// `mov eax, [obj+0x110]; mov edi, [eax+0x28]` — two
// dereferences. Earlier version of this code missed the
// first one and read instruction bytes interpreted as GUIDs.
auto *fields = *reinterpret_cast<const uint8_t *const *>(
obj + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
if (fields == nullptr)
return 0;
guid = *reinterpret_cast<const uint64_t *>(
fields + Offsets::OFF_UNIT_FIELD_TARGET);
}
return guid;
}
uint64_t __fastcall Hook_h(const char *token) {
if (token == nullptr || *token == '\0')
return Original_o(token);
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
if (sstrcmpi(token, kPrefix, kPrefixLen) != 0)
return Original_o(token);
// Must have at least one digit after `"nameplate"`; otherwise
// it's `"nameplate"` (no number) or `"nameplateX"` — fall
// through to the original so the engine raises its
// "Unknown unit name" error consistently.
const char *p = token + kPrefixLen;
if (*p < '0' || *p > '9')
return Original_o(token);
int index = 0;
while (*p >= '0' && *p <= '9') {
index = index * 10 + (*p - '0');
++p;
}
const uint64_t guid = NamePlate::Events::GetGUIDByIndex(index);
if (guid == 0)
return 0; // OOB / no plate at index — modern returns nil
return WalkSuffix(guid, p);
}
} // namespace
static const Game::HookAutoRegister _hook{
Offsets::FUN_TOKEN_TO_GUID,
reinterpret_cast<void *>(&Hook_h),
reinterpret_cast<void **>(&Original_o)};
} // namespace NamePlate::TokenResolver
+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
+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/>.
// `C_Spell.IsAutoAttackSpell` / `C_Spell.IsRangedAutoAttackSpell` and
// the spellbook-slot variants.
//
// Melee — there's only one spell that triggers melee swings (`6603` —
// "Auto Attack"), and no `Spell.dbc` attribute uniquely identifies it
// vs. on-next-swing abilities like Heroic Strike or Maul. We test
// against the engine constant directly.
//
// Ranged — both vanilla ranged auto-attacks (`75` Auto Shot, `5019`
// Shoot wand) set bit 1 of `Spell.dbc.Attributes` (the
// `SPELL_ATTR_AUTO_REPEAT` flag at value `0x02`). Verified empirically
// against the in-game cache: Attr `0x00050012` (Auto Shot) and
// `0x00000012` (Shoot) share that bit; on-next-swing Heroic Strike
// (`0x00050014`) and other ranged spells like Aimed Shot / Fireball do
// NOT. The attribute check naturally extends to any future
// auto-repeating spell a private server might add.
#include "Lookup.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
#include <cstring>
namespace Spell::AutoAttack {
namespace {
constexpr int SPELL_AUTO_ATTACK = 6603;
constexpr int OFF_SPELL_ATTRIBUTES = 0x18;
constexpr uint32_t SPELL_ATTR_AUTO_REPEAT = 0x00000002;
bool IsMelee(int spellID) {
return spellID == SPELL_AUTO_ATTACK;
}
bool IsRanged(int spellID) {
if (spellID <= 0)
return false;
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
if (rec == nullptr)
return false;
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
rec + OFF_SPELL_ATTRIBUTES);
return (attr & SPELL_ATTR_AUTO_REPEAT) != 0;
}
int ReadSpellID(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0;
return static_cast<int>(Game::Lua::ToNumber(L, 1));
}
int ReadSpellBookSlotID(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0;
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
int bookType = 0;
if (Game::Lua::IsString(L, 2)) {
const char *s = Game::Lua::ToString(L, 2);
if (s != nullptr && _stricmp(s, "pet") == 0)
bookType = 1;
} else if (Game::Lua::IsNumber(L, 2)) {
bookType = static_cast<int>(Game::Lua::ToNumber(L, 2));
}
return Spell::Lookup::SpellbookSlotToID(slot, bookType);
}
} // namespace
static int __fastcall Script_IsAutoAttackSpell(void *L) {
Game::Lua::PushBool(L, IsMelee(ReadSpellID(L)));
return 1;
}
static int __fastcall Script_IsRangedAutoAttackSpell(void *L) {
Game::Lua::PushBool(L, IsRanged(ReadSpellID(L)));
return 1;
}
static int __fastcall Script_IsAutoAttackSpellBookItem(void *L) {
Game::Lua::PushBool(L, IsMelee(ReadSpellBookSlotID(L)));
return 1;
}
static int __fastcall Script_IsRangedAutoAttackSpellBookItem(void *L) {
Game::Lua::PushBool(L, IsRanged(ReadSpellBookSlotID(L)));
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsAutoAttackSpell",
&Script_IsAutoAttackSpell);
Game::Lua::RegisterTableFunction("C_Spell", "IsRangedAutoAttackSpell",
&Script_IsRangedAutoAttackSpell);
Game::Lua::RegisterTableFunction("C_SpellBook", "IsAutoAttackSpellBookItem",
&Script_IsAutoAttackSpellBookItem);
Game::Lua::RegisterTableFunction("C_SpellBook", "IsRangedAutoAttackSpellBookItem",
&Script_IsRangedAutoAttackSpellBookItem);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::AutoAttack
+51
View File
@@ -0,0 +1,51 @@
// 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_Spell.DoesSpellExist(spellID)` — true iff the spell ID resolves
// to a populated `Spell.dbc` record. Cheap existence probe useful
// before calling other `C_Spell.*` functions that might OOB-crash or
// return nil/empty for missing spells.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Spell::Exists {
static int __fastcall Script_DoesSpellExist(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (spellID <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
Game::Lua::PushBool(L, rec != nullptr);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "DoesSpellExist",
&Script_DoesSpellExist);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::Exists
+110
View File
@@ -0,0 +1,110 @@
// 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_Spell.SpellHasRange(spellIdentifier)` and the parallel
// `SpellHasRange(slot, bookType)` — true iff the spell's
// `SpellRange.dbc` row has a non-zero min or max range.
//
// Vanilla 1.12 doesn't ship `SpellHasRange` as a global at all; 3.3.5+
// added it but with the spellID-only signature. We provide the modern
// namespaced shape *and* a `(slot, bookType)` positional form mirroring
// the other dual-signature globals (`GetSpellInfo`, `GetSpellLink`,
// etc.) so callers can use whichever they already have.
#include "Arg.h"
#include "Lookup.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
#include <cstring>
namespace Spell::HasRange {
namespace {
// Spell.dbc RangeIndex field — mirrors `Script_GetSpellInfo`'s
// local constant. Future refactor: promote to `Offsets.h`.
constexpr int OFF_SPELL_RANGE_INDEX = 0x90;
// SpellRange.dbc record layout (verified, documented in CLAUDE.md
// under "Sub-DBC record layouts"):
// +0x00 id, +0x04 minRange (float), +0x08 maxRange (float), ...
constexpr int OFF_SPELLRANGE_MIN = 0x04;
constexpr int OFF_SPELLRANGE_MAX = 0x08;
bool SpellIDHasRange(int spellID) {
if (spellID <= 0)
return false;
const uint8_t *spellRec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
if (spellRec == nullptr)
return false;
const uint32_t rangeIdx = *reinterpret_cast<const uint32_t *>(
spellRec + OFF_SPELL_RANGE_INDEX);
if (rangeIdx == 0)
return false;
const uint8_t *rangeRec = DBC::Record(Offsets::VAR_SPELL_RANGE_RECORDS,
Offsets::VAR_SPELL_RANGE_COUNT,
rangeIdx);
if (rangeRec == nullptr)
return false;
const float minRange = *reinterpret_cast<const float *>(
rangeRec + OFF_SPELLRANGE_MIN);
const float maxRange = *reinterpret_cast<const float *>(
rangeRec + OFF_SPELLRANGE_MAX);
return minRange > 0.0f || maxRange > 0.0f;
}
} // namespace
static int __fastcall Script_C_Spell_SpellHasRange(void *L) {
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
Game::Lua::PushBool(L, SpellIDHasRange(spellID));
return 1;
}
// `SpellHasRange(slot, bookType)` — vanilla-style positional shape.
// `bookType` is the same string convention used by `GetSpellName`:
// `"spell"` (or omitted / anything-not-pet) → player book,
// `"pet"` → pet book.
static int __fastcall Script_SpellHasRange(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
int bookType = 0;
if (Game::Lua::IsString(L, 2)) {
const char *s = Game::Lua::ToString(L, 2);
if (s != nullptr && _stricmp(s, "pet") == 0)
bookType = 1;
}
const int spellID = Spell::Lookup::SpellbookSlotToID(slot, bookType);
Game::Lua::PushBool(L, SpellIDHasRange(spellID));
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "SpellHasRange",
&Script_C_Spell_SpellHasRange);
Game::Lua::RegisterGlobalFunction("SpellHasRange",
&Script_SpellHasRange);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::HasRange
+94
View File
@@ -0,0 +1,94 @@
// 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_Spell.IsSelfBuff(spellID)` — true iff every active effect on the
// spell targets the caster and only the caster. Read directly from
// `Spell.dbc.EffectImplicitTargetA[3]` and `EffectImplicitTargetB[3]`
// — an effect is "self-only" when both targets are either
// `TARGET_NONE` (0) or `TARGET_SELF` (1). Effects with a zero
// `Effect` code are skipped (unused effect slots).
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Spell::IsSelfBuff {
namespace {
// Spell.dbc per-effect arrays. Each is an int32[3] in the record.
// Field-index derivation against the known `EffectApplyAuraName` at
// field 91 / +0x16C — the implicit-target pair sits at field 82/85.
constexpr int OFF_SPELL_EFFECT = 0xF4; // SPELL_EFFECT_* code
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148;
constexpr int OFF_SPELL_EFFECT_TARGET_B = 0x154;
constexpr int EFFECT_COUNT = 3;
// Vanilla 1.12 implicit-target codes (CMaNGOS `Targets` enum).
constexpr int TARGET_NONE = 0;
constexpr int TARGET_SELF = 1;
bool IsSelfOnlyTarget(int target) {
return target == TARGET_NONE || target == TARGET_SELF;
}
} // namespace
static int __fastcall Script_IsSelfBuff(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (spellID <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
if (rec == nullptr) {
Game::Lua::PushBool(L, false);
return 1;
}
bool sawEffect = false;
for (int i = 0; i < EFFECT_COUNT; ++i) {
const int effect = *reinterpret_cast<const int *>(
rec + OFF_SPELL_EFFECT + i * 4);
if (effect == 0)
continue;
sawEffect = true;
const int targetA = *reinterpret_cast<const int *>(
rec + OFF_SPELL_EFFECT_TARGET_A + i * 4);
const int targetB = *reinterpret_cast<const int *>(
rec + OFF_SPELL_EFFECT_TARGET_B + i * 4);
if (!IsSelfOnlyTarget(targetA) || !IsSelfOnlyTarget(targetB)) {
Game::Lua::PushBool(L, false);
return 1;
}
}
Game::Lua::PushBool(L, sawEffect);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsSelfBuff",
&Script_IsSelfBuff);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::IsSelfBuff
+70
View File
@@ -0,0 +1,70 @@
// 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_Spell.GetSchoolString(schoolMask)` — localized name for a damage
// school bitmask. Vanilla spells are single-school (the multi-school
// combos like "Frostfire" / "Shadowflame" / "Spellstorm" were
// TBC-and-later additions), so we resolve any single-bit mask to its
// `SPELL_SCHOOL<n>_CAP` global string and return "Unknown" for any
// combination — matching the modern API's documented fallback.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstdio>
namespace Spell::SchoolString {
namespace {
// Returns the 0-based school index for a single-bit mask, or -1 if
// the mask has zero or multiple bits set (or is out of the 7-school
// range vanilla supports).
int SingleBitSchoolIndex(uint32_t mask) {
if (mask == 0 || (mask & (mask - 1)) != 0)
return -1;
for (int i = 0; i < 7; ++i) {
if (mask == (1u << i))
return i;
}
return -1;
}
} // namespace
static int __fastcall Script_GetSchoolString(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushString(L, "Unknown");
return 1;
}
const auto mask = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
const int idx = SingleBitSchoolIndex(mask);
if (idx < 0) {
Game::Lua::PushString(L, "Unknown");
return 1;
}
char globalName[24];
std::snprintf(globalName, sizeof globalName, "SPELL_SCHOOL%d_CAP", idx);
Game::Lua::PushLocalizedString(L, globalName, "Unknown");
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSchoolString",
&Script_GetSchoolString);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::SchoolString
+65
View File
@@ -0,0 +1,65 @@
// 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_Spell.GetSpellSubtext(spellIdentifier)` — the localized "Rank N"
// / "Passive" / "Racial Passive" string from `Spell.dbc.Rank[9]`.
// String-keyed input resolves by name through the spellbook (modern
// `SpellIdentifier` semantics); numeric input is a direct spellID
// lookup.
#include "Arg.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Spell::Subtext {
namespace {
// `Spell.dbc.Rank[9]` — same 9-slot locale-string array shape as the
// adjacent `Name[9]` field. Defined locally; sibling modules
// (`Info.cpp`, `Tooltip.cpp`, `macro/Spell.cpp`) carry the same
// constant — promoting to `Offsets.h` is a future refactor.
constexpr int OFF_SPELL_RANK = 0x204;
} // namespace
static int __fastcall Script_GetSpellSubtext(void *L) {
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
if (spellID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
const char *subtext = DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID),
OFF_SPELL_RANK);
if (subtext == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
Game::Lua::PushString(L, subtext);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellSubtext",
&Script_GetSpellSubtext);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::Subtext
+24 -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,15 @@ static int __fastcall Script_UnitGUID(void *L) {
// nil if none of the known tokens point at it.
//
// The search walks the modern retail token order with post-vanilla
// tokens dropped (no `vehicle`, `nameplateN`, `arenaN`, `arenapetN`,
// `bossN`, `focus`, `softenemy`, `softfriend`, `softinteract` —
// those all post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise
// "Unknown unit name" for them). Order:
// tokens dropped (no `vehicle`, `arenaN`, `arenapetN`, `bossN`,
// `focus`, `softenemy`, `softfriend`, `softinteract` — those all
// post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit
// name" for them). `nameplateN` is included — we hook the token
// resolver so the engine recognizes it (see `nameplate/TokenResolver.cpp`).
// Order:
//
// player → pet → party1..4 → partypet1..4 → raid1..40
// → raidpet1..40 → target → npc → mouseover
// → raidpet1..40 → nameplate1..N → target → npc → mouseover
//
// The result is inherently unstable — multiple tokens can map to the
// same GUID at once (your target IS your mouseover IS your party1), and
@@ -183,6 +186,22 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
if (check(buf)) return 1;
}
// Visible nameplates. The ordered list (see
// `NamePlate::Events::g_orderedGUIDs`) is dense — `GetGUIDByIndex`
// returns 0 once we step past the populated count, terminating
// the loop. Skips the token-resolver hook + SStrCmpI cost per
// index by comparing GUIDs directly.
for (int i = 1; ; ++i) {
const uint64_t plateGuid = NamePlate::Events::GetGUIDByIndex(i);
if (plateGuid == 0)
break;
if (plateGuid == target) {
std::snprintf(buf, sizeof buf, "nameplate%d", i);
Game::Lua::PushString(L, buf);
return 1;
}
}
if (check("target")) return 1;
if (check("npc")) return 1;
if (check("mouseover")) return 1;