Skip to content

Commit 1d370ad

Browse files
committed
GetInventoryItemsForSlot: filter by player proficiency
Mail helms were showing up for cloth classes — the function only checked invType→slot shape, not whether the player could actually equip the item. Adds a proficiency gate using the engine's per-itemClass mask table at [0x00C4D4A0], populated by the SMSG_SET_PROFICIENCY handler at 0x005E7B70. `table[itemClass] & (1 << subclass)` is the same lookup 4.3.4's Script_GetInventoryItemsForSlot does (via FUN_00553AD0 -> DAT_00DD4D38), and the same source the engine's own tooltip red-text coloring and cursor-drop equip check read from. Covers both armor and weapon proficiencies in one mask lookup.
1 parent 35c78ea commit 1d370ad

2 files changed

Lines changed: 80 additions & 4 deletions

File tree

src/Offsets.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,35 @@ enum Offsets {
14881488
VAR_SKILL_LINE_COUNT = 0x00C0D930,
14891489
OFF_SKILL_LINE_NAME = 0x0C,
14901490

1491+
// Player's known-skills table — separate from the DBC. Populated
1492+
// by the engine on login from server data; entries are pointers
1493+
// to per-skill records. Each record's `+0x00` is the
1494+
// `SkillLine.dbc` row (or `0` for header / category rows), and
1495+
// `+0x10` is the current skill rank (other rank/modifier fields
1496+
// follow). Source: `Script_GetSkillLineInfo` at `0x004D3610`,
1497+
// which walks this array indexed by the Lua arg.
1498+
VAR_PLAYER_SKILL_LIST = 0x00B7318C,
1499+
VAR_PLAYER_SKILL_COUNT = 0x00B731B8,
1500+
OFF_PLAYER_SKILL_LINE_ID = 0x00,
1501+
OFF_PLAYER_SKILL_RANK = 0x10,
1502+
1503+
// Per-itemClass proficiency bitmap maintained by the engine.
1504+
// 17 u32 entries (indexed by Item.dbc `m_class`, 0..16). Each
1505+
// entry is a bitmask where bit N is set if the player has
1506+
// proficiency in subclass N of that item class — i.e.
1507+
// `table[4] & (1 << 3)` = "can wear Mail" (armor=class 4,
1508+
// mail=subclass 3).
1509+
//
1510+
// Populated by SMSG_SET_PROFICIENCY's handler at
1511+
// `FUN_005E7B70`, which writes `table[itemClass] = mask`
1512+
// verbatim from the packet. Same source the engine itself uses
1513+
// for equip eligibility (the tooltip "Mail" red-text logic
1514+
// and the cursor-drop-on-paperdoll check both read this). Note:
1515+
// covers **weapons too** (class 2), making it strictly more
1516+
// general than walking the per-skill-line table for individual
1517+
// armor proficiencies.
1518+
VAR_PROFICIENCY_TABLE = 0x00C4D4A0,
1519+
14911520
// Spell.dbc `BaseLevel` — the level a spell becomes available
14921521
// (trainer offers it, quest reward grants it, etc.). Distinct
14931522
// from `SpellLevel` at +0x74 which is the effective level used

src/item/InventoryItemsForSlot.cpp

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,62 @@ bool FitsSlot(int slot1Based, uint32_t invType) {
105105
return (kSlotMaskByInvType[invType] & (1u << (slot1Based - 1))) != 0;
106106
}
107107

108+
// ── Proficiency gate ──────────────────────────────────────────────
109+
// `FitsSlot` only checks invType→slot shape compatibility; without
110+
// also gating on the player's proficiencies this would return (e.g.)
111+
// mail helms for a warlock since both are `invType=HEAD`. Modern
112+
// WoW's equipment-set picker filters by what the player can actually
113+
// equip; we mirror that.
114+
//
115+
// Source of truth: the engine's per-itemClass proficiency bitmap at
116+
// `[VAR_PROFICIENCY_TABLE]` (17 u32 entries indexed by `m_class`).
117+
// SMSG_SET_PROFICIENCY's handler at `0x005E7B70` writes the bitmask
118+
// for each class verbatim from the wire. Bit N of `table[class]` =
119+
// "player can equip subclass N of this class". Same data the engine
120+
// itself reads for the tooltip "Mail/Leather/Plate" red-text and the
121+
// cursor-drop-on-paperdoll-slot check — covers both armor and weapon
122+
// proficiencies in one lookup.
123+
//
124+
// 4.3.4's `Script_GetInventoryItemsForSlot` does the equivalent check
125+
// via `FUN_00553AD0(itemClass) & (1 << subclass)`, reading from
126+
// `DAT_00DD4D38` (its own copy of the same packet-fed table).
127+
128+
bool PlayerHasProficiency(uint32_t itemClass, uint32_t subclass) {
129+
if (itemClass > 16)
130+
return true; // table only covers 0..16; be permissive past that
131+
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
132+
static_cast<uintptr_t>(Offsets::VAR_PROFICIENCY_TABLE) + itemClass * 4);
133+
if (mask == 0)
134+
return true; // table not populated yet (pre-SMSG_SET_PROFICIENCY)
135+
// — fail open so empty-mask doesn't blank the list
136+
return (mask & (1u << subclass)) != 0;
137+
}
138+
139+
bool PlayerCanEquip(const uint8_t *record) {
140+
if (record == nullptr)
141+
return true; // missing record — be permissive, don't hide the item
142+
const uint32_t classCode = *reinterpret_cast<const uint32_t *>(
143+
record + Offsets::OFF_ITEMSTATS_CLASS);
144+
const uint32_t subclass = *reinterpret_cast<const uint32_t *>(
145+
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
146+
return PlayerHasProficiency(classCode, subclass);
147+
}
148+
108149
const uint8_t *PeekItemRecord(uint32_t itemID) {
109150
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
110151
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
111152
const uint64_t zeroGuid = 0;
112153
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
113154
}
114155

115-
uint32_t ReadInventoryType(const uint8_t *cgItem) {
156+
const uint8_t *ItemRecordForCGItem(const uint8_t *cgItem) {
116157
const int itemID = Item::ID::FromCGItem(cgItem);
117158
if (itemID <= 0)
118-
return 0;
119-
auto *record = PeekItemRecord(static_cast<uint32_t>(itemID));
159+
return nullptr;
160+
return PeekItemRecord(static_cast<uint32_t>(itemID));
161+
}
162+
163+
uint32_t ReadInventoryType(const uint8_t *record) {
120164
if (record == nullptr)
121165
return 0;
122166
return *reinterpret_cast<const uint32_t *>(
@@ -132,7 +176,10 @@ void CollectIfFits(int slot, const uint8_t *cgItem, int location,
132176
std::vector<Hit> &out) {
133177
if (cgItem == nullptr)
134178
return;
135-
if (!FitsSlot(slot, ReadInventoryType(cgItem)))
179+
const uint8_t *record = ItemRecordForCGItem(cgItem);
180+
if (!FitsSlot(slot, ReadInventoryType(record)))
181+
return;
182+
if (!PlayerCanEquip(record))
136183
return;
137184
out.push_back({location, cgItem});
138185
}

0 commit comments

Comments
 (0)