character_warband_groups

This commit is contained in:
luis
2026-10-09 17:29:32 -03:00
parent 0ac8b8d0d5
commit 5901d9b19a
6 changed files with 183 additions and 19 deletions
@@ -890,14 +890,23 @@ void CharacterDatabaseConnection::DoPrepareStatements()
//WowCommunity
//WARBANDS
PrepareStatement(CHAR_SEL_WARBAND_GROUPS, "SELECT groupId, orderIndex, warbandSceneId, flags, contentSetId, name FROM character_warband_groups WHERE battlenetAccountId = ? ORDER BY orderIndex", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_WARBAND_GROUP_MEMBERS, "SELECT gm.groupId, gm.memberIndex, gm.guid, gm.warbandScenePlacementId, gm.memberType, gm.contentSetId FROM character_warband_group_members gm INNER JOIN character_warband_groups g ON gm.groupId = g.groupId WHERE g.battlenetAccountId = ? ORDER BY gm.groupId, gm.memberIndex", CONNECTION_ASYNC);
// Group selects are CONNECTION_BOTH: the character enumeration holder runs them async, and
// WorldSession::SaveWarbandGroupsToDB re-reads CHAR_SEL_WARBAND_GROUPS synchronously to pick up the ids the
// rewrite just got assigned (an ASYNC-only statement has no m_mStmt on the sync connection -> assertion).
PrepareStatement(CHAR_SEL_WARBAND_GROUPS, "SELECT groupId, orderIndex, warbandSceneId, flags, contentSetId, name FROM character_warband_groups WHERE battlenetAccountId = ? ORDER BY orderIndex", CONNECTION_BOTH);
PrepareStatement(CHAR_SEL_WARBAND_GROUP_MEMBERS, "SELECT gm.groupId, gm.memberIndex, gm.guid, gm.warbandScenePlacementId, gm.memberType, gm.contentSetId FROM character_warband_group_members gm INNER JOIN character_warband_groups g ON gm.groupId = g.groupId WHERE g.battlenetAccountId = ? ORDER BY gm.groupId, gm.memberIndex", CONNECTION_BOTH);
PrepareStatement(CHAR_INS_WARBAND_GROUP, "INSERT INTO character_warband_groups (groupId, battlenetAccountId, orderIndex, warbandSceneId, flags, contentSetId, name) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
PrepareStatement(CHAR_UPD_WARBAND_GROUP, "UPDATE character_warband_groups SET orderIndex = ?, warbandSceneId = ?, flags = ?, contentSetId = ?, name = ? WHERE groupId = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_WARBAND_GROUPS_BY_ACCOUNT, "DELETE FROM character_warband_groups WHERE battlenetAccountId = ?", CONNECTION_ASYNC);
// The warband group rewrite transaction (WorldSession::SaveWarbandGroupsToDB) is committed with
// DirectCommitTransaction so the auto_increment ids exist for the immediate read-back, which runs it on a
// synchronous connection: every statement in that transaction must be CONNECTION_BOTH (ASYNC-only statements
// have no handle there -> "Could not fetch prepared statement ... connection type: synchronous").
PrepareStatement(CHAR_DEL_WARBAND_GROUPS_BY_ACCOUNT, "DELETE FROM character_warband_groups WHERE battlenetAccountId = ?", CONNECTION_BOTH);
PrepareStatement(CHAR_INS_WARBAND_GROUP_MEMBER, "INSERT INTO character_warband_group_members (groupId, memberIndex, guid, warbandScenePlacementId, memberType, contentSetId) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_WARBAND_GROUP_MEMBERS, "DELETE FROM character_warband_group_members WHERE groupId = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_WARBAND_MEMBER_BY_GUID, "DELETE FROM character_warband_group_members WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_WARBAND_GROUP_MEMBERS_BY_ACCOUNT, "DELETE gm FROM character_warband_group_members gm INNER JOIN character_warband_groups g ON gm.groupId = g.groupId WHERE g.battlenetAccountId = ?", CONNECTION_BOTH);
PrepareStatement(CHAR_INS_WARBAND_GROUP_AUTO, "INSERT INTO character_warband_groups (battlenetAccountId, orderIndex, warbandSceneId, flags, contentSetId, name) VALUES (?, ?, ?, ?, ?, ?)", CONNECTION_BOTH);
PrepareStatement(CHAR_SEL_ACCOUNT_BANK_TAB_SETTINGS, "SELECT tabId, name, icon, description, depositFlags FROM account_bank_tab_settings WHERE battlenetAccountId = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_ACCOUNT_BANK_TAB_SETTINGS, "DELETE FROM account_bank_tab_settings WHERE battlenetAccountId = ?", CONNECTION_ASYNC);
@@ -722,6 +722,12 @@ enum CharacterDatabaseStatements : uint32
CHAR_INS_WARBAND_GROUP_MEMBER,
CHAR_DEL_WARBAND_GROUP_MEMBERS,
CHAR_DEL_WARBAND_MEMBER_BY_GUID,
// Rewriting a warband deletes this account's member rows through a join on the groups table, so they must
// go before the groups rows themselves
CHAR_DEL_WARBAND_GROUP_MEMBERS_BY_ACCOUNT,
// groupId is a global auto_increment primary key (not per account), so group inserts let the server assign
// the id instead of writing the one the client sent - which would collide across accounts
CHAR_INS_WARBAND_GROUP_AUTO,
CHAR_SEL_ACCOUNT_BANK_TAB_SETTINGS,
CHAR_DEL_ACCOUNT_BANK_TAB_SETTINGS,
+150 -4
View File
@@ -591,7 +591,7 @@ public:
SetSize(MAX);
}
bool Initialize(uint32 accountId, bool withDeclinedNames, bool isDeletedCharacters)
bool Initialize(uint32 accountId, uint32 battlenetAccountId, bool withDeclinedNames, bool isDeletedCharacters)
{
_isDeletedCharacters = isDeletedCharacters;
@@ -614,6 +614,15 @@ public:
stmt->setUInt32(0, accountId);
result &= SetPreparedQuery(SHOP_BOOSTS, stmt);
// Warband groups belong to the battle.net account, not the WoW account above
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_WARBAND_GROUPS);
stmt->setUInt32(0, battlenetAccountId);
result &= SetPreparedQuery(WARBAND_GROUPS, stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_WARBAND_GROUP_MEMBERS);
stmt->setUInt32(0, battlenetAccountId);
result &= SetPreparedQuery(WARBAND_GROUP_MEMBERS, stmt);
return result;
}
@@ -749,6 +758,12 @@ void WorldSession::HandleCharEnum(CharacterDatabaseQueryHolder const& holder)
});
}
// Warband groups are rebuilt from the account rows rather than kept in session memory, so the selection
// screen gets the same list on every login (and on every server-pushed re-enumeration).
if (!charEnum.IsDeletedCharacters)
LoadWarbandGroupsFromDB(holder.GetPreparedResult(EnumCharactersQueryHolder::WARBAND_GROUPS),
holder.GetPreparedResult(EnumCharactersQueryHolder::WARBAND_GROUP_MEMBERS));
if (_warbandGroups)
charEnum.WarbandGroups = _warbandGroups->Groups;
@@ -769,7 +784,7 @@ void WorldSession::SendCharacterEnum()
/// get all the data necessary for loading all characters (along with their pets) on the account
std::shared_ptr<EnumCharactersQueryHolder> holder = std::make_shared<EnumCharactersQueryHolder>();
if (!holder->Initialize(GetAccountId(), sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED), false))
if (!holder->Initialize(GetAccountId(), GetBattlenetAccountId(), sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED), false))
{
HandleCharEnum(*holder);
return;
@@ -790,7 +805,7 @@ void WorldSession::HandleCharUndeleteEnumOpcode(WorldPackets::Character::EnumCha
{
/// get all the data necessary for loading all undeleted characters (along with their pets) on the account
std::shared_ptr<EnumCharactersQueryHolder> holder = std::make_shared<EnumCharactersQueryHolder>();
if (!holder->Initialize(GetAccountId(), sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED), true))
if (!holder->Initialize(GetAccountId(), GetBattlenetAccountId(), sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED), true))
{
HandleCharEnum(*holder);
return;
@@ -3426,6 +3441,137 @@ void WorldSession::HandleSetupWarbandGroups(WorldPackets::Character::SetupWarban
_warbandGroups = std::make_unique<WorldPackets::Character::AccountWarbandGroups>();
_warbandGroups->Groups = std::move(setupWarbandGroups.Groups);
// Persist: without this the list only lived for the session, so the next character enumeration came back
// empty and the client had to push the whole thing again.
SaveWarbandGroupsToDB();
}
void WorldSession::LoadWarbandGroupsFromDB(PreparedQueryResult groupsResult, PreparedQueryResult membersResult)
{
if (!groupsResult)
return; // no stored groups: keep whatever this session already holds
// Members first - they are keyed by group id, which only the group rows below carry
std::unordered_map<uint64, std::vector<WorldPackets::Character::WarbandGroupMember>> membersByGroup;
if (membersResult)
{
do
{
Field* fields = membersResult->Fetch(); // groupId, memberIndex, guid, warbandScenePlacementId, memberType, contentSetId
WorldPackets::Character::WarbandGroupMember& member = membersByGroup[fields[0].GetUInt64()].emplace_back();
member.Guid = ObjectGuid::Create<HighGuid::Player>(fields[2].GetUInt64());
member.WarbandScenePlacementID = fields[3].GetUInt32();
member.Type = fields[4].GetInt32();
member.ContentSetID = fields[5].GetInt32();
} while (membersResult->NextRow());
}
auto groups = std::make_unique<WorldPackets::Character::AccountWarbandGroups>();
do
{
Field* fields = groupsResult->Fetch(); // groupId, orderIndex, warbandSceneId, flags, contentSetId, name
WorldPackets::Character::WarbandGroup& group = groups->Groups.emplace_back();
group.GroupID = fields[0].GetUInt64();
group.OrderIndex = fields[1].GetUInt8();
group.WarbandSceneID = fields[2].GetUInt32();
group.Flags = fields[3].GetUInt32();
group.ContentSetID = fields[4].GetInt32();
group.Name = fields[5].GetString();
if (auto itr = membersByGroup.find(group.GroupID); itr != membersByGroup.end())
group.Members = std::move(itr->second);
} while (groupsResult->NextRow());
_warbandGroups = std::move(groups);
}
void WorldSession::SaveWarbandGroupsToDB()
{
if (!_warbandGroups)
return;
uint32 const bnetAccountId = GetBattlenetAccountId();
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
// Members first: they identify "this account's" rows through a join on the groups table, which goes next.
// Groups are rewritten as a whole - the client owns the full list and sends it in one packet.
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_WARBAND_GROUP_MEMBERS_BY_ACCOUNT);
stmt->setUInt32(0, bnetAccountId);
trans->Append(stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_WARBAND_GROUPS_BY_ACCOUNT);
stmt->setUInt32(0, bnetAccountId);
trans->Append(stmt);
for (auto const& group : _warbandGroups->Groups)
{
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_WARBAND_GROUP_AUTO);
stmt->setUInt32(0, bnetAccountId);
stmt->setUInt8(1, group.OrderIndex);
stmt->setUInt32(2, group.WarbandSceneID);
stmt->setUInt32(3, group.Flags);
stmt->setInt32(4, group.ContentSetID);
stmt->setString(5, group.Name);
trans->Append(stmt);
}
// Committed directly (synchronously): the read-back below needs the auto_increment ids these inserts just
// got, and CommitTransaction only enqueues the work on the async pool, so the SELECT could run first.
CharacterDatabase.DirectCommitTransaction(trans);
if (_warbandGroups->Groups.empty())
return;
// groupId is a global auto_increment primary key shared by every account, so the ids the client sent are not
// written back: read the rows just inserted (they are this account's only ones now) and adopt the assigned
// ids in insertion order, which is what the next enumeration echoes to the client.
CharacterDatabasePreparedStatement* selStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_WARBAND_GROUPS);
selStmt->setUInt32(0, bnetAccountId);
PreparedQueryResult groupRows = CharacterDatabase.Query(selStmt);
if (!groupRows)
{
TC_LOG_ERROR("network", "WorldSession::SaveWarbandGroupsToDB: account {} wrote {} warband group(s) but read back none.", bnetAccountId, _warbandGroups->Groups.size());
return;
}
std::vector<uint64> assignedIds;
do
assignedIds.push_back(groupRows->Fetch()[0].GetUInt64());
while (groupRows->NextRow());
std::sort(assignedIds.begin(), assignedIds.end()); // auto_increment ids ascend in insert order
if (assignedIds.size() != _warbandGroups->Groups.size())
{
TC_LOG_ERROR("network", "WorldSession::SaveWarbandGroupsToDB: account {} expected {} warband group(s), read back {}.", bnetAccountId, _warbandGroups->Groups.size(), assignedIds.size());
return;
}
CharacterDatabaseTransaction memberTrans = CharacterDatabase.BeginTransaction();
for (std::size_t i = 0; i < assignedIds.size(); ++i)
{
WorldPackets::Character::WarbandGroup& group = _warbandGroups->Groups[i];
group.GroupID = assignedIds[i];
uint8 memberIndex = 0;
for (auto const& member : group.Members)
{
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_WARBAND_GROUP_MEMBER);
stmt->setUInt64(0, group.GroupID);
stmt->setUInt8(1, memberIndex++);
stmt->setUInt64(2, member.Guid.GetCounter());
stmt->setUInt32(3, member.WarbandScenePlacementID);
stmt->setInt32(4, member.Type);
stmt->setInt32(5, member.ContentSetID);
memberTrans->Append(stmt);
}
}
CharacterDatabase.CommitTransaction(memberTrans);
}
void WorldSession::HandleGetAccountCharListOpcode(WorldPackets::Character::GetAccountCharacterList& accountCharList)
@@ -3440,7 +3586,7 @@ void WorldSession::HandleGetAccountCharListOpcode(WorldPackets::Character::GetAc
};
std::shared_ptr<EnumCharactersQueryHolder> holder = std::make_shared<EnumCharactersQueryHolder>();
if (!holder->Initialize(GetAccountId(), false, false))
if (!holder->Initialize(GetAccountId(), GetBattlenetAccountId(), false, false))
{
sendEmpty();
return;
+11 -11
View File
@@ -116,7 +116,7 @@ void HousingMgr::Initialize()
"falling back to base room entry {}", _baseRoomEntryId);
}
TC_LOG_INFO("server.loading", ">> Loaded housing data: {} decor, {} levels, "
TC_LOG_DEBUG("server.loading", ">> Loaded housing data: {} decor, {} levels, "
"{} rooms, {} themes, {} decor materials, {} exterior wmos, {} level rewards, "
"{} initiatives, {} neighborhood maps, {} neighborhood plots, "
"{} decor categories, {} decor subcategories, {} decor dye slots, "
@@ -552,7 +552,7 @@ int32 HousingMgr::ResolvePlotIndex(WorldObject const* searcher, ObjectGuid corne
return -1;
}
// Housing/Neighborhood GUIDs have no GO entry — callers sometimes pass them for diagnostics; return -1.
// Housing/Neighborhood GUIDs have no GO entry ? callers sometimes pass them for diagnostics; return -1.
if (cornerstoneGuid.GetHigh() != HighGuid::GameObject)
{
return -1;
@@ -706,7 +706,7 @@ bool HousingMgr::CanVisitorAccessPlot(Player const* visitor, ObjectGuid ownerGui
if (settingsFlags & partyFlag)
{
// Party requires both online — same Group instance.
// Party requires both online ? same Group instance.
if (ownerPlayer && visitor->GetGroup() && visitor->GetGroup() == ownerPlayer->GetGroup())
return true;
}
@@ -722,7 +722,7 @@ bool HousingMgr::CanVisitorAccessPlot(Player const* visitor, ObjectGuid ownerGui
if (settingsFlags & friendsFlag)
{
// Friends are mutual on retail — visitor's social manager has the same record.
// Friends are mutual on retail ? visitor's social manager has the same record.
if (visitor->GetSocial() && visitor->GetSocial()->HasFriend(ownerGuid))
return true;
}
@@ -1066,7 +1066,7 @@ void HousingMgr::BuildRoomComponentTextureIndex()
_textureByOptionId.clear();
_textureByComponentType.clear();
// Build option→texture link from RoomComponentOptionTexture join table
// Build option?texture link from RoomComponentOptionTexture join table
for (RoomComponentOptionTextureEntry const* link : sRoomComponentOptionTextureStore)
{
if (!link)
@@ -1074,7 +1074,7 @@ void HousingMgr::BuildRoomComponentTextureIndex()
_textureByOptionId[link->RoomComponentOptionID] = link->RoomComponentTextureID;
}
// Build type→texture fallback from RoomComponentTexture (Type = component type: 1=wall, 2=floor, 3=ceiling)
// Build type?texture fallback from RoomComponentTexture (Type = component type: 1=wall, 2=floor, 3=ceiling)
for (RoomComponentTextureEntry const* tex : sRoomComponentTextureStore)
{
if (!tex || tex->Type <= 0)
@@ -1126,7 +1126,7 @@ void HousingMgr::EnsureDoorGameObjectTemplates()
continue;
}
// Create a GOOBER template (type=10) — clickable interaction object for house entry
// Create a GOOBER template (type=10) ? clickable interaction object for house entry
std::string name = entry->Name[DEFAULT_LOCALE] ? entry->Name[DEFAULT_LOCALE] : "Housing Door";
// Insert directly into ObjectMgr's in-memory store (derived from DB2, no DB write).
@@ -1164,9 +1164,9 @@ void HousingMgr::EnsureDoorGameObjectTemplates()
}
if (created)
TC_LOG_INFO("server.loading", ">> Auto-created {} missing door GO templates from ExteriorComponent DB2", created);
TC_LOG_DEBUG("server.loading", ">> Auto-created {} missing door GO templates from ExteriorComponent DB2", created);
if (scripted)
TC_LOG_INFO("server.loading", ">> Bound go_housing_door to {} door GO templates", scripted);
TC_LOG_DEBUG("server.loading", ">> Bound go_housing_door to {} door GO templates", scripted);
}
void HousingMgr::BuildExteriorComponentIndexes()
@@ -1271,7 +1271,7 @@ uint32 HousingMgr::GetDefaultFixtureForType(uint8 componentType, uint32 wmoDataI
return itr->second;
}
// Fallback: scan all sizes for this (type, wmo) — useful when caller doesn't know the size.
// Fallback: scan all sizes for this (type, wmo) ? useful when caller doesn't know the size.
for (uint8 sz = 1; sz <= 4; ++sz)
{
if (sz == houseSize)
@@ -1419,7 +1419,7 @@ int32 HousingMgr::GetFactionDefaultThemeID(int32 factionRestriction) const
int32 HousingMgr::GetDefaultSubThemeID(int32 baseThemeID) const
{
// The base theme's "(neutral)" child in HouseTheme.db2 — always the lowest child ID; a childless theme stands for itself.
// The base theme's "(neutral)" child in HouseTheme.db2 ? always the lowest child ID; a childless theme stands for itself.
int32 defaultSubTheme = 0;
for (auto const& [id, theme] : _houseThemeStore)
if (theme.ParentThemeID == baseThemeID && (!defaultSubTheme || int32(id) < defaultSubTheme))
+1 -1
View File
@@ -415,7 +415,7 @@ void Map::LoadGridsInRange(float x, float y, float radius)
EnsureGridLoaded(GridCoord(gx, gy));
}
TransferAbortParams Map::CannotEnter(Player* player)
TransferAbortParams Map::CannotEnter(Player* /*player*/)
{
return { TRANSFER_ABORT_NONE };
}
+3
View File
@@ -1706,6 +1706,9 @@ public:
void HandlePlayerLoginOpcode(WorldPackets::Character::PlayerLogin& playerLogin);
//WowCommunity
void HandleSetupWarbandGroups(WorldPackets::Character::SetupWarbandGroups& setupWarbandGroups);
// character_warband_groups / _members persistence for the setup handler above (CharacterHandler.cpp)
void LoadWarbandGroupsFromDB(PreparedQueryResult groupsResult, PreparedQueryResult membersResult);
void SaveWarbandGroupsToDB();
void HandleGetAccountCharListOpcode(WorldPackets::Character::GetAccountCharacterList& accountCharList);
void HandleGetRegionwideCharacterRestrictionAndMailData(WorldPackets::Character::GetRegionwideCharacterRestrictionAndMailData& packet);
void HandleGetRafAccountInfo(WorldPackets::RaF::GetRafAccountInfo& packet);