diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f2a83f38..25b6b8de 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -933,13 +933,38 @@ public: enum class TradeStatus : uint8_t { None = 0, PendingIncoming, Open, Accepted, Complete }; + + static constexpr int TRADE_SLOT_COUNT = 6; // WoW has 6 normal trade slots + slot 6 for non-trade item + + struct TradeSlot { + uint32_t itemId = 0; + uint32_t displayId = 0; + uint32_t stackCount = 0; + uint64_t itemGuid = 0; + uint8_t bag = 0xFF; // 0xFF = not set + uint8_t bagSlot = 0xFF; + bool occupied = false; + }; + TradeStatus getTradeStatus() const { return tradeStatus_; } bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; } + bool isTradeOpen() const { return tradeStatus_ == TradeStatus::Open || tradeStatus_ == TradeStatus::Accepted; } const std::string& getTradePeerName() const { return tradePeerName_; } + + // My trade slots (what I'm offering) + const std::array& getMyTradeSlots() const { return myTradeSlots_; } + // Peer's trade slots (what they're offering) + const std::array& getPeerTradeSlots() const { return peerTradeSlots_; } + uint64_t getMyTradeGold() const { return myTradeGold_; } + uint64_t getPeerTradeGold() const { return peerTradeGold_; } + void acceptTradeRequest(); // respond to incoming SMSG_TRADE_STATUS(1) with CMSG_BEGIN_TRADE void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE void cancelTrade(); // CMSG_CANCEL_TRADE + void setTradeItem(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot); + void clearTradeItem(uint8_t tradeSlot); + void setTradeGold(uint64_t copper); // ---- Duel ---- bool hasPendingDuelRequest() const { return pendingDuelRequest_; } @@ -1653,6 +1678,8 @@ private: void handleQuestConfirmAccept(network::Packet& packet); void handleSummonRequest(network::Packet& packet); void handleTradeStatus(network::Packet& packet); + void handleTradeStatusExtended(network::Packet& packet); + void resetTradeState(); void handleDuelRequested(network::Packet& packet); void handleDuelComplete(network::Packet& packet); void handleDuelWinner(network::Packet& packet); @@ -2077,6 +2104,10 @@ private: TradeStatus tradeStatus_ = TradeStatus::None; uint64_t tradePeerGuid_= 0; std::string tradePeerName_; + std::array myTradeSlots_{}; + std::array peerTradeSlots_{}; + uint64_t myTradeGold_ = 0; + uint64_t peerTradeGold_ = 0; // Duel state bool pendingDuelRequest_ = false; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 5d75e887..61d36ebf 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1356,6 +1356,33 @@ public: static network::Packet build(); }; +/** CMSG_SET_TRADE_ITEM packet builder (tradeSlot, bag, bagSlot) */ +class SetTradeItemPacket { +public: + // tradeSlot: 0-5 (normal) or 6 (backpack money-only slot) + // bag: 255 = main backpack, 19-22 = bag slots + // bagSlot: slot within bag + static network::Packet build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot); +}; + +/** CMSG_CLEAR_TRADE_ITEM packet builder (remove item from trade slot) */ +class ClearTradeItemPacket { +public: + static network::Packet build(uint8_t tradeSlot); +}; + +/** CMSG_SET_TRADE_GOLD packet builder (gold offered, in copper) */ +class SetTradeGoldPacket { +public: + static network::Packet build(uint64_t copper); +}; + +/** CMSG_UNACCEPT_TRADE packet builder (unaccept without cancelling) */ +class UnacceptTradePacket { +public: + static network::Packet build(); +}; + /** CMSG_ATTACKSWING packet builder */ class AttackSwingPacket { public: diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 8bd19235..655b20cb 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -224,6 +224,7 @@ private: void renderDuelRequestPopup(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); void renderTradeRequestPopup(game::GameHandler& gameHandler); + void renderTradeWindow(game::GameHandler& gameHandler); void renderSummonRequestPopup(game::GameHandler& gameHandler); void renderSharedQuestPopup(game::GameHandler& gameHandler); void renderItemTextWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index e6e234c4..af655890 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3102,9 +3102,11 @@ void GameHandler::handlePacket(network::Packet& packet) { addSystemChatMessage("Summon cancelled."); break; case Opcode::SMSG_TRADE_STATUS: - case Opcode::SMSG_TRADE_STATUS_EXTENDED: handleTradeStatus(packet); break; + case Opcode::SMSG_TRADE_STATUS_EXTENDED: + handleTradeStatusExtended(packet); + break; case Opcode::SMSG_LOOT_ROLL: handleLootRoll(packet); break; @@ -19047,13 +19049,17 @@ void GameHandler::handleTradeStatus(network::Packet& packet) { break; } case 2: // OPEN_WINDOW + myTradeSlots_.fill(TradeSlot{}); + peerTradeSlots_.fill(TradeSlot{}); + myTradeGold_ = 0; + peerTradeGold_ = 0; tradeStatus_ = TradeStatus::Open; addSystemChatMessage("Trade window opened."); break; case 3: // CANCELLED case 9: // REJECTED case 12: // CLOSE_WINDOW - tradeStatus_ = TradeStatus::None; + resetTradeState(); addSystemChatMessage("Trade cancelled."); break; case 4: // ACCEPTED (partner accepted) @@ -19061,9 +19067,8 @@ void GameHandler::handleTradeStatus(network::Packet& packet) { addSystemChatMessage("Trade accepted. Awaiting other player..."); break; case 8: // COMPLETE - tradeStatus_ = TradeStatus::Complete; addSystemChatMessage("Trade complete!"); - tradeStatus_ = TradeStatus::None; // reset after notification + resetTradeState(); break; case 7: // BACK_TO_TRADE (unaccepted after a change) tradeStatus_ = TradeStatus::Open; @@ -19102,10 +19107,104 @@ void GameHandler::acceptTrade() { void GameHandler::cancelTrade() { if (!socket) return; - tradeStatus_ = TradeStatus::None; + resetTradeState(); socket->send(CancelTradePacket::build()); } +void GameHandler::setTradeItem(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot) { + if (!isTradeOpen() || !socket || tradeSlot >= TRADE_SLOT_COUNT) return; + socket->send(SetTradeItemPacket::build(tradeSlot, bag, bagSlot)); +} + +void GameHandler::clearTradeItem(uint8_t tradeSlot) { + if (!isTradeOpen() || !socket || tradeSlot >= TRADE_SLOT_COUNT) return; + myTradeSlots_[tradeSlot] = TradeSlot{}; + socket->send(ClearTradeItemPacket::build(tradeSlot)); +} + +void GameHandler::setTradeGold(uint64_t copper) { + if (!isTradeOpen() || !socket) return; + myTradeGold_ = copper; + socket->send(SetTradeGoldPacket::build(copper)); +} + +void GameHandler::resetTradeState() { + tradeStatus_ = TradeStatus::None; + myTradeGold_ = 0; + peerTradeGold_ = 0; + myTradeSlots_.fill(TradeSlot{}); + peerTradeSlots_.fill(TradeSlot{}); +} + +void GameHandler::handleTradeStatusExtended(network::Packet& packet) { + // WotLK 3.3.5a SMSG_TRADE_STATUS_EXTENDED format: + // uint8 isSelfState (1 = my trade window, 0 = peer's) + // uint32 tradeId + // uint32 slotCount (7: 6 normal + 1 extra for enchanting) + // Per slot (up to slotCount): + // uint8 slotIndex + // uint32 itemId + // uint32 displayId + // uint32 stackCount + // uint8 isWrapped + // uint64 giftCreatorGuid + // uint32 enchantId (and several more enchant/stat fields) + // ... (complex; we parse only the essential fields) + // uint64 coins (gold offered by the sender of this message) + + size_t rem = packet.getSize() - packet.getReadPos(); + if (rem < 9) return; + + uint8_t isSelf = packet.readUInt8(); + uint32_t tradeId = packet.readUInt32(); (void)tradeId; + uint32_t slotCount= packet.readUInt32(); + + auto& slots = isSelf ? myTradeSlots_ : peerTradeSlots_; + + for (uint32_t i = 0; i < slotCount && (packet.getSize() - packet.getReadPos()) >= 14; ++i) { + uint8_t slotIdx = packet.readUInt8(); + uint32_t itemId = packet.readUInt32(); + uint32_t displayId = packet.readUInt32(); + uint32_t stackCount = packet.readUInt32(); + + // isWrapped + giftCreatorGuid + several enchant fields — skip them all + // We need at least 1+8+4*5 = 29 bytes for the rest of this slot entry + bool isWrapped = false; + if (packet.getSize() - packet.getReadPos() >= 1) { + isWrapped = (packet.readUInt8() != 0); + } + // Skip giftCreatorGuid (8) + enchantId*5 (20) + suffixFactor (4) + randPropId (4) + lockId (4) + // + maxDurability (4) + durability (4) = 49 bytes + // Plus if wrapped: giftCreatorGuid already consumed; additional guid = 0 + constexpr size_t SLOT_TRAIL = 49; + if (packet.getSize() - packet.getReadPos() >= SLOT_TRAIL) { + packet.setReadPos(packet.getReadPos() + SLOT_TRAIL); + } else { + packet.setReadPos(packet.getSize()); + return; + } + (void)isWrapped; + + if (slotIdx < TRADE_SLOT_COUNT) { + TradeSlot& s = slots[slotIdx]; + s.itemId = itemId; + s.displayId = displayId; + s.stackCount = stackCount; + s.occupied = (itemId != 0); + } + } + + // Gold offered (uint64 copper) + if (packet.getSize() - packet.getReadPos() >= 8) { + uint64_t coins = packet.readUInt64(); + if (isSelf) myTradeGold_ = coins; + else peerTradeGold_ = coins; + } + + LOG_DEBUG("SMSG_TRADE_STATUS_EXTENDED: isSelf=", (int)isSelf, + " myGold=", myTradeGold_, " peerGold=", peerTradeGold_); +} + // --------------------------------------------------------------------------- // Group loot roll (SMSG_LOOT_ROLL / SMSG_LOOT_ROLL_WON / CMSG_LOOT_ROLL) // --------------------------------------------------------------------------- diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index c3adbcb0..d9388cd8 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2177,6 +2177,35 @@ network::Packet AcceptTradePacket::build() { return packet; } +network::Packet SetTradeItemPacket::build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot) { + network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_ITEM)); + packet.writeUInt8(tradeSlot); + packet.writeUInt8(bag); + packet.writeUInt8(bagSlot); + LOG_DEBUG("Built CMSG_SET_TRADE_ITEM slot=", (int)tradeSlot, " bag=", (int)bag, " bagSlot=", (int)bagSlot); + return packet; +} + +network::Packet ClearTradeItemPacket::build(uint8_t tradeSlot) { + network::Packet packet(wireOpcode(Opcode::CMSG_CLEAR_TRADE_ITEM)); + packet.writeUInt8(tradeSlot); + LOG_DEBUG("Built CMSG_CLEAR_TRADE_ITEM slot=", (int)tradeSlot); + return packet; +} + +network::Packet SetTradeGoldPacket::build(uint64_t copper) { + network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_GOLD)); + packet.writeUInt64(copper); + LOG_DEBUG("Built CMSG_SET_TRADE_GOLD copper=", copper); + return packet; +} + +network::Packet UnacceptTradePacket::build() { + network::Packet packet(wireOpcode(Opcode::CMSG_UNACCEPT_TRADE)); + LOG_DEBUG("Built CMSG_UNACCEPT_TRADE"); + return packet; +} + network::Packet InitiateTradePacket::build(uint64_t targetGuid) { network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE)); packet.writeUInt64(targetGuid); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 3bfa4b83..803e07d4 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -414,6 +414,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderDuelRequestPopup(gameHandler); renderLootRollPopup(gameHandler); renderTradeRequestPopup(gameHandler); + renderTradeWindow(gameHandler); renderSummonRequestPopup(gameHandler); renderSharedQuestPopup(gameHandler); renderItemTextWindow(gameHandler); @@ -5980,6 +5981,150 @@ void GameScreen::renderTradeRequestPopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderTradeWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isTradeOpen()) return; + + const auto& mySlots = gameHandler.getMyTradeSlots(); + const auto& peerSlots = gameHandler.getPeerTradeSlots(); + const uint64_t myGold = gameHandler.getMyTradeGold(); + const uint64_t peerGold = gameHandler.getPeerTradeGold(); + const auto& peerName = gameHandler.getTradePeerName(); + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 240.0f, screenH / 2.0f - 180.0f), ImGuiCond_Once); + ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once); + + bool open = true; + if (ImGui::Begin(("Trade with " + peerName).c_str(), &open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + + auto formatGold = [](uint64_t copper, char* buf, size_t bufsz) { + uint64_t g = copper / 10000; + uint64_t s = (copper % 10000) / 100; + uint64_t c = copper % 100; + if (g > 0) std::snprintf(buf, bufsz, "%llug %llus %lluc", + (unsigned long long)g, (unsigned long long)s, (unsigned long long)c); + else if (s > 0) std::snprintf(buf, bufsz, "%llus %lluc", + (unsigned long long)s, (unsigned long long)c); + else std::snprintf(buf, bufsz, "%lluc", (unsigned long long)c); + }; + + auto renderSlotColumn = [&](const char* label, + const std::array& slots, + uint64_t gold, bool isMine) { + ImGui::Text("%s", label); + ImGui::Separator(); + + for (int i = 0; i < game::GameHandler::TRADE_SLOT_COUNT; ++i) { + const auto& slot = slots[i]; + ImGui::PushID(i * (isMine ? 1 : -1) - (isMine ? 0 : 100)); + + if (slot.occupied && slot.itemId != 0) { + const auto* info = gameHandler.getItemInfo(slot.itemId); + std::string name = (info && info->valid && !info->name.empty()) + ? info->name + : ("Item " + std::to_string(slot.itemId)); + if (slot.stackCount > 1) + name += " x" + std::to_string(slot.stackCount); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 1.0f), " %d. %s", i + 1, name.c_str()); + + if (isMine && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + gameHandler.clearTradeItem(static_cast(i)); + } + if (isMine && ImGui::IsItemHovered()) { + ImGui::SetTooltip("Double-click to remove"); + } + } else { + ImGui::TextDisabled(" %d. (empty)", i + 1); + + // Allow dragging inventory items into trade slots via right-click context menu + if (isMine && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + ImGui::OpenPopup(("##additem" + std::to_string(i)).c_str()); + } + } + + if (isMine) { + // Drag-from-inventory: show small popup listing bag items + if (ImGui::BeginPopup(("##additem" + std::to_string(i)).c_str())) { + ImGui::TextDisabled("Add from inventory:"); + const auto& inv = gameHandler.getInventory(); + // Backpack slots 0-15 (bag=255) + for (int si = 0; si < game::Inventory::BACKPACK_SLOTS; ++si) { + const auto& slot = inv.getBackpackSlot(si); + if (slot.empty()) continue; + const auto* ii = gameHandler.getItemInfo(slot.item.itemId); + std::string iname = (ii && ii->valid && !ii->name.empty()) + ? ii->name + : (!slot.item.name.empty() ? slot.item.name + : ("Item " + std::to_string(slot.item.itemId))); + if (ImGui::Selectable(iname.c_str())) { + // bag=255 = main backpack + gameHandler.setTradeItem(static_cast(i), 255u, + static_cast(si)); + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } + } + ImGui::PopID(); + } + + // Gold row + char gbuf[48]; + formatGold(gold, gbuf, sizeof(gbuf)); + ImGui::Spacing(); + if (isMine) { + ImGui::Text("Gold offered: %s", gbuf); + static char goldInput[32] = "0"; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText("##goldset", goldInput, sizeof(goldInput), + ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_EnterReturnsTrue)) { + uint64_t copper = std::strtoull(goldInput, nullptr, 10); + gameHandler.setTradeGold(copper); + } + ImGui::SameLine(); + ImGui::TextDisabled("(copper, Enter to set)"); + } else { + ImGui::Text("Gold offered: %s", gbuf); + } + }; + + // Two-column layout: my offer | peer offer + float colW = ImGui::GetContentRegionAvail().x * 0.5f - 4.0f; + ImGui::BeginChild("##myoffer", ImVec2(colW, 240.0f), true); + renderSlotColumn("Your offer", mySlots, myGold, true); + ImGui::EndChild(); + + ImGui::SameLine(); + + ImGui::BeginChild("##peroffer", ImVec2(colW, 240.0f), true); + renderSlotColumn((peerName + "'s offer").c_str(), peerSlots, peerGold, false); + ImGui::EndChild(); + + // Buttons + ImGui::Spacing(); + ImGui::Separator(); + float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; + if (ImGui::Button("Accept Trade", ImVec2(bw, 0))) { + gameHandler.acceptTrade(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(bw, 0))) { + gameHandler.cancelTrade(); + } + } + ImGui::End(); + + if (!open) { + gameHandler.cancelTrade(); + } +} + void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingLootRoll()) return;