From 3c754801c4a44c0f67fe6c11eacb7f8b7e87b1ce Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 19 Feb 2026 05:48:40 -0800 Subject: [PATCH] Stabilize buyback flow and single-row buyback UI - keep vendor buyback as one-item LIFO row in vendor window - add robust pending buyback tracking with wire slot probing (74..85) - recover from stale optimistic sell/buyback entries and refresh vendor list - keep buy packet construction branch-compatible (direct packet build) --- include/game/game_handler.hpp | 1 + src/game/game_handler.cpp | 74 +++++++++++++++++++++++++++++++++-- src/ui/game_screen.cpp | 70 ++++++++++++++++++++++++++++++--- 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 0da7fdcc..5df20457 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1542,6 +1542,7 @@ private: std::deque buybackItems_; std::unordered_map pendingSellToBuyback_; int pendingBuybackSlot_ = -1; + uint32_t pendingBuybackWireSlot_ = 0; uint32_t pendingBuyItemId_ = 0; uint32_t pendingBuyItemSlot_ = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ad00b774..db92318c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -977,6 +977,7 @@ void GameHandler::handlePacket(network::Packet& packet) { addSystemChatMessage(std::string(msg) + " (code " + std::to_string(resultCode) + ")"); } pendingBuybackSlot_ = -1; + pendingBuybackWireSlot_ = 0; // Refresh vendor list so UI state stays in sync after buyback result. if (currentVendorItems.vendorGuid != 0 && socket && state == WorldState::IN_WORLD) { @@ -1722,6 +1723,7 @@ void GameHandler::handlePacket(network::Packet& packet) { if (result == 0) { pendingSellToBuyback_.erase(itemGuid); } else { + bool removedPending = false; auto it = pendingSellToBuyback_.find(itemGuid); if (it != pendingSellToBuyback_.end()) { for (auto bit = buybackItems_.begin(); bit != buybackItems_.end(); ++bit) { @@ -1731,6 +1733,23 @@ void GameHandler::handlePacket(network::Packet& packet) { } } pendingSellToBuyback_.erase(it); + removedPending = true; + } + if (!removedPending) { + // Some cores return a non-item GUID on sell failure; drop the newest + // optimistic entry if it is still pending so stale rows don't block buyback. + if (!buybackItems_.empty()) { + uint64_t frontGuid = buybackItems_.front().itemGuid; + if (pendingSellToBuyback_.erase(frontGuid) > 0) { + buybackItems_.pop_front(); + removedPending = true; + } + } + } + if (!removedPending && !pendingSellToBuyback_.empty()) { + // Last-resort desync recovery. + pendingSellToBuyback_.clear(); + buybackItems_.clear(); } static const char* sellErrors[] = { "OK", "Can't find item", "Can't sell item", @@ -1827,8 +1846,42 @@ void GameHandler::handlePacket(network::Packet& packet) { " item/slot=", itemIdOrSlot, " err=", static_cast(errCode), " pendingBuybackSlot=", pendingBuybackSlot_, + " pendingBuybackWireSlot=", pendingBuybackWireSlot_, " pendingBuyItemId=", pendingBuyItemId_, " pendingBuyItemSlot=", pendingBuyItemSlot_); + if (pendingBuybackSlot_ >= 0) { + // Some cores require probing absolute buyback slots until a live entry is found. + if (errCode == 0) { + constexpr uint16_t kWotlkCmsgBuybackItemOpcode = 0x290; + constexpr uint32_t kBuybackSlotEnd = 85; + if (pendingBuybackWireSlot_ >= 74 && pendingBuybackWireSlot_ < kBuybackSlotEnd && + socket && state == WorldState::IN_WORLD && currentVendorItems.vendorGuid != 0) { + ++pendingBuybackWireSlot_; + LOG_INFO("Buyback retry: vendorGuid=0x", std::hex, currentVendorItems.vendorGuid, + std::dec, " uiSlot=", pendingBuybackSlot_, + " wireSlot=", pendingBuybackWireSlot_); + network::Packet retry(kWotlkCmsgBuybackItemOpcode); + retry.writeUInt64(currentVendorItems.vendorGuid); + retry.writeUInt32(pendingBuybackWireSlot_); + socket->send(retry); + break; + } + // Exhausted slot probe: drop stale local row and advance. + if (pendingBuybackSlot_ < static_cast(buybackItems_.size())) { + buybackItems_.erase(buybackItems_.begin() + pendingBuybackSlot_); + } + pendingBuybackSlot_ = -1; + pendingBuybackWireSlot_ = 0; + if (currentVendorItems.vendorGuid != 0 && socket && state == WorldState::IN_WORLD) { + auto pkt = ListInventoryPacket::build(currentVendorItems.vendorGuid); + socket->send(pkt); + } + break; + } + pendingBuybackSlot_ = -1; + pendingBuybackWireSlot_ = 0; + } + const char* msg = "Purchase failed."; switch (errCode) { case 0: msg = "Purchase failed: item not found."; break; @@ -9078,6 +9131,7 @@ void GameHandler::closeVendor() { buybackItems_.clear(); pendingSellToBuyback_.clear(); pendingBuybackSlot_ = -1; + pendingBuybackWireSlot_ = 0; pendingBuyItemId_ = 0; pendingBuyItemSlot_ = 0; } @@ -9089,7 +9143,14 @@ void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, u " wire=0x", std::hex, wireOpcode(Opcode::CMSG_BUY_ITEM), std::dec); pendingBuyItemId_ = itemId; pendingBuyItemSlot_ = slot; - auto packet = BuyItemPacket::build(vendorGuid, itemId, slot, count); + // Build directly to avoid helper-signature drift across branches (3-arg vs 4-arg helper). + network::Packet packet(wireOpcode(Opcode::CMSG_BUY_ITEM)); + packet.writeUInt64(vendorGuid); + packet.writeUInt32(itemId); // item entry + packet.writeUInt32(slot); // vendor slot index + packet.writeUInt32(count); + // WotLK/AzerothCore expects a trailing byte here. + packet.writeUInt8(0); socket->send(packet); } @@ -9102,13 +9163,18 @@ void GameHandler::buyBackItem(uint32_t buybackSlot) { // This request is independent from normal buy path; avoid stale pending buy context in logs. pendingBuyItemId_ = 0; pendingBuyItemSlot_ = 0; + // Build directly so this compiles even when Opcode::CMSG_BUYBACK_ITEM / BuybackItemPacket + // are not available in some branches. + constexpr uint16_t kWotlkCmsgBuybackItemOpcode = 0x290; LOG_INFO("Buyback request: vendorGuid=0x", std::hex, currentVendorItems.vendorGuid, std::dec, " uiSlot=", buybackSlot, " wireSlot=", wireSlot, " source=absolute-buyback-slot", - " wire=0x", std::hex, - wireOpcode(Opcode::CMSG_BUYBACK_ITEM), std::dec); + " wire=0x", std::hex, kWotlkCmsgBuybackItemOpcode, std::dec); pendingBuybackSlot_ = static_cast(buybackSlot); - auto packet = BuybackItemPacket::build(currentVendorItems.vendorGuid, wireSlot); + pendingBuybackWireSlot_ = wireSlot; + network::Packet packet(kWotlkCmsgBuybackItemOpcode); + packet.writeUInt64(currentVendorItems.vendorGuid); + packet.writeUInt32(wireSlot); socket->send(packet); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 205220a3..da6caa0a 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -361,8 +361,18 @@ void GameScreen::render(game::GameHandler& gameHandler) { inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler); // Auto-open bags when vendor window opens - if (gameHandler.isVendorWindowOpen() && !inventoryScreen.isOpen()) { - inventoryScreen.setOpen(true); + if (gameHandler.isVendorWindowOpen()) { + if (inventoryScreen.isSeparateBags()) { + if (!inventoryScreen.isBackpackOpen() && + !inventoryScreen.isBagOpen(0) && + !inventoryScreen.isBagOpen(1) && + !inventoryScreen.isBagOpen(2) && + !inventoryScreen.isBagOpen(3)) { + inventoryScreen.openAllBags(); + } + } else if (!inventoryScreen.isOpen()) { + inventoryScreen.setOpen(true); + } } // Bags (B key toggle handled inside) @@ -800,7 +810,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { if (!bonusLine.empty()) { ImGui::TextColored(green, "%s", bonusLine.c_str()); } - if (!isWeapon && info->armor > 0) { + if (info->armor > 0) { ImGui::Text("%d Armor", info->armor); } if (info->sellPrice > 0) { @@ -826,7 +836,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed; ImGui::Text("%.1f DPS", dps); } - if (!isWeaponInventoryType(eq->item.inventoryType) && eq->item.armor > 0) { + if (eq->item.armor > 0) { ImGui::Text("%d Armor", eq->item.armor); } std::string eqBonusLine; @@ -4944,6 +4954,55 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell"); ImGui::Separator(); + const auto& buyback = gameHandler.getBuybackItems(); + if (!buyback.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Buy Back"); + if (ImGui::BeginTable("BuybackTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 110.0f); + ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 62.0f); + ImGui::TableHeadersRow(); + // Show only the most recently sold item (LIFO). + const int i = 0; + const auto& entry = buyback[0]; + uint32_t sellPrice = entry.item.sellPrice; + if (sellPrice == 0) { + if (auto* info = gameHandler.getItemInfo(entry.item.itemId); info && info->valid) { + sellPrice = info->sellPrice; + } + } + uint64_t price = static_cast(sellPrice) * + static_cast(entry.count > 0 ? entry.count : 1); + uint32_t g = static_cast(price / 10000); + uint32_t s = static_cast((price / 100) % 100); + uint32_t c = static_cast(price % 100); + bool canAfford = money >= price; + + ImGui::TableNextRow(); + ImGui::PushID(8000 + i); + ImGui::TableSetColumnIndex(0); + const char* name = entry.item.name.empty() ? "Unknown Item" : entry.item.name.c_str(); + if (entry.count > 1) { + ImGui::Text("%s x%u", name, entry.count); + } else { + ImGui::Text("%s", name); + } + ImGui::TableSetColumnIndex(1); + if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::Text("%ug %us %uc", g, s, c); + if (!canAfford) ImGui::PopStyleColor(); + ImGui::TableSetColumnIndex(2); + if (!canAfford) ImGui::BeginDisabled(); + if (ImGui::SmallButton("Buy Back##buyback_0")) { + gameHandler.buyBackItem(0); + } + if (!canAfford) ImGui::EndDisabled(); + ImGui::PopID(); + ImGui::EndTable(); + } + ImGui::Separator(); + } + if (vendor.items.empty()) { ImGui::TextDisabled("This vendor has nothing for sale."); } else { @@ -5021,7 +5080,8 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { } ImGui::TableSetColumnIndex(3); - if (ImGui::SmallButton("Buy")) { + std::string buyBtnId = "Buy##vendor_" + std::to_string(vi); + if (ImGui::SmallButton(buyBtnId.c_str())) { gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, 1); }