feat: show socket gems and consolidate enchant name DBC cache in item tooltips

Extends OnlineItemInfo to track gem enchant IDs (socket slots 2-4) from item
update fields; socket display now shows inserted gem name inline (e.g.
"Red Socket: Bold Scarlet Ruby"). Consolidates redundant SpellItemEnchantment
DBC loads into one shared static per tooltip variant.
This commit is contained in:
Kelsi 2026-03-18 04:04:23 -07:00
parent 167e710f92
commit d7c377292e
3 changed files with 133 additions and 116 deletions

View file

@ -2135,6 +2135,12 @@ public:
if (it == onlineItems_.end()) return {0, 0};
return {it->second.permanentEnchantId, it->second.temporaryEnchantId};
}
// Returns the socket gem enchant IDs (3 slots; 0 = empty socket) for an item by GUID.
std::array<uint32_t, 3> getItemSocketEnchantIds(uint64_t guid) const {
auto it = onlineItems_.find(guid);
if (it == onlineItems_.end()) return {};
return it->second.socketEnchantIds;
}
uint64_t getVendorGuid() const { return currentVendorItems.vendorGuid; }
/**
@ -2633,6 +2639,7 @@ private:
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::array<uint32_t, 3> socketEnchantIds{}; // ITEM_ENCHANTMENT_SLOT 2-4 (gems)
};
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;

View file

@ -11679,18 +11679,24 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY));
const uint16_t enchBase = (fieldIndex(UF::ITEM_FIELD_STACK_COUNT) != 0xFFFF)
? static_cast<uint16_t>(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();
auto permEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase) : block.fields.end();
auto tempEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 3u) : block.fields.end();
auto sock1EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 6u) : block.fields.end();
auto sock2EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 9u) : block.fields.end();
auto sock3EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 12u) : 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 (permEnchIt != block.fields.end()) info.permanentEnchantId = permEnchIt->second;
if (tempEnchIt != block.fields.end()) info.temporaryEnchantId = tempEnchIt->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;
if (sock1EnchIt != block.fields.end()) info.socketEnchantIds[0] = sock1EnchIt->second;
if (sock2EnchIt != block.fields.end()) info.socketEnchantIds[1] = sock2EnchIt->second;
if (sock3EnchIt != block.fields.end()) info.socketEnchantIds[2] = sock3EnchIt->second;
bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end());
onlineItems_[block.guid] = info;
if (isNew) newItemCreated = true;
@ -12277,10 +12283,13 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
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;
// Slot 0 = permanent (field +0), slot 1 = temp (+3), slots 2-4 = sockets (+6,+9,+12).
const uint16_t itemEnchBase = (itemStackField != 0xFFFF) ? (itemStackField + 8u) : 0xFFFF;
const uint16_t itemPermEnchField = itemEnchBase;
const uint16_t itemTempEnchField = (itemEnchBase != 0xFFFF) ? (itemEnchBase + 3u) : 0xFFFF;
const uint16_t itemTempEnchField = (itemEnchBase != 0xFFFF) ? (itemEnchBase + 3u) : 0xFFFF;
const uint16_t itemSock1EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 6u) : 0xFFFF;
const uint16_t itemSock2EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 9u) : 0xFFFF;
const uint16_t itemSock3EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 12u) : 0xFFFF;
auto it = onlineItems_.find(block.guid);
bool isItemInInventory = (it != onlineItems_.end());
@ -12311,6 +12320,21 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
it->second.temporaryEnchantId = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock1EnchField != 0xFFFF && key == itemSock1EnchField) {
if (it->second.socketEnchantIds[0] != val) {
it->second.socketEnchantIds[0] = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock2EnchField != 0xFFFF && key == itemSock2EnchField) {
if (it->second.socketEnchantIds[1] != val) {
it->second.socketEnchantIds[1] = val;
inventoryChanged = true;
}
} else if (isItemInInventory && itemSock3EnchField != 0xFFFF && key == itemSock3EnchField) {
if (it->second.socketEnchantIds[2] != val) {
it->second.socketEnchantIds[2] = val;
inventoryChanged = true;
}
}
}
// Update container slot GUIDs on bag content changes

View file

@ -2410,6 +2410,27 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
}
void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory, uint64_t itemGuid) {
// Shared SpellItemEnchantment name lookup — used for socket gems, permanent and temp enchants.
static std::unordered_map<uint32_t, std::string> s_enchLookupB;
static bool s_enchLookupLoadedB = false;
if (!s_enchLookupLoadedB && assetManager_) {
s_enchLookupLoadedB = 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_enchLookupB[eid] = std::move(en);
}
}
}
ImGui::BeginTooltip();
ImVec4 qColor = getQualityColor(item.quality);
@ -2794,39 +2815,33 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
};
// Get socket gem enchant IDs for this item (filled from item update fields)
std::array<uint32_t, 3> sockGems{};
if (itemGuid != 0 && gameHandler_)
sockGems = gameHandler_->getItemSocketEnchantIds(itemGuid);
bool hasSocket = false;
for (int i = 0; i < 3; ++i) {
if (qi2->socketColor[i] == 0) continue;
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
for (const auto& st : kSocketTypes) {
if (qi2->socketColor[i] & st.mask) {
ImGui::TextColored(st.col, "%s", st.label);
if (sockGems[i] != 0) {
auto git = s_enchLookupB.find(sockGems[i]);
if (git != s_enchLookupB.end())
ImGui::TextColored(st.col, "%s: %s", st.label, git->second.c_str());
else
ImGui::TextColored(st.col, "%s: (gem %u)", st.label, sockGems[i]);
} else {
ImGui::TextColored(st.col, "%s", st.label);
}
break;
}
}
}
if (hasSocket && qi2->socketBonus != 0) {
static std::unordered_map<uint32_t, std::string> s_enchantNamesD;
static bool s_enchantNamesLoadedD = false;
if (!s_enchantNamesLoadedD && assetManager_) {
s_enchantNamesLoadedD = true;
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nameField = lay ? lay->field("Name") : 8u;
if (nameField == 0xFFFFFFFF) nameField = 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 || nameField >= fc) continue;
std::string ename = dbc->getString(r, nameField);
if (!ename.empty()) s_enchantNamesD[eid] = std::move(ename);
}
}
}
auto enchIt = s_enchantNamesD.find(qi2->socketBonus);
if (enchIt != s_enchantNamesD.end())
auto enchIt = s_enchLookupB.find(qi2->socketBonus);
if (enchIt != s_enchLookupB.end())
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
else
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", qi2->socketBonus);
@ -2921,36 +2936,15 @@ 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<uint32_t, std::string> 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);
}
if (permId != 0) {
auto it2 = s_enchLookupB.find(permId);
const char* ename = (it2 != s_enchLookupB.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_enchLookupB.find(tempId);
const char* ename = (it2 != s_enchLookupB.end()) ? it2->second.c_str() : nullptr;
if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename);
}
}
@ -3107,6 +3101,27 @@ 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, uint64_t itemGuid) {
// Shared SpellItemEnchantment name lookup — used for socket gems, socket bonus, and enchants.
static std::unordered_map<uint32_t, std::string> s_enchLookup;
static bool s_enchLookupLoaded = false;
if (!s_enchLookupLoaded && assetManager_) {
s_enchLookupLoaded = 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_enchLookup[eid] = std::move(en);
}
}
}
ImGui::BeginTooltip();
ImVec4 qColor = getQualityColor(static_cast<game::ItemQuality>(info.quality));
@ -3441,40 +3456,33 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info,
{ 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } },
{ 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } },
};
// Get socket gem enchant IDs for this item (filled from item update fields)
std::array<uint32_t, 3> sockGems{};
if (itemGuid != 0 && gameHandler_)
sockGems = gameHandler_->getItemSocketEnchantIds(itemGuid);
bool hasSocket = false;
for (int i = 0; i < 3; ++i) {
if (info.socketColor[i] == 0) continue;
if (!hasSocket) { ImGui::Spacing(); hasSocket = true; }
for (const auto& st : kSocketTypes) {
if (info.socketColor[i] & st.mask) {
ImGui::TextColored(st.col, "%s", st.label);
if (sockGems[i] != 0) {
auto git = s_enchLookup.find(sockGems[i]);
if (git != s_enchLookup.end())
ImGui::TextColored(st.col, "%s: %s", st.label, git->second.c_str());
else
ImGui::TextColored(st.col, "%s: (gem %u)", st.label, sockGems[i]);
} else {
ImGui::TextColored(st.col, "%s", st.label);
}
break;
}
}
}
if (hasSocket && info.socketBonus != 0) {
// Socket bonus is a SpellItemEnchantment ID — look up via SpellItemEnchantment.dbc
static std::unordered_map<uint32_t, std::string> s_enchantNames;
static bool s_enchantNamesLoaded = false;
if (!s_enchantNamesLoaded && assetManager_) {
s_enchantNamesLoaded = true;
auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc");
if (dbc && dbc->isLoaded()) {
const auto* lay = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr;
uint32_t nameField = lay ? lay->field("Name") : 8u;
if (nameField == 0xFFFFFFFF) nameField = 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 || nameField >= fc) continue;
std::string ename = dbc->getString(r, nameField);
if (!ename.empty()) s_enchantNames[eid] = std::move(ename);
}
}
}
auto enchIt = s_enchantNames.find(info.socketBonus);
if (enchIt != s_enchantNames.end())
auto enchIt = s_enchLookup.find(info.socketBonus);
if (enchIt != s_enchLookup.end())
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str());
else
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", info.socketBonus);
@ -3484,37 +3492,15 @@ 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<uint32_t, std::string> 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);
}
if (permId != 0) {
auto it2 = s_enchLookup.find(permId);
const char* ename = (it2 != s_enchLookup.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_enchLookup.find(tempId);
const char* ename = (it2 != s_enchLookup.end()) ? it2->second.c_str() : nullptr;
if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename);
}
}