From de5c1223073462b04e27c3ff983bfe2162211f3a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 23:30:44 -0700 Subject: [PATCH] feat: parse SMSG_SET_FACTION_ATWAR/VISIBLE and show at-war status in reputation panel - Parse SMSG_SET_FACTION_ATWAR (uint32 repListId + uint8 set) to track per-faction at-war flags in initialFactions_ flags byte - Parse SMSG_SET_FACTION_VISIBLE (uint32 repListId + uint8 visible) to track faction visibility changes from the server - Add FACTION_FLAG_* constants (VISIBLE, AT_WAR, HIDDEN, etc.) to GameHandler - Build repListId <-> factionId bidirectional maps when loading Faction.dbc (ReputationListID field 1); used to correlate flag packets with standings - Fix Faction.dbc field layout comment: field 1=ReputationListID, field 23=Name (was incorrectly documented as field 22 with no ReputationListID field) - Add isFactionAtWar(), isFactionVisible(), getFactionIdByRepListId(), getRepListIdByFactionId() accessors on GameHandler - Reputation panel now shows watched faction at top, highlights at-war factions in red with "(At War)" label, and marks tracked faction in gold --- include/game/game_handler.hpp | 29 +++++++++++ src/game/game_handler.cpp | 95 ++++++++++++++++++++++++++++------- src/ui/inventory_screen.cpp | 24 +++++++-- 3 files changed, 126 insertions(+), 22 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 930ae2e9..413cdb70 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1488,8 +1488,33 @@ public: uint8_t flags = 0; int32_t standing = 0; }; + // Faction flag bitmask constants (from Faction.dbc ReputationFlags / SMSG_INITIALIZE_FACTIONS) + static constexpr uint8_t FACTION_FLAG_VISIBLE = 0x01; // shown in reputation list + static constexpr uint8_t FACTION_FLAG_AT_WAR = 0x02; // player is at war + static constexpr uint8_t FACTION_FLAG_HIDDEN = 0x04; // never shown + static constexpr uint8_t FACTION_FLAG_INVISIBLE_FORCED = 0x08; + static constexpr uint8_t FACTION_FLAG_PEACE_FORCED = 0x10; + const std::vector& getInitialFactions() const { return initialFactions_; } const std::unordered_map& getFactionStandings() const { return factionStandings_; } + + // Returns true if the player has "at war" toggled for the faction at repListId + bool isFactionAtWar(uint32_t repListId) const { + if (repListId >= initialFactions_.size()) return false; + return (initialFactions_[repListId].flags & FACTION_FLAG_AT_WAR) != 0; + } + // Returns true if the faction is visible in the reputation list + bool isFactionVisible(uint32_t repListId) const { + if (repListId >= initialFactions_.size()) return false; + const uint8_t f = initialFactions_[repListId].flags; + if (f & FACTION_FLAG_HIDDEN) return false; + if (f & FACTION_FLAG_INVISIBLE_FORCED) return false; + return (f & FACTION_FLAG_VISIBLE) != 0; + } + // Returns the faction ID for a given repListId (0 if unknown) + uint32_t getFactionIdByRepListId(uint32_t repListId) const; + // Returns the repListId for a given faction ID (0xFFFFFFFF if not found) + uint32_t getRepListIdByFactionId(uint32_t factionId) const; // Shaman totems (4 slots: 0=Earth, 1=Fire, 2=Water, 3=Air) struct TotemSlot { uint32_t spellId = 0; @@ -2566,6 +2591,10 @@ private: std::unordered_map factionStandings_; // Faction name cache (factionId → name), populated lazily from Faction.dbc std::unordered_map factionNameCache_; + // repListId → factionId mapping (populated with factionNameCache) + std::unordered_map factionRepListToId_; + // factionId → repListId reverse mapping + std::unordered_map factionIdToRepList_; bool factionNameCacheLoaded_ = false; void loadFactionNameCache(); std::string getFactionName(uint32_t factionId) const; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a653c523..97de0b71 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3720,11 +3720,40 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; } - case Opcode::SMSG_SET_FACTION_ATWAR: - case Opcode::SMSG_SET_FACTION_VISIBLE: - // uint32 factionId [+ uint8 flags for ATWAR] — consume; hostility is tracked via update fields - packet.setReadPos(packet.getSize()); + case Opcode::SMSG_SET_FACTION_ATWAR: { + // uint32 repListId + uint8 set (1=set at-war, 0=clear at-war) + if (packet.getSize() - packet.getReadPos() < 5) { + packet.setReadPos(packet.getSize()); break; + } + uint32_t repListId = packet.readUInt32(); + uint8_t setAtWar = packet.readUInt8(); + if (repListId < initialFactions_.size()) { + if (setAtWar) + initialFactions_[repListId].flags |= FACTION_FLAG_AT_WAR; + else + initialFactions_[repListId].flags &= ~FACTION_FLAG_AT_WAR; + LOG_DEBUG("SMSG_SET_FACTION_ATWAR: repListId=", repListId, + " atWar=", (int)setAtWar); + } break; + } + case Opcode::SMSG_SET_FACTION_VISIBLE: { + // uint32 repListId + uint8 visible (1=show, 0=hide) + if (packet.getSize() - packet.getReadPos() < 5) { + packet.setReadPos(packet.getSize()); break; + } + uint32_t repListId = packet.readUInt32(); + uint8_t visible = packet.readUInt8(); + if (repListId < initialFactions_.size()) { + if (visible) + initialFactions_[repListId].flags |= FACTION_FLAG_VISIBLE; + else + initialFactions_[repListId].flags &= ~FACTION_FLAG_VISIBLE; + LOG_DEBUG("SMSG_SET_FACTION_VISIBLE: repListId=", repListId, + " visible=", (int)visible); + } + break; + } case Opcode::SMSG_FEATURE_SYSTEM_STATUS: case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: @@ -22153,32 +22182,60 @@ void GameHandler::loadFactionNameCache() { // Faction.dbc WotLK 3.3.5a field layout: // 0: ID - // 1-4: ReputationRaceMask[4] - // 5-8: ReputationClassMask[4] - // 9-12: ReputationBase[4] - // 13-16: ReputationFlags[4] - // 17: ParentFactionID - // 18-19: Spillover rates (floats) - // 20-21: MaxRank - // 22: Name (English locale, string ref) - constexpr uint32_t ID_FIELD = 0; - constexpr uint32_t NAME_FIELD = 22; // enUS name string + // 1: ReputationListID (-1 / 0xFFFFFFFF = no reputation tracking) + // 2-5: ReputationRaceMask[4] + // 6-9: ReputationClassMask[4] + // 10-13: ReputationBase[4] + // 14-17: ReputationFlags[4] + // 18: ParentFactionID + // 19-20: SpilloverRateIn, SpilloverRateOut (floats) + // 21-22: SpilloverMaxRankIn, SpilloverMaxRankOut + // 23: Name (English locale, string ref) + constexpr uint32_t ID_FIELD = 0; + constexpr uint32_t REPLIST_FIELD = 1; + constexpr uint32_t NAME_FIELD = 23; // enUS name string + // Classic/TBC have fewer fields; fall back gracefully + const bool hasRepListField = dbc->getFieldCount() > REPLIST_FIELD; if (dbc->getFieldCount() <= NAME_FIELD) { LOG_WARNING("Faction.dbc: unexpected field count ", dbc->getFieldCount()); - return; + // Don't abort — still try to load names from a shorter layout } + const uint32_t nameField = (dbc->getFieldCount() > NAME_FIELD) ? NAME_FIELD : 22u; uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t factionId = dbc->getUInt32(i, ID_FIELD); if (factionId == 0) continue; - std::string name = dbc->getString(i, NAME_FIELD); - if (!name.empty()) { - factionNameCache_[factionId] = std::move(name); + if (dbc->getFieldCount() > nameField) { + std::string name = dbc->getString(i, nameField); + if (!name.empty()) { + factionNameCache_[factionId] = std::move(name); + } + } + // Build repListId ↔ factionId mapping (WotLK field 1) + if (hasRepListField) { + uint32_t repListId = dbc->getUInt32(i, REPLIST_FIELD); + if (repListId != 0xFFFFFFFFu) { + factionRepListToId_[repListId] = factionId; + factionIdToRepList_[factionId] = repListId; + } } } - LOG_INFO("Faction.dbc: loaded ", factionNameCache_.size(), " faction names"); + LOG_INFO("Faction.dbc: loaded ", factionNameCache_.size(), " faction names, ", + factionRepListToId_.size(), " with reputation tracking"); +} + +uint32_t GameHandler::getFactionIdByRepListId(uint32_t repListId) const { + const_cast(this)->loadFactionNameCache(); + auto it = factionRepListToId_.find(repListId); + return (it != factionRepListToId_.end()) ? it->second : 0u; +} + +uint32_t GameHandler::getRepListIdByFactionId(uint32_t factionId) const { + const_cast(this)->loadFactionNameCache(); + auto it = factionIdToRepList_.find(factionId); + return (it != factionIdToRepList_.end()) ? it->second : 0xFFFFFFFFu; } std::string GameHandler::getFactionName(uint32_t factionId) const { diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 8e5c538e..cfea2be4 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1420,10 +1420,13 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) { ImGui::BeginChild("##ReputationList", ImVec2(0, 0), true); - // Sort factions alphabetically by name + // Sort: watched faction first, then alphabetically by name + uint32_t watchedFactionId = gameHandler.getWatchedFactionId(); std::vector> sortedFactions(standings.begin(), standings.end()); std::sort(sortedFactions.begin(), sortedFactions.end(), [&](const auto& a, const auto& b) { + if (a.first == watchedFactionId) return true; + if (b.first == watchedFactionId) return false; const std::string& na = gameHandler.getFactionNamePublic(a.first); const std::string& nb = gameHandler.getFactionNamePublic(b.first); return na < nb; @@ -1435,10 +1438,25 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) { const std::string& factionName = gameHandler.getFactionNamePublic(factionId); const char* displayName = factionName.empty() ? "Unknown Faction" : factionName.c_str(); - // Faction name + tier label on same line + // Determine at-war status via repListId lookup + uint32_t repListId = gameHandler.getRepListIdByFactionId(factionId); + bool atWar = (repListId != 0xFFFFFFFFu) && gameHandler.isFactionAtWar(repListId); + bool isWatched = (factionId == watchedFactionId); + + // Faction name + tier label on same line; mark at-war and watched factions ImGui::TextColored(tier.color, "[%s]", tier.name); ImGui::SameLine(90.0f); - ImGui::Text("%s", displayName); + if (atWar) { + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", displayName); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "(At War)"); + } else if (isWatched) { + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 1.0f), "%s", displayName); + ImGui::SameLine(); + ImGui::TextDisabled("(Tracked)"); + } else { + ImGui::Text("%s", displayName); + } // Progress bar showing position within current tier float ratio = 0.0f;