mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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)
This commit is contained in:
parent
1fdf450c5e
commit
3c754801c4
3 changed files with 136 additions and 9 deletions
|
|
@ -1542,6 +1542,7 @@ private:
|
|||
std::deque<BuybackItem> buybackItems_;
|
||||
std::unordered_map<uint64_t, BuybackItem> pendingSellToBuyback_;
|
||||
int pendingBuybackSlot_ = -1;
|
||||
uint32_t pendingBuybackWireSlot_ = 0;
|
||||
uint32_t pendingBuyItemId_ = 0;
|
||||
uint32_t pendingBuyItemSlot_ = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<int>(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<int>(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<int>(buybackSlot);
|
||||
auto packet = BuybackItemPacket::build(currentVendorItems.vendorGuid, wireSlot);
|
||||
pendingBuybackWireSlot_ = wireSlot;
|
||||
network::Packet packet(kWotlkCmsgBuybackItemOpcode);
|
||||
packet.writeUInt64(currentVendorItems.vendorGuid);
|
||||
packet.writeUInt32(wireSlot);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<uint64_t>(sellPrice) *
|
||||
static_cast<uint64_t>(entry.count > 0 ? entry.count : 1);
|
||||
uint32_t g = static_cast<uint32_t>(price / 10000);
|
||||
uint32_t s = static_cast<uint32_t>((price / 100) % 100);
|
||||
uint32_t c = static_cast<uint32_t>(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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue