From 1fd3d5fdc8736772078a63ad87da6ee80adaa5f8 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 03:50:24 -0700 Subject: [PATCH] feat: display permanent and temporary enchants in item tooltips for equipped items Tracks ITEM_ENCHANTMENT_SLOT 0 (permanent) and 1 (temporary) from item update fields in OnlineItemInfo, then looks up names from SpellItemEnchantment.dbc and renders them in both ItemDef and ItemQueryResponseData tooltip variants. --- include/game/game_handler.hpp | 12 +++++ include/ui/inventory_screen.hpp | 4 +- src/game/game_handler.cpp | 28 +++++++++-- src/ui/inventory_screen.cpp | 82 +++++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 8 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 9090a1fa..04d0e78d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2125,6 +2125,16 @@ public: if (index < 0 || index >= static_cast(backpackSlotGuids_.size())) return 0; return backpackSlotGuids_[index]; } + uint64_t getEquipSlotGuid(int slot) const { + if (slot < 0 || slot >= static_cast(equipSlotGuids_.size())) return 0; + return equipSlotGuids_[slot]; + } + // Returns the permanent and temporary enchant IDs for an item by GUID (0 if unknown). + std::pair getItemEnchantIds(uint64_t guid) const { + auto it = onlineItems_.find(guid); + if (it == onlineItems_.end()) return {0, 0}; + return {it->second.permanentEnchantId, it->second.temporaryEnchantId}; + } uint64_t getVendorGuid() const { return currentVendorItems.vendorGuid; } /** @@ -2621,6 +2631,8 @@ private: uint32_t stackCount = 1; uint32_t curDurability = 0; uint32_t maxDurability = 0; + uint32_t permanentEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 0 (enchanting) + uint32_t temporaryEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 1 (sharpening stones, poisons) }; std::unordered_map onlineItems_; std::unordered_map itemInfoCache_; diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index b9c30c6c..21ccdc00 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -99,7 +99,7 @@ private: std::unordered_map iconCache_; public: VkDescriptorSet getItemIcon(uint32_t displayInfoId); - void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr); + void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr, uint64_t itemGuid = 0); private: // Character model preview @@ -161,7 +161,7 @@ private: SlotKind kind, int backpackIndex, game::EquipSlot equipSlot, int bagIndex = -1, int bagSlotIndex = -1); - void renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory = nullptr); + void renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory = nullptr, uint64_t itemGuid = 0); // Held item helpers void pickupFromBackpack(game::Inventory& inv, int index); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 84010de9..50e45512 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -11677,14 +11677,20 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT)); auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY)); auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY)); + const uint16_t enchBase = (fieldIndex(UF::ITEM_FIELD_STACK_COUNT) != 0xFFFF) + ? static_cast(fieldIndex(UF::ITEM_FIELD_STACK_COUNT) + 8u) : 0xFFFFu; + auto permEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase) : block.fields.end(); + auto tempEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 3u) : block.fields.end(); if (entryIt != block.fields.end() && entryIt->second != 0) { // Preserve existing info when doing partial updates OnlineItemInfo info = onlineItems_.count(block.guid) ? onlineItems_[block.guid] : OnlineItemInfo{}; info.entry = entryIt->second; - if (stackIt != block.fields.end()) info.stackCount = stackIt->second; - if (durIt != block.fields.end()) info.curDurability = durIt->second; - if (maxDurIt!= block.fields.end()) info.maxDurability = maxDurIt->second; + if (stackIt != block.fields.end()) info.stackCount = stackIt->second; + if (durIt != block.fields.end()) info.curDurability = durIt->second; + if (maxDurIt != block.fields.end()) info.maxDurability = maxDurIt->second; + if (permEnchIt != block.fields.end()) info.permanentEnchantId = permEnchIt->second; + if (tempEnchIt != block.fields.end()) info.temporaryEnchantId = tempEnchIt->second; bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end()); onlineItems_[block.guid] = info; if (isNew) newItemCreated = true; @@ -12269,6 +12275,12 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY); const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS); const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1); + // ITEM_FIELD_ENCHANTMENT starts 8 fields after ITEM_FIELD_STACK_COUNT (fixed offset + // across all expansions: +DURATION, +5×SPELL_CHARGES, +FLAGS = +8). + // Slot 0 = permanent enchant (field +0), slot 1 = temp enchant (field +3). + const uint16_t itemEnchBase = (itemStackField != 0xFFFF) ? (itemStackField + 8u) : 0xFFFF; + const uint16_t itemPermEnchField = itemEnchBase; + const uint16_t itemTempEnchField = (itemEnchBase != 0xFFFF) ? (itemEnchBase + 3u) : 0xFFFF; auto it = onlineItems_.find(block.guid); bool isItemInInventory = (it != onlineItems_.end()); @@ -12289,6 +12301,16 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem it->second.maxDurability = val; inventoryChanged = true; } + } else if (isItemInInventory && itemPermEnchField != 0xFFFF && key == itemPermEnchField) { + if (it->second.permanentEnchantId != val) { + it->second.permanentEnchantId = val; + inventoryChanged = true; + } + } else if (isItemInInventory && itemTempEnchField != 0xFFFF && key == itemTempEnchField) { + if (it->second.temporaryEnchantId != val) { + it->second.temporaryEnchantId = val; + inventoryChanged = true; + } } } // Update container slot GUIDs on bag content changes diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 7b91ef22..931a2b10 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -2401,12 +2401,15 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite if (ImGui::IsItemHovered() && !holdingItem) { // Pass inventory for backpack/bag items only; equipped items compare against themselves otherwise const game::Inventory* tooltipInv = (kind == SlotKind::EQUIPMENT) ? nullptr : &inventory; - renderItemTooltip(item, tooltipInv); + uint64_t slotGuid = 0; + if (kind == SlotKind::EQUIPMENT && gameHandler_) + slotGuid = gameHandler_->getEquipSlotGuid(static_cast(equipSlot)); + renderItemTooltip(item, tooltipInv, slotGuid); } } } -void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory) { +void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory, uint64_t itemGuid) { ImGui::BeginTooltip(); ImVec4 qColor = getQualityColor(item.quality); @@ -2915,6 +2918,42 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I } } + // Weapon/armor enchant display for equipped items (reads from item update fields) + if (itemGuid != 0 && gameHandler_) { + auto [permId, tempId] = gameHandler_->getItemEnchantIds(itemGuid); + if (permId != 0 || tempId != 0) { + static std::unordered_map s_enchNamesB; + static bool s_enchNamesLoadedB = false; + if (!s_enchNamesLoadedB && assetManager_) { + s_enchNamesLoadedB = true; + auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* lay = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr; + uint32_t nf = lay ? lay->field("Name") : 8u; + if (nf == 0xFFFFFFFF) nf = 8; + uint32_t fc = dbc->getFieldCount(); + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t eid = dbc->getUInt32(r, 0); + if (eid == 0 || nf >= fc) continue; + std::string en = dbc->getString(r, nf); + if (!en.empty()) s_enchNamesB[eid] = std::move(en); + } + } + } + if (permId != 0) { + auto it2 = s_enchNamesB.find(permId); + const char* ename = (it2 != s_enchNamesB.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "Enchanted: %s", ename); + } + if (tempId != 0) { + auto it2 = s_enchNamesB.find(tempId); + const char* ename = (it2 != s_enchNamesB.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename); + } + } + } + // "Begins a Quest" line (shown in yellow-green like the game) if (item.startQuestId != 0) { ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Begins a Quest"); @@ -3067,7 +3106,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I // --------------------------------------------------------------------------- // Tooltip overload for ItemQueryResponseData (used by loot window, etc.) // --------------------------------------------------------------------------- -void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory) { +void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory, uint64_t itemGuid) { ImGui::BeginTooltip(); ImVec4 qColor = getQualityColor(static_cast(info.quality)); @@ -3442,6 +3481,43 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, } } + // Weapon/armor enchant display for equipped items + if (itemGuid != 0 && gameHandler_) { + auto [permId, tempId] = gameHandler_->getItemEnchantIds(itemGuid); + if (permId != 0 || tempId != 0) { + // Lazy-load SpellItemEnchantment.dbc for enchant name lookup + static std::unordered_map s_enchNames; + static bool s_enchNamesLoaded = false; + if (!s_enchNamesLoaded && assetManager_) { + s_enchNamesLoaded = true; + auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* lay = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr; + uint32_t nf = lay ? lay->field("Name") : 8u; + if (nf == 0xFFFFFFFF) nf = 8; + uint32_t fc = dbc->getFieldCount(); + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t eid = dbc->getUInt32(r, 0); + if (eid == 0 || nf >= fc) continue; + std::string en = dbc->getString(r, nf); + if (!en.empty()) s_enchNames[eid] = std::move(en); + } + } + } + if (permId != 0) { + auto it2 = s_enchNames.find(permId); + const char* ename = (it2 != s_enchNames.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "Enchanted: %s", ename); + } + if (tempId != 0) { + auto it2 = s_enchNames.find(tempId); + const char* ename = (it2 != s_enchNames.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename); + } + } + } + // Item set membership if (info.itemSetId != 0) { // Lazy-load full ItemSet.dbc data (name + item IDs + bonus spells/thresholds)