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.
This commit is contained in:
Kelsi 2026-02-16 21:11:18 -08:00
parent 0d4a9c38f7
commit 381d896348
14 changed files with 1839 additions and 15 deletions

View file

@ -249,5 +249,26 @@
"CMSG_MAIL_DELETE": "0x249", "CMSG_MAIL_DELETE": "0x249",
"CMSG_MAIL_MARK_AS_READ": "0x247", "CMSG_MAIL_MARK_AS_READ": "0x247",
"SMSG_RECEIVED_MAIL": "0x285", "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"
} }

View file

@ -785,6 +785,51 @@ public:
void mailMarkAsRead(uint32_t mailId); void mailMarkAsRead(uint32_t mailId);
void refreshMailList(); 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 // Trainer
bool isTrainerWindowOpen() const { return trainerWindowOpen_; } bool isTrainerWindowOpen() const { return trainerWindowOpen_; }
const TrainerListData& getTrainerSpells() const { return currentTrainerList_; } const TrainerListData& getTrainerSpells() const { return currentTrainerList_; }
@ -1010,6 +1055,20 @@ private:
void handleArenaTeamEvent(network::Packet& packet); void handleArenaTeamEvent(network::Packet& packet);
void handleArenaError(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 ---- // ---- Mail handlers ----
void handleShowMailbox(network::Packet& packet); void handleShowMailbox(network::Packet& packet);
void handleMailListResult(network::Packet& packet); void handleMailListResult(network::Packet& packet);
@ -1360,6 +1419,31 @@ private:
bool showMailCompose_ = false; bool showMailCompose_ = false;
bool hasNewMail_ = false; bool hasNewMail_ = false;
// Bank
bool bankOpen_ = false;
uint64_t bankerGuid_ = 0;
std::array<uint64_t, 28> bankSlotGuids_{};
std::array<uint64_t, 7> 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 // Vendor
bool vendorWindowOpen = false; bool vendorWindowOpen = false;
ListInventoryData currentVendorItems; ListInventoryData currentVendorItems;

View file

@ -56,6 +56,8 @@ public:
static constexpr int NUM_EQUIP_SLOTS = 23; static constexpr int NUM_EQUIP_SLOTS = 23;
static constexpr int NUM_BAG_SLOTS = 4; static constexpr int NUM_BAG_SLOTS = 4;
static constexpr int MAX_BAG_SIZE = 36; static constexpr int MAX_BAG_SIZE = 36;
static constexpr int BANK_SLOTS = 28;
static constexpr int BANK_BAG_SLOTS = 7;
Inventory(); Inventory();
@ -76,6 +78,19 @@ public:
const ItemSlot& getBagSlot(int bagIndex, int slotIndex) const; const ItemSlot& getBagSlot(int bagIndex, int slotIndex) const;
bool setBagSlot(int bagIndex, int slotIndex, const ItemDef& item); 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 // Utility
int findFreeBackpackSlot() const; int findFreeBackpackSlot() const;
bool addItem(const ItemDef& item); bool addItem(const ItemDef& item);
@ -92,6 +107,11 @@ private:
std::array<ItemSlot, MAX_BAG_SIZE> slots{}; std::array<ItemSlot, MAX_BAG_SIZE> slots{};
}; };
std::array<BagData, NUM_BAG_SLOTS> bags{}; std::array<BagData, NUM_BAG_SLOTS> bags{};
// Bank
std::array<ItemSlot, BANK_SLOTS> bankSlots_{};
std::array<BagData, BANK_BAG_SLOTS> bankBags_{};
uint8_t purchasedBankBagSlots_ = 0;
}; };
const char* getQualityName(ItemQuality quality); const char* getQualityName(ItemQuality quality);

View file

@ -382,6 +382,39 @@ enum class LogicalOpcode : uint16_t {
SMSG_RECEIVED_MAIL, SMSG_RECEIVED_MAIL,
MSG_QUERY_NEXT_MAIL_TIME, 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 // Sentinel
COUNT COUNT
}; };

View file

@ -44,6 +44,8 @@ enum class UF : uint16_t {
PLAYER_QUEST_LOG_START, PLAYER_QUEST_LOG_START,
PLAYER_FIELD_INV_SLOT_HEAD, PLAYER_FIELD_INV_SLOT_HEAD,
PLAYER_FIELD_PACK_SLOT_1, PLAYER_FIELD_PACK_SLOT_1,
PLAYER_FIELD_BANK_SLOT_1,
PLAYER_FIELD_BANKBAG_SLOT_1,
PLAYER_SKILL_INFO_START, PLAYER_SKILL_INFO_START,
PLAYER_EXPLORED_ZONES_START, PLAYER_EXPLORED_ZONES_START,

View file

@ -2262,5 +2262,213 @@ public:
static network::Packet build(uint64_t mailboxGuid, uint32_t mailId); 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<GuildBankItemSlot> items;
};
struct GuildBankData {
uint64_t money = 0;
uint8_t tabId = 0;
int32_t withdrawAmount = -1; // -1 = unlimited
std::vector<GuildBankTab> tabs; // Only populated on fullUpdate
std::vector<GuildBankItemSlot> 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<AuctionEntry> 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<uint32_t>& 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 game
} // namespace wowee } // namespace wowee

View file

@ -187,6 +187,9 @@ private:
void renderChatBubbles(game::GameHandler& gameHandler); void renderChatBubbles(game::GameHandler& gameHandler);
void renderMailWindow(game::GameHandler& gameHandler); void renderMailWindow(game::GameHandler& gameHandler);
void renderMailComposeWindow(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 * Inventory screen
@ -254,6 +257,19 @@ private:
char mailBodyBuffer_[2048] = ""; char mailBodyBuffer_[2048] = "";
int mailComposeMoney_[3] = {0, 0, 0}; // gold, silver, copper 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 // Left-click targeting: distinguish click from camera drag
glm::vec2 leftClickPressPos_ = glm::vec2(0.0f); glm::vec2 leftClickPressPos_ = glm::vec2(0.0f);
bool leftClickWasPress_ = false; bool leftClickWasPress_ = false;

View file

@ -296,6 +296,11 @@ void GameHandler::update(float deltaTime) {
clearTarget(); clearTarget();
} }
if (auctionSearchDelayTimer_ > 0.0f) {
auctionSearchDelayTimer_ -= deltaTime;
if (auctionSearchDelayTimer_ < 0.0f) auctionSearchDelayTimer_ = 0.0f;
}
if (pendingMoneyDeltaTimer_ > 0.0f) { if (pendingMoneyDeltaTimer_ > 0.0f) {
pendingMoneyDeltaTimer_ -= deltaTime; pendingMoneyDeltaTimer_ -= deltaTime;
if (pendingMoneyDeltaTimer_ <= 0.0f) { if (pendingMoneyDeltaTimer_ <= 0.0f) {
@ -1526,6 +1531,36 @@ void GameHandler::handlePacket(network::Packet& packet) {
handleQueryNextMailTime(packet); handleQueryNextMailTime(packet);
break; 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: default:
// In pre-world states we need full visibility (char create/login handshakes). // 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. // 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<uint16_t, uint32_t>& field
slotsChanged = true; slotsChanged = true;
} }
} }
// Bank slots: 28 slots × 2 fields = 56 fields starting at PLAYER_FIELD_BANK_SLOT_1
int bankBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1));
if (bankBase != 0xFFFF && key >= static_cast<uint16_t>(bankBase) &&
key <= static_cast<uint16_t>(bankBase) + (game::Inventory::BANK_SLOTS * 2 - 1)) {
int slotIndex = (key - bankBase) / 2;
bool isLow = ((key - bankBase) % 2 == 0);
if (slotIndex < static_cast<int>(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<int>(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1));
if (bankBagBase != 0xFFFF && key >= static_cast<uint16_t>(bankBagBase) &&
key <= static_cast<uint16_t>(bankBagBase) + (game::Inventory::BANK_BAG_SLOTS * 2 - 1)) {
int slotIndex = (key - bankBagBase) / 2;
bool isLow = ((key - bankBagBase) % 2 == 0);
if (slotIndex < static_cast<int>(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; 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<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.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<int>(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<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.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 // Only mark equipment dirty if equipped item displayInfoIds actually changed
std::array<uint32_t, 19> currentEquipDisplayIds{}; std::array<uint32_t, 19> currentEquipDisplayIds{};
for (int i = 0; i < 19; i++) { for (int i = 0; i < 19; i++) {
@ -7450,17 +7612,46 @@ void GameHandler::interactWithGameObject(uint64_t guid) {
void GameHandler::selectGossipOption(uint32_t optionId) { void GameHandler::selectGossipOption(uint32_t optionId) {
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; 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); auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, currentGossip.menuId, optionId);
socket->send(packet); socket->send(packet);
// If this is an innkeeper "make this inn your home" option, send binder activate.
for (const auto& opt : currentGossip.options) { for (const auto& opt : currentGossip.options) {
if (opt.id != optionId) continue; 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::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<char>(std::tolower(c)); }); [](unsigned char c){ return static_cast<char>(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); auto bindPkt = BinderActivatePacket::build(currentGossip.npcGuid);
socket->send(bindPkt); socket->send(bindPkt);
LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); 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); 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<uint8_t>(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<size_t>(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 game
} // namespace wowee } // namespace wowee

View file

@ -68,6 +68,46 @@ bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) {
return true; 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 { int Inventory::findFreeBackpackSlot() const {
for (int i = 0; i < BACKPACK_SLOTS; i++) { for (int i = 0; i < BACKPACK_SLOTS; i++) {
if (backpack[i].empty()) return i; if (backpack[i].empty()) return i;

View file

@ -307,6 +307,36 @@ static const OpcodeNameEntry kOpcodeNames[] = {
{"CMSG_MAIL_MARK_AS_READ", LogicalOpcode::CMSG_MAIL_MARK_AS_READ}, {"CMSG_MAIL_MARK_AS_READ", LogicalOpcode::CMSG_MAIL_MARK_AS_READ},
{"SMSG_RECEIVED_MAIL", LogicalOpcode::SMSG_RECEIVED_MAIL}, {"SMSG_RECEIVED_MAIL", LogicalOpcode::SMSG_RECEIVED_MAIL},
{"MSG_QUERY_NEXT_MAIL_TIME", LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME}, {"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 // clang-format on
@ -615,6 +645,36 @@ void OpcodeTable::loadWotlkDefaults() {
{LogicalOpcode::CMSG_MAIL_MARK_AS_READ, 0x247}, {LogicalOpcode::CMSG_MAIL_MARK_AS_READ, 0x247},
{LogicalOpcode::SMSG_RECEIVED_MAIL, 0x285}, {LogicalOpcode::SMSG_RECEIVED_MAIL, 0x285},
{LogicalOpcode::MSG_QUERY_NEXT_MAIL_TIME, 0x284}, {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(); logicalToWire_.clear();

View file

@ -673,9 +673,10 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes
opt.id = packet.readUInt32(); opt.id = packet.readUInt32();
opt.icon = packet.readUInt8(); opt.icon = packet.readUInt8();
opt.isCoded = (packet.readUInt8() != 0); 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.text = packet.readString();
opt.boxText = packet.readString(); opt.boxText = "";
data.options.push_back(opt); data.options.push_back(opt);
} }

View file

@ -45,6 +45,8 @@ static const UFNameEntry kUFNames[] = {
{"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START}, {"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START},
{"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD}, {"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD},
{"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1}, {"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_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
{"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START}, {"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START},
{"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID}, {"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID},
@ -84,6 +86,8 @@ void UpdateFieldTable::loadWotlkDefaults() {
{UF::PLAYER_QUEST_LOG_START, 158}, {UF::PLAYER_QUEST_LOG_START, 158},
{UF::PLAYER_FIELD_INV_SLOT_HEAD, 324}, {UF::PLAYER_FIELD_INV_SLOT_HEAD, 324},
{UF::PLAYER_FIELD_PACK_SLOT_1, 370}, {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_SKILL_INFO_START, 636},
{UF::PLAYER_EXPLORED_ZONES_START, 1041}, {UF::PLAYER_EXPLORED_ZONES_START, 1041},
{UF::GAMEOBJECT_DISPLAYID, 8}, {UF::GAMEOBJECT_DISPLAYID, 8},

View file

@ -3480,5 +3480,316 @@ bool PacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessa
return true; return true;
} }
// ============================================================
// Bank System
// ============================================================
network::Packet BankerActivatePacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::CMSG_BANKER_ACTIVATE));
p.writeUInt64(guid);
return p;
}
network::Packet BuyBankSlotPacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::CMSG_BUY_BANK_SLOT));
p.writeUInt64(guid);
return p;
}
network::Packet AutoBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet p(wireOpcode(Opcode::CMSG_AUTOBANK_ITEM));
p.writeUInt8(srcBag);
p.writeUInt8(srcSlot);
return p;
}
network::Packet AutoStoreBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet p(wireOpcode(Opcode::CMSG_AUTOSTORE_BANK_ITEM));
p.writeUInt8(srcBag);
p.writeUInt8(srcSlot);
return p;
}
// ============================================================
// Guild Bank System
// ============================================================
network::Packet GuildBankerActivatePacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANKER_ACTIVATE));
p.writeUInt64(guid);
p.writeUInt8(0); // full slots update
return p;
}
network::Packet GuildBankQueryTabPacket::build(uint64_t guid, uint8_t tabId, bool fullUpdate) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_QUERY_TAB));
p.writeUInt64(guid);
p.writeUInt8(tabId);
p.writeUInt8(fullUpdate ? 1 : 0);
return p;
}
network::Packet GuildBankBuyTabPacket::build(uint64_t guid, uint8_t tabId) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_BUY_TAB));
p.writeUInt64(guid);
p.writeUInt8(tabId);
return p;
}
network::Packet GuildBankDepositMoneyPacket::build(uint64_t guid, uint32_t amount) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_DEPOSIT_MONEY));
p.writeUInt64(guid);
p.writeUInt32(amount);
return p;
}
network::Packet GuildBankWithdrawMoneyPacket::build(uint64_t guid, uint32_t amount) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_WITHDRAW_MONEY));
p.writeUInt64(guid);
p.writeUInt32(amount);
return p;
}
network::Packet GuildBankSwapItemsPacket::buildBankToInventory(
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
uint8_t destBag, uint8_t destSlot, uint32_t splitCount)
{
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
p.writeUInt64(guid);
p.writeUInt8(0); // bankToCharacter = false -> 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<int32_t>(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<uint32_t>& outbiddedIds)
{
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_BIDDER_ITEMS));
p.writeUInt64(auctioneerGuid);
p.writeUInt32(offset);
p.writeUInt32(static_cast<uint32_t>(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 game
} // namespace wowee } // namespace wowee

View file

@ -254,6 +254,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderTaxiWindow(gameHandler); renderTaxiWindow(gameHandler);
renderMailWindow(gameHandler); renderMailWindow(gameHandler);
renderMailComposeWindow(gameHandler); renderMailComposeWindow(gameHandler);
renderBankWindow(gameHandler);
renderGuildBankWindow(gameHandler);
renderAuctionHouseWindow(gameHandler);
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
renderMinimapMarkers(gameHandler); renderMinimapMarkers(gameHandler);
renderDeathScreen(gameHandler); renderDeathScreen(gameHandler);
@ -4177,18 +4180,62 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
ImGui::Spacing(); ImGui::Spacing();
// Gossip options // Gossip option icons - matches WoW GossipOptionIcon enum
static const char* gossipIcons[] = {"[Chat]", "[Vendor]", "[Taxi]", "[Trainer]", "[Spiritguide]", static const char* gossipIcons[] = {
"[Tabardvendor]", "[Battlemaster]", "[Banker]", "[Petitioner]", "[Chat]", // 0 = GOSSIP_ICON_CHAT
"[Tabarddesigner]", "[Auctioneer]"}; "[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<std::string, std::string> 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) { for (const auto& opt : gossip.options) {
ImGui::PushID(static_cast<int>(opt.id)); ImGui::PushID(static_cast<int>(opt.id));
// Determine icon label - use text-based detection for shared icons
const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]"; const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
std::string processedText = replaceGenderPlaceholders(opt.text, gameHandler); if (opt.text == "GOSSIP_OPTION_AUCTIONEER") icon = "[Auctioneer]";
char label[256]; else if (opt.text == "GOSSIP_OPTION_BANKER") icon = "[Banker]";
snprintf(label, sizeof(label), "%s %s", icon, processedText.c_str()); else if (opt.text == "GOSSIP_OPTION_VENDOR") icon = "[Vendor]";
if (ImGui::Selectable(label)) { 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); gameHandler.selectGossipOption(opt.id);
} }
ImGui::PopID(); 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<uint8_t>(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<int>(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<uint8_t>(67 + bagIdx), static_cast<uint8_t>(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<uint32_t>(data.money / 10000);
uint32_t silver = static_cast<uint32_t>((data.money / 100) % 100);
uint32_t copper = static_cast<uint32_t>(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<uint8_t>(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<int>(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<game::ItemQuality>(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<uint32_t>(auctionQuality_ - 1) : 0xFFFFFFFF;
gameHandler.auctionSearch(auctionSearchName_,
static_cast<uint8_t>(auctionLevelMin_),
static_cast<uint8_t>(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<game::ItemQuality>(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<int>(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<game::ItemQuality>(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<game::ItemQuality>(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<int>(i) + 8000);
if (ImGui::SmallButton("Cancel")) {
gameHandler.auctionCancelItem(a.auctionId);
}
ImGui::PopID();
}
ImGui::EndTable();
}
}
ImGui::End();
if (!open) gameHandler.closeAuctionHouse();
}
}} // namespace wowee::ui }} // namespace wowee::ui