From 40c016ccdbf382ce0702f11684a95ad097d9e94e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 19:13:38 -0800 Subject: [PATCH] Fix online equipment slot mapping, auto-equip packet, and backpack slot offsets Correct PLAYER_FIELD_INV_SLOT_HEAD default from 322 to 324 (UNIT_END+0xB0) which was shifting every equipment slot by one position. Fix auto-detection to validate against known 3.3.5a base. Change CMSG_AUTOEQUIP_ITEM to send uint8 bag+slot instead of uint64 GUID, and add slot offset 23 for backpack items in both auto-equip and use-item packets. --- include/game/world_packets.hpp | 2 +- src/game/game_handler.cpp | 63 +++++++++++++++++++++++++++------- src/game/world_packets.cpp | 5 +-- src/ui/inventory_screen.cpp | 8 ++--- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 6cd107c6..1f18ade2 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1134,7 +1134,7 @@ public: /** CMSG_AUTOEQUIP_ITEM packet builder */ class AutoEquipItemPacket { public: - static network::Packet build(uint64_t itemGuid); + static network::Packet build(uint8_t srcBag, uint8_t srcSlot); }; /** CMSG_LOOT_RELEASE packet builder */ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7c12417c..ed46a03c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3155,7 +3155,48 @@ void GameHandler::detectInventorySlotBases(const std::map& f std::sort(matchingPairs.begin(), matchingPairs.end()); if (invSlotBase_ < 0) { - invSlotBase_ = matchingPairs.front(); + // The lowest matching field is the first EQUIPPED slot (not necessarily HEAD). + // With 2+ matches we can derive the true base: all matches must be at + // even offsets from the base, spaced 2 fields per slot. + // Use the known 3.3.5a default (324) and verify matches align to it. + constexpr int knownBase = 324; + constexpr int slotStride = 2; + bool allAlign = true; + for (uint16_t p : matchingPairs) { + if (p < knownBase || (p - knownBase) % slotStride != 0) { + allAlign = false; + break; + } + } + if (allAlign) { + invSlotBase_ = knownBase; + } else { + // Fallback: if we have 2+ matches, derive base from their spacing + if (matchingPairs.size() >= 2) { + uint16_t lo = matchingPairs[0]; + // lo must be base + 2*slotN, and slotN is 0..22 + // Try each possible slot for 'lo' and see if all others also land on valid slots + for (int s = 0; s <= 22; s++) { + int candidate = lo - s * slotStride; + if (candidate < 0) break; + bool ok = true; + for (uint16_t p : matchingPairs) { + int off = p - candidate; + if (off < 0 || off % slotStride != 0 || off / slotStride > 22) { + ok = false; + break; + } + } + if (ok) { + invSlotBase_ = candidate; + break; + } + } + if (invSlotBase_ < 0) invSlotBase_ = knownBase; + } else { + invSlotBase_ = knownBase; + } + } packSlotBase_ = invSlotBase_ + (game::Inventory::NUM_EQUIP_SLOTS * 2); LOG_INFO("Detected inventory field base: equip=", invSlotBase_, " pack=", packSlotBase_); @@ -3164,8 +3205,10 @@ void GameHandler::detectInventorySlotBases(const std::map& f bool GameHandler::applyInventoryFields(const std::map& fields) { bool slotsChanged = false; - int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : 322; - int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : 368; + // WoW 3.3.5a: PLAYER_FIELD_INV_SLOT_HEAD = UNIT_END + 0x00B0 = 324 + // PLAYER_FIELD_PACK_SLOT_1 = UNIT_END + 0x00DE = 370 + int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : 324; + int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : 370; for (const auto& [key, val] : fields) { if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) { @@ -4074,15 +4117,10 @@ void GameHandler::autoEquipItemBySlot(int backpackIndex) { return; } - uint64_t itemGuid = backpackSlotGuids_[backpackIndex]; - if (itemGuid == 0) { - itemGuid = resolveOnlineItemGuid(slot.item.itemId); - } - if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { - auto packet = AutoEquipItemPacket::build(itemGuid); + if (state == WorldState::IN_WORLD && socket) { + // WoW inventory: equipment 0-18, bags 19-22, backpack 23-38 + auto packet = AutoEquipItemPacket::build(0xFF, static_cast(23 + backpackIndex)); socket->send(packet); - } else if (itemGuid == 0) { - LOG_WARNING("Auto-equip failed: missing item GUID for slot ", backpackIndex); } } @@ -4101,7 +4139,8 @@ void GameHandler::useItemBySlot(int backpackIndex) { itemGuid = resolveOnlineItemGuid(slot.item.itemId); } if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { - auto packet = UseItemPacket::build(0xFF, static_cast(backpackIndex), itemGuid); + // WoW inventory: equipment 0-18, bags 19-22, backpack 23-38 + auto packet = UseItemPacket::build(0xFF, static_cast(23 + backpackIndex), itemGuid); socket->send(packet); } else if (itemGuid == 0) { LOG_WARNING("Use item failed: missing item GUID for slot ", backpackIndex); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 88285edd..13927601 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1921,9 +1921,10 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64 return packet; } -network::Packet AutoEquipItemPacket::build(uint64_t itemGuid) { +network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) { network::Packet packet(static_cast(Opcode::CMSG_AUTOEQUIP_ITEM)); - packet.writeUInt64(itemGuid); + packet.writeUInt8(srcBag); + packet.writeUInt8(srcSlot); return packet; } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 2d699244..f11459fb 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1097,18 +1097,16 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite inventoryDirty = true; } } else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { - bool looksEquipable = (item.inventoryType > 0) || - (item.armor > 0) || - (!item.subclassName.empty()); if (gameHandler_ && !gameHandler_->isSinglePlayerMode()) { - if (looksEquipable) { + if (item.inventoryType > 0) { // Auto-equip (online) gameHandler_->autoEquipItemBySlot(backpackIndex); } else { // Use consumable (online) gameHandler_->useItemBySlot(backpackIndex); } - } else if (looksEquipable) { + } else if (item.inventoryType > 0 || item.armor > 0 || + !item.subclassName.empty()) { // Auto-equip (single-player) uint8_t equippingType = item.inventoryType; game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);