feat(Core): Add clustering support (#16832)

Co-authored-by: 3kynox <thierry.prost@gmail.com>
Co-authored-by: nox <nox@noxen.net>
Co-authored-by: Ludwig <sudlud@users.noreply.github.com>
This commit is contained in:
Anton Popovichenko
2026-07-27 18:51:04 +02:00
committed by GitHub
parent f23ac046ab
commit 6f0ba8e896
80 changed files with 3675 additions and 297 deletions
@@ -18,6 +18,8 @@
#include "ChannelMgr.h"
#include "ObjectMgr.h" // for normalizePlayerName
#include "Player.h"
#include "Language.h"
#include "TC9Sidecar.h"
#include <cctype>
void WorldSession::HandleJoinChannel(WorldPacket& recvPacket)
@@ -38,6 +40,23 @@ void WorldSession::HandleJoinChannel(WorldPacket& recvPacket)
AreaTableEntry const* zone = sAreaTableStore.LookupEntry(GetPlayer()->GetZoneId());
if (!zone || !GetPlayer()->CanJoinConstantChannelInZone(channel, zone))
return;
// Cluster mode rebuilds the localized channel name so nodes agree on it;
// stock servers keep the client-supplied name.
if (sToCloud9Sidecar->ClusterModeEnabled())
{
auto const locale = GetSessionDbcLocale();
std::string const& zoneName = zone->area_name[locale];
std::string const cityName = sObjectMgr->GetAcoreStringForDBCLocale(LANG_CHANNEL_CITY);
char const* nameExt = (channel->flags & CHANNEL_DBC_FLAG_CITY_ONLY) ? cityName.c_str() : zoneName.c_str();
std::array<char, 128> buffer{};
if (char const* pattern = channel->pattern[locale])
{
std::snprintf(buffer.data(), buffer.size(), pattern, nameExt);
channelName = buffer.data();
}
}
}
if (channelName.empty())
+62 -47
View File
@@ -54,6 +54,7 @@
#include "SpellAuraEffects.h"
#include "SpellAuras.h"
#include "StringConvert.h"
#include "TC9Sidecar.h"
#include "Tokenize.h"
#include "Transport.h"
#include "Util.h"
@@ -81,125 +82,126 @@ bool LoginQueryHolder::Initialize()
SetSize(MAX_PLAYER_LOGIN_QUERY);
bool res = true;
ObjectGuid::LowType lowGuid = m_guid.GetCounter();
uint64 rawGUID = m_guid.GetRawValue();
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_FROM, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_AURAS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_AURAS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SPELL);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SPELLS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_QUESTSTATUS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_DAILYQUESTSTATUS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_DAILY_QUEST_STATUS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_WEEKLYQUESTSTATUS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_WEEKLY_QUEST_STATUS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_MONTHLYQUESTSTATUS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MONTHLY_QUEST_STATUS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SEASONALQUESTSTATUS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SEASONAL_QUEST_STATUS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_REPUTATION);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_REPUTATION, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_INVENTORY);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_INVENTORY, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACTIONS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAIL);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAILS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_MAILITEMS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SOCIALLIST);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SOCIAL_LIST, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_HOMEBIND);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_HOME_BIND, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SPELLCOOLDOWNS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SPELL_COOLDOWNS, stmt);
if (sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED))
{
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_DECLINEDNAMES);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_DECLINED_NAMES, stmt);
}
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACHIEVEMENTS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_CRITERIAPROGRESS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_EQUIPMENTSETS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_EQUIPMENT_SETS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ENTRY_POINT);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ENTRY_POINT, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_GLYPHS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_GLYPHS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_TALENTS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_TALENTS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_PLAYER_ACCOUNT_DATA);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_ACCOUNT_DATA, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_SKILLS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_SKILLS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_RANDOMBG);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_RANDOM_BG, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_BANNED);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_BANNED, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_QUESTSTATUSREW);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_QUEST_STATUS_REW, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_BREW_OF_THE_MONTH);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_BREW_OF_THE_MONTH, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES);
@@ -207,19 +209,19 @@ bool LoginQueryHolder::Initialize()
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_INSTANCE_LOCK_TIMES, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CORPSE_LOCATION);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CORPSE_LOCATION, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_SETTINGS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_CHARACTER_SETTINGS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_PETS);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_PET_SLOTS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_ACHIEVEMENT_OFFLINE_UPDATES);
stmt->SetData(0, lowGuid);
stmt->SetData(0, rawGUID);
res &= SetPreparedQuery(PLAYER_LOGIN_QUERY_LOAD_OFFLINE_ACHIEVEMENTS_UPDATES, stmt);
return res;
@@ -699,7 +701,9 @@ void WorldSession::HandlePlayerLoginOpcode(WorldPacket& recvData)
ObjectGuid playerGuid;
recvData >> playerGuid;
if (!IsLegitCharacterForAccount(playerGuid))
// The ownership check is delegated to the gateway in cluster mode, but a
// non-player GUID is invalid regardless of who authenticated the session.
if (!playerGuid.IsPlayer() || (!sToCloud9Sidecar->ClusterModeEnabled() && !IsLegitCharacterForAccount(playerGuid)))
{
LOG_ERROR("network", "Account ({}) can't login with that character ({}).", GetAccountId(), playerGuid.ToString());
KickPlayer("Account can't login with this character");
@@ -847,20 +851,28 @@ void WorldSession::HandlePlayerLoginFromDB(LoginQueryHolder const& holder)
chH.PSendSysMessage("{}", GitRevision::GetFullVersion());
}
if (uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(pCurrChar->GetGUID()))
if (!sToCloud9Sidecar->ClusterModeEnabled())
{
Guild* guild = sGuildMgr->GetGuildById(guildId);
Guild::Member const* member = guild ? guild->GetMember(pCurrChar->GetGUID()) : nullptr;
if (member)
if (uint32 guildId = sCharacterCache->GetCharacterGuildIdByGuid(pCurrChar->GetGUID()))
{
pCurrChar->SetInGuild(guildId);
pCurrChar->SetRank(member->GetRankId());
guild->SendLoginInfo(this);
Guild* guild = sGuildMgr->GetGuildById(guildId);
Guild::Member const* member = guild ? guild->GetMember(pCurrChar->GetGUID()) : nullptr;
if (member)
{
pCurrChar->SetInGuild(guildId);
pCurrChar->SetRank(member->GetRankId());
guild->SendLoginInfo(this);
}
else
{
LOG_ERROR("network.opcode", "Player {} ({}) marked as member of not existing guild (id: {}), removing guild membership for player.",
pCurrChar->GetName(), pCurrChar->GetGUID().ToString(), guildId);
pCurrChar->SetInGuild(0);
pCurrChar->SetRank(0);
}
}
else
{
LOG_ERROR("network.opcode", "Player {} ({}) marked as member of not existing guild (id: {}), removing guild membership for player.",
pCurrChar->GetName(), pCurrChar->GetGUID().ToString(), guildId);
pCurrChar->SetInGuild(0);
pCurrChar->SetRank(0);
}
@@ -914,9 +926,12 @@ void WorldSession::HandlePlayerLoginFromDB(LoginQueryHolder const& holder)
pCurrChar->SendInitialPacketsAfterAddToMap();
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ONLINE);
stmt->SetData(0, pCurrChar->GetGUID().GetCounter());
CharacterDatabase.Execute(stmt);
if (!sToCloud9Sidecar->ClusterModeEnabled())
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CHAR_ONLINE);
stmt->SetData(0, pCurrChar->GetGUID().GetCounter());
CharacterDatabase.Execute(stmt);
}
LoginDatabasePreparedStatement* loginStmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_ACCOUNT_ONLINE);
loginStmt->SetData(0, realm.Id.Realm);
+13 -2
View File
@@ -713,6 +713,18 @@ namespace Acore
void WorldSession::HandleTextEmoteOpcode(WorldPacket& recvData)
{
uint32 text_emote;
recvData >> text_emote;
constexpr uint32 readyEmote = 126;
// Handle confirmation redirect when the player types "/ready" after the new node becomes available for redirection.
if (text_emote == readyEmote && GetPlayer()->GetMap()->IsPlayerRedirectKickTimerActive())
{
HandleTC9PrepareForRedirect(recvData);
return;
}
if (!GetPlayer()->IsAlive())
return;
@@ -728,10 +740,9 @@ void WorldSession::HandleTextEmoteOpcode(WorldPacket& recvData)
if (GetPlayer()->IsSpectator())
return;
uint32 text_emote, emoteNum;
uint32 emoteNum;
ObjectGuid guid;
recvData >> text_emote;
recvData >> emoteNum;
recvData >> guid;
+93 -5
View File
@@ -27,6 +27,7 @@
#include "SocialMgr.h"
#include "Spell.h"
#include "SpellMgr.h"
#include "TC9Sidecar.h"
#include "World.h"
#include "WorldPacket.h"
#include "WorldSession.h"
@@ -505,6 +506,79 @@ void WorldSession::HandleAcceptTradeOpcode(WorldPacket& /*recvPacket*/)
}
}
bool needsCrossrealmHandling = sToCloud9Sidecar->IsCrossrealm() && _player->GetGUID().GetRealmID() != trader->GetGUID().GetRealmID();
// Create new items for crossrealm usage.
if (needsCrossrealmHandling)
{
CharacterDatabasePreparedStatement* stmt = nullptr;
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
stmt = CharacterDatabase.GetPreparedStatement(CHAR_NO_OP_PROVIDE_REALM_CONTEXT);
stmt->SetData(0, _player->GetGUID().GetRealmID());
trans->Append(stmt);
for (uint8 i = 0; i < TRADE_SLOT_TRADED_COUNT; i++)
{
if (myItems[i])
{
Item* newItem = myItems[i]->CloneItem(myItems[i]->GetCount(), trader);
if (!newItem)
{
ItemPosCountVec playerDst;
if (_player->CanStoreItem(NULL_BAG, NULL_SLOT, playerDst, myItems[i], false) == EQUIP_ERR_OK)
_player->MoveItemToInventory(playerDst, myItems[i], true, true);
// Should we handle else statement here?
continue;
}
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE);
stmt->SetData(0, myItems[i]->GetGUID().GetCounter());
trans->Append(stmt);
delete myItems[i];
myItems[i] = newItem;
}
}
CharacterDatabase.CommitTransaction(trans);
trans = CharacterDatabase.BeginTransaction();
stmt = CharacterDatabase.GetPreparedStatement(CHAR_NO_OP_PROVIDE_REALM_CONTEXT);
stmt->SetData(0, trader->GetGUID().GetRealmID());
trans->Append(stmt);
for (uint8 i = 0; i < TRADE_SLOT_TRADED_COUNT; i++)
{
if (hisItems[i])
{
Item* newItem = hisItems[i]->CloneItem(hisItems[i]->GetCount(), _player);
if (!newItem)
{
ItemPosCountVec playerDst;
if (trader->CanStoreItem(NULL_BAG, NULL_SLOT, playerDst, hisItems[i], false) == EQUIP_ERR_OK)
trader->MoveItemToInventory(playerDst, hisItems[i], true, true);
// Should we handle else statement here?
continue;
}
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEM_INSTANCE);
stmt->SetData(0, hisItems[i]->GetGUID().GetCounter());
trans->Append(stmt);
delete hisItems[i];
hisItems[i] = newItem;
}
}
CharacterDatabase.CommitTransaction(trans);
}
// execute trade: 2. store
moveItems(myItems, hisItems);
@@ -575,11 +649,25 @@ void WorldSession::HandleAcceptTradeOpcode(WorldPacket& /*recvPacket*/)
delete trader->m_trade;
trader->m_trade = nullptr;
// desynchronized with the other saves here (SaveInventoryAndGoldToDB() not have own transaction guards)
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
_player->SaveInventoryAndGoldToDB(trans);
trader->SaveInventoryAndGoldToDB(trans);
CharacterDatabase.CommitTransaction(trans);
// We can't use single transaction with different databases.
if (needsCrossrealmHandling)
{
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
_player->SaveInventoryAndGoldToDB(trans);
CharacterDatabase.CommitTransaction(trans);
trans = CharacterDatabase.BeginTransaction();
trader->SaveInventoryAndGoldToDB(trans);
CharacterDatabase.CommitTransaction(trans);
}
else
{
// desynchronized with the other saves here (SaveInventoryAndGoldToDB() not have own transaction guards)
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
_player->SaveInventoryAndGoldToDB(trans);
trader->SaveInventoryAndGoldToDB(trans);
CharacterDatabase.CommitTransaction(trans);
}
info.Status = TRADE_STATUS_TRADE_COMPLETE;
trader->GetSession()->SendTradeStatus(info);