mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +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::deque<BuybackItem> buybackItems_;
|
||||||
std::unordered_map<uint64_t, BuybackItem> pendingSellToBuyback_;
|
std::unordered_map<uint64_t, BuybackItem> pendingSellToBuyback_;
|
||||||
int pendingBuybackSlot_ = -1;
|
int pendingBuybackSlot_ = -1;
|
||||||
|
uint32_t pendingBuybackWireSlot_ = 0;
|
||||||
uint32_t pendingBuyItemId_ = 0;
|
uint32_t pendingBuyItemId_ = 0;
|
||||||
uint32_t pendingBuyItemSlot_ = 0;
|
uint32_t pendingBuyItemSlot_ = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -977,6 +977,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
addSystemChatMessage(std::string(msg) + " (code " + std::to_string(resultCode) + ")");
|
addSystemChatMessage(std::string(msg) + " (code " + std::to_string(resultCode) + ")");
|
||||||
}
|
}
|
||||||
pendingBuybackSlot_ = -1;
|
pendingBuybackSlot_ = -1;
|
||||||
|
pendingBuybackWireSlot_ = 0;
|
||||||
|
|
||||||
// Refresh vendor list so UI state stays in sync after buyback result.
|
// Refresh vendor list so UI state stays in sync after buyback result.
|
||||||
if (currentVendorItems.vendorGuid != 0 && socket && state == WorldState::IN_WORLD) {
|
if (currentVendorItems.vendorGuid != 0 && socket && state == WorldState::IN_WORLD) {
|
||||||
|
|
@ -1722,6 +1723,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
pendingSellToBuyback_.erase(itemGuid);
|
pendingSellToBuyback_.erase(itemGuid);
|
||||||
} else {
|
} else {
|
||||||
|
bool removedPending = false;
|
||||||
auto it = pendingSellToBuyback_.find(itemGuid);
|
auto it = pendingSellToBuyback_.find(itemGuid);
|
||||||
if (it != pendingSellToBuyback_.end()) {
|
if (it != pendingSellToBuyback_.end()) {
|
||||||
for (auto bit = buybackItems_.begin(); bit != buybackItems_.end(); ++bit) {
|
for (auto bit = buybackItems_.begin(); bit != buybackItems_.end(); ++bit) {
|
||||||
|
|
@ -1731,6 +1733,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingSellToBuyback_.erase(it);
|
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[] = {
|
static const char* sellErrors[] = {
|
||||||
"OK", "Can't find item", "Can't sell item",
|
"OK", "Can't find item", "Can't sell item",
|
||||||
|
|
@ -1827,8 +1846,42 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
" item/slot=", itemIdOrSlot,
|
" item/slot=", itemIdOrSlot,
|
||||||
" err=", static_cast<int>(errCode),
|
" err=", static_cast<int>(errCode),
|
||||||
" pendingBuybackSlot=", pendingBuybackSlot_,
|
" pendingBuybackSlot=", pendingBuybackSlot_,
|
||||||
|
" pendingBuybackWireSlot=", pendingBuybackWireSlot_,
|
||||||
" pendingBuyItemId=", pendingBuyItemId_,
|
" pendingBuyItemId=", pendingBuyItemId_,
|
||||||
" pendingBuyItemSlot=", pendingBuyItemSlot_);
|
" 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.";
|
const char* msg = "Purchase failed.";
|
||||||
switch (errCode) {
|
switch (errCode) {
|
||||||
case 0: msg = "Purchase failed: item not found."; break;
|
case 0: msg = "Purchase failed: item not found."; break;
|
||||||
|
|
@ -9078,6 +9131,7 @@ void GameHandler::closeVendor() {
|
||||||
buybackItems_.clear();
|
buybackItems_.clear();
|
||||||
pendingSellToBuyback_.clear();
|
pendingSellToBuyback_.clear();
|
||||||
pendingBuybackSlot_ = -1;
|
pendingBuybackSlot_ = -1;
|
||||||
|
pendingBuybackWireSlot_ = 0;
|
||||||
pendingBuyItemId_ = 0;
|
pendingBuyItemId_ = 0;
|
||||||
pendingBuyItemSlot_ = 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);
|
" wire=0x", std::hex, wireOpcode(Opcode::CMSG_BUY_ITEM), std::dec);
|
||||||
pendingBuyItemId_ = itemId;
|
pendingBuyItemId_ = itemId;
|
||||||
pendingBuyItemSlot_ = slot;
|
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);
|
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.
|
// This request is independent from normal buy path; avoid stale pending buy context in logs.
|
||||||
pendingBuyItemId_ = 0;
|
pendingBuyItemId_ = 0;
|
||||||
pendingBuyItemSlot_ = 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,
|
LOG_INFO("Buyback request: vendorGuid=0x", std::hex, currentVendorItems.vendorGuid,
|
||||||
std::dec, " uiSlot=", buybackSlot, " wireSlot=", wireSlot,
|
std::dec, " uiSlot=", buybackSlot, " wireSlot=", wireSlot,
|
||||||
" source=absolute-buyback-slot",
|
" source=absolute-buyback-slot",
|
||||||
" wire=0x", std::hex,
|
" wire=0x", std::hex, kWotlkCmsgBuybackItemOpcode, std::dec);
|
||||||
wireOpcode(Opcode::CMSG_BUYBACK_ITEM), std::dec);
|
|
||||||
pendingBuybackSlot_ = static_cast<int>(buybackSlot);
|
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);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -361,8 +361,18 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
||||||
|
|
||||||
// Auto-open bags when vendor window opens
|
// Auto-open bags when vendor window opens
|
||||||
if (gameHandler.isVendorWindowOpen() && !inventoryScreen.isOpen()) {
|
if (gameHandler.isVendorWindowOpen()) {
|
||||||
inventoryScreen.setOpen(true);
|
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)
|
// Bags (B key toggle handled inside)
|
||||||
|
|
@ -800,7 +810,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
||||||
if (!bonusLine.empty()) {
|
if (!bonusLine.empty()) {
|
||||||
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
||||||
}
|
}
|
||||||
if (!isWeapon && info->armor > 0) {
|
if (info->armor > 0) {
|
||||||
ImGui::Text("%d Armor", info->armor);
|
ImGui::Text("%d Armor", info->armor);
|
||||||
}
|
}
|
||||||
if (info->sellPrice > 0) {
|
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;
|
float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed;
|
||||||
ImGui::Text("%.1f DPS", dps);
|
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);
|
ImGui::Text("%d Armor", eq->item.armor);
|
||||||
}
|
}
|
||||||
std::string eqBonusLine;
|
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::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell");
|
||||||
ImGui::Separator();
|
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()) {
|
if (vendor.items.empty()) {
|
||||||
ImGui::TextDisabled("This vendor has nothing for sale.");
|
ImGui::TextDisabled("This vendor has nothing for sale.");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -5021,7 +5080,8 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableSetColumnIndex(3);
|
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);
|
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue