From c919477e7450b967d7dc9073acf816e684c6f91e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 26 Feb 2026 00:59:07 -0800 Subject: [PATCH] Fix item use (CMSG_USE_ITEM), mount tab, and inventory right-click - Fix SMSG_ITEM_QUERY_SINGLE_RESPONSE parsing: read statsCount stat pairs instead of always 10, use 2 damage entries (MAX_ITEM_PROTO_DAMAGES), and parse item spell data (spellId + spellTrigger per slot) - Pass item spell ID in CMSG_USE_ITEM packet so server processes item use requests (spellId=0 caused silent server rejection) - Add spellId parameter to buildUseItem interface across all expansions - Fix spellbook mount tab to use SkillLine 777 (Mounts) instead of 762 (Riding), so known mount summon spells appear correctly - Fix inventory right-click: use IsItemHovered+IsMouseClicked instead of IsItemClicked for InvisibleButton (which only tracks left-clicks) - Fix SlotKind enum declaration order in inventory_screen.hpp --- include/game/packet_parsers.hpp | 6 +- include/game/world_packets.hpp | 8 +- include/ui/inventory_screen.hpp | 15 +++- include/ui/spellbook_screen.hpp | 2 +- src/game/game_handler.cpp | 61 +++++++++----- src/game/packet_parsers_classic.cpp | 2 +- src/game/world_packets.cpp | 122 ++++++++++------------------ src/ui/inventory_screen.cpp | 70 ++++++++++++---- src/ui/spellbook_screen.cpp | 99 ++++++++++++++-------- 9 files changed, 233 insertions(+), 152 deletions(-) diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 15a271bd..f1466a1d 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -51,8 +51,8 @@ public: } /** Build CMSG_USE_ITEM (WotLK default: bag + slot + castCount + spellId + itemGuid + glyphIndex + castFlags + targets) */ - virtual network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) { - return UseItemPacket::build(bagIndex, slotIndex, itemGuid); + virtual network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) { + return UseItemPacket::build(bagIndex, slotIndex, itemGuid, spellId); } // --- Character Enumeration --- @@ -313,7 +313,7 @@ public: const MovementInfo& info, uint64_t playerGuid = 0) override; network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override; - network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) override; + network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) override; bool parseCastFailed(network::Packet& packet, CastFailedData& data) override; bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 308ec642..3116873b 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1512,6 +1512,12 @@ struct ItemQueryResponseData { int32_t spirit = 0; uint32_t sellPrice = 0; std::string subclassName; + // Item spells (up to 5) + struct ItemSpell { + uint32_t spellId = 0; + uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn + }; + std::array spells{}; bool valid = false; }; @@ -1879,7 +1885,7 @@ public: /** CMSG_USE_ITEM packet builder */ class UseItemPacket { public: - static network::Packet build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid); + static network::Packet build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0); }; /** CMSG_AUTOEQUIP_ITEM packet builder */ diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index ccc55631..2d2ca9c9 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -125,6 +125,19 @@ private: int heldBagSlotIndex = -1; game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS; + // Slot rendering with interaction support + enum class SlotKind { BACKPACK, EQUIPMENT }; + + // Click-and-hold pickup tracking + bool pickupPending_ = false; + float pickupPressTime_ = 0.0f; + SlotKind pickupSlotKind_ = SlotKind::BACKPACK; + int pickupBackpackIndex_ = -1; + int pickupBagIndex_ = -1; + int pickupBagSlotIndex_ = -1; + game::EquipSlot pickupEquipSlot_ = game::EquipSlot::NUM_SLOTS; + static constexpr float kPickupHoldThreshold = 0.12f; // seconds + void renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper); void renderAggregateBags(game::Inventory& inventory, uint64_t moneyCopper); void renderBagWindow(const char* title, bool& isOpen, game::Inventory& inventory, @@ -133,8 +146,6 @@ private: void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false); void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0); - // Slot rendering with interaction support - enum class SlotKind { BACKPACK, EQUIPMENT }; void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot, float size, const char* label, SlotKind kind, int backpackIndex, diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 537e862c..7a083183 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -66,7 +66,7 @@ private: bool skillLineDbLoaded = false; std::unordered_map skillLineNames; std::unordered_map skillLineCategories; - std::unordered_map spellToSkillLine; + std::unordered_multimap spellToSkillLine; // Categorized spell tabs std::vector spellTabs; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 22bd86be..0858d31a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7436,15 +7436,10 @@ void GameHandler::handleInspectResults(network::Packet& packet) { uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const { if (itemId == 0) return 0; - uint64_t found = 0; for (const auto& [guid, info] : onlineItems_) { - if (info.entry != itemId) continue; - if (found != 0) { - return 0; // Ambiguous - } - found = guid; + if (info.entry == itemId) return guid; } - return found; + return 0; } void GameHandler::detectInventorySlotBases(const std::map& fields) { @@ -9309,6 +9304,14 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) { } } } + + // Show chat message for non-talent spells + const std::string& name = getSpellName(spellId); + if (!name.empty()) { + addSystemChatMessage("You have learned a new spell: " + name + "."); + } else { + addSystemChatMessage("You have learned a new spell."); + } } void GameHandler::handleRemovedSpell(network::Packet& packet) { @@ -10673,29 +10676,33 @@ void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { void GameHandler::useItemBySlot(int backpackIndex) { if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return; const auto& slot = inventory.getBackpackSlot(backpackIndex); - if (slot.empty()) { - LOG_WARNING("useItemBySlot: slot ", backpackIndex, " is empty"); - return; - } - - LOG_INFO("useItemBySlot: backpackIndex=", backpackIndex, " itemId=", slot.item.itemId, - " wowSlot=", 23 + backpackIndex); + if (slot.empty()) return; uint64_t itemGuid = backpackSlotGuids_[backpackIndex]; if (itemGuid == 0) { itemGuid = resolveOnlineItemGuid(slot.item.itemId); } - LOG_INFO("useItemBySlot: itemGuid=0x", std::hex, itemGuid, std::dec); if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { + // Find the item's on-use spell ID from cached item info + uint32_t useSpellId = 0; + if (auto* info = getItemInfo(slot.item.itemId)) { + for (const auto& sp : info->spells) { + // SpellTrigger: 0=Use, 5=Learn + if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) { + useSpellId = sp.spellId; + break; + } + } + } + // WoW inventory: equipment 0-18, bags 19-22, backpack 23-38 auto packet = packetParsers_ - ? packetParsers_->buildUseItem(0xFF, static_cast(23 + backpackIndex), itemGuid) - : UseItemPacket::build(0xFF, static_cast(23 + backpackIndex), itemGuid); - LOG_INFO("useItemBySlot: sending CMSG_USE_ITEM, packetSize=", packet.getSize()); + ? packetParsers_->buildUseItem(0xFF, static_cast(23 + backpackIndex), itemGuid, useSpellId) + : UseItemPacket::build(0xFF, static_cast(23 + backpackIndex), itemGuid, useSpellId); socket->send(packet); } else if (itemGuid == 0) { - LOG_WARNING("Use item failed: missing item GUID for slot ", backpackIndex); + addSystemChatMessage("Cannot use that item right now."); } } @@ -10722,17 +10729,29 @@ void GameHandler::useItemInBag(int bagIndex, int slotIndex) { " itemGuid=0x", std::hex, itemGuid, std::dec); if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { + // Find the item's on-use spell ID + uint32_t useSpellId = 0; + if (auto* info = getItemInfo(slot.item.itemId)) { + for (const auto& sp : info->spells) { + if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) { + useSpellId = sp.spellId; + break; + } + } + } + // WoW bag addressing: bagIndex = equip slot of bag container (19-22) // For CMSG_USE_ITEM: bag = 19+bagIndex, slot = slot within bag uint8_t wowBag = static_cast(19 + bagIndex); auto packet = packetParsers_ - ? packetParsers_->buildUseItem(wowBag, static_cast(slotIndex), itemGuid) - : UseItemPacket::build(wowBag, static_cast(slotIndex), itemGuid); + ? packetParsers_->buildUseItem(wowBag, static_cast(slotIndex), itemGuid, useSpellId) + : UseItemPacket::build(wowBag, static_cast(slotIndex), itemGuid, useSpellId); LOG_INFO("useItemInBag: sending CMSG_USE_ITEM, bag=", (int)wowBag, " slot=", slotIndex, " packetSize=", packet.getSize()); socket->send(packet); } else if (itemGuid == 0) { LOG_WARNING("Use item in bag failed: missing item GUID for bag ", bagIndex, " slot ", slotIndex); + addSystemChatMessage("Cannot use that item right now."); } } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index a6e81764..2e335af1 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -305,7 +305,7 @@ network::Packet ClassicPacketParsers::buildCastSpell(uint32_t spellId, uint64_t // Vanilla 1.12.x: bag(u8) + slot(u8) + spellIndex(u8) + SpellCastTargets(u16) // NO spellId, itemGuid, glyphIndex, or castFlags fields (those are WotLK) // ============================================================================ -network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t /*itemGuid*/) { +network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t /*itemGuid*/, uint32_t /*spellId*/) { network::Packet packet(wireOpcode(LogicalOpcode::CMSG_USE_ITEM)); packet.writeUInt8(bagIndex); packet.writeUInt8(slotIndex); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 260ea594..6505ea91 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2398,94 +2398,62 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa data.containerSlots = packet.readUInt32(); uint32_t statsCount = packet.readUInt32(); - // Server always sends 10 stat pairs; statsCount tells how many are meaningful - for (uint32_t i = 0; i < 10; i++) { + // Server sends exactly statsCount stat pairs (not always 10). + uint32_t statsToRead = std::min(statsCount, 10u); + for (uint32_t i = 0; i < statsToRead; i++) { uint32_t statType = packet.readUInt32(); int32_t statValue = static_cast(packet.readUInt32()); - if (i < statsCount) { - switch (statType) { - case 3: data.agility = statValue; break; - case 4: data.strength = statValue; break; - case 5: data.intellect = statValue; break; - case 6: data.spirit = statValue; break; - case 7: data.stamina = statValue; break; - default: break; - } + switch (statType) { + case 3: data.agility = statValue; break; + case 4: data.strength = statValue; break; + case 5: data.intellect = statValue; break; + case 6: data.spirit = statValue; break; + case 7: data.stamina = statValue; break; + default: break; } } packet.readUInt32(); // ScalingStatDistribution packet.readUInt32(); // ScalingStatValue - const size_t preDamagePos = packet.getReadPos(); - struct DamageParseResult { - float damageMin = 0.0f; - float damageMax = 0.0f; - int32_t armor = 0; - uint32_t delayMs = 0; - bool ok = false; - }; - auto parseDamageBlock = [&](int damageEntries) -> DamageParseResult { - DamageParseResult r; - packet.setReadPos(preDamagePos); - bool haveWeaponDamage = false; - for (int i = 0; i < damageEntries; i++) { - float dmgMin = packet.readFloat(); - float dmgMax = packet.readFloat(); - uint32_t damageType = packet.readUInt32(); - if (!haveWeaponDamage && dmgMax > 0.0f) { - if (damageType == 0 || r.damageMax <= 0.0f) { - r.damageMin = dmgMin; - r.damageMax = dmgMax; - haveWeaponDamage = (damageType == 0); - } + // WotLK 3.3.5a: MAX_ITEM_PROTO_DAMAGES = 2 + bool haveWeaponDamage = false; + for (int i = 0; i < 2; i++) { + float dmgMin = packet.readFloat(); + float dmgMax = packet.readFloat(); + uint32_t damageType = packet.readUInt32(); + if (!haveWeaponDamage && dmgMax > 0.0f) { + if (damageType == 0 || data.damageMax <= 0.0f) { + data.damageMin = dmgMin; + data.damageMax = dmgMax; + haveWeaponDamage = (damageType == 0); } } - - r.armor = static_cast(packet.readUInt32()); - if (packet.getSize() - packet.getReadPos() >= 28) { - packet.readUInt32(); // HolyRes - packet.readUInt32(); // FireRes - packet.readUInt32(); // NatureRes - packet.readUInt32(); // FrostRes - packet.readUInt32(); // ShadowRes - packet.readUInt32(); // ArcaneRes - r.delayMs = packet.readUInt32(); - r.ok = true; - } - return r; - }; - - // All WoW versions (Classic, TBC, WotLK) use exactly 5 damage entries in - // SMSG_ITEM_QUERY_SINGLE_RESPONSE. Default to 5. Fall back to 2 only if - // the 5-entry parse fails or yields clearly implausible results for weapons. - DamageParseResult parsed2 = parseDamageBlock(2); - DamageParseResult parsed5 = parseDamageBlock(5); - - auto looksWeaponItem = [&](const DamageParseResult& r) { - return (data.itemClass == 2) && (r.damageMax > 0.0f) && (r.delayMs > 0); - }; - - const DamageParseResult* chosen = &parsed5; - if (parsed5.ok && parsed2.ok) { - // Only prefer parsed2 if it identifies as a weapon and parsed5 doesn't. - // This handles non-standard 2-entry servers for weapon items. - if (looksWeaponItem(parsed2) && !looksWeaponItem(parsed5)) chosen = &parsed2; - } else if (!parsed5.ok && parsed2.ok) { - chosen = &parsed2; } - int chosenDamageEntries = (chosen == &parsed5) ? 5 : 2; - data.damageMin = chosen->damageMin; - data.damageMax = chosen->damageMax; - data.armor = chosen->armor; - data.delayMs = chosen->delayMs; + data.armor = static_cast(packet.readUInt32()); + packet.readUInt32(); // HolyRes + packet.readUInt32(); // FireRes + packet.readUInt32(); // NatureRes + packet.readUInt32(); // FrostRes + packet.readUInt32(); // ShadowRes + packet.readUInt32(); // ArcaneRes + data.delayMs = packet.readUInt32(); + packet.readUInt32(); // AmmoType + packet.readFloat(); // RangedModRange + + // 5 item spells: SpellId, SpellTrigger, SpellCharges, SpellCooldown, SpellCategory, SpellCategoryCooldown + for (int i = 0; i < 5; i++) { + if (packet.getReadPos() + 24 > packet.getSize()) break; + data.spells[i].spellId = packet.readUInt32(); + data.spells[i].spellTrigger = packet.readUInt32(); + packet.readUInt32(); // SpellCharges + packet.readUInt32(); // SpellCooldown + packet.readUInt32(); // SpellCategory + packet.readUInt32(); // SpellCategoryCooldown + } data.valid = !data.name.empty(); - LOG_INFO("Item query: '", data.name, "' class=", data.itemClass, - " invType=", data.inventoryType, " quality=", data.quality, - " armor=", data.armor, " dmgEntries=", chosenDamageEntries, - " statsCount=", statsCount, " sellPrice=", data.sellPrice); return true; } @@ -3142,13 +3110,13 @@ network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) { return packet; } -network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) { +network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId) { network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM)); packet.writeUInt8(bagIndex); packet.writeUInt8(slotIndex); packet.writeUInt8(0); // cast count - packet.writeUInt32(0); // spell id - packet.writeUInt64(itemGuid); + packet.writeUInt32(spellId); // spell id from item data + packet.writeUInt64(itemGuid); // full 8-byte GUID packet.writeUInt32(0); // glyph index packet.writeUInt8(0); // cast flags // SpellCastTargets: self diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 3d7f03dc..ad3c3c2c 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -648,6 +648,11 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { cancelPickup(inventory); } + // Cancel pending pickup if mouse released before threshold + if (pickupPending_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + pickupPending_ = false; + } + if (separateBags_) { renderSeparateBags(inventory, moneyCopper); } else { @@ -1341,7 +1346,9 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite ImGui::InvisibleButton("slot", ImVec2(size, size)); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) { + // Drop held item on mouse release over empty slot + if (ImGui::IsItemHovered() && holdingItem && validDrop && + ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { placeInBackpack(inventory, backpackIndex); } else if (kind == SlotKind::BACKPACK && isBagSlot) { @@ -1400,17 +1407,44 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite ImGui::InvisibleButton("slot", ImVec2(size, size)); - // Left-click: pickup or place/swap - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - if (!holdingItem) { - if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { - pickupFromBackpack(inventory, backpackIndex); - } else if (kind == SlotKind::BACKPACK && isBagSlot) { - pickupFromBag(inventory, bagIndex, bagSlotIndex); - } else if (kind == SlotKind::EQUIPMENT) { - pickupFromEquipment(inventory, equipSlot); + // Left mouse: hold to pick up, release to drop/swap + if (!holdingItem) { + // Start pickup tracking on mouse press + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + pickupPending_ = true; + pickupPressTime_ = ImGui::GetTime(); + pickupSlotKind_ = kind; + pickupBackpackIndex_ = backpackIndex; + pickupBagIndex_ = bagIndex; + pickupBagSlotIndex_ = bagSlotIndex; + pickupEquipSlot_ = equipSlot; + } + // Check if held long enough to pick up + if (pickupPending_ && ImGui::IsMouseDown(ImGuiMouseButton_Left) && + (ImGui::GetTime() - pickupPressTime_) >= kPickupHoldThreshold) { + // Verify this is the same slot that was pressed + bool sameSlot = (pickupSlotKind_ == kind); + if (kind == SlotKind::BACKPACK && !isBagSlot) + sameSlot = sameSlot && (pickupBackpackIndex_ == backpackIndex); + else if (kind == SlotKind::BACKPACK && isBagSlot) + sameSlot = sameSlot && (pickupBagIndex_ == bagIndex) && (pickupBagSlotIndex_ == bagSlotIndex); + else if (kind == SlotKind::EQUIPMENT) + sameSlot = sameSlot && (pickupEquipSlot_ == equipSlot); + + if (sameSlot && ImGui::IsItemHovered()) { + pickupPending_ = false; + if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + pickupFromBackpack(inventory, backpackIndex); + } else if (kind == SlotKind::BACKPACK && isBagSlot) { + pickupFromBag(inventory, bagIndex, bagSlotIndex); + } else if (kind == SlotKind::EQUIPMENT) { + pickupFromEquipment(inventory, equipSlot); + } } - } else { + } + } else { + // Drop/swap on mouse release over a filled slot + if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { placeInBackpack(inventory, backpackIndex); } else if (kind == SlotKind::BACKPACK && isBagSlot) { @@ -1422,12 +1456,14 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite } // Right-click: bank deposit (if bank open), vendor sell (if vendor mode), or auto-equip/use - if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem && gameHandler_) { - LOG_INFO("Right-click slot: kind=", (int)kind, + // Note: InvisibleButton only tracks left-click by default, so use IsItemHovered+IsMouseClicked + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !holdingItem && gameHandler_) { + LOG_WARNING("Right-click slot: kind=", (int)kind, " backpackIndex=", backpackIndex, " bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex, " vendorMode=", vendorMode_, - " bankOpen=", gameHandler_->isBankOpen()); + " bankOpen=", gameHandler_->isBankOpen(), + " item='", item.name, "' invType=", (int)item.inventoryType); if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) { gameHandler_->attachItemFromBackpack(backpackIndex); } else if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && isBagSlot) { @@ -1444,12 +1480,18 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot); gameHandler_->unequipToBackpack(equipSlot); } else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + LOG_INFO("Right-click backpack item: name='", item.name, + "' inventoryType=", (int)item.inventoryType, + " itemId=", item.itemId); if (item.inventoryType > 0) { gameHandler_->autoEquipItemBySlot(backpackIndex); } else { gameHandler_->useItemBySlot(backpackIndex); } } else if (kind == SlotKind::BACKPACK && isBagSlot) { + LOG_INFO("Right-click bag item: name='", item.name, + "' inventoryType=", (int)item.inventoryType, + " bagIndex=", bagIndex, " slotIndex=", bagSlotIndex); if (item.inventoryType > 0) { gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex); } else { diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index 73a43091..73aa45b0 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -133,11 +133,17 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) { uint32_t id = skillLineDbc->getUInt32(i, slL ? (*slL)["ID"] : 0); uint32_t category = skillLineDbc->getUInt32(i, slL ? (*slL)["Category"] : 1); std::string name = skillLineDbc->getString(i, slL ? (*slL)["Name"] : 3); - if (id > 0 && !name.empty()) { - skillLineNames[id] = name; + if (id > 0) { + if (!name.empty()) { + skillLineNames[id] = name; + } skillLineCategories[id] = category; } } + LOG_INFO("Spellbook: Loaded ", skillLineNames.size(), " skill line names, ", + skillLineCategories.size(), " categories from SkillLine.dbc"); + } else { + LOG_WARNING("Spellbook: Could not load SkillLine.dbc"); } auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc"); @@ -147,9 +153,12 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) { uint32_t skillLineId = slaDbc->getUInt32(i, slaL ? (*slaL)["SkillLineID"] : 1); uint32_t spellId = slaDbc->getUInt32(i, slaL ? (*slaL)["SpellID"] : 2); if (spellId > 0 && skillLineId > 0) { - spellToSkillLine[spellId] = skillLineId; + spellToSkillLine.emplace(spellId, skillLineId); } } + LOG_INFO("Spellbook: Loaded ", spellToSkillLine.size(), " spell-to-skillline mappings from SkillLineAbility.dbc"); + } else { + LOG_WARNING("Spellbook: Could not load SkillLineAbility.dbc"); } } @@ -161,9 +170,10 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set& known static constexpr uint32_t CAT_PROFESSION = 11; // Primary professions static constexpr uint32_t CAT_SECONDARY = 9; // Secondary skills (Cooking, First Aid, Fishing, Riding, Companions) - // Special skill line IDs within category 9 that get their own tabs - static constexpr uint32_t SKILLLINE_RIDING = 762; // Mounts - static constexpr uint32_t SKILLLINE_COMPANIONS = 778; // Vanity/companion pets + // Special skill line IDs that get their own tabs + static constexpr uint32_t SKILLLINE_MOUNTS = 777; // Mount summon spells (category 7) + static constexpr uint32_t SKILLLINE_RIDING = 762; // Riding skill ranks (category 9) + static constexpr uint32_t SKILLLINE_COMPANIONS = 778; // Vanity/companion pets (category 7) // Buckets std::map> specSpells; // class spec trees @@ -178,47 +188,72 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set& known const SpellInfo* info = &it->second; - auto slIt = spellToSkillLine.find(spellId); - if (slIt != spellToSkillLine.end()) { + // Check all skill lines this spell belongs to, prefer class (cat 7) > profession > secondary > special + auto range = spellToSkillLine.equal_range(spellId); + bool categorized = false; + + uint32_t bestSkillLine = 0; + int bestPriority = -1; // 4=class, 3=profession, 2=secondary, 1=mount/companion + + for (auto slIt = range.first; slIt != range.second; ++slIt) { uint32_t skillLineId = slIt->second; - // Mounts: Riding skill line (762) - if (skillLineId == SKILLLINE_RIDING) { - mountSpells.push_back(info); + if (skillLineId == SKILLLINE_MOUNTS || skillLineId == SKILLLINE_RIDING) { + if (bestPriority < 1) { bestPriority = 1; bestSkillLine = SKILLLINE_MOUNTS; } continue; } - - // Companions: vanity pets skill line (778) if (skillLineId == SKILLLINE_COMPANIONS) { - companionSpells.push_back(info); + if (bestPriority < 1) { bestPriority = 1; bestSkillLine = skillLineId; } continue; } auto catIt = skillLineCategories.find(skillLineId); if (catIt != skillLineCategories.end()) { uint32_t cat = catIt->second; - - // Class spec abilities - if (cat == CAT_CLASS) { - specSpells[skillLineId].push_back(info); - continue; - } - - // Primary professions - if (cat == CAT_PROFESSION) { - profSpells[skillLineId].push_back(info); - continue; - } - - // Secondary skills (Cooking, First Aid, Fishing) - if (cat == CAT_SECONDARY) { - profSpells[skillLineId].push_back(info); - continue; + if (cat == CAT_CLASS && bestPriority < 4) { + bestPriority = 4; bestSkillLine = skillLineId; + } else if (cat == CAT_PROFESSION && bestPriority < 3) { + bestPriority = 3; bestSkillLine = skillLineId; + } else if (cat == CAT_SECONDARY && bestPriority < 2) { + bestPriority = 2; bestSkillLine = skillLineId; } } } - generalSpells.push_back(info); + if (bestSkillLine > 0) { + if (bestSkillLine == SKILLLINE_MOUNTS) { + mountSpells.push_back(info); + categorized = true; + } else if (bestSkillLine == SKILLLINE_COMPANIONS) { + companionSpells.push_back(info); + categorized = true; + } else { + auto catIt = skillLineCategories.find(bestSkillLine); + if (catIt != skillLineCategories.end()) { + uint32_t cat = catIt->second; + if (cat == CAT_CLASS) { + specSpells[bestSkillLine].push_back(info); + categorized = true; + } else if (cat == CAT_PROFESSION || cat == CAT_SECONDARY) { + profSpells[bestSkillLine].push_back(info); + categorized = true; + } + } + } + } + + if (!categorized) { + generalSpells.push_back(info); + } + } + + LOG_INFO("Spellbook categorize: ", specSpells.size(), " spec groups, ", + generalSpells.size(), " general, ", profSpells.size(), " prof groups, ", + mountSpells.size(), " mounts, ", companionSpells.size(), " companions"); + for (const auto& [slId, spells] : specSpells) { + auto nameIt = skillLineNames.find(slId); + LOG_INFO(" Spec tab: skillLine=", slId, " name='", + (nameIt != skillLineNames.end() ? nameIt->second : "?"), "' spells=", spells.size()); } auto byName = [](const SpellInfo* a, const SpellInfo* b) { return a->name < b->name; };