Skip to content

Commit 6641e5a

Browse files
committed
item: add C_Item.IsConsumableItem (+ global IsConsumableItem)
Returns true if an item is a consumable: item class Consumable (0), or inventory type INVTYPE_AMMO (24) / INVTYPE_THROWN (25). Registered as both the namespaced C_Item form and the bare global, like IsUsableItem. Definition established by decoding 3.3.5's Script_IsConsumableItem (0x005173E0) then verifying the surviving contract on a modern client: 3.3.5 walked the item's on-use spells and returned true for any with a real effect (which would include on-use trinkets), but that heuristic did NOT survive -- an on-use trinket with a numeric Use: effect (Talisman of Ephemeral Power) returns false on modern. It's a plain class/ammo check. Verified in-game on our client, matching modern exactly: potion / bandage / Faintly Glowing Skull (all class 0) and Rough Arrow (Ammo) -> true; Hearthstone, Talisman, plain armor -> false. itemID / "item:NNN" / link input (names -> false, no vanilla name->ID map); uncached -> false with no async load, same contract as C_Item.IsEquippableItem. Documented in API.md (+ TOC) and the README Item-row index.
1 parent db1f00e commit 6641e5a

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
4141
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
4242
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
4343
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
44-
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `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.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `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.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `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.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
44+
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `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.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `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.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
4545
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
4646
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
4747
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |

docs/API.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ build instructions.
227227
- [`C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`](#c_itemgetitemuniquenessitemlocation--c_itemgetitemuniquenessbyiditem)
228228
- [`C_Item.GetStackCount(itemLocation)`](#c_itemgetstackcountitemlocation)
229229
- [`C_Item.IsBound(itemLocation)`](#c_itemisbounditemlocation)
230+
- [`IsConsumableItem(item)` / `C_Item.IsConsumableItem(item)`](#isconsumableitemitem--c_itemisconsumableitemitem)
230231
- [`C_Item.IsEquippableItem(item)`](#c_itemisequippableitemitem)
231232
- [`IsUsableItem(item)` / `C_Item.IsUsableItem(item)`](#isusableitemitem--c_itemisusableitemitem)
232233
- [`C_Item.IsEquippedItem(item)`](#c_itemisequippeditemitem)
@@ -5315,6 +5316,36 @@ if C_Item.IsBound({bagID = 0, slotIndex = 1}) then ... end
53155316
if C_Item.IsBound(itemGUID) then ... end
53165317
```
53175318

5319+
### `IsConsumableItem(item)` / `C_Item.IsConsumableItem(item)`
5320+
5321+
Returns `true` if the item is a consumable, `false` otherwise. An item
5322+
counts as consumable when its **class is `Consumable` (0)** *or* its
5323+
**inventory type is `INVTYPE_AMMO` (24) or `INVTYPE_THROWN` (25)** — ammo
5324+
and thrown weapons being consumed in use. Registered as both the bare
5325+
global and the namespaced `C_Item` form, like `IsUsableItem`.
5326+
5327+
`item` is an itemID number or `"item:N..."` link. Item names aren't
5328+
accepted (vanilla has no name→ID resolver). Returns `false` for uncached
5329+
items (no async load fired) — same cache contract as
5330+
`C_Item.IsEquippableItem`.
5331+
5332+
```lua
5333+
IsConsumableItem(118) -- Minor Healing Potion → true (class Consumable)
5334+
C_Item.IsConsumableItem(1251) -- Linen Bandage → true (class Consumable)
5335+
C_Item.IsConsumableItem(2512) -- Rough Arrow → true (INVTYPE_AMMO)
5336+
C_Item.IsConsumableItem(6948) -- Hearthstone → false
5337+
C_Item.IsConsumableItem(18820) -- Talisman of Ephemeral Power → false (on-use trinket)
5338+
```
5339+
5340+
This is a class/ammo check, **not** a "has a usable effect" check.
5341+
3.3.5's `IsConsumableItem` walked the item's on-use spells and returned
5342+
true for any with a real effect (which would include on-use trinkets),
5343+
but that heuristic didn't survive into the modern client: verified in-game
5344+
that an on-use trinket with a numeric `Use:` effect (Talisman of Ephemeral
5345+
Power) returns `false`, while class-`Consumable` items (potions, bandages,
5346+
the class-0 "Faintly Glowing Skull") and ammo return `true`. We match the
5347+
modern contract.
5348+
53185349
### `C_Item.IsEquippableItem(item)`
53195350

53205351
Returns `true` if `item` can be equipped in any character-pane slot,

src/item/Consumable.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `C_Item.IsConsumableItem(item)` — true iff the item is a consumable:
15+
// item class `Consumable` (0), or an inventory type of `INVTYPE_AMMO`
16+
// (24) / `INVTYPE_THROWN` (25) — ammo and thrown weapons being consumed
17+
// in use. `item` is an itemID, `"item:NNN"` string, or item link (names
18+
// aren't resolvable in vanilla → false).
19+
//
20+
// Definition established by decoding 3.3.5's `Script_IsConsumableItem`
21+
// (`0x005173E0`) and then verifying the surviving contract against a
22+
// modern client with `/dump`: it is NOT 3.3.5's on-use-spell-effect walk
23+
// (an on-use trinket like Talisman of Ephemeral Power returns *false*),
24+
// but a plain class-or-ammo/thrown check. Confirmed true: Minor Healing
25+
// Potion / Linen Bandage / Faintly Glowing Skull (all class 0), Rough
26+
// Arrow (Ammo). Confirmed false: Hearthstone (class Misc, valueless
27+
// teleport), on-use trinkets, plain armor.
28+
//
29+
// Cache-miss handling mirrors `C_Item.IsEquippableItem`: returns false
30+
// without firing a load (sync call, matches modern semantics). Callers
31+
// that want auto-warm behavior should run `C_Item.RequestLoadItemDataByID`
32+
// first and re-check on `ITEM_DATA_LOAD_RESULT`.
33+
34+
#include "Game.h"
35+
#include "Offsets.h"
36+
#include "item/Arg.h"
37+
38+
#include <cstdint>
39+
40+
namespace Item::Consumable {
41+
42+
namespace {
43+
44+
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
45+
const uint64_t *guid, void *callback,
46+
void *userData, int unused);
47+
48+
const uint8_t *PeekItemRecord(uint32_t itemID) {
49+
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
50+
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
51+
const uint64_t zeroGuid = 0;
52+
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
53+
}
54+
55+
constexpr uint32_t kItemClassConsumable = 0;
56+
constexpr uint32_t kInvTypeAmmo = 24; // INVTYPE_AMMO
57+
constexpr uint32_t kInvTypeThrown = 25; // INVTYPE_THROWN
58+
59+
int __fastcall Script_C_Item_IsConsumableItem(void *L) {
60+
const int itemID = Item::Arg::ResolveItemID(L, 1);
61+
if (itemID <= 0) {
62+
Game::Lua::PushBoolean(L, 0);
63+
return 1;
64+
}
65+
const uint8_t *record = PeekItemRecord(static_cast<uint32_t>(itemID));
66+
if (record == nullptr) {
67+
Game::Lua::PushBoolean(L, 0);
68+
return 1;
69+
}
70+
const uint32_t classID = *reinterpret_cast<const uint32_t *>(
71+
record + Offsets::OFF_ITEMSTATS_CLASS);
72+
const uint32_t invType = *reinterpret_cast<const uint32_t *>(
73+
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
74+
75+
const bool consumable = classID == kItemClassConsumable ||
76+
invType == kInvTypeAmmo || invType == kInvTypeThrown;
77+
Game::Lua::PushBoolean(L, consumable);
78+
return 1;
79+
}
80+
81+
void RegisterLuaFunctions() {
82+
// Modern exposes both the namespaced `C_Item.IsConsumableItem` and the
83+
// bare global `IsConsumableItem` (the older, still-live form); register
84+
// both against the same handler, like `IsUsableItem`.
85+
Game::Lua::RegisterTableFunction("C_Item", "IsConsumableItem",
86+
&Script_C_Item_IsConsumableItem);
87+
Game::Lua::RegisterGlobalFunction("IsConsumableItem",
88+
&Script_C_Item_IsConsumableItem);
89+
}
90+
91+
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
92+
93+
} // namespace
94+
95+
} // namespace Item::Consumable

0 commit comments

Comments
 (0)