From 381d8963480df3560cf9e219734dbeb7b5bdd4e7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 16 Feb 2026 21:11:18 -0800 Subject: [PATCH] Implement bank, guild bank, and auction house systems Add 27 new opcodes, packet builders/parsers, handler methods, inventory extension with 28 bank slots + 7 bank bags, and UI windows for personal bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid). Fix Classic gossip parser to omit boxMoney/boxText fields not present in Vanilla protocol, fix gossip icon labels with text-based NPC type detection, and add Turtle WoW opcode mappings for bank and auction interactions. --- Data/expansions/turtle/opcodes.json | 23 +- include/game/game_handler.hpp | 84 +++++ include/game/inventory.hpp | 20 + include/game/opcode_table.hpp | 33 ++ include/game/update_field_table.hpp | 2 + include/game/world_packets.hpp | 208 +++++++++++ include/ui/game_screen.hpp | 16 + src/game/game_handler.cpp | 499 ++++++++++++++++++++++++- src/game/inventory.cpp | 40 ++ src/game/opcode_table.cpp | 60 +++ src/game/packet_parsers_classic.cpp | 5 +- src/game/update_field_table.cpp | 4 + src/game/world_packets.cpp | 311 ++++++++++++++++ src/ui/game_screen.cpp | 549 +++++++++++++++++++++++++++- 14 files changed, 1839 insertions(+), 15 deletions(-) diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index 338b9684..55965275 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -249,5 +249,26 @@ "CMSG_MAIL_DELETE": "0x249", "CMSG_MAIL_MARK_AS_READ": "0x247", "SMSG_RECEIVED_MAIL": "0x285", - "MSG_QUERY_NEXT_MAIL_TIME": "0x284" + "MSG_QUERY_NEXT_MAIL_TIME": "0x284", + + "CMSG_BANKER_ACTIVATE": "0x1B7", + "SMSG_SHOW_BANK": "0x1B8", + "CMSG_BUY_BANK_SLOT": "0x1B9", + "SMSG_BUY_BANK_SLOT_RESULT": "0x1BA", + "CMSG_AUTOSTORE_BANK_ITEM": "0x282", + "CMSG_AUTOBANK_ITEM": "0x283", + + "MSG_AUCTION_HELLO": "0x255", + "CMSG_AUCTION_SELL_ITEM": "0x256", + "CMSG_AUCTION_REMOVE_ITEM": "0x257", + "CMSG_AUCTION_LIST_ITEMS": "0x258", + "CMSG_AUCTION_LIST_OWNER_ITEMS": "0x259", + "CMSG_AUCTION_PLACE_BID": "0x25A", + "SMSG_AUCTION_COMMAND_RESULT": "0x25B", + "SMSG_AUCTION_LIST_RESULT": "0x25C", + "SMSG_AUCTION_OWNER_LIST_RESULT": "0x25D", + "SMSG_AUCTION_OWNER_NOTIFICATION": "0x25E", + "SMSG_AUCTION_BIDDER_NOTIFICATION": "0x260", + "CMSG_AUCTION_LIST_BIDDER_ITEMS": "0x264", + "SMSG_AUCTION_BIDDER_LIST_RESULT": "0x265" } diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index c04edcb4..15a03176 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -785,6 +785,51 @@ public: void mailMarkAsRead(uint32_t mailId); void refreshMailList(); + // Bank + void openBank(uint64_t guid); + void closeBank(); + void buyBankSlot(); + void depositItem(uint8_t srcBag, uint8_t srcSlot); + void withdrawItem(uint8_t srcBag, uint8_t srcSlot); + bool isBankOpen() const { return bankOpen_; } + uint64_t getBankerGuid() const { return bankerGuid_; } + + // Guild Bank + void openGuildBank(uint64_t guid); + void closeGuildBank(); + void queryGuildBankTab(uint8_t tabId); + void buyGuildBankTab(); + void depositGuildBankMoney(uint32_t amount); + void withdrawGuildBankMoney(uint32_t amount); + void guildBankWithdrawItem(uint8_t tabId, uint8_t bankSlot, uint8_t destBag, uint8_t destSlot); + void guildBankDepositItem(uint8_t tabId, uint8_t bankSlot, uint8_t srcBag, uint8_t srcSlot); + bool isGuildBankOpen() const { return guildBankOpen_; } + const GuildBankData& getGuildBankData() const { return guildBankData_; } + uint8_t getGuildBankActiveTab() const { return guildBankActiveTab_; } + void setGuildBankActiveTab(uint8_t tab) { guildBankActiveTab_ = tab; } + + // Auction House + void openAuctionHouse(uint64_t guid); + void closeAuctionHouse(); + void auctionSearch(const std::string& name, uint8_t levelMin, uint8_t levelMax, + uint32_t quality, uint32_t itemClass, uint32_t itemSubClass, + uint32_t invTypeMask, uint8_t usableOnly, uint32_t offset = 0); + void auctionSellItem(uint64_t itemGuid, uint32_t stackCount, uint32_t bid, + uint32_t buyout, uint32_t duration); + void auctionPlaceBid(uint32_t auctionId, uint32_t amount); + void auctionBuyout(uint32_t auctionId, uint32_t buyoutPrice); + void auctionCancelItem(uint32_t auctionId); + void auctionListOwnerItems(uint32_t offset = 0); + void auctionListBidderItems(uint32_t offset = 0); + bool isAuctionHouseOpen() const { return auctionOpen_; } + uint64_t getAuctioneerGuid() const { return auctioneerGuid_; } + const AuctionListResult& getAuctionBrowseResults() const { return auctionBrowseResults_; } + const AuctionListResult& getAuctionOwnerResults() const { return auctionOwnerResults_; } + const AuctionListResult& getAuctionBidderResults() const { return auctionBidderResults_; } + int getAuctionActiveTab() const { return auctionActiveTab_; } + void setAuctionActiveTab(int tab) { auctionActiveTab_ = tab; } + float getAuctionSearchDelay() const { return auctionSearchDelayTimer_; } + // Trainer bool isTrainerWindowOpen() const { return trainerWindowOpen_; } const TrainerListData& getTrainerSpells() const { return currentTrainerList_; } @@ -1010,6 +1055,20 @@ private: void handleArenaTeamEvent(network::Packet& packet); void handleArenaError(network::Packet& packet); + // ---- Bank handlers ---- + void handleShowBank(network::Packet& packet); + void handleBuyBankSlotResult(network::Packet& packet); + + // ---- Guild Bank handlers ---- + void handleGuildBankList(network::Packet& packet); + + // ---- Auction House handlers ---- + void handleAuctionHello(network::Packet& packet); + void handleAuctionListResult(network::Packet& packet); + void handleAuctionOwnerListResult(network::Packet& packet); + void handleAuctionBidderListResult(network::Packet& packet); + void handleAuctionCommandResult(network::Packet& packet); + // ---- Mail handlers ---- void handleShowMailbox(network::Packet& packet); void handleMailListResult(network::Packet& packet); @@ -1360,6 +1419,31 @@ private: bool showMailCompose_ = false; bool hasNewMail_ = false; + // Bank + bool bankOpen_ = false; + uint64_t bankerGuid_ = 0; + std::array bankSlotGuids_{}; + std::array bankBagSlotGuids_{}; + + // Guild Bank + bool guildBankOpen_ = false; + uint64_t guildBankerGuid_ = 0; + GuildBankData guildBankData_; + uint8_t guildBankActiveTab_ = 0; + + // Auction House + bool auctionOpen_ = false; + uint64_t auctioneerGuid_ = 0; + uint32_t auctionHouseId_ = 0; + AuctionListResult auctionBrowseResults_; + AuctionListResult auctionOwnerResults_; + AuctionListResult auctionBidderResults_; + int auctionActiveTab_ = 0; // 0=Browse, 1=Bids, 2=Auctions + float auctionSearchDelayTimer_ = 0.0f; + // Routing: which result vector to populate from next SMSG_AUCTION_LIST_RESULT + enum class AuctionResultTarget { BROWSE, OWNER, BIDDER }; + AuctionResultTarget pendingAuctionTarget_ = AuctionResultTarget::BROWSE; + // Vendor bool vendorWindowOpen = false; ListInventoryData currentVendorItems; diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index 45bf5952..bb36e3bd 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -56,6 +56,8 @@ public: static constexpr int NUM_EQUIP_SLOTS = 23; static constexpr int NUM_BAG_SLOTS = 4; static constexpr int MAX_BAG_SIZE = 36; + static constexpr int BANK_SLOTS = 28; + static constexpr int BANK_BAG_SLOTS = 7; Inventory(); @@ -76,6 +78,19 @@ public: const ItemSlot& getBagSlot(int bagIndex, int slotIndex) const; bool setBagSlot(int bagIndex, int slotIndex, const ItemDef& item); + // Bank slots (28 main + 7 bank bags) + const ItemSlot& getBankSlot(int index) const; + bool setBankSlot(int index, const ItemDef& item); + bool clearBankSlot(int index); + + const ItemSlot& getBankBagSlot(int bagIndex, int slotIndex) const; + bool setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item); + int getBankBagSize(int bagIndex) const; + void setBankBagSize(int bagIndex, int size); + + uint8_t getPurchasedBankBagSlots() const { return purchasedBankBagSlots_; } + void setPurchasedBankBagSlots(uint8_t count) { purchasedBankBagSlots_ = count; } + // Utility int findFreeBackpackSlot() const; bool addItem(const ItemDef& item); @@ -92,6 +107,11 @@ private: std::array slots{}; }; std::array bags{}; + + // Bank + std::array bankSlots_{}; + std::array bankBags_{}; + uint8_t purchasedBankBagSlots_ = 0; }; const char* getQualityName(ItemQuality quality); diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 74632a37..b68986af 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -382,6 +382,39 @@ enum class LogicalOpcode : uint16_t { SMSG_RECEIVED_MAIL, MSG_QUERY_NEXT_MAIL_TIME, + // ---- Bank ---- + CMSG_BANKER_ACTIVATE, + SMSG_SHOW_BANK, + CMSG_BUY_BANK_SLOT, + SMSG_BUY_BANK_SLOT_RESULT, + CMSG_AUTOBANK_ITEM, + CMSG_AUTOSTORE_BANK_ITEM, + + // ---- Guild Bank ---- + CMSG_GUILD_BANKER_ACTIVATE, + CMSG_GUILD_BANK_QUERY_TAB, + SMSG_GUILD_BANK_LIST, + CMSG_GUILD_BANK_SWAP_ITEMS, + CMSG_GUILD_BANK_BUY_TAB, + CMSG_GUILD_BANK_UPDATE_TAB, + CMSG_GUILD_BANK_DEPOSIT_MONEY, + CMSG_GUILD_BANK_WITHDRAW_MONEY, + + // ---- Auction House ---- + MSG_AUCTION_HELLO, + CMSG_AUCTION_SELL_ITEM, + CMSG_AUCTION_REMOVE_ITEM, + CMSG_AUCTION_LIST_ITEMS, + CMSG_AUCTION_LIST_OWNER_ITEMS, + CMSG_AUCTION_PLACE_BID, + SMSG_AUCTION_COMMAND_RESULT, + SMSG_AUCTION_LIST_RESULT, + SMSG_AUCTION_OWNER_LIST_RESULT, + SMSG_AUCTION_BIDDER_LIST_RESULT, + SMSG_AUCTION_OWNER_NOTIFICATION, + SMSG_AUCTION_BIDDER_NOTIFICATION, + CMSG_AUCTION_LIST_BIDDER_ITEMS, + // Sentinel COUNT }; diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 5f79754b..29293cc9 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -44,6 +44,8 @@ enum class UF : uint16_t { PLAYER_QUEST_LOG_START, PLAYER_FIELD_INV_SLOT_HEAD, PLAYER_FIELD_PACK_SLOT_1, + PLAYER_FIELD_BANK_SLOT_1, + PLAYER_FIELD_BANKBAG_SLOT_1, PLAYER_SKILL_INFO_START, PLAYER_EXPLORED_ZONES_START, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 52a44efd..ecb84c73 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2262,5 +2262,213 @@ public: static network::Packet build(uint64_t mailboxGuid, uint32_t mailId); }; +// ============================================================ +// Bank System +// ============================================================ + +/** CMSG_BANKER_ACTIVATE packet builder */ +class BankerActivatePacket { +public: + static network::Packet build(uint64_t guid); +}; + +/** CMSG_BUY_BANK_SLOT packet builder */ +class BuyBankSlotPacket { +public: + static network::Packet build(uint64_t guid); +}; + +/** CMSG_AUTOBANK_ITEM packet builder (deposit item to bank) */ +class AutoBankItemPacket { +public: + static network::Packet build(uint8_t srcBag, uint8_t srcSlot); +}; + +/** CMSG_AUTOSTORE_BANK_ITEM packet builder (withdraw item from bank) */ +class AutoStoreBankItemPacket { +public: + static network::Packet build(uint8_t srcBag, uint8_t srcSlot); +}; + +// ============================================================ +// Guild Bank System +// ============================================================ + +struct GuildBankItemSlot { + uint8_t slotId = 0; + uint32_t itemEntry = 0; + uint32_t stackCount = 1; + uint32_t enchantId = 0; + uint32_t randomPropertyId = 0; +}; + +struct GuildBankTab { + std::string tabName; + std::string tabIcon; + std::vector items; +}; + +struct GuildBankData { + uint64_t money = 0; + uint8_t tabId = 0; + int32_t withdrawAmount = -1; // -1 = unlimited + std::vector tabs; // Only populated on fullUpdate + std::vector tabItems; // Current tab items +}; + +/** CMSG_GUILD_BANKER_ACTIVATE packet builder */ +class GuildBankerActivatePacket { +public: + static network::Packet build(uint64_t guid); +}; + +/** CMSG_GUILD_BANK_QUERY_TAB packet builder */ +class GuildBankQueryTabPacket { +public: + static network::Packet build(uint64_t guid, uint8_t tabId, bool fullUpdate); +}; + +/** CMSG_GUILD_BANK_BUY_TAB packet builder */ +class GuildBankBuyTabPacket { +public: + static network::Packet build(uint64_t guid, uint8_t tabId); +}; + +/** CMSG_GUILD_BANK_DEPOSIT_MONEY packet builder */ +class GuildBankDepositMoneyPacket { +public: + static network::Packet build(uint64_t guid, uint32_t amount); +}; + +/** CMSG_GUILD_BANK_WITHDRAW_MONEY packet builder */ +class GuildBankWithdrawMoneyPacket { +public: + static network::Packet build(uint64_t guid, uint32_t amount); +}; + +/** CMSG_GUILD_BANK_SWAP_ITEMS packet builder */ +class GuildBankSwapItemsPacket { +public: + // Bank to inventory + static network::Packet buildBankToInventory(uint64_t guid, uint8_t tabId, uint8_t bankSlot, + uint8_t destBag, uint8_t destSlot, uint32_t splitCount = 0); + // Inventory to bank + static network::Packet buildInventoryToBank(uint64_t guid, uint8_t tabId, uint8_t bankSlot, + uint8_t srcBag, uint8_t srcSlot, uint32_t splitCount = 0); +}; + +/** SMSG_GUILD_BANK_LIST parser */ +class GuildBankListParser { +public: + static bool parse(network::Packet& packet, GuildBankData& data); +}; + +// ============================================================ +// Auction House System +// ============================================================ + +struct AuctionEntry { + uint32_t auctionId = 0; + uint32_t itemEntry = 0; + uint32_t stackCount = 1; + uint32_t enchantId = 0; + uint32_t randomPropertyId = 0; + uint32_t suffixFactor = 0; + uint64_t ownerGuid = 0; + uint32_t startBid = 0; + uint32_t minBidIncrement = 0; + uint32_t buyoutPrice = 0; + uint32_t timeLeftMs = 0; + uint64_t bidderGuid = 0; + uint32_t currentBid = 0; +}; + +struct AuctionListResult { + std::vector auctions; + uint32_t totalCount = 0; + uint32_t searchDelay = 0; +}; + +struct AuctionCommandResult { + uint32_t auctionId = 0; + uint32_t action = 0; // 0=create, 1=cancel, 2=bid, 3=buyout + uint32_t errorCode = 0; // 0=success + uint32_t bidError = 0; // secondary error for bid actions +}; + +struct AuctionHelloData { + uint64_t auctioneerGuid = 0; + uint32_t auctionHouseId = 0; + uint8_t enabled = 1; +}; + +/** MSG_AUCTION_HELLO packet builder */ +class AuctionHelloPacket { +public: + static network::Packet build(uint64_t guid); +}; + +/** MSG_AUCTION_HELLO parser (server response) */ +class AuctionHelloParser { +public: + static bool parse(network::Packet& packet, AuctionHelloData& data); +}; + +/** CMSG_AUCTION_LIST_ITEMS packet builder */ +class AuctionListItemsPacket { +public: + static network::Packet build(uint64_t guid, uint32_t offset, + const std::string& searchName, + uint8_t levelMin, uint8_t levelMax, + uint32_t invTypeMask, uint32_t itemClass, + uint32_t itemSubClass, uint32_t quality, + uint8_t usableOnly, uint8_t exactMatch); +}; + +/** CMSG_AUCTION_SELL_ITEM packet builder */ +class AuctionSellItemPacket { +public: + static network::Packet build(uint64_t auctioneerGuid, uint64_t itemGuid, + uint32_t stackCount, uint32_t bid, + uint32_t buyout, uint32_t duration); +}; + +/** CMSG_AUCTION_PLACE_BID packet builder */ +class AuctionPlaceBidPacket { +public: + static network::Packet build(uint64_t auctioneerGuid, uint32_t auctionId, uint32_t amount); +}; + +/** CMSG_AUCTION_REMOVE_ITEM packet builder */ +class AuctionRemoveItemPacket { +public: + static network::Packet build(uint64_t auctioneerGuid, uint32_t auctionId); +}; + +/** CMSG_AUCTION_LIST_OWNER_ITEMS packet builder */ +class AuctionListOwnerItemsPacket { +public: + static network::Packet build(uint64_t auctioneerGuid, uint32_t offset); +}; + +/** CMSG_AUCTION_LIST_BIDDER_ITEMS packet builder */ +class AuctionListBidderItemsPacket { +public: + static network::Packet build(uint64_t auctioneerGuid, uint32_t offset, + const std::vector& outbiddedIds = {}); +}; + +/** SMSG_AUCTION_LIST_RESULT parser (shared for browse/owner/bidder) */ +class AuctionListResultParser { +public: + static bool parse(network::Packet& packet, AuctionListResult& data); +}; + +/** SMSG_AUCTION_COMMAND_RESULT parser */ +class AuctionCommandResultParser { +public: + static bool parse(network::Packet& packet, AuctionCommandResult& data); +}; + } // namespace game } // namespace wowee diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 5c8f5e41..dec4eef2 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -187,6 +187,9 @@ private: void renderChatBubbles(game::GameHandler& gameHandler); void renderMailWindow(game::GameHandler& gameHandler); void renderMailComposeWindow(game::GameHandler& gameHandler); + void renderBankWindow(game::GameHandler& gameHandler); + void renderGuildBankWindow(game::GameHandler& gameHandler); + void renderAuctionHouseWindow(game::GameHandler& gameHandler); /** * Inventory screen @@ -254,6 +257,19 @@ private: char mailBodyBuffer_[2048] = ""; int mailComposeMoney_[3] = {0, 0, 0}; // gold, silver, copper + // Auction house UI state + char auctionSearchName_[256] = ""; + int auctionLevelMin_ = 0; + int auctionLevelMax_ = 0; + int auctionQuality_ = 0; + int auctionSellDuration_ = 2; // 0=12h, 1=24h, 2=48h + int auctionSellBid_[3] = {0, 0, 0}; // gold, silver, copper + int auctionSellBuyout_[3] = {0, 0, 0}; // gold, silver, copper + int auctionSelectedItem_ = -1; + + // Guild bank money input + int guildBankMoneyInput_[3] = {0, 0, 0}; // gold, silver, copper + // Left-click targeting: distinguish click from camera drag glm::vec2 leftClickPressPos_ = glm::vec2(0.0f); bool leftClickWasPress_ = false; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6397f30f..0a0ba132 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -296,6 +296,11 @@ void GameHandler::update(float deltaTime) { clearTarget(); } + if (auctionSearchDelayTimer_ > 0.0f) { + auctionSearchDelayTimer_ -= deltaTime; + if (auctionSearchDelayTimer_ < 0.0f) auctionSearchDelayTimer_ = 0.0f; + } + if (pendingMoneyDeltaTimer_ > 0.0f) { pendingMoneyDeltaTimer_ -= deltaTime; if (pendingMoneyDeltaTimer_ <= 0.0f) { @@ -1526,6 +1531,36 @@ void GameHandler::handlePacket(network::Packet& packet) { handleQueryNextMailTime(packet); break; + // ---- Bank ---- + case Opcode::SMSG_SHOW_BANK: + handleShowBank(packet); + break; + case Opcode::SMSG_BUY_BANK_SLOT_RESULT: + handleBuyBankSlotResult(packet); + break; + + // ---- Guild Bank ---- + case Opcode::SMSG_GUILD_BANK_LIST: + handleGuildBankList(packet); + break; + + // ---- Auction House ---- + case Opcode::MSG_AUCTION_HELLO: + handleAuctionHello(packet); + break; + case Opcode::SMSG_AUCTION_LIST_RESULT: + handleAuctionListResult(packet); + break; + case Opcode::SMSG_AUCTION_OWNER_LIST_RESULT: + handleAuctionOwnerListResult(packet); + break; + case Opcode::SMSG_AUCTION_BIDDER_LIST_RESULT: + handleAuctionBidderListResult(packet); + break; + case Opcode::SMSG_AUCTION_COMMAND_RESULT: + handleAuctionCommandResult(packet); + break; + default: // In pre-world states we need full visibility (char create/login handshakes). // In-world we keep de-duplication to avoid heavy log I/O in busy areas. @@ -5776,6 +5811,34 @@ bool GameHandler::applyInventoryFields(const std::map& field slotsChanged = true; } } + + // Bank slots: 28 slots × 2 fields = 56 fields starting at PLAYER_FIELD_BANK_SLOT_1 + int bankBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1)); + if (bankBase != 0xFFFF && key >= static_cast(bankBase) && + key <= static_cast(bankBase) + (game::Inventory::BANK_SLOTS * 2 - 1)) { + int slotIndex = (key - bankBase) / 2; + bool isLow = ((key - bankBase) % 2 == 0); + if (slotIndex < static_cast(bankSlotGuids_.size())) { + uint64_t& guid = bankSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } + } + + // Bank bag slots: 7 slots × 2 fields = 14 fields starting at PLAYER_FIELD_BANKBAG_SLOT_1 + int bankBagBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1)); + if (bankBagBase != 0xFFFF && key >= static_cast(bankBagBase) && + key <= static_cast(bankBagBase) + (game::Inventory::BANK_BAG_SLOTS * 2 - 1)) { + int slotIndex = (key - bankBagBase) / 2; + bool isLow = ((key - bankBagBase) % 2 == 0); + if (slotIndex < static_cast(bankBagSlotGuids_.size())) { + uint64_t& guid = bankBagSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } + } } return slotsChanged; @@ -5953,6 +6016,105 @@ void GameHandler::rebuildOnlineInventory() { } } + // Bank slots (28 main slots) + for (int i = 0; i < 28; i++) { + uint64_t guid = bankSlotGuids_[i]; + if (guid == 0) { inventory.clearBankSlot(i); continue; } + + auto itemIt = onlineItems_.find(guid); + if (itemIt == onlineItems_.end()) continue; + + ItemDef def; + def.itemId = itemIt->second.entry; + def.stackCount = itemIt->second.stackCount; + 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.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.bagSlots = infoIt->second.containerSlots; + } else { + def.name = "Item " + std::to_string(def.itemId); + queryItemInfo(def.itemId, guid); + } + + inventory.setBankSlot(i, def); + } + + // Bank bag contents (7 bank bag slots) + for (int bagIdx = 0; bagIdx < 7; bagIdx++) { + uint64_t bagGuid = bankBagSlotGuids_[bagIdx]; + if (bagGuid == 0) { inventory.setBankBagSize(bagIdx, 0); continue; } + + int numSlots = 0; + auto contIt = containerContents_.find(bagGuid); + if (contIt != containerContents_.end()) { + numSlots = static_cast(contIt->second.numSlots); + } + if (numSlots <= 0) { + auto bagItemIt = onlineItems_.find(bagGuid); + if (bagItemIt != onlineItems_.end()) { + auto bagInfoIt = itemInfoCache_.find(bagItemIt->second.entry); + if (bagInfoIt != itemInfoCache_.end()) { + numSlots = bagInfoIt->second.containerSlots; + } + } + } + if (numSlots <= 0) continue; + + inventory.setBankBagSize(bagIdx, numSlots); + + if (contIt == containerContents_.end()) continue; + const auto& container = contIt->second; + for (int s = 0; s < numSlots && s < 36; s++) { + uint64_t itemGuid = container.slotGuids[s]; + if (itemGuid == 0) continue; + + auto itemIt = onlineItems_.find(itemGuid); + if (itemIt == onlineItems_.end()) continue; + + ItemDef def; + def.itemId = itemIt->second.entry; + def.stackCount = itemIt->second.stackCount; + 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.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.bagSlots = infoIt->second.containerSlots; + } else { + def.name = "Item " + std::to_string(def.itemId); + queryItemInfo(def.itemId, itemGuid); + } + + inventory.setBankBagSlot(bagIdx, s, def); + } + } + // Only mark equipment dirty if equipped item displayInfoIds actually changed std::array currentEquipDisplayIds{}; for (int i = 0; i < 19; i++) { @@ -7450,17 +7612,46 @@ void GameHandler::interactWithGameObject(uint64_t guid) { void GameHandler::selectGossipOption(uint32_t optionId) { if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; + LOG_INFO("selectGossipOption: optionId=", optionId, + " npcGuid=0x", std::hex, currentGossip.npcGuid, std::dec, + " menuId=", currentGossip.menuId, + " numOptions=", currentGossip.options.size()); auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, currentGossip.menuId, optionId); socket->send(packet); - // If this is an innkeeper "make this inn your home" option, send binder activate. for (const auto& opt : currentGossip.options) { if (opt.id != optionId) continue; + LOG_INFO(" matched option: id=", opt.id, " icon=", (int)opt.icon, " text='", opt.text, "'"); + + // Icon-based NPC interaction fallbacks + // Some servers need the specific activate packet in addition to gossip select + if (opt.icon == 6) { + // GOSSIP_ICON_MONEY_BAG = banker + auto pkt = BankerActivatePacket::build(currentGossip.npcGuid); + socket->send(pkt); + LOG_INFO("Sent CMSG_BANKER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); + } + + // Text-based NPC type detection for servers using placeholder strings std::string text = opt.text; - std::transform(text.begin(), text.end(), text.begin(), + std::string textLower = text; + std::transform(textLower.begin(), textLower.end(), textLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); - if (text.find("make this inn your home") != std::string::npos || - text.find("set your home") != std::string::npos) { + + if (text == "GOSSIP_OPTION_AUCTIONEER" || textLower.find("auction") != std::string::npos) { + auto pkt = AuctionHelloPacket::build(currentGossip.npcGuid); + socket->send(pkt); + LOG_INFO("Sent MSG_AUCTION_HELLO for npc=0x", std::hex, currentGossip.npcGuid, std::dec); + } + + if (text == "GOSSIP_OPTION_BANKER" || textLower.find("deposit box") != std::string::npos) { + auto pkt = BankerActivatePacket::build(currentGossip.npcGuid); + socket->send(pkt); + LOG_INFO("Sent CMSG_BANKER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); + } + + if (textLower.find("make this inn your home") != std::string::npos || + textLower.find("set your home") != std::string::npos) { auto bindPkt = BinderActivatePacket::build(currentGossip.npcGuid); socket->send(bindPkt); LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); @@ -9949,5 +10140,305 @@ glm::vec3 GameHandler::getComposedWorldPosition() { return glm::vec3(movementInfo.x, movementInfo.y, movementInfo.z); } +// ============================================================ +// Bank System +// ============================================================ + +void GameHandler::openBank(uint64_t guid) { + if (!isConnected()) return; + auto pkt = BankerActivatePacket::build(guid); + socket->send(pkt); +} + +void GameHandler::closeBank() { + bankOpen_ = false; + bankerGuid_ = 0; +} + +void GameHandler::buyBankSlot() { + if (!isConnected() || !bankOpen_) return; + auto pkt = BuyBankSlotPacket::build(bankerGuid_); + socket->send(pkt); +} + +void GameHandler::depositItem(uint8_t srcBag, uint8_t srcSlot) { + if (!isConnected() || !bankOpen_) return; + auto pkt = AutoBankItemPacket::build(srcBag, srcSlot); + socket->send(pkt); +} + +void GameHandler::withdrawItem(uint8_t srcBag, uint8_t srcSlot) { + if (!isConnected() || !bankOpen_) return; + auto pkt = AutoStoreBankItemPacket::build(srcBag, srcSlot); + socket->send(pkt); +} + +void GameHandler::handleShowBank(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 8) return; + bankerGuid_ = packet.readUInt64(); + bankOpen_ = true; + gossipWindowOpen = false; // Close gossip when bank opens + // Bank items are already tracked via update fields (bank slot GUIDs) + // Trigger rebuild to populate bank slots in inventory + rebuildOnlineInventory(); + LOG_INFO("SMSG_SHOW_BANK: banker=0x", std::hex, bankerGuid_, std::dec); +} + +void GameHandler::handleBuyBankSlotResult(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t result = packet.readUInt32(); + if (result == 0) { + addSystemChatMessage("Bank slot purchased."); + inventory.setPurchasedBankBagSlots(inventory.getPurchasedBankBagSlots() + 1); + } else { + addSystemChatMessage("Cannot purchase bank slot."); + } +} + +// ============================================================ +// Guild Bank System +// ============================================================ + +void GameHandler::openGuildBank(uint64_t guid) { + if (!isConnected()) return; + auto pkt = GuildBankerActivatePacket::build(guid); + socket->send(pkt); +} + +void GameHandler::closeGuildBank() { + guildBankOpen_ = false; + guildBankerGuid_ = 0; +} + +void GameHandler::queryGuildBankTab(uint8_t tabId) { + if (!isConnected() || !guildBankOpen_) return; + guildBankActiveTab_ = tabId; + auto pkt = GuildBankQueryTabPacket::build(guildBankerGuid_, tabId, true); + socket->send(pkt); +} + +void GameHandler::buyGuildBankTab() { + if (!isConnected() || !guildBankOpen_) return; + uint8_t nextTab = static_cast(guildBankData_.tabs.size()); + auto pkt = GuildBankBuyTabPacket::build(guildBankerGuid_, nextTab); + socket->send(pkt); +} + +void GameHandler::depositGuildBankMoney(uint32_t amount) { + if (!isConnected() || !guildBankOpen_) return; + auto pkt = GuildBankDepositMoneyPacket::build(guildBankerGuid_, amount); + socket->send(pkt); +} + +void GameHandler::withdrawGuildBankMoney(uint32_t amount) { + if (!isConnected() || !guildBankOpen_) return; + auto pkt = GuildBankWithdrawMoneyPacket::build(guildBankerGuid_, amount); + socket->send(pkt); +} + +void GameHandler::guildBankWithdrawItem(uint8_t tabId, uint8_t bankSlot, uint8_t destBag, uint8_t destSlot) { + if (!isConnected() || !guildBankOpen_) return; + auto pkt = GuildBankSwapItemsPacket::buildBankToInventory(guildBankerGuid_, tabId, bankSlot, destBag, destSlot); + socket->send(pkt); +} + +void GameHandler::guildBankDepositItem(uint8_t tabId, uint8_t bankSlot, uint8_t srcBag, uint8_t srcSlot) { + if (!isConnected() || !guildBankOpen_) return; + auto pkt = GuildBankSwapItemsPacket::buildInventoryToBank(guildBankerGuid_, tabId, bankSlot, srcBag, srcSlot); + socket->send(pkt); +} + +void GameHandler::handleGuildBankList(network::Packet& packet) { + GuildBankData data; + if (!GuildBankListParser::parse(packet, data)) { + LOG_WARNING("Failed to parse SMSG_GUILD_BANK_LIST"); + return; + } + guildBankData_ = data; + guildBankOpen_ = true; + guildBankActiveTab_ = data.tabId; + + // Ensure item info for all guild bank items + for (const auto& item : data.tabItems) { + if (item.itemEntry != 0) ensureItemInfo(item.itemEntry); + } + + LOG_INFO("SMSG_GUILD_BANK_LIST: tab=", (int)data.tabId, + " items=", data.tabItems.size(), + " tabs=", data.tabs.size(), + " money=", data.money); +} + +// ============================================================ +// Auction House System +// ============================================================ + +void GameHandler::openAuctionHouse(uint64_t guid) { + if (!isConnected()) return; + auto pkt = AuctionHelloPacket::build(guid); + socket->send(pkt); +} + +void GameHandler::closeAuctionHouse() { + auctionOpen_ = false; + auctioneerGuid_ = 0; +} + +void GameHandler::auctionSearch(const std::string& name, uint8_t levelMin, uint8_t levelMax, + uint32_t quality, uint32_t itemClass, uint32_t itemSubClass, + uint32_t invTypeMask, uint8_t usableOnly, uint32_t offset) +{ + if (!isConnected() || !auctionOpen_) return; + if (auctionSearchDelayTimer_ > 0.0f) { + addSystemChatMessage("Please wait before searching again."); + return; + } + pendingAuctionTarget_ = AuctionResultTarget::BROWSE; + auto pkt = AuctionListItemsPacket::build(auctioneerGuid_, offset, name, + levelMin, levelMax, invTypeMask, + itemClass, itemSubClass, quality, usableOnly, 0); + socket->send(pkt); +} + +void GameHandler::auctionSellItem(uint64_t itemGuid, uint32_t stackCount, + uint32_t bid, uint32_t buyout, uint32_t duration) +{ + if (!isConnected() || !auctionOpen_) return; + auto pkt = AuctionSellItemPacket::build(auctioneerGuid_, itemGuid, stackCount, bid, buyout, duration); + socket->send(pkt); +} + +void GameHandler::auctionPlaceBid(uint32_t auctionId, uint32_t amount) { + if (!isConnected() || !auctionOpen_) return; + auto pkt = AuctionPlaceBidPacket::build(auctioneerGuid_, auctionId, amount); + socket->send(pkt); +} + +void GameHandler::auctionBuyout(uint32_t auctionId, uint32_t buyoutPrice) { + auctionPlaceBid(auctionId, buyoutPrice); +} + +void GameHandler::auctionCancelItem(uint32_t auctionId) { + if (!isConnected() || !auctionOpen_) return; + auto pkt = AuctionRemoveItemPacket::build(auctioneerGuid_, auctionId); + socket->send(pkt); +} + +void GameHandler::auctionListOwnerItems(uint32_t offset) { + if (!isConnected() || !auctionOpen_) return; + pendingAuctionTarget_ = AuctionResultTarget::OWNER; + auto pkt = AuctionListOwnerItemsPacket::build(auctioneerGuid_, offset); + socket->send(pkt); +} + +void GameHandler::auctionListBidderItems(uint32_t offset) { + if (!isConnected() || !auctionOpen_) return; + pendingAuctionTarget_ = AuctionResultTarget::BIDDER; + auto pkt = AuctionListBidderItemsPacket::build(auctioneerGuid_, offset); + socket->send(pkt); +} + +void GameHandler::handleAuctionHello(network::Packet& packet) { + size_t pktSize = packet.getSize(); + size_t readPos = packet.getReadPos(); + LOG_INFO("handleAuctionHello: packetSize=", pktSize, " readPos=", readPos); + // Hex dump first 20 bytes for debugging + const auto& rawData = packet.getData(); + std::string hex; + size_t dumpLen = std::min(rawData.size(), 20); + for (size_t i = 0; i < dumpLen; ++i) { + char b[4]; snprintf(b, sizeof(b), "%02x ", rawData[i]); + hex += b; + } + LOG_INFO(" hex dump: ", hex); + AuctionHelloData data; + if (!AuctionHelloParser::parse(packet, data)) { + LOG_WARNING("Failed to parse MSG_AUCTION_HELLO response, size=", pktSize, " readPos=", readPos); + return; + } + auctioneerGuid_ = data.auctioneerGuid; + auctionHouseId_ = data.auctionHouseId; + auctionOpen_ = true; + gossipWindowOpen = false; // Close gossip when auction house opens + auctionActiveTab_ = 0; + auctionBrowseResults_ = AuctionListResult{}; + auctionOwnerResults_ = AuctionListResult{}; + auctionBidderResults_ = AuctionListResult{}; + LOG_INFO("MSG_AUCTION_HELLO: auctioneer=0x", std::hex, data.auctioneerGuid, std::dec, + " house=", data.auctionHouseId, " enabled=", (int)data.enabled); +} + +void GameHandler::handleAuctionListResult(network::Packet& packet) { + AuctionListResult result; + if (!AuctionListResultParser::parse(packet, result)) { + LOG_WARNING("Failed to parse SMSG_AUCTION_LIST_RESULT"); + return; + } + + auctionBrowseResults_ = result; + auctionSearchDelayTimer_ = result.searchDelay / 1000.0f; + + // Ensure item info for all auction items + for (const auto& entry : result.auctions) { + if (entry.itemEntry != 0) ensureItemInfo(entry.itemEntry); + } + + LOG_INFO("SMSG_AUCTION_LIST_RESULT: ", result.auctions.size(), " items, total=", result.totalCount); +} + +void GameHandler::handleAuctionOwnerListResult(network::Packet& packet) { + AuctionListResult result; + if (!AuctionListResultParser::parse(packet, result)) { + LOG_WARNING("Failed to parse SMSG_AUCTION_OWNER_LIST_RESULT"); + return; + } + auctionOwnerResults_ = result; + for (const auto& entry : result.auctions) { + if (entry.itemEntry != 0) ensureItemInfo(entry.itemEntry); + } + LOG_INFO("SMSG_AUCTION_OWNER_LIST_RESULT: ", result.auctions.size(), " items"); +} + +void GameHandler::handleAuctionBidderListResult(network::Packet& packet) { + AuctionListResult result; + if (!AuctionListResultParser::parse(packet, result)) { + LOG_WARNING("Failed to parse SMSG_AUCTION_BIDDER_LIST_RESULT"); + return; + } + auctionBidderResults_ = result; + for (const auto& entry : result.auctions) { + if (entry.itemEntry != 0) ensureItemInfo(entry.itemEntry); + } + LOG_INFO("SMSG_AUCTION_BIDDER_LIST_RESULT: ", result.auctions.size(), " items"); +} + +void GameHandler::handleAuctionCommandResult(network::Packet& packet) { + AuctionCommandResult result; + if (!AuctionCommandResultParser::parse(packet, result)) { + LOG_WARNING("Failed to parse SMSG_AUCTION_COMMAND_RESULT"); + return; + } + + const char* actions[] = {"Create", "Cancel", "Bid", "Buyout"}; + const char* actionName = (result.action < 4) ? actions[result.action] : "Unknown"; + + if (result.errorCode == 0) { + std::string msg = std::string("Auction ") + actionName + " successful."; + addSystemChatMessage(msg); + // Refresh appropriate list + if (result.action == 0) auctionListOwnerItems(); + else if (result.action == 1) auctionListOwnerItems(); + } else { + const char* errors[] = {"OK", "Inventory", "Not enough money", "Item not found", + "Higher bid", "Increment", "Not enough items", + "DB error", "Restricted account"}; + const char* errName = (result.errorCode < 9) ? errors[result.errorCode] : "Unknown"; + std::string msg = std::string("Auction ") + actionName + " failed: " + errName; + addSystemChatMessage(msg); + } + LOG_INFO("SMSG_AUCTION_COMMAND_RESULT: action=", actionName, + " error=", result.errorCode); +} + } // namespace game } // namespace wowee diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index a7e7c6f2..f42a42e5 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -68,6 +68,46 @@ bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) { return true; } +const ItemSlot& Inventory::getBankSlot(int index) const { + if (index < 0 || index >= BANK_SLOTS) return EMPTY_SLOT; + return bankSlots_[index]; +} + +bool Inventory::setBankSlot(int index, const ItemDef& item) { + if (index < 0 || index >= BANK_SLOTS) return false; + bankSlots_[index].item = item; + return true; +} + +bool Inventory::clearBankSlot(int index) { + if (index < 0 || index >= BANK_SLOTS) return false; + bankSlots_[index].item = ItemDef{}; + return true; +} + +const ItemSlot& Inventory::getBankBagSlot(int bagIndex, int slotIndex) const { + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return EMPTY_SLOT; + if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return EMPTY_SLOT; + return bankBags_[bagIndex].slots[slotIndex]; +} + +bool Inventory::setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item) { + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return false; + if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return false; + bankBags_[bagIndex].slots[slotIndex].item = item; + return true; +} + +int Inventory::getBankBagSize(int bagIndex) const { + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return 0; + return bankBags_[bagIndex].size; +} + +void Inventory::setBankBagSize(int bagIndex, int size) { + if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return; + bankBags_[bagIndex].size = std::min(size, MAX_BAG_SIZE); +} + int Inventory::findFreeBackpackSlot() const { for (int i = 0; i < BACKPACK_SLOTS; i++) { if (backpack[i].empty()) return i; diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 3dfe1bc6..2e7fda54 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -307,6 +307,36 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"CMSG_MAIL_MARK_AS_READ", LogicalOpcode::CMSG_MAIL_MARK_AS_READ}, {"SMSG_RECEIVED_MAIL", LogicalOpcode::SMSG_RECEIVED_MAIL}, {"MSG_QUERY_NEXT_MAIL_TIME", LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME}, + // Bank + {"CMSG_BANKER_ACTIVATE", LogicalOpcode::CMSG_BANKER_ACTIVATE}, + {"SMSG_SHOW_BANK", LogicalOpcode::SMSG_SHOW_BANK}, + {"CMSG_BUY_BANK_SLOT", LogicalOpcode::CMSG_BUY_BANK_SLOT}, + {"SMSG_BUY_BANK_SLOT_RESULT", LogicalOpcode::SMSG_BUY_BANK_SLOT_RESULT}, + {"CMSG_AUTOBANK_ITEM", LogicalOpcode::CMSG_AUTOBANK_ITEM}, + {"CMSG_AUTOSTORE_BANK_ITEM", LogicalOpcode::CMSG_AUTOSTORE_BANK_ITEM}, + // Guild Bank + {"CMSG_GUILD_BANKER_ACTIVATE", LogicalOpcode::CMSG_GUILD_BANKER_ACTIVATE}, + {"CMSG_GUILD_BANK_QUERY_TAB", LogicalOpcode::CMSG_GUILD_BANK_QUERY_TAB}, + {"SMSG_GUILD_BANK_LIST", LogicalOpcode::SMSG_GUILD_BANK_LIST}, + {"CMSG_GUILD_BANK_SWAP_ITEMS", LogicalOpcode::CMSG_GUILD_BANK_SWAP_ITEMS}, + {"CMSG_GUILD_BANK_BUY_TAB", LogicalOpcode::CMSG_GUILD_BANK_BUY_TAB}, + {"CMSG_GUILD_BANK_UPDATE_TAB", LogicalOpcode::CMSG_GUILD_BANK_UPDATE_TAB}, + {"CMSG_GUILD_BANK_DEPOSIT_MONEY", LogicalOpcode::CMSG_GUILD_BANK_DEPOSIT_MONEY}, + {"CMSG_GUILD_BANK_WITHDRAW_MONEY", LogicalOpcode::CMSG_GUILD_BANK_WITHDRAW_MONEY}, + // Auction House + {"MSG_AUCTION_HELLO", LogicalOpcode::MSG_AUCTION_HELLO}, + {"CMSG_AUCTION_SELL_ITEM", LogicalOpcode::CMSG_AUCTION_SELL_ITEM}, + {"CMSG_AUCTION_REMOVE_ITEM", LogicalOpcode::CMSG_AUCTION_REMOVE_ITEM}, + {"CMSG_AUCTION_LIST_ITEMS", LogicalOpcode::CMSG_AUCTION_LIST_ITEMS}, + {"CMSG_AUCTION_LIST_OWNER_ITEMS", LogicalOpcode::CMSG_AUCTION_LIST_OWNER_ITEMS}, + {"CMSG_AUCTION_PLACE_BID", LogicalOpcode::CMSG_AUCTION_PLACE_BID}, + {"SMSG_AUCTION_COMMAND_RESULT", LogicalOpcode::SMSG_AUCTION_COMMAND_RESULT}, + {"SMSG_AUCTION_LIST_RESULT", LogicalOpcode::SMSG_AUCTION_LIST_RESULT}, + {"SMSG_AUCTION_OWNER_LIST_RESULT", LogicalOpcode::SMSG_AUCTION_OWNER_LIST_RESULT}, + {"SMSG_AUCTION_BIDDER_LIST_RESULT", LogicalOpcode::SMSG_AUCTION_BIDDER_LIST_RESULT}, + {"SMSG_AUCTION_OWNER_NOTIFICATION", LogicalOpcode::SMSG_AUCTION_OWNER_NOTIFICATION}, + {"SMSG_AUCTION_BIDDER_NOTIFICATION", LogicalOpcode::SMSG_AUCTION_BIDDER_NOTIFICATION}, + {"CMSG_AUCTION_LIST_BIDDER_ITEMS", LogicalOpcode::CMSG_AUCTION_LIST_BIDDER_ITEMS}, }; // clang-format on @@ -615,6 +645,36 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::CMSG_MAIL_MARK_AS_READ, 0x247}, {LogicalOpcode::SMSG_RECEIVED_MAIL, 0x285}, {LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME, 0x284}, + // Bank + {LogicalOpcode::CMSG_BANKER_ACTIVATE, 0x1B7}, + {LogicalOpcode::SMSG_SHOW_BANK, 0x1B8}, + {LogicalOpcode::CMSG_BUY_BANK_SLOT, 0x1B9}, + {LogicalOpcode::SMSG_BUY_BANK_SLOT_RESULT, 0x1BA}, + {LogicalOpcode::CMSG_AUTOBANK_ITEM, 0x283}, + {LogicalOpcode::CMSG_AUTOSTORE_BANK_ITEM, 0x282}, + // Guild Bank + {LogicalOpcode::CMSG_GUILD_BANKER_ACTIVATE, 0x3E6}, + {LogicalOpcode::CMSG_GUILD_BANK_QUERY_TAB, 0x3E7}, + {LogicalOpcode::SMSG_GUILD_BANK_LIST, 0x3E8}, + {LogicalOpcode::CMSG_GUILD_BANK_SWAP_ITEMS, 0x3E9}, + {LogicalOpcode::CMSG_GUILD_BANK_BUY_TAB, 0x3EA}, + {LogicalOpcode::CMSG_GUILD_BANK_UPDATE_TAB, 0x3EB}, + {LogicalOpcode::CMSG_GUILD_BANK_DEPOSIT_MONEY, 0x3EC}, + {LogicalOpcode::CMSG_GUILD_BANK_WITHDRAW_MONEY, 0x3ED}, + // Auction House + {LogicalOpcode::MSG_AUCTION_HELLO, 0x255}, + {LogicalOpcode::CMSG_AUCTION_SELL_ITEM, 0x256}, + {LogicalOpcode::CMSG_AUCTION_REMOVE_ITEM, 0x257}, + {LogicalOpcode::CMSG_AUCTION_LIST_ITEMS, 0x258}, + {LogicalOpcode::CMSG_AUCTION_LIST_OWNER_ITEMS, 0x259}, + {LogicalOpcode::CMSG_AUCTION_PLACE_BID, 0x25A}, + {LogicalOpcode::SMSG_AUCTION_COMMAND_RESULT, 0x25B}, + {LogicalOpcode::SMSG_AUCTION_LIST_RESULT, 0x25C}, + {LogicalOpcode::SMSG_AUCTION_OWNER_LIST_RESULT, 0x25D}, + {LogicalOpcode::SMSG_AUCTION_BIDDER_LIST_RESULT, 0x265}, + {LogicalOpcode::SMSG_AUCTION_OWNER_NOTIFICATION, 0x25E}, + {LogicalOpcode::SMSG_AUCTION_BIDDER_NOTIFICATION, 0x260}, + {LogicalOpcode::CMSG_AUCTION_LIST_BIDDER_ITEMS, 0x264}, }; logicalToWire_.clear(); diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index fbd9ba48..ef7a121d 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -673,9 +673,10 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes opt.id = packet.readUInt32(); opt.icon = packet.readUInt8(); opt.isCoded = (packet.readUInt8() != 0); - opt.boxMoney = packet.readUInt32(); + // Classic/Vanilla: NO boxMoney or boxText fields (commented out in mangoszero) + opt.boxMoney = 0; opt.text = packet.readString(); - opt.boxText = packet.readString(); + opt.boxText = ""; data.options.push_back(opt); } diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index d03c01b9..a46dc89f 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -45,6 +45,8 @@ 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_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}, {"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START}, {"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID}, @@ -84,6 +86,8 @@ void UpdateFieldTable::loadWotlkDefaults() { {UF::PLAYER_QUEST_LOG_START, 158}, {UF::PLAYER_FIELD_INV_SLOT_HEAD, 324}, {UF::PLAYER_FIELD_PACK_SLOT_1, 370}, + {UF::PLAYER_FIELD_BANK_SLOT_1, 402}, + {UF::PLAYER_FIELD_BANKBAG_SLOT_1, 458}, {UF::PLAYER_SKILL_INFO_START, 636}, {UF::PLAYER_EXPLORED_ZONES_START, 1041}, {UF::GAMEOBJECT_DISPLAYID, 8}, diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 2e89dedd..038be17c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3480,5 +3480,316 @@ bool PacketParsers::parseMailList(network::Packet& packet, std::vector bank source + p.writeUInt8(tabId); + p.writeUInt8(bankSlot); + p.writeUInt32(0); // itemEntry (unused client side) + p.writeUInt8(0); // autoStore = false + if (splitCount > 0) { + p.writeUInt8(splitCount); + } + p.writeUInt8(destBag); + p.writeUInt8(destSlot); + return p; +} + +network::Packet GuildBankSwapItemsPacket::buildInventoryToBank( + uint64_t guid, uint8_t tabId, uint8_t bankSlot, + uint8_t srcBag, uint8_t srcSlot, uint32_t splitCount) +{ + network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS)); + p.writeUInt64(guid); + p.writeUInt8(1); // bankToCharacter = true -> char to bank + p.writeUInt8(tabId); + p.writeUInt8(bankSlot); + p.writeUInt32(0); // itemEntry + p.writeUInt8(0); // autoStore + if (splitCount > 0) { + p.writeUInt8(splitCount); + } + p.writeUInt8(srcBag); + p.writeUInt8(srcSlot); + return p; +} + +bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) { + if (packet.getSize() - packet.getReadPos() < 14) return false; + + data.money = packet.readUInt64(); + data.tabId = packet.readUInt8(); + data.withdrawAmount = static_cast(packet.readUInt32()); + uint8_t fullUpdate = packet.readUInt8(); + + if (fullUpdate) { + uint8_t tabCount = packet.readUInt8(); + data.tabs.resize(tabCount); + for (uint8_t i = 0; i < tabCount; ++i) { + data.tabs[i].tabName = packet.readString(); + data.tabs[i].tabIcon = packet.readString(); + } + } + + uint8_t numSlots = packet.readUInt8(); + data.tabItems.clear(); + for (uint8_t i = 0; i < numSlots; ++i) { + GuildBankItemSlot slot; + slot.slotId = packet.readUInt8(); + slot.itemEntry = packet.readUInt32(); + if (slot.itemEntry != 0) { + // Enchant info + uint32_t enchantMask = packet.readUInt32(); + for (int bit = 0; bit < 10; ++bit) { + if (enchantMask & (1u << bit)) { + uint32_t enchId = packet.readUInt32(); + uint32_t enchDur = packet.readUInt32(); + uint32_t enchCharges = packet.readUInt32(); + if (bit == 0) slot.enchantId = enchId; + (void)enchDur; (void)enchCharges; + } + } + slot.stackCount = packet.readUInt32(); + /*spare=*/ packet.readUInt32(); + slot.randomPropertyId = packet.readUInt32(); + if (slot.randomPropertyId) { + /*suffixFactor=*/ packet.readUInt32(); + } + } + data.tabItems.push_back(slot); + } + return true; +} + +// ============================================================ +// Auction House System +// ============================================================ + +network::Packet AuctionHelloPacket::build(uint64_t guid) { + network::Packet p(wireOpcode(Opcode::MSG_AUCTION_HELLO)); + p.writeUInt64(guid); + return p; +} + +bool AuctionHelloParser::parse(network::Packet& packet, AuctionHelloData& data) { + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining < 12) { + LOG_WARNING("AuctionHelloParser: too small, remaining=", remaining); + return false; + } + data.auctioneerGuid = packet.readUInt64(); + data.auctionHouseId = packet.readUInt32(); + // WotLK has an extra uint8 enabled field; Vanilla does not + if (packet.getReadPos() < packet.getSize()) { + data.enabled = packet.readUInt8(); + } else { + data.enabled = 1; + } + return true; +} + +network::Packet AuctionListItemsPacket::build( + uint64_t guid, uint32_t offset, + const std::string& searchName, + uint8_t levelMin, uint8_t levelMax, + uint32_t invTypeMask, uint32_t itemClass, + uint32_t itemSubClass, uint32_t quality, + uint8_t usableOnly, uint8_t exactMatch) +{ + network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_ITEMS)); + p.writeUInt64(guid); + p.writeUInt32(offset); + p.writeString(searchName); + p.writeUInt8(levelMin); + p.writeUInt8(levelMax); + p.writeUInt32(invTypeMask); + p.writeUInt32(itemClass); + p.writeUInt32(itemSubClass); + p.writeUInt32(quality); + p.writeUInt8(usableOnly); + p.writeUInt8(0); // getAll (0 = normal search) + // Sort columns (0 = none) + p.writeUInt8(0); + return p; +} + +network::Packet AuctionSellItemPacket::build( + uint64_t auctioneerGuid, uint64_t itemGuid, + uint32_t stackCount, uint32_t bid, + uint32_t buyout, uint32_t duration) +{ + network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_SELL_ITEM)); + p.writeUInt64(auctioneerGuid); + p.writeUInt32(1); // item count (WotLK supports multiple, we send 1) + p.writeUInt64(itemGuid); + p.writeUInt32(stackCount); + p.writeUInt32(bid); + p.writeUInt32(buyout); + p.writeUInt32(duration); + return p; +} + +network::Packet AuctionPlaceBidPacket::build(uint64_t auctioneerGuid, uint32_t auctionId, uint32_t amount) { + network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_PLACE_BID)); + p.writeUInt64(auctioneerGuid); + p.writeUInt32(auctionId); + p.writeUInt32(amount); + return p; +} + +network::Packet AuctionRemoveItemPacket::build(uint64_t auctioneerGuid, uint32_t auctionId) { + network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_REMOVE_ITEM)); + p.writeUInt64(auctioneerGuid); + p.writeUInt32(auctionId); + return p; +} + +network::Packet AuctionListOwnerItemsPacket::build(uint64_t auctioneerGuid, uint32_t offset) { + network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_OWNER_ITEMS)); + p.writeUInt64(auctioneerGuid); + p.writeUInt32(offset); + return p; +} + +network::Packet AuctionListBidderItemsPacket::build( + uint64_t auctioneerGuid, uint32_t offset, + const std::vector& outbiddedIds) +{ + network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_BIDDER_ITEMS)); + p.writeUInt64(auctioneerGuid); + p.writeUInt32(offset); + p.writeUInt32(static_cast(outbiddedIds.size())); + for (uint32_t id : outbiddedIds) + p.writeUInt32(id); + return p; +} + +bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data) { + if (packet.getSize() - packet.getReadPos() < 4) return false; + + uint32_t count = packet.readUInt32(); + data.auctions.clear(); + data.auctions.reserve(count); + + for (uint32_t i = 0; i < count; ++i) { + if (packet.getReadPos() + 64 > packet.getSize()) break; + AuctionEntry e; + e.auctionId = packet.readUInt32(); + e.itemEntry = packet.readUInt32(); + // 3 enchant slots: enchantId, duration, charges + e.enchantId = packet.readUInt32(); + packet.readUInt32(); // enchant duration + packet.readUInt32(); // enchant charges + packet.readUInt32(); // enchant2 id + packet.readUInt32(); // enchant2 duration + packet.readUInt32(); // enchant2 charges + packet.readUInt32(); // enchant3 id + packet.readUInt32(); // enchant3 duration + packet.readUInt32(); // enchant3 charges + e.randomPropertyId = packet.readUInt32(); + e.suffixFactor = packet.readUInt32(); + e.stackCount = packet.readUInt32(); + packet.readUInt32(); // item charges + packet.readUInt32(); // item flags (unused) + e.ownerGuid = packet.readUInt64(); + e.startBid = packet.readUInt32(); + e.minBidIncrement = packet.readUInt32(); + e.buyoutPrice = packet.readUInt32(); + e.timeLeftMs = packet.readUInt32(); + e.bidderGuid = packet.readUInt64(); + e.currentBid = packet.readUInt32(); + data.auctions.push_back(e); + } + + data.totalCount = packet.readUInt32(); + data.searchDelay = packet.readUInt32(); + return true; +} + +bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandResult& data) { + if (packet.getSize() - packet.getReadPos() < 12) return false; + data.auctionId = packet.readUInt32(); + data.action = packet.readUInt32(); + data.errorCode = packet.readUInt32(); + if (data.errorCode != 0 && data.action == 2 && packet.getReadPos() + 4 <= packet.getSize()) { + data.bidError = packet.readUInt32(); + } + return true; +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 7c54c04f..76db9e43 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -254,6 +254,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderTaxiWindow(gameHandler); renderMailWindow(gameHandler); renderMailComposeWindow(gameHandler); + renderBankWindow(gameHandler); + renderGuildBankWindow(gameHandler); + renderAuctionHouseWindow(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now renderMinimapMarkers(gameHandler); renderDeathScreen(gameHandler); @@ -4177,18 +4180,62 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { ImGui::Spacing(); - // Gossip options - static const char* gossipIcons[] = {"[Chat]", "[Vendor]", "[Taxi]", "[Trainer]", "[Spiritguide]", - "[Tabardvendor]", "[Battlemaster]", "[Banker]", "[Petitioner]", - "[Tabarddesigner]", "[Auctioneer]"}; + // Gossip option icons - matches WoW GossipOptionIcon enum + static const char* gossipIcons[] = { + "[Chat]", // 0 = GOSSIP_ICON_CHAT + "[Vendor]", // 1 = GOSSIP_ICON_VENDOR + "[Taxi]", // 2 = GOSSIP_ICON_TAXI + "[Trainer]", // 3 = GOSSIP_ICON_TRAINER + "[Interact]", // 4 = GOSSIP_ICON_INTERACT_1 + "[Interact]", // 5 = GOSSIP_ICON_INTERACT_2 + "[Banker]", // 6 = GOSSIP_ICON_MONEY_BAG (banker) + "[Chat]", // 7 = GOSSIP_ICON_TALK + "[Tabard]", // 8 = GOSSIP_ICON_TABARD + "[Battlemaster]", // 9 = GOSSIP_ICON_BATTLE + "[Option]", // 10 = GOSSIP_ICON_DOT + }; + + // Default text for server-sent gossip option placeholders + static const std::unordered_map gossipPlaceholders = { + {"GOSSIP_OPTION_BANKER", "I would like to check my deposit box."}, + {"GOSSIP_OPTION_AUCTIONEER", "I'd like to browse your auctions."}, + {"GOSSIP_OPTION_VENDOR", "I want to browse your goods."}, + {"GOSSIP_OPTION_TAXIVENDOR", "I'd like to fly."}, + {"GOSSIP_OPTION_TRAINER", "I seek training."}, + {"GOSSIP_OPTION_INNKEEPER", "Make this inn your home."}, + {"GOSSIP_OPTION_SPIRITGUIDE", "Return me to life."}, + {"GOSSIP_OPTION_SPIRITHEALER", "Bring me back to life."}, + {"GOSSIP_OPTION_STABLEPET", "I'd like to stable my pet."}, + {"GOSSIP_OPTION_ARMORER", "I need to repair my equipment."}, + {"GOSSIP_OPTION_GOSSIP", "What can you tell me?"}, + {"GOSSIP_OPTION_BATTLEFIELD", "I'd like to go to the battleground."}, + {"GOSSIP_OPTION_TABARDDESIGNER", "I want to create a guild tabard."}, + {"GOSSIP_OPTION_PETITIONER", "I want to create a guild."}, + }; for (const auto& opt : gossip.options) { ImGui::PushID(static_cast(opt.id)); + + // Determine icon label - use text-based detection for shared icons const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]"; - std::string processedText = replaceGenderPlaceholders(opt.text, gameHandler); - char label[256]; - snprintf(label, sizeof(label), "%s %s", icon, processedText.c_str()); - if (ImGui::Selectable(label)) { + if (opt.text == "GOSSIP_OPTION_AUCTIONEER") icon = "[Auctioneer]"; + else if (opt.text == "GOSSIP_OPTION_BANKER") icon = "[Banker]"; + else if (opt.text == "GOSSIP_OPTION_VENDOR") icon = "[Vendor]"; + else if (opt.text == "GOSSIP_OPTION_TRAINER") icon = "[Trainer]"; + else if (opt.text == "GOSSIP_OPTION_INNKEEPER") icon = "[Innkeeper]"; + else if (opt.text == "GOSSIP_OPTION_STABLEPET") icon = "[Stable Master]"; + else if (opt.text == "GOSSIP_OPTION_ARMORER") icon = "[Repair]"; + + // Resolve placeholder text from server + std::string displayText = opt.text; + auto placeholderIt = gossipPlaceholders.find(displayText); + if (placeholderIt != gossipPlaceholders.end()) { + displayText = placeholderIt->second; + } + + std::string processedText = replaceGenderPlaceholders(displayText, gameHandler); + std::string label = std::string(icon) + " " + processedText; + if (ImGui::Selectable(label.c_str())) { gameHandler.selectGossipOption(opt.id); } ImGui::PopID(); @@ -6464,4 +6511,490 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { } } +// ============================================================ +// Bank Window +// ============================================================ + +void GameScreen::renderBankWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isBankOpen()) return; + + bool open = true; + ImGui::SetNextWindowSize(ImVec2(480, 420), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Bank", &open)) { + ImGui::End(); + if (!open) gameHandler.closeBank(); + return; + } + + auto& inv = gameHandler.getInventory(); + + // Main bank slots (28 = 7 columns × 4 rows) + ImGui::Text("Bank Slots"); + ImGui::Separator(); + for (int i = 0; i < game::Inventory::BANK_SLOTS; i++) { + if (i % 7 != 0) ImGui::SameLine(); + const auto& slot = inv.getBankSlot(i); + + ImGui::PushID(i + 1000); + if (slot.empty()) { + ImGui::Button("##bank", ImVec2(42, 42)); + } else { + auto* info = gameHandler.getItemInfo(slot.item.itemId); + ImVec4 qc = InventoryScreen::getQualityColor(slot.item.quality); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); + + std::string label = std::to_string(slot.item.stackCount > 1 ? slot.item.stackCount : 0); + if (slot.item.stackCount <= 1) label = "##b" + std::to_string(i); + if (ImGui::Button(label.c_str(), ImVec2(42, 42))) { + // Right-click to withdraw: bag=0xFF means bank, slot=i + // Use CMSG_AUTOSTORE_BANK_ITEM with bank container + // WoW bank slots are inventory slots 39-66 (BANK_SLOT_1 = 39) + gameHandler.withdrawItem(0xFF, static_cast(39 + i)); + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextColored(qc, "%s", slot.item.name.c_str()); + if (slot.item.stackCount > 1) ImGui::Text("Count: %u", slot.item.stackCount); + ImGui::EndTooltip(); + } + } + ImGui::PopID(); + } + + // Bank bag slots + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("Bank Bags"); + uint8_t purchased = inv.getPurchasedBankBagSlots(); + for (int i = 0; i < game::Inventory::BANK_BAG_SLOTS; i++) { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i + 2000); + + int bagSize = inv.getBankBagSize(i); + if (i < static_cast(purchased) || bagSize > 0) { + if (ImGui::Button(bagSize > 0 ? std::to_string(bagSize).c_str() : "Empty", ImVec2(50, 30))) { + // Could open bag contents + } + } else { + if (ImGui::Button("Buy", ImVec2(50, 30))) { + gameHandler.buyBankSlot(); + } + } + ImGui::PopID(); + } + + // Show expanded bank bag contents + for (int bagIdx = 0; bagIdx < game::Inventory::BANK_BAG_SLOTS; bagIdx++) { + int bagSize = inv.getBankBagSize(bagIdx); + if (bagSize <= 0) continue; + + ImGui::Spacing(); + ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize); + for (int s = 0; s < bagSize; s++) { + if (s % 7 != 0) ImGui::SameLine(); + const auto& slot = inv.getBankBagSlot(bagIdx, s); + ImGui::PushID(3000 + bagIdx * 100 + s); + if (slot.empty()) { + ImGui::Button("##bb", ImVec2(42, 42)); + } else { + ImVec4 qc = InventoryScreen::getQualityColor(slot.item.quality); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); + std::string lbl = slot.item.stackCount > 1 ? std::to_string(slot.item.stackCount) : ("##bb" + std::to_string(bagIdx * 100 + s)); + if (ImGui::Button(lbl.c_str(), ImVec2(42, 42))) { + // Withdraw from bank bag: bank bag container indices start at 67 + gameHandler.withdrawItem(static_cast(67 + bagIdx), static_cast(s)); + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextColored(qc, "%s", slot.item.name.c_str()); + if (slot.item.stackCount > 1) ImGui::Text("Count: %u", slot.item.stackCount); + ImGui::EndTooltip(); + } + } + ImGui::PopID(); + } + } + + ImGui::End(); + + if (!open) gameHandler.closeBank(); +} + +// ============================================================ +// Guild Bank Window +// ============================================================ + +void GameScreen::renderGuildBankWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isGuildBankOpen()) return; + + bool open = true; + ImGui::SetNextWindowSize(ImVec2(520, 500), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Guild Bank", &open)) { + ImGui::End(); + if (!open) gameHandler.closeGuildBank(); + return; + } + + const auto& data = gameHandler.getGuildBankData(); + uint8_t activeTab = gameHandler.getGuildBankActiveTab(); + + // Money display + uint32_t gold = static_cast(data.money / 10000); + uint32_t silver = static_cast((data.money / 100) % 100); + uint32_t copper = static_cast(data.money % 100); + ImGui::Text("Guild Bank Money: "); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); + + // Tab bar + if (!data.tabs.empty()) { + for (size_t i = 0; i < data.tabs.size(); i++) { + if (i > 0) ImGui::SameLine(); + bool selected = (i == activeTab); + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f)); + std::string tabLabel = data.tabs[i].tabName.empty() ? ("Tab " + std::to_string(i + 1)) : data.tabs[i].tabName; + if (ImGui::Button(tabLabel.c_str())) { + gameHandler.queryGuildBankTab(static_cast(i)); + } + if (selected) ImGui::PopStyleColor(); + } + } + + // Buy tab button + if (data.tabs.size() < 6) { + ImGui::SameLine(); + if (ImGui::Button("Buy Tab")) { + gameHandler.buyGuildBankTab(); + } + } + + ImGui::Separator(); + + // Tab items (98 slots = 14 columns × 7 rows) + for (size_t i = 0; i < data.tabItems.size(); i++) { + if (i % 14 != 0) ImGui::SameLine(); + const auto& item = data.tabItems[i]; + ImGui::PushID(static_cast(i) + 5000); + + if (item.itemEntry == 0) { + ImGui::Button("##gb", ImVec2(34, 34)); + } else { + auto* info = gameHandler.getItemInfo(item.itemEntry); + game::ItemQuality quality = game::ItemQuality::COMMON; + std::string name = "Item " + std::to_string(item.itemEntry); + if (info) { + quality = static_cast(info->quality); + name = info->name; + } + ImVec4 qc = InventoryScreen::getQualityColor(quality); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); + std::string lbl = item.stackCount > 1 ? std::to_string(item.stackCount) : ("##gi" + std::to_string(i)); + if (ImGui::Button(lbl.c_str(), ImVec2(34, 34))) { + // Withdraw: auto-store to first free bag slot + gameHandler.guildBankWithdrawItem(activeTab, item.slotId, 0xFF, 0); + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextColored(qc, "%s", name.c_str()); + if (item.stackCount > 1) ImGui::Text("Count: %u", item.stackCount); + ImGui::EndTooltip(); + } + } + ImGui::PopID(); + } + + // Money deposit/withdraw + ImGui::Separator(); + ImGui::Text("Money:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(60); + ImGui::InputInt("##gbg", &guildBankMoneyInput_[0], 0); ImGui::SameLine(); ImGui::Text("g"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(40); + ImGui::InputInt("##gbs", &guildBankMoneyInput_[1], 0); ImGui::SameLine(); ImGui::Text("s"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(40); + ImGui::InputInt("##gbc", &guildBankMoneyInput_[2], 0); ImGui::SameLine(); ImGui::Text("c"); + + ImGui::SameLine(); + if (ImGui::Button("Deposit")) { + uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2]; + if (amount > 0) gameHandler.depositGuildBankMoney(amount); + } + ImGui::SameLine(); + if (ImGui::Button("Withdraw")) { + uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2]; + if (amount > 0) gameHandler.withdrawGuildBankMoney(amount); + } + + if (data.withdrawAmount >= 0) { + ImGui::Text("Remaining withdrawals: %d", data.withdrawAmount); + } + + ImGui::End(); + + if (!open) gameHandler.closeGuildBank(); +} + +// ============================================================ +// Auction House Window +// ============================================================ + +void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isAuctionHouseOpen()) return; + + bool open = true; + ImGui::SetNextWindowSize(ImVec2(650, 500), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Auction House", &open)) { + ImGui::End(); + if (!open) gameHandler.closeAuctionHouse(); + return; + } + + int tab = gameHandler.getAuctionActiveTab(); + + // Tab buttons + const char* tabNames[] = {"Browse", "Bids", "Auctions"}; + for (int i = 0; i < 3; i++) { + if (i > 0) ImGui::SameLine(); + bool selected = (tab == i); + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f)); + if (ImGui::Button(tabNames[i], ImVec2(100, 0))) { + gameHandler.setAuctionActiveTab(i); + if (i == 1) gameHandler.auctionListBidderItems(); + else if (i == 2) gameHandler.auctionListOwnerItems(); + } + if (selected) ImGui::PopStyleColor(); + } + + ImGui::Separator(); + + if (tab == 0) { + // Browse tab - Search filters + ImGui::SetNextItemWidth(200); + ImGui::InputText("Name", auctionSearchName_, sizeof(auctionSearchName_)); + ImGui::SameLine(); + ImGui::SetNextItemWidth(50); + ImGui::InputInt("Min Lv", &auctionLevelMin_, 0); + ImGui::SameLine(); + ImGui::SetNextItemWidth(50); + ImGui::InputInt("Max Lv", &auctionLevelMax_, 0); + + const char* qualities[] = {"All", "Poor", "Common", "Uncommon", "Rare", "Epic", "Legendary"}; + ImGui::SetNextItemWidth(100); + ImGui::Combo("Quality", &auctionQuality_, qualities, 7); + + ImGui::SameLine(); + float delay = gameHandler.getAuctionSearchDelay(); + if (delay > 0.0f) { + ImGui::BeginDisabled(); + ImGui::Button("Search..."); + ImGui::EndDisabled(); + } else { + if (ImGui::Button("Search")) { + uint32_t q = auctionQuality_ > 0 ? static_cast(auctionQuality_ - 1) : 0xFFFFFFFF; + gameHandler.auctionSearch(auctionSearchName_, + static_cast(auctionLevelMin_), + static_cast(auctionLevelMax_), + q, 0xFFFFFFFF, 0xFFFFFFFF, 0, 0); + } + } + + ImGui::Separator(); + + // Results table + const auto& results = gameHandler.getAuctionBrowseResults(); + ImGui::Text("%zu results (of %u total)", results.auctions.size(), results.totalCount); + + if (ImGui::BeginChild("AuctionResults", ImVec2(0, -80), true)) { + if (ImGui::BeginTable("AuctionTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { + ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40); + ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < results.auctions.size(); i++) { + const auto& auction = results.auctions[i]; + auto* info = gameHandler.getItemInfo(auction.itemEntry); + std::string name = info ? info->name : ("Item #" + std::to_string(auction.itemEntry)); + game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; + ImVec4 qc = InventoryScreen::getQualityColor(quality); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(qc, "%s", name.c_str()); + + ImGui::TableSetColumnIndex(1); + ImGui::Text("%u", auction.stackCount); + + ImGui::TableSetColumnIndex(2); + // Time left display + uint32_t mins = auction.timeLeftMs / 60000; + if (mins > 720) ImGui::Text("Long"); + else if (mins > 120) ImGui::Text("Medium"); + else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short"); + + ImGui::TableSetColumnIndex(3); + { + uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid; + ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); + } + + ImGui::TableSetColumnIndex(4); + if (auction.buyoutPrice > 0) { + ImGui::Text("%ug%us%uc", auction.buyoutPrice / 10000, + (auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100); + } else { + ImGui::TextDisabled("--"); + } + + ImGui::TableSetColumnIndex(5); + ImGui::PushID(static_cast(i) + 7000); + if (auction.buyoutPrice > 0 && ImGui::SmallButton("Buy")) { + gameHandler.auctionBuyout(auction.auctionId, auction.buyoutPrice); + } + if (auction.buyoutPrice > 0) ImGui::SameLine(); + if (ImGui::SmallButton("Bid")) { + uint32_t bidAmt = auction.currentBid > 0 + ? auction.currentBid + auction.minBidIncrement + : auction.startBid; + gameHandler.auctionPlaceBid(auction.auctionId, bidAmt); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + ImGui::EndChild(); + + // Sell section + ImGui::Separator(); + ImGui::Text("Sell:"); + ImGui::SameLine(); + ImGui::Text("Bid:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(50); + ImGui::InputInt("##sbg", &auctionSellBid_[0], 0); ImGui::SameLine(); ImGui::Text("g"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(35); + ImGui::InputInt("##sbs", &auctionSellBid_[1], 0); ImGui::SameLine(); ImGui::Text("s"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(35); + ImGui::InputInt("##sbc", &auctionSellBid_[2], 0); ImGui::SameLine(); ImGui::Text("c"); + + ImGui::Text(" "); ImGui::SameLine(); + ImGui::Text("Buyout:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(50); + ImGui::InputInt("##sbog", &auctionSellBuyout_[0], 0); ImGui::SameLine(); ImGui::Text("g"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(35); + ImGui::InputInt("##sbos", &auctionSellBuyout_[1], 0); ImGui::SameLine(); ImGui::Text("s"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(35); + ImGui::InputInt("##sboc", &auctionSellBuyout_[2], 0); ImGui::SameLine(); ImGui::Text("c"); + + const char* durations[] = {"12 hours", "24 hours", "48 hours"}; + ImGui::SameLine(); + ImGui::SetNextItemWidth(90); + ImGui::Combo("##dur", &auctionSellDuration_, durations, 3); + + } else if (tab == 1) { + // Bids tab + const auto& results = gameHandler.getAuctionBidderResults(); + ImGui::Text("Your Bids: %zu items", results.auctions.size()); + + if (ImGui::BeginTable("BidTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40); + ImGui::TableSetupColumn("Your Bid", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableHeadersRow(); + + for (const auto& a : results.auctions) { + auto* info = gameHandler.getItemInfo(a.itemEntry); + std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry)); + game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(InventoryScreen::getQualityColor(quality), "%s", name.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%u", a.stackCount); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%ug%us%uc", a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100); + ImGui::TableSetColumnIndex(3); + if (a.buyoutPrice > 0) + ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + else + ImGui::TextDisabled("--"); + ImGui::TableSetColumnIndex(4); + uint32_t mins = a.timeLeftMs / 60000; + if (mins > 720) ImGui::Text("Long"); + else if (mins > 120) ImGui::Text("Medium"); + else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short"); + } + ImGui::EndTable(); + } + + } else if (tab == 2) { + // Auctions tab (your listings) + const auto& results = gameHandler.getAuctionOwnerResults(); + ImGui::Text("Your Auctions: %zu items", results.auctions.size()); + + if (ImGui::BeginTable("OwnerTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40); + ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("##cancel", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < results.auctions.size(); i++) { + const auto& a = results.auctions[i]; + auto* info = gameHandler.getItemInfo(a.itemEntry); + std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry)); + game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(InventoryScreen::getQualityColor(quality), "%s", name.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%u", a.stackCount); + ImGui::TableSetColumnIndex(2); + { + uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid; + ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); + } + ImGui::TableSetColumnIndex(3); + if (a.buyoutPrice > 0) + ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + else + ImGui::TextDisabled("--"); + ImGui::TableSetColumnIndex(4); + ImGui::PushID(static_cast(i) + 8000); + if (ImGui::SmallButton("Cancel")) { + gameHandler.auctionCancelItem(a.auctionId); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + + ImGui::End(); + + if (!open) gameHandler.closeAuctionHouse(); +} + }} // namespace wowee::ui