добавлены коллекции аккаунтов

This commit is contained in:
2026-08-05 18:39:58 +04:00
parent 7c092e8a72
commit 444b2b9d80
16 changed files with 5069 additions and 1 deletions
+94
View File
@@ -0,0 +1,94 @@
#ifndef ACCOUNT_BOUND_H
#define ACCOUNT_BOUND_H
#include "Config.h"
#include "Log.h"
#include <charconv>
#include <cctype>
#include <string>
#include <string_view>
#include <unordered_set>
namespace AccountBound
{
struct IdFilter
{
bool AllowAll = true;
std::unordered_set<uint32> Allowed;
std::unordered_set<uint32> Blocked;
bool Allows(uint32 id) const
{
return id && !Blocked.contains(id) && (AllowAll || Allowed.contains(id));
}
};
inline std::string Trim(std::string_view value)
{
std::size_t first = 0;
while (first < value.size() && std::isspace(static_cast<unsigned char>(value[first])))
++first;
std::size_t last = value.size();
while (last > first && std::isspace(static_cast<unsigned char>(value[last - 1])))
--last;
return std::string(value.substr(first, last - first));
}
inline void ParseIds(std::string const& raw, std::unordered_set<uint32>& destination)
{
std::size_t start = 0;
while (start <= raw.size())
{
std::size_t end = raw.find(',', start);
if (end == std::string::npos)
end = raw.size();
std::string token = Trim(std::string_view(raw).substr(start, end - start));
if (!token.empty())
{
uint32 id = 0;
auto const [ptr, error] = std::from_chars(token.data(), token.data() + token.size(), id);
if (error == std::errc() && ptr == token.data() + token.size() && id)
destination.insert(id);
else
LOG_WARN("module.accountbound", "AccountBound: ignored invalid ID '{}' in an AllowList or BlockList.", token);
}
if (end == raw.size())
break;
start = end + 1;
}
}
inline bool IsCategoryEnabled(std::string_view category, bool defaultValue = true)
{
if (!sConfigMgr->GetOption<bool>("AccountBound.Enable", true))
return false;
return sConfigMgr->GetOption<bool>(
"AccountBound." + std::string(category) + ".Enable", defaultValue);
}
inline IdFilter LoadIdFilter(std::string_view category)
{
IdFilter filter;
std::string const prefix = "AccountBound." + std::string(category);
std::string const allowList = Trim(sConfigMgr->GetOption<std::string>(prefix + ".AllowList", "all"));
if (!allowList.empty() && allowList != "all" && allowList != "ALL" && allowList != "*")
{
filter.AllowAll = false;
if (allowList != "none" && allowList != "NONE")
ParseIds(allowList, filter.Allowed);
}
ParseIds(sConfigMgr->GetOption<std::string>(prefix + ".BlockList", ""), filter.Blocked);
return filter;
}
}
#endif
+378
View File
@@ -0,0 +1,378 @@
/*
* Account-bound achievements for AzerothCore.
*/
#include "AccountBound.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DBCEnums.h"
#include "DBCStores.h"
#include "GameTime.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "PlayerScript.h"
#include "StringFormat.h"
#include "WorldScript.h"
#include <algorithm>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace
{
struct ModuleConfig
{
bool Enabled = true;
bool StartupBackfill = true;
bool SameFactionOnly = false;
bool SyncOnCreate = true;
bool SyncRealmFirst = false;
bool SyncHidden = false;
bool ConvertFactionSpecific = true;
bool SyncUnpairedFactionSpecific = false;
AccountBound::IdFilter Filter;
};
struct CharacterInfo
{
uint32 Guid = 0;
uint32 AccountId = 0;
uint8 Race = 0;
};
ModuleConfig Config;
bool CanShareBetweenRaces(uint8 sourceRace, uint8 targetRace)
{
return !Config.SameFactionOnly || Player::TeamIdForRace(sourceRace) == Player::TeamIdForRace(targetRace);
}
bool IsRealmFirstAchievement(AchievementEntry const* achievement)
{
return achievement && (achievement->flags & (ACHIEVEMENT_FLAG_REALM_FIRST_REACH | ACHIEVEMENT_FLAG_REALM_FIRST_KILL));
}
bool IsAchievementAllowed(AchievementEntry const* achievement)
{
if (!achievement || (achievement->flags & ACHIEVEMENT_FLAG_COUNTER))
return false;
if (!Config.Filter.Allows(achievement->ID))
return false;
if (!Config.SyncRealmFirst && IsRealmFirstAchievement(achievement))
return false;
if (!Config.SyncHidden && (achievement->flags & ACHIEVEMENT_FLAG_HIDDEN))
return false;
return true;
}
uint32 GetFactionAchievementForTeam(uint32 achievementId, TeamId targetTeam)
{
AchievementEntry const* achievement = sAchievementStore.LookupEntry(achievementId);
if (!IsAchievementAllowed(achievement))
return 0;
if (achievement->requiredFaction == ACHIEVEMENT_FACTION_ANY)
return achievementId;
if (achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && targetTeam == TEAM_ALLIANCE)
return achievementId;
if (achievement->requiredFaction == ACHIEVEMENT_FACTION_HORDE && targetTeam == TEAM_HORDE)
return achievementId;
if (!Config.ConvertFactionSpecific)
return Config.SyncUnpairedFactionSpecific ? achievementId : 0;
for (auto const& [allianceAchievement, hordeAchievement] : sObjectMgr->FactionChangeAchievements)
{
uint32 mappedAchievement = 0;
if (achievementId == allianceAchievement && targetTeam == TEAM_HORDE)
mappedAchievement = hordeAchievement;
else if (achievementId == hordeAchievement && targetTeam == TEAM_ALLIANCE)
mappedAchievement = allianceAchievement;
if (!mappedAchievement)
continue;
AchievementEntry const* mappedEntry = sAchievementStore.LookupEntry(mappedAchievement);
return IsAchievementAllowed(mappedEntry) ? mappedAchievement : 0;
}
return Config.SyncUnpairedFactionSpecific ? achievementId : 0;
}
uint32 GetAchievementForRace(uint32 achievementId, uint8 race)
{
return GetFactionAchievementForTeam(achievementId, Player::TeamIdForRace(race));
}
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);
}
void InsertAchievementForCharacter(CharacterDatabaseTransaction& trans, uint32 targetGuid, uint32 achievementId, uint32 date)
{
AppendOrCommit(trans, Acore::StringFormat(
"INSERT IGNORE INTO character_achievement (guid, achievement, date) VALUES ({}, {}, {})",
targetGuid, achievementId, date));
}
std::vector<CharacterInfo> LoadAccountCharacters(uint32 accountId)
{
std::vector<CharacterInfo> characters;
QueryResult result = CharacterDatabase.Query(
"SELECT guid, account, race FROM characters WHERE account = {}", accountId);
if (!result)
return characters;
do
{
Field* fields = result->Fetch();
characters.push_back({
fields[0].Get<uint32>(),
fields[1].Get<uint32>(),
fields[2].Get<uint8>()
});
} while (result->NextRow());
return characters;
}
void SyncAchievementToAccount(Player* player, uint32 achievementId, uint32 date)
{
if (!Config.Enabled || !player)
return;
uint8 const sourceRace = player->getRace(true);
std::vector<CharacterInfo> characters = LoadAccountCharacters(player->GetSession()->GetAccountId());
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint32 synced = 0;
for (CharacterInfo const& target : characters)
{
if (target.Guid == player->GetGUID().GetCounter() || !CanShareBetweenRaces(sourceRace, target.Race))
continue;
uint32 const targetAchievementId = GetAchievementForRace(achievementId, target.Race);
if (!targetAchievementId)
continue;
InsertAchievementForCharacter(trans, target.Guid, targetAchievementId, date);
++synced;
}
CommitIfNeeded(trans);
if (synced)
LOG_DEBUG("module.accountboundachievements",
"AccountBoundAchievements: synced achievement {} from player {} to {} account character(s).",
achievementId, player->GetGUID().GetCounter(), synced);
}
void BackfillAchievementsForCharacter(Player* player)
{
if (!Config.Enabled || !player)
return;
uint32 const accountId = player->GetSession()->GetAccountId();
uint32 const targetGuid = player->GetGUID().GetCounter();
uint8 const targetRace = player->getRace(true);
QueryResult result = CharacterDatabase.Query(
"SELECT ca.achievement, MIN(ca.date), c.race "
"FROM character_achievement ca "
"INNER JOIN characters c ON c.guid = ca.guid "
"WHERE c.account = {} AND c.guid <> {} "
"GROUP BY ca.achievement, c.race "
"ORDER BY MIN(ca.date)",
accountId, targetGuid);
if (!result)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint32 inserted = 0;
do
{
Field* fields = result->Fetch();
uint32 const achievementId = fields[0].Get<uint32>();
uint32 const date = fields[1].Get<uint32>();
uint8 const sourceRace = fields[2].Get<uint8>();
if (!CanShareBetweenRaces(sourceRace, targetRace))
continue;
uint32 const targetAchievementId = GetAchievementForRace(achievementId, targetRace);
if (!targetAchievementId)
continue;
InsertAchievementForCharacter(trans, targetGuid, targetAchievementId, date);
++inserted;
} while (result->NextRow());
CommitIfNeeded(trans);
if (inserted)
LOG_INFO("module.accountboundachievements",
"AccountBoundAchievements: seeded {} achievement row(s) for new character {}.",
inserted, targetGuid);
}
void BackfillAllAchievements()
{
if (!Config.Enabled || !Config.StartupBackfill)
return;
QueryResult charactersResult = CharacterDatabase.Query(
"SELECT guid, account, race FROM characters WHERE account <> 0");
if (!charactersResult)
return;
std::unordered_map<uint32, std::vector<CharacterInfo>> charactersByAccount;
do
{
Field* fields = charactersResult->Fetch();
CharacterInfo character = {
fields[0].Get<uint32>(),
fields[1].Get<uint32>(),
fields[2].Get<uint8>()
};
charactersByAccount[character.AccountId].push_back(character);
} while (charactersResult->NextRow());
QueryResult achievementsResult = CharacterDatabase.Query(
"SELECT c.account, c.race, ca.achievement, MIN(ca.date) "
"FROM character_achievement ca "
"INNER JOIN characters c ON c.guid = ca.guid "
"WHERE c.account <> 0 "
"GROUP BY c.account, c.race, ca.achievement "
"ORDER BY MIN(ca.date)");
if (!achievementsResult)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint64 inserted = 0;
do
{
Field* fields = achievementsResult->Fetch();
uint32 const accountId = fields[0].Get<uint32>();
uint8 const sourceRace = fields[1].Get<uint8>();
uint32 const achievementId = fields[2].Get<uint32>();
uint32 const date = fields[3].Get<uint32>();
auto accountItr = charactersByAccount.find(accountId);
if (accountItr == charactersByAccount.end())
continue;
for (CharacterInfo const& target : accountItr->second)
{
if (!CanShareBetweenRaces(sourceRace, target.Race))
continue;
uint32 const targetAchievementId = GetAchievementForRace(achievementId, target.Race);
if (!targetAchievementId)
continue;
InsertAchievementForCharacter(trans, target.Guid, targetAchievementId, date);
++inserted;
}
} while (achievementsResult->NextRow());
CommitIfNeeded(trans);
LOG_INFO("module.accountboundachievements",
"AccountBoundAchievements: startup backfill queued {} achievement row(s).", inserted);
}
void LoadModuleConfig()
{
Config.Enabled = AccountBound::IsCategoryEnabled("Achievements");
Config.StartupBackfill = sConfigMgr->GetOption<bool>("AccountBound.Achievements.StartupBackfill", true);
Config.SameFactionOnly = sConfigMgr->GetOption<bool>("AccountBound.Achievements.SameFactionOnly", false);
Config.SyncOnCreate = sConfigMgr->GetOption<bool>("AccountBound.Achievements.SyncOnCreate", true);
Config.SyncRealmFirst = sConfigMgr->GetOption<bool>("AccountBound.Achievements.SyncRealmFirst", false);
Config.SyncHidden = sConfigMgr->GetOption<bool>("AccountBound.Achievements.SyncHidden", false);
Config.ConvertFactionSpecific = sConfigMgr->GetOption<bool>("AccountBound.Achievements.ConvertFactionSpecific", true);
Config.SyncUnpairedFactionSpecific = sConfigMgr->GetOption<bool>("AccountBound.Achievements.SyncUnpairedFactionSpecific", false);
Config.Filter = AccountBound::LoadIdFilter("Achievements");
}
}
class AccountBoundAchievementsWorldScript : public WorldScript
{
public:
AccountBoundAchievementsWorldScript() : WorldScript("AccountBoundAchievementsWorldScript", {
WORLDHOOK_ON_AFTER_CONFIG_LOAD,
WORLDHOOK_ON_STARTUP
}) { }
void OnAfterConfigLoad(bool reload) override
{
LoadModuleConfig();
LOG_INFO("module.accountboundachievements",
"AccountBoundAchievements: {}. CreateSync={}, StartupBackfill={}, SameFactionOnly={}, RealmFirst={}.",
Config.Enabled ? (reload ? "configuration reloaded" : "configuration loaded") : "disabled",
Config.Enabled && Config.SyncOnCreate ? "on" : "off",
Config.Enabled && Config.StartupBackfill ? "on" : "off",
Config.Enabled && Config.SameFactionOnly ? "on" : "off",
Config.Enabled && Config.SyncRealmFirst ? "on" : "off");
}
void OnStartup() override
{
BackfillAllAchievements();
}
};
class AccountBoundAchievementsPlayerScript : public PlayerScript
{
public:
AccountBoundAchievementsPlayerScript() : PlayerScript("AccountBoundAchievementsPlayerScript", {
PLAYERHOOK_ON_CREATE,
PLAYERHOOK_ON_ACHI_COMPLETE
}) { }
void OnPlayerCreate(Player* player) override
{
if (Config.SyncOnCreate)
BackfillAchievementsForCharacter(player);
}
void OnPlayerAchievementComplete(Player* player, AchievementEntry const* achievement) override
{
if (achievement)
SyncAchievementToAccount(player, achievement->ID, uint32(GameTime::GetGameTime().count()));
}
};
void AddAccountBoundAchievementsScripts()
{
new AccountBoundAchievementsWorldScript();
new AccountBoundAchievementsPlayerScript();
}
+526
View File
@@ -0,0 +1,526 @@
/*
* AccountWideFriends module for AzerothCore.
*/
#include "AccountBound.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "Player.h"
#include "PlayerScript.h"
#include "SocialMgr.h"
#include "WorldScript.h"
#include "WorldSession.h"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace
{
constexpr uint32 FriendFlag = SOCIAL_FLAG_FRIEND;
constexpr uint32 WithoutFriendFlagMask = 0xFE;
struct ModuleConfig
{
bool Enabled = true;
bool SyncOnCreate = true;
bool SyncOnlineChanges = true;
bool StartupBackfill = true;
bool SameFactionOnly = false;
uint32 SyncIntervalMs = 3000;
};
struct CharacterInfo
{
uint32 Guid = 0;
uint8 Race = 0;
};
using FriendMap = std::unordered_map<uint32, std::string>;
ModuleConfig Config;
std::unordered_map<uint32, uint32> UpdateTimersByCharacter;
std::unordered_map<uint32, FriendMap> CachedFriendsByCharacter;
bool IsEnabled()
{
return Config.Enabled;
}
std::string EscapeSqlString(std::string text)
{
CharacterDatabase.EscapeString(text);
return text;
}
bool CanShareBetweenRaces(uint8 ownerRace, uint8 friendRace)
{
return !Config.SameFactionOnly || Player::TeamIdForRace(ownerRace) == Player::TeamIdForRace(friendRace);
}
std::vector<CharacterInfo> LoadAccountCharacterInfos(uint32 accountId)
{
std::vector<CharacterInfo> characters;
QueryResult result = CharacterDatabase.Query(
"SELECT guid, race FROM characters WHERE account = {}", accountId);
if (!result)
return characters;
do
{
Field* fields = result->Fetch();
characters.push_back({ fields[0].Get<uint32>(), fields[1].Get<uint8>() });
} while (result->NextRow());
return characters;
}
std::vector<uint32> LoadAccountCharacters(uint32 accountId)
{
std::vector<uint32> characterGuids;
for (CharacterInfo const& character : LoadAccountCharacterInfos(accountId))
characterGuids.push_back(character.Guid);
return characterGuids;
}
std::unordered_set<uint32> MakeGuidSet(std::vector<uint32> const& guids)
{
return { guids.begin(), guids.end() };
}
std::unordered_set<uint32> MakeGuidSet(std::vector<CharacterInfo> const& characters)
{
std::unordered_set<uint32> guids;
for (CharacterInfo const& character : characters)
guids.insert(character.Guid);
return guids;
}
uint8 LoadCharacterRace(uint32 characterGuid)
{
QueryResult result = CharacterDatabase.Query(
"SELECT race FROM characters WHERE guid = {}", characterGuid);
if (!result)
return 0;
return result->Fetch()[0].Get<uint8>();
}
bool CanCharacterShareWithFriend(CharacterInfo const& owner, uint32 friendGuid)
{
uint8 const friendRace = LoadCharacterRace(friendGuid);
return friendRace && CanShareBetweenRaces(owner.Race, friendRace);
}
void RemoveFriendFromCharacter(uint32 ownerGuid, uint32 friendGuid)
{
CharacterDatabase.DirectExecute(
"UPDATE character_social SET flags = flags & {} "
"WHERE guid = {} AND friend = {} AND (flags & {}) <> 0",
WithoutFriendFlagMask, ownerGuid, friendGuid, FriendFlag);
CharacterDatabase.DirectExecute(
"DELETE FROM character_social WHERE guid = {} AND friend = {} AND flags = 0",
ownerGuid, friendGuid);
}
FriendMap LoadCharacterFriends(uint32 characterGuid)
{
FriendMap friends;
QueryResult result = CharacterDatabase.Query(
"SELECT social.friend, COALESCE(social.note, ''), owner.race, friend_char.race "
"FROM character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"INNER JOIN characters friend_char ON friend_char.guid = social.friend "
"WHERE social.guid = {} AND owner.account <> friend_char.account AND (social.flags & {}) <> 0",
characterGuid, FriendFlag);
if (!result)
return friends;
do
{
Field* fields = result->Fetch();
uint32 const friendGuid = fields[0].Get<uint32>();
uint8 const ownerRace = fields[2].Get<uint8>();
uint8 const friendRace = fields[3].Get<uint8>();
if (friendGuid && friendGuid != characterGuid && CanShareBetweenRaces(ownerRace, friendRace))
friends[friendGuid] = fields[1].Get<std::string>();
} while (result->NextRow());
return friends;
}
FriendMap LoadAccountFriendUnion(uint32 accountId)
{
FriendMap friends;
std::vector<uint32> accountCharacters = LoadAccountCharacters(accountId);
std::unordered_set<uint32> ownCharacters = MakeGuidSet(accountCharacters);
QueryResult result = CharacterDatabase.Query(
"SELECT social.friend, COALESCE(social.note, '') "
"FROM character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"WHERE owner.account = {} AND (social.flags & {}) <> 0 "
"ORDER BY social.note <> '' DESC",
accountId, FriendFlag);
if (!result)
return friends;
do
{
Field* fields = result->Fetch();
uint32 const friendGuid = fields[0].Get<uint32>();
if (!friendGuid || ownCharacters.contains(friendGuid))
continue;
std::string const note = fields[1].Get<std::string>();
auto [itr, inserted] = friends.emplace(friendGuid, note);
if (!inserted && itr->second.empty() && !note.empty())
itr->second = note;
} while (result->NextRow());
return friends;
}
void InsertOrUpdateFriendForCharacter(uint32 ownerGuid, uint32 friendGuid, std::string const& note, bool forceNote)
{
if (!ownerGuid || !friendGuid || ownerGuid == friendGuid)
return;
std::string safeNote = note.substr(0, 48);
safeNote = EscapeSqlString(safeNote);
if (forceNote)
{
CharacterDatabase.DirectExecute(
"INSERT INTO character_social (guid, friend, flags, note) VALUES ({}, {}, {}, '{}') "
"ON DUPLICATE KEY UPDATE flags = flags | {}, note = '{}'",
ownerGuid, friendGuid, FriendFlag, safeNote, FriendFlag, safeNote);
return;
}
CharacterDatabase.DirectExecute(
"INSERT INTO character_social (guid, friend, flags, note) VALUES ({}, {}, {}, '{}') "
"ON DUPLICATE KEY UPDATE flags = flags | {}, note = IF(VALUES(note) <> '', VALUES(note), note)",
ownerGuid, friendGuid, FriendFlag, safeNote, FriendFlag);
}
uint32 SyncFriendToCharacters(
std::vector<CharacterInfo> const& accountCharacters,
std::unordered_set<uint32> const& ownCharacters,
uint32 friendGuid,
std::string const& note,
bool forceNote)
{
if (!friendGuid || ownCharacters.contains(friendGuid))
return 0;
uint32 synced = 0;
for (CharacterInfo const& owner : accountCharacters)
{
if (owner.Guid == friendGuid || !CanCharacterShareWithFriend(owner, friendGuid))
continue;
InsertOrUpdateFriendForCharacter(owner.Guid, friendGuid, note, forceNote);
++synced;
}
return synced;
}
uint32 SyncFriendToAccount(uint32 accountId, uint32 friendGuid, std::string const& note, bool forceNote = false)
{
std::vector<CharacterInfo> accountCharacters = LoadAccountCharacterInfos(accountId);
std::unordered_set<uint32> ownCharacters = MakeGuidSet(accountCharacters);
return SyncFriendToCharacters(accountCharacters, ownCharacters, friendGuid, note, forceNote);
}
void RemoveInvalidFactionFriends(uint32 accountId)
{
if (!Config.SameFactionOnly || !accountId)
return;
QueryResult result = CharacterDatabase.Query(
"SELECT social.guid, social.friend, owner.race, friend_char.race "
"FROM character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"INNER JOIN characters friend_char ON friend_char.guid = social.friend "
"WHERE owner.account = {} AND owner.account <> friend_char.account AND (social.flags & {}) <> 0",
accountId, FriendFlag);
if (!result)
return;
do
{
Field* fields = result->Fetch();
uint32 const ownerGuid = fields[0].Get<uint32>();
uint32 const friendGuid = fields[1].Get<uint32>();
uint8 const ownerRace = fields[2].Get<uint8>();
uint8 const friendRace = fields[3].Get<uint8>();
if (!CanShareBetweenRaces(ownerRace, friendRace))
RemoveFriendFromCharacter(ownerGuid, friendGuid);
} while (result->NextRow());
}
void RemoveOwnAccountFriends(uint32 accountId)
{
if (!accountId)
return;
CharacterDatabase.DirectExecute(
"UPDATE character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"INNER JOIN characters friend_char ON friend_char.guid = social.friend "
"SET social.flags = social.flags & {} "
"WHERE owner.account = {} AND friend_char.account = {} AND (social.flags & {}) <> 0",
WithoutFriendFlagMask, accountId, accountId, FriendFlag);
CharacterDatabase.DirectExecute(
"DELETE social FROM character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"INNER JOIN characters friend_char ON friend_char.guid = social.friend "
"WHERE owner.account = {} AND friend_char.account = {} AND social.flags = 0",
accountId, accountId);
}
uint32 SyncAccountFriends(uint32 accountId)
{
RemoveOwnAccountFriends(accountId);
RemoveInvalidFactionFriends(accountId);
std::vector<CharacterInfo> accountCharacters = LoadAccountCharacterInfos(accountId);
if (accountCharacters.empty())
return 0;
std::unordered_set<uint32> ownCharacters = MakeGuidSet(accountCharacters);
FriendMap accountFriends = LoadAccountFriendUnion(accountId);
uint32 synced = 0;
for (auto const& [friendGuid, note] : accountFriends)
synced += SyncFriendToCharacters(accountCharacters, ownCharacters, friendGuid, note, false);
return synced;
}
void RemoveFriendFromAccount(uint32 accountId, uint32 friendGuid)
{
if (!accountId || !friendGuid)
return;
CharacterDatabase.DirectExecute(
"UPDATE character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"SET social.flags = social.flags & {} "
"WHERE owner.account = {} AND social.friend = {} AND (social.flags & {}) <> 0",
WithoutFriendFlagMask, accountId, friendGuid, FriendFlag);
CharacterDatabase.DirectExecute(
"DELETE social FROM character_social social "
"INNER JOIN characters owner ON owner.guid = social.guid "
"WHERE owner.account = {} AND social.friend = {} AND social.flags = 0",
accountId, friendGuid);
}
void CacheCharacterFriends(uint32 characterGuid)
{
CachedFriendsByCharacter[characterGuid] = LoadCharacterFriends(characterGuid);
}
void DetectAndSyncOnlineChanges(Player* player)
{
if (!player || !player->GetSession())
return;
uint32 const accountId = player->GetSession()->GetAccountId();
uint32 const characterGuid = player->GetGUID().GetCounter();
RemoveOwnAccountFriends(accountId);
RemoveInvalidFactionFriends(accountId);
FriendMap currentFriends = LoadCharacterFriends(characterGuid);
auto cacheItr = CachedFriendsByCharacter.find(characterGuid);
if (cacheItr == CachedFriendsByCharacter.end())
{
CachedFriendsByCharacter[characterGuid] = std::move(currentFriends);
return;
}
FriendMap const& cachedFriends = cacheItr->second;
bool changed = false;
for (auto const& [friendGuid, note] : currentFriends)
{
auto cachedFriend = cachedFriends.find(friendGuid);
if (cachedFriend == cachedFriends.end())
{
SyncFriendToAccount(accountId, friendGuid, note);
changed = true;
continue;
}
if (cachedFriend->second != note)
{
SyncFriendToAccount(accountId, friendGuid, note, true);
changed = true;
}
}
for (auto const& cachedFriendEntry : cachedFriends)
{
uint32 const friendGuid = cachedFriendEntry.first;
if (!currentFriends.contains(friendGuid))
{
RemoveFriendFromAccount(accountId, friendGuid);
changed = true;
}
}
if (changed)
LOG_DEBUG("module.accountwidefriends", "AccountWideFriends: synced friend changes for account {} from character {}.", accountId, characterGuid);
CacheCharacterFriends(characterGuid);
}
void BackfillAllAccounts()
{
QueryResult result = CharacterDatabase.Query(
"SELECT DISTINCT account FROM characters WHERE account <> 0");
if (!result)
return;
uint32 accountCount = 0;
uint32 syncCount = 0;
do
{
Field* fields = result->Fetch();
uint32 const accountId = fields[0].Get<uint32>();
syncCount += SyncAccountFriends(accountId);
++accountCount;
} while (result->NextRow());
LOG_INFO("module.accountwidefriends", "AccountWideFriends: startup backfill checked {} account(s) and queued {} friend row sync operation(s).", accountCount, syncCount);
}
void LoadModuleConfig()
{
Config.Enabled = AccountBound::IsCategoryEnabled("Friends");
Config.SyncOnCreate = sConfigMgr->GetOption<bool>("AccountBound.Friends.SyncOnCreate", true);
Config.SyncOnlineChanges = sConfigMgr->GetOption<bool>("AccountBound.Friends.SyncOnlineChanges", true);
Config.StartupBackfill = sConfigMgr->GetOption<bool>("AccountBound.Friends.StartupBackfill", true);
Config.SameFactionOnly = sConfigMgr->GetOption<bool>("AccountBound.Friends.SameFactionOnly", false);
Config.SyncIntervalMs = std::max<uint32>(
1000,
sConfigMgr->GetOption<uint32>("AccountBound.Friends.SyncIntervalSeconds", 3) * 1000);
}
}
class AccountWideFriendsWorldScript : public WorldScript
{
public:
AccountWideFriendsWorldScript() : WorldScript("AccountWideFriendsWorldScript", {
WORLDHOOK_ON_AFTER_CONFIG_LOAD,
WORLDHOOK_ON_STARTUP
}) { }
void OnAfterConfigLoad(bool reload) override
{
LoadModuleConfig();
LOG_INFO("module.accountwidefriends", "AccountWideFriends: {}. Enabled={}, CreateSync={}, OnlineSync={}, SameFactionOnly={}, IntervalMs={}, StartupBackfill={}.",
reload ? "configuration reloaded" : "configuration loaded",
Config.Enabled ? "on" : "off",
Config.Enabled && Config.SyncOnCreate ? "on" : "off",
Config.Enabled && Config.SyncOnlineChanges ? "on" : "off",
Config.Enabled && Config.SameFactionOnly ? "on" : "off",
Config.SyncIntervalMs,
Config.Enabled && Config.StartupBackfill ? "on" : "off");
}
void OnStartup() override
{
if (IsEnabled() && Config.StartupBackfill)
BackfillAllAccounts();
}
};
class AccountWideFriendsPlayerScript : public PlayerScript
{
public:
AccountWideFriendsPlayerScript() : PlayerScript("AccountWideFriendsPlayerScript", {
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_CREATE,
PLAYERHOOK_ON_LOGOUT,
PLAYERHOOK_ON_UPDATE
}) { }
void OnPlayerLogin(Player* player) override
{
if (!IsEnabled() || !player || !player->GetSession())
return;
CacheCharacterFriends(player->GetGUID().GetCounter());
}
void OnPlayerCreate(Player* player) override
{
if (IsEnabled() && Config.SyncOnCreate && player && player->GetSession())
SyncAccountFriends(player->GetSession()->GetAccountId());
}
void OnPlayerLogout(Player* player) override
{
if (!player)
return;
uint32 const characterGuid = player->GetGUID().GetCounter();
UpdateTimersByCharacter.erase(characterGuid);
CachedFriendsByCharacter.erase(characterGuid);
}
void OnPlayerUpdate(Player* player, uint32 diff) override
{
if (!IsEnabled() || !Config.SyncOnlineChanges || !player)
return;
uint32 const characterGuid = player->GetGUID().GetCounter();
uint32& timer = UpdateTimersByCharacter[characterGuid];
timer = std::min<uint32>(Config.SyncIntervalMs, timer + diff);
if (timer < Config.SyncIntervalMs)
return;
timer = 0;
DetectAndSyncOnlineChanges(player);
}
};
void AddAccountWideFriendsScripts()
{
new AccountWideFriendsWorldScript();
new AccountWideFriendsPlayerScript();
}
+692
View File
@@ -0,0 +1,692 @@
/*
* Account-bound mounts for AzerothCore.
*/
#include "AccountBound.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DBCEnums.h"
#include "ItemTemplate.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "PlayerScript.h"
#include "RaceMgr.h"
#include "SpellAuraDefines.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "StringFormat.h"
#include "WorldScript.h"
#include <algorithm>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace
{
struct ModuleConfig
{
bool Enabled = true;
bool StartupBackfill = true;
bool SyncOnCreate = true;
bool ConvertFactionSpecific = false;
bool RespectFactionRestrictions = true;
bool RequireRiding = true;
bool SyncClassMounts = true;
bool RequireClass = true;
bool ClassMountsSameFactionOnly = true;
bool CleanupInvalid = true;
bool BackfillOnRidingSkillChange = true;
AccountBound::IdFilter Filter;
};
struct CharacterInfo
{
uint32 Guid = 0;
uint32 AccountId = 0;
uint8 Race = 0;
uint8 Class = 0;
uint16 RidingSkill = 0;
};
struct MountSpellRequirements
{
uint16 RequiredRidingRank = 1;
uint32 ClassMask = 0;
};
ModuleConfig Config;
std::unordered_map<uint32, TeamId> MountSpellFactionMap;
std::unordered_map<uint32, MountSpellRequirements> MountSpellRequirementsMap;
bool IsMountSpell(uint32 spellId)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
return spellInfo && spellInfo->HasAura(SPELL_AURA_MOUNTED);
}
uint32 GetClassMaskForClass(uint8 classId)
{
if (!classId || classId >= MAX_CLASSES)
return 0;
return 1u << (classId - 1);
}
uint32 NormalizeClassMask(uint32 classMask)
{
classMask &= CLASSMASK_ALL_PLAYABLE;
return classMask && classMask != CLASSMASK_ALL_PLAYABLE ? classMask : 0;
}
uint16 GetRidingRankFromRequiredSpell(uint32 spellId)
{
switch (spellId)
{
case 33388: return 75; // Apprentice Riding
case 33391: return 150; // Journeyman Riding
case 34090: return 225; // Expert Riding
case 34091: return 300; // Artisan Riding
default: return 0;
}
}
MountSpellRequirements& GetMountSpellRequirements(uint32 spellId)
{
return MountSpellRequirementsMap.try_emplace(spellId).first->second;
}
void SetMountRidingRequirement(uint32 spellId, uint16 requiredRidingRank)
{
if (!IsMountSpell(spellId) || !requiredRidingRank)
return;
MountSpellRequirements& requirements = GetMountSpellRequirements(spellId);
requirements.RequiredRidingRank = std::max(requirements.RequiredRidingRank, requiredRidingRank);
}
void SetMountClassRestriction(uint32 spellId, uint32 classMask)
{
classMask = NormalizeClassMask(classMask);
if (!IsMountSpell(spellId) || !classMask)
return;
GetMountSpellRequirements(spellId).ClassMask |= classMask;
}
void ApplyClassMountFallbacks()
{
uint32 const paladinMask = GetClassMaskForClass(CLASS_PALADIN);
uint32 const warlockMask = GetClassMaskForClass(CLASS_WARLOCK);
uint32 const deathKnightMask = GetClassMaskForClass(CLASS_DEATH_KNIGHT);
uint32 const druidMask = GetClassMaskForClass(CLASS_DRUID);
for (uint32 spellId : { 13819u, 23214u, 34767u, 34769u, 66906u })
SetMountClassRestriction(spellId, paladinMask);
for (uint32 spellId : { 5784u, 23161u })
SetMountClassRestriction(spellId, warlockMask);
for (uint32 spellId : { 48778u, 54729u })
SetMountClassRestriction(spellId, deathKnightMask);
for (uint32 spellId : { 33943u, 40120u, 40121u })
SetMountClassRestriction(spellId, druidMask);
}
TeamId GetAllowedMountTeamForRaceMask(uint32 allowableRace)
{
uint32 const playableRaceMask = sRaceMgr->GetPlayableRaceMask();
uint32 const allowedRaceMask = allowableRace ? (allowableRace & playableRaceMask) : playableRaceMask;
if (!allowedRaceMask)
return TEAM_NEUTRAL;
bool const hasAllianceRace = (allowedRaceMask & sRaceMgr->GetAllianceRaceMask()) != 0;
bool const hasHordeRace = (allowedRaceMask & sRaceMgr->GetHordeRaceMask()) != 0;
if (hasAllianceRace && !hasHordeRace)
return TEAM_ALLIANCE;
if (hasHordeRace && !hasAllianceRace)
return TEAM_HORDE;
return TEAM_NEUTRAL;
}
void SetMountSpellFaction(uint32 spellId, TeamId teamId, bool force = false)
{
if (!IsMountSpell(spellId))
return;
auto itr = MountSpellFactionMap.find(spellId);
if (itr == MountSpellFactionMap.end())
{
MountSpellFactionMap.emplace(spellId, teamId);
return;
}
if (force)
{
itr->second = teamId;
return;
}
if (teamId == TEAM_NEUTRAL)
{
itr->second = TEAM_NEUTRAL;
return;
}
if (itr->second == TEAM_NEUTRAL)
{
itr->second = teamId;
return;
}
if (itr->second != teamId)
itr->second = TEAM_NEUTRAL;
}
void BuildMountCache()
{
MountSpellFactionMap.clear();
MountSpellRequirementsMap.clear();
for (uint32 spellId = 1; spellId < sSpellMgr->GetSpellInfoStoreSize(); ++spellId)
{
if (!IsMountSpell(spellId))
continue;
MountSpellFactionMap.emplace(spellId, TEAM_NEUTRAL);
MountSpellRequirementsMap.emplace(spellId, MountSpellRequirements{});
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
for (SkillLineAbilityMap::const_iterator itr = bounds.first; itr != bounds.second; ++itr)
{
SkillLineAbilityEntry const* skillLineAbility = itr->second;
if (!skillLineAbility)
continue;
SetMountClassRestriction(spellId, skillLineAbility->ClassMask);
if (skillLineAbility->SkillLine == SKILL_RIDING)
SetMountRidingRequirement(spellId, uint16(skillLineAbility->MinSkillLineRank));
}
}
if (ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore())
{
for (auto const& itemTemplatePair : *itemTemplates)
{
ItemTemplate const& itemTemplate = itemTemplatePair.second;
if (itemTemplate.Class != ITEM_CLASS_MISC || itemTemplate.SubClass != ITEM_SUBCLASS_JUNK_MOUNT)
continue;
TeamId const itemTeam = GetAllowedMountTeamForRaceMask(itemTemplate.AllowableRace);
for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
{
int32 const itemSpellId = itemTemplate.Spells[i].SpellId;
if (itemSpellId <= 0 || !IsMountSpell(uint32(itemSpellId)))
continue;
SetMountSpellFaction(uint32(itemSpellId), itemTeam);
SetMountClassRestriction(uint32(itemSpellId), itemTemplate.AllowableClass);
if (itemTemplate.RequiredSkill == SKILL_RIDING)
SetMountRidingRequirement(uint32(itemSpellId), uint16(itemTemplate.RequiredSkillRank));
SetMountRidingRequirement(uint32(itemSpellId), GetRidingRankFromRequiredSpell(itemTemplate.RequiredSpell));
}
}
}
for (auto const& [allianceSpell, hordeSpell] : sObjectMgr->FactionChangeSpells)
{
if (!IsMountSpell(allianceSpell) || !IsMountSpell(hordeSpell))
continue;
SetMountSpellFaction(allianceSpell, TEAM_ALLIANCE, true);
SetMountSpellFaction(hordeSpell, TEAM_HORDE, true);
}
ApplyClassMountFallbacks();
LOG_INFO("module.accountboundmounts",
"AccountBoundMounts: cached {} faction rule(s) and {} requirement rule(s).",
MountSpellFactionMap.size(), MountSpellRequirementsMap.size());
}
TeamId GetMountSpellFaction(uint32 spellId)
{
auto itr = MountSpellFactionMap.find(spellId);
return itr != MountSpellFactionMap.end() ? itr->second : TEAM_NEUTRAL;
}
uint32 GetFactionConvertedMountSpell(uint32 spellId, TeamId targetTeam)
{
for (auto const& [allianceSpell, hordeSpell] : sObjectMgr->FactionChangeSpells)
{
if (!IsMountSpell(allianceSpell) || !IsMountSpell(hordeSpell))
continue;
if (spellId == allianceSpell && targetTeam == TEAM_HORDE)
return hordeSpell;
if (spellId == hordeSpell && targetTeam == TEAM_ALLIANCE)
return allianceSpell;
}
return 0;
}
bool IsMountSpellAllowedForTeam(uint32 spellId, TeamId targetTeam)
{
if (!Config.RespectFactionRestrictions)
return true;
TeamId const mountTeam = GetMountSpellFaction(spellId);
return mountTeam == TEAM_NEUTRAL || mountTeam == targetTeam;
}
uint32 GetMountSpellForRace(uint32 spellId, uint8 race)
{
if (!Config.Filter.Allows(spellId) || !IsMountSpell(spellId))
return 0;
TeamId const targetTeam = Player::TeamIdForRace(race);
if (IsMountSpellAllowedForTeam(spellId, targetTeam))
return spellId;
if (!Config.ConvertFactionSpecific)
return 0;
uint32 const convertedSpellId = GetFactionConvertedMountSpell(spellId, targetTeam);
return Config.Filter.Allows(convertedSpellId) &&
IsMountSpellAllowedForTeam(convertedSpellId, targetTeam) ? convertedSpellId : 0;
}
bool IsClassRestrictedMount(uint32 spellId)
{
return GetMountSpellRequirements(spellId).ClassMask != 0;
}
bool ShouldAccountSyncMount(uint32 spellId)
{
return Config.Filter.Allows(spellId) && IsMountSpell(spellId) &&
(Config.SyncClassMounts || !IsClassRestrictedMount(spellId));
}
bool CanShareClassMountFromRace(uint32 spellId, uint8 sourceRace, uint8 targetRace)
{
if (!Config.ClassMountsSameFactionOnly || !sourceRace || !IsClassRestrictedMount(spellId))
return true;
return Player::TeamIdForRace(sourceRace) == Player::TeamIdForRace(targetRace);
}
bool CanCharacterReceiveMount(uint32 spellId, uint8 classId, uint16 ridingSkill, uint8 sourceRace = 0, uint8 targetRace = 0)
{
MountSpellRequirements const& requirements = GetMountSpellRequirements(spellId);
if (Config.RequireRiding && ridingSkill < requirements.RequiredRidingRank)
return false;
if (Config.RequireClass && requirements.ClassMask && !(requirements.ClassMask & GetClassMaskForClass(classId)))
return false;
return !targetRace || CanShareClassMountFromRace(spellId, sourceRace, targetRace);
}
bool CanCharacterReceiveMount(uint32 spellId, CharacterInfo const& character, uint8 sourceRace = 0)
{
return CanCharacterReceiveMount(spellId, character.Class, character.RidingSkill, sourceRace, character.Race);
}
bool CanPlayerReceiveMount(uint32 spellId, Player* player, uint8 sourceRace = 0)
{
return player && CanCharacterReceiveMount(
spellId, player->getClass(), player->GetSkillValue(SKILL_RIDING), sourceRace, player->getRace(true));
}
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);
}
void InsertMountForCharacter(CharacterDatabaseTransaction& trans, uint32 targetGuid, uint32 spellId)
{
AppendOrCommit(trans, Acore::StringFormat(
"INSERT IGNORE INTO character_spell (guid, spell, specMask) VALUES ({}, {}, {})",
targetGuid, spellId, uint32(SPEC_MASK_ALL)));
}
void DeleteMountForCharacter(CharacterDatabaseTransaction& trans, uint32 targetGuid, uint32 spellId)
{
AppendOrCommit(trans, Acore::StringFormat(
"DELETE FROM character_spell WHERE guid = {} AND spell = {}",
targetGuid, spellId));
}
std::vector<CharacterInfo> LoadAccountCharacters(uint32 accountId)
{
std::vector<CharacterInfo> characters;
QueryResult result = CharacterDatabase.Query(
"SELECT c.guid, c.account, c.race, c.`class`, COALESCE(cs.value, 0) "
"FROM characters c "
"LEFT JOIN character_skills cs ON cs.guid = c.guid AND cs.skill = {} "
"WHERE c.account = {}",
uint32(SKILL_RIDING), accountId);
if (!result)
return characters;
do
{
Field* fields = result->Fetch();
characters.push_back({
fields[0].Get<uint32>(),
fields[1].Get<uint32>(),
fields[2].Get<uint8>(),
fields[3].Get<uint8>(),
fields[4].Get<uint16>()
});
} while (result->NextRow());
return characters;
}
void SyncMountToAccount(Player* player, uint32 spellId)
{
if (!Config.Enabled || !player || !ShouldAccountSyncMount(spellId))
return;
uint8 const sourceRace = player->getRace(true);
std::vector<CharacterInfo> characters = LoadAccountCharacters(player->GetSession()->GetAccountId());
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint32 synced = 0;
for (CharacterInfo const& target : characters)
{
if (target.Guid == player->GetGUID().GetCounter())
continue;
uint32 const targetSpellId = GetMountSpellForRace(spellId, target.Race);
if (!targetSpellId || !CanCharacterReceiveMount(targetSpellId, target, sourceRace))
continue;
InsertMountForCharacter(trans, target.Guid, targetSpellId);
++synced;
}
CommitIfNeeded(trans);
if (synced)
LOG_DEBUG("module.accountboundmounts",
"AccountBoundMounts: synced mount spell {} from player {} to {} account character(s).",
spellId, player->GetGUID().GetCounter(), synced);
}
void BackfillMountsForCharacter(Player* player)
{
if (!Config.Enabled || !player)
return;
uint32 const accountId = player->GetSession()->GetAccountId();
uint32 const targetGuid = player->GetGUID().GetCounter();
uint8 const targetRace = player->getRace(true);
QueryResult result = CharacterDatabase.Query(
"SELECT DISTINCT cs.spell, c.race "
"FROM character_spell cs "
"INNER JOIN characters c ON c.guid = cs.guid "
"WHERE c.account = {} AND c.guid <> {}",
accountId, targetGuid);
if (!result)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint32 inserted = 0;
do
{
Field* fields = result->Fetch();
uint32 const spellId = fields[0].Get<uint32>();
uint8 const sourceRace = fields[1].Get<uint8>();
if (!ShouldAccountSyncMount(spellId))
continue;
uint32 const targetSpellId = GetMountSpellForRace(spellId, targetRace);
if (!targetSpellId || !CanPlayerReceiveMount(targetSpellId, player, sourceRace))
continue;
InsertMountForCharacter(trans, targetGuid, targetSpellId);
++inserted;
} while (result->NextRow());
CommitIfNeeded(trans);
if (inserted)
LOG_INFO("module.accountboundmounts",
"AccountBoundMounts: seeded {} mount spell row(s) for character {}.",
inserted, targetGuid);
}
void BackfillAllMounts()
{
if (!Config.Enabled || !Config.StartupBackfill)
return;
QueryResult charactersResult = CharacterDatabase.Query(
"SELECT c.guid, c.account, c.race, c.`class`, COALESCE(cs.value, 0) "
"FROM characters c "
"LEFT JOIN character_skills cs ON cs.guid = c.guid AND cs.skill = {} "
"WHERE c.account <> 0",
uint32(SKILL_RIDING));
if (!charactersResult)
return;
std::unordered_map<uint32, std::vector<CharacterInfo>> charactersByAccount;
do
{
Field* fields = charactersResult->Fetch();
CharacterInfo character = {
fields[0].Get<uint32>(),
fields[1].Get<uint32>(),
fields[2].Get<uint8>(),
fields[3].Get<uint8>(),
fields[4].Get<uint16>()
};
charactersByAccount[character.AccountId].push_back(character);
} while (charactersResult->NextRow());
QueryResult spellsResult = CharacterDatabase.Query(
"SELECT DISTINCT c.account, c.race, cs.spell "
"FROM character_spell cs "
"INNER JOIN characters c ON c.guid = cs.guid "
"WHERE c.account <> 0");
if (!spellsResult)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint64 inserted = 0;
do
{
Field* fields = spellsResult->Fetch();
uint32 const accountId = fields[0].Get<uint32>();
uint8 const sourceRace = fields[1].Get<uint8>();
uint32 const spellId = fields[2].Get<uint32>();
if (!ShouldAccountSyncMount(spellId))
continue;
auto accountItr = charactersByAccount.find(accountId);
if (accountItr == charactersByAccount.end())
continue;
for (CharacterInfo const& target : accountItr->second)
{
uint32 const targetSpellId = GetMountSpellForRace(spellId, target.Race);
if (!targetSpellId || !CanCharacterReceiveMount(targetSpellId, target, sourceRace))
continue;
InsertMountForCharacter(trans, target.Guid, targetSpellId);
++inserted;
}
} while (spellsResult->NextRow());
CommitIfNeeded(trans);
LOG_INFO("module.accountboundmounts",
"AccountBoundMounts: startup backfill queued {} mount spell row(s).", inserted);
}
void CleanupInvalidMounts()
{
if (!Config.Enabled || !Config.CleanupInvalid)
return;
QueryResult result = CharacterDatabase.Query(
"SELECT c.guid, c.race, c.`class`, COALESCE(sk.value, 0), cs.spell "
"FROM character_spell cs "
"INNER JOIN characters c ON c.guid = cs.guid "
"LEFT JOIN character_skills sk ON sk.guid = c.guid AND sk.skill = {}",
uint32(SKILL_RIDING));
if (!result)
return;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint64 removed = 0;
do
{
Field* fields = result->Fetch();
CharacterInfo character = {
fields[0].Get<uint32>(),
0,
fields[1].Get<uint8>(),
fields[2].Get<uint8>(),
fields[3].Get<uint16>()
};
uint32 const spellId = fields[4].Get<uint32>();
if (!ShouldAccountSyncMount(spellId))
continue;
uint32 const allowedSpellId = GetMountSpellForRace(spellId, character.Race);
if (allowedSpellId == spellId && CanCharacterReceiveMount(spellId, character))
continue;
DeleteMountForCharacter(trans, character.Guid, spellId);
++removed;
} while (result->NextRow());
CommitIfNeeded(trans);
if (removed)
LOG_INFO("module.accountboundmounts",
"AccountBoundMounts: removed {} invalid mount spell row(s).", removed);
}
void LoadModuleConfig()
{
Config.Enabled = AccountBound::IsCategoryEnabled("Mounts");
Config.StartupBackfill = sConfigMgr->GetOption<bool>("AccountBound.Mounts.StartupBackfill", true);
Config.SyncOnCreate = sConfigMgr->GetOption<bool>("AccountBound.Mounts.SyncOnCreate", true);
Config.ConvertFactionSpecific = sConfigMgr->GetOption<bool>("AccountBound.Mounts.ConvertFactionSpecific", true);
Config.RespectFactionRestrictions = sConfigMgr->GetOption<bool>("AccountBound.Mounts.RespectFactionRestrictions", true);
Config.RequireRiding = sConfigMgr->GetOption<bool>("AccountBound.Mounts.RequireRiding", true);
Config.SyncClassMounts = sConfigMgr->GetOption<bool>("AccountBound.Mounts.SyncClassMounts", true);
Config.RequireClass = sConfigMgr->GetOption<bool>("AccountBound.Mounts.RequireClass", true);
Config.ClassMountsSameFactionOnly = sConfigMgr->GetOption<bool>("AccountBound.Mounts.ClassMountsSameFactionOnly", true);
Config.CleanupInvalid = sConfigMgr->GetOption<bool>("AccountBound.Mounts.CleanupInvalid", true);
Config.BackfillOnRidingSkillChange = sConfigMgr->GetOption<bool>("AccountBound.Mounts.BackfillOnRidingSkillChange", true);
Config.Filter = AccountBound::LoadIdFilter("Mounts");
}
}
class AccountBoundMountsWorldScript : public WorldScript
{
public:
AccountBoundMountsWorldScript() : WorldScript("AccountBoundMountsWorldScript", {
WORLDHOOK_ON_AFTER_CONFIG_LOAD,
WORLDHOOK_ON_STARTUP
}) { }
void OnAfterConfigLoad(bool reload) override
{
LoadModuleConfig();
LOG_INFO("module.accountboundmounts",
"AccountBoundMounts: {}. CreateSync={}, StartupBackfill={}, RequireRiding={}, RequireClass={}.",
Config.Enabled ? (reload ? "configuration reloaded" : "configuration loaded") : "disabled",
Config.Enabled && Config.SyncOnCreate ? "on" : "off",
Config.Enabled && Config.StartupBackfill ? "on" : "off",
Config.Enabled && Config.RequireRiding ? "on" : "off",
Config.Enabled && Config.RequireClass ? "on" : "off");
}
void OnStartup() override
{
if (!Config.Enabled)
return;
BuildMountCache();
CleanupInvalidMounts();
BackfillAllMounts();
}
};
class AccountBoundMountsPlayerScript : public PlayerScript
{
public:
AccountBoundMountsPlayerScript() : PlayerScript("AccountBoundMountsPlayerScript", {
PLAYERHOOK_ON_CREATE,
PLAYERHOOK_ON_LEARN_SPELL,
PLAYERHOOK_ON_SET_SKILL
}) { }
void OnPlayerCreate(Player* player) override
{
if (Config.SyncOnCreate)
BackfillMountsForCharacter(player);
}
void OnPlayerLearnSpell(Player* player, uint32 spellId) override
{
SyncMountToAccount(player, spellId);
}
void OnPlayerSetSkill(Player* player, uint32 skillId, uint32 value, uint32 /*max*/, uint32 /*step*/, uint32 newValue) override
{
if (!Config.Enabled || !Config.BackfillOnRidingSkillChange || skillId != SKILL_RIDING || newValue <= value)
return;
BackfillMountsForCharacter(player);
}
};
void AddAccountBoundMountsScripts()
{
new AccountBoundMountsWorldScript();
new AccountBoundMountsPlayerScript();
}
+348
View File
@@ -0,0 +1,348 @@
/*
* Account-bound companion pets for AzerothCore.
*/
#include "AccountBound.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
#include "Log.h"
#include "Player.h"
#include "PlayerScript.h"
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "StringFormat.h"
#include "WorldScript.h"
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace
{
struct ModuleConfig
{
bool Enabled = true;
bool StartupBackfill = true;
bool SyncOnCreate = true;
bool SameFactionOnly = false;
bool IncludeCompanionSkillLine = true;
bool IncludeMinipetSummons = true;
AccountBound::IdFilter Filter;
};
struct CharacterInfo
{
uint32 Guid = 0;
uint8 Race = 0;
};
ModuleConfig Config;
std::unordered_set<uint32> CompanionSpells;
bool HasCompanionSkillLine(uint32 spellId)
{
SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellId);
for (SkillLineAbilityMap::const_iterator itr = bounds.first; itr != bounds.second; ++itr)
if (itr->second && itr->second->SkillLine == SKILL_COMPANIONS)
return true;
return false;
}
bool IsMinipetSummon(SpellInfo const* spellInfo)
{
if (!spellInfo)
return false;
for (uint8 effectIndex = 0; effectIndex < MAX_SPELL_EFFECTS; ++effectIndex)
{
SpellEffectInfo const& effect = spellInfo->Effects[effectIndex];
if (effect.Effect != SPELL_EFFECT_SUMMON || effect.MiscValue <= 0)
continue;
SummonPropertiesEntry const* properties = sSummonPropertiesStore.LookupEntry(effect.MiscValueB);
if (properties && properties->Type == SUMMON_TYPE_MINIPET)
return true;
}
return false;
}
bool IsCompanionSpell(uint32 spellId)
{
return Config.Filter.Allows(spellId) && CompanionSpells.find(spellId) != CompanionSpells.end();
}
void BuildCompanionCache()
{
CompanionSpells.clear();
for (uint32 spellId = 1; spellId < sSpellMgr->GetSpellInfoStoreSize(); ++spellId)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
continue;
bool const companionSkill = Config.IncludeCompanionSkillLine && HasCompanionSkillLine(spellId);
bool const minipetSummon = Config.IncludeMinipetSummons && IsMinipetSummon(spellInfo);
if (companionSkill || minipetSummon)
CompanionSpells.insert(spellId);
}
LOG_INFO("module.accountboundpets",
"AccountBoundPets: cached {} companion pet spell(s).", CompanionSpells.size());
}
bool CanShareBetweenRaces(uint8 sourceRace, uint8 targetRace)
{
return !Config.SameFactionOnly || Player::TeamIdForRace(sourceRace) == Player::TeamIdForRace(targetRace);
}
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);
}
void InsertCompanionForCharacter(CharacterDatabaseTransaction& trans, uint32 targetGuid, uint32 spellId)
{
AppendOrCommit(trans, Acore::StringFormat(
"INSERT IGNORE INTO character_spell (guid, spell, specMask) VALUES ({}, {}, {})",
targetGuid, spellId, uint32(SPEC_MASK_ALL)));
}
std::vector<CharacterInfo> LoadAccountCharacters(uint32 accountId)
{
std::vector<CharacterInfo> characters;
QueryResult result = CharacterDatabase.Query(
"SELECT guid, race FROM characters WHERE account = {}", accountId);
if (!result)
return characters;
do
{
Field* fields = result->Fetch();
characters.push_back({
fields[0].Get<uint32>(),
fields[1].Get<uint8>()
});
} while (result->NextRow());
return characters;
}
void SyncCompanionToAccount(Player* player, uint32 spellId)
{
if (!Config.Enabled || !player || !IsCompanionSpell(spellId))
return;
uint32 const sourceGuid = player->GetGUID().GetCounter();
uint8 const sourceRace = player->getRace(true);
std::vector<CharacterInfo> characters = LoadAccountCharacters(player->GetSession()->GetAccountId());
CharacterDatabaseTransaction trans;
uint32 synced = 0;
for (CharacterInfo const& target : characters)
{
if (target.Guid == sourceGuid || !CanShareBetweenRaces(sourceRace, target.Race))
continue;
InsertCompanionForCharacter(trans, target.Guid, spellId);
++synced;
}
CommitIfNeeded(trans);
if (synced)
LOG_DEBUG("module.accountboundpets",
"AccountBoundPets: synced companion spell {} from player {} to {} account character(s).",
spellId, sourceGuid, synced);
}
void SeedCompanionsForCharacter(Player* player)
{
if (!Config.Enabled || !player)
return;
uint32 const accountId = player->GetSession()->GetAccountId();
uint32 const targetGuid = player->GetGUID().GetCounter();
uint8 const targetRace = player->getRace(true);
QueryResult result = CharacterDatabase.Query(
"SELECT DISTINCT cs.spell, c.race "
"FROM character_spell cs "
"INNER JOIN characters c ON c.guid = cs.guid "
"WHERE c.account = {} AND c.guid <> {}",
accountId, targetGuid);
if (!result)
return;
CharacterDatabaseTransaction trans;
uint32 inserted = 0;
do
{
Field* fields = result->Fetch();
uint32 const spellId = fields[0].Get<uint32>();
uint8 const sourceRace = fields[1].Get<uint8>();
if (!IsCompanionSpell(spellId) || !CanShareBetweenRaces(sourceRace, targetRace))
continue;
InsertCompanionForCharacter(trans, targetGuid, spellId);
++inserted;
} while (result->NextRow());
CommitIfNeeded(trans);
if (inserted)
LOG_INFO("module.accountboundpets",
"AccountBoundPets: seeded {} companion spell row(s) for character {}.",
inserted, targetGuid);
}
void BackfillAllCompanions()
{
if (!Config.Enabled || !Config.StartupBackfill)
return;
QueryResult charactersResult = CharacterDatabase.Query(
"SELECT guid, account, race FROM characters WHERE account <> 0");
if (!charactersResult)
return;
std::unordered_map<uint32, std::vector<CharacterInfo>> charactersByAccount;
do
{
Field* fields = charactersResult->Fetch();
charactersByAccount[fields[1].Get<uint32>()].push_back({
fields[0].Get<uint32>(),
fields[2].Get<uint8>()
});
} while (charactersResult->NextRow());
QueryResult spellsResult = CharacterDatabase.Query(
"SELECT DISTINCT c.account, c.race, cs.spell "
"FROM character_spell cs "
"INNER JOIN characters c ON c.guid = cs.guid "
"WHERE c.account <> 0");
if (!spellsResult)
return;
CharacterDatabaseTransaction trans;
uint64 queued = 0;
do
{
Field* fields = spellsResult->Fetch();
uint32 const accountId = fields[0].Get<uint32>();
uint8 const sourceRace = fields[1].Get<uint8>();
uint32 const spellId = fields[2].Get<uint32>();
if (!IsCompanionSpell(spellId))
continue;
auto accountItr = charactersByAccount.find(accountId);
if (accountItr == charactersByAccount.end())
continue;
for (CharacterInfo const& target : accountItr->second)
{
if (!CanShareBetweenRaces(sourceRace, target.Race))
continue;
InsertCompanionForCharacter(trans, target.Guid, spellId);
++queued;
}
} while (spellsResult->NextRow());
CommitIfNeeded(trans);
LOG_INFO("module.accountboundpets",
"AccountBoundPets: startup backfill queued {} companion spell row(s).", queued);
}
void LoadModuleConfig()
{
Config.Enabled = AccountBound::IsCategoryEnabled("Pets");
Config.StartupBackfill = sConfigMgr->GetOption<bool>("AccountBound.Pets.StartupBackfill", true);
Config.SyncOnCreate = sConfigMgr->GetOption<bool>("AccountBound.Pets.SyncOnCreate", true);
Config.SameFactionOnly = sConfigMgr->GetOption<bool>("AccountBound.Pets.SameFactionOnly", false);
Config.IncludeCompanionSkillLine = sConfigMgr->GetOption<bool>("AccountBound.Pets.IncludeCompanionSkillLine", true);
Config.IncludeMinipetSummons = sConfigMgr->GetOption<bool>("AccountBound.Pets.IncludeMinipetSummons", true);
Config.Filter = AccountBound::LoadIdFilter("Pets");
}
}
class AccountBoundPetsWorldScript : public WorldScript
{
public:
AccountBoundPetsWorldScript() : WorldScript("AccountBoundPetsWorldScript", {
WORLDHOOK_ON_AFTER_CONFIG_LOAD,
WORLDHOOK_ON_STARTUP
}) { }
void OnAfterConfigLoad(bool reload) override
{
LoadModuleConfig();
LOG_INFO("module.accountboundpets",
"AccountBoundPets: {}. StartupBackfill={}, CreateSync={}, SameFactionOnly={}.",
Config.Enabled ? (reload ? "configuration reloaded" : "configuration loaded") : "disabled",
Config.Enabled && Config.StartupBackfill ? "on" : "off",
Config.Enabled && Config.SyncOnCreate ? "on" : "off",
Config.Enabled && Config.SameFactionOnly ? "on" : "off");
}
void OnStartup() override
{
if (!Config.Enabled)
return;
BuildCompanionCache();
BackfillAllCompanions();
}
};
class AccountBoundPetsPlayerScript : public PlayerScript
{
public:
AccountBoundPetsPlayerScript() : PlayerScript("AccountBoundPetsPlayerScript", {
PLAYERHOOK_ON_CREATE,
PLAYERHOOK_ON_LEARN_SPELL
}) { }
void OnPlayerCreate(Player* player) override
{
if (Config.SyncOnCreate)
SeedCompanionsForCharacter(player);
}
void OnPlayerLearnSpell(Player* player, uint32 spellId) override
{
SyncCompanionToAccount(player, spellId);
}
};
void AddAccountBoundPetsScripts()
{
new AccountBoundPetsWorldScript();
new AccountBoundPetsPlayerScript();
}
File diff suppressed because it is too large Load Diff
+419
View File
@@ -0,0 +1,419 @@
/*
* 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 <algorithm>
#include <string_view>
#include <unordered_map>
#include <vector>
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<uint32, ReputationInfo> 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<CharacterInfo> LoadCharacters(uint32 accountId = 0)
{
std::vector<CharacterInfo> characters;
std::unordered_map<uint32, std::size_t> 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<uint32>();
auto [itr, inserted] = characterIndexes.try_emplace(guid, characters.size());
if (inserted)
{
characters.push_back({
guid,
fields[1].Get<uint32>(),
fields[2].Get<uint8>(),
fields[3].Get<uint8>(),
{}
});
}
if (!fields[4].IsNull())
{
characters[itr->second].Reputations[fields[4].Get<uint32>()] = {
fields[5].Get<int32>(),
fields[6].Get<uint16>()
};
}
} 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<CharacterInfo> 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<CharacterInfo> 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<CharacterInfo> characters = LoadCharacters();
std::unordered_map<uint32, std::vector<std::size_t>> 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<CharacterInfo> const snapshot = [&]()
{
std::vector<CharacterInfo> 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<bool>("AccountBound.Reputations.StartupBackfill", true);
Config.SyncOnCreate = sConfigMgr->GetOption<bool>("AccountBound.Reputations.SyncOnCreate", true);
Config.SyncOnChange = sConfigMgr->GetOption<bool>("AccountBound.Reputations.SyncOnChange", true);
Config.SameFactionOnly = sConfigMgr->GetOption<bool>("AccountBound.Reputations.SameFactionOnly", true);
Config.ConvertFactionSpecific = sConfigMgr->GetOption<bool>("AccountBound.Reputations.ConvertFactionSpecific", true);
Config.SyncUnpairedCrossFaction = sConfigMgr->GetOption<bool>("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();
}
+466
View File
@@ -0,0 +1,466 @@
/*
* Account-bound character titles for AzerothCore.
*/
#include "AchievementMgr.h"
#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 "StringFormat.h"
#include "WorldScript.h"
#include <array>
#include <sstream>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace
{
using KnownTitlesMask = std::array<uint32, KNOWN_TITLES_SIZE * 2>;
struct ModuleConfig
{
bool Enabled = true;
bool StartupBackfill = true;
bool SyncOnCreate = true;
bool SyncOnSave = true;
bool SyncRealmFirst = false;
bool ConvertFactionSpecific = true;
AccountBound::IdFilter Filter;
};
ModuleConfig Config;
std::unordered_map<uint32, uint32> TitleIdByBitIndex;
std::unordered_set<uint32> RealmFirstTitleIds;
std::unordered_map<uint32, KnownTitlesMask> LastKnownTitlesByCharacter;
bool IsRealmFirstAchievement(AchievementEntry const* achievement)
{
return achievement && (achievement->flags & (ACHIEVEMENT_FLAG_REALM_FIRST_REACH | ACHIEVEMENT_FLAG_REALM_FIRST_KILL));
}
bool IsTitleAllowed(uint32 titleId)
{
return Config.Filter.Allows(titleId) &&
(Config.SyncRealmFirst || !RealmFirstTitleIds.contains(titleId));
}
void BuildTitleCache()
{
TitleIdByBitIndex.clear();
RealmFirstTitleIds =
{
// Restored beta/datamined Realm First titles used by RealmFirstTitles.
85, 86, 87, 89, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
116, 117, 118, 119, 123, 156
};
for (uint32 titleId = 0; titleId < sCharTitlesStore.GetNumRows(); ++titleId)
if (CharTitlesEntry const* title = sCharTitlesStore.LookupEntry(titleId))
TitleIdByBitIndex.try_emplace(title->bit_index, titleId);
for (uint32 achievementId = 0; achievementId < sAchievementStore.GetNumRows(); ++achievementId)
{
AchievementEntry const* achievement = sAchievementStore.LookupEntry(achievementId);
if (!IsRealmFirstAchievement(achievement))
continue;
if (AchievementReward const* reward = sAchievementMgr->GetAchievementReward(achievement))
for (uint32 titleId : reward->titleId)
if (titleId)
RealmFirstTitleIds.insert(titleId);
}
LOG_INFO("module.accountboundtitles",
"AccountBoundTitles: cached {} title bit mapping(s) and {} Realm First exclusion(s).",
TitleIdByBitIndex.size(), RealmFirstTitleIds.size());
}
KnownTitlesMask ParseKnownTitles(std::string const& text)
{
KnownTitlesMask titles = {};
std::stringstream stream(text);
for (uint32& value : titles)
stream >> value;
return titles;
}
KnownTitlesMask GetPlayerKnownTitles(Player const* player)
{
KnownTitlesMask titles = {};
if (!player)
return titles;
for (uint32 index = 0; index < titles.size(); ++index)
titles[index] = player->GetUInt32Value(PLAYER__FIELD_KNOWN_TITLES + index);
return titles;
}
std::string KnownTitlesToString(KnownTitlesMask const& titles)
{
std::ostringstream stream;
for (uint32 index = 0; index < titles.size(); ++index)
{
if (index)
stream << ' ';
stream << titles[index];
}
return stream.str();
}
bool HasKnownTitle(KnownTitlesMask const& titles, uint32 bitIndex)
{
if (bitIndex >= titles.size() * 32)
return false;
return (titles[bitIndex / 32] & (1u << (bitIndex % 32))) != 0;
}
bool AddKnownTitle(KnownTitlesMask& titles, uint32 bitIndex)
{
if (bitIndex >= titles.size() * 32)
return false;
uint32& mask = titles[bitIndex / 32];
uint32 const flag = 1u << (bitIndex % 32);
if (mask & flag)
return false;
mask |= flag;
return true;
}
uint32 GetTitleForRace(uint32 titleId, uint8 sourceRace, uint8 targetRace)
{
if (!IsTitleAllowed(titleId))
return 0;
TeamId const sourceTeam = Player::TeamIdForRace(sourceRace);
TeamId const targetTeam = Player::TeamIdForRace(targetRace);
if (sourceTeam == targetTeam)
return titleId;
for (auto const& [allianceTitle, hordeTitle] : sObjectMgr->FactionChangeTitles)
{
if (titleId == allianceTitle)
return Config.ConvertFactionSpecific && targetTeam == TEAM_HORDE &&
IsTitleAllowed(hordeTitle) ? hordeTitle : 0;
if (titleId == hordeTitle)
return Config.ConvertFactionSpecific && targetTeam == TEAM_ALLIANCE &&
IsTitleAllowed(allianceTitle) ? allianceTitle : 0;
}
return titleId;
}
uint32 MergeTitlesForRace(KnownTitlesMask const& sourceTitles, uint8 sourceRace, uint8 targetRace, KnownTitlesMask& targetTitles)
{
uint32 added = 0;
for (uint32 sourceBitIndex = 0; sourceBitIndex < sourceTitles.size() * 32; ++sourceBitIndex)
{
if (!HasKnownTitle(sourceTitles, sourceBitIndex))
continue;
auto titleItr = TitleIdByBitIndex.find(sourceBitIndex);
if (titleItr == TitleIdByBitIndex.end())
continue;
uint32 const targetTitleId = GetTitleForRace(titleItr->second, sourceRace, targetRace);
CharTitlesEntry const* targetTitle = sCharTitlesStore.LookupEntry(targetTitleId);
if (targetTitle && AddKnownTitle(targetTitles, targetTitle->bit_index))
++added;
}
return added;
}
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);
}
void UpdateTitlesForCharacter(CharacterDatabaseTransaction& trans, uint32 targetGuid, KnownTitlesMask const& titles)
{
AppendOrCommit(trans, Acore::StringFormat(
"UPDATE characters SET knownTitles = '{}' WHERE guid = {}",
KnownTitlesToString(titles), targetGuid));
}
void BackfillTitlesForCharacter(Player* player)
{
if (!Config.Enabled || !player)
return;
uint32 const accountId = player->GetSession()->GetAccountId();
uint32 const targetGuid = player->GetGUID().GetCounter();
uint8 const targetRace = player->getRace(true);
KnownTitlesMask targetTitles = {};
uint32 added = 0;
QueryResult targetResult = CharacterDatabase.Query(
"SELECT COALESCE(knownTitles, '') FROM characters WHERE guid = {}", targetGuid);
if (targetResult)
targetTitles = ParseKnownTitles(targetResult->Fetch()[0].Get<std::string>());
QueryResult result = CharacterDatabase.Query(
"SELECT guid, race, COALESCE(knownTitles, '') "
"FROM characters WHERE account = {} AND guid <> {}",
accountId, targetGuid);
if (result)
{
do
{
Field* fields = result->Fetch();
uint8 const sourceRace = fields[1].Get<uint8>();
KnownTitlesMask const sourceTitles = ParseKnownTitles(fields[2].Get<std::string>());
added += MergeTitlesForRace(sourceTitles, sourceRace, targetRace, targetTitles);
} while (result->NextRow());
}
if (added)
{
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
UpdateTitlesForCharacter(trans, targetGuid, targetTitles);
CommitIfNeeded(trans);
LOG_INFO("module.accountboundtitles",
"AccountBoundTitles: seeded {} account title(s) for character {}.",
added, targetGuid);
}
}
void SyncTitlesFromPlayerToAccount(Player* player)
{
if (!Config.Enabled || !Config.SyncOnSave || !player)
return;
uint32 const sourceGuid = player->GetGUID().GetCounter();
uint8 const sourceRace = player->getRace(true);
KnownTitlesMask const sourceTitles = GetPlayerKnownTitles(player);
auto snapshotItr = LastKnownTitlesByCharacter.find(sourceGuid);
if (snapshotItr != LastKnownTitlesByCharacter.end() && snapshotItr->second == sourceTitles)
return;
QueryResult result = CharacterDatabase.Query(
"SELECT guid, race, COALESCE(knownTitles, '') "
"FROM characters WHERE account = {} AND guid <> {}",
player->GetSession()->GetAccountId(), sourceGuid);
if (!result)
{
LastKnownTitlesByCharacter[sourceGuid] = sourceTitles;
return;
}
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint32 updatedCharacters = 0;
uint32 addedTitles = 0;
do
{
Field* fields = result->Fetch();
uint32 const targetGuid = fields[0].Get<uint32>();
uint8 const targetRace = fields[1].Get<uint8>();
KnownTitlesMask targetTitles = ParseKnownTitles(fields[2].Get<std::string>());
uint32 const added = MergeTitlesForRace(sourceTitles, sourceRace, targetRace, targetTitles);
if (!added)
continue;
UpdateTitlesForCharacter(trans, targetGuid, targetTitles);
++updatedCharacters;
addedTitles += added;
} while (result->NextRow());
CommitIfNeeded(trans);
LastKnownTitlesByCharacter[sourceGuid] = sourceTitles;
if (updatedCharacters)
LOG_INFO("module.accountboundtitles",
"AccountBoundTitles: propagated {} title(s) from character {} to {} account character(s).",
addedTitles, sourceGuid, updatedCharacters);
}
void BackfillAllTitles()
{
if (!Config.Enabled || !Config.StartupBackfill)
return;
struct TitleCharacterInfo
{
uint32 Guid;
uint8 Race;
KnownTitlesMask Titles;
};
QueryResult result = CharacterDatabase.Query(
"SELECT guid, account, race, COALESCE(knownTitles, '') "
"FROM characters WHERE account <> 0");
if (!result)
return;
std::unordered_map<uint32, std::vector<TitleCharacterInfo>> charactersByAccount;
do
{
Field* fields = result->Fetch();
charactersByAccount[fields[1].Get<uint32>()].push_back({
fields[0].Get<uint32>(),
fields[2].Get<uint8>(),
ParseKnownTitles(fields[3].Get<std::string>())
});
} while (result->NextRow());
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
uint32 updatedCharacters = 0;
uint32 addedTitles = 0;
for (auto const& [accountId, characters] : charactersByAccount)
{
(void)accountId;
for (TitleCharacterInfo const& target : characters)
{
KnownTitlesMask mergedTitles = target.Titles;
uint32 added = 0;
for (TitleCharacterInfo const& source : characters)
{
if (source.Guid != target.Guid)
added += MergeTitlesForRace(source.Titles, source.Race, target.Race, mergedTitles);
}
if (!added)
continue;
UpdateTitlesForCharacter(trans, target.Guid, mergedTitles);
++updatedCharacters;
addedTitles += added;
}
}
CommitIfNeeded(trans);
LOG_INFO("module.accountboundtitles",
"AccountBoundTitles: startup backfill added {} title(s) to {} character(s).",
addedTitles, updatedCharacters);
}
void LoadModuleConfig()
{
Config.Enabled = AccountBound::IsCategoryEnabled("Titles");
Config.StartupBackfill = sConfigMgr->GetOption<bool>("AccountBound.Titles.StartupBackfill", true);
Config.SyncOnCreate = sConfigMgr->GetOption<bool>("AccountBound.Titles.SyncOnCreate", true);
Config.SyncOnSave = sConfigMgr->GetOption<bool>("AccountBound.Titles.SyncOnSave", true);
Config.SyncRealmFirst = sConfigMgr->GetOption<bool>("AccountBound.Titles.SyncRealmFirst", false);
Config.ConvertFactionSpecific = sConfigMgr->GetOption<bool>("AccountBound.Titles.ConvertFactionSpecific", true);
Config.Filter = AccountBound::LoadIdFilter("Titles");
}
}
class AccountBoundTitlesWorldScript : public WorldScript
{
public:
AccountBoundTitlesWorldScript() : WorldScript("AccountBoundTitlesWorldScript", {
WORLDHOOK_ON_AFTER_CONFIG_LOAD,
WORLDHOOK_ON_STARTUP
}) { }
void OnAfterConfigLoad(bool reload) override
{
LoadModuleConfig();
LOG_INFO("module.accountboundtitles",
"AccountBoundTitles: {}. CreateSync={}, SaveSync={}, StartupBackfill={}, RealmFirst={}.",
Config.Enabled ? (reload ? "configuration reloaded" : "configuration loaded") : "disabled",
Config.Enabled && Config.SyncOnCreate ? "on" : "off",
Config.Enabled && Config.SyncOnSave ? "on" : "off",
Config.Enabled && Config.StartupBackfill ? "on" : "off",
Config.Enabled && Config.SyncRealmFirst ? "on" : "off");
}
void OnStartup() override
{
if (!Config.Enabled)
return;
BuildTitleCache();
BackfillAllTitles();
}
};
class AccountBoundTitlesPlayerScript : public PlayerScript
{
public:
AccountBoundTitlesPlayerScript() : PlayerScript("AccountBoundTitlesPlayerScript", {
PLAYERHOOK_ON_LOAD_FROM_DB,
PLAYERHOOK_ON_LOGOUT,
PLAYERHOOK_ON_CREATE,
PLAYERHOOK_ON_SAVE
}) { }
void OnPlayerLoadFromDB(Player* player) override
{
if (player)
LastKnownTitlesByCharacter[player->GetGUID().GetCounter()] = GetPlayerKnownTitles(player);
}
void OnPlayerCreate(Player* player) override
{
if (Config.SyncOnCreate)
BackfillTitlesForCharacter(player);
}
void OnPlayerSave(Player* player) override
{
SyncTitlesFromPlayerToAccount(player);
}
void OnPlayerLogout(Player* player) override
{
if (player)
LastKnownTitlesByCharacter.erase(player->GetGUID().GetCounter());
}
};
void AddAccountBoundTitlesScripts()
{
new AccountBoundTitlesWorldScript();
new AccountBoundTitlesPlayerScript();
}
+18
View File
@@ -0,0 +1,18 @@
void AddAccountBoundAchievementsScripts();
void AddAccountBoundMountsScripts();
void AddAccountBoundPetsScripts();
void AddAccountBoundProfessionsScripts();
void AddAccountBoundReputationsScripts();
void AddAccountBoundTitlesScripts();
void AddAccountWideFriendsScripts();
void AddAccountBoundScripts()
{
AddAccountBoundAchievementsScripts();
AddAccountBoundMountsScripts();
AddAccountBoundPetsScripts();
AddAccountBoundProfessionsScripts();
AddAccountBoundReputationsScripts();
AddAccountBoundTitlesScripts();
AddAccountWideFriendsScripts();
}