feat: parse and display Heroic/Unique/Unique-Equipped item flags in tooltips

This commit is contained in:
Kelsi 2026-03-13 11:32:32 -07:00
parent 03f8642fad
commit ef7494700e
5 changed files with 39 additions and 8 deletions

View file

@ -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;

View file

@ -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<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
data.containerSlots = packet.readUInt32();

View file

@ -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<int32_t>(packet.readUInt32()); // Stackable
data.maxCount = static_cast<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
data.containerSlots = packet.readUInt32();
// TBC: statsCount prefix + exactly statsCount pairs (WotLK always sends 10)

View file

@ -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<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
data.containerSlots = packet.readUInt32();

View file

@ -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;