mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 23:20:44 +00:00
The repo was internally inconsistent: the LICENSE file is GPL v3 but every src/ header carried the LGPL v3 notice and the README said "LGPL v3 or later". LGPL was also the wrong fit — this is an injected mod plus a bundled addon, not a library other code links against, so LGPL's linking exception buys nothing. It's also required: src/voicechat/Tts.cpp is ported from awesome_wotlk's VoiceChat.cpp, which is GPL v3, and a derivative of GPL code cannot be relicensed under more permissive terms. Dependencies stay compatible — MinHook (BSD-2) and the nampower-mirrored packet shapes (BSD) are GPL-compatible, and SAPI is covered by the system-library exception. Swept "GNU Lesser General Public License" -> "GNU General Public License" across all 257 src/ headers and changed the README license line to match the GPL v3 LICENSE file. Comment-only; no build impact.
276 lines
11 KiB
C++
276 lines
11 KiB
C++
// This file is part of ClassicAPI.
|
|
//
|
|
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
|
// of the GNU 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 General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU 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
|