mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec73652b67 | ||
|
|
4c798ba58a | ||
|
|
fbe81b7f01 | ||
|
|
1c0e20cca8 | ||
|
|
1242f1dd9f | ||
|
|
de0c41dd1d | ||
|
|
3e22e2c087 | ||
|
|
beb9dec6e1 | ||
|
|
e760d5f07e | ||
|
|
d0b6a7c0f3 | ||
|
|
96b0cf6075 |
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
""
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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-
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
|
||||
// returns the `abs(SPELL_CHARGES[0])` value of the item in the
|
||||
// specified bag slot, or `nil` for empty slots and items that don't
|
||||
// carry a charge concept. Wands return their remaining cast count
|
||||
// (e.g. 50 → 49 → …); single-use items (Healthstone, Mana Gem,
|
||||
// Goblin Sapper Charge) return `1`.
|
||||
//
|
||||
// Modern equivalent: none — modern WoW exposes charges only through
|
||||
// tooltip text scanning. `C_Item.GetItemCount(includeBank, includeCharges)`
|
||||
// totals charges across every matching stack; this is the per-slot
|
||||
// reader.
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Charges.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
namespace Container::Charges {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemCharges(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
return Item::Charges::PushChargesForItem(
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
|
||||
&Script_C_Container_GetContainerItemCharges);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::Charges
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,39 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Charges.h"
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Item::Charges {
|
||||
|
||||
int PushChargesForItem(void *L, const uint8_t *item) {
|
||||
if (item == nullptr)
|
||||
return 0;
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
if (descriptor == nullptr)
|
||||
return 0;
|
||||
const int32_t stack = static_cast<int32_t>(*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
const int32_t rawCharges = *reinterpret_cast<const int32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_SPELL_CHARGES_0);
|
||||
const int32_t usesPerItem = (rawCharges < 0) ? -rawCharges : 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(stack * usesPerItem));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace Item::Charges
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::Charges {
|
||||
|
||||
// Pushes the total uses available in this single slot — the
|
||||
// per-slot equivalent of `GetItemCount(_, _, includeCharges=true)`.
|
||||
// Returns `1` (pushed one value) on success, `0` (pushes nothing,
|
||||
// caller surfaces as `nil`) when the item is missing or the
|
||||
// descriptor is null.
|
||||
//
|
||||
// Math is `stack * usesPerItem` where `usesPerItem` follows
|
||||
// `Item::Count::GetUsesPerItem`:
|
||||
// - `SPELL_CHARGES[0] >= 0` → `1` (no destroy-on-zero behavior;
|
||||
// stack count alone reflects "how many uses").
|
||||
// - `SPELL_CHARGES[0] < 0` → `abs(value)` (consume-on-use items;
|
||||
// wand at 50 has `-50` so each item contributes 50).
|
||||
//
|
||||
// Worked examples:
|
||||
// - Wand at 50 charges (stack=1, raw=-50): `1 * 50 = 50`
|
||||
// - Hearthstone (stack=1, raw=0): `1 * 1 = 1` (cooldown-
|
||||
// only, no charges concept; we still surface "1 use").
|
||||
// - Water stack of 20 (stack=20, raw=-1): `20 * 1 = 20`
|
||||
// - Equipped sword (stack=1, raw=0): `1 * 1 = 1`
|
||||
//
|
||||
// Caller can `return PushChargesForItem(L, item);` to forward the
|
||||
// count directly.
|
||||
int PushChargesForItem(void *L, const uint8_t *item);
|
||||
|
||||
} // namespace Item::Charges
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `FocusUnit(unit)` / `ClearFocus()` / `focus` unit token. Polyfills
|
||||
// the post-vanilla focus-target system on top of the engine's
|
||||
// existing token resolver via the hook in
|
||||
// `unit/TokenExtensions.cpp`.
|
||||
//
|
||||
// Storage: a single 64-bit GUID held in-process. The 3.3.5 engine
|
||||
// keeps the same shape (two globals at `DAT_00BD07D0`/`D4`); we
|
||||
// don't share those because vanilla never reads them. The resolver
|
||||
// hook reads our `Unit::Focus::Get()` directly.
|
||||
//
|
||||
// Setter mirrors 3.3.5's `FUN_0051FF20` minus the post-vanilla
|
||||
// `UnitFlag bit 0x2000` ("focus glow" rendering hint, introduced in
|
||||
// TBC for the default focus frame). Vanilla addons (pfUI) render
|
||||
// their own focus indicator and don't need the hint.
|
||||
//
|
||||
// Auto-clear on despawn: modern WoW fires `PLAYER_FOCUS_CHANGED`
|
||||
// when the focused unit leaves the client's object table (out of
|
||||
// rendering range, despawn) and does NOT auto-refocus when they
|
||||
// come back. We mirror that by probing `ObjectByGUID(g_focusGUID)`
|
||||
// every world tick — when it returns null, we `Set(0)` which fires
|
||||
// the event. Cost is one hash-table lookup per tick while focus is
|
||||
// set; zero when no focus is active.
|
||||
|
||||
#include "Focus.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Focus {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEventName = "PLAYER_FOCUS_CHANGED";
|
||||
const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
|
||||
uint64_t g_focusGUID = 0;
|
||||
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
uint64_t ResolveTokenGUID(const char *token) {
|
||||
if (token == nullptr || *token == '\0')
|
||||
return 0;
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Per-tick despawn watcher. Probes the engine's object table for
|
||||
// `g_focusGUID`; when it disappears (out of range, fully despawned),
|
||||
// clear focus and fire PLAYER_FOCUS_CHANGED — matches modern's
|
||||
// "leaves render distance → focus drops" behavior. Won't refocus
|
||||
// when the unit comes back, also matching modern.
|
||||
void OnWorldTick() {
|
||||
if (g_focusGUID == 0)
|
||||
return;
|
||||
auto resolve = reinterpret_cast<ResolveByGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
if (resolve(Offsets::OBJ_TYPE_UNIT, "Focus",
|
||||
static_cast<uint32_t>(g_focusGUID),
|
||||
static_cast<uint32_t>(g_focusGUID >> 32),
|
||||
0x172) == nullptr)
|
||||
Set(0);
|
||||
}
|
||||
|
||||
// `FocusUnit(unit)` — sets focus to whatever GUID `unit` currently
|
||||
// resolves to. Modern signature: `FocusUnit(unit)` with the token
|
||||
// argument. Modern also allows `FocusUnit()` (no arg) to mean
|
||||
// `"target"`, matching the `/focus` slash command's default.
|
||||
int __fastcall Script_FocusUnit(void *L) {
|
||||
if (Game::Lua::IsString(L, 1)) {
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
Set(ResolveTokenGUID(token));
|
||||
} else {
|
||||
Set(ResolveTokenGUID("target"));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ClearFocus()` — drops the focus, fires PLAYER_FOCUS_CHANGED.
|
||||
int __fastcall Script_ClearFocus(void *L) {
|
||||
(void)L;
|
||||
Set(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("FocusUnit", &Script_FocusUnit);
|
||||
Game::Lua::RegisterGlobalFunction("ClearFocus", &Script_ClearFocus);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t Get() { return g_focusGUID; }
|
||||
|
||||
void Set(uint64_t guid) {
|
||||
if (guid == g_focusGUID)
|
||||
return; // no-op: same target → no event fire (matches 3.3.5)
|
||||
g_focusGUID = guid;
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
}
|
||||
|
||||
} // namespace Unit::Focus
|
||||
@@ -0,0 +1,31 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Focus {
|
||||
|
||||
// Current focus GUID, or `0` when nothing is focused. Read by the
|
||||
// `focus` / `focustarget*` branch of the token resolver in
|
||||
// `unit/TokenExtensions.cpp`.
|
||||
uint64_t Get();
|
||||
|
||||
// Sets focus to `guid` and fires `PLAYER_FOCUS_CHANGED` (no args).
|
||||
// Identity-checks first — assigning the same GUID is a no-op,
|
||||
// matching 3.3.5's `FUN_0051FF20` behavior so the event only fires
|
||||
// on a real transition. Pass `0` to clear focus.
|
||||
void Set(uint64_t guid);
|
||||
|
||||
} // namespace Unit::Focus
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user