/* * Account-bound reputations for AzerothCore. */ #include "AccountBound.h" #include "Config.h" #include "DatabaseEnv.h" #include "DBCStores.h" #include "Log.h" #include "ObjectMgr.h" #include "Player.h" #include "PlayerScript.h" #include "ReputationMgr.h" #include "StringFormat.h" #include "WorldScript.h" #include #include #include #include namespace { struct ModuleConfig { bool Enabled = true; bool StartupBackfill = true; bool SyncOnCreate = true; bool SyncOnChange = true; bool SameFactionOnly = true; bool ConvertFactionSpecific = true; bool SyncUnpairedCrossFaction = false; AccountBound::IdFilter Filter; }; struct ReputationInfo { int32 Standing = 0; uint16 Flags = 0; }; struct CharacterInfo { uint32 Guid = 0; uint32 AccountId = 0; uint8 Race = 0; uint8 Class = 0; std::unordered_map Reputations; }; ModuleConfig Config; void AppendOrCommit(CharacterDatabaseTransaction& trans, std::string_view sql) { if (!trans) trans = CharacterDatabase.BeginTransaction(); trans->Append(sql); if (trans->GetSize() >= 1000) { CharacterDatabase.DirectCommitTransaction(trans); trans = CharacterDatabase.BeginTransaction(); } } void CommitIfNeeded(CharacterDatabaseTransaction& trans) { if (trans && trans->GetSize()) CharacterDatabase.DirectCommitTransaction(trans); } bool CanShareBetweenRaces(uint8 sourceRace, uint8 targetRace) { return !Config.SameFactionOnly || Player::TeamIdForRace(sourceRace) == Player::TeamIdForRace(targetRace); } uint32 GetFactionForTarget(uint32 factionId, uint8 sourceRace, uint8 targetRace) { if (!Config.Filter.Allows(factionId)) return 0; TeamId const sourceTeam = Player::TeamIdForRace(sourceRace); TeamId const targetTeam = Player::TeamIdForRace(targetRace); if (sourceTeam == targetTeam) return factionId; if (Config.SameFactionOnly) return 0; if (Config.ConvertFactionSpecific) { for (auto const& [allianceFaction, hordeFaction] : sObjectMgr->FactionChangeReputation) { if (sourceTeam == TEAM_ALLIANCE && factionId == allianceFaction) return Config.Filter.Allows(targetTeam == TEAM_HORDE ? hordeFaction : allianceFaction) ? (targetTeam == TEAM_HORDE ? hordeFaction : allianceFaction) : 0; if (sourceTeam == TEAM_HORDE && factionId == hordeFaction) return Config.Filter.Allows(targetTeam == TEAM_ALLIANCE ? allianceFaction : hordeFaction) ? (targetTeam == TEAM_ALLIANCE ? allianceFaction : hordeFaction) : 0; } } return Config.SyncUnpairedCrossFaction ? factionId : 0; } int32 GetAbsoluteStanding(CharacterInfo const& character, uint32 factionId, ReputationInfo const& reputation) { FactionEntry const* faction = sFactionStore.LookupEntry(factionId); if (!faction || !faction->CanHaveReputation()) return ReputationMgr::Reputation_Bottom; int32 const base = sObjectMgr->GetBaseReputationOf(faction, character.Race, character.Class); return std::clamp(base + reputation.Standing, ReputationMgr::Reputation_Bottom, ReputationMgr::Reputation_Cap); } int32 GetStoredStanding(CharacterInfo const& character, uint32 factionId, int32 absoluteStanding) { FactionEntry const* faction = sFactionStore.LookupEntry(factionId); if (!faction) return 0; int32 const base = sObjectMgr->GetBaseReputationOf(faction, character.Race, character.Class); return std::clamp(absoluteStanding, ReputationMgr::Reputation_Bottom, ReputationMgr::Reputation_Cap) - base; } std::vector LoadCharacters(uint32 accountId = 0) { std::vector characters; std::unordered_map characterIndexes; QueryResult result; if (accountId) { result = CharacterDatabase.Query( "SELECT c.guid, c.account, c.race, c.class, cr.faction, cr.standing, cr.flags " "FROM characters c " "LEFT JOIN character_reputation cr ON cr.guid = c.guid " "WHERE c.account = {} " "ORDER BY c.guid", accountId); } else { result = CharacterDatabase.Query( "SELECT c.guid, c.account, c.race, c.class, cr.faction, cr.standing, cr.flags " "FROM characters c " "LEFT JOIN character_reputation cr ON cr.guid = c.guid " "WHERE c.account <> 0 " "ORDER BY c.account, c.guid"); } if (!result) return characters; do { Field* fields = result->Fetch(); uint32 const guid = fields[0].Get(); auto [itr, inserted] = characterIndexes.try_emplace(guid, characters.size()); if (inserted) { characters.push_back({ guid, fields[1].Get(), fields[2].Get(), fields[3].Get(), {} }); } if (!fields[4].IsNull()) { characters[itr->second].Reputations[fields[4].Get()] = { fields[5].Get(), fields[6].Get() }; } } while (result->NextRow()); return characters; } void PersistReputation( CharacterDatabaseTransaction& trans, CharacterInfo const& target, uint32 factionId, int32 absoluteStanding, uint16 flags) { int32 const storedStanding = GetStoredStanding(target, factionId, absoluteStanding); AppendOrCommit(trans, Acore::StringFormat( "INSERT INTO character_reputation (guid, faction, standing, flags) " "VALUES ({}, {}, {}, {}) " "ON DUPLICATE KEY UPDATE standing = VALUES(standing), flags = flags | VALUES(flags)", target.Guid, factionId, storedStanding, flags)); } bool ApplyReputation( CharacterDatabaseTransaction& trans, CharacterInfo& target, uint32 factionId, int32 absoluteStanding, uint16 flags) { FactionEntry const* faction = sFactionStore.LookupEntry(factionId); if (!faction || !faction->CanHaveReputation()) return false; int32 currentStanding = sObjectMgr->GetBaseReputationOf(faction, target.Race, target.Class); auto currentItr = target.Reputations.find(factionId); if (currentItr != target.Reputations.end()) currentStanding = GetAbsoluteStanding(target, factionId, currentItr->second); absoluteStanding = std::clamp(absoluteStanding, ReputationMgr::Reputation_Bottom, ReputationMgr::Reputation_Cap); if (absoluteStanding <= currentStanding) return false; uint16 const mergedFlags = currentItr == target.Reputations.end() ? flags : uint16(currentItr->second.Flags | flags); PersistReputation(trans, target, factionId, absoluteStanding, mergedFlags); target.Reputations[factionId] = { GetStoredStanding(target, factionId, absoluteStanding), mergedFlags }; return true; } uint32 MergeReputations(CharacterInfo const& source, CharacterInfo& target, CharacterDatabaseTransaction& trans) { if (source.Guid == target.Guid || !CanShareBetweenRaces(source.Race, target.Race)) return 0; uint32 updated = 0; for (auto const& [sourceFactionId, sourceReputation] : source.Reputations) { uint32 const targetFactionId = GetFactionForTarget(sourceFactionId, source.Race, target.Race); if (!targetFactionId) continue; int32 const absoluteStanding = GetAbsoluteStanding(source, sourceFactionId, sourceReputation); if (ApplyReputation(trans, target, targetFactionId, absoluteStanding, sourceReputation.Flags)) ++updated; } return updated; } void BackfillReputationsForCharacter(Player* player) { if (!Config.Enabled || !player) return; std::vector characters = LoadCharacters(player->GetSession()->GetAccountId()); auto targetItr = std::find_if(characters.begin(), characters.end(), [player](CharacterInfo const& character) { return character.Guid == player->GetGUID().GetCounter(); }); if (targetItr == characters.end()) return; CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); uint32 updated = 0; for (CharacterInfo const& source : characters) updated += MergeReputations(source, *targetItr, trans); CommitIfNeeded(trans); if (updated) LOG_INFO("module.accountboundreputations", "AccountBoundReputations: seeded {} improved reputation(s) for character {}.", updated, targetItr->Guid); } void SyncReputationToAccount(Player* player, uint32 factionId, int32 absoluteStanding) { if (!Config.Enabled || !Config.SyncOnChange || !player) return; FactionEntry const* sourceFaction = sFactionStore.LookupEntry(factionId); if (!sourceFaction || !sourceFaction->CanHaveReputation()) return; std::vector characters = LoadCharacters(player->GetSession()->GetAccountId()); auto sourceItr = std::find_if(characters.begin(), characters.end(), [player](CharacterInfo const& character) { return character.Guid == player->GetGUID().GetCounter(); }); if (sourceItr == characters.end()) return; uint16 flags = 0; if (FactionState const* state = player->GetReputationMgr().GetState(sourceFaction)) flags = state->Flags; CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); uint32 updated = 0; for (CharacterInfo& target : characters) { if (target.Guid == sourceItr->Guid || !CanShareBetweenRaces(sourceItr->Race, target.Race)) continue; uint32 const targetFactionId = GetFactionForTarget(factionId, sourceItr->Race, target.Race); if (targetFactionId && ApplyReputation(trans, target, targetFactionId, absoluteStanding, flags)) ++updated; } CommitIfNeeded(trans); if (updated) LOG_DEBUG("module.accountboundreputations", "AccountBoundReputations: propagated faction {} from character {} to {} account character(s).", factionId, sourceItr->Guid, updated); } void BackfillAllReputations() { if (!Config.Enabled || !Config.StartupBackfill) return; std::vector characters = LoadCharacters(); std::unordered_map> charactersByAccount; for (std::size_t index = 0; index < characters.size(); ++index) charactersByAccount[characters[index].AccountId].push_back(index); CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); uint64 updated = 0; for (auto const& [accountId, indexes] : charactersByAccount) { (void)accountId; std::vector const snapshot = [&]() { std::vector result; result.reserve(indexes.size()); for (std::size_t index : indexes) result.push_back(characters[index]); return result; }(); for (std::size_t targetIndex : indexes) for (CharacterInfo const& source : snapshot) updated += MergeReputations(source, characters[targetIndex], trans); } CommitIfNeeded(trans); LOG_INFO("module.accountboundreputations", "AccountBoundReputations: startup backfill queued {} improved reputation row(s).", updated); } void LoadModuleConfig() { Config.Enabled = AccountBound::IsCategoryEnabled("Reputations"); Config.StartupBackfill = sConfigMgr->GetOption("AccountBound.Reputations.StartupBackfill", true); Config.SyncOnCreate = sConfigMgr->GetOption("AccountBound.Reputations.SyncOnCreate", true); Config.SyncOnChange = sConfigMgr->GetOption("AccountBound.Reputations.SyncOnChange", true); Config.SameFactionOnly = sConfigMgr->GetOption("AccountBound.Reputations.SameFactionOnly", true); Config.ConvertFactionSpecific = sConfigMgr->GetOption("AccountBound.Reputations.ConvertFactionSpecific", true); Config.SyncUnpairedCrossFaction = sConfigMgr->GetOption("AccountBound.Reputations.SyncUnpairedCrossFaction", false); Config.Filter = AccountBound::LoadIdFilter("Reputations"); } } class AccountBoundReputationsWorldScript : public WorldScript { public: AccountBoundReputationsWorldScript() : WorldScript("AccountBoundReputationsWorldScript", { WORLDHOOK_ON_AFTER_CONFIG_LOAD, WORLDHOOK_ON_STARTUP }) { } void OnAfterConfigLoad(bool reload) override { LoadModuleConfig(); LOG_INFO("module.accountboundreputations", "AccountBoundReputations: {}. CreateSync={}, ChangeSync={}, StartupBackfill={}, SameFactionOnly={}.", Config.Enabled ? (reload ? "configuration reloaded" : "configuration loaded") : "disabled", Config.Enabled && Config.SyncOnCreate ? "on" : "off", Config.Enabled && Config.SyncOnChange ? "on" : "off", Config.Enabled && Config.StartupBackfill ? "on" : "off", Config.Enabled && Config.SameFactionOnly ? "on" : "off"); } void OnStartup() override { BackfillAllReputations(); } }; class AccountBoundReputationsPlayerScript : public PlayerScript { public: AccountBoundReputationsPlayerScript() : PlayerScript("AccountBoundReputationsPlayerScript", { PLAYERHOOK_ON_CREATE, PLAYERHOOK_ON_REPUTATION_CHANGE }) { } void OnPlayerCreate(Player* player) override { if (Config.SyncOnCreate) BackfillReputationsForCharacter(player); } bool OnPlayerReputationChange(Player* player, uint32 factionId, int32& standing, bool /*incremental*/) override { SyncReputationToAccount(player, factionId, standing); return true; } }; void AddAccountBoundReputationsScripts() { new AccountBoundReputationsWorldScript(); new AccountBoundReputationsPlayerScript(); }