From 7a2bb28dc0be8d9064d6629c7f6b1d0f0bd5b80f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 23:47:43 -0800 Subject: [PATCH] Add arena/BG opcodes, fix mount speed, buff bar icons, and autorun cancel Add 36 arena and battleground opcodes with handlers for queue status, team events, invites, and errors. Fix SMSG_FORCE_RUN_SPEED_CHANGE parsing (uint8 not uint32) and remove manual mount speed tracking. Buff bar now shows spell icons and is positioned below the minimap. Both mouse buttons cancel autorun. --- include/game/game_handler.hpp | 9 +- include/game/opcodes.hpp | 40 +++++ src/game/game_handler.cpp | 227 +++++++++++++++++++++++++--- src/rendering/camera_controller.cpp | 3 + src/ui/game_screen.cpp | 80 +++++++--- 5 files changed, 313 insertions(+), 46 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2f25f9f7..62946ea6 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -659,6 +659,14 @@ private: // ---- Speed change handler ---- void handleForceRunSpeedChange(network::Packet& packet); + // ---- Arena / Battleground handlers ---- + void handleBattlefieldStatus(network::Packet& packet); + void handleArenaTeamCommandResult(network::Packet& packet); + void handleArenaTeamQueryResponse(network::Packet& packet); + void handleArenaTeamInvite(network::Packet& packet); + void handleArenaTeamEvent(network::Packet& packet); + void handleArenaError(network::Packet& packet); + // ---- Taxi handlers ---- void handleShowTaxiNodes(network::Packet& packet); void handleActivateTaxiReply(network::Packet& packet); @@ -914,7 +922,6 @@ private: NpcSwingCallback npcSwingCallback_; MountCallback mountCallback_; uint32_t currentMountDisplayId_ = 0; - float preMountRunSpeed_ = 0.0f; float serverRunSpeed_ = 7.0f; bool playerDead_ = false; bool releasedSpirit_ = false; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 1914ff3b..3d38f43a 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -263,6 +263,46 @@ enum class Opcode : uint16_t { SMSG_ACTIVATETAXIREPLY = 0x01AE, SMSG_NEW_TAXI_PATH = 0x01AF, CMSG_ACTIVATETAXIEXPRESS = 0x0312, + + // ---- Battleground ---- + SMSG_BATTLEFIELD_PORT_DENIED = 0x014B, + SMSG_REMOVED_FROM_PVP_QUEUE = 0x0170, + CMSG_BATTLEFIELD_LIST = 0x023C, + SMSG_BATTLEFIELD_LIST = 0x023D, + CMSG_BATTLEFIELD_JOIN = 0x023E, + CMSG_BATTLEFIELD_STATUS = 0x02D3, + SMSG_BATTLEFIELD_STATUS = 0x02D4, + CMSG_BATTLEFIELD_PORT = 0x02D5, + CMSG_BATTLEMASTER_HELLO = 0x02D7, + MSG_PVP_LOG_DATA = 0x02E0, + CMSG_LEAVE_BATTLEFIELD = 0x02E1, + SMSG_GROUP_JOINED_BATTLEGROUND = 0x02E8, + MSG_BATTLEGROUND_PLAYER_POSITIONS = 0x02E9, + SMSG_BATTLEGROUND_PLAYER_JOINED = 0x02EC, + SMSG_BATTLEGROUND_PLAYER_LEFT = 0x02ED, + CMSG_BATTLEMASTER_JOIN = 0x02EE, + SMSG_JOINED_BATTLEGROUND_QUEUE = 0x038A, + + // ---- Arena Team ---- + CMSG_ARENA_TEAM_CREATE = 0x0348, + SMSG_ARENA_TEAM_COMMAND_RESULT = 0x0349, + CMSG_ARENA_TEAM_QUERY = 0x034B, + SMSG_ARENA_TEAM_QUERY_RESPONSE = 0x034C, + CMSG_ARENA_TEAM_ROSTER = 0x034D, + SMSG_ARENA_TEAM_ROSTER = 0x034E, + CMSG_ARENA_TEAM_INVITE = 0x034F, + SMSG_ARENA_TEAM_INVITE = 0x0350, + CMSG_ARENA_TEAM_ACCEPT = 0x0351, + CMSG_ARENA_TEAM_DECLINE = 0x0352, + CMSG_ARENA_TEAM_LEAVE = 0x0353, + CMSG_ARENA_TEAM_REMOVE = 0x0354, + CMSG_ARENA_TEAM_DISBAND = 0x0355, + CMSG_ARENA_TEAM_LEADER = 0x0356, + SMSG_ARENA_TEAM_EVENT = 0x0357, + CMSG_BATTLEMASTER_JOIN_ARENA = 0x0358, + SMSG_ARENA_TEAM_STATS = 0x035B, + SMSG_ARENA_ERROR = 0x0376, + MSG_INSPECT_ARENA_TEAMS = 0x0377, }; } // namespace game diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 55a1eda3..24c91c48 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -748,6 +748,59 @@ void GameHandler::handlePacket(network::Packet& packet) { addSystemChatMessage("New flight path discovered!"); break; + // ---- Arena / Battleground ---- + case Opcode::SMSG_BATTLEFIELD_STATUS: + handleBattlefieldStatus(packet); + break; + case Opcode::SMSG_BATTLEFIELD_LIST: + LOG_INFO("Received SMSG_BATTLEFIELD_LIST"); + break; + case Opcode::SMSG_BATTLEFIELD_PORT_DENIED: + addSystemChatMessage("Battlefield port denied."); + break; + case Opcode::SMSG_REMOVED_FROM_PVP_QUEUE: + addSystemChatMessage("You have been removed from the PvP queue."); + break; + case Opcode::SMSG_GROUP_JOINED_BATTLEGROUND: + addSystemChatMessage("Your group has joined the battleground."); + break; + case Opcode::SMSG_JOINED_BATTLEGROUND_QUEUE: + addSystemChatMessage("You have joined the battleground queue."); + break; + case Opcode::SMSG_BATTLEGROUND_PLAYER_JOINED: + LOG_INFO("Battleground player joined"); + break; + case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT: + LOG_INFO("Battleground player left"); + break; + case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT: + handleArenaTeamCommandResult(packet); + break; + case Opcode::SMSG_ARENA_TEAM_QUERY_RESPONSE: + handleArenaTeamQueryResponse(packet); + break; + case Opcode::SMSG_ARENA_TEAM_ROSTER: + LOG_INFO("Received SMSG_ARENA_TEAM_ROSTER"); + break; + case Opcode::SMSG_ARENA_TEAM_INVITE: + handleArenaTeamInvite(packet); + break; + case Opcode::SMSG_ARENA_TEAM_EVENT: + handleArenaTeamEvent(packet); + break; + case Opcode::SMSG_ARENA_TEAM_STATS: + LOG_INFO("Received SMSG_ARENA_TEAM_STATS"); + break; + case Opcode::SMSG_ARENA_ERROR: + handleArenaError(packet); + break; + case Opcode::MSG_PVP_LOG_DATA: + LOG_INFO("Received MSG_PVP_LOG_DATA"); + break; + case Opcode::MSG_INSPECT_ARENA_TEAMS: + LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS"); + break; + default: LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec); break; @@ -1402,16 +1455,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.guid == playerGuid) { uint32_t old = currentMountDisplayId_; currentMountDisplayId_ = val; - if (old == 0 && val != 0) { - preMountRunSpeed_ = serverRunSpeed_; - } else if (old != 0 && val == 0) { - if (preMountRunSpeed_ > 0.1f && preMountRunSpeed_ < 100.0f) { - serverRunSpeed_ = preMountRunSpeed_; - } else { - serverRunSpeed_ = 7.0f; - } - preMountRunSpeed_ = 0.0f; - } if (val != old && mountCallback_) mountCallback_(val); } unit->setMountDisplayId(val); @@ -1584,16 +1627,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.guid == playerGuid) { uint32_t old = currentMountDisplayId_; currentMountDisplayId_ = val; - if (old == 0 && val != 0) { - preMountRunSpeed_ = serverRunSpeed_; - } else if (old != 0 && val == 0) { - if (preMountRunSpeed_ > 0.1f && preMountRunSpeed_ < 100.0f) { - serverRunSpeed_ = preMountRunSpeed_; - } else { - serverRunSpeed_ = 7.0f; - } - preMountRunSpeed_ = 0.0f; - } if (val != old && mountCallback_) mountCallback_(val); } unit->setMountDisplayId(val); @@ -3269,8 +3302,17 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) { uint64_t guid = UpdateObjectParser::readPackedGuid(packet); // uint32 counter uint32_t counter = packet.readUInt32(); - // uint32 unknown (TrinityCore/AzerothCore adds this for run speed) - packet.readUInt32(); + + // Determine format from remaining bytes: + // 5 bytes remaining = uint8(1) + float(4) — standard 3.3.5a + // 8 bytes remaining = uint32(4) + float(4) — some forks + // 4 bytes remaining = float(4) — no unknown field + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining >= 8) { + packet.readUInt32(); // unknown (extended format) + } else if (remaining >= 5) { + packet.readUInt8(); // unknown (standard 3.3.5a) + } // float newSpeed float newSpeed = packet.readFloat(); @@ -3306,6 +3348,147 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) { serverRunSpeed_ = newSpeed; } +// ============================================================ +// Arena / Battleground Handlers +// ============================================================ + +void GameHandler::handleBattlefieldStatus(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t queueSlot = packet.readUInt32(); + + // Minimal packet = just queueSlot + arenaType(1) when status is NONE + if (packet.getSize() - packet.getReadPos() < 1) { + LOG_INFO("Battlefield status: queue slot ", queueSlot, " cleared"); + return; + } + + uint8_t arenaType = packet.readUInt8(); + if (packet.getSize() - packet.getReadPos() < 1) return; + + // Unknown byte + packet.readUInt8(); + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t bgTypeId = packet.readUInt32(); + + if (packet.getSize() - packet.getReadPos() < 2) return; + uint16_t unk2 = packet.readUInt16(); + (void)unk2; + + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t clientInstanceId = packet.readUInt32(); + + if (packet.getSize() - packet.getReadPos() < 1) return; + uint8_t isRatedArena = packet.readUInt8(); + + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t statusId = packet.readUInt32(); + + std::string bgName = "Battleground #" + std::to_string(bgTypeId); + if (arenaType > 0) { + bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena"; + } + + switch (statusId) { + case 0: // STATUS_NONE + LOG_INFO("Battlefield status: NONE for ", bgName); + break; + case 1: // STATUS_WAIT_QUEUE + addSystemChatMessage("Queued for " + bgName + "."); + LOG_INFO("Battlefield status: WAIT_QUEUE for ", bgName); + break; + case 2: // STATUS_WAIT_JOIN + addSystemChatMessage(bgName + " is ready! Type /join to enter."); + LOG_INFO("Battlefield status: WAIT_JOIN for ", bgName); + break; + case 3: // STATUS_IN_PROGRESS + addSystemChatMessage("Entered " + bgName + "."); + LOG_INFO("Battlefield status: IN_PROGRESS for ", bgName); + break; + case 4: // STATUS_WAIT_LEAVE + LOG_INFO("Battlefield status: WAIT_LEAVE for ", bgName); + break; + default: + LOG_INFO("Battlefield status: unknown (", statusId, ") for ", bgName); + break; + } +} + +void GameHandler::handleArenaTeamCommandResult(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 8) return; + uint32_t command = packet.readUInt32(); + std::string name = packet.readString(); + uint32_t error = packet.readUInt32(); + + static const char* commands[] = { "create", "invite", "leave", "remove", "disband", "leader" }; + std::string cmdName = (command < 6) ? commands[command] : "unknown"; + + if (error == 0) { + addSystemChatMessage("Arena team " + cmdName + " successful" + + (name.empty() ? "." : ": " + name)); + } else { + addSystemChatMessage("Arena team " + cmdName + " failed" + + (name.empty() ? "." : " for " + name + ".")); + } + LOG_INFO("Arena team command: ", cmdName, " name=", name, " error=", error); +} + +void GameHandler::handleArenaTeamQueryResponse(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t teamId = packet.readUInt32(); + std::string teamName = packet.readString(); + LOG_INFO("Arena team query response: id=", teamId, " name=", teamName); +} + +void GameHandler::handleArenaTeamInvite(network::Packet& packet) { + std::string playerName = packet.readString(); + std::string teamName = packet.readString(); + addSystemChatMessage(playerName + " has invited you to join " + teamName + "."); + LOG_INFO("Arena team invite from ", playerName, " to ", teamName); +} + +void GameHandler::handleArenaTeamEvent(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 1) return; + uint8_t event = packet.readUInt8(); + + static const char* events[] = { + "joined", "left", "removed", "leader changed", + "disbanded", "created" + }; + std::string eventName = (event < 6) ? events[event] : "unknown event"; + + // Read string params (up to 3) + uint8_t strCount = 0; + if (packet.getSize() - packet.getReadPos() >= 1) { + strCount = packet.readUInt8(); + } + + std::string param1, param2; + if (strCount >= 1 && packet.getSize() > packet.getReadPos()) param1 = packet.readString(); + if (strCount >= 2 && packet.getSize() > packet.getReadPos()) param2 = packet.readString(); + + std::string msg = "Arena team " + eventName; + if (!param1.empty()) msg += ": " + param1; + if (!param2.empty()) msg += " (" + param2 + ")"; + addSystemChatMessage(msg); + LOG_INFO("Arena team event: ", eventName, " ", param1, " ", param2); +} + +void GameHandler::handleArenaError(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 4) return; + uint32_t error = packet.readUInt32(); + + std::string msg; + switch (error) { + case 1: msg = "The other team is not big enough."; break; + case 2: msg = "That team is full."; break; + case 3: msg = "Not enough members to start."; break; + case 4: msg = "Too many members."; break; + default: msg = "Arena error (code " + std::to_string(error) + ")"; break; + } + addSystemChatMessage(msg); + LOG_INFO("Arena error: ", error, " - ", msg); +} + void GameHandler::handleMonsterMove(network::Packet& packet) { MonsterMoveData data; if (!MonsterMoveParser::parse(packet, data)) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 7f7c0eca..244b4f9f 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -147,6 +147,9 @@ void CameraController::update(float deltaTime) { } bool mouseAutorun = !uiWantsKeyboard && !sitting && leftMouseDown && rightMouseDown; + if (mouseAutorun) { + autoRunning = false; + } bool nowForward = keyW || mouseAutorun || autoRunning; bool nowBackward = keyS; bool nowStrafeLeft = false; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 2043a8c1..66448dec 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2847,15 +2847,21 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + auto* assetMgr = core::Application::getInstance().getAssetManager(); - ImGui::SetNextWindowPos(ImVec2(screenW - 400, 30), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(390, 0), ImGuiCond_Always); + // Position below the minimap (minimap is 200px + 10px margin from top-right) + constexpr float ICON_SIZE = 32.0f; + constexpr int ICONS_PER_ROW = 8; + float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f; + ImGui::SetNextWindowPos(ImVec2(screenW - barW - 10.0f, 220.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(barW, 0), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_AlwaysAutoResize; + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar; ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); if (ImGui::Begin("##BuffBar", nullptr, flags)) { int shown = 0; @@ -2863,33 +2869,60 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { const auto& aura = auras[i]; if (aura.isEmpty()) continue; - if (shown > 0 && shown % 8 != 0) ImGui::SameLine(); + if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine(); ImGui::PushID(static_cast(i)); - // Green border for buffs, red for debuffs - bool isBuff = (aura.flags & 0x02) != 0; // POSITIVE flag - ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : ImVec4(0.8f, 0.2f, 0.2f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_Button, borderColor); + bool isBuff = (aura.flags & 0x02) != 0; + ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); - char label[16]; - snprintf(label, sizeof(label), "%u", aura.spellId); - if (ImGui::Button(label, ImVec2(40, 40))) { - // Right-click to cancel own buffs - if (isBuff) { - gameHandler.cancelAura(aura.spellId); + // Try to get spell icon + GLuint iconTex = 0; + if (assetMgr) { + iconTex = getSpellIcon(aura.spellId, assetMgr); + } + + bool clicked = false; + if (iconTex) { + ImGui::PushStyleColor(ImGuiCol_Button, borderColor); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + clicked = ImGui::ImageButton("##aura", + (ImTextureID)(uintptr_t)iconTex, + ImVec2(ICON_SIZE - 4, ICON_SIZE - 4)); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, borderColor); + char label[8]; + snprintf(label, sizeof(label), "%u", aura.spellId); + clicked = ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE)); + ImGui::PopStyleColor(); + } + + // Tooltip with spell name and duration + if (ImGui::IsItemHovered()) { + std::string name; + auto it = actionSpellNames.find(aura.spellId); + if (it != actionSpellNames.end()) { + name = it->second; + } else { + name = "Spell #" + std::to_string(aura.spellId); + } + if (aura.durationMs > 0) { + int seconds = aura.durationMs / 1000; + if (seconds < 60) { + ImGui::SetTooltip("%s (%ds)", name.c_str(), seconds); + } else { + ImGui::SetTooltip("%s (%dm %ds)", name.c_str(), seconds / 60, seconds % 60); + } + } else { + ImGui::SetTooltip("%s", name.c_str()); } } - ImGui::PopStyleColor(); - // Duration text - if (aura.durationMs > 0) { - int seconds = aura.durationMs / 1000; - if (seconds < 60) { - ImGui::Text("%ds", seconds); - } else { - ImGui::Text("%dm", seconds / 60); - } + // Click to cancel own buffs + if (clicked && isBuff) { + gameHandler.cancelAura(aura.spellId); } ImGui::PopID(); @@ -2898,6 +2931,7 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { } ImGui::End(); + ImGui::PopStyleVar(); ImGui::PopStyleColor(); }