Compare commits

...
11 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
17 changed files with 878 additions and 89 deletions
+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;
-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
""
+6 -3
View File
@@ -23,19 +23,20 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Macros](docs/API.md#macros) | `GetMacroSpell` |
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
@@ -65,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) |
@@ -98,6 +99,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| `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)* |
@@ -125,6 +127,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| 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
+267 -19
View File
@@ -34,7 +34,9 @@ build instructions.
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
- [`C_Container.GetContainerItemDurability(containerIndex, slotIndex)`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
- [`C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)`](#c_containergetcontaineritemrepaircostcontainerindex-slotindex)
- [`C_Container.GetContainerItemCharges(containerIndex, slotIndex)`](#c_containergetcontaineritemchargescontainerindex-slotindex)
- [`C_Container.GetContainerNumFreeSlots(bagID)`](#c_containergetcontainernumfreeslotsbagid)
- [`C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`](#c_containeriscontaineritemopenablecontainerindex-slotindex)
- [`C_Container.PlayerHasHearthstone()`](#c_containerplayerhashearthstone)
- [`C_Container.UseHearthstone()`](#c_containerusehearthstone)
- [`C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)`](#c_containerswapitemssrcbag-srcslot-dstbag-dstslot)
@@ -74,6 +76,7 @@ build instructions.
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
- [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event)
- [`NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` events](#name_plate_created--name_plate_unit_added--name_plate_unit_removed-events)
- [`PLAYER_FOCUS_CHANGED` event](#player_focus_changed-event)
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
@@ -93,6 +96,11 @@ build instructions.
- [`C_Reputation.SetWatchedFactionByID(factionID)`](#c_reputationsetwatchedfactionbyidfactionid)
- [`C_Reputation.GetLastStandingChange()`](#c_reputationgetlaststandingchange)
- [Focus](#focus)
- [`FocusUnit(unit)`](#focusunitunit)
- [`ClearFocus()`](#clearfocus)
- [Unit token (`focus` / `focustarget`)](#unit-token-focus--focustarget)
- [FriendList](#friendlist)
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
- [`C_FriendList.IsWhoQueryPending()`](#c_friendlistiswhoquerypending)
@@ -171,6 +179,7 @@ build instructions.
- [`C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`](#c_itemgetitemuniquenessitemlocation--c_itemgetitemuniquenessbyiditem)
- [`C_Item.GetItemLink(itemLocation)`](#c_itemgetitemlinkitemlocation)
- [`C_Item.GetItemInventoryType(itemLocation)` / `C_Item.GetItemInventoryTypeByID(item)`](#c_itemgetiteminventorytypeitemlocation--c_itemgetiteminventorytypebyiditem)
- [`C_Item.IsItemOpenable(itemLocation)`](#c_itemisitemopenableitemlocation)
- [`C_Item.IsLocked(itemLocation)`](#c_itemislockeditemlocation)
- [`C_Item.LockItem(itemLocation)`](#c_itemlockitemitemlocation)
- [`C_Item.LockItemByGUID(itemGUID)`](#c_itemlockitembyguiditemguid)
@@ -641,10 +650,6 @@ if UnitAffectingCombat("player") then
end
```
Equivalent to `UnitAffectingCombat("player")` but faster — reads the
`UNIT_FLAG_IN_COMBAT` bit directly off the player CGUnit's
m_objectFields, no token-resolution roundtrip.
## Container
### `C_Container.GetContainerItemID(bagIndex, slotIndex)`
@@ -764,6 +769,62 @@ Useful for "repair only items above N copper" smart-repair logic
without scanning tooltips. ClassicAPI addition; modern WoW has no
direct equivalent.
### `C_Container.GetContainerItemCharges(containerIndex, slotIndex)`
Per-slot equivalent of
[`C_Item.GetItemCount`](#c_itemgetitemcountitem-includebank-includecharges)'s
`includeCharges=true` mode — returns the total uses available in
*this single slot*, where `GetItemCount` totals the same value
across every matching slot.
```
uses = C_Container.GetContainerItemCharges(containerIndex, slotIndex)
```
`containerIndex` accepts the same values as
[`C_Container.GetContainerItemDurability`](#c_containergetcontaineritemdurabilitycontainerindex-slotindex)
(`0` = backpack, `1..4` = equipped bags, `-1`/`5..11` = bank, etc.).
`slotIndex` is 1-based.
Math is `stack * usesPerItem`, where `usesPerItem` is
`abs(SPELL_CHARGES[0])` for items with a negative charges field
(consume-on-use: wands, healthstones, mana gems, sapper charges)
and `1` for everything else. Worked examples:
| Slot contents | Stack | Raw charges | Returns |
|---|---:|---:|---:|
| Wand of Decay at 50 charges | 1 | -50 | **50** |
| Wand at 1 charge | 1 | -1 | **1** |
| Healthstone | 1 | -1 | **1** |
| Stack of 20 water | 20 | -1 | **20** |
| Stack of 5 mana potions | 5 | -1 | **5** |
| Hearthstone (cooldown-only) | 1 | 0 | **1** |
| Any other item in the slot | 1 | 0 | **1** |
Returns `nil` only when the slot is empty.
```lua
-- "how many charges does my wand have left?"
local charges = C_Container.GetContainerItemCharges(0, 1)
print("Wand charges remaining: " .. charges)
-- "how many drinks across the bags?" — same as GetItemCount(water, false, true)
local total = 0
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
if C_Container.GetContainerItemID(bag, slot) == waterID then
total = total + C_Container.GetContainerItemCharges(bag, slot)
end
end
end
```
ClassicAPI addition; modern WoW has no direct equivalent (modern
addons read charges off tooltip text). Useful when you need a
per-slot number rather than `GetItemCount`'s rollup — e.g. to
display "X charges" on the slot UI for a wand without re-walking
every other matching item.
### `C_Container.GetContainerNumFreeSlots(bagID)`
Returns the number of empty slots in the given bag, plus the bag's
@@ -817,6 +878,27 @@ internally and counts slots that resolve to a null `CGItem *` (i.e.,
empty). Cross-checked in-game against a manual
`C_Container.GetContainerItemID` walk; counts match.
### `C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`
Positional-arg wrapper for [`C_Item.IsItemOpenable`](#c_itemisitemopenableitemlocation--c_itemisitemopenablebyiditem)
against a bag/slot pair. Same `(isOpenable, canOpen)` tuple — see
the linked section for full semantics. Both returns are `nil` for
empty slots or items whose data hasn't been cached yet.
```lua
-- Sweep main backpack for openables
for slot = 1, GetContainerNumSlots(0) do
if C_Container.IsContainerItemOpenable(0, slot) then
-- ...
end
end
```
Same cache-warming caveat as the underlying `C_Item.IsItemOpenable` —
a freshly-seen item may report `false` for one call while the
`SMSG_ITEM_QUERY_SINGLE` round-trips. Bag-resident items are almost
always already cached.
### `C_Container.PlayerHasHearthstone()`
Returns the itemID of the hearthstone if one is in the player's bags,
@@ -1649,6 +1731,33 @@ show path re-allocates from the pool. Those transient zeroes never
become events because the unit appears in both the previous and
current tick's snapshot.
### `PLAYER_FOCUS_CHANGED` event
Fires whenever the player's focus changes — assignment via
[`FocusUnit`](#focusunitunit), clear via [`ClearFocus`](#clearfocus),
or a `FocusUnit(token)` call where the token resolves to no GUID
(implicit clear). No payload args; the new focus GUID is read off
[`UnitGUID("focus")`](#unitguidunit) in the handler.
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_FOCUS_CHANGED")
f:SetScript("OnEvent", function()
local guid = UnitGUID("focus")
if guid then
print("focusing", UnitName("focus"))
else
print("focus cleared")
end
end)
```
**Identity-checked**: assigning the same GUID twice is a no-op
(no event refires), matching 3.3.5's `FUN_0051FF20` behavior.
Mirrors modern's documented semantics — "fired whenever the
player's focus target is changed, including when the focus target
is lost or cleared".
### `QUEST_ACCEPTED` event
Fires once per quest the player just accepted, with two payload args:
@@ -2065,6 +2174,82 @@ This is a ClassicAPI-only call; modern WoW has no equivalent (modern
addons get the factionID from `FACTION_STANDING_CHANGED` directly, so
they don't need a separate getter).
## Focus
Polyfills modern WoW's focus-target system: a single sticky GUID
that addons can pin to a unit and address via the `"focus"` unit
token across the entire `UnitX` API surface. Backed by a hook on
`FUN_TOKEN_TO_GUID` (shared with the [`nameplateN`](#unit-tokens-nameplaten)
tokens — see `unit/TokenExtensions.cpp`).
State is **session-only** — drops on `/reload`, `/logout`, and zone
loads that recreate Lua state. Modern Classic Era behaves the same;
addons that want persistence have to re-`FocusUnit` from `SavedVariables`
at `ADDON_LOADED`.
**Auto-clear on despawn.** When the focused unit leaves the client's
object table — out of rendering range, full despawn, dies and
decays — focus drops and [`PLAYER_FOCUS_CHANGED`](#player_focus_changed-event)
fires. The unit re-entering range does NOT auto-refocus. Matches
modern WoW's documented behavior. Implementation: per-tick
`ObjectByGUID(g_focusGUID)` probe; on null result, `Set(0)`.
### `FocusUnit(unit)`
Sets focus to the given unit. Argument is a unit token —
`"target"`, `"mouseover"`, `"party1"`, `"nameplate1"`, anything the
resolver accepts. Calling with no argument is shorthand for
`FocusUnit("target")` (matches modern's `/focus` slash command
default).
```lua
FocusUnit("target") -- pin the current target
FocusUnit("nameplate1") -- pin the unit behind nameplate1
FocusUnit() -- same as FocusUnit("target")
```
Fires [`PLAYER_FOCUS_CHANGED`](#player_focus_changed-event) if the
resolved GUID differs from the current focus. No-op (no event) if
the same unit is already focused — matches the
identity-check-first behavior of 3.3.5's `FUN_0051FF20`.
Passing a token that doesn't resolve to a unit (e.g. `"target"`
with nothing targeted, or `"party5"` solo) clears focus — same as
calling `ClearFocus()`.
### `ClearFocus()`
Drops the focus. Fires `PLAYER_FOCUS_CHANGED` if there was one;
no-op otherwise.
```lua
ClearFocus()
```
### Unit token (`focus` / `focustarget`)
`"focus"` resolves to whatever GUID `FocusUnit` last stashed.
Accepted by every `UnitX` function:
```lua
local name = UnitName("focus") -- nil if no focus
local hp = UnitHealth("focus")
local _, class = UnitClass("focus")
```
Chains compose through the engine's standard suffix walker:
`"focustarget"`, `"focustargettarget"`, etc. — same behavior as
`"targettarget"`, mirrored instruction-for-instruction in our hook
so addons get the engine semantics they expect.
Returns `nil` cleanly when no focus is set; doesn't raise the
"Unknown unit name" error.
[`UnitTokenFromGUID`](#unittokenfromguidguid) scans `"focus"` right
after `"target"` (matching retail order), so a focused unit's GUID
reverse-resolves to `"focus"` only if it isn't already addressable
as `"player"` / `"party*"` / `"raid*"` / `"nameplate*"` / `"target"`.
## FriendList
### `C_FriendList.SendWhoQueryByName(name)`
@@ -2552,14 +2737,28 @@ lines; re-saving a set repopulates itemIDs.
### `CLASSIC_API_VERSION`
Defined once FrameScript has booted. Addons can use this to detect that
the DLL is loaded and which version is in use. The value is
`X*10000 + Y*100 + Z` for a tag of `vX.Y.Z` passed to CMake at configure
time via `-DCLASSICAPI_TAG=vX.Y.Z`. Unset versions resolve to `0`.
Defined once FrameScript has booted. Addons can use this to detect
that the DLL is loaded and which version is in use. The value is
`X*10000 + Y*100 + Z` for a tag of `vX.Y.Z` passed to CMake at
configure time via `-DCLASSICAPI_TAG=vX.Y.Z`.
Untagged builds (local dev, CI without a release tag) get a
**sentinel value of `99999999`** — encoded as `v9999.99.99`, chosen
to be higher than every plausible real release so addon-side
feature gates like `CLASSIC_API_VERSION >= 10200` don't reject the
dev build.
```lua
if CLASSIC_API_VERSION and CLASSIC_API_VERSION >= 10200 then
-- ClassicAPI v1.2.0 or newer is loaded
-- ClassicAPI v1.2.0 or newer is loaded (or untagged dev build)
end
```
If you specifically want to detect a dev build:
```lua
if CLASSIC_API_VERSION == 99999999 then
-- running against a locally-built, untagged DLL
end
```
@@ -3890,6 +4089,55 @@ if t == 1 then -- head
end
```
### `C_Item.IsItemOpenable(itemLocation)`
Returns two values: `(isOpenable, canOpen)`.
- **`isOpenable`** — `true` if the item type is right-click-openable
(sack, clam, simple chest, quest box, lockbox, etc. — anything
whose tooltip *could* show `<Right Click to Open>`). Intrinsic to
the item type. From `ItemStats.Flags & 0x4`.
- **`canOpen`** — `true` if the player can right-click *this specific
instance* and trigger the open action right now. For unlocked-by-
default items (clams, sacks) this matches `isOpenable`. For items
with a lock (lockboxes), it's only true after the lock has been
removed (rogue Pick Lock, key item, etc.) — so a priest looking at
a fresh lockbox sees `(true, false)`, while a rogue who just
picked it sees `(true, true)`.
Both values are `nil` when the item data isn't cached yet (background
`SMSG_ITEM_QUERY_SINGLE` fired) or when the location is empty /
invalid. Lets callers distinguish "data unknown" from "definitely
not openable".
There's no `ByID` variant — the `canOpen` check depends on a specific
instance's `ITEM_FIELD_FLAGS`, which you can only have via an
itemLocation. If you only have an itemID, use
`C_Item.GetItemInfoInstant` to inspect the item type instead.
```lua
-- Loop a bag and gather every openable the player can act on now
for slot = 1, GetContainerNumSlots(0) do
local _, canOpen = C_Container.IsContainerItemOpenable(0, slot)
if canOpen then
-- pickup + use; this slot is good to open
end
end
-- Show a different hint depending on the lock state
local isOpenable, canOpen = C_Item.IsItemOpenable({bagID = 0, slotIndex = 1})
if isOpenable and not canOpen then
print("This item is locked.")
end
```
Implementation: `isOpenable` reads `ItemStats.Flags & 0x4` from the
client-side ItemSparse cache. `canOpen` adds a check that either
`ItemStats.LockID == 0` (no lock) or the CGItem instance's
`ITEM_FIELD_FLAGS & 0x4` (UNLOCKED) bit is set. Same conditions the
engine's tooltip builder uses at `0x0052E323` to gate the
`ITEM_OPENABLE` line.
### `C_Item.IsLocked(itemLocation)`
Returns `true` if the item is in the client-side "in-transaction"
@@ -6769,19 +7017,19 @@ known unit tokens and return the first one currently mapped to that
GUID, or `nil` if none of them point at it.
The search order matches modern retail with post-1.12 tokens
omitted (`vehicle`, `arenaN`, `arenapetN`, `bossN`, `focus`,
`softenemy`, `softfriend`, `softinteract` all post-date vanilla and
the engine's resolver doesn't recognize them). `nameplateN` is
included between `raidpetN` and `target` — we hook the resolver so
the engine recognizes the token form (see
[Unit tokens](#unit-tokens-nameplaten)). As with the other
positional tokens, the returned `"nameplate3"`-style result is only
valid at that instant; the slot may shift to a different unit
between calls, so re-verify with `UnitGUID(token)` before reusing.
omitted (`vehicle`, `arenaN`, `arenapetN`, `bossN`, `softenemy`,
`softfriend`, `softinteract` all post-date vanilla and the engine's
resolver doesn't recognize them). `nameplateN` and `focus` are
included — we hook the resolver so the engine recognizes both token
forms. As with the other positional tokens, a returned
`"nameplate3"` result is only valid at that instant; the slot may
shift to a different unit between calls, so re-verify with
`UnitGUID(token)` before reusing.
```
player → pet → party1..4 → partypet1..4 → raid1..40
→ raidpet1..40 → nameplate1..N → target → npc → mouseover
→ raidpet1..40 → nameplate1..N → target → focus
→ npc → mouseover
```
```lua
+34
View File
@@ -893,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
@@ -1060,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-
+55
View File
@@ -0,0 +1,55 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
// returns the `abs(SPELL_CHARGES[0])` value of the item in the
// specified bag slot, or `nil` for empty slots and items that don't
// carry a charge concept. Wands return their remaining cast count
// (e.g. 50 → 49 → …); single-use items (Healthstone, Mana Gem,
// Goblin Sapper Charge) return `1`.
//
// Modern equivalent: none — modern WoW exposes charges only through
// tooltip text scanning. `C_Item.GetItemCount(includeBank, includeCharges)`
// totals charges across every matching stack; this is the per-slot
// reader.
#include "Game.h"
#include "item/Charges.h"
#include "item/Location.h"
namespace Container::Charges {
namespace {
int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L,
"Usage: C_Container.GetContainerItemCharges(containerIndex, slotIndex)");
return 0;
}
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
return Item::Charges::PushChargesForItem(
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::Charges
+49
View File
@@ -0,0 +1,49 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Container.IsContainerItemOpenable(bag, slot)` — positional-arg
// wrapper around `Item::Openable::PushIsItemOpenable`. Same chain
// `C_Container.GetContainerItemDurability` and friends use:
// `ResolveBag` → `GetItemBySlot`. Returns the
// `(isOpenable, canOpen)` tuple — see [[item/Openable.h]].
#include "Game.h"
#include "item/Location.h"
#include "item/Openable.h"
namespace Container::Openable {
namespace {
int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L,
"Usage: C_Container.IsContainerItemOpenable(containerIndex, slotIndex)");
return 0;
}
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
return Item::Openable::PushIsItemOpenable(
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Container::Openable
+39
View File
@@ -0,0 +1,39 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Charges.h"
#include "../Game.h"
#include "../Offsets.h"
#include <cstdlib>
namespace Item::Charges {
int PushChargesForItem(void *L, const uint8_t *item) {
if (item == nullptr)
return 0;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
item + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 0;
const int32_t stack = static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
const int32_t rawCharges = *reinterpret_cast<const int32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_SPELL_CHARGES_0);
const int32_t usesPerItem = (rawCharges < 0) ? -rawCharges : 1;
Game::Lua::PushNumber(L, static_cast<double>(stack * usesPerItem));
return 1;
}
} // namespace Item::Charges
+44
View File
@@ -0,0 +1,44 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Item::Charges {
// Pushes the total uses available in this single slot — the
// per-slot equivalent of `GetItemCount(_, _, includeCharges=true)`.
// Returns `1` (pushed one value) on success, `0` (pushes nothing,
// caller surfaces as `nil`) when the item is missing or the
// descriptor is null.
//
// Math is `stack * usesPerItem` where `usesPerItem` follows
// `Item::Count::GetUsesPerItem`:
// - `SPELL_CHARGES[0] >= 0` → `1` (no destroy-on-zero behavior;
// stack count alone reflects "how many uses").
// - `SPELL_CHARGES[0] < 0` → `abs(value)` (consume-on-use items;
// wand at 50 has `-50` so each item contributes 50).
//
// Worked examples:
// - Wand at 50 charges (stack=1, raw=-50): `1 * 50 = 50`
// - Hearthstone (stack=1, raw=0): `1 * 1 = 1` (cooldown-
// only, no charges concept; we still surface "1 use").
// - Water stack of 20 (stack=20, raw=-1): `20 * 1 = 20`
// - Equipped sword (stack=1, raw=0): `1 * 1 = 1`
//
// Caller can `return PushChargesForItem(L, item);` to forward the
// count directly.
int PushChargesForItem(void *L, const uint8_t *item);
} // namespace Item::Charges
+113
View File
@@ -0,0 +1,113 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Item.IsItemOpenable(itemLocation)` — backport of the modern WoW
// accessor that says "right-clicking this item triggers an open/loot
// interaction" (clams, sacks, simple chests, quest boxes, lockboxes).
// Returns two values: `(isOpenable, canOpen)` — the first is the
// item-type-intrinsic openable flag, the second is whether the
// player can right-click *this specific instance* right now (lock
// is gone or never existed).
//
// Vanilla 1.12's tooltip builder gates the `<Right Click to Open>`
// string (`ITEM_OPENABLE`) on `ItemStats.Flags & 0x4` AND
// (`LockID == 0` OR `ITEM_FIELD_FLAGS & 0x4`). We mirror both checks
// — see `Offsets::ITEMSTATS_FLAG_OPENABLE`, `ITEMSTATS_LOCK_ID`, and
// `ITEM_FLAG_UNLOCKED`.
#include "Game.h"
#include "Offsets.h"
#include "item/Data.h"
#include "item/ID.h"
#include "item/Location.h"
#include "item/Openable.h"
#include <cstdint>
namespace Item::Openable {
namespace {
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid, void *callback,
void *userData, int unused);
const uint8_t *PeekItemRecord(uint32_t itemID) {
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
const uint64_t zeroGuid = 0;
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
} // namespace
namespace {
// Reads `ITEM_FIELD_FLAGS` from the instance descriptor at +0x114.
// Defaults to 0 if `cgItem` is null or the descriptor pointer hasn't
// been populated yet.
uint32_t InstanceFlags(const uint8_t *cgItem) {
if (cgItem == nullptr)
return 0;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 0;
return *reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_FLAGS);
}
} // namespace
int PushIsItemOpenable(void *L, const uint8_t *cgItem) {
const int itemID = Item::ID::FromCGItem(cgItem);
if (itemID <= 0)
return 0;
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
if (record == nullptr) {
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
return 0;
}
const uint32_t typeFlags = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_FLAGS);
const uint32_t lockID = *reinterpret_cast<const uint32_t *>(
record + Offsets::OFF_ITEMSTATS_LOCK_ID);
const bool isOpenable = (typeFlags & Offsets::ITEMSTATS_FLAG_OPENABLE) != 0;
const uint32_t instFlags = InstanceFlags(cgItem);
const bool canOpen = isOpenable && (lockID == 0 ||
(instFlags & Offsets::ITEM_FLAG_UNLOCKED) != 0);
Game::Lua::PushBool(L, isOpenable);
Game::Lua::PushBool(L, canOpen);
return 2;
}
namespace {
int __fastcall Script_C_Item_IsItemOpenable(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::Error(L, "Usage: C_Item.IsItemOpenable(itemLocation)");
return 0;
}
return PushIsItemOpenable(L, Item::Location::Resolve(L, 1));
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "IsItemOpenable",
&Script_C_Item_IsItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Item::Openable
+37
View File
@@ -0,0 +1,37 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace Item::Openable {
// Pushes two values for the openable check on a player-owned item
// (`isOpenable, canOpen`), returning 2. The two semantics:
//
// `isOpenable` — bool. The item type carries the openable flag
// (`ItemStats.Flags & 0x4` — clams, sacks, lockboxes, etc.).
// `canOpen` — bool. The player can right-click this specific
// instance and trigger the open action right now. True for
// openable items with no lock (`LockID == 0`) or whose instance
// has been unlocked (`ITEM_FIELD_FLAGS & 0x4` — set after a
// rogue picks the lock, a key is used, etc.). False for fresh
// lockboxes a non-rogue is looking at.
//
// `cgItem` may be null (empty slot), in which case nothing is pushed
// and 0 is returned (Lua sees nil for both returns). Uncached itemID
// also returns 0 plus a background cache warm.
int PushIsItemOpenable(void *L, const uint8_t *cgItem);
} // namespace Item::Openable
+3 -3
View File
@@ -32,7 +32,7 @@
// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the
// `"nameplateN"` unit token (formatted from the plate's index in
// `g_orderedGUIDs` at fire time). The token resolves to the unit
// via the `nameplateN`-aware token resolver in `TokenResolver.cpp`
// 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.
@@ -86,7 +86,7 @@ std::unordered_set<const void *> g_seenPlates;
// Ordered list of currently-visible nameplate GUIDs, in
// creation-order. Append on UNIT_ADDED, erase on UNIT_REMOVED. Backs
// the `nameplateN` unit-token resolver in `TokenResolver.cpp`. Order
// 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).
@@ -239,7 +239,7 @@ void PrepareForReload() {
}
// Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver
// in `TokenResolver.cpp` can map an index to a GUID without seeing
// in `unit/TokenExtensions.cpp` can map an index to a GUID without seeing
// the internal vector.
uint64_t GetGUIDByIndex(int oneBased) {
if (oneBased <= 0)
+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
+9 -6
View File
@@ -107,14 +107,16 @@ static int __fastcall Script_UnitGUID(void *L) {
//
// The search walks the modern retail token order with post-vanilla
// tokens dropped (no `vehicle`, `arenaN`, `arenapetN`, `bossN`,
// `focus`, `softenemy`, `softfriend`, `softinteract` — those all
// post-date 1.12 and `FUN_TOKEN_TO_GUID` would raise "Unknown unit
// name" for them). `nameplateN` is included — we hook the token
// resolver so the engine recognizes it (see `nameplate/TokenResolver.cpp`).
// Order:
// `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 → nameplate1..N → 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
@@ -203,6 +205,7 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
}
if (check("target")) return 1;
if (check("focus")) return 1;
if (check("npc")) return 1;
if (check("mouseover")) return 1;
@@ -11,45 +11,41 @@
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Extend `FUN_TOKEN_TO_GUID` (`0x00515970`) with modern WoW's
// `nameplateN` unit-token family. Hooking the central token→GUID
// resolver means every `Script_Unit*` (UnitName, UnitGUID, UnitHealth,
// UnitClass, UnitExists, …) gains nameplate-token support for free,
// 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.
//
// Resolution:
// Currently extended families:
//
// 1. SStrCmpI the input against `"nameplate"` (9 chars). Non-match →
// fall through to the original resolver and behave exactly as
// before (engine handles `player` / `target` / `partyN` / etc.,
// or raises "Unknown unit name").
// 2. Parse trailing digits — `nameplate1`, `nameplate12`, ... — into
// a 1-based index `N`.
// 3. Look up `N` in `NamePlate::Events`' ordered GUID list. Out of
// range returns 0 quietly, which the engine treats as "no unit"
// and Lua callers see as `nil` — matches modern behavior, no
// "Unknown unit name" error.
// 4. If the token continues past the digits (`nameplate1target`,
// `nameplate1targettarget`, …) mirror the engine's own suffix
// walker at `LAB_005159d3`: each `"target"` chunk advances the
// GUID by reading `m_objectFields + OFF_UNIT_FIELD_TARGET`
// (UNIT_FIELD_TARGET) via `ObjectByGUID`.
// - `nameplate1`..`nameplateN` — index into the ordered list of
// visible nameplates maintained in `nameplate/Events.cpp`. See
// `Unit tokens (nameplateN)` in docs/API.md.
//
// The engine's walker is **inside** the function we hook, so a hook
// that returns early at the prefix-parse step would skip it
// entirely. Replicating the walker here keeps `nameplate1target`
// working without rewriting the resolver. The walker's logic is
// short (one `SStrCmpI` + one `ObjectByGUID` + one field-read per
// iteration) and matches the engine instruction-for-instruction —
// verified against the disassembly of `0x005159D3..0x00515A2C`.
// - `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 NamePlate::TokenResolver {
namespace Unit::TokenExtensions {
namespace {
@@ -64,8 +60,10 @@ using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
uint32_t guidLo, uint32_t guidHi,
int priority);
constexpr const char kPrefix[] = "nameplate";
constexpr int kPrefixLen = static_cast<int>(sizeof(kPrefix) - 1);
constexpr const char 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);
@@ -87,7 +85,7 @@ uint64_t WalkSuffix(uint64_t guid, const char *suffix) {
return 0; // unknown suffix component — modern returns nil
suffix += kSuffixTargetLen;
auto *obj = static_cast<uint8_t *>(
resolve(Offsets::OBJ_TYPE_UNIT, "nameplate",
resolve(Offsets::OBJ_TYPE_UNIT, "ClassicAPI",
static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32),
kResolvePriority));
@@ -113,27 +111,36 @@ uint64_t __fastcall Hook_h(const char *token) {
return Original_o(token);
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
if (sstrcmpi(token, kPrefix, kPrefixLen) != 0)
return Original_o(token);
// Must have at least one digit after `"nameplate"`; otherwise
// it's `"nameplate"` (no number) or `"nameplateX"` — fall
// through to the original so the engine raises its
// "Unknown unit name" error consistently.
const char *p = token + kPrefixLen;
if (*p < '0' || *p > '9')
return Original_o(token);
int index = 0;
while (*p >= '0' && *p <= '9') {
index = index * 10 + (*p - '0');
++p;
// `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);
}
const uint64_t guid = NamePlate::Events::GetGUIDByIndex(index);
if (guid == 0)
return 0; // OOB / no plate at index — modern returns nil
return WalkSuffix(guid, p);
// `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
@@ -143,4 +150,4 @@ static const Game::HookAutoRegister _hook{
reinterpret_cast<void *>(&Hook_h),
reinterpret_cast<void **>(&Original_o)};
} // namespace NamePlate::TokenResolver
} // namespace Unit::TokenExtensions