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
This commit is contained in:
Kelsi 2026-03-12 23:30:44 -07:00
parent 1d9dc6dcae
commit de5c122307
3 changed files with 126 additions and 22 deletions

View file

@ -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<FactionStandingInit>& getInitialFactions() const { return initialFactions_; }
const std::unordered_map<uint32_t, int32_t>& 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<uint32_t, int32_t> factionStandings_;
// Faction name cache (factionId → name), populated lazily from Faction.dbc
std::unordered_map<uint32_t, std::string> factionNameCache_;
// repListId → factionId mapping (populated with factionNameCache)
std::unordered_map<uint32_t, uint32_t> factionRepListToId_;
// factionId → repListId reverse mapping
std::unordered_map<uint32_t, uint32_t> factionIdToRepList_;
bool factionNameCacheLoaded_ = false;
void loadFactionNameCache();
std::string getFactionName(uint32_t factionId) const;

View file

@ -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<GameHandler*>(this)->loadFactionNameCache();
auto it = factionRepListToId_.find(repListId);
return (it != factionRepListToId_.end()) ? it->second : 0u;
}
uint32_t GameHandler::getRepListIdByFactionId(uint32_t factionId) const {
const_cast<GameHandler*>(this)->loadFactionNameCache();
auto it = factionIdToRepList_.find(factionId);
return (it != factionIdToRepList_.end()) ? it->second : 0xFFFFFFFFu;
}
std::string GameHandler::getFactionName(uint32_t factionId) const {

View file

@ -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<std::pair<uint32_t, int32_t>> 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;