From a1f73fdd3948df38635f8f275bca44309a087c18 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 26 Feb 2026 10:25:55 -0800 Subject: [PATCH] Handle SMSG_PARTY_MEMBER_STATS to show group health out of visual range Parse SMSG_PARTY_MEMBER_STATS and SMSG_PARTY_MEMBER_STATS_FULL packets so party frames display health, power, level, and online status even when group members are not nearby. Expansion-aware field sizes: uint16 health for Classic/TBC, uint32 for WotLK, plus per-expansion aura and vehicle seat handling. --- include/game/game_handler.hpp | 1 + include/game/group_defines.hpp | 13 +++ src/game/game_handler.cpp | 174 +++++++++++++++++++++++++++++++++ src/ui/game_screen.cpp | 64 +++++++++--- 4 files changed, 237 insertions(+), 15 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 181a167a..57949bcd 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1127,6 +1127,7 @@ private: void handleGroupList(network::Packet& packet); void handleGroupUninvite(network::Packet& packet); void handlePartyCommandResult(network::Packet& packet); + void handlePartyMemberStats(network::Packet& packet, bool isFull); // ---- Guild handlers ---- void handleGuildInfo(network::Packet& packet); diff --git a/include/game/group_defines.hpp b/include/game/group_defines.hpp index 60f1a9f0..0daed84b 100644 --- a/include/game/group_defines.hpp +++ b/include/game/group_defines.hpp @@ -17,6 +17,19 @@ struct GroupMember { uint8_t subGroup = 0; // Raid subgroup (0 for party) uint8_t flags = 0; // Assistant, main tank, etc. uint8_t roles = 0; // LFG roles (3.3.5a) + + // Party member stats (from SMSG_PARTY_MEMBER_STATS) + uint32_t curHealth = 0; + uint32_t maxHealth = 0; + uint8_t powerType = 0; + uint16_t curPower = 0; + uint16_t maxPower = 0; + uint16_t level = 0; + uint16_t zoneId = 0; + int16_t posX = 0; + int16_t posY = 0; + uint16_t onlineStatus = 0; // GROUP_UPDATE_FLAG_STATUS bitmask + bool hasPartyStats = false; // true once we've received stats }; /** diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4910f3cf..d48f80ef 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1789,6 +1789,12 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_PARTY_COMMAND_RESULT: handlePartyCommandResult(packet); break; + case Opcode::SMSG_PARTY_MEMBER_STATS: + handlePartyMemberStats(packet, false); + break; + case Opcode::SMSG_PARTY_MEMBER_STATS_FULL: + handlePartyMemberStats(packet, true); + break; case Opcode::MSG_RAID_READY_CHECK: // Server ready-check prompt (minimal handling for now). packet.setReadPos(packet.getSize()); @@ -9713,6 +9719,174 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) { } } +void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { + auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); }; + + // Classic/TBC use uint16 for health fields and simpler aura format; + // WotLK uses uint32 health and uint32+uint8 per aura. + const bool isWotLK = isActiveExpansion("wotlk"); + + // SMSG_PARTY_MEMBER_STATS_FULL has a leading padding byte + if (isFull) { + if (remaining() < 1) return; + packet.readUInt8(); + } + + uint64_t memberGuid = UpdateObjectParser::readPackedGuid(packet); + if (remaining() < 4) return; + uint32_t updateFlags = packet.readUInt32(); + + // Find matching group member + game::GroupMember* member = nullptr; + for (auto& m : partyData.members) { + if (m.guid == memberGuid) { + member = &m; + break; + } + } + if (!member) { + packet.setReadPos(packet.getSize()); + return; + } + + // Parse each flag field in order + if (updateFlags & 0x0001) { // STATUS + if (remaining() >= 2) + member->onlineStatus = packet.readUInt16(); + } + if (updateFlags & 0x0002) { // CUR_HP + if (isWotLK) { + if (remaining() >= 4) + member->curHealth = packet.readUInt32(); + } else { + if (remaining() >= 2) + member->curHealth = packet.readUInt16(); + } + } + if (updateFlags & 0x0004) { // MAX_HP + if (isWotLK) { + if (remaining() >= 4) + member->maxHealth = packet.readUInt32(); + } else { + if (remaining() >= 2) + member->maxHealth = packet.readUInt16(); + } + } + if (updateFlags & 0x0008) { // POWER_TYPE + if (remaining() >= 1) + member->powerType = packet.readUInt8(); + } + if (updateFlags & 0x0010) { // CUR_POWER + if (remaining() >= 2) + member->curPower = packet.readUInt16(); + } + if (updateFlags & 0x0020) { // MAX_POWER + if (remaining() >= 2) + member->maxPower = packet.readUInt16(); + } + if (updateFlags & 0x0040) { // LEVEL + if (remaining() >= 2) + member->level = packet.readUInt16(); + } + if (updateFlags & 0x0080) { // ZONE + if (remaining() >= 2) + member->zoneId = packet.readUInt16(); + } + if (updateFlags & 0x0100) { // POSITION + if (remaining() >= 4) { + member->posX = static_cast(packet.readUInt16()); + member->posY = static_cast(packet.readUInt16()); + } + } + if (updateFlags & 0x0200) { // AURAS + if (remaining() >= 8) { + uint64_t auraMask = packet.readUInt64(); + for (int i = 0; i < 64; ++i) { + if (auraMask & (uint64_t(1) << i)) { + if (isWotLK) { + // WotLK: uint32 spellId + uint8 auraFlags + if (remaining() < 5) break; + packet.readUInt32(); + packet.readUInt8(); + } else { + // Classic/TBC: uint16 spellId only + if (remaining() < 2) break; + packet.readUInt16(); + } + } + } + } + } + if (updateFlags & 0x0400) { // PET_GUID + if (remaining() >= 8) + packet.readUInt64(); + } + if (updateFlags & 0x0800) { // PET_NAME + if (remaining() > 0) + packet.readString(); + } + if (updateFlags & 0x1000) { // PET_MODEL_ID + if (remaining() >= 2) + packet.readUInt16(); + } + if (updateFlags & 0x2000) { // PET_CUR_HP + if (isWotLK) { + if (remaining() >= 4) + packet.readUInt32(); + } else { + if (remaining() >= 2) + packet.readUInt16(); + } + } + if (updateFlags & 0x4000) { // PET_MAX_HP + if (isWotLK) { + if (remaining() >= 4) + packet.readUInt32(); + } else { + if (remaining() >= 2) + packet.readUInt16(); + } + } + if (updateFlags & 0x8000) { // PET_POWER_TYPE + if (remaining() >= 1) + packet.readUInt8(); + } + if (updateFlags & 0x10000) { // PET_CUR_POWER + if (remaining() >= 2) + packet.readUInt16(); + } + if (updateFlags & 0x20000) { // PET_MAX_POWER + if (remaining() >= 2) + packet.readUInt16(); + } + if (updateFlags & 0x40000) { // PET_AURAS + if (remaining() >= 8) { + uint64_t petAuraMask = packet.readUInt64(); + for (int i = 0; i < 64; ++i) { + if (petAuraMask & (uint64_t(1) << i)) { + if (isWotLK) { + if (remaining() < 5) break; + packet.readUInt32(); + packet.readUInt8(); + } else { + if (remaining() < 2) break; + packet.readUInt16(); + } + } + } + } + } + if (isWotLK && (updateFlags & 0x80000)) { // VEHICLE_SEAT (WotLK only) + if (remaining() >= 4) + packet.readUInt32(); + } + + member->hasPartyStats = true; + LOG_DEBUG("Party member stats for ", member->name, + ": HP=", member->curHealth, "/", member->maxHealth, + " Level=", member->level); +} + // ============================================================ // Guild Handlers // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 1b6b2609..d8efa743 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4213,27 +4213,61 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { for (const auto& member : partyData.members) { ImGui::PushID(static_cast(member.guid)); + // Name with level and status info + std::string label = member.name; + if (member.hasPartyStats && member.level > 0) { + label += " [" + std::to_string(member.level) + "]"; + } + if (member.hasPartyStats) { + bool isOnline = (member.onlineStatus & 0x0001) != 0; + bool isDead = (member.onlineStatus & 0x0020) != 0; + bool isGhost = (member.onlineStatus & 0x0010) != 0; + if (!isOnline) label += " (offline)"; + else if (isDead || isGhost) label += " (dead)"; + } + // Clickable name to target - if (ImGui::Selectable(member.name.c_str(), gameHandler.getTargetGuid() == member.guid)) { + if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) { gameHandler.setTarget(member.guid); } - // Try to show health from entity - auto entity = gameHandler.getEntityManager().getEntity(member.guid); - if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) { - auto unit = std::static_pointer_cast(entity); - uint32_t hp = unit->getHealth(); - uint32_t maxHp = unit->getMaxHealth(); - if (maxHp > 0) { - float pct = static_cast(hp) / static_cast(maxHp); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, - pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : - pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : - ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::ProgressBar(pct, ImVec2(-1, 12), ""); - ImGui::PopStyleColor(); + // Health bar: prefer party stats, fall back to entity + uint32_t hp = 0, maxHp = 0; + if (member.hasPartyStats && member.maxHealth > 0) { + hp = member.curHealth; + maxHp = member.maxHealth; + } else { + auto entity = gameHandler.getEntityManager().getEntity(member.guid); + if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) { + auto unit = std::static_pointer_cast(entity); + hp = unit->getHealth(); + maxHp = unit->getMaxHealth(); } } + if (maxHp > 0) { + float pct = static_cast(hp) / static_cast(maxHp); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, + pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : + pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : + ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::ProgressBar(pct, ImVec2(-1, 12), ""); + ImGui::PopStyleColor(); + } + + // Power bar (mana/rage/energy) from party stats + if (member.hasPartyStats && member.maxPower > 0) { + float powerPct = static_cast(member.curPower) / static_cast(member.maxPower); + ImVec4 powerColor; + switch (member.powerType) { + case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue) + case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red) + case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow) + default: powerColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); break; + } + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor); + ImGui::ProgressBar(powerPct, ImVec2(-1, 8), ""); + ImGui::PopStyleColor(); + } ImGui::Separator(); ImGui::PopID();