From 20cdff0790709aceaee35b5f4fc93d02c995aac5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 19 Feb 2026 17:45:09 -0800 Subject: [PATCH] Fix armor stat in character stats panel via UNIT_FIELD_RESISTANCES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The character stats panel was showing Armor: 0 because summing armor from item query responses is fragile (depends on correct BuyCount/damage block parsing). Instead, read the server-authoritative total armor directly from UNIT_FIELD_RESISTANCES (physical resistance, index 0) in the player entity update fields. Added UNIT_FIELD_RESISTANCES to the UF enum and all four expansion JSON files with correct wire indices: WotLK 3.3.5a: 99 (NPC_FLAGS=82 + emotestate + stat×5 + posstat×5 + negstat×5) TBC 2.4.3: 185 (NPC_FLAGS=168 + same relative layout) Classic 1.12: 154 (NPC_FLAGS=147 + emotestate + stat×5, no posstat/negstat) Turtle WoW: 154 (same as Classic) Stats panel uses server armor when > 0, falls back to summed item-query armor otherwise. Armor rating resets to 0 on world entry and is updated from both CREATE_OBJECT and VALUES update blocks. --- Data/expansions/classic/update_fields.json | 1 + Data/expansions/tbc/update_fields.json | 1 + Data/expansions/turtle/update_fields.json | 1 + Data/expansions/wotlk/update_fields.json | 1 + include/game/game_handler.hpp | 4 ++++ include/game/update_field_table.hpp | 1 + include/ui/inventory_screen.hpp | 2 +- src/game/game_handler.cpp | 10 ++++++++++ src/game/update_field_table.cpp | 2 ++ src/ui/inventory_screen.cpp | 15 +++++++++++---- 10 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json index de9aa094..f0a21619 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -15,6 +15,7 @@ "UNIT_FIELD_AURAS": 50, "UNIT_NPC_FLAGS": 147, "UNIT_DYNAMIC_FLAGS": 143, + "UNIT_FIELD_RESISTANCES": 154, "UNIT_END": 188, "PLAYER_FLAGS": 190, "PLAYER_BYTES": 191, diff --git a/Data/expansions/tbc/update_fields.json b/Data/expansions/tbc/update_fields.json index c4335ad3..2a1aea0e 100644 --- a/Data/expansions/tbc/update_fields.json +++ b/Data/expansions/tbc/update_fields.json @@ -15,6 +15,7 @@ "UNIT_FIELD_MOUNTDISPLAYID": 154, "UNIT_NPC_FLAGS": 168, "UNIT_DYNAMIC_FLAGS": 164, + "UNIT_FIELD_RESISTANCES": 185, "UNIT_END": 234, "PLAYER_FLAGS": 236, "PLAYER_BYTES": 237, diff --git a/Data/expansions/turtle/update_fields.json b/Data/expansions/turtle/update_fields.json index de9aa094..f0a21619 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -15,6 +15,7 @@ "UNIT_FIELD_AURAS": 50, "UNIT_NPC_FLAGS": 147, "UNIT_DYNAMIC_FLAGS": 143, + "UNIT_FIELD_RESISTANCES": 154, "UNIT_END": 188, "PLAYER_FLAGS": 190, "PLAYER_BYTES": 191, diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index 93855773..c26aefe6 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -15,6 +15,7 @@ "UNIT_FIELD_MOUNTDISPLAYID": 69, "UNIT_NPC_FLAGS": 82, "UNIT_DYNAMIC_FLAGS": 147, + "UNIT_FIELD_RESISTANCES": 99, "UNIT_END": 148, "PLAYER_FLAGS": 150, "PLAYER_BYTES": 151, diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f1b92b5f..e60dd066 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -273,6 +273,9 @@ public: // Money (copper) uint64_t getMoneyCopper() const { return playerMoneyCopper_; } + // Server-authoritative armor (UNIT_FIELD_RESISTANCES[0]) + int32_t getArmorRating() const { return playerArmorRating_; } + // Inventory Inventory& getInventory() { return inventory; } const Inventory& getInventory() const { return inventory; } @@ -1435,6 +1438,7 @@ private: float pendingLootMoneyNotifyTimer_ = 0.0f; std::unordered_map recentLootMoneyAnnounceCooldowns_; uint64_t playerMoneyCopper_ = 0; + int32_t playerArmorRating_ = 0; // Some servers/custom clients shift update field indices. We can auto-detect coinage by correlating // money-notify deltas with update-field diffs and then overriding UF::PLAYER_FIELD_COINAGE at runtime. uint32_t pendingMoneyDelta_ = 0; diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 29293cc9..e735d9d3 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -32,6 +32,7 @@ enum class UF : uint16_t { UNIT_FIELD_AURAS, // Start of aura spell ID array (48 consecutive uint32 slots, classic/vanilla only) UNIT_NPC_FLAGS, UNIT_DYNAMIC_FLAGS, + UNIT_FIELD_RESISTANCES, // Physical armor (index 0 of the resistance array) UNIT_END, // Player fields diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 698d1c32..0d1c03fb 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -131,7 +131,7 @@ private: int bagIndex, float defaultX, float defaultY, uint64_t moneyCopper); void renderEquipmentPanel(game::Inventory& inventory); void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false); - void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel); + void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0); // Slot rendering with interaction support enum class SlotKind { BACKPACK, EQUIPMENT }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5dd1e0db..9ea5c811 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2772,6 +2772,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { lastPlayerFields_.clear(); onlineEquipDirty_ = false; playerMoneyCopper_ = 0; + playerArmorRating_ = 0; knownSpells.clear(); spellCooldowns.clear(); actionBar = {}; @@ -4427,6 +4428,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP); const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); + const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); for (const auto& [key, val] : block.fields) { if (key == ufPlayerXp) { playerXp_ = val; } else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; } @@ -4440,6 +4442,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { playerMoneyCopper_ = val; LOG_INFO("Money set from update fields: ", val, " copper"); } + else if (ufArmor != 0xFFFF && key == ufArmor) { + playerArmorRating_ = static_cast(val); + LOG_INFO("Armor rating from update fields: ", playerArmorRating_); + } // 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. @@ -4684,6 +4690,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS); + const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); for (const auto& [key, val] : block.fields) { if (key == ufPlayerXp) { playerXp_ = val; @@ -4711,6 +4718,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { playerMoneyCopper_ = val; LOG_INFO("Money updated via VALUES: ", val, " copper"); } + else if (ufArmor != 0xFFFF && key == ufArmor) { + playerArmorRating_ = static_cast(val); + } else if (key == ufPlayerFlags) { constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; bool wasGhost = releasedSpirit_; diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index a46dc89f..33bf038e 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -35,6 +35,7 @@ static const UFNameEntry kUFNames[] = { {"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS}, {"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS}, {"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS}, + {"UNIT_FIELD_RESISTANCES", UF::UNIT_FIELD_RESISTANCES}, {"UNIT_END", UF::UNIT_END}, {"PLAYER_FLAGS", UF::PLAYER_FLAGS}, {"PLAYER_BYTES", UF::PLAYER_BYTES}, @@ -76,6 +77,7 @@ void UpdateFieldTable::loadWotlkDefaults() { {UF::UNIT_FIELD_MOUNTDISPLAYID, 69}, {UF::UNIT_NPC_FLAGS, 82}, {UF::UNIT_DYNAMIC_FLAGS, 147}, + {UF::UNIT_FIELD_RESISTANCES, 99}, {UF::UNIT_END, 148}, {UF::PLAYER_FLAGS, 150}, {UF::PLAYER_BYTES, 151}, diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 45536ca1..99890e33 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1066,7 +1066,7 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { if (ImGui::BeginTabItem("Stats")) { ImGui::Spacing(); - renderStatsPanel(inventory, gameHandler.getPlayerLevel()); + renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating()); ImGui::EndTabItem(); } @@ -1269,15 +1269,13 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) { // Stats Panel // ============================================================ -void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel) { +void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor) { // Sum equipment stats - int32_t totalArmor = 0; int32_t totalStr = 0, totalAgi = 0, totalSta = 0, totalInt = 0, totalSpi = 0; for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { const auto& slot = inventory.getEquipSlot(static_cast(s)); if (slot.empty()) continue; - totalArmor += slot.item.armor; totalStr += slot.item.strength; totalAgi += slot.item.agility; totalSta += slot.item.stamina; @@ -1285,6 +1283,15 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play totalSpi += slot.item.spirit; } + // Use server-authoritative armor from UNIT_FIELD_RESISTANCES when available. + // Falls back to summing item query armors if server armor wasn't received yet. + int32_t itemQueryArmor = 0; + for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { + const auto& slot = inventory.getEquipSlot(static_cast(s)); + if (!slot.empty()) itemQueryArmor += slot.item.armor; + } + int32_t totalArmor = (serverArmor > 0) ? serverArmor : itemQueryArmor; + // Base stats: 20 + level int32_t baseStat = 20 + static_cast(playerLevel);