diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 52a6f1a0..b1ddeb97 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2534,6 +2534,7 @@ private: std::unordered_set pendingItemQueries_; std::array equipSlotGuids_{}; std::array backpackSlotGuids_{}; + std::array keyringSlotGuids_{}; // Container (bag) contents: containerGuid -> array of item GUIDs per slot struct ContainerInfo { uint32_t numSlots = 0; diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index ac6af201..ea6f6110 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -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 backpack{}; + std::array keyring_{}; std::array equipment{}; struct BagData { diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 5e42a049..e4687352 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -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, diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c96ef05d..0461ca59 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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& field bool slotsChanged = false; int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD)); int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1)); + int bankBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1)); + int bankBagBase = static_cast(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(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& field else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); slotsChanged = true; } - } - - // Bank slots starting at PLAYER_FIELD_BANK_SLOT_1 - int bankBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1)); - int bankBagBase = static_cast(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(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(bankBase) && key <= static_cast(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(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; }()); } diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index 259fb872..0d694aba 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -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; diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index ae45deb7..6a736546 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -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}, diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index ac6e3426..3d4b0c17 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -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,