feat: track UNIT_FIELD_STAT0-4 from server update fields for accurate character stats

Add UNIT_FIELD_STAT0-4 (STR/AGI/STA/INT/SPI) to the UF enum and wire up
per-expansion indices in all four expansion JSON files (WotLK: 84-88,
Classic/Turtle: 138-142, TBC: 159-163). Read the values in both CREATE
and VALUES player update handlers and store in playerStats_[5].

renderStatsPanel now uses the server-authoritative totals when available,
falling back to the previous 20+level estimate only if the server hasn't
sent UNIT_FIELD_STAT* yet. Item-query bonuses are still shown as (+N)
alongside the server total for both paths.
This commit is contained in:
Kelsi 2026-03-10 23:08:15 -07:00
parent d95abfb607
commit 99de1fa3e5
10 changed files with 115 additions and 30 deletions

View file

@ -6098,6 +6098,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
onlineEquipDirty_ = false;
playerMoneyCopper_ = 0;
playerArmorRating_ = 0;
std::fill(std::begin(playerStats_), std::end(playerStats_), -1);
knownSpells.clear();
spellCooldowns.clear();
actionBar = {};
@ -8142,6 +8143,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
const uint16_t ufPBytes2 = fieldIndex(UF::PLAYER_BYTES_2);
const uint16_t ufStats[5] = {
fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1),
fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3),
fieldIndex(UF::UNIT_FIELD_STAT4)
};
for (const auto& [key, val] : block.fields) {
if (key == ufPlayerXp) { playerXp_ = val; }
else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; }
@ -8170,6 +8176,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
isResting_ = (restStateByte != 0);
}
else {
for (int si = 0; si < 5; ++si) {
if (ufStats[si] != 0xFFFF && key == ufStats[si]) {
playerStats_[si] = static_cast<int32_t>(val);
break;
}
}
}
// Do not synthesize quest-log entries from raw update-field slots.
// Slot layouts differ on some classic-family realms and can produce
// phantom "already accepted" quests that block quest acceptance.
@ -8454,6 +8468,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2);
const uint16_t ufStatsV[5] = {
fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1),
fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3),
fieldIndex(UF::UNIT_FIELD_STAT4)
};
for (const auto& [key, val] : block.fields) {
if (key == ufPlayerXp) {
playerXp_ = val;
@ -8510,6 +8529,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if (ghostStateCallback_) ghostStateCallback_(false);
}
}
else {
for (int si = 0; si < 5; ++si) {
if (ufStatsV[si] != 0xFFFF && key == ufStatsV[si]) {
playerStats_[si] = static_cast<int32_t>(val);
break;
}
}
}
}
// Do not auto-create quests from VALUES quest-log slot fields for the
// same reason as CREATE_OBJECT2 above (can be misaligned per realm).