feat(ui): show keyring in inventory

This commit is contained in:
Kelsi 2026-03-14 08:42:25 -07:00
parent 800862c50a
commit 2c32b72f95
7 changed files with 152 additions and 10 deletions

View file

@ -2534,6 +2534,7 @@ private:
std::unordered_set<uint32_t> pendingItemQueries_;
std::array<uint64_t, 23> equipSlotGuids_{};
std::array<uint64_t, 16> backpackSlotGuids_{};
std::array<uint64_t, 32> keyringSlotGuids_{};
// Container (bag) contents: containerGuid -> array of item GUIDs per slot
struct ContainerInfo {
uint32_t numSlots = 0;

View file

@ -69,6 +69,7 @@ struct ItemSlot {
class Inventory {
public:
static constexpr int BACKPACK_SLOTS = 16;
static constexpr int KEYRING_SLOTS = 32;
static constexpr int NUM_EQUIP_SLOTS = 23;
static constexpr int NUM_BAG_SLOTS = 4;
static constexpr int MAX_BAG_SIZE = 36;
@ -88,6 +89,12 @@ public:
bool setEquipSlot(EquipSlot slot, const ItemDef& item);
bool clearEquipSlot(EquipSlot slot);
// Keyring
const ItemSlot& getKeyringSlot(int index) const;
bool setKeyringSlot(int index, const ItemDef& item);
bool clearKeyringSlot(int index);
int getKeyringSize() const { return KEYRING_SLOTS; }
// Extra bags
int getBagSize(int bagIndex) const;
void setBagSize(int bagIndex, int size);
@ -123,6 +130,7 @@ public:
private:
std::array<ItemSlot, BACKPACK_SLOTS> backpack{};
std::array<ItemSlot, KEYRING_SLOTS> keyring_{};
std::array<ItemSlot, NUM_EQUIP_SLOTS> equipment{};
struct BagData {

View file

@ -57,6 +57,7 @@ enum class UF : uint16_t {
PLAYER_QUEST_LOG_START,
PLAYER_FIELD_INV_SLOT_HEAD,
PLAYER_FIELD_PACK_SLOT_1,
PLAYER_FIELD_KEYRING_SLOT_1,
PLAYER_FIELD_BANK_SLOT_1,
PLAYER_FIELD_BANKBAG_SLOT_1,
PLAYER_SKILL_INFO_START,

View file

@ -8447,6 +8447,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
pendingItemQueries_.clear();
equipSlotGuids_ = {};
backpackSlotGuids_ = {};
keyringSlotGuids_ = {};
invSlotBase_ = -1;
packSlotBase_ = -1;
lastPlayerFields_.clear();
@ -13597,6 +13598,21 @@ bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& field
bool slotsChanged = false;
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1));
int bankBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1));
int bankBagBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1));
// Derive slot counts from field gap (Classic=24/6, TBC/WotLK=28/7).
if (bankBase != 0xFFFF && bankBagBase != 0xFFFF) {
effectiveBankSlots_ = std::min((bankBagBase - bankBase) / 2, 28);
effectiveBankBagSlots_ = (effectiveBankSlots_ <= 24) ? 6 : 7;
}
int keyringBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_KEYRING_SLOT_1));
if (keyringBase == 0xFFFF && bankBagBase != 0xFFFF) {
// Layout fallback for profiles that don't define PLAYER_FIELD_KEYRING_SLOT_1.
// Bank bag slots are followed by 12 vendor buyback slots (24 fields), then keyring.
keyringBase = bankBagBase + (effectiveBankBagSlots_ * 2) + 24;
}
for (const auto& [key, val] : fields) {
if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) {
@ -13617,15 +13633,17 @@ bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& field
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
}
// Bank slots starting at PLAYER_FIELD_BANK_SLOT_1
int bankBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1));
int bankBagBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1));
// Derive slot counts from field gap (Classic=24/6, TBC/WotLK=28/7)
if (bankBase != 0xFFFF && bankBagBase != 0xFFFF) {
effectiveBankSlots_ = std::min((bankBagBase - bankBase) / 2, 28);
effectiveBankBagSlots_ = (effectiveBankSlots_ <= 24) ? 6 : 7;
} else if (keyringBase != 0xFFFF &&
key >= keyringBase &&
key <= keyringBase + (game::Inventory::KEYRING_SLOTS * 2 - 1)) {
int slotIndex = (key - keyringBase) / 2;
bool isLow = ((key - keyringBase) % 2 == 0);
if (slotIndex < static_cast<int>(keyringSlotGuids_.size())) {
uint64_t& guid = keyringSlotGuids_[slotIndex];
if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val;
else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32);
slotsChanged = true;
}
}
if (bankBase != 0xFFFF && key >= static_cast<uint16_t>(bankBase) &&
key <= static_cast<uint16_t>(bankBase) + (effectiveBankSlots_ * 2 - 1)) {
@ -13786,6 +13804,55 @@ void GameHandler::rebuildOnlineInventory() {
inventory.setBackpackSlot(i, def);
}
// Keyring slots
for (int i = 0; i < game::Inventory::KEYRING_SLOTS; i++) {
uint64_t guid = keyringSlotGuids_[i];
if (guid == 0) continue;
auto itemIt = onlineItems_.find(guid);
if (itemIt == onlineItems_.end()) continue;
ItemDef def;
def.itemId = itemIt->second.entry;
def.stackCount = itemIt->second.stackCount;
def.curDurability = itemIt->second.curDurability;
def.maxDurability = itemIt->second.maxDurability;
def.maxStack = 1;
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
if (infoIt != itemInfoCache_.end()) {
def.name = infoIt->second.name;
def.quality = static_cast<ItemQuality>(infoIt->second.quality);
def.inventoryType = infoIt->second.inventoryType;
def.maxStack = std::max(1, infoIt->second.maxStack);
def.displayInfoId = infoIt->second.displayInfoId;
def.subclassName = infoIt->second.subclassName;
def.damageMin = infoIt->second.damageMin;
def.damageMax = infoIt->second.damageMax;
def.delayMs = infoIt->second.delayMs;
def.armor = infoIt->second.armor;
def.stamina = infoIt->second.stamina;
def.strength = infoIt->second.strength;
def.agility = infoIt->second.agility;
def.intellect = infoIt->second.intellect;
def.spirit = infoIt->second.spirit;
def.sellPrice = infoIt->second.sellPrice;
def.itemLevel = infoIt->second.itemLevel;
def.requiredLevel = infoIt->second.requiredLevel;
def.bindType = infoIt->second.bindType;
def.description = infoIt->second.description;
def.startQuestId = infoIt->second.startQuestId;
def.extraStats.clear();
for (const auto& es : infoIt->second.extraStats)
def.extraStats.push_back({es.statType, es.statValue});
} else {
def.name = "Item " + std::to_string(def.itemId);
queryItemInfo(def.itemId, guid);
}
inventory.setKeyringSlot(i, def);
}
// Bag contents (BAG1-BAG4 are equip slots 19-22)
for (int bagIdx = 0; bagIdx < 4; bagIdx++) {
uint64_t bagGuid = equipSlotGuids_[19 + bagIdx];
@ -14029,6 +14096,8 @@ void GameHandler::rebuildOnlineInventory() {
int c = 0; for (auto g : equipSlotGuids_) if (g) c++; return c;
}(), " backpack=", [&](){
int c = 0; for (auto g : backpackSlotGuids_) if (g) c++; return c;
}(), " keyring=", [&](){
int c = 0; for (auto g : keyringSlotGuids_) if (g) c++; return c;
}());
}

View file

@ -45,6 +45,23 @@ bool Inventory::clearEquipSlot(EquipSlot slot) {
return true;
}
const ItemSlot& Inventory::getKeyringSlot(int index) const {
if (index < 0 || index >= KEYRING_SLOTS) return EMPTY_SLOT;
return keyring_[index];
}
bool Inventory::setKeyringSlot(int index, const ItemDef& item) {
if (index < 0 || index >= KEYRING_SLOTS) return false;
keyring_[index].item = item;
return true;
}
bool Inventory::clearKeyringSlot(int index) {
if (index < 0 || index >= KEYRING_SLOTS) return false;
keyring_[index].item = ItemDef{};
return true;
}
int Inventory::getBagSize(int bagIndex) const {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0;
return bags[bagIndex].size;

View file

@ -54,6 +54,7 @@ static const UFNameEntry kUFNames[] = {
{"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START},
{"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD},
{"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1},
{"PLAYER_FIELD_KEYRING_SLOT_1", UF::PLAYER_FIELD_KEYRING_SLOT_1},
{"PLAYER_FIELD_BANK_SLOT_1", UF::PLAYER_FIELD_BANK_SLOT_1},
{"PLAYER_FIELD_BANKBAG_SLOT_1", UF::PLAYER_FIELD_BANKBAG_SLOT_1},
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},

View file

@ -1017,7 +1017,11 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
int rows = (numSlots + columns - 1) / columns;
float contentH = rows * (slotSize + 4.0f) + 10.0f;
if (bagIndex < 0) contentH += 25.0f; // money display for backpack
if (bagIndex < 0) {
int keyringRows = (inventory.getKeyringSize() + columns - 1) / columns;
contentH += 25.0f; // money display for backpack
contentH += 30.0f + keyringRows * (slotSize + 4.0f); // keyring header + slots
}
float gridW = columns * (slotSize + 4.0f) + 30.0f;
// Ensure window is wide enough for the title + close button
const char* displayTitle = title;
@ -1065,6 +1069,23 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
ImGui::PopID();
}
if (bagIndex < 0) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
if (i % columns != 0) ImGui::SameLine();
const auto& slot = inventory.getKeyringSlot(i);
char id[32];
snprintf(id, sizeof(id), "##skr_%d", i);
ImGui::PushID(id);
// Keyring is display-only for now.
renderItemSlot(inventory, slot, slotSize, nullptr,
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
ImGui::PopID();
}
}
// Money display at bottom of backpack
if (bagIndex < 0 && moneyCopper > 0) {
ImGui::Spacing();
@ -2020,6 +2041,30 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool colla
ImGui::PopID();
}
}
bool keyringHasAnyItems = false;
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
if (!inventory.getKeyringSlot(i).empty()) {
keyringHasAnyItems = true;
break;
}
}
if (!collapseEmptySections || keyringHasAnyItems) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring");
for (int i = 0; i < inventory.getKeyringSize(); ++i) {
if (i % columns != 0) ImGui::SameLine();
const auto& slot = inventory.getKeyringSlot(i);
char sid[32];
snprintf(sid, sizeof(sid), "##keyring_%d", i);
ImGui::PushID(sid);
// Keyring is display-only for now.
renderItemSlot(inventory, slot, slotSize, nullptr,
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
ImGui::PopID();
}
}
}
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,