добавлены коллекции аккаунтов
This commit is contained in:
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user