From ef7494700e2c81f0bb0ad863211471e0a8a0fa08 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 11:32:32 -0700 Subject: [PATCH] feat: parse and display Heroic/Unique/Unique-Equipped item flags in tooltips --- include/game/world_packets.hpp | 2 ++ src/game/packet_parsers_classic.cpp | 4 ++-- src/game/packet_parsers_tbc.cpp | 6 +++--- src/game/world_packets.cpp | 6 +++--- src/ui/inventory_screen.cpp | 29 +++++++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 257df817..d864b57e 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1587,7 +1587,9 @@ struct ItemQueryResponseData { uint32_t subClass = 0; uint32_t displayInfoId = 0; uint32_t quality = 0; + uint32_t itemFlags = 0; // Item flag bitmask (Heroic=0x8, Unique-Equipped=0x1000000) uint32_t inventoryType = 0; + int32_t maxCount = 0; // Max that can be carried (1 = Unique, 0 = unlimited) int32_t maxStack = 1; uint32_t containerSlots = 0; float damageMin = 0.0f; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 53b07f15..041af211 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1381,7 +1381,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ return false; } - packet.readUInt32(); // Flags + data.itemFlags = packet.readUInt32(); // Flags // Vanilla: NO Flags2 packet.readUInt32(); // BuyPrice data.sellPrice = packet.readUInt32(); // SellPrice @@ -1405,7 +1405,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ packet.readUInt32(); // RequiredCityRank data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank - packet.readUInt32(); // MaxCount + data.maxCount = static_cast(packet.readUInt32()); // MaxCount (1 = Unique) data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 45ef8dde..935b34ae 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -998,7 +998,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery return false; } - packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2) + data.itemFlags = packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2) // TBC: NO Flags2, NO BuyCount packet.readUInt32(); // BuyPrice data.sellPrice = packet.readUInt32(); @@ -1022,8 +1022,8 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery packet.readUInt32(); // RequiredCityRank data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank - packet.readUInt32(); // MaxCount - data.maxStack = static_cast(packet.readUInt32()); // Stackable + data.maxCount = static_cast(packet.readUInt32()); // MaxCount (1 = Unique) + data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); // TBC: statsCount prefix + exactly statsCount pairs (WotLK always sends 10) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index d8f2c98a..dbcbf4c9 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2846,7 +2846,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before flags (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // Flags + data.itemFlags = packet.readUInt32(); // Flags packet.readUInt32(); // Flags2 packet.readUInt32(); // BuyCount packet.readUInt32(); // BuyPrice @@ -2856,7 +2856,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa if (data.inventoryType > 28) { // inventoryType out of range — BuyCount probably not present; rewind and try 4 fields packet.setReadPos(postQualityPos); - packet.readUInt32(); // Flags + data.itemFlags = packet.readUInt32(); // Flags packet.readUInt32(); // Flags2 packet.readUInt32(); // BuyPrice data.sellPrice = packet.readUInt32(); // SellPrice @@ -2879,7 +2879,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa packet.readUInt32(); // RequiredCityRank data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank - packet.readUInt32(); // MaxCount + data.maxCount = static_cast(packet.readUInt32()); // MaxCount (1 = Unique) data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 3510c4a5..8b7c7a57 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -2315,6 +2315,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", item.itemLevel); } + // Heroic / Unique / Unique-Equipped indicators + if (gameHandler_) { + const auto* qi = gameHandler_->getItemInfo(item.itemId); + if (qi && qi->valid) { + constexpr uint32_t kFlagHeroic = 0x8; + constexpr uint32_t kFlagUniqueEquipped = 0x1000000; + if (qi->itemFlags & kFlagHeroic) { + ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic"); + } + if (qi->maxCount == 1) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique"); + } else if (qi->itemFlags & kFlagUniqueEquipped) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped"); + } + } + } + // Binding type switch (item.bindType) { case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break; @@ -2810,6 +2827,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", info.itemLevel); } + // Unique / Heroic indicators + constexpr uint32_t kFlagHeroic = 0x8; // ITEM_FLAG_HEROIC_TOOLTIP + constexpr uint32_t kFlagUniqueEquipped = 0x1000000; // ITEM_FLAG_UNIQUE_EQUIPPABLE + if (info.itemFlags & kFlagHeroic) { + ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic"); + } + if (info.maxCount == 1) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique"); + } else if (info.itemFlags & kFlagUniqueEquipped) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped"); + } + // Binding type switch (info.bindType) { case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break;