diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 556fdf46..effd9176 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -707,6 +707,18 @@ public: bool hasPendingGroupInvite() const { return pendingGroupInvite; } const std::string& getPendingInviterName() const { return pendingInviterName; } + // ---- Trade ---- + enum class TradeStatus : uint8_t { + None = 0, PendingIncoming, Open, Accepted, Complete + }; + TradeStatus getTradeStatus() const { return tradeStatus_; } + bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; } + const std::string& getTradePeerName() const { return tradePeerName_; } + 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 + // ---- Duel ---- bool hasPendingDuelRequest() const { return pendingDuelRequest_; } const std::string& getDuelChallengerName() const { return duelChallengerName_; } @@ -1268,6 +1280,7 @@ private: // ---- Instance lockout handler ---- void handleRaidInstanceInfo(network::Packet& packet); + void handleTradeStatus(network::Packet& packet); void handleDuelRequested(network::Packet& packet); void handleDuelComplete(network::Packet& packet); void handleDuelWinner(network::Packet& packet); @@ -1625,6 +1638,11 @@ private: bool pendingGroupInvite = false; std::string pendingInviterName; + // Trade state + TradeStatus tradeStatus_ = TradeStatus::None; + uint64_t tradePeerGuid_= 0; + std::string tradePeerName_; + // Duel state bool pendingDuelRequest_ = false; uint64_t duelChallengerGuid_= 0; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 7b272c45..42f64bc9 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1326,6 +1326,24 @@ public: static network::Packet build(uint64_t targetGuid); }; +/** CMSG_BEGIN_TRADE packet builder (no payload — accepts incoming trade request) */ +class BeginTradePacket { +public: + static network::Packet build(); +}; + +/** CMSG_CANCEL_TRADE packet builder (no payload) */ +class CancelTradePacket { +public: + static network::Packet build(); +}; + +/** CMSG_ACCEPT_TRADE packet builder (no payload — lock in current offer) */ +class AcceptTradePacket { +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 6cd82c52..5668e45f 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -206,6 +206,7 @@ private: void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); + void renderTradeRequestPopup(game::GameHandler& gameHandler); void renderBuffBar(game::GameHandler& gameHandler); void renderLootWindow(game::GameHandler& gameHandler); void renderGossipWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8fbe0549..a29a9950 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1953,6 +1953,10 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_LOOT_REMOVED: handleLootRemoved(packet); break; + case Opcode::SMSG_TRADE_STATUS: + case Opcode::SMSG_TRADE_STATUS_EXTENDED: + handleTradeStatus(packet); + break; case Opcode::SMSG_LOOT_ROLL: handleLootRoll(packet); break; @@ -14991,6 +14995,100 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) { " error=", result.errorCode); } +// --------------------------------------------------------------------------- +// Trade (SMSG_TRADE_STATUS / SMSG_TRADE_STATUS_EXTENDED) +// WotLK 3.3.5a status values: +// 0=busy, 1=begin_trade(+guid), 2=open_window, 3=cancelled, 4=accepted, +// 5=busy2, 6=no_target, 7=back_to_trade, 8=complete, 9=rejected, +// 10=too_far, 11=wrong_faction, 12=close_window, 13=ignore, +// 14-19=stun/dead/logout, 20=trial, 21=conjured_only +// --------------------------------------------------------------------------- + +void GameHandler::handleTradeStatus(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t status = packet.readUInt32(); + + switch (status) { + case 1: { // BEGIN_TRADE — incoming request; read initiator GUID + if (packet.getSize() - packet.getReadPos() >= 8) { + tradePeerGuid_ = packet.readUInt64(); + } + // Resolve name from entity list + tradePeerName_.clear(); + auto entity = entityManager.getEntity(tradePeerGuid_); + if (auto* unit = dynamic_cast(entity.get())) { + tradePeerName_ = unit->getName(); + } + if (tradePeerName_.empty()) { + char tmp[32]; + std::snprintf(tmp, sizeof(tmp), "0x%llX", + static_cast(tradePeerGuid_)); + tradePeerName_ = tmp; + } + tradeStatus_ = TradeStatus::PendingIncoming; + addSystemChatMessage(tradePeerName_ + " wants to trade with you."); + break; + } + case 2: // OPEN_WINDOW + tradeStatus_ = TradeStatus::Open; + addSystemChatMessage("Trade window opened."); + break; + case 3: // CANCELLED + case 9: // REJECTED + case 12: // CLOSE_WINDOW + tradeStatus_ = TradeStatus::None; + addSystemChatMessage("Trade cancelled."); + break; + case 4: // ACCEPTED (partner accepted) + tradeStatus_ = TradeStatus::Accepted; + addSystemChatMessage("Trade accepted. Awaiting other player..."); + break; + case 8: // COMPLETE + tradeStatus_ = TradeStatus::Complete; + addSystemChatMessage("Trade complete!"); + tradeStatus_ = TradeStatus::None; // reset after notification + break; + case 7: // BACK_TO_TRADE (unaccepted after a change) + tradeStatus_ = TradeStatus::Open; + addSystemChatMessage("Trade offer changed."); + break; + case 10: addSystemChatMessage("Trade target is too far away."); break; + case 11: addSystemChatMessage("Trade failed: wrong faction."); break; + case 13: addSystemChatMessage("Trade failed: player ignores you."); break; + case 14: addSystemChatMessage("Trade failed: you are stunned."); break; + case 15: addSystemChatMessage("Trade failed: target is stunned."); break; + case 16: addSystemChatMessage("Trade failed: you are dead."); break; + case 17: addSystemChatMessage("Trade failed: target is dead."); break; + case 20: addSystemChatMessage("Trial accounts cannot trade."); break; + default: break; + } + LOG_DEBUG("SMSG_TRADE_STATUS: status=", status); +} + +void GameHandler::acceptTradeRequest() { + if (tradeStatus_ != TradeStatus::PendingIncoming || !socket) return; + tradeStatus_ = TradeStatus::Open; + socket->send(BeginTradePacket::build()); +} + +void GameHandler::declineTradeRequest() { + if (!socket) return; + tradeStatus_ = TradeStatus::None; + socket->send(CancelTradePacket::build()); +} + +void GameHandler::acceptTrade() { + if (tradeStatus_ != TradeStatus::Open || !socket) return; + tradeStatus_ = TradeStatus::Accepted; + socket->send(AcceptTradePacket::build()); +} + +void GameHandler::cancelTrade() { + if (!socket) return; + tradeStatus_ = TradeStatus::None; + socket->send(CancelTradePacket::build()); +} + // --------------------------------------------------------------------------- // 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 e41c4693..69961140 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2139,6 +2139,24 @@ network::Packet DuelProposedPacket::build(uint64_t targetGuid) { return packet; } +network::Packet BeginTradePacket::build() { + network::Packet packet(wireOpcode(Opcode::CMSG_BEGIN_TRADE)); + LOG_DEBUG("Built CMSG_BEGIN_TRADE"); + return packet; +} + +network::Packet CancelTradePacket::build() { + network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_TRADE)); + LOG_DEBUG("Built CMSG_CANCEL_TRADE"); + return packet; +} + +network::Packet AcceptTradePacket::build() { + network::Packet packet(wireOpcode(Opcode::CMSG_ACCEPT_TRADE)); + LOG_DEBUG("Built CMSG_ACCEPT_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 93cab28d..3d9cec81 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -398,6 +398,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); renderLootRollPopup(gameHandler); + renderTradeRequestPopup(gameHandler); renderGuildInvitePopup(gameHandler); renderGuildRoster(gameHandler); renderBuffBar(gameHandler); @@ -4402,6 +4403,30 @@ void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderTradeRequestPopup(game::GameHandler& gameHandler) { + if (!gameHandler.hasPendingTradeRequest()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 370), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); + + if (ImGui::Begin("Trade Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + ImGui::Text("%s wants to trade with you.", gameHandler.getTradePeerName().c_str()); + ImGui::Spacing(); + + if (ImGui::Button("Accept", ImVec2(130, 30))) { + gameHandler.acceptTradeRequest(); + } + ImGui::SameLine(); + if (ImGui::Button("Decline", ImVec2(130, 30))) { + gameHandler.declineTradeRequest(); + } + } + ImGui::End(); +} + void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingLootRoll()) return;