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:
Kelsi 2026-03-12 12:02:59 -07:00
parent a4c23b7fa2
commit 79c0887db2
4 changed files with 249 additions and 2 deletions

View file

@ -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");