-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCount.cpp
More file actions
275 lines (252 loc) · 11.3 KB
/
Count.cpp
File metadata and controls
275 lines (252 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include "Offsets.h"
#include "item/Arg.h"
#include "item/ID.h"
#include "item/Location.h"
#include <cstdint>
namespace Item::Count {
namespace {
// Reads ITEM_FIELD_STACK_COUNT off a CGItem's m_objectFields
// descriptor at +0x20. Returns 0 if the descriptor is null. Stack
// counts are always positive — non-stackable items have count = 1.
int GetStackCount(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 static_cast<int>(*reinterpret_cast<const uint32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
}
// Per-item uses count for `includeUses` mode. Reads
// ITEM_FIELD_SPELL_CHARGES[0] (signed dword). Only negative values
// count — they mark "consume-on-use, destroyed at 0" items
// (Healthstone, Mana Gem, etc.). Positive or zero returns 1, so the
// multiplier is neutral for stacked consumables and ordinary items.
// Mirrors 3.3.5's `Script_GetItemCount` charge gate at
// `FUN_0051c2e0` — it only swaps to the charge-summing walker when
// `SPELL_CHARGES[0] < 0`.
int GetUsesPerItem(const uint8_t *cgItem) {
if (cgItem == nullptr)
return 1;
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
cgItem + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr)
return 1;
const int32_t raw = *reinterpret_cast<const int32_t *>(
descriptor + Offsets::OFF_DESCRIPTOR_SPELL_CHARGES_0);
if (raw >= 0)
return 1;
return -raw;
}
// Combined per-slot contribution to GetItemCount. `includeUses=false`
// gives plain stack count (matches old behavior); `true` multiplies by
// abs(SPELL_CHARGES[0]) for charged items.
int GetItemContribution(const uint8_t *cgItem, bool includeUses) {
const int stack = GetStackCount(cgItem);
if (!includeUses)
return stack;
return stack * GetUsesPerItem(cgItem);
}
// Walks `bagID` and accumulates per-slot contributions for items
// matching `targetItemID`. Skips empty slots. Each `ResolveBag` call
// clobbers Lua stack[1]/[2]; we own the stack inside the callback.
int CountInBag(void *L, int bagID, int targetItemID, bool includeUses) {
int total = 0;
const int slotCount = Item::Location::GetBagSlotCount(bagID);
for (int slot = 1; slot <= slotCount; slot++) {
const uint8_t *item = Item::Location::ResolveBag(L, bagID, slot);
if (item == nullptr)
continue;
if (Item::ID::FromCGItem(item) != targetItemID)
continue;
total += GetItemContribution(item, includeUses);
}
return total;
}
// Direct GUID-array bypass for bank slots — sidesteps the gate at
// `0x006228C1` that `GetItemBySlot` applies to slots 39..68 when no
// bank window is open. The slot data in `invMgr+0x04` is populated at
// login (verified empirically — see `Offsets::OFF_INVMGR_GUID_ARRAY`),
// so we can read GUIDs directly and resolve them via the engine's own
// object resolver `FUN_OBJECT_RESOLVE_BY_GUID` (the same function
// `GetItemBySlot` would call internally if the gate let us through).
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
using ResolveObjectByGuid_t = void *(__fastcall *)(int type,
const char *debugName,
uint32_t guidLo,
uint32_t guidHi,
int priority);
const uint8_t *PlayerInvMgr() {
auto ResolveUnitToken =
reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<uint8_t *>(ResolveUnitToken("player"));
if (player == nullptr)
return nullptr;
return player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
}
const uint8_t *ResolveByGuid(int type, uint64_t guid) {
if (guid == 0)
return nullptr;
auto fn = reinterpret_cast<ResolveObjectByGuid_t>(
Offsets::FUN_OBJECT_RESOLVE_BY_GUID);
return static_cast<const uint8_t *>(
fn(type, "ItemMgr",
static_cast<uint32_t>(guid),
static_cast<uint32_t>(guid >> 32),
0x172));
}
// Walks linear slots `[firstSlot, lastSlot]` of `invMgr`'s GUID array
// at `+0x04`, resolves each non-zero GUID as a TYPE_ITEM, sums
// contributions of items matching `targetItemID`. Used for both the
// main bank (slots 39..62 in player invMgr) and individual bank-bag
// invMgrs (slots 0..N-1).
int CountInGuidArray(const uint8_t *invMgr, int firstSlot, int lastSlot,
int targetItemID, bool includeUses) {
if (invMgr == nullptr)
return 0;
auto *guidArray = *reinterpret_cast<const uint64_t *const *>(
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
if (guidArray == nullptr)
return 0;
int total = 0;
for (int slot = firstSlot; slot <= lastSlot; slot++) {
const uint8_t *item = ResolveByGuid(Offsets::OBJ_TYPE_ITEM, guidArray[slot]);
if (item == nullptr)
continue;
if (Item::ID::FromCGItem(item) != targetItemID)
continue;
total += GetItemContribution(item, includeUses);
}
return total;
}
// For bank bag CONTENTS: the equipped bank bags themselves live at
// linear slots 63..68 in the player invMgr. Each is a CGContainer with
// its own invMgr accessed via `vtable[+0x10]` (`CGContainer::GetInvMgr`
// or similar). We resolve the bag, get its invMgr, and walk that.
//
// Layout: bag invMgr+0x00 = max slot count (the bag's size); bag
// invMgr+0x04 = its GUID array. Same shape as the player invMgr.
int CountInBankBags(int targetItemID, bool includeUses) {
auto *playerInvMgr = PlayerInvMgr();
if (playerInvMgr == nullptr)
return 0;
auto *playerGuidArray = *reinterpret_cast<const uint64_t *const *>(
playerInvMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
if (playerGuidArray == nullptr)
return 0;
int total = 0;
using GetBagInvMgr_t = void *(__thiscall *)(void *bag);
for (int slot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT;
slot <= Offsets::INVMGR_BANK_BAG_LAST_SLOT; slot++) {
const uint8_t *bag = ResolveByGuid(Offsets::OBJ_TYPE_CONTAINER,
playerGuidArray[slot]);
if (bag == nullptr)
continue;
// vtable +0x10 returns the bag's own invMgr pointer
auto *vtable = *reinterpret_cast<const uint8_t *const *>(bag);
auto getInvMgr = reinterpret_cast<GetBagInvMgr_t>(
*reinterpret_cast<const uintptr_t *>(vtable + 0x10));
auto *bagInvMgr = static_cast<const uint8_t *>(
getInvMgr(const_cast<uint8_t *>(bag)));
if (bagInvMgr == nullptr)
continue;
const int bagSlots = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInvMgr));
if (bagSlots <= 0)
continue;
total += CountInGuidArray(bagInvMgr, 0, bagSlots - 1, targetItemID,
includeUses);
}
return total;
}
// Walks the player's 19 equipment slots and accumulates contributions
// for items matching `targetItemID`. Equipped items are part of the
// player's total inventory in the modern API — `GetItemCount` of a
// currently-equipped trinket returns 1, not 0.
int CountEquipped(int targetItemID, bool includeUses) {
int total = 0;
for (int slot = 1; slot <= 19; slot++) {
const uint8_t *item = Item::Location::ResolveEquipmentSlot(slot);
if (item == nullptr)
continue;
if (Item::ID::FromCGItem(item) != targetItemID)
continue;
total += GetItemContribution(item, includeUses);
}
return total;
}
// `C_Item.GetItemCount(itemInfo [, includeBank [, includeUses]])` —
// returns the player's total count of `itemInfo` across equipped
// items + bags (and optionally bank). `itemInfo` is a number or
// `"item:NNN"` string. Item names are NOT accepted (vanilla itself
// has no name → ID resolver).
//
// What's counted:
// - **Equipped items** (slots 1..19) — always. Matches modern
// behavior: a currently-equipped trinket returns 1.
// - **Bags 0..4** (backpack + 4 equipped bag slots) — always.
// Live walk via `Item::Location::ResolveBag`.
// - **Bank** (if `includeBank=true`): main bank (linear slots
// 39..62 in player invMgr) + each equipped bank bag's contents
// (slots 63..68 in player invMgr). Bank walks **bypass
// `GetItemBySlot`** by reading the GUID array at `invMgr+0x04`
// directly and resolving via `FUN_OBJECT_RESOLVE_BY_GUID`. This
// works without the bank window ever being opened — the GUIDs
// are populated from server data at login. The engine gates
// `GetItemBySlot` for bank slots via a banker-GUID check at
// `VAR_BANK_GATE_GUID`, but the underlying data is always
// present.
//
// `includeUses` (arg 3, boolean) multiplies each match by the item's
// `abs(SPELL_CHARGES[0])` — so a wand with 50 charges contributes 50
// instead of 1. Items without charges contribute their plain stack
// count (via the `or 1` neutralizer in `GetUsesPerItem`), so the flag
// is a no-op for them.
int __fastcall Script_C_Item_GetItemCount(void *L) {
const int itemID = Item::Arg::ResolveItemID(L, 1);
if (itemID <= 0) {
Game::Lua::PushNumber(L, 0);
return 1;
}
// Read both flags before the bag walk — `ResolveBag` clobbers
// the Lua stack via SetTop(0) and would invalidate any later
// `ToBoolean` reads.
const bool includeBank = Game::Lua::ToBoolean(L, 2) != 0;
const bool includeUses = Game::Lua::ToBoolean(L, 3) != 0;
int total = CountEquipped(itemID, includeUses);
for (int bag = 0; bag <= 4; bag++)
total += CountInBag(L, bag, itemID, includeUses);
if (includeBank) {
// Direct GUID-array reads — bypass the bank gate so this works
// without the bank window having ever been opened in the
// session.
total += CountInGuidArray(PlayerInvMgr(),
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
Offsets::INVMGR_BANK_MAIN_LAST_SLOT,
itemID, includeUses);
total += CountInBankBags(itemID, includeUses);
}
Game::Lua::SetTop(L, 0);
Game::Lua::PushNumber(L, static_cast<double>(total));
return 1;
}
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetItemCount",
&Script_C_Item_GetItemCount);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::Count