mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: add BG scoreboard (MSG_PVP_LOG_DATA) and fix TBC aura cache for party frames
- Parse MSG_PVP_LOG_DATA to populate BgScoreboardData (players, KB, deaths, HKs, honor, BG-specific stats, winner) - Add /score command to request the scorecard while in a battleground - Render sortable per-player table with team color-coding and self-highlight - Refresh button re-requests live data from server - Fix TBC SMSG_INIT/SET_EXTRA_AURA_INFO_OBSOLETE to populate unitAurasCache_ for all GUIDs (not just player/target), mirroring WotLK aura update behavior so party frame debuff dots work on TBC servers
This commit is contained in:
parent
a4c23b7fa2
commit
79c0887db2
4 changed files with 249 additions and 2 deletions
|
|
@ -393,6 +393,28 @@ public:
|
|||
void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
||||
|
||||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||||
struct BgPlayerScore {
|
||||
uint64_t guid = 0;
|
||||
std::string name;
|
||||
uint8_t team = 0; // 0=Horde, 1=Alliance
|
||||
uint32_t killingBlows = 0;
|
||||
uint32_t deaths = 0;
|
||||
uint32_t honorableKills = 0;
|
||||
uint32_t bonusHonor = 0;
|
||||
std::vector<std::pair<std::string, uint32_t>> bgStats; // BG-specific fields
|
||||
};
|
||||
struct BgScoreboardData {
|
||||
std::vector<BgPlayerScore> players;
|
||||
bool hasWinner = false;
|
||||
uint8_t winner = 0; // 0=Horde, 1=Alliance
|
||||
bool isArena = false;
|
||||
};
|
||||
void requestPvpLog();
|
||||
const BgScoreboardData* getBgScoreboard() const {
|
||||
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
|
||||
}
|
||||
|
||||
// Network latency (milliseconds, updated each PONG response)
|
||||
uint32_t getLatencyMs() const { return lastLatency; }
|
||||
|
||||
|
|
@ -1921,6 +1943,7 @@ private:
|
|||
void handleArenaTeamEvent(network::Packet& packet);
|
||||
void handleArenaTeamStats(network::Packet& packet);
|
||||
void handleArenaError(network::Packet& packet);
|
||||
void handlePvpLogData(network::Packet& packet);
|
||||
|
||||
// ---- Bank handlers ----
|
||||
void handleShowBank(network::Packet& packet);
|
||||
|
|
@ -2283,6 +2306,9 @@ private:
|
|||
// Arena team stats (indexed by team slot, updated by SMSG_ARENA_TEAM_STATS)
|
||||
std::vector<ArenaTeamStats> arenaTeamStats_;
|
||||
|
||||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||||
BgScoreboardData bgScoreboard_;
|
||||
|
||||
// Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||||
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
||||
|
||||
|
|
|
|||
|
|
@ -434,6 +434,10 @@ private:
|
|||
// Threat window
|
||||
bool showThreatWindow_ = false;
|
||||
void renderThreatWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// BG scoreboard window
|
||||
bool showBgScoreboard_ = false;
|
||||
void renderBgScoreboard(game::GameHandler& gameHandler);
|
||||
uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps)
|
||||
uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK)
|
||||
|
||||
|
|
|
|||
|
|
@ -4988,7 +4988,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleArenaError(packet);
|
||||
break;
|
||||
case Opcode::MSG_PVP_LOG_DATA:
|
||||
LOG_INFO("Received MSG_PVP_LOG_DATA");
|
||||
handlePvpLogData(packet);
|
||||
break;
|
||||
case Opcode::MSG_INSPECT_ARENA_TEAMS:
|
||||
LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS");
|
||||
|
|
@ -5207,6 +5207,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
std::vector<AuraSlot>* auraList = nullptr;
|
||||
if (auraTargetGuid == playerGuid) auraList = &playerAuras;
|
||||
else if (auraTargetGuid == targetGuid) auraList = &targetAuras;
|
||||
else if (auraTargetGuid != 0) auraList = &unitAurasCache_[auraTargetGuid];
|
||||
|
||||
if (auraList && isInit) auraList->clear();
|
||||
|
||||
|
|
@ -13467,6 +13468,80 @@ void GameHandler::handleArenaError(network::Packet& packet) {
|
|||
LOG_INFO("Arena error: ", error, " - ", msg);
|
||||
}
|
||||
|
||||
void GameHandler::requestPvpLog() {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
// MSG_PVP_LOG_DATA is bidirectional: client sends an empty packet to request
|
||||
network::Packet pkt(wireOpcode(Opcode::MSG_PVP_LOG_DATA));
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Requested PvP log data");
|
||||
}
|
||||
|
||||
void GameHandler::handlePvpLogData(network::Packet& packet) {
|
||||
auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||||
if (remaining() < 1) return;
|
||||
|
||||
bgScoreboard_ = BgScoreboardData{};
|
||||
bgScoreboard_.isArena = (packet.readUInt8() != 0);
|
||||
|
||||
if (bgScoreboard_.isArena) {
|
||||
// Skip arena-specific header (two teams × (rating change uint32 + name string + 5×uint32))
|
||||
// Rather than hardcoding arena parse we skip gracefully up to playerCount
|
||||
// Each arena team block: uint32 + string + uint32*5 — variable length due to string.
|
||||
// Skip by scanning for the uint32 playerCount heuristically; simply consume rest.
|
||||
packet.setReadPos(packet.getSize());
|
||||
return;
|
||||
}
|
||||
|
||||
if (remaining() < 4) return;
|
||||
uint32_t playerCount = packet.readUInt32();
|
||||
bgScoreboard_.players.reserve(playerCount);
|
||||
|
||||
for (uint32_t i = 0; i < playerCount && remaining() >= 13; ++i) {
|
||||
BgPlayerScore ps;
|
||||
ps.guid = packet.readUInt64();
|
||||
ps.team = packet.readUInt8();
|
||||
ps.killingBlows = packet.readUInt32();
|
||||
ps.honorableKills = packet.readUInt32();
|
||||
ps.deaths = packet.readUInt32();
|
||||
ps.bonusHonor = packet.readUInt32();
|
||||
|
||||
// Resolve player name from entity manager
|
||||
{
|
||||
auto ent = entityManager.getEntity(ps.guid);
|
||||
if (ent && (ent->getType() == game::ObjectType::PLAYER ||
|
||||
ent->getType() == game::ObjectType::UNIT)) {
|
||||
auto u = std::static_pointer_cast<game::Unit>(ent);
|
||||
if (!u->getName().empty()) ps.name = u->getName();
|
||||
}
|
||||
}
|
||||
|
||||
// BG-specific stat blocks: uint32 count + N × (string fieldName + uint32 value)
|
||||
if (remaining() < 4) { bgScoreboard_.players.push_back(std::move(ps)); break; }
|
||||
uint32_t statCount = packet.readUInt32();
|
||||
for (uint32_t s = 0; s < statCount && remaining() >= 5; ++s) {
|
||||
std::string fieldName;
|
||||
while (remaining() > 0) {
|
||||
char c = static_cast<char>(packet.readUInt8());
|
||||
if (c == '\0') break;
|
||||
fieldName += c;
|
||||
}
|
||||
uint32_t val = (remaining() >= 4) ? packet.readUInt32() : 0;
|
||||
ps.bgStats.emplace_back(std::move(fieldName), val);
|
||||
}
|
||||
|
||||
bgScoreboard_.players.push_back(std::move(ps));
|
||||
}
|
||||
|
||||
if (remaining() >= 1) {
|
||||
bgScoreboard_.hasWinner = (packet.readUInt8() != 0);
|
||||
if (bgScoreboard_.hasWinner && remaining() >= 1)
|
||||
bgScoreboard_.winner = packet.readUInt8();
|
||||
}
|
||||
|
||||
LOG_INFO("PvP log: ", bgScoreboard_.players.size(), " players, hasWinner=",
|
||||
bgScoreboard_.hasWinner, " winner=", (int)bgScoreboard_.winner);
|
||||
}
|
||||
|
||||
void GameHandler::handleOtherPlayerMovement(network::Packet& packet) {
|
||||
// Server relays MSG_MOVE_* for other players: packed GUID (WotLK) or full uint64 (TBC/Classic)
|
||||
const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||
|
|
|
|||
|
|
@ -611,6 +611,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderGmTicketWindow(gameHandler);
|
||||
renderInspectWindow(gameHandler);
|
||||
renderThreatWindow(gameHandler);
|
||||
renderBgScoreboard(gameHandler);
|
||||
renderObjectiveTracker(gameHandler);
|
||||
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||
if (showMinimap_) {
|
||||
|
|
@ -3979,6 +3980,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// /score command — BG scoreboard
|
||||
if (cmdLower == "score") {
|
||||
gameHandler.requestPvpLog();
|
||||
showBgScoreboard_ = true;
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// /time command
|
||||
if (cmdLower == "time") {
|
||||
gameHandler.queryServerTime();
|
||||
|
|
@ -4065,7 +4074,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
"Movement: /sit /stand /kneel /dismount",
|
||||
"Misc: /played /time /zone /afk [msg] /dnd [msg] /inspect",
|
||||
" /helm /cloak /trade /join <channel> /leave <channel>",
|
||||
" /unstuck /logout /ticket /help",
|
||||
" /score /unstuck /logout /ticket /help",
|
||||
};
|
||||
for (const char* line : kHelpLines) {
|
||||
game::MessageChatData helpMsg;
|
||||
|
|
@ -17738,6 +17747,139 @@ void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
// ─── BG Scoreboard ────────────────────────────────────────────────────────────
|
||||
void GameScreen::renderBgScoreboard(game::GameHandler& gameHandler) {
|
||||
if (!showBgScoreboard_) return;
|
||||
|
||||
const game::GameHandler::BgScoreboardData* data = gameHandler.getBgScoreboard();
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(150, 100), ImGuiCond_FirstUseEver);
|
||||
|
||||
const char* title = "Battleground Score###BgScore";
|
||||
if (!ImGui::Begin(title, &showBgScoreboard_, ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
ImGui::TextDisabled("No score data yet.");
|
||||
ImGui::TextDisabled("Use /score to request the scoreboard while in a battleground.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Winner banner
|
||||
if (data->hasWinner) {
|
||||
const char* winnerStr = (data->winner == 1) ? "Alliance" : "Horde";
|
||||
ImVec4 winnerColor = (data->winner == 1) ? ImVec4(0.4f, 0.6f, 1.0f, 1.0f)
|
||||
: ImVec4(1.0f, 0.35f, 0.35f, 1.0f);
|
||||
float textW = ImGui::CalcTextSize(winnerStr).x + ImGui::CalcTextSize(" Victory!").x;
|
||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - textW) * 0.5f);
|
||||
ImGui::TextColored(winnerColor, "%s", winnerStr);
|
||||
ImGui::SameLine(0, 4);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Victory!");
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
if (ImGui::SmallButton("Refresh")) {
|
||||
gameHandler.requestPvpLog();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%zu players", data->players.size());
|
||||
|
||||
// Score table
|
||||
constexpr ImGuiTableFlags kTableFlags =
|
||||
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV |
|
||||
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Sortable;
|
||||
|
||||
// Build dynamic column count based on what BG-specific stats are present
|
||||
int numBgCols = 0;
|
||||
std::vector<std::string> bgColNames;
|
||||
for (const auto& ps : data->players) {
|
||||
for (const auto& [fieldName, val] : ps.bgStats) {
|
||||
// Extract short name after last '.' (e.g. "BattlegroundAB.AbFlagCaptures" → "Caps")
|
||||
std::string shortName = fieldName;
|
||||
auto dotPos = fieldName.rfind('.');
|
||||
if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1);
|
||||
bool found = false;
|
||||
for (const auto& n : bgColNames) { if (n == shortName) { found = true; break; } }
|
||||
if (!found) bgColNames.push_back(shortName);
|
||||
}
|
||||
}
|
||||
numBgCols = static_cast<int>(bgColNames.size());
|
||||
|
||||
// Fixed cols: Team | Name | KB | Deaths | HKs | Honor; then BG-specific
|
||||
int totalCols = 6 + numBgCols;
|
||||
float tableH = ImGui::GetContentRegionAvail().y;
|
||||
if (ImGui::BeginTable("##BgScoreTable", totalCols, kTableFlags, ImVec2(0.0f, tableH))) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableSetupColumn("Team", ImGuiTableColumnFlags_WidthFixed, 56.0f);
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("KB", ImGuiTableColumnFlags_WidthFixed, 38.0f);
|
||||
ImGui::TableSetupColumn("Deaths", ImGuiTableColumnFlags_WidthFixed, 52.0f);
|
||||
ImGui::TableSetupColumn("HKs", ImGuiTableColumnFlags_WidthFixed, 38.0f);
|
||||
ImGui::TableSetupColumn("Honor", ImGuiTableColumnFlags_WidthFixed, 52.0f);
|
||||
for (const auto& col : bgColNames)
|
||||
ImGui::TableSetupColumn(col.c_str(), ImGuiTableColumnFlags_WidthFixed, 52.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// Sort: Alliance first, then Horde; within each team by KB desc
|
||||
std::vector<const game::GameHandler::BgPlayerScore*> sorted;
|
||||
sorted.reserve(data->players.size());
|
||||
for (const auto& ps : data->players) sorted.push_back(&ps);
|
||||
std::stable_sort(sorted.begin(), sorted.end(),
|
||||
[](const game::GameHandler::BgPlayerScore* a,
|
||||
const game::GameHandler::BgPlayerScore* b) {
|
||||
if (a->team != b->team) return a->team > b->team; // Alliance(1) first
|
||||
return a->killingBlows > b->killingBlows;
|
||||
});
|
||||
|
||||
uint64_t playerGuid = gameHandler.getPlayerGuid();
|
||||
for (const auto* ps : sorted) {
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// Team
|
||||
ImGui::TableNextColumn();
|
||||
if (ps->team == 1)
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.6f, 1.0f, 1.0f), "Alliance");
|
||||
else
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Horde");
|
||||
|
||||
// Name (highlight player's own row)
|
||||
ImGui::TableNextColumn();
|
||||
bool isSelf = (ps->guid == playerGuid);
|
||||
if (isSelf) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
||||
const char* nameStr = ps->name.empty() ? "Unknown" : ps->name.c_str();
|
||||
ImGui::TextUnformatted(nameStr);
|
||||
if (isSelf) ImGui::PopStyleColor();
|
||||
|
||||
ImGui::TableNextColumn(); ImGui::Text("%u", ps->killingBlows);
|
||||
ImGui::TableNextColumn(); ImGui::Text("%u", ps->deaths);
|
||||
ImGui::TableNextColumn(); ImGui::Text("%u", ps->honorableKills);
|
||||
ImGui::TableNextColumn(); ImGui::Text("%u", ps->bonusHonor);
|
||||
|
||||
for (const auto& col : bgColNames) {
|
||||
ImGui::TableNextColumn();
|
||||
uint32_t val = 0;
|
||||
for (const auto& [fieldName, fval] : ps->bgStats) {
|
||||
std::string shortName = fieldName;
|
||||
auto dotPos = fieldName.rfind('.');
|
||||
if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1);
|
||||
if (shortName == col) { val = fval; break; }
|
||||
}
|
||||
if (val > 0) ImGui::Text("%u", val);
|
||||
else ImGui::TextDisabled("-");
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ─── Quest Objective Tracker ──────────────────────────────────────────────────
|
||||
void GameScreen::renderObjectiveTracker(game::GameHandler& gameHandler) {
|
||||
if (gameHandler.getState() != game::WorldState::IN_WORLD) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue