Compare commits

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

* UnitTokenFromGUID: place focus after target, not after mouseover

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

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

---------

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

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

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

* fixed NameCache header

* C_NamePlate.GetNamePlates

* GetNamePlates will show blizzard nameplates as well

* C_NamePlate.GetNamePlateForUnit

* nameplate events

* C_NamePlate.GetNamePlateForGUID

* NAME_PLATE_CREATED now sends frame as argument

* document verification provenance for nameplate-adjacent offsets

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

* harden nameplate frame push against stale registry refs

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

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

* support nameplateN unit tokens

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

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

* document nameplateN unit-token support

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

* return stable nameplate wrapper across calls

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

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

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

* NAME_PLATE_UNIT events now fire token

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

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

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

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

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

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

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2026-05-26 16:23:09 -05:00
Brues d5a45c73cb Addon: LinkUtil 2026-05-26 02:10:41 -05:00
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
Brues 54e503d1dd InCombatLockdown just returns false now 2026-05-25 14:59:00 -05:00
Brues 0d5c392c20 C_Map.GetBestMapForUnit 2026-05-25 00:20:53 -05:00
Brues da183c3f9a Update API.md 2026-05-24 19:38:27 -05:00
Brues 4a78208c6a corrected GetInstanceInfo shape. added mapid 2026-05-24 19:37:52 -05:00
Brues f61c014973 GetInstanceInfo 2026-05-24 19:33:54 -05:00
36 changed files with 3627 additions and 199 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
+1 -1
View File
@@ -110,7 +110,7 @@ PLAYER_FACTION_COLORS =
{
[0] = PLAYER_FACTION_COLOR_HORDE,
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
};-- Quest
};
WOW_PROJECT_MAINLINE = 1;
WOW_PROJECT_CLASSIC = 2;
+4 -1
View File
@@ -62,7 +62,10 @@ LinkTypes = {
LinkUtil = {};
function LinkUtil.FormatLink(linkType, linkDisplayText, ...)
local linkFormatTable = { string.format("|H%s", linkType), ... };
local linkFormatTable = { string.format("|H%s", linkType) };
for i = 1, arg.n do
linkFormatTable[i + 1] = arg[i];
end
local returnLink = table.concat(linkFormatTable, ":");
if linkDisplayText then
return returnLink .. string.format("|h%s|h", linkDisplayText);
-6
View File
@@ -17,12 +17,6 @@
-- Secure-Templates system, so the protective wrappers in 3.3.5
-- reduce to a plain pcall.
--
-- Skipping if print is already defined keeps this file safe to load
-- alongside any other DLL or addon that might inject its own print.
if type(print) == "function" then
return
end
----------------------------------------------------------------------------
-- tostringall: convert every vararg to a string, preserving count.
+6 -1
View File
@@ -27,7 +27,12 @@ set(TINYCBOR_SOURCES
add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
set(CLASSICAPI_VERSION_VALUE 1)
# Dev-build sentinel — represents "newer than every plausible
# release". Encoded as v9999.99.99 = 9999 * 10000 + 99 * 100 + 99 =
# 99,999,999. Picked so addon-side feature gates of the form
# `CLASSIC_API_VERSION >= X` always pass during local development;
# CI release builds overwrite it via `-DCLASSICAPI_TAG=vX.Y.Z`.
set(CLASSICAPI_VERSION_VALUE 99999999)
set(CLASSICAPI_TAG
""
+21 -5
View File
@@ -23,24 +23,29 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [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` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Macros](docs/API.md#macros) | `GetMacroSpell` |
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.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` |
@@ -61,7 +66,7 @@ the glue state because GlueXML had no way to reach them otherwise.
| Group | Calls |
|-------|-------|
| CharacterOrder | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
@@ -91,6 +96,10 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| `HEARTHSTONE_BOUND` | *(none)* |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
| `NAME_PLATE_CREATED` | `nameplateFrame` |
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
| `PLAYER_FOCUS_CHANGED` | *(none)* |
| `PLAYER_STARTED_LOOKING` | *(none)* |
| `PLAYER_STOPPED_LOOKING` | *(none)* |
| `PLAYER_STARTED_MOVING` | *(none)* |
@@ -113,6 +122,13 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` |
| Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` |
### Unit tokens
| Token | Resolves to |
|-------|-------------|
| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). |
| `focus` / `focustarget` | Sticky target set via [`FocusUnit`](docs/API.md#focusunitunit), cleared via [`ClearFocus`](docs/API.md#clearfocus). Same `UnitX` coverage as `nameplateN`. Fires [`PLAYER_FOCUS_CHANGED`](docs/API.md#player_focus_changed-event) on transition. See [Focus](docs/API.md#focus). |
## Installation
Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the
+997 -149
View File
File diff suppressed because it is too large Load Diff
+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
+163
View File
@@ -200,6 +200,30 @@ enum Offsets {
// Registers a single global Lua function. __fastcall(name, func).
FUN_FRAMESCRIPT_REGISTER_FUNCTION = 0x00704120,
// `FrameScript_Object::ScriptRegister(this, name)` — `__thiscall`,
// `this` = a `CFrameScriptObject *`. On first call (when `this+0x04`
// is zero) builds a Lua wrapper table `{[0] = lightuserdata(this)}`
// with `_G["__framescript_meta"]` as metatable, `luaL_ref`s it into
// the registry, stores the refkey at `this+0x08`. Always increments
// `this+0x04` (the Lua-side refcount). Optional `name` argument
// installs `_G[name] = wrapper` for engine-named frames.
//
// We call this in `PushNamePlateFrame` so the engine and our own
// C_NamePlate getters operate on the **same** wrapper table —
// every push through `lua_rawgeti(REGISTRY, this+0x08)` (the
// canonical engine path) lands on the same Lua object pfUI
// received in `NAME_PLATE_CREATED`, so addon-set fields
// (`plate.nameplate = decoratedButton`) survive engine-side
// re-fetches. Earlier note in `Info.cpp` warned about pinning the
// refcount; for pool-managed nameplates the engine never
// un-registers them anyway, so the pin is benign.
FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER = 0x00701BD0,
// `this+0x04` Lua refcount, incremented by `ScriptRegister`. We
// read it as a "has the engine ever exposed this CObject to Lua"
// probe — equivalent to checking `this+0x08 > 0` but more direct.
OFF_COBJECT_LUA_REFCOUNT = 0x04,
// Direct cvar lookup — `__fastcall(const char *name) → CVar* | NULL`.
// Hash-table by-name lookup over the CVar registry; same call
// `Script_GetCVar` makes internally before the engine wraps the
@@ -281,6 +305,28 @@ enum Offsets {
// input it's the right primitive.
FUN_TOKEN_TO_GUID = 0x00515970,
// `SStrCmpI(a, b, n)` — Storm's case-insensitive memcmp-style
// comparator. **`int __stdcall(const char *a, const char *b, int n)`**
// — the function ends with `ret 0xc`, so the callee pops the
// 3-arg stack frame. Declaring it as `__cdecl` and calling makes
// MSVC emit a redundant `add esp, 12` post-call, drifting ESP
// upward by 12 per call and corrupting the caller's stack frame
// — manifested as a deep-Lua crash whose `L` pointer landed in
// `.text`. Returns 0 when the first `n` characters match
// (ignoring case) or both strings end before `n`.
FUN_SSTR_CMP_I = 0x0064A4C0,
// UNIT_FIELD_TARGET within `m_objectFields` — 64-bit GUID at byte
// offsets +0x28 (lo) / +0x2C (hi). Verified by disassembling
// `FUN_TOKEN_TO_GUID`'s suffix walker at `0x00515A1C-A2C`:
// `mov eax, [obj + 0x110]; mov edi, [eax + 0x28]; mov ebx, [eax + 0x2c]`
// — reads the target GUID to chain `targettarget`-style tokens.
// Distinct from the higher field offsets in the
// `OFF_UNIT_FIELD_*` block; UNIT_FIELD_TARGET is one of the few
// 1.12 offsets that matches the CMaNGOS-documented vanilla
// layout.
OFF_UNIT_FIELD_TARGET = 0x28,
// Party / raid roster counts and the party GUID array referenced
// in the `FUN_TOKEN_TO_GUID` dispatch comment above. Used by
// `UnitTokenFromGUID` to cap its candidate iteration — solo
@@ -847,6 +893,14 @@ enum Offsets {
// descriptor bytes for a worn-and-bound item.
OFF_DESCRIPTOR_FLAGS = 0x3C,
ITEM_FLAG_SOULBOUND = 0x01,
// Bit 2 — instance is "unlocked", i.e. the lock referenced by
// `ItemStats.LockID` has been opened (rogue Pick Lock, key item,
// etc.). For unlockable items, the engine gates the
// `<Right Click to Open>` tooltip on this bit: a fresh lockbox
// has it clear (priest sees no openable affordance), a picked
// lockbox has it set. Items with `LockID == 0` (clams, simple
// sacks) never need the bit.
ITEM_FLAG_UNLOCKED = 0x04,
// Client-side "in-transaction" lock — set when the engine puts an
// item on the cursor / mail-attaches it / trade-attaches it, cleared
@@ -1014,7 +1068,33 @@ enum Offsets {
OFF_ITEMSTATS_NAME = 0x08,
OFF_ITEMSTATS_DISPLAY_INFO_ID = 0x18,
OFF_ITEMSTATS_QUALITY = 0x1C, // u32 — 0=Poor … 5=Legendary
// `m_flags` — item-class flags bitfield. Bits we read:
// `ITEMSTATS_FLAG_OPENABLE` (`0x4`, bit 2): freely right-click-
// openable container (sack, clam, simple chest, quest box).
// The tooltip line `<Right Click to Open>` (`ITEM_OPENABLE`
// string) renders for items with this bit set, gated on the
// player's lock skill when the item also carries a non-zero
// LockID. Same bit modern WoW's `C_Item.IsItemOpenable`
// surfaces.
// Other documented bits (`0x1` = BIND_ON_PICKUP, `0x2` = CONJURED,
// `0x10` = LOOTABLE, `0x200` = WRAPPER, etc.) live in the same
// field but we don't read them here. Add named constants as
// consumers materialize.
// Reader at `0x0052E323` (the tooltip-builder's openable branch in
// `FUN_0052B650`) does `(ItemStats[+0x20] & 0x4)` against this
// field; verified by checking the field offset matches the
// 5-DWORD-per-row layout `puVar4[8]` resolves to in the decompile.
OFF_ITEMSTATS_FLAGS = 0x20,
ITEMSTATS_FLAG_OPENABLE = 0x4,
OFF_ITEMSTATS_SELL_PRICE = 0x28,
// `m_lockID` — references `Lock.dbc`. Zero means "no lock"; any
// nonzero value gates the openable interaction on the player
// successfully unlocking the item (instance flag bit 2 in
// `ITEM_FIELD_FLAGS`). Same field the tooltip builder reads as
// `ItemStats[+0x1AC]` (puVar4[0x6b]) when deciding whether the
// `<Right Click to Open>` line should require the lock to be
// picked first.
OFF_ITEMSTATS_LOCK_ID = 0x1AC,
OFF_ITEMSTATS_INVENTORY_TYPE = 0x2C,
OFF_ITEMSTATS_ITEM_LEVEL = 0x38, // u32 — base ilvl from ItemSparse
// `m_stackable` — max stack size for this item type (1 for non-
@@ -1082,6 +1162,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.
@@ -1994,6 +2082,65 @@ enum Offsets {
OFF_SPELLDISPEL_HAS_NAME = 0x28,
OFF_SPELLDISPEL_NAME = 0x2C,
// Map.dbc — instance/zone metadata. Standard 5-DWORD class instance
// shape; records and count are catalogued in `docs/DBCs.md`. Record
// schema (vanilla, derived from `FUN_00495c90` IsInInstance helper
// and `FUN_0049e1c0` raid-instance warning handler):
// +0x08 uint32 instance type (0=world, 1=dungeon, 2=raid, 3=BG)
// +0x10..+0x30 9 locale-string ptrs (localized display name)
// No max-players column in vanilla — that data was added in TBC via
// MapDifficulty.dbc.
VAR_MAP_RECORDS = 0x00C0DAA8,
VAR_MAP_COUNT = 0x00C0DAAC,
OFF_MAP_INSTANCE_TYPE = 0x08,
OFF_MAP_NAME = 0x10,
// Currently-loaded map ID (the Map.dbc ID for the zone/instance the
// player is in). Written by the map-transition handler
// `FUN_00495d10`; read by `FUN_00495c90` (the helper backing
// `Script_IsInInstance`).
VAR_CURRENT_MAP_ID = 0x00B4E378,
// Local player's current AreaTable.dbc area ID (u32 storage; only
// the low 16 bits are used — the value is broadcast over the wire
// as a u16 in `SMSG_PARTY_MEMBER_STATS`). Written by the zone-change
// handler `FUN_00494780(areaID, parentAreaID, zoneName, subzone,
// realZone, eventID)`; read by `Script_GetRealZoneText` and the
// outbound party-stats builder `FUN_005f0880`.
VAR_PLAYER_AREA_ID = 0x00B4E314,
// Unified party/raid member stats lookup —
// `__fastcall(GUID *guidPair) -> uint8_t *`. Returns a pointer to
// the member's stats data on hit, NULL on miss. Tail-jumps from
// party storage (`FUN_004e8820`, 4 slots at `0x00BC70B0`) to raid
// storage (`FUN_004bb0f0`, 40-slot pointer array at `0x00B712A8`).
// Both sub-calls return pointers where the field at `+0x14` is the
// u16 areaID (AreaTable.dbc ID) — for party that's at struct +0x14
// directly, for raid it's at `*raidArr[i] + 0x10 + 0x14`. Caller
// doesn't need to distinguish the two cases.
//
// Within the returned stats block: `+0x08` status flags, `+0x09`
// powerType, `+0x0A..+0x10` HP/maxHP/power/maxPower, `+0x12` level,
// `+0x14` u16 areaID, `+0x16/+0x18` position. Verified from the
// outbound stats builder `FUN_005f0880` (mirrors inbound packet
// shape from SMSG_PARTY_MEMBER_STATS_FULL).
FUN_GROUP_MEMBER_STATS_LOOKUP = 0x00496400,
OFF_GROUP_MEMBER_AREA_ID = 0x14,
// Per-slot GUID tables — used to map party/raid token strings
// ("party1", "raid17") to GUIDs without going through
// `FUN_RESOLVE_UNIT_TOKEN`, which only resolves to a local CGUnit
// and returns NULL for group members in other zones.
//
// Party: 4 entries × 8 bytes (lo + hi). `guids[i]` corresponds to
// "partyN" where N = i+1.
//
// Raid: 40 entries × 4 bytes — each is a `RaidMember *`. GUID is
// at `*member + 0` (lo) and `*member + 4` (hi). Verified from
// `FUN_004bb0f0`. Slot may be NULL (empty / offline raid member).
VAR_PARTY_MEMBER_GUIDS = 0x00BC6F48,
VAR_RAID_MEMBER_PTRS = 0x00B712A8,
VAR_LOCALE_INDEX = 0x00C0E080, // 0..8, picks one of the 9 localized strings
LUA_IS_NUMBER = 0x6F34D0,
@@ -2022,6 +2169,22 @@ enum Offsets {
// to L->top → `add [ecx+8], 0x10` incr_top). `0x6F32B0` is
// `lua_replace` (single TValue copy + decr_top).
LUA_PUSH_VALUE = 0x6F3350,
// `luaL_ref(L, t)` — pops the top, stores it in the table at `t`
// at a freshly-allocated integer key, returns the key. Use with
// `LUA_REGISTRY_INDEX` to stash Lua values across C-side scopes;
// pair with `LUA_REF_UNREF` to release. Verified by decompiling
// `FUN_006f5310`: classic luaL_ref shape — nil-top early-out
// returning `LUA_REFNIL = -1`, FREELIST_REF chain pop (pushvalue +
// tonumber on table[0]), else objlen-based new slot, finally
// table[ref] = popped value.
LUA_REF_REF = 0x6F5310,
// `luaL_unref(L, t, ref)` — releases a ref previously returned
// by `luaL_ref`, freeing the slot for future allocations. Verified
// by decompiling `FUN_006f5400`: `ref >= 0` guard (skips
// `LUA_NOREF = -2` / `LUA_REFNIL = -1`), reads current FREELIST_REF
// head, writes it to `table[ref]`, then updates FREELIST_REF = ref
// — the canonical freelist-link operation.
LUA_REF_UNREF = 0x6F5400,
LUA_PUSH_CCLOSURE = 0x6F3920,
LUA_NEW_TABLE = 0x6F3C90,
LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset)
+55
View File
@@ -0,0 +1,55 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
// returns the `abs(SPELL_CHARGES[0])` value of the item in the
// specified bag slot, or `nil` for empty slots and items that don't
// carry a charge concept. Wands return their remaining cast count
// (e.g. 50 → 49 → …); single-use items (Healthstone, Mana Gem,
// Goblin Sapper Charge) return `1`.
//
// Modern equivalent: none — modern WoW exposes charges only through
// tooltip text scanning. `C_Item.GetItemCount(includeBank, includeCharges)`
// totals charges across every matching stack; this is the per-slot
// reader.
#include "Game.h"
#include "item/Charges.h"
#include "item/Location.h"
namespace Container::Charges {
namespace {
int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L,
"Usage: C_Container.GetContainerItemCharges(containerIndex, slotIndex)");
return 0;
}
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
return Item::Charges::PushChargesForItem(
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::Charges
+49
View File
@@ -0,0 +1,49 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.IsContainerItemOpenable(bag, slot)` — positional-arg
// wrapper around `Item::Openable::PushIsItemOpenable`. Same chain
// `C_Container.GetContainerItemDurability` and friends use:
// `ResolveBag` → `GetItemBySlot`. Returns the
// `(isOpenable, canOpen)` tuple — see [[item/Openable.h]].
#include "Game.h"
#include "item/Location.h"
#include "item/Openable.h"
namespace Container::Openable {
namespace {
int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L,
"Usage: C_Container.IsContainerItemOpenable(containerIndex, slotIndex)");
return 0;
}
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
return Item::Openable::PushIsItemOpenable(
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::Openable
+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
+128
View File
@@ -0,0 +1,128 @@
// 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 Instance::Info {
namespace {
// Map.dbc instance-type enum, indexable from 0..4. Vanilla never uses
// slot 4 (arena), but the modern API string set includes it.
const char *const kTypeNames[] = {
"none", // 0 — open world
"party", // 1 — 5-man dungeon
"raid", // 2 — raid
"pvp", // 3 — battleground
"arena", // 4 — unused in vanilla
};
// Per-type player cap. Vanilla has no per-map cap stored client-side
// (MapDifficulty.dbc was a TBC addition), so we return the type's
// canonical max. ZG / AQ20 / WSG / AB return 40 instead of their true
// caps; same answer for any custom raid on private servers. Addons
// that need exact caps must supply their own table.
int CapForType(uint32_t type) {
switch (type) {
case 1: return 5;
case 2: return 40;
case 3: return 40;
case 4: return 5;
default: return 0;
}
}
const uint8_t *MapRecord(uint32_t mapID) {
const int count = *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_COUNT));
if (static_cast<int>(mapID) < 0 || static_cast<int>(mapID) > count)
return nullptr;
const uint8_t *const *records =
*reinterpret_cast<const uint8_t *const *const *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_RECORDS));
if (records == nullptr)
return nullptr;
return records[mapID];
}
} // namespace
// `GetInstanceInfo()` — returns the modern 9-tuple shape, with vanilla
// degeneracies for fields the 1.12 client doesn't track:
// name, type, difficultyID, difficultyName, maxPlayers,
// dynamicDifficulty, isDynamic, instanceID, instanceGroupSize
//
// - `name` / `type` come from Map.dbc.
// - `difficultyID` / `difficultyName` are always `1` / `"Normal"` (no
// heroic mode pre-TBC; MapDifficulty.dbc doesn't exist).
// - `maxPlayers` is the type's canonical cap (see `CapForType`).
// - `dynamicDifficulty` is always `0` and `isDynamic` is always `false`
// (dynamic difficulty was a Cataclysm-era addition).
// - `instanceID` is the current Map.dbc ID. (Modern docs call this
// "instanceID" but it's really the mapID — both vanilla and modern
// put the same value here.)
// - `instanceGroupSize` mirrors `maxPlayers` (no per-group-config
// variants in vanilla).
static int __fastcall Script_GetInstanceInfo(void *L) {
const uint32_t mapID = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
const uint8_t *rec = MapRecord(mapID);
if (rec == nullptr) {
Game::Lua::PushString(L, "");
Game::Lua::PushString(L, "none");
Game::Lua::PushNumber(L, 1.0);
Game::Lua::PushString(L, "Normal");
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushBool(L, false);
Game::Lua::PushNumber(L, static_cast<double>(mapID));
Game::Lua::PushNumber(L, 0.0);
return 9;
}
const uint32_t type = *reinterpret_cast<const uint32_t *>(
rec + Offsets::OFF_MAP_INSTANCE_TYPE);
const uint32_t locale = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
const char *name = *reinterpret_cast<const char *const *>(
rec + Offsets::OFF_MAP_NAME + locale * 4);
const char *typeName =
(type < sizeof(kTypeNames) / sizeof(kTypeNames[0])) ? kTypeNames[type]
: "none";
const int cap = CapForType(type);
Game::Lua::PushString(L, name != nullptr ? name : "");
Game::Lua::PushString(L, typeName);
Game::Lua::PushNumber(L, 1.0);
Game::Lua::PushString(L, "Normal");
Game::Lua::PushNumber(L, static_cast<double>(cap));
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushBool(L, false);
Game::Lua::PushNumber(L, static_cast<double>(mapID));
Game::Lua::PushNumber(L, static_cast<double>(cap));
return 9;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetInstanceInfo",
&Script_GetInstanceInfo);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Instance::Info
+39
View File
@@ -0,0 +1,39 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Charges.h"
#include "../Game.h"
#include "../Offsets.h"
#include <cstdlib>
namespace Item::Charges {
int PushChargesForItem(void *L, const uint8_t *item) {
if (item == nullptr)
return 0;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
item + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 0;
const int32_t stack = static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
const int32_t rawCharges = *reinterpret_cast<const int32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_SPELL_CHARGES_0);
const int32_t usesPerItem = (rawCharges < 0) ? -rawCharges : 1;
Game::Lua::PushNumber(L, static_cast<double>(stack * usesPerItem));
return 1;
}
} // namespace Item::Charges
+44
View File
@@ -0,0 +1,44 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Item::Charges {
// Pushes the total uses available in this single slot — the
// per-slot equivalent of `GetItemCount(_, _, includeCharges=true)`.
// Returns `1` (pushed one value) on success, `0` (pushes nothing,
// caller surfaces as `nil`) when the item is missing or the
// descriptor is null.
//
// Math is `stack * usesPerItem` where `usesPerItem` follows
// `Item::Count::GetUsesPerItem`:
// - `SPELL_CHARGES[0] >= 0` → `1` (no destroy-on-zero behavior;
// stack count alone reflects "how many uses").
// - `SPELL_CHARGES[0] < 0` → `abs(value)` (consume-on-use items;
// wand at 50 has `-50` so each item contributes 50).
//
// Worked examples:
// - Wand at 50 charges (stack=1, raw=-50): `1 * 50 = 50`
// - Hearthstone (stack=1, raw=0): `1 * 1 = 1` (cooldown-
// only, no charges concept; we still surface "1 use").
// - Water stack of 20 (stack=20, raw=-1): `20 * 1 = 20`
// - Equipped sword (stack=1, raw=0): `1 * 1 = 1`
//
// Caller can `return PushChargesForItem(L, item);` to forward the
// count directly.
int PushChargesForItem(void *L, const uint8_t *item);
} // namespace Item::Charges
+113
View File
@@ -0,0 +1,113 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Item.IsItemOpenable(itemLocation)` — backport of the modern WoW
// accessor that says "right-clicking this item triggers an open/loot
// interaction" (clams, sacks, simple chests, quest boxes, lockboxes).
// Returns two values: `(isOpenable, canOpen)` — the first is the
// item-type-intrinsic openable flag, the second is whether the
// player can right-click *this specific instance* right now (lock
// is gone or never existed).
//
// Vanilla 1.12's tooltip builder gates the `<Right Click to Open>`
// string (`ITEM_OPENABLE`) on `ItemStats.Flags & 0x4` AND
// (`LockID == 0` OR `ITEM_FIELD_FLAGS & 0x4`). We mirror both checks
// — see `Offsets::ITEMSTATS_FLAG_OPENABLE`, `ITEMSTATS_LOCK_ID`, and
// `ITEM_FLAG_UNLOCKED`.
#include "Game.h"
#include "Offsets.h"
#include "item/Data.h"
#include "item/ID.h"
#include "item/Location.h"
#include "item/Openable.h"
#include <cstdint>
namespace Item::Openable {
namespace {
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
const uint8_t *PeekItemRecord(uint32_t itemID) {
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
const uint64_t zeroGuid = 0;
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
} // namespace
namespace {
// Reads `ITEM_FIELD_FLAGS` from the instance descriptor at +0x114.
// Defaults to 0 if `cgItem` is null or the descriptor pointer hasn't
// been populated yet.
uint32_t InstanceFlags(const uint8_t *cgItem) {
if (cgItem == nullptr)
return 0;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 0;
return *reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_FLAGS);
}
} // namespace
int PushIsItemOpenable(void *L, const uint8_t *cgItem) {
const int itemID = Item::ID::FromCGItem(cgItem);
if (itemID <= 0)
return 0;
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr) {
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
return 0;
}
const uint32_t typeFlags = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_FLAGS);
const uint32_t lockID = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_LOCK_ID);
const bool isOpenable = (typeFlags & Offsets::ITEMSTATS_FLAG_OPENABLE) != 0;
const uint32_t instFlags = InstanceFlags(cgItem);
const bool canOpen = isOpenable && (lockID == 0 ||
(instFlags & Offsets::ITEM_FLAG_UNLOCKED) != 0);
Game::Lua::PushBool(L, isOpenable);
Game::Lua::PushBool(L, canOpen);
return 2;
}
namespace {
int __fastcall Script_C_Item_IsItemOpenable(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::Error(L, "Usage: C_Item.IsItemOpenable(itemLocation)");
return 0;
}
return PushIsItemOpenable(L, Item::Location::Resolve(L, 1));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "IsItemOpenable",
&Script_C_Item_IsItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Item::Openable
+37
View File
@@ -0,0 +1,37 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Item::Openable {
// Pushes two values for the openable check on a player-owned item
// (`isOpenable, canOpen`), returning 2. The two semantics:
//
// `isOpenable` — bool. The item type carries the openable flag
// (`ItemStats.Flags & 0x4` — clams, sacks, lockboxes, etc.).
// `canOpen` — bool. The player can right-click this specific
// instance and trigger the open action right now. True for
// openable items with no lock (`LockID == 0`) or whose instance
// has been unlocked (`ITEM_FIELD_FLAGS & 0x4` — set after a
// rogue picks the lock, a key is used, etc.). False for fresh
// lockboxes a non-rogue is looking at.
//
// `cgItem` may be null (empty slot), in which case nothing is pushed
// and 0 is returned (Lua sees nil for both returns). Uncached itemID
// also returns 0 plus a background cache warm.
int PushIsItemOpenable(void *L, const uint8_t *cgItem);
} // namespace Item::Openable
+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
+185
View File
@@ -0,0 +1,185 @@
// 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>
#include <cstring>
namespace Map::Info {
namespace {
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
using GroupMemberStatsLookup_t = const uint8_t *(__fastcall *)(const uint64_t *guid);
// Parse a positive 1-based decimal integer from `s`. Returns -1 if `s`
// is empty, has non-digit chars, or doesn't fit `[1, max]`. Used to
// turn the trailing digits of "party3" / "raid17" into a 0-based slot.
int ParseSlot(const char *s, int max) {
if (s == nullptr || *s == '\0')
return -1;
int value = 0;
for (const char *p = s; *p != '\0'; ++p) {
if (*p < '0' || *p > '9')
return -1;
value = value * 10 + (*p - '0');
if (value > max)
return -1;
}
return value >= 1 ? value - 1 : -1;
}
// Look up the GUID stored in the party member slot directly. Slots are
// 0..3 in the parallel GUID table at `VAR_PARTY_MEMBER_GUIDS` (8-byte
// stride). Returns 0 if the slot is empty (no party member there).
uint64_t PartySlotGUID(int slot) {
if (slot < 0 || slot >= 4)
return 0;
auto *guids = reinterpret_cast<const uint64_t *>(
static_cast<uintptr_t>(Offsets::VAR_PARTY_MEMBER_GUIDS));
return guids[slot];
}
// Raid slot storage at `0x00B712A8` is an array of 40 `RaidMember *`
// pointers; the GUID sits at `*member + 0` (lo) and `*member + 4`
// (hi). Verified from `FUN_004bb0f0`'s disassembly:
// MOV EAX, [ECX*4 + 0xb712a8] ; entry = arr[i]
// MOV EDI, [EAX] ; guid_lo
// MOV EAX, [EAX + 4] ; guid_hi
uint64_t RaidSlotGUID(int slot) {
if (slot < 0 || slot >= 40)
return 0;
auto *const *arr = reinterpret_cast<const uint8_t *const *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
const uint8_t *entry = arr[slot];
if (entry == nullptr)
return 0;
return *reinterpret_cast<const uint64_t *>(entry);
}
uint16_t LocalPlayerArea() {
return static_cast<uint16_t>(*reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_PLAYER_AREA_ID)));
}
uint64_t LocalPlayerGUID() {
auto *player = *reinterpret_cast<uint8_t *const *>(
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
if (player == nullptr)
return 0;
return *reinterpret_cast<const uint64_t *>(
player + Offsets::OFF_LOCAL_PLAYER_GUID);
}
// Pushes `area` as a number, or nil if `area == 0`. Vanilla zone 0
// shows up before the engine has initialized the player's location;
// we treat it as "unknown" rather than returning a spurious zero.
void PushAreaOrNil(void *L, uint16_t area) {
if (area == 0) {
Game::Lua::PushNil(L);
} else {
Game::Lua::PushNumber(L, static_cast<double>(area));
}
}
} // namespace
// `C_Map.GetBestMapForUnit(unitToken)` — returns the AreaTable.dbc area
// ID for the given unit. Vanilla 1.12 has no UiMap.db2 concept; the
// closest equivalent is the zone-level AreaTable ID that the engine
// itself tracks for the local player and broadcasts for party/raid
// members via SMSG_PARTY_MEMBER_STATS_FULL.
//
// Coverage:
// - `"player"` — direct read of `VAR_PLAYER_AREA_ID`.
// - `"target"`, `"mouseover"`, `"focus"`, etc. that resolve to the
// local player — same.
// - `"party1".."party4"`, `"raid1".."raid40"`, and tokens resolving to
// any party/raid member's GUID — read via the unified
// `FUN_GROUP_MEMBER_STATS_LOOKUP` engine helper.
// - Anything else (NPCs, units outside the player's group) — nil.
//
// **The returned ID is a vanilla AreaTable.dbc ID, not a modern UI map
// ID.** Addons that hardcode modern IDs (e.g. `if uiMapID == 84 then`
// for retail Stormwind) will not match. Use IDs produced by this same
// function or by `GetCurrentMapAreaID`-style helpers from this backport.
static int __fastcall Script_GetBestMapForUnit(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;
}
// Fast path: "player" doesn't need token resolution.
if (std::strcmp(token, "player") == 0) {
PushAreaOrNil(L, LocalPlayerArea());
return 1;
}
// "partyN" / "raidN" — parse directly so we work for members in
// other zones (no local CGUnit, so the engine's token resolver
// would return NULL). Slot-indexed GUID tables are populated
// continuously by party/raid sync regardless of sync range.
uint64_t guid = 0;
if (std::strncmp(token, "party", 5) == 0) {
guid = PartySlotGUID(ParseSlot(token + 5, 4));
} else if (std::strncmp(token, "raid", 4) == 0) {
guid = RaidSlotGUID(ParseSlot(token + 4, 40));
} else {
// Use TokenToGUID directly so we get the GUID for tokens like
// "target" / "mouseover" even when the unit has no local CGUnit
// (e.g., name-based /target of a party member in another zone).
auto fn = reinterpret_cast<TokenToGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
guid = fn(token);
}
if (guid == 0) {
Game::Lua::PushNil(L);
return 1;
}
// Token resolved to the local player (e.g. "target" on yourself).
if (guid == LocalPlayerGUID()) {
PushAreaOrNil(L, LocalPlayerArea());
return 1;
}
auto lookup = reinterpret_cast<GroupMemberStatsLookup_t>(
static_cast<uintptr_t>(Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP));
const uint8_t *stats = lookup(&guid);
if (stats == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
const uint16_t area = *reinterpret_cast<const uint16_t *>(
stats + Offsets::OFF_GROUP_MEMBER_AREA_ID);
PushAreaOrNil(L, area);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Map", "GetBestMapForUnit",
&Script_GetBestMapForUnit);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Map::Info
+253
View File
@@ -0,0 +1,253 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Nameplate lifecycle events: NAME_PLATE_CREATED, NAME_PLATE_UNIT_ADDED,
// NAME_PLATE_UNIT_REMOVED.
//
// Per-frame poll: walk visible CGUnits with allocated nameplates, diff
// against last frame's snapshot, fire events on real transitions only.
// The engine's internal hide/show cycle (z-order rebuilds, anchor
// changes, ~7 callers of `FUN_00608A10` that transiently zero `+0xE60`)
// is absorbed because we only compare with the previous *frame's*
// state, not every transient `unit + 0xE60` write.
//
// Payload shape (matches modern WoW exactly):
// - `NAME_PLATE_CREATED` — `arg1` is the nameplate **Frame**. The
// engine's printf-style dispatcher (`FUN_FIRE_EVENT`) only knows
// `%s`/`%d`/`%u`/`%f` format codes with no "push Lua value" option,
// so we route this event through a pre-set path: save current
// `_G.arg1`, set it to the frame, fire with empty format
// (dispatcher leaves `_G.arg<N>` alone when no codes are parsed),
// restore.
// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the
// `"nameplateN"` unit token (formatted from the plate's index in
// `g_orderedGUIDs` at fire time). The token resolves to the unit
// via the `nameplateN`-aware token resolver in `unit/TokenExtensions.cpp`
// — addons can pass it straight to `UnitName`, `UnitGUID`, etc.,
// or to `GetNamePlateForUnit` for the frame.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "nameplate/Walk.h"
#include "tick/WorldTick.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace NamePlate::Events {
namespace {
constexpr const char *kEventCreated = "NAME_PLATE_CREATED";
constexpr const char *kEventUnitAdded = "NAME_PLATE_UNIT_ADDED";
constexpr const char *kEventUnitRemoved = "NAME_PLATE_UNIT_REMOVED";
const Event::Custom::AutoReserve _r1{kEventCreated};
const Event::Custom::AutoReserve _r2{kEventUnitAdded};
const Event::Custom::AutoReserve _r3{kEventUnitRemoved};
// Previous tick's snapshot — GUID → nameplate-frame pointer for each
// nameplated unit. Compared against the next tick's walk to compute
// ADDED/REMOVED diffs.
std::unordered_map<uint64_t, const void *> g_lastTickPlates;
// Scratch map reused each tick, swapped into `g_lastTickPlates` at
// the end. File-static so we don't pay the constructor/destructor
// cycle every frame — `clear()` keeps the existing bucket capacity.
std::unordered_map<uint64_t, const void *> g_currentTickPlates;
// Frame pointers we've ever surfaced as nameplate plates. First
// sighting fires NAME_PLATE_CREATED; same pointer reappearing (pool
// reuse) doesn't refire.
//
// Bounded by the engine's CGNamePlateFrame freelist high-water mark:
// `FUN_006087F0` first checks the global freelist head at
// `DAT_00c4d920` and recycles any waiting frame before falling back
// to `SMemAlloc(0x518)`. So the set grows only up to the peak
// simultaneous-visible-plate count for the session — typically
// <80 even in AV-scale scenes (matching the `reserve(64)` below) —
// then tops out as pool reuse covers all subsequent shows.
std::unordered_set<const void *> g_seenPlates;
// Ordered list of currently-visible nameplate GUIDs, in
// creation-order. Append on UNIT_ADDED, erase on UNIT_REMOVED. Backs
// the `nameplateN` unit-token resolver in `unit/TokenExtensions.cpp`. Order
// matches modern WoW semantics: stable for the lifetime of each
// plate, gaps when middle plates vanish (until the next REMOVED
// shifts later entries down).
std::vector<uint64_t> g_orderedGUIDs;
// Fire `eventName` with a pre-formatted string as `arg1`. The engine
// dispatcher's `%s` format code pushes the C string into `_G.arg1`
// as a Lua string — no escaping concerns for our own input
// (`"nameplateN"`).
void FireWithString(const char *eventName, const char *value) {
if (value == nullptr)
return;
const int slot = Event::Custom::Lookup(eventName);
if (slot < 0)
return;
Event::Custom::Fire(slot, "%s", value);
}
// Format a 1-based nameplate index as `"nameplateN"` for ADDED /
// REMOVED event payloads. Buffer should be at least 24 bytes — 9
// for the prefix, up to 10 digits for the index, room for null.
// Returns `buf` for convenient inline use.
const char *FormatNamePlateToken(char *buf, size_t bufSize, int oneBasedIndex) {
std::snprintf(buf, bufSize, "nameplate%d", oneBasedIndex);
return buf;
}
// Fire `eventName` with the nameplate `Frame` set as `_G.arg1`.
// `FUN_FIRE_EVENT`'s format-string parser only handles primitive
// types, but it only mutates `_G.arg<N>` for codes it actually
// parses. With an empty format we pre-set `_G.arg1` and the
// dispatcher leaves it alone. Restore the previous value after
// fire so we don't leak our frame into unrelated global state.
//
// Lua-stack-clean: stack depth on entry == stack depth on exit.
using LuaRefRef_t = int(__fastcall *)(void *L, int t);
using LuaRefUnref_t = void(__fastcall *)(void *L, int t, int ref);
using LuaRawGetI_t = void(__fastcall *)(void *L, int t, int n);
void FireWithFrame(const char *eventName, void *frame) {
if (frame == nullptr)
return;
const int slot = Event::Custom::Lookup(eventName);
if (slot < 0)
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
auto refRef = reinterpret_cast<LuaRefRef_t>(
static_cast<uintptr_t>(Offsets::LUA_REF_REF));
auto refUnref = reinterpret_cast<LuaRefUnref_t>(
static_cast<uintptr_t>(Offsets::LUA_REF_UNREF));
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
// Save current `_G.arg1` to the registry.
Game::Lua::PushString(L, "arg1");
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
const int savedRef = refRef(L, Game::Lua::REGISTRY_INDEX);
// Set `_G.arg1 = frame`.
Game::Lua::PushString(L, "arg1");
NamePlate::Info::PushNamePlateFrame(L, frame);
Game::Lua::SetTable(L, Game::Lua::GLOBALS_INDEX);
// Fire — empty format, so the dispatcher doesn't touch `arg1`.
Event::Custom::Fire(slot, "");
// Restore previous `_G.arg1` from the saved registry ref.
Game::Lua::PushString(L, "arg1");
rawgeti(L, Game::Lua::REGISTRY_INDEX, savedRef);
Game::Lua::SetTable(L, Game::Lua::GLOBALS_INDEX);
refUnref(L, Game::Lua::REGISTRY_INDEX, savedRef);
}
void OnWorldTick() {
g_currentTickPlates.clear();
g_currentTickPlates.reserve(64); // typical visible-nameplate ceiling
NamePlate::Walk::ForEachNamePlatedUnit(
[](const uint8_t *, const uint8_t *nameplate,
const uint8_t *instance) {
const uint64_t guid = *reinterpret_cast<const uint64_t *>(instance);
if (guid == 0)
return;
g_currentTickPlates.emplace(guid, nameplate);
});
// Fire CREATED (with the Frame as arg1) for never-before-seen
// frame pointers; ADDED (with `"nameplateN"` token as arg1) for
// GUIDs not in last tick's snapshot. New entries are appended to
// the ordered list *before* firing so the token resolves to the
// newly-added plate during the event handler.
for (const auto &kv : g_currentTickPlates) {
if (g_seenPlates.insert(kv.second).second)
FireWithFrame(kEventCreated, const_cast<void *>(kv.second));
if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) {
g_orderedGUIDs.push_back(kv.first);
char tokenBuf[24];
FireWithString(kEventUnitAdded,
FormatNamePlateToken(tokenBuf, sizeof tokenBuf,
static_cast<int>(g_orderedGUIDs.size())));
}
}
// Fire REMOVED for GUIDs in last tick's snapshot but not current.
// We compute the token from the position *before* erasing so the
// event payload reflects the slot the unit just vacated; the
// handler can still resolve the token to the unit via
// `g_orderedGUIDs[slot]` during dispatch. Later plates shift down
// when we erase — matches modern semantics.
for (const auto &kv : g_lastTickPlates) {
if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) {
auto it = std::find(g_orderedGUIDs.begin(), g_orderedGUIDs.end(),
kv.first);
if (it == g_orderedGUIDs.end())
continue;
const int oneBased = static_cast<int>(it - g_orderedGUIDs.begin()) + 1;
char tokenBuf[24];
FireWithString(kEventUnitRemoved,
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased));
g_orderedGUIDs.erase(it);
}
}
g_lastTickPlates.swap(g_currentTickPlates);
}
} // namespace
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
// Called from `FrameScript_Initialize_h` ahead of the engine's Lua
// teardown. Clears the diff state alongside `NamePlate::Info`'s
// wrapper-cache reset so that on the first post-reload tick every
// currently-visible plate refires `NAME_PLATE_CREATED` and
// `NAME_PLATE_UNIT_ADDED`. Without this, addons that decorate via
// CREATED (pfUI: builds its overlay button per-pointer) never see
// the existing plates after a `/reload` — `g_seenPlates` would
// suppress every refire, and the freshly-built wrapper would lack
// the addon's `.nameplate` field.
//
// `g_orderedGUIDs` is also cleared so the post-reload token indices
// start at `nameplate1` again, matching the order plates re-fire in.
void PrepareForReload() {
g_seenPlates.clear();
g_lastTickPlates.clear();
g_orderedGUIDs.clear();
}
// Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver
// in `unit/TokenExtensions.cpp` can map an index to a GUID without seeing
// the internal vector.
uint64_t GetGUIDByIndex(int oneBased) {
if (oneBased <= 0)
return 0;
const size_t idx = static_cast<size_t>(oneBased - 1);
if (idx >= g_orderedGUIDs.size())
return 0;
return g_orderedGUIDs[idx];
}
} // namespace NamePlate::Events
+265
View File
@@ -0,0 +1,265 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_NamePlate.GetNamePlates()` — returns nameplate Frame objects,
// matching modern WoW's signature. Two paths because vanilla has two
// kinds of nameplates:
//
// 1. **Addon-created** (pfUI, TidyPlates, etc.) — already registered
// with Lua via `CreateFrame`. Their Lua-registry ref-key sits at
// `+0x08`; we push `registry[refKey]` to return the cached
// wrapper. Identity stable across calls.
//
// 2. **Default vanilla nameplates** — created internally without ever
// calling `CreateFrame`, so `+0x08` is 0. We build a fresh wrapper
// table per call: `{[0] = lightuserdata(frame)}` with the global
// `__framescript_meta` metatable. Methods work
// (`:GetWidth()` / `:GetAlpha()` / etc.) but the wrapper isn't
// cached engine-side — calling `GetNamePlates()` again returns a
// different table for the same frame. Don't compare wrappers by
// identity, and don't cache them across the unit going out of
// range (the underlying frame may be freed).
//
// We deliberately don't call the engine's frame-registration helper
// (`FUN_00701BD0`) for the unregistered case — it increments a Lua
// refcount on the frame that's never decremented, pinning the frame
// in memory.
//
// `C_NamePlate.GetNamePlateGUIDs()` — companion returning the GUID
// strings of the same set of units, regardless of registration
// state. Cheapest enumeration when an addon only needs the GUIDs.
//
// Vanilla 1.12 stores each unit's nameplate pointer at `CGUnit + 0xE60`
// (verified via `FUN_006086E0`'s "ensure nameplate exists" path). The
// nameplate also caches the unit's GUID at `+0x4E8` for back-lookup.
// There's no central "active nameplates" list — the engine maintains
// per-unit pointers updated by per-unit state-change handlers.
//
// To enumerate, we walk the local-player-anchored object hash table
// (`player + 0x1C` = bucket array, `player + 0x24` = mask). Each
// bucket header stores the link-field offset at byte 0 and the
// chain-head pointer at byte 8 — Storm's intrusive-hash pattern.
// Filter by `TYPEMASK_UNIT` (`flags & 0x08` at `*(entry+8) + 8`) and
// check `+0xE60` for a non-null nameplate pointer.
#include "Game.h"
#include "Offsets.h"
#include "guid/Guid.h"
#include "nameplate/Walk.h"
#include <cstdint>
namespace NamePlate::Info {
namespace {
using NamePlate::Walk::ForEachNamePlatedUnit;
using NamePlate::Walk::kOffUnitNamePlate;
using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n);
using ScriptRegister_t = void(__fastcall *)(void *this_, void *edx_unused,
const char *name);
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
} // namespace
static int __fastcall Script_GetNamePlates(void *L) {
Game::Lua::NewTable(L);
int nextIndex = 1;
ForEachNamePlatedUnit(
[L, &nextIndex](const uint8_t *, const uint8_t *nameplate,
const uint8_t *) {
Game::Lua::PushNumber(L, static_cast<double>(nextIndex++));
// Shared helper validates refkey freshness (defensive vs.
// stale-across-reload) and falls back to a fresh wrapper
// if the registry slot doesn't actually hold the frame
// table any more.
PushNamePlateFrame(L, const_cast<uint8_t *>(nameplate));
Game::Lua::SetTable(L, -3);
});
return 1;
}
static int __fastcall Script_GetNamePlateGUIDs(void *L) {
Game::Lua::NewTable(L);
int nextIndex = 1;
ForEachNamePlatedUnit(
[L, &nextIndex](const uint8_t *, const uint8_t *,
const uint8_t *instance) {
const uint64_t guid = *reinterpret_cast<const uint64_t *>(instance);
if (guid == 0)
return;
char buf[Guid::STRING_SIZE];
Game::Lua::PushNumber(L, static_cast<double>(nextIndex++));
Game::Lua::PushString(L,
Guid::FormatAsString(guid, buf, sizeof buf));
Game::Lua::SetTable(L, -3);
});
return 1;
}
// Exported via `nameplate/Walk.h` so `Events.cpp` can reuse the same
// frame-push path. Internal callers (Script_GetNamePlates etc.) call
// it through the unqualified name (they live in the same namespace).
//
// Delegate wrapper construction to the engine's own
// `FrameScript_Object::ScriptRegister`. First call for a never-seen
// nameplate builds a `{[0] = lightuserdata}` table with the
// framescript metatable, `luaL_ref`s it into the registry, and
// writes the refkey to `nameplate + 0x08` (`OFF_COBJECT_LUA_REGISTRY_REF`).
// Subsequent calls find a populated refkey and just rawgeti.
//
// Why delegate instead of building our own wrapper:
//
// - **Single source of truth.** Every code path that pushes this
// frame — ours via this function, addons' `CreateFrame("Type",
// "name", plate)` extracting the parent, the engine's own internal
// pushers — funnels through `rawgeti(REGISTRY, plate+0x08)` and
// gets the same Lua table. Earlier "build our own wrapper +
// `luaL_ref` it" approach kept a side cache, but pfUI's
// `CreateFrame("Button", "name", parent)` inside `NAME_PLATE_CREATED`
// ends up causing the engine to register the parent itself —
// the engine's wrapper diverges from ours, and later
// `GetNamePlateForUnit` calls return the engine's bare wrapper
// instead of our decorated one. Addon fields on the wrapper
// (`plate.nameplate = styledButton`) disappear from the API's
// perspective even though they still exist on the orphaned
// table.
//
// - **Refcount-pinning is benign here.** `ScriptRegister`
// unconditionally increments `nameplate + 0x04`, which normally
// keeps the engine from GC-ing the CObject. Nameplates are
// pool-managed and never destroyed during a session, so pinning
// doesn't actually leak anything. We call `ScriptRegister` at
// most once per nameplate pointer (gated by `refKey <= 0`), so
// the refcount tops out at one increment per pool slot.
//
// Defensive fallback: if the registry slot exists but isn't a table
// any more (some other code freed it), we re-register. This shouldn't
// happen in practice — engines only unref on frame destruction —
// but a stale type check keeps callers from ever seeing a non-table.
void PushNamePlateFrame(void *L, void *nameplate) {
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
auto scriptRegister = reinterpret_cast<ScriptRegister_t>(
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER));
int refKey = *reinterpret_cast<const int *>(
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
if (refKey <= 0) {
scriptRegister(nameplate, nullptr, nullptr);
refKey = *reinterpret_cast<const int *>(
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
}
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE)
return;
Game::Lua::SetTop(L, -2);
// Slot got freed somewhere — re-register to rebuild a fresh
// wrapper and slot. Zero the refcount so `ScriptRegister`
// re-enters its build branch.
*reinterpret_cast<int *>(
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REFCOUNT) = 0;
scriptRegister(nameplate, nullptr, nullptr);
refKey = *reinterpret_cast<const int *>(
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
}
// GUID → nameplate Frame pushed on stack. Pushes nil if the GUID
// doesn't resolve to a visible CGUnit, or the unit has no allocated
// nameplate.
static void PushNamePlateForGUID(void *L, uint64_t guid) {
if (guid == 0) {
Game::Lua::PushNil(L);
return;
}
auto resolve = reinterpret_cast<ResolveByGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
auto *unit = static_cast<uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "NamePlate",
static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32),
0x172));
if (unit == nullptr) {
Game::Lua::PushNil(L);
return;
}
auto *nameplate = *reinterpret_cast<uint8_t *const *>(
unit + kOffUnitNamePlate);
if (nameplate == nullptr) {
Game::Lua::PushNil(L);
return;
}
PushNamePlateFrame(L, nameplate);
}
// `C_NamePlate.GetNamePlateForUnit(unitToken)` — returns the
// nameplate Frame for the given unit, or `nil` if the unit has no
// nameplate (out of range, hidden, etc.). Resolves the token to a
// GUID via the engine's `FUN_TOKEN_TO_GUID` so distant party/raid
// members can be queried.
static int __fastcall Script_GetNamePlateForUnit(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const char *token = Game::Lua::ToString(L, 1);
if (token == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
auto tokenToGuid = reinterpret_cast<TokenToGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
PushNamePlateForGUID(L, tokenToGuid(token));
return 1;
}
// `C_NamePlate.GetNamePlateForGUID(guidString)` — same as
// `GetNamePlateForUnit` but takes a `"0xHHHHHHHHHHHHHHHH"` GUID
// string. Useful for handling the `NAME_PLATE_UNIT_ADDED` /
// `_REMOVED` events whose payload is a GUID, not a unit token.
static int __fastcall Script_GetNamePlateForGUID(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const char *guidStr = Game::Lua::ToString(L, 1);
uint64_t guid = 0;
if (guidStr == nullptr || !Guid::Parse(guidStr, &guid)) {
Game::Lua::PushNil(L);
return 1;
}
PushNamePlateForGUID(L, guid);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlates",
&Script_GetNamePlates);
Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateGUIDs",
&Script_GetNamePlateGUIDs);
Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateForUnit",
&Script_GetNamePlateForUnit);
Game::Lua::RegisterTableFunction("C_NamePlate", "GetNamePlateForGUID",
&Script_GetNamePlateForGUID);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace NamePlate::Info
+121
View File
@@ -0,0 +1,121 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// Shared nameplate enumeration — walks the local-player-anchored
// object hash table for visible `TYPEMASK_UNIT` entries and invokes
// `emit(unit, nameplate, instance)` for each unit that currently has
// an allocated nameplate (`unit + 0xE60` non-null).
//
// Header-only template — used by both the Lua-facing accessors in
// `nameplate/Info.cpp` and the per-tick differ in `nameplate/Events.cpp`.
namespace NamePlate::Info {
// Pushes the nameplate Frame at `nameplate` onto the Lua stack.
// Returns the engine's canonical wrapper table — the same one
// `rawgeti(REGISTRY, nameplate + 0x08)` resolves to anywhere else
// in the engine — so addon-set fields (pfUI: `plate.nameplate`)
// survive every roundtrip through our API. Defined in `Info.cpp`.
void PushNamePlateFrame(void *L, void *nameplate);
} // namespace NamePlate::Info
namespace NamePlate::Events {
// Returns the GUID currently bound to `nameplateN` (1-based, matching
// the modern Lua token form). `0` means no nameplate at that index.
// Backed by the per-tick UNIT_ADDED / UNIT_REMOVED diff in
// `Events.cpp` — order is creation-order (append on ADDED, erase on
// REMOVED), stable within a frame.
uint64_t GetGUIDByIndex(int oneBased);
// Clears the per-tick diff state (seen pointers, last tick's
// snapshot, ordered GUID list) so the post-`/reload` tick refires
// CREATED + UNIT_ADDED for every currently-visible plate. Called
// from `FrameScript_Initialize_h` alongside the wrapper-cache reset
// in `NamePlate::Info::PrepareForReload`.
void PrepareForReload();
} // namespace NamePlate::Events
namespace NamePlate::Walk {
// `0x00B41414` is a player-related global that overlays multiple
// sub-objects at different offsets. We read the object-hash-table
// fields at `+0x1C` (bucket array) and `+0x24` (bucket mask) — that
// sub-struct is safe to walk for any unit the engine knows about.
// **Do not feed this pointer to inventory routines** (`GetItemBySlot`
// etc.); the CGPlayer_C inventory manager lives at a different
// offset (`+0x1D38`) and is sourced via `ResolveUnitToken("player")`,
// not via this global. See CLAUDE.md "Resolving an item-location"
// for the inventory-crash story.
constexpr uintptr_t kLocalPlayerGlobal = 0x00B41414;
constexpr int kOffPlayerBucketArray = 0x1C;
constexpr int kOffPlayerBucketMask = 0x24;
constexpr int kBucketStride = 12;
constexpr int kBucketLinkOffsetField = 0;
constexpr int kBucketChainHeadField = 8;
constexpr int kOffEntryInstanceBlock = 0x08;
constexpr int kOffInstanceTypeMask = 0x08;
constexpr uint32_t kTypeMaskUnit = 0x08;
constexpr int kOffUnitNamePlate = 0xE60;
template <typename F>
int ForEachNamePlatedUnit(F &&emit) {
auto *player = *reinterpret_cast<uint8_t *const *>(kLocalPlayerGlobal);
if (player == nullptr)
return 0;
auto *buckets = *reinterpret_cast<uint8_t *const *>(
player + kOffPlayerBucketArray);
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
player + kOffPlayerBucketMask);
if (buckets == nullptr || mask == 0xFFFFFFFFu)
return 0;
int count = 0;
for (uint32_t b = 0; b <= mask; ++b) {
const uint8_t *bucket = buckets + b * kBucketStride;
const uint32_t linkOffset = *reinterpret_cast<const uint32_t *>(
bucket + kBucketLinkOffsetField);
uintptr_t entry = *reinterpret_cast<const uintptr_t *>(
bucket + kBucketChainHeadField);
while (entry != 0 && (entry & 1) == 0) {
auto *obj = reinterpret_cast<const uint8_t *>(entry);
auto *instance = *reinterpret_cast<const uint8_t *const *>(
obj + kOffEntryInstanceBlock);
if (instance != nullptr) {
const uint32_t typeMask = *reinterpret_cast<const uint32_t *>(
instance + kOffInstanceTypeMask);
if ((typeMask & kTypeMaskUnit) != 0) {
const auto *nameplate = *reinterpret_cast<const uint8_t *const *>(
obj + kOffUnitNamePlate);
if (nameplate != nullptr) {
emit(obj, nameplate, instance);
++count;
}
}
}
entry = *reinterpret_cast<const uintptr_t *>(
obj + linkOffset + 4);
}
}
return count;
}
} // namespace NamePlate::Walk
+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
+7 -29
View File
@@ -12,39 +12,17 @@
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Unit::Combat {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
// `InCombatLockdown()` — returns whether the local player is currently
// in combat. Modern WoW gates secure-frame UI manipulation on this; 1.12
// has no secure-frame system, so the function reduces to "is the player
// in combat" — `UnitAffectingCombat("player")` would compute the same
// answer, just via the slower string→token→unit→fields path. We read
// the flag directly off the player CGUnit's m_objectFields.
//
// The bit (`UNIT_FLAG_IN_COMBAT = 0x00080000`) is the same one
// `Script_UnitAffectingCombat` at `0x00517E4A`-`0x517E5C` tests.
// `InCombatLockdown()` — always returns `false` in 1.12.1. The
// secure-frame system that combat lockdown gates didn't exist in
// vanilla; the function is provided purely so that addons backported
// from later expansions can call it without erroring. Addons that
// actually want "is the player in combat?" should use
// `UnitAffectingCombat("player")`, which the stock 1.12 engine ships.
static int __fastcall Script_InCombatLockdown(void *L) {
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<const uint8_t *>(resolve("player"));
if (player == nullptr) {
Game::Lua::PushBool(L, 0);
return 1;
}
auto *fields = *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_UNIT_DESCRIPTOR);
if (fields == nullptr) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
fields + Offsets::OFF_UNIT_FIELD_FLAGS);
Game::Lua::PushBoolean(L, (flags & Offsets::UNIT_FLAG_IN_COMBAT) != 0);
Game::Lua::PushBool(L, false);
return 1;
}
+127
View File
@@ -0,0 +1,127 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `FocusUnit(unit)` / `ClearFocus()` / `focus` unit token. Polyfills
// the post-vanilla focus-target system on top of the engine's
// existing token resolver via the hook in
// `unit/TokenExtensions.cpp`.
//
// Storage: a single 64-bit GUID held in-process. The 3.3.5 engine
// keeps the same shape (two globals at `DAT_00BD07D0`/`D4`); we
// don't share those because vanilla never reads them. The resolver
// hook reads our `Unit::Focus::Get()` directly.
//
// Setter mirrors 3.3.5's `FUN_0051FF20` minus the post-vanilla
// `UnitFlag bit 0x2000` ("focus glow" rendering hint, introduced in
// TBC for the default focus frame). Vanilla addons (pfUI) render
// their own focus indicator and don't need the hint.
//
// Auto-clear on despawn: modern WoW fires `PLAYER_FOCUS_CHANGED`
// when the focused unit leaves the client's object table (out of
// rendering range, despawn) and does NOT auto-refocus when they
// come back. We mirror that by probing `ObjectByGUID(g_focusGUID)`
// every world tick — when it returns null, we `Set(0)` which fires
// the event. Cost is one hash-table lookup per tick while focus is
// set; zero when no focus is active.
#include "Focus.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Unit::Focus {
namespace {
constexpr const char *kEventName = "PLAYER_FOCUS_CHANGED";
const Event::Custom::AutoReserve _reserve{kEventName};
uint64_t g_focusGUID = 0;
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
uint64_t ResolveTokenGUID(const char *token) {
if (token == nullptr || *token == '\0')
return 0;
auto fn = reinterpret_cast<TokenToGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
return fn(token);
}
// Per-tick despawn watcher. Probes the engine's object table for
// `g_focusGUID`; when it disappears (out of range, fully despawned),
// clear focus and fire PLAYER_FOCUS_CHANGED — matches modern's
// "leaves render distance → focus drops" behavior. Won't refocus
// when the unit comes back, also matching modern.
void OnWorldTick() {
if (g_focusGUID == 0)
return;
auto resolve = reinterpret_cast<ResolveByGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
if (resolve(Offsets::OBJ_TYPE_UNIT, "Focus",
static_cast<uint32_t>(g_focusGUID),
static_cast<uint32_t>(g_focusGUID >> 32),
0x172) == nullptr)
Set(0);
}
// `FocusUnit(unit)` — sets focus to whatever GUID `unit` currently
// resolves to. Modern signature: `FocusUnit(unit)` with the token
// argument. Modern also allows `FocusUnit()` (no arg) to mean
// `"target"`, matching the `/focus` slash command's default.
int __fastcall Script_FocusUnit(void *L) {
if (Game::Lua::IsString(L, 1)) {
const char *token = Game::Lua::ToString(L, 1);
Set(ResolveTokenGUID(token));
} else {
Set(ResolveTokenGUID("target"));
}
return 0;
}
// `ClearFocus()` — drops the focus, fires PLAYER_FOCUS_CHANGED.
int __fastcall Script_ClearFocus(void *L) {
(void)L;
Set(0);
return 0;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("FocusUnit", &Script_FocusUnit);
Game::Lua::RegisterGlobalFunction("ClearFocus", &Script_ClearFocus);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace
uint64_t Get() { return g_focusGUID; }
void Set(uint64_t guid) {
if (guid == g_focusGUID)
return; // no-op: same target → no event fire (matches 3.3.5)
g_focusGUID = guid;
const int slot = Event::Custom::Lookup(kEventName);
if (slot >= 0)
Event::Custom::Fire(slot, "");
}
} // namespace Unit::Focus
+31
View File
@@ -0,0 +1,31 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Unit::Focus {
// Current focus GUID, or `0` when nothing is focused. Read by the
// `focus` / `focustarget*` branch of the token resolver in
// `unit/TokenExtensions.cpp`.
uint64_t Get();
// Sets focus to `guid` and fires `PLAYER_FOCUS_CHANGED` (no args).
// Identity-checks first — assigning the same GUID is a no-op,
// matching 3.3.5's `FUN_0051FF20` behavior so the event only fires
// on a real transition. Pass `0` to clear focus.
void Set(uint64_t guid);
} // namespace Unit::Focus
+27 -5
View File
@@ -14,6 +14,7 @@
#include "Game.h"
#include "Offsets.h"
#include "guid/Guid.h"
#include "nameplate/Walk.h"
#include <cstdint>
#include <cstdio>
@@ -105,13 +106,17 @@ static int __fastcall Script_UnitGUID(void *L) {
// nil if none of the known tokens point at it.
//
// The search walks the modern retail token order with post-vanilla
// tokens dropped (no `vehicle`, `nameplateN`, `arenaN`, `arenapetN`,
// `bossN`, `focus`, `softenemy`, `softfriend`, `softinteract` —
// those all post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise
// "Unknown unit name" for them). Order:
// tokens dropped (no `vehicle`, `arenaN`, `arenapetN`, `bossN`,
// `softenemy`, `softfriend`, `softinteract` — those all post-date
// 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit name" for
// them). `nameplateN` and `focus` are included — we hook the token
// resolver so the engine recognizes them (see
// `unit/TokenExtensions.cpp` and `unit/Focus.cpp`). Order matches
// retail with the dropped tokens skipped in place:
//
// player → pet → party1..4 → partypet1..4 → raid1..40
// → raidpet1..40 → target → npc → mouseover
// → raidpet1..40 → nameplate1..N → target → focus
// → npc → mouseover
//
// The result is inherently unstable — multiple tokens can map to the
// same GUID at once (your target IS your mouseover IS your party1), and
@@ -183,7 +188,24 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
if (check(buf)) return 1;
}
// Visible nameplates. The ordered list (see
// `NamePlate::Events::g_orderedGUIDs`) is dense — `GetGUIDByIndex`
// returns 0 once we step past the populated count, terminating
// the loop. Skips the token-resolver hook + SStrCmpI cost per
// index by comparing GUIDs directly.
for (int i = 1; ; ++i) {
const uint64_t plateGuid = NamePlate::Events::GetGUIDByIndex(i);
if (plateGuid == 0)
break;
if (plateGuid == target) {
std::snprintf(buf, sizeof buf, "nameplate%d", i);
Game::Lua::PushString(L, buf);
return 1;
}
}
if (check("target")) return 1;
if (check("focus")) return 1;
if (check("npc")) return 1;
if (check("mouseover")) return 1;
+153
View File
@@ -0,0 +1,153 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Extend `FUN_TOKEN_TO_GUID` (`0x00515970`) with post-vanilla unit
// tokens. Hooking the central token→GUID resolver means every
// `Script_Unit*` (UnitName, UnitGUID, UnitHealth, UnitClass,
// UnitExists, …) gains support for the new token families for free,
// since they all funnel here.
//
// Currently extended families:
//
// - `nameplate1`..`nameplateN` — index into the ordered list of
// visible nameplates maintained in `nameplate/Events.cpp`. See
// `Unit tokens (nameplateN)` in docs/API.md.
//
// - `focus` — single-slot sticky target backed by `Unit::Focus`.
// See `unit/Focus.cpp` for the storage + `PLAYER_FOCUS_CHANGED`
// event firing.
//
// Both families compose with the engine's own `target`-suffix walker
// (`focustarget`, `nameplate1targettarget`) via the shared
// `WalkSuffix` helper, which mirrors the engine's
// `LAB_005159d3..0x00515A2C` walker instruction-for-instruction
// (SStrCmpI "target" + ObjectByGUID + read UNIT_FIELD_TARGET).
//
// Resolution order in the hook: focus check first (literal-equal,
// short token), then nameplate prefix (longer, requires digit).
// Both branches short-circuit; on no-match the original engine
// resolver runs unchanged.
#include "Game.h"
#include "Offsets.h"
#include "nameplate/Walk.h"
#include "unit/Focus.h"
#include <cstdint>
namespace Unit::TokenExtensions {
namespace {
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
// `__stdcall`, not `__cdecl` — `FUN_00064A4C0` ends with `ret 0xc`,
// callee cleans 3-arg stack frame. Calling it through a cdecl typedef
// makes MSVC emit `add esp, 12` after the call on top of the callee's
// own cleanup, drifting ESP +12 per call and corrupting the caller's
// frame.
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
constexpr const char kNamePlatePrefix[] = "nameplate";
constexpr int kNamePlatePrefixLen = static_cast<int>(sizeof(kNamePlatePrefix) - 1);
constexpr const char kFocusPrefix[] = "focus";
constexpr int kFocusPrefixLen = static_cast<int>(sizeof(kFocusPrefix) - 1);
constexpr const char kSuffixTarget[] = "target";
constexpr int kSuffixTargetLen = static_cast<int>(sizeof(kSuffixTarget) - 1);
// Engine's own priority value for the suffix walker's ObjectByGUID
// calls (verified at `0x005159FE: PUSH 0x6E`). Different from the
// `0x172` we use elsewhere — matching the engine here so the
// resolver's lookup path is identical to the native `targettarget`
// walker.
constexpr int kResolvePriority = 0x6e;
TokenToGUID_t Original_o = nullptr;
uint64_t WalkSuffix(uint64_t guid, const char *suffix) {
auto resolve = reinterpret_cast<ResolveByGUID_t>(
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
while (guid != 0 && *suffix != '\0') {
if (sstrcmpi(suffix, kSuffixTarget, kSuffixTargetLen) != 0)
return 0; // unknown suffix component — modern returns nil
suffix += kSuffixTargetLen;
auto *obj = static_cast<uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "ClassicAPI",
static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32),
kResolvePriority));
if (obj == nullptr)
return 0;
// `m_objectFields` is a *pointer* stored at `obj + 0x110`,
// not an inline array. Engine's walker does
// `mov eax, [obj+0x110]; mov edi, [eax+0x28]` — two
// dereferences. Earlier version of this code missed the
// first one and read instruction bytes interpreted as GUIDs.
auto *fields = *reinterpret_cast<const uint8_t *const *>(
obj + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
if (fields == nullptr)
return 0;
guid = *reinterpret_cast<const uint64_t *>(
fields + Offsets::OFF_UNIT_FIELD_TARGET);
}
return guid;
}
uint64_t __fastcall Hook_h(const char *token) {
if (token == nullptr || *token == '\0')
return Original_o(token);
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
// `focus` / `focustarget*`. The 5-char prefix is unique among
// unit tokens (no `focuscast` / `focused*` etc. live in vanilla
// or modern resolvers), so any match continues as our token.
if (sstrcmpi(token, kFocusPrefix, kFocusPrefixLen) == 0) {
const uint64_t guid = Unit::Focus::Get();
if (guid == 0)
return 0;
return WalkSuffix(guid, token + kFocusPrefixLen);
}
// `nameplateN` / `nameplate1target*`. Requires a digit after the
// prefix; anything else (`nameplate`, `nameplateX`) falls through
// to the engine for its standard "Unknown unit name" error.
if (sstrcmpi(token, kNamePlatePrefix, kNamePlatePrefixLen) == 0) {
const char *p = token + kNamePlatePrefixLen;
if (*p >= '0' && *p <= '9') {
int index = 0;
while (*p >= '0' && *p <= '9') {
index = index * 10 + (*p - '0');
++p;
}
const uint64_t guid = NamePlate::Events::GetGUIDByIndex(index);
if (guid == 0)
return 0;
return WalkSuffix(guid, p);
}
}
return Original_o(token);
}
} // namespace
static const Game::HookAutoRegister _hook{
Offsets::FUN_TOKEN_TO_GUID,
reinterpret_cast<void *>(&Hook_h),
reinterpret_cast<void **>(&Original_o)};
} // namespace Unit::TokenExtensions