diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 4bfcec31..a11cd067 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1247,6 +1247,29 @@ public: }; const std::vector& getArenaTeamStats() const { return arenaTeamStats_; } + // ---- Arena Team Roster ---- + struct ArenaTeamMember { + uint64_t guid = 0; + std::string name; + bool online = false; + uint32_t weekGames = 0; + uint32_t weekWins = 0; + uint32_t seasonGames = 0; + uint32_t seasonWins = 0; + uint32_t personalRating = 0; + }; + struct ArenaTeamRoster { + uint32_t teamId = 0; + std::vector members; + }; + // Returns roster for the given teamId, or nullptr if not yet received + const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const { + for (const auto& r : arenaTeamRosters_) { + if (r.teamId == teamId) return &r; + } + return nullptr; + } + // ---- Phase 5: Loot ---- void lootTarget(uint64_t guid); void lootItem(uint8_t slotIndex); @@ -2080,6 +2103,7 @@ private: void handleInstanceDifficulty(network::Packet& packet); void handleArenaTeamCommandResult(network::Packet& packet); void handleArenaTeamQueryResponse(network::Packet& packet); + void handleArenaTeamRoster(network::Packet& packet); void handleArenaTeamInvite(network::Packet& packet); void handleArenaTeamEvent(network::Packet& packet); void handleArenaTeamStats(network::Packet& packet); @@ -2454,7 +2478,9 @@ private: std::vector instanceLockouts_; // Arena team stats (indexed by team slot, updated by SMSG_ARENA_TEAM_STATS) - std::vector arenaTeamStats_; + std::vector arenaTeamStats_; + // Arena team rosters (updated by SMSG_ARENA_TEAM_ROSTER) + std::vector arenaTeamRosters_; // BG scoreboard (MSG_PVP_LOG_DATA) BgScoreboardData bgScoreboard_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c11f3607..00e2fd2f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5113,7 +5113,7 @@ void GameHandler::handlePacket(network::Packet& packet) { handleArenaTeamQueryResponse(packet); break; case Opcode::SMSG_ARENA_TEAM_ROSTER: - LOG_INFO("Received SMSG_ARENA_TEAM_ROSTER"); + handleArenaTeamRoster(packet); break; case Opcode::SMSG_ARENA_TEAM_INVITE: handleArenaTeamInvite(packet); @@ -13692,6 +13692,70 @@ void GameHandler::handleArenaTeamQueryResponse(network::Packet& packet) { LOG_INFO("Arena team query response: id=", teamId, " name=", teamName); } +void GameHandler::handleArenaTeamRoster(network::Packet& packet) { + // SMSG_ARENA_TEAM_ROSTER (WotLK 3.3.5a): + // uint32 teamId + // uint8 unk (0 = not captainship packet) + // uint32 memberCount + // For each member: + // uint64 guid + // uint8 online (1=online, 0=offline) + // string name (null-terminated) + // uint32 gamesWeek + // uint32 winsWeek + // uint32 gamesSeason + // uint32 winsSeason + // uint32 personalRating + // float modDay (unused here) + // float modWeek (unused here) + if (packet.getSize() - packet.getReadPos() < 9) return; + + uint32_t teamId = packet.readUInt32(); + /*uint8_t unk =*/ packet.readUInt8(); + uint32_t memberCount = packet.readUInt32(); + + // Sanity cap to avoid huge allocations from malformed packets + if (memberCount > 100) memberCount = 100; + + ArenaTeamRoster roster; + roster.teamId = teamId; + roster.members.reserve(memberCount); + + for (uint32_t i = 0; i < memberCount; ++i) { + if (packet.getSize() - packet.getReadPos() < 12) break; + + ArenaTeamMember m; + m.guid = packet.readUInt64(); + m.online = (packet.readUInt8() != 0); + m.name = packet.readString(); + if (packet.getSize() - packet.getReadPos() < 20) break; + m.weekGames = packet.readUInt32(); + m.weekWins = packet.readUInt32(); + m.seasonGames = packet.readUInt32(); + m.seasonWins = packet.readUInt32(); + m.personalRating = packet.readUInt32(); + // skip 2 floats (modDay, modWeek) + if (packet.getSize() - packet.getReadPos() >= 8) { + packet.readFloat(); + packet.readFloat(); + } + roster.members.push_back(std::move(m)); + } + + // Replace existing roster for this team or append + for (auto& r : arenaTeamRosters_) { + if (r.teamId == teamId) { + r = std::move(roster); + LOG_INFO("SMSG_ARENA_TEAM_ROSTER: updated teamId=", teamId, + " members=", r.members.size()); + return; + } + } + LOG_INFO("SMSG_ARENA_TEAM_ROSTER: new teamId=", teamId, + " members=", roster.members.size()); + arenaTeamRosters_.push_back(std::move(roster)); +} + void GameHandler::handleArenaTeamInvite(network::Packet& packet) { std::string playerName = packet.readString(); std::string teamName = packet.readString(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index a711ae9f..e53d6748 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -11854,11 +11854,11 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } - // ---- Arena tab (WotLK: shows per-team rating/record) ---- + // ---- Arena tab (WotLK: shows per-team rating/record + roster) ---- const auto& arenaStats = gameHandler.getArenaTeamStats(); if (!arenaStats.empty()) { if (ImGui::BeginTabItem("Arena")) { - ImGui::BeginChild("##ArenaList", ImVec2(200, 200), false); + ImGui::BeginChild("##ArenaList", ImVec2(0, 0), false); for (size_t ai = 0; ai < arenaStats.size(); ++ai) { const auto& ts = arenaStats[ai]; @@ -11887,6 +11887,49 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ? ts.seasonGames - ts.seasonWins : 0; ImGui::Text("Season: %u W / %u L", ts.seasonWins, seasLosses); + // Roster members (from SMSG_ARENA_TEAM_ROSTER) + const auto* roster = gameHandler.getArenaTeamRoster(ts.teamId); + if (roster && !roster->members.empty()) { + ImGui::Spacing(); + ImGui::TextDisabled("-- Roster (%zu members) --", + roster->members.size()); + // Column headers + ImGui::Columns(4, "##arenaRosterCols", false); + ImGui::SetColumnWidth(0, 110.0f); + ImGui::SetColumnWidth(1, 60.0f); + ImGui::SetColumnWidth(2, 60.0f); + ImGui::SetColumnWidth(3, 60.0f); + ImGui::TextDisabled("Name"); ImGui::NextColumn(); + ImGui::TextDisabled("Rating"); ImGui::NextColumn(); + ImGui::TextDisabled("Week"); ImGui::NextColumn(); + ImGui::TextDisabled("Season"); ImGui::NextColumn(); + ImGui::Separator(); + + for (const auto& m : roster->members) { + // Name coloured green (online) or grey (offline) + if (m.online) + ImGui::TextColored(ImVec4(0.4f,1.0f,0.4f,1.0f), + "%s", m.name.c_str()); + else + ImGui::TextDisabled("%s", m.name.c_str()); + ImGui::NextColumn(); + + ImGui::Text("%u", m.personalRating); + ImGui::NextColumn(); + + uint32_t wL = m.weekGames > m.weekWins + ? m.weekGames - m.weekWins : 0; + ImGui::Text("%uW/%uL", m.weekWins, wL); + ImGui::NextColumn(); + + uint32_t sL = m.seasonGames > m.seasonWins + ? m.seasonGames - m.seasonWins : 0; + ImGui::Text("%uW/%uL", m.seasonWins, sL); + ImGui::NextColumn(); + } + ImGui::Columns(1); + } + ImGui::Unindent(8.0f); if (ai + 1 < arenaStats.size())