1889 lines
89 KiB
C++
1889 lines
89 KiB
C++
/*
|
|
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "BattlegroundQueue.h"
|
|
#include "ArenaTeam.h"
|
|
#include "ArenaTeamMgr.h"
|
|
#include "BattlegroundMgr.h"
|
|
#include "BattlegroundPackets.h"
|
|
#include "Chat.h"
|
|
#include "DB2Stores.h"
|
|
#include "GameTime.h"
|
|
#include "Group.h"
|
|
#include "Language.h"
|
|
#include "Log.h"
|
|
#include "ObjectAccessor.h"
|
|
#include "Player.h"
|
|
#include "World.h"
|
|
|
|
//WowCommunity
|
|
#include "LFG.h"
|
|
#include "MapUtils.h"
|
|
#include <algorithm>
|
|
#include <utility>
|
|
//WowCommunity
|
|
|
|
/*********************************************************/
|
|
/*** BATTLEGROUND QUEUE SYSTEM ***/
|
|
/*********************************************************/
|
|
|
|
//WowCommunity
|
|
|
|
namespace
|
|
{
|
|
// Resolve one queued player to a single role.
|
|
//
|
|
// The specialization is the primary source: ChrSpecialization.db2 Role is a fact about the character, it
|
|
// exists for every member of a duo and not only for the queuer who sent the packet, and its numbering
|
|
// (Tank 0, Healer 1, Dps 2) is the very numbering the client expects back in the role block of
|
|
// SMSG_BATTLEFIELD_STATUS_WAIT_FOR_GROUPS.
|
|
//
|
|
// declaredRoles is the queuer's lfg::PLAYER_ROLE_* mask and is only used to NARROW that: a player whose spec
|
|
// role is not among the roles they offered is placed on one they did offer instead. It is deliberately not
|
|
// the primary source - it is self-reported, it is absent for every member except the queuer, and three of the
|
|
// join handlers accept a mask with several bits set at once.
|
|
ChrSpecializationRole ResolveQueueRole(Player const* player, uint8 declaredRoles)
|
|
{
|
|
ChrSpecializationRole specRole = ChrSpecializationRole::Dps;
|
|
if (ChrSpecializationEntry const* spec = player->GetPrimarySpecializationEntry())
|
|
specRole = spec->GetRole();
|
|
|
|
if (!declaredRoles)
|
|
return specRole;
|
|
|
|
auto declares = [declaredRoles](ChrSpecializationRole role)
|
|
{
|
|
switch (role)
|
|
{
|
|
case ChrSpecializationRole::Tank: return (declaredRoles & lfg::PLAYER_ROLE_TANK) != 0;
|
|
case ChrSpecializationRole::Healer: return (declaredRoles & lfg::PLAYER_ROLE_HEALER) != 0;
|
|
case ChrSpecializationRole::Dps: return (declaredRoles & lfg::PLAYER_ROLE_DAMAGE) != 0;
|
|
default: return false;
|
|
}
|
|
};
|
|
|
|
if (declares(specRole))
|
|
return specRole;
|
|
|
|
// Spec role was not offered. Take the scarcest role they did offer - a tank who queued as anything else
|
|
// is far more useful to the matchmaker as that other thing than as a tank it cannot actually field.
|
|
if (declares(ChrSpecializationRole::Tank))
|
|
return ChrSpecializationRole::Tank;
|
|
if (declares(ChrSpecializationRole::Healer))
|
|
return ChrSpecializationRole::Healer;
|
|
if (declares(ChrSpecializationRole::Dps))
|
|
return ChrSpecializationRole::Dps;
|
|
|
|
// Mask carried only PLAYER_ROLE_LEADER or nothing recognisable.
|
|
return specRole;
|
|
}
|
|
|
|
std::size_t RoleIndex(ChrSpecializationRole role)
|
|
{
|
|
return std::size_t(AsUnderlyingType(role));
|
|
}
|
|
|
|
// What one side of a solo-queue match must consist of. Damagers are whatever the other two quotas leave.
|
|
PvpRoleHeadcount SideQuota(uint32 playersPerTeam, uint32 tanksPerTeam, uint32 healersPerTeam)
|
|
{
|
|
return
|
|
{
|
|
uint8(tanksPerTeam),
|
|
uint8(healersPerTeam),
|
|
uint8(playersPerTeam - tanksPerTeam - healersPerTeam)
|
|
};
|
|
}
|
|
|
|
// A role the mode does not ask for is not a role in that mode: its players are damage dealers, which is both
|
|
// what retail does - the captured rated Blitz lobby wanted 0 tanks and reported every protection or blood
|
|
// spec in it as a damager - and the only sane alternative to leaving tanks queued forever behind a quota of
|
|
// zero they can never fill. It is also what turns both quotas being 0 into "no role balancing at all",
|
|
// because then every player folds into the damage bucket and the damage bucket is the whole team.
|
|
ChrSpecializationRole FoldRole(ChrSpecializationRole role, PvpRoleHeadcount const& quota)
|
|
{
|
|
return quota[RoleIndex(role)] ? role : ChrSpecializationRole::Dps;
|
|
}
|
|
}
|
|
//WowCommunity
|
|
|
|
BattlegroundQueue::BattlegroundQueue(BattlegroundQueueTypeId queueId) : m_queueId(queueId)
|
|
{
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
|
{
|
|
for (uint32 j = 0; j < MAX_BATTLEGROUND_BRACKETS; ++j)
|
|
{
|
|
m_SumOfWaitTimes[i][j] = 0;
|
|
m_WaitTimeLastPlayer[i][j] = 0;
|
|
for (uint32 k = 0; k < COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME; ++k)
|
|
m_WaitTimes[i][j][k] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
BattlegroundQueue::~BattlegroundQueue()
|
|
{
|
|
m_events.KillAllEvents(false);
|
|
|
|
for (int i = 0; i < MAX_BATTLEGROUND_BRACKETS; ++i)
|
|
{
|
|
for (uint32 j = 0; j < BG_QUEUE_GROUP_TYPES_COUNT; ++j)
|
|
{
|
|
for (GroupsQueueType::iterator itr = m_QueuedGroups[i][j].begin(); itr!= m_QueuedGroups[i][j].end(); ++itr)
|
|
delete (*itr);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*********************************************************/
|
|
/*** BATTLEGROUND QUEUE SELECTION POOLS ***/
|
|
/*********************************************************/
|
|
|
|
// selection pool initialization, used to clean up from prev selection
|
|
void BattlegroundQueue::SelectionPool::Init()
|
|
{
|
|
SelectedGroups.clear();
|
|
PlayerCount = 0;
|
|
}
|
|
|
|
// remove group info from selection pool
|
|
// returns true when we need to try to add new group to selection pool
|
|
// returns false when selection pool is ok or when we kicked smaller group than we need to kick
|
|
// sometimes it can be called on empty selection pool
|
|
bool BattlegroundQueue::SelectionPool::KickGroup(uint32 size)
|
|
{
|
|
//find maxgroup or LAST group with size == size and kick it
|
|
bool found = false;
|
|
GroupsQueueType::iterator groupToKick = SelectedGroups.begin();
|
|
for (GroupsQueueType::iterator itr = groupToKick; itr != SelectedGroups.end(); ++itr)
|
|
{
|
|
if (abs((int32)((*itr)->Players.size() - size)) <= 1)
|
|
{
|
|
groupToKick = itr;
|
|
found = true;
|
|
}
|
|
else if (!found && (*itr)->Players.size() >= (*groupToKick)->Players.size())
|
|
groupToKick = itr;
|
|
}
|
|
//if pool is empty, do nothing
|
|
if (GetPlayerCount())
|
|
{
|
|
//update player count
|
|
GroupQueueInfo* ginfo = (*groupToKick);
|
|
SelectedGroups.erase(groupToKick);
|
|
PlayerCount -= ginfo->Players.size();
|
|
//return false if we kicked smaller group or there are enough players in selection pool
|
|
if (ginfo->Players.size() <= size + 1)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// add group to selection pool
|
|
// used when building selection pools
|
|
// returns true if we can invite more players, or when we added group to selection pool
|
|
// returns false when selection pool is full
|
|
bool BattlegroundQueue::SelectionPool::AddGroup(GroupQueueInfo* ginfo, uint32 desiredCount)
|
|
{
|
|
//if group is larger than desired count - don't allow to add it to pool
|
|
if (!ginfo->IsInvitedToBGInstanceGUID && desiredCount >= PlayerCount + ginfo->Players.size())
|
|
{
|
|
SelectedGroups.push_back(ginfo);
|
|
// increase selected players count
|
|
PlayerCount += ginfo->Players.size();
|
|
return true;
|
|
}
|
|
if (PlayerCount < desiredCount)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/*********************************************************/
|
|
/*** BATTLEGROUND QUEUES ***/
|
|
/*********************************************************/
|
|
|
|
// add group or player (grp == NULL) to bg queue with the given leader and bg specifications
|
|
GroupQueueInfo* BattlegroundQueue::AddGroup(Player const* leader, Group const* group, Team team, PVPDifficultyEntry const* bracketEntry, bool isPremade, uint32 ArenaRating, uint32 MatchmakerRating, uint8 roles)
|
|
{
|
|
BattlegroundBracketId bracketId = bracketEntry->GetBracketId();
|
|
|
|
// create new ginfo
|
|
GroupQueueInfo* ginfo = new GroupQueueInfo;
|
|
ginfo->IsInvitedToBGInstanceGUID = 0;
|
|
ginfo->JoinTime = GameTime::GetGameTimeMS();
|
|
ginfo->RemoveInviteTime = 0;
|
|
ginfo->Team = team;
|
|
ginfo->ArenaTeamRating = ArenaRating;
|
|
ginfo->ArenaMatchmakerRating = MatchmakerRating;
|
|
ginfo->OpponentsTeamRating = 0;
|
|
ginfo->OpponentsMatchmakerRating = 0;
|
|
ginfo->Roles = roles;
|
|
|
|
ginfo->Players.clear();
|
|
|
|
//compute index (if group is premade or joined a rated match) to queues
|
|
uint32 index = 0;
|
|
if (!m_queueId.Rated && !isPremade)
|
|
index += PVP_TEAMS_COUNT;
|
|
if (ginfo->Team == HORDE)
|
|
index++;
|
|
TC_LOG_DEBUG("bg.battleground", "Adding Group to BattlegroundQueue bgTypeId : {}, bracket_id : {}, index : {}", m_queueId.BattlemasterListId, bracketId, index);
|
|
|
|
uint32 lastOnlineTime = GameTime::GetGameTimeMS();
|
|
|
|
//announce world (this don't need mutex)
|
|
if (m_queueId.Rated && sWorld->getBoolConfig(CONFIG_ARENA_QUEUE_ANNOUNCER_ENABLE))
|
|
{
|
|
ArenaTeam* team = sArenaTeamMgr->GetArenaTeamById(0);
|
|
if (team)
|
|
sWorld->SendWorldText(LANG_ARENA_QUEUE_ANNOUNCE_WORLD_JOIN, team->GetName().c_str(), m_queueId.TeamSize, m_queueId.TeamSize, ginfo->ArenaTeamRating);
|
|
}
|
|
|
|
//add players from group to ginfo
|
|
if (group)
|
|
{
|
|
for (GroupReference const& itr : group->GetMembers())
|
|
{
|
|
Player* member = itr.GetSource();
|
|
PlayerQueueInfo& pl_info = m_QueuedPlayers[member->GetGUID()];
|
|
pl_info.LastOnlineTime = lastOnlineTime;
|
|
pl_info.GroupInfo = ginfo;
|
|
// Only the queuer's declared mask exists on the wire, so it narrows only the queuer's own role;
|
|
// every other member is resolved from their specialization alone.
|
|
pl_info.Role = ResolveQueueRole(member, member == leader ? roles : uint8(0));
|
|
// add the pinfo to ginfo's list
|
|
ginfo->Players[member->GetGUID()] = &pl_info;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PlayerQueueInfo& pl_info = m_QueuedPlayers[leader->GetGUID()];
|
|
pl_info.LastOnlineTime = lastOnlineTime;
|
|
pl_info.GroupInfo = ginfo;
|
|
pl_info.Role = ResolveQueueRole(leader, roles);
|
|
ginfo->Players[leader->GetGUID()] = &pl_info;
|
|
}
|
|
|
|
//add GroupInfo to m_QueuedGroups
|
|
{
|
|
m_QueuedGroups[bracketId][index].push_back(ginfo);
|
|
|
|
//announce to world, this code needs mutex
|
|
if (!m_queueId.Rated && !isPremade && sWorld->getBoolConfig(CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_ENABLE))
|
|
{
|
|
if (BattlegroundTemplate const* bg = sBattlegroundMgr->GetBattlegroundTemplateByTypeId(BattlegroundTypeId(m_queueId.BattlemasterListId)))
|
|
{
|
|
uint32 MinPlayers = bg->GetMinPlayersPerTeam();
|
|
uint32 qHorde = 0;
|
|
uint32 qAlliance = 0;
|
|
uint32 q_min_level = bracketEntry->MinLevel;
|
|
uint32 q_max_level = bracketEntry->MaxLevel;
|
|
GroupsQueueType::const_iterator itr;
|
|
for (itr = m_QueuedGroups[bracketId][BG_QUEUE_NORMAL_ALLIANCE].begin(); itr != m_QueuedGroups[bracketId][BG_QUEUE_NORMAL_ALLIANCE].end(); ++itr)
|
|
if (!(*itr)->IsInvitedToBGInstanceGUID)
|
|
qAlliance += (*itr)->Players.size();
|
|
for (itr = m_QueuedGroups[bracketId][BG_QUEUE_NORMAL_HORDE].begin(); itr != m_QueuedGroups[bracketId][BG_QUEUE_NORMAL_HORDE].end(); ++itr)
|
|
if (!(*itr)->IsInvitedToBGInstanceGUID)
|
|
qHorde += (*itr)->Players.size();
|
|
|
|
// Show queue status to player only (when joining queue)
|
|
if (sWorld->getBoolConfig(CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_PLAYERONLY))
|
|
{
|
|
ChatHandler chatHandler(leader->GetSession());
|
|
chatHandler.PSendSysMessage(LANG_BG_QUEUE_ANNOUNCE_SELF, bg->BattlemasterEntry->Name[chatHandler.GetSessionDbcLocale()], q_min_level, q_max_level,
|
|
qAlliance, (MinPlayers > qAlliance) ? MinPlayers - qAlliance : (uint32)0, qHorde, (MinPlayers > qHorde) ? MinPlayers - qHorde : (uint32)0);
|
|
}
|
|
// System message
|
|
else
|
|
{
|
|
sWorld->SendWorldText(LANG_BG_QUEUE_ANNOUNCE_WORLD, bg->BattlemasterEntry->Name[sWorld->GetDefaultDbcLocale()], q_min_level, q_max_level,
|
|
qAlliance, (MinPlayers > qAlliance) ? MinPlayers - qAlliance : (uint32)0, qHorde, (MinPlayers > qHorde) ? MinPlayers - qHorde : (uint32)0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ginfo;
|
|
}
|
|
|
|
void BattlegroundQueue::PlayerInvitedToBGUpdateAverageWaitTime(GroupQueueInfo* ginfo, BattlegroundBracketId bracket_id)
|
|
{
|
|
uint32 timeInQueue = getMSTimeDiff(ginfo->JoinTime, GameTime::GetGameTimeMS());
|
|
uint8 team_index = TEAM_ALLIANCE; //default set to TEAM_ALLIANCE - or non rated arenas!
|
|
if (!m_queueId.TeamSize)
|
|
{
|
|
if (ginfo->Team == HORDE)
|
|
team_index = TEAM_HORDE;
|
|
}
|
|
else
|
|
{
|
|
if (m_queueId.Rated)
|
|
team_index = TEAM_HORDE; //for rated arenas use TEAM_HORDE
|
|
}
|
|
|
|
//store pointer to arrayindex of player that was added first
|
|
uint32* lastPlayerAddedPointer = &(m_WaitTimeLastPlayer[team_index][bracket_id]);
|
|
//remove his time from sum
|
|
m_SumOfWaitTimes[team_index][bracket_id] -= m_WaitTimes[team_index][bracket_id][(*lastPlayerAddedPointer)];
|
|
//set average time to new
|
|
m_WaitTimes[team_index][bracket_id][(*lastPlayerAddedPointer)] = timeInQueue;
|
|
//add new time to sum
|
|
m_SumOfWaitTimes[team_index][bracket_id] += timeInQueue;
|
|
//set index of last player added to next one
|
|
(*lastPlayerAddedPointer)++;
|
|
(*lastPlayerAddedPointer) %= COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME;
|
|
}
|
|
|
|
uint32 BattlegroundQueue::GetAverageQueueWaitTime(GroupQueueInfo* ginfo, BattlegroundBracketId bracket_id) const
|
|
{
|
|
uint8 team_index = TEAM_ALLIANCE; //default set to TEAM_ALLIANCE - or non rated arenas!
|
|
if (!m_queueId.TeamSize)
|
|
{
|
|
if (ginfo->Team == HORDE)
|
|
team_index = TEAM_HORDE;
|
|
}
|
|
else
|
|
{
|
|
if (m_queueId.Rated)
|
|
team_index = TEAM_HORDE; //for rated arenas use TEAM_HORDE
|
|
}
|
|
//check if there is enought values(we always add values > 0)
|
|
if (m_WaitTimes[team_index][bracket_id][COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME - 1])
|
|
return (m_SumOfWaitTimes[team_index][bracket_id] / COUNT_OF_PLAYERS_TO_AVERAGE_WAIT_TIME);
|
|
else
|
|
//if there aren't enough values return 0 - not available
|
|
return 0;
|
|
}
|
|
|
|
//remove player from queue and from group info, if group info is empty then remove it too
|
|
void BattlegroundQueue::RemovePlayer(ObjectGuid guid, bool decreaseInvitedCount)
|
|
{
|
|
int32 bracket_id = -1; // signed for proper for-loop finish
|
|
QueuedPlayersMap::iterator itr;
|
|
|
|
//remove player from map, if he's there
|
|
itr = m_QueuedPlayers.find(guid);
|
|
if (itr == m_QueuedPlayers.end())
|
|
{
|
|
//This happens if a player logs out while in a bg because WorldSession::LogoutPlayer() notifies the bg twice
|
|
std::string playerName = "Unknown";
|
|
if (Player* player = ObjectAccessor::FindPlayer(guid))
|
|
playerName = player->GetName();
|
|
TC_LOG_DEBUG("bg.battleground", "BattlegroundQueue: couldn't find player {} ({})", playerName, guid.ToString());
|
|
return;
|
|
}
|
|
|
|
GroupQueueInfo* group = itr->second.GroupInfo;
|
|
GroupsQueueType::iterator group_itr;
|
|
// mostly people with the highest levels are in battlegrounds, thats why
|
|
// we count from MAX_BATTLEGROUND_QUEUES - 1 to 0
|
|
|
|
uint32 index = (group->Team == HORDE) ? BG_QUEUE_PREMADE_HORDE : BG_QUEUE_PREMADE_ALLIANCE;
|
|
|
|
for (int32 bracket_id_tmp = MAX_BATTLEGROUND_BRACKETS - 1; bracket_id_tmp >= 0 && bracket_id == -1; --bracket_id_tmp)
|
|
{
|
|
//we must check premade and normal team's queue - because when players from premade are joining bg,
|
|
//they leave groupinfo so we can't use its players size to find out index
|
|
for (uint32 j = index; j < BG_QUEUE_GROUP_TYPES_COUNT; j += PVP_TEAMS_COUNT)
|
|
{
|
|
GroupsQueueType::iterator k = m_QueuedGroups[bracket_id_tmp][j].begin();
|
|
for (; k != m_QueuedGroups[bracket_id_tmp][j].end(); ++k)
|
|
{
|
|
if ((*k) == group)
|
|
{
|
|
bracket_id = bracket_id_tmp;
|
|
group_itr = k;
|
|
//we must store index to be able to erase iterator
|
|
index = j;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//player can't be in queue without group, but just in case
|
|
if (bracket_id == -1)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue: ERROR Cannot find groupinfo for {}", guid.ToString());
|
|
return;
|
|
}
|
|
TC_LOG_DEBUG("bg.battleground", "BattlegroundQueue: Removing {}, from bracket_id {}", guid.ToString(), (uint32)bracket_id);
|
|
|
|
// ALL variables are correctly set
|
|
// We can ignore leveling up in queue - it should not cause crash
|
|
// remove player from group
|
|
// if only one player there, remove group
|
|
|
|
// remove player queue info from group queue info
|
|
std::map<ObjectGuid, PlayerQueueInfo*>::iterator pitr = group->Players.find(guid);
|
|
if (pitr != group->Players.end())
|
|
group->Players.erase(pitr);
|
|
|
|
// if invited to bg, and should decrease invited count, then do it
|
|
if (decreaseInvitedCount && group->IsInvitedToBGInstanceGUID)
|
|
if (Battleground* bg = sBattlegroundMgr->GetBattleground(group->IsInvitedToBGInstanceGUID, BattlegroundTypeId(m_queueId.BattlemasterListId)))
|
|
bg->DecreaseInvitedCount(group->Team);
|
|
|
|
// remove player queue info
|
|
m_QueuedPlayers.erase(itr);
|
|
|
|
// announce to world if arena team left queue for rated match, show only once
|
|
if (m_queueId.TeamSize && m_queueId.Rated && group->Players.empty() && sWorld->getBoolConfig(CONFIG_ARENA_QUEUE_ANNOUNCER_ENABLE))
|
|
if (ArenaTeam* team = sArenaTeamMgr->GetArenaTeamById(0))
|
|
sWorld->SendWorldText(LANG_ARENA_QUEUE_ANNOUNCE_WORLD_EXIT, team->GetName().c_str(), m_queueId.TeamSize, m_queueId.TeamSize, group->ArenaTeamRating);
|
|
|
|
// if player leaves queue and he is invited to rated arena match, then he have to lose
|
|
if (group->IsInvitedToBGInstanceGUID && m_queueId.Rated && decreaseInvitedCount)
|
|
{
|
|
if (ArenaTeam* at = sArenaTeamMgr->GetArenaTeamById(0))
|
|
{
|
|
TC_LOG_DEBUG("bg.battleground", "UPDATING memberLost's personal arena rating for {} by opponents rating: {}", guid.ToString(), group->OpponentsTeamRating);
|
|
if (Player* player = ObjectAccessor::FindConnectedPlayer(guid))
|
|
at->MemberLost(player, group->OpponentsMatchmakerRating);
|
|
else
|
|
at->OfflineMemberLost(guid, group->OpponentsMatchmakerRating);
|
|
at->SaveToDB();
|
|
}
|
|
}
|
|
|
|
// remove group queue info if needed
|
|
if (group->Players.empty())
|
|
{
|
|
m_QueuedGroups[bracket_id][index].erase(group_itr);
|
|
delete group;
|
|
return;
|
|
}
|
|
|
|
// if group wasn't empty, so it wasn't deleted, and player have left a rated
|
|
// queue -> everyone from the group should leave too
|
|
// don't remove recursively if already invited to bg!
|
|
if (!group->IsInvitedToBGInstanceGUID && m_queueId.Rated)
|
|
{
|
|
// remove next player, this is recursive
|
|
// first send removal information
|
|
if (Player* plr2 = ObjectAccessor::FindConnectedPlayer(group->Players.begin()->first))
|
|
{
|
|
uint32 queueSlot = plr2->GetBattlegroundQueueIndex(m_queueId);
|
|
|
|
plr2->RemoveBattlegroundQueueId(m_queueId); // must be called this way, because if you move this call to
|
|
// queue->removeplayer, it causes bugs
|
|
|
|
WorldPackets::Battleground::BattlefieldStatusNone battlefieldStatus;
|
|
BattlegroundMgr::BuildBattlegroundStatusNone(&battlefieldStatus, plr2, queueSlot, plr2->GetBattlegroundQueueJoinTime(m_queueId));
|
|
plr2->SendDirectMessage(battlefieldStatus.Write());
|
|
}
|
|
// then actually delete, this may delete the group as well!
|
|
RemovePlayer(group->Players.begin()->first, decreaseInvitedCount);
|
|
}
|
|
}
|
|
|
|
//returns true when player pl_guid is in queue and is invited to bgInstanceGuid
|
|
bool BattlegroundQueue::IsPlayerInvited(ObjectGuid pl_guid, const uint32 bgInstanceGuid, const uint32 removeTime)
|
|
{
|
|
QueuedPlayersMap::const_iterator qItr = m_QueuedPlayers.find(pl_guid);
|
|
return (qItr != m_QueuedPlayers.end()
|
|
&& qItr->second.GroupInfo->IsInvitedToBGInstanceGUID == bgInstanceGuid
|
|
&& qItr->second.GroupInfo->RemoveInviteTime == removeTime);
|
|
}
|
|
|
|
bool BattlegroundQueue::GetPlayerGroupInfoData(ObjectGuid guid, GroupQueueInfo* ginfo)
|
|
{
|
|
QueuedPlayersMap::const_iterator qItr = m_QueuedPlayers.find(guid);
|
|
if (qItr == m_QueuedPlayers.end())
|
|
return false;
|
|
*ginfo = *(qItr->second.GroupInfo);
|
|
return true;
|
|
}
|
|
|
|
uint32 BattlegroundQueue::GetPlayersInQueue(TeamId id)
|
|
{
|
|
return m_SelectionPools[id].GetPlayerCount();
|
|
}
|
|
|
|
uint32 BattlegroundQueue::GetQueuedPlayersCount(TeamId teamId, BattlegroundBracketId bracketId) const
|
|
{
|
|
uint32 count = 0;
|
|
Team team = (teamId == TEAM_ALLIANCE) ? ALLIANCE : HORDE;
|
|
|
|
// Count players from queued groups for the specific bracket
|
|
for (uint32 i = 0; i < BG_QUEUE_GROUP_TYPES_COUNT; ++i)
|
|
{
|
|
for (GroupQueueInfo* ginfo : m_QueuedGroups[bracketId][i])
|
|
{
|
|
if (ginfo->Team == team)
|
|
count += ginfo->Players.size();
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
bool BattlegroundQueue::InviteGroupToBG(GroupQueueInfo* ginfo, Battleground* bg, Team side, uint32 inviteTime /*= INVITE_ACCEPT_WAIT_TIME*/, bool proposalManaged /*= false*/)
|
|
{
|
|
// set side if needed
|
|
if (side)
|
|
ginfo->Team = side;
|
|
|
|
if (!ginfo->IsInvitedToBGInstanceGUID)
|
|
{
|
|
// not yet invited
|
|
// set invitation
|
|
ginfo->IsInvitedToBGInstanceGUID = bg->GetInstanceID();
|
|
BattlegroundTypeId bgTypeId = BattlegroundTypeId(m_queueId.BattlemasterListId);
|
|
BattlegroundQueueTypeId bgQueueTypeId = m_queueId;
|
|
BattlegroundBracketId bracket_id = bg->GetBracketId();
|
|
|
|
ginfo->RemoveInviteTime = GameTime::GetGameTimeMS() + inviteTime;
|
|
|
|
// loop through the players
|
|
for (std::map<ObjectGuid, PlayerQueueInfo*>::iterator itr = ginfo->Players.begin(); itr != ginfo->Players.end(); ++itr)
|
|
{
|
|
// get the player
|
|
Player* player = ObjectAccessor::FindConnectedPlayer(itr->first);
|
|
// if offline, skip him, this should not happen - player is removed from queue when he logs out
|
|
if (!player)
|
|
continue;
|
|
|
|
// invite the player
|
|
PlayerInvitedToBGUpdateAverageWaitTime(ginfo, bracket_id);
|
|
//sBattlegroundMgr->InvitePlayer(player, bg, ginfo->Team);
|
|
|
|
// set invited player counters
|
|
bg->IncreaseInvitedCount(ginfo->Team);
|
|
|
|
player->SetInviteForBattlegroundQueueType(bgQueueTypeId, ginfo->IsInvitedToBGInstanceGUID);
|
|
|
|
// create remind invite events - but never under a proposal. Retail sends exactly ONE
|
|
// SMSG_BATTLEFIELD_STATUS_NEED_CONFIRMATION per proposal window, the one at its start:
|
|
// "C:/sniff/rated BG 12.0.7.pkt" has three windows and each carries a single 0x480000
|
|
// (12.0.7 numbering throughout this capture note: 0x480000 = SMSG_BATTLEFIELD_STATUS_NEED_
|
|
// CONFIRMATION, 0x48000D = ..._WAIT_FOR_GROUPS, 0x48000E = ..._GROUP_PROPOSAL_FAILED. 12.1
|
|
// shifted the families up by three, so this tree registers them as 0x4B0000/0x4B000D/0x4B000E.)
|
|
// (ticks 136714 / 864798 / 896705), followed only by 0x48000D until the window ends at
|
|
// 166467 / 894382 / 916110 - 29,7 s, 29,6 s and an early STATUS_ACTIVE, never a second
|
|
// confirmation. Without this gate the reminder would land INVITATION_REMIND_TIME before
|
|
// the deadline, i.e. 8000 ms into the 28000 ms PROPOSAL_CONFIRM_WAIT_TIME window, and
|
|
// overwrite the role display that SendProposalStatus is maintaining. Worse, it would go
|
|
// out to members who already accepted: ProposalAccept only sets member.Accepted and
|
|
// leaves ginfo->IsInvitedToBGInstanceGUID and ginfo->RemoveInviteTime alone, so the
|
|
// IsPlayerInvited check in BGQueueInviteEvent::Execute stays true for the whole window.
|
|
if (!proposalManaged && inviteTime > INVITATION_REMIND_TIME)
|
|
{
|
|
BGQueueInviteEvent* inviteEvent = new BGQueueInviteEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgTypeId, ginfo->RemoveInviteTime, m_queueId);
|
|
m_events.AddEvent(inviteEvent, m_events.CalculateTime(Milliseconds(inviteTime - INVITATION_REMIND_TIME)));
|
|
}
|
|
// create automatic remove events - but never under a proposal, whose own deadline event removes
|
|
// the players who did not accept and requeues the ones who did. Both firing on the same tick
|
|
// would tear the lobby in half.
|
|
if (!proposalManaged)
|
|
{
|
|
BGQueueRemoveEvent* removeEvent = new BGQueueRemoveEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgQueueTypeId, ginfo->RemoveInviteTime);
|
|
m_events.AddEvent(removeEvent, m_events.CalculateTime(Milliseconds(inviteTime)));
|
|
}
|
|
// create remind invite events
|
|
if (inviteTime > INVITATION_REMIND_TIME)
|
|
{
|
|
BGQueueInviteEvent* inviteEvent = new BGQueueInviteEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgTypeId, ginfo->RemoveInviteTime, m_queueId);
|
|
m_events.AddEvent(inviteEvent, m_events.CalculateTime(Milliseconds(inviteTime - INVITATION_REMIND_TIME)));
|
|
}
|
|
// create automatic remove events - but never under a proposal, whose own deadline event removes
|
|
// the players who did not accept and requeues the ones who did. Both firing on the same tick
|
|
// would tear the lobby in half.
|
|
if (!proposalManaged)
|
|
{
|
|
BGQueueRemoveEvent* removeEvent = new BGQueueRemoveEvent(player->GetGUID(), ginfo->IsInvitedToBGInstanceGUID, bgQueueTypeId, ginfo->RemoveInviteTime);
|
|
m_events.AddEvent(removeEvent, m_events.CalculateTime(Milliseconds(inviteTime)));
|
|
}
|
|
|
|
uint32 queueSlot = player->GetBattlegroundQueueIndex(bgQueueTypeId);
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Battleground: invited player {} {} to BG instance {} queueindex {} bgtype {}",
|
|
player->GetName(), player->GetGUID().ToString(), bg->GetInstanceID(), queueSlot, m_queueId.BattlemasterListId);
|
|
|
|
WorldPackets::Battleground::BattlefieldStatusNeedConfirmation battlefieldStatus;
|
|
BattlegroundMgr::BuildBattlegroundStatusNeedConfirmation(&battlefieldStatus, bg, player, queueSlot, player->GetBattlegroundQueueJoinTime(bgQueueTypeId), inviteTime, bgQueueTypeId, itr->second->Role);
|
|
player->SendDirectMessage(battlefieldStatus.Write());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
This function is inviting players to already running battlegrounds
|
|
Invitation type is based on config file
|
|
large groups are disadvantageous, because they will be kicked first if invitation type = 1
|
|
*/
|
|
void BattlegroundQueue::FillPlayersToBG(Battleground* bg, BattlegroundBracketId bracket_id)
|
|
{
|
|
int32 hordeFree = bg->GetFreeSlotsForTeam(HORDE);
|
|
int32 aliFree = bg->GetFreeSlotsForTeam(ALLIANCE);
|
|
uint32 aliCount = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].size();
|
|
uint32 hordeCount = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].size();
|
|
|
|
// try to get even teams
|
|
if (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE) == BG_QUEUE_INVITATION_TYPE_EVEN)
|
|
{
|
|
// check if the teams are even
|
|
if (hordeFree == 1 && aliFree == 1)
|
|
{
|
|
// if we are here, the teams have the same amount of players
|
|
// then we have to allow to join the same amount of players
|
|
int32 hordeExtra = hordeCount - aliCount;
|
|
int32 aliExtra = aliCount - hordeCount;
|
|
|
|
hordeExtra = std::max(hordeExtra, 0);
|
|
aliExtra = std::max(aliExtra, 0);
|
|
|
|
if (aliCount != hordeCount)
|
|
{
|
|
aliFree -= aliExtra;
|
|
hordeFree -= hordeExtra;
|
|
|
|
aliFree = std::max(aliFree, 0);
|
|
hordeFree = std::max(hordeFree, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
//iterator for iterating through bg queue
|
|
GroupsQueueType::const_iterator Ali_itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].begin();
|
|
//count of groups in queue - used to stop cycles
|
|
|
|
//index to queue which group is current
|
|
uint32 aliIndex = 0;
|
|
for (; aliIndex < aliCount && m_SelectionPools[TEAM_ALLIANCE].AddGroup((*Ali_itr), aliFree); aliIndex++)
|
|
++Ali_itr;
|
|
//the same thing for horde
|
|
GroupsQueueType::const_iterator Horde_itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].begin();
|
|
|
|
uint32 hordeIndex = 0;
|
|
for (; hordeIndex < hordeCount && m_SelectionPools[TEAM_HORDE].AddGroup((*Horde_itr), hordeFree); hordeIndex++)
|
|
++Horde_itr;
|
|
|
|
//if ofc like BG queue invitation is set in config, then we are happy
|
|
if (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE) == BG_QUEUE_INVITATION_TYPE_NO_BALANCE)
|
|
return;
|
|
|
|
/*
|
|
if we reached this code, then we have to solve NP - complete problem called Subset sum problem
|
|
So one solution is to check all possible invitation subgroups, or we can use these conditions:
|
|
1. Last time when BattlegroundQueue::Update was executed we invited all possible players - so there is only small possibility
|
|
that we will invite now whole queue, because only 1 change has been made to queues from the last BattlegroundQueue::Update call
|
|
2. Other thing we should consider is group order in queue
|
|
*/
|
|
|
|
// At first we need to compare free space in bg and our selection pool
|
|
int32 diffAli = aliFree - int32(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount());
|
|
int32 diffHorde = hordeFree - int32(m_SelectionPools[TEAM_HORDE].GetPlayerCount());
|
|
while (abs(diffAli - diffHorde) > 1 && (m_SelectionPools[TEAM_HORDE].GetPlayerCount() > 0 || m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() > 0))
|
|
{
|
|
//each cycle execution we need to kick at least 1 group
|
|
if (diffAli < diffHorde)
|
|
{
|
|
//kick alliance group, add to pool new group if needed
|
|
if (m_SelectionPools[TEAM_ALLIANCE].KickGroup(diffHorde - diffAli))
|
|
{
|
|
for (; aliIndex < aliCount && m_SelectionPools[TEAM_ALLIANCE].AddGroup((*Ali_itr), (aliFree >= diffHorde) ? aliFree - diffHorde : 0); aliIndex++)
|
|
++Ali_itr;
|
|
}
|
|
//if ali selection is already empty, then kick horde group, but if there are less horde than ali in bg - break;
|
|
if (!m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount())
|
|
{
|
|
if (aliFree <= diffHorde + 1)
|
|
break;
|
|
m_SelectionPools[TEAM_HORDE].KickGroup(diffHorde - diffAli);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//kick horde group, add to pool new group if needed
|
|
if (m_SelectionPools[TEAM_HORDE].KickGroup(diffAli - diffHorde))
|
|
{
|
|
for (; hordeIndex < hordeCount && m_SelectionPools[TEAM_HORDE].AddGroup((*Horde_itr), (hordeFree >= diffAli) ? hordeFree - diffAli : 0); hordeIndex++)
|
|
++Horde_itr;
|
|
}
|
|
if (!m_SelectionPools[TEAM_HORDE].GetPlayerCount())
|
|
{
|
|
if (hordeFree <= diffAli + 1)
|
|
break;
|
|
m_SelectionPools[TEAM_ALLIANCE].KickGroup(diffAli - diffHorde);
|
|
}
|
|
}
|
|
//count diffs after small update
|
|
diffAli = aliFree - int32(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount());
|
|
diffHorde = hordeFree - int32(m_SelectionPools[TEAM_HORDE].GetPlayerCount());
|
|
}
|
|
}
|
|
|
|
// this method checks if premade versus premade battleground is possible
|
|
// then after 30 mins (default) in queue it moves premade group to normal queue
|
|
// it tries to invite as much players as it can - to MaxPlayersPerTeam, because premade groups have more than MinPlayersPerTeam players
|
|
bool BattlegroundQueue::CheckPremadeMatch(BattlegroundBracketId bracket_id, uint32 MinPlayersPerTeam, uint32 MaxPlayersPerTeam)
|
|
{
|
|
//check match
|
|
if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].empty() && !m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].empty())
|
|
{
|
|
//start premade match
|
|
//if groups aren't invited
|
|
GroupsQueueType::const_iterator ali_group, horde_group;
|
|
for (ali_group = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].begin(); ali_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].end(); ++ali_group)
|
|
if (!(*ali_group)->IsInvitedToBGInstanceGUID)
|
|
break;
|
|
for (horde_group = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].begin(); horde_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].end(); ++horde_group)
|
|
if (!(*horde_group)->IsInvitedToBGInstanceGUID)
|
|
break;
|
|
|
|
if (ali_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].end() && horde_group != m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].end())
|
|
{
|
|
m_SelectionPools[TEAM_ALLIANCE].AddGroup((*ali_group), MaxPlayersPerTeam);
|
|
m_SelectionPools[TEAM_HORDE].AddGroup((*horde_group), MaxPlayersPerTeam);
|
|
//add groups/players from normal queue to size of bigger group
|
|
uint32 maxPlayers = std::min(m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount(), m_SelectionPools[TEAM_HORDE].GetPlayerCount());
|
|
GroupsQueueType::const_iterator itr;
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++)
|
|
{
|
|
for (itr = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].begin(); itr != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].end(); ++itr)
|
|
{
|
|
//if itr can join BG and player count is less that maxPlayers, then add group to selectionpool
|
|
if (!(*itr)->IsInvitedToBGInstanceGUID && !m_SelectionPools[i].AddGroup((*itr), maxPlayers))
|
|
break;
|
|
}
|
|
}
|
|
//premade selection pools are set
|
|
return true;
|
|
}
|
|
}
|
|
// now check if we can move group from Premade queue to normal queue (timer has expired) or group size lowered!!
|
|
// this could be 2 cycles but i'm checking only first team in queue - it can cause problem -
|
|
// if first is invited to BG and seconds timer expired, but we can ignore it, because players have only 80 seconds to click to enter bg
|
|
// and when they click or after 80 seconds the queue info is removed from queue
|
|
uint32 time_before = GameTime::GetGameTimeMS() - sWorld->getIntConfig(CONFIG_BATTLEGROUND_PREMADE_GROUP_WAIT_FOR_MATCH);
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++)
|
|
{
|
|
if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].empty())
|
|
{
|
|
GroupsQueueType::iterator itr = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].begin();
|
|
if (!(*itr)->IsInvitedToBGInstanceGUID && ((*itr)->JoinTime < time_before || (*itr)->Players.size() < MinPlayersPerTeam))
|
|
{
|
|
//we must insert group to normal queue and erase pointer from premade queue
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].push_front((*itr));
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE + i].erase(itr);
|
|
}
|
|
}
|
|
}
|
|
//selection pools are not set
|
|
return false;
|
|
}
|
|
|
|
// this method tries to create battleground or arena with MinPlayersPerTeam against MinPlayersPerTeam
|
|
bool BattlegroundQueue::CheckNormalMatch(BattlegroundBracketId bracket_id, uint32 minPlayers, uint32 maxPlayers)
|
|
{
|
|
GroupsQueueType::const_iterator itr_team[PVP_TEAMS_COUNT];
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++)
|
|
{
|
|
itr_team[i] = m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].begin();
|
|
for (; itr_team[i] != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + i].end(); ++(itr_team[i]))
|
|
{
|
|
if (!(*(itr_team[i]))->IsInvitedToBGInstanceGUID)
|
|
{
|
|
m_SelectionPools[i].AddGroup(*(itr_team[i]), maxPlayers);
|
|
if (m_SelectionPools[i].GetPlayerCount() >= minPlayers)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//try to invite same number of players - this cycle may cause longer wait time even if there are enough players in queue, but we want ballanced bg
|
|
uint32 j = TEAM_ALLIANCE;
|
|
if (m_SelectionPools[TEAM_HORDE].GetPlayerCount() < m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount())
|
|
j = TEAM_HORDE;
|
|
if (sWorld->getIntConfig(CONFIG_BATTLEGROUND_INVITATION_TYPE) != BG_QUEUE_INVITATION_TYPE_NO_BALANCE
|
|
&& m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers)
|
|
{
|
|
//we will try to invite more groups to team with less players indexed by j
|
|
++(itr_team[j]); //this will not cause a crash, because for cycle above reached break;
|
|
for (; itr_team[j] != m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE + j].end(); ++(itr_team[j]))
|
|
{
|
|
if (!(*(itr_team[j]))->IsInvitedToBGInstanceGUID)
|
|
if (!m_SelectionPools[j].AddGroup(*(itr_team[j]), m_SelectionPools[(j + 1) % PVP_TEAMS_COUNT].GetPlayerCount()))
|
|
break;
|
|
}
|
|
// do not allow to start bg with more than 2 players more on 1 faction
|
|
if (abs((int32)(m_SelectionPools[TEAM_HORDE].GetPlayerCount() - m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount())) > 2)
|
|
return false;
|
|
}
|
|
//allow 1v0 if debug bg
|
|
if (sBattlegroundMgr->isTesting() && (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() || m_SelectionPools[TEAM_HORDE].GetPlayerCount()))
|
|
return true;
|
|
//allow 1v0 if FSB override is enabled
|
|
if (sBattlegroundMgr->IsFSBOverrideEnabled() && (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers || m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers))
|
|
return true;
|
|
//return true if there are enough players in selection pools - enable to work .debug bg command correctly
|
|
return m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= minPlayers && m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= minPlayers;
|
|
}
|
|
|
|
// this method will check if we can invite players to same faction skirmish match
|
|
bool BattlegroundQueue::CheckSkirmishForSameFaction(BattlegroundBracketId bracket_id, uint32 minPlayersPerTeam)
|
|
{
|
|
if (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() < minPlayersPerTeam && m_SelectionPools[TEAM_HORDE].GetPlayerCount() < minPlayersPerTeam)
|
|
return false;
|
|
TeamId teamIndex = TEAM_ALLIANCE;
|
|
TeamId otherTeam = TEAM_HORDE;
|
|
Team otherTeamId = HORDE;
|
|
if (m_SelectionPools[TEAM_HORDE].GetPlayerCount() == minPlayersPerTeam)
|
|
{
|
|
teamIndex = TEAM_HORDE;
|
|
otherTeam = TEAM_ALLIANCE;
|
|
otherTeamId = ALLIANCE;
|
|
}
|
|
//clear other team's selection
|
|
m_SelectionPools[otherTeam].Init();
|
|
//store last ginfo pointer
|
|
GroupQueueInfo* ginfo = m_SelectionPools[teamIndex].SelectedGroups.back();
|
|
//set itr_team to group that was added to selection pool latest
|
|
GroupsQueueType::iterator itr_team = m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(teamIndex)].begin();
|
|
for (; itr_team != m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(teamIndex)].end(); ++itr_team)
|
|
if (ginfo == *itr_team)
|
|
break;
|
|
if (itr_team == m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(teamIndex)].end())
|
|
return false;
|
|
GroupsQueueType::iterator itr_team2 = itr_team;
|
|
++itr_team2;
|
|
//invite players to other selection pool
|
|
for (; itr_team2 != m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(teamIndex)].end(); ++itr_team2)
|
|
{
|
|
//if selection pool is full then break;
|
|
if (!(*itr_team2)->IsInvitedToBGInstanceGUID && !m_SelectionPools[otherTeam].AddGroup(*itr_team2, minPlayersPerTeam))
|
|
break;
|
|
}
|
|
if (m_SelectionPools[otherTeam].GetPlayerCount() != minPlayersPerTeam && !sBattlegroundMgr->IsFSBOverrideEnabled())
|
|
return false;
|
|
|
|
//here we have correct 2 selections and we need to change one teams team and move selection pool teams to other team's queue
|
|
for (GroupsQueueType::iterator itr = m_SelectionPools[otherTeam].SelectedGroups.begin(); itr != m_SelectionPools[otherTeam].SelectedGroups.end(); ++itr)
|
|
{
|
|
//set correct team
|
|
(*itr)->Team = otherTeamId;
|
|
//add team to other queue
|
|
m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(otherTeam)].push_front(*itr);
|
|
//remove team from old queue
|
|
GroupsQueueType::iterator itr2 = itr_team;
|
|
++itr2;
|
|
for (; itr2 != m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(teamIndex)].end(); ++itr2)
|
|
{
|
|
if (*itr2 == *itr)
|
|
{
|
|
m_QueuedGroups[bracket_id][uint8(BG_QUEUE_NORMAL_ALLIANCE) + uint8(teamIndex)].erase(itr2);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void BattlegroundQueue::UpdateEvents(uint32 diff)
|
|
{
|
|
m_events.Update(diff);
|
|
}
|
|
|
|
/*
|
|
this method is called when group is inserted, or player / group is removed from BG Queue - there is only one player's status changed, so we don't use while (true) cycles to invite whole queue
|
|
it must be called after fully adding the members of a group to ensure group joining
|
|
should be called from Battleground::RemovePlayer function in some cases
|
|
*/
|
|
void BattlegroundQueue::BattlegroundQueueUpdate(uint32 /*diff*/, BattlegroundBracketId bracket_id, uint32 arenaRating)
|
|
{
|
|
//if no players in queue - do nothing
|
|
if (m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].empty() &&
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].empty() &&
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_ALLIANCE].empty() &&
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_NORMAL_HORDE].empty())
|
|
return;
|
|
|
|
// battleground with free slot for player should be always in the beggining of the queue
|
|
// maybe it would be better to create bgfreeslotqueue for each bracket_id
|
|
|
|
BattlegroundTemplate const* bg_template = sBattlegroundMgr->GetBattlegroundTemplateByTypeId(BattlegroundTypeId(m_queueId.BattlemasterListId));
|
|
if (!bg_template)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "Battleground: Update: bg template not found for {}", m_queueId.BattlemasterListId);
|
|
return;
|
|
}
|
|
|
|
// loop over queues for every map
|
|
for (int32 mapId : bg_template->MapIDs)
|
|
{
|
|
BGFreeSlotQueueContainer& bgQueues = sBattlegroundMgr->GetBGFreeSlotQueueStore(mapId);
|
|
for (BGFreeSlotQueueContainer::iterator itr = bgQueues.begin(); itr != bgQueues.end();)
|
|
{
|
|
Battleground* bg = *itr; ++itr;
|
|
// DO NOT allow queue manager to invite new player to rated games
|
|
if (!bg->isRated() && bg->GetBracketId() == bracket_id &&
|
|
bg->GetStatus() > STATUS_WAIT_QUEUE && bg->GetStatus() < STATUS_WAIT_LEAVE)
|
|
{
|
|
// clear selection pools
|
|
m_SelectionPools[TEAM_ALLIANCE].Init();
|
|
m_SelectionPools[TEAM_HORDE].Init();
|
|
|
|
// call a function that does the job for us
|
|
FillPlayersToBG(bg, bracket_id);
|
|
|
|
// now everything is set, invite players
|
|
for (GroupsQueueType::const_iterator citr = m_SelectionPools[TEAM_ALLIANCE].SelectedGroups.begin(); citr != m_SelectionPools[TEAM_ALLIANCE].SelectedGroups.end(); ++citr)
|
|
InviteGroupToBG((*citr), bg, (*citr)->Team);
|
|
|
|
for (GroupsQueueType::const_iterator citr = m_SelectionPools[TEAM_HORDE].SelectedGroups.begin(); citr != m_SelectionPools[TEAM_HORDE].SelectedGroups.end(); ++citr)
|
|
InviteGroupToBG((*citr), bg, (*citr)->Team);
|
|
|
|
if (!bg->HasFreeSlots())
|
|
bg->RemoveFromBGFreeSlotQueue();
|
|
}
|
|
}
|
|
}
|
|
|
|
// finished iterating through the bgs with free slots, maybe we need to create a new bg
|
|
|
|
// get the min. players per team, properly for larger arenas as well. (must have full teams for arena matches!)
|
|
uint32 MinPlayersPerTeam = bg_template->GetMinPlayersPerTeam();
|
|
uint32 MaxPlayersPerTeam = bg_template->GetMaxPlayersPerTeam();
|
|
|
|
if (bg_template->IsArena())
|
|
{
|
|
MaxPlayersPerTeam = m_queueId.TeamSize;
|
|
MinPlayersPerTeam = sBattlegroundMgr->isArenaTesting() ? 1 : m_queueId.TeamSize;
|
|
}
|
|
else if (sBattlegroundMgr->isTesting())
|
|
MinPlayersPerTeam = 1;
|
|
|
|
m_SelectionPools[TEAM_ALLIANCE].Init();
|
|
m_SelectionPools[TEAM_HORDE].Init();
|
|
|
|
// A solo queue must NOT be offered to CheckPremadeMatch, even though its template is a battleground and
|
|
// not an arena. CheckPremadeMatch takes one head entry per faction, adds it whole with
|
|
// AddGroup(entry, MaxPlayersPerTeam) and then tops both sides up from the BG_QUEUE_NORMAL_* lists - which
|
|
// for a rated queue are permanently empty, because AddGroup only routes an entry there when
|
|
// `!m_queueId.Rated && !isPremade`. So with one solo entry queued per faction it would report a match of
|
|
// one against one, create the instance and StartBattleground() it: a 1v1 "Battleground Blitz" past both
|
|
// the role quota and the 8-per-side headcount, invited through the ordinary 90 s single-invite path
|
|
// instead of the all-or-nothing proposal. Its second half is worse still - the demotion loop below moves
|
|
// any entry smaller than MinPlayersPerTeam (6 for BattlemasterList 1101) out of the premade lists, and
|
|
// those lists are the only ones CheckSoloQueueMatch reads, so a single pass would empty the solo queue
|
|
// into a list nothing rated ever looks at again.
|
|
// The RatedBattlegroundBlitz branch at the end of this function is this queue's matchmaker.
|
|
bool const isSoloQueue = BattlegroundQueueIdType(m_queueId.Type) == BattlegroundQueueIdType::RatedBattlegroundBlitz
|
|
|| BattlegroundQueueIdType(m_queueId.Type) == BattlegroundQueueIdType::RatedSoloShuffle;
|
|
|
|
if (!bg_template->IsArena() && !isSoloQueue)
|
|
{
|
|
if (CheckPremadeMatch(bracket_id, MinPlayersPerTeam, MaxPlayersPerTeam))
|
|
{
|
|
// create new battleground
|
|
Battleground* bg2 = sBattlegroundMgr->CreateNewBattleground(m_queueId, bracket_id);
|
|
if (!bg2)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create battleground: {}", m_queueId.BattlemasterListId);
|
|
return;
|
|
}
|
|
// invite those selection pools
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++)
|
|
for (GroupsQueueType::const_iterator citr = m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.begin(); citr != m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.end(); ++citr)
|
|
InviteGroupToBG((*citr), bg2, (*citr)->Team);
|
|
|
|
bg2->StartBattleground();
|
|
//clear structures
|
|
m_SelectionPools[TEAM_ALLIANCE].Init();
|
|
m_SelectionPools[TEAM_HORDE].Init();
|
|
}
|
|
}
|
|
|
|
// now check if there are in queues enough players to start new game of (normal battleground, or non-rated arena)
|
|
if (!m_queueId.Rated)
|
|
{
|
|
// if there are enough players in pools, start new battleground or non rated arena
|
|
if (CheckNormalMatch(bracket_id, MinPlayersPerTeam, MaxPlayersPerTeam)
|
|
|| (bg_template->IsArena() && CheckSkirmishForSameFaction(bracket_id, MinPlayersPerTeam)))
|
|
{
|
|
// we successfully created a pool
|
|
Battleground* bg2 = sBattlegroundMgr->CreateNewBattleground(m_queueId, bracket_id);
|
|
if (!bg2)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create battleground: {}", m_queueId.BattlemasterListId);
|
|
return;
|
|
}
|
|
|
|
// invite those selection pools
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; i++)
|
|
for (GroupsQueueType::const_iterator citr = m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.begin(); citr != m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.end(); ++citr)
|
|
InviteGroupToBG((*citr), bg2, (*citr)->Team);
|
|
// start bg
|
|
bg2->StartBattleground();
|
|
}
|
|
}
|
|
else if (bg_template->IsArena())
|
|
{
|
|
// found out the minimum and maximum ratings the newly added team should battle against
|
|
// arenaRating is the rating of the latest joined team, or 0
|
|
// 0 is on (automatic update call) and we must set it to team's with longest wait time
|
|
if (!arenaRating)
|
|
{
|
|
GroupQueueInfo* front1 = nullptr;
|
|
GroupQueueInfo* front2 = nullptr;
|
|
if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].empty())
|
|
{
|
|
front1 = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].front();
|
|
arenaRating = front1->ArenaMatchmakerRating;
|
|
}
|
|
if (!m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].empty())
|
|
{
|
|
front2 = m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].front();
|
|
arenaRating = front2->ArenaMatchmakerRating;
|
|
}
|
|
if (front1 && front2)
|
|
{
|
|
if (front1->JoinTime < front2->JoinTime)
|
|
arenaRating = front1->ArenaMatchmakerRating;
|
|
}
|
|
else if (!front1 && !front2)
|
|
return; //queues are empty
|
|
}
|
|
|
|
//set rating range
|
|
uint32 arenaMinRating = (arenaRating <= sBattlegroundMgr->GetMaxRatingDifference()) ? 0 : arenaRating - sBattlegroundMgr->GetMaxRatingDifference();
|
|
uint32 arenaMaxRating = arenaRating + sBattlegroundMgr->GetMaxRatingDifference();
|
|
// if max rating difference is set and the time past since server startup is greater than the rating discard time
|
|
// (after what time the ratings aren't taken into account when making teams) then
|
|
// the discard time is current_time - time_to_discard, teams that joined after that, will have their ratings taken into account
|
|
// else leave the discard time on 0, this way all ratings will be discarded
|
|
// this has to be signed value - when the server starts, this value would be negative and thus overflow
|
|
int32 discardTime = GameTime::GetGameTimeMS() - sBattlegroundMgr->GetRatingDiscardTimer();
|
|
|
|
// we need to find 2 teams which will play next game
|
|
GroupsQueueType::iterator itr_teams[PVP_TEAMS_COUNT];
|
|
uint8 found = 0;
|
|
uint8 team = 0;
|
|
|
|
for (uint8 i = BG_QUEUE_PREMADE_ALLIANCE; i < BG_QUEUE_NORMAL_ALLIANCE; i++)
|
|
{
|
|
// take the group that joined first
|
|
GroupsQueueType::iterator itr2 = m_QueuedGroups[bracket_id][i].begin();
|
|
for (; itr2 != m_QueuedGroups[bracket_id][i].end(); ++itr2)
|
|
{
|
|
// if group match conditions, then add it to pool
|
|
if (!(*itr2)->IsInvitedToBGInstanceGUID
|
|
&& (((*itr2)->ArenaMatchmakerRating >= arenaMinRating && (*itr2)->ArenaMatchmakerRating <= arenaMaxRating)
|
|
|| (int32)(*itr2)->JoinTime < discardTime))
|
|
{
|
|
itr_teams[found++] = itr2;
|
|
team = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
return;
|
|
|
|
if (found == 1)
|
|
{
|
|
for (GroupsQueueType::iterator itr3 = itr_teams[0]; itr3 != m_QueuedGroups[bracket_id][team].end(); ++itr3)
|
|
{
|
|
|
|
// disable this check to allow arena queue without other team
|
|
if (itr3 == itr_teams[0])
|
|
continue; // skip already found team
|
|
|
|
if (!(*itr3)->IsInvitedToBGInstanceGUID
|
|
&& (((*itr3)->ArenaMatchmakerRating >= arenaMinRating && (*itr3)->ArenaMatchmakerRating <= arenaMaxRating)
|
|
|| (int32)(*itr3)->JoinTime < discardTime))
|
|
{
|
|
itr_teams[found++] = itr3;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//if we have 2 teams, then start new arena and invite players!
|
|
if (found == 2)
|
|
{
|
|
GroupQueueInfo* aTeam = *itr_teams[TEAM_ALLIANCE];
|
|
GroupQueueInfo* hTeam = *itr_teams[TEAM_HORDE];
|
|
Battleground* arena = sBattlegroundMgr->CreateNewBattleground(m_queueId, bracket_id);
|
|
if (!arena)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue::Update couldn't create arena instance for rated arena match!");
|
|
return;
|
|
}
|
|
|
|
aTeam->OpponentsTeamRating = hTeam->ArenaTeamRating;
|
|
hTeam->OpponentsTeamRating = aTeam->ArenaTeamRating;
|
|
aTeam->OpponentsMatchmakerRating = hTeam->ArenaMatchmakerRating;
|
|
hTeam->OpponentsMatchmakerRating = aTeam->ArenaMatchmakerRating;
|
|
TC_LOG_DEBUG("bg.battleground", "setting oposite teamrating for to {}", aTeam->OpponentsTeamRating);
|
|
TC_LOG_DEBUG("bg.battleground", "setting oposite teamrating for to {}", hTeam->OpponentsTeamRating);
|
|
|
|
// now we must move team if we changed its faction to another faction queue, because then we will spam log by errors in Queue::RemovePlayer
|
|
if (aTeam->Team != ALLIANCE)
|
|
{
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].push_front(aTeam);
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].erase(itr_teams[TEAM_ALLIANCE]);
|
|
}
|
|
if (hTeam->Team != HORDE)
|
|
{
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].push_front(hTeam);
|
|
m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].erase(itr_teams[TEAM_HORDE]);
|
|
}
|
|
|
|
arena->SetArenaMatchmakerRating(ALLIANCE, aTeam->ArenaMatchmakerRating);
|
|
arena->SetArenaMatchmakerRating(HORDE, hTeam->ArenaMatchmakerRating);
|
|
InviteGroupToBG(aTeam, arena, ALLIANCE);
|
|
InviteGroupToBG(hTeam, arena, HORDE);
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Starting rated arena match!");
|
|
arena->StartBattleground();
|
|
}
|
|
}
|
|
else if (BattlegroundQueueIdType(m_queueId.Type) == BattlegroundQueueIdType::RatedSoloShuffle)
|
|
{
|
|
// Rated Solo Shuffle: assemble a 6-player lobby, exactly 3 per team, 1 healer per team (2 healers +
|
|
// 4 dps), no tanks. Same solo-matchmaker + all-or-nothing proposal machinery as Blitz.
|
|
uint32 const perTeam = sBattlegroundMgr->isTesting() ? 1 : 3;
|
|
uint32 const tanks = 0;
|
|
uint32 const healers = sBattlegroundMgr->isTesting() ? 0 : 1;
|
|
|
|
for (std::pair<uint32 const, BattlegroundProposal> const& pending : m_Proposals)
|
|
if (pending.second.BracketId == bracket_id)
|
|
return;
|
|
|
|
if (CheckSoloQueueMatch(bracket_id, perTeam, tanks, healers))
|
|
{
|
|
Battleground* bg2 = sBattlegroundMgr->CreateNewBattleground(m_queueId, bracket_id);
|
|
if (!bg2)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create Rated Solo Shuffle instance: {}", m_queueId.BattlemasterListId);
|
|
return;
|
|
}
|
|
|
|
bg2->StartBattleground();
|
|
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
|
for (GroupsQueueType::const_iterator citr = m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.begin(); citr != m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.end(); ++citr)
|
|
InviteGroupToBG((*citr), bg2, (*citr)->Team, PROPOSAL_CONFIRM_WAIT_TIME, true);
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Proposing a Rated Solo Shuffle match (3 per team, 1 healer per team)");
|
|
|
|
StartProposal(bg2, bracket_id, perTeam, SideQuota(perTeam, tanks, healers));
|
|
|
|
m_SelectionPools[TEAM_ALLIANCE].Init();
|
|
m_SelectionPools[TEAM_HORDE].Init();
|
|
}
|
|
}
|
|
else if (BattlegroundQueueIdType(m_queueId.Type) == BattlegroundQueueIdType::TrainingGrounds)
|
|
{
|
|
// Solo practice: pop the instant a single player is queued, with no opponents, into a
|
|
// BattlegroundTrainingGrounds instance (which never auto-ends). Direct invite (no proposal), like the
|
|
// rated-arena path - the player accepts via CMSG_BATTLEFIELD_PORT and the normal entry flow ports them.
|
|
for (uint32 qi = BG_QUEUE_NORMAL_ALLIANCE; qi <= BG_QUEUE_NORMAL_HORDE; ++qi)
|
|
{
|
|
for (GroupQueueInfo* ginfo : m_QueuedGroups[bracket_id][qi])
|
|
{
|
|
if (ginfo->IsInvitedToBGInstanceGUID)
|
|
continue;
|
|
|
|
Battleground* bg2 = sBattlegroundMgr->CreateNewBattleground(m_queueId, bracket_id);
|
|
if (!bg2)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create Training Grounds instance: {}", m_queueId.BattlemasterListId);
|
|
return;
|
|
}
|
|
|
|
InviteGroupToBG(ginfo, bg2, ginfo->Team);
|
|
bg2->StartBattleground();
|
|
return; // one instance per pass
|
|
}
|
|
}
|
|
}
|
|
else if (BattlegroundQueueIdType(m_queueId.Type) == BattlegroundQueueIdType::RatedBattlegroundBlitz)
|
|
{
|
|
// Rated + non-arena previously fell through here doing nothing at all, which is why a rated
|
|
// battleground queue could never pop.
|
|
uint32 const perTeam = sBattlegroundMgr->isTesting() ? 1 : MaxPlayersPerTeam;
|
|
uint32 const tanks = sBattlegroundMgr->isTesting() ? 0 : sWorld->getIntConfig(CONFIG_BATTLEGROUND_BLITZ_TANKS_PER_TEAM);
|
|
uint32 const healers = sBattlegroundMgr->isTesting() ? 0 : sWorld->getIntConfig(CONFIG_BATTLEGROUND_BLITZ_HEALERS_PER_TEAM);
|
|
|
|
// One proposal at a time per bracket: a second lobby built while the first is still deciding would be
|
|
// drawing from entries the first one has already claimed.
|
|
for (std::pair<uint32 const, BattlegroundProposal> const& pending : m_Proposals)
|
|
if (pending.second.BracketId == bracket_id)
|
|
return;
|
|
|
|
if (CheckSoloQueueMatch(bracket_id, perTeam, tanks, healers))
|
|
{
|
|
// The battleground is created BEFORE anyone is asked to confirm, which is what makes the map id
|
|
// and the per-side split real numbers rather than guesses - see the decode block in
|
|
// BattlegroundPackets.h. A proposal that collapses drops this instance again.
|
|
Battleground* bg2 = sBattlegroundMgr->CreateNewBattleground(m_queueId, bracket_id);
|
|
if (!bg2)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue::Update - Cannot create Battleground Blitz instance: {}", m_queueId.BattlemasterListId);
|
|
return;
|
|
}
|
|
|
|
// Registers the instance with BattlegroundMgr, which CMSG_BATTLEFIELD_PORT needs to resolve the
|
|
// invite. Rated battlegrounds are skipped by the free-slot filler above, so this does not let
|
|
// anyone else be dealt into the lobby while it is deciding.
|
|
bg2->StartBattleground();
|
|
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
|
for (GroupsQueueType::const_iterator citr = m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.begin(); citr != m_SelectionPools[TEAM_ALLIANCE + i].SelectedGroups.end(); ++citr)
|
|
InviteGroupToBG((*citr), bg2, (*citr)->Team, PROPOSAL_ACCEPT_WAIT_TIME, true);
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Proposing a Battleground Blitz match ({} per team: {} tanks, {} healers, {} damagers)",
|
|
perTeam, tanks, healers, perTeam - tanks - healers);
|
|
|
|
StartProposal(bg2, bracket_id, perTeam, SideQuota(perTeam, tanks, healers));
|
|
|
|
m_SelectionPools[TEAM_ALLIANCE].Init();
|
|
m_SelectionPools[TEAM_HORDE].Init();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*********************************************************/
|
|
/*** BATTLEGROUND QUEUE EVENTS ***/
|
|
/*********************************************************/
|
|
|
|
bool BGQueueInviteEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/)
|
|
{
|
|
Player* player = ObjectAccessor::FindConnectedPlayer(m_PlayerGuid);
|
|
// player logged off (we should do nothing, he is correctly removed from queue in another procedure)
|
|
if (!player)
|
|
return true;
|
|
|
|
Battleground* bg = sBattlegroundMgr->GetBattleground(m_BgInstanceGUID, m_BgTypeId);
|
|
//if battleground ended and its instance deleted - do nothing
|
|
if (!bg)
|
|
return true;
|
|
|
|
uint32 queueSlot = player->GetBattlegroundQueueIndex(m_QueueId);
|
|
if (queueSlot < PLAYER_MAX_BATTLEGROUND_QUEUES) // player is in queue or in battleground
|
|
{
|
|
// check if player is invited to this bg
|
|
BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(m_QueueId);
|
|
if (bgQueue.IsPlayerInvited(m_PlayerGuid, m_BgInstanceGUID, m_RemoveTime))
|
|
{
|
|
// Timeout is the REMAINING life of the invitation, not a constant. InviteGroupToBG schedules this
|
|
// reminder INVITATION_REMIND_TIME before ginfo->RemoveInviteTime, and that deadline is what
|
|
// BGQueueRemoveEvent / BGQueueProposalTimeoutEvent actually act on - so it is the only honest
|
|
// source for the number. A constant INVITE_ACCEPT_WAIT_TIME - INVITATION_REMIND_TIME would restart
|
|
// the client's confirmation countdown at 70000 ms when 20000 ms are left, and would be wrong by a
|
|
// further 60000 ms for the shorter PROPOSAL_ACCEPT_WAIT_TIME window of the solo queues, whose
|
|
// SMSG_BATTLEFIELD_STATUS_WAIT_FOR_GROUPS already told the same client 30000 ms.
|
|
uint32 now = GameTime::GetGameTimeMS();
|
|
uint32 remaining = m_RemoveTime > now ? m_RemoveTime - now : 0;
|
|
|
|
WorldPackets::Battleground::BattlefieldStatusNeedConfirmation battlefieldStatus;
|
|
BattlegroundMgr::BuildBattlegroundStatusNeedConfirmation(&battlefieldStatus, bg, player, queueSlot, player->GetBattlegroundQueueJoinTime(m_QueueId), remaining, m_QueueId, bgQueue.GetPlayerRole(m_PlayerGuid));
|
|
player->SendDirectMessage(battlefieldStatus.Write());
|
|
}
|
|
}
|
|
return true; //event will be deleted
|
|
}
|
|
|
|
void BGQueueInviteEvent::Abort(uint64 /*e_time*/)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
/*
|
|
this event has many possibilities when it is executed:
|
|
1. player is in battleground (he clicked enter on invitation window)
|
|
2. player left battleground queue and he isn't there any more
|
|
3. player left battleground queue and he joined it again and IsInvitedToBGInstanceGUID = 0
|
|
4. player left queue and he joined again and he has been invited to same battleground again -> we should not remove him from queue yet
|
|
5. player is invited to bg and he didn't choose what to do and timer expired - only in this condition we should call queue::RemovePlayer
|
|
we must remove player in the 5. case even if battleground object doesn't exist!
|
|
*/
|
|
bool BGQueueRemoveEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/)
|
|
{
|
|
Player* player = ObjectAccessor::FindConnectedPlayer(m_PlayerGuid);
|
|
if (!player)
|
|
// player logged off (we should do nothing, he is correctly removed from queue in another procedure)
|
|
return true;
|
|
|
|
Battleground* bg = sBattlegroundMgr->GetBattleground(m_BgInstanceGUID, BattlegroundTypeId(m_BgQueueTypeId.BattlemasterListId));
|
|
//battleground can be deleted already when we are removing queue info
|
|
//bg pointer can be NULL! so use it carefully!
|
|
|
|
uint32 queueSlot = player->GetBattlegroundQueueIndex(m_BgQueueTypeId);
|
|
if (queueSlot < PLAYER_MAX_BATTLEGROUND_QUEUES) // player is in queue, or in Battleground
|
|
{
|
|
// check if player is in queue for this BG and if we are removing his invite event
|
|
BattlegroundQueue &bgQueue = sBattlegroundMgr->GetBattlegroundQueue(m_BgQueueTypeId);
|
|
if (bgQueue.IsPlayerInvited(m_PlayerGuid, m_BgInstanceGUID, m_RemoveTime))
|
|
{
|
|
TC_LOG_DEBUG("bg.battleground", "Battleground: removing player {} from bg queue for instance {} because of not pressing enter battle in time.", player->GetGUID().ToString(), m_BgInstanceGUID);
|
|
|
|
player->RemoveBattlegroundQueueId(m_BgQueueTypeId);
|
|
bgQueue.RemovePlayer(m_PlayerGuid, true);
|
|
//update queues if battleground isn't ended
|
|
if (bg && bg->isBattleground() && bg->GetStatus() != STATUS_WAIT_LEAVE)
|
|
sBattlegroundMgr->ScheduleQueueUpdate(0, m_BgQueueTypeId, bg->GetBracketId());
|
|
|
|
WorldPackets::Battleground::BattlefieldStatusNone battlefieldStatus;
|
|
BattlegroundMgr::BuildBattlegroundStatusNone(&battlefieldStatus, player, queueSlot, player->GetBattlegroundQueueJoinTime(m_BgQueueTypeId));
|
|
player->SendDirectMessage(battlefieldStatus.Write());
|
|
}
|
|
}
|
|
|
|
//event will be deleted
|
|
return true;
|
|
}
|
|
|
|
void BGQueueRemoveEvent::Abort(uint64 /*e_time*/)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
bool BattlegroundQueue::AddWargameSide(Player* leader, Group* group, Battleground* bg, PVPDifficultyEntry const* bracketEntry, Team team)
|
|
{
|
|
// Queue the group onto the forced team (premade so it lands in the premade pool), then register the queue slot
|
|
// on each member and invite the whole side. Rating fields are irrelevant for war games (0).
|
|
GroupQueueInfo* ginfo = AddGroup(leader, group, team, bracketEntry, true, 0, 0);
|
|
if (!ginfo)
|
|
return false;
|
|
|
|
if (group)
|
|
{
|
|
for (GroupReference const& ref : group->GetMembers())
|
|
if (Player* member = ref.GetSource())
|
|
member->AddBattlegroundQueueId(m_queueId);
|
|
}
|
|
else
|
|
leader->AddBattlegroundQueueId(m_queueId);
|
|
|
|
return InviteGroupToBG(ginfo, bg, team);
|
|
}
|
|
|
|
// Solo-queue matchmaker for Battleground Blitz (and any future rated solo-queue mode).
|
|
//
|
|
// Differs from CheckNormalMatch/CheckPremadeMatch in two ways:
|
|
// - it draws from the RATED lists. AddGroup only pushes an entry into BG_QUEUE_NORMAL_* when the queue is
|
|
// unrated (`if (!m_queueId.Rated && !isPremade) index += PVP_TEAMS_COUNT`), so for a rated queue every
|
|
// entry - solo or duo - lives in BG_QUEUE_PREMADE_ALLIANCE / _HORDE.
|
|
// - it is faction-blind. Blitz matches cross-faction, so entries are dealt into whichever pool needs them
|
|
// and their Team is rewritten, exactly as CheckSkirmishForSameFaction does for same-faction skirmishes.
|
|
//
|
|
// tanksPerTeam and healersPerTeam are configured quotas; damagers are the remainder of playersPerTeam, so a
|
|
// team composition is fully specified by those two numbers. They are not client-derived: the wire says what
|
|
// a queuer is willing to play, not how retail balances a Blitz match. Setting both to 0 turns the role check
|
|
// off and leaves a pure headcount match. The defaults (0 tanks, 2 healers, 8 per side) reproduce the
|
|
// 0 tanks / 4 healers / 12 damagers composition the client was told in C:\sniff\rated BG 12.0.7.pkt.
|
|
//
|
|
// Roles come from PlayerQueueInfo::Role, so they are per player, not per queue entry - a duo whose members
|
|
// have different specs is counted as the two roles it really contains.
|
|
bool BattlegroundQueue::CheckSoloQueueMatch(BattlegroundBracketId bracket_id, uint32 playersPerTeam, uint32 tanksPerTeam, uint32 healersPerTeam)
|
|
{
|
|
if (tanksPerTeam + healersPerTeam > playersPerTeam)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue: solo queue {} is configured with {} tanks + {} healers per team but only {} slots; refusing to match.",
|
|
m_queueId.BattlemasterListId, tanksPerTeam, healersPerTeam, playersPerTeam);
|
|
return false;
|
|
}
|
|
|
|
uint8 const ratedLists[PVP_TEAMS_COUNT] = { uint8(BG_QUEUE_PREMADE_ALLIANCE), uint8(BG_QUEUE_PREMADE_HORDE) };
|
|
|
|
PvpRoleHeadcount const perSideQuota = SideQuota(playersPerTeam, tanksPerTeam, healersPerTeam);
|
|
|
|
std::vector<GroupQueueInfo*> candidates;
|
|
uint32 totalPlayers = 0;
|
|
|
|
for (uint8 list : ratedLists)
|
|
{
|
|
for (GroupQueueInfo* ginfo : m_QueuedGroups[bracket_id][list])
|
|
{
|
|
if (ginfo->IsInvitedToBGInstanceGUID)
|
|
continue;
|
|
|
|
totalPlayers += uint32(ginfo->Players.size());
|
|
candidates.push_back(ginfo);
|
|
}
|
|
}
|
|
|
|
if (totalPlayers < playersPerTeam * PVP_TEAMS_COUNT)
|
|
return false;
|
|
|
|
// Longest waiting first, so the role quota never becomes a reason to jump the queue.
|
|
std::stable_sort(candidates.begin(), candidates.end(), [](GroupQueueInfo const* left, GroupQueueInfo const* right)
|
|
{
|
|
return left->JoinTime < right->JoinTime;
|
|
});
|
|
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
|
m_SelectionPools[i].Init();
|
|
|
|
// Remaining need of each side, drained as entries are placed.
|
|
std::array<PvpRoleHeadcount, PVP_TEAMS_COUNT> need;
|
|
need.fill(perSideQuota);
|
|
|
|
for (GroupQueueInfo* ginfo : candidates)
|
|
{
|
|
if (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() >= playersPerTeam
|
|
&& m_SelectionPools[TEAM_HORDE].GetPlayerCount() >= playersPerTeam)
|
|
break;
|
|
|
|
// What this entry brings, per role. A duo of a healer and a damager counts as one of each.
|
|
PvpRoleHeadcount entryRoles = { };
|
|
for (std::pair<ObjectGuid const, PlayerQueueInfo*> const& player : ginfo->Players)
|
|
++entryRoles[RoleIndex(FoldRole(player.second->Role, perSideQuota))];
|
|
|
|
for (uint32 side = 0; side < PVP_TEAMS_COUNT; ++side)
|
|
{
|
|
// SelectionPool::AddGroup answers "may I keep filling", not "did I take it", so the fit has to be
|
|
// decided here - otherwise the role budget below would be spent on an entry that was never added.
|
|
if (m_SelectionPools[side].GetPlayerCount() + ginfo->Players.size() > playersPerTeam)
|
|
continue;
|
|
|
|
bool fits = true;
|
|
for (std::size_t role = 0; role < PVP_QUEUE_ROLE_COUNT && fits; ++role)
|
|
fits = entryRoles[role] <= need[side][role];
|
|
|
|
if (!fits)
|
|
continue;
|
|
|
|
m_SelectionPools[side].AddGroup(ginfo, playersPerTeam);
|
|
|
|
for (std::size_t role = 0; role < PVP_QUEUE_ROLE_COUNT; ++role)
|
|
need[side][role] -= entryRoles[role];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (m_SelectionPools[TEAM_ALLIANCE].GetPlayerCount() != playersPerTeam
|
|
|| m_SelectionPools[TEAM_HORDE].GetPlayerCount() != playersPerTeam)
|
|
{
|
|
// Leave no half-built selection behind for the next caller.
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
|
m_SelectionPools[i].Init();
|
|
return false;
|
|
}
|
|
|
|
// Both pools are full. Re-home every entry that ended up on the other faction's side: set its Team, push it
|
|
// into the destination rated list and erase it from the source list. Skipping the list move makes
|
|
// RemovePlayer fail to find the entry and leaks queue slots - the same idiom as CheckSkirmishForSameFaction.
|
|
for (uint32 i = 0; i < PVP_TEAMS_COUNT; ++i)
|
|
{
|
|
TeamId const poolTeam = TeamId(TEAM_ALLIANCE + i);
|
|
Team const poolTeamId = poolTeam == TEAM_ALLIANCE ? ALLIANCE : HORDE;
|
|
|
|
for (GroupQueueInfo* ginfo : m_SelectionPools[poolTeam].SelectedGroups)
|
|
{
|
|
if (ginfo->Team == poolTeamId)
|
|
continue;
|
|
|
|
uint8 const sourceList = ginfo->Team == ALLIANCE ? uint8(BG_QUEUE_PREMADE_ALLIANCE) : uint8(BG_QUEUE_PREMADE_HORDE);
|
|
uint8 const destList = poolTeam == TEAM_ALLIANCE ? uint8(BG_QUEUE_PREMADE_ALLIANCE) : uint8(BG_QUEUE_PREMADE_HORDE);
|
|
|
|
ginfo->Team = poolTeamId;
|
|
m_QueuedGroups[bracket_id][destList].push_front(ginfo);
|
|
|
|
for (GroupsQueueType::iterator itr = m_QueuedGroups[bracket_id][sourceList].begin();
|
|
itr != m_QueuedGroups[bracket_id][sourceList].end(); ++itr)
|
|
{
|
|
if (*itr == ginfo)
|
|
{
|
|
m_QueuedGroups[bracket_id][sourceList].erase(itr);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*********************************************************/
|
|
/*** ALL-OR-NOTHING GROUP PROPOSALS ***/
|
|
/*********************************************************/
|
|
|
|
BattlegroundProposal* BattlegroundQueue::FindProposalFor(ObjectGuid guid)
|
|
{
|
|
return const_cast<BattlegroundProposal*>(std::as_const(*this).FindProposalFor(guid));
|
|
}
|
|
|
|
BattlegroundProposal const* BattlegroundQueue::FindProposalFor(ObjectGuid guid) const
|
|
{
|
|
for (std::pair<uint32 const, BattlegroundProposal> const& pair : m_Proposals)
|
|
for (BattlegroundProposalMember const& member : pair.second.Members)
|
|
if (member.Guid == guid)
|
|
return &pair.second;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void BattlegroundQueue::StartProposal(Battleground* bg, BattlegroundBracketId bracketId, uint32 playersPerTeam, PvpRoleHeadcount const& perSideQuota)
|
|
{
|
|
BattlegroundProposal proposal;
|
|
proposal.BgInstanceGUID = bg->GetInstanceID();
|
|
proposal.BgTypeId = bg->GetTypeID();
|
|
proposal.BracketId = bracketId;
|
|
proposal.MapId = bg->GetMapId();
|
|
proposal.SlotsPerSide.fill(uint8(playersPerTeam));
|
|
|
|
for (uint32 side = 0; side < PVP_TEAMS_COUNT; ++side)
|
|
{
|
|
for (GroupQueueInfo const* ginfo : m_SelectionPools[side].SelectedGroups)
|
|
{
|
|
for (std::pair<ObjectGuid const, PlayerQueueInfo*> const& player : ginfo->Players)
|
|
{
|
|
// Mirrors InviteGroupToBG, which only invites and only counts players it finds online.
|
|
if (!ObjectAccessor::FindConnectedPlayer(player.first))
|
|
continue;
|
|
|
|
BattlegroundProposalMember& member = proposal.Members.emplace_back();
|
|
member.Guid = player.first;
|
|
member.Side = TeamId(side);
|
|
// Folded exactly as CheckSoloQueueMatch folded it, so the role block the client is sent
|
|
// describes the same lobby the matcher built.
|
|
member.Role = FoldRole(player.second->Role, perSideQuota);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (proposal.Members.empty())
|
|
return;
|
|
|
|
uint32 const instanceId = proposal.BgInstanceGUID;
|
|
BattlegroundProposal const& stored = m_Proposals.insert_or_assign(instanceId, std::move(proposal)).first->second;
|
|
|
|
m_events.AddEvent(new BGQueueProposalTimeoutEvent(instanceId, m_queueId), m_events.CalculateTime(Milliseconds(PROPOSAL_ACCEPT_WAIT_TIME)));
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Battleground: opened a {} ms group proposal over BG instance {} (bracket {}, map {}) for {} players.",
|
|
uint32(PROPOSAL_ACCEPT_WAIT_TIME), instanceId, uint32(bracketId), stored.MapId, uint32(stored.Members.size()));
|
|
|
|
SendProposalStatus(stored, BattlegroundProposalStatus::Waiting);
|
|
}
|
|
|
|
// Tells every member where the proposal stands.
|
|
// Waiting - SMSG_BATTLEFIELD_STATUS_WAIT_FOR_GROUPS, accepted players as Secured, undecided ones as Awaited.
|
|
// Failed - SMSG_BATTLEFIELD_STATUS_GROUP_PROPOSAL_FAILED, which keeps Secured and moves the outstanding
|
|
// players into Lost. That is the exact shape of both captured 0x48000E bodies.
|
|
// Formed - SMSG_BATTLEFIELD_STATUS_WAIT_FOR_GROUPS with every counter zeroed, matching the terminal
|
|
// 0x48000D body the capture ends on once the match exists.
|
|
void BattlegroundQueue::SendProposalStatus(BattlegroundProposal const& proposal, BattlegroundProposalStatus status) const
|
|
{
|
|
WorldPackets::Battleground::PvpRoleQueueCounts counts;
|
|
std::array<uint8, PVP_TEAMS_COUNT> awaitedPerSide = { };
|
|
|
|
if (status != BattlegroundProposalStatus::Formed)
|
|
{
|
|
for (BattlegroundProposalMember const& member : proposal.Members)
|
|
{
|
|
std::size_t const role = RoleIndex(member.Role);
|
|
if (member.Accepted)
|
|
{
|
|
++counts.Secured[role];
|
|
continue;
|
|
}
|
|
|
|
++awaitedPerSide[member.Side];
|
|
if (status == BattlegroundProposalStatus::Failed)
|
|
++counts.Lost[role];
|
|
else
|
|
++counts.Awaited[role];
|
|
}
|
|
}
|
|
|
|
for (BattlegroundProposalMember const& member : proposal.Members)
|
|
{
|
|
// Counted above, but not told: the decliner is answered by their own leave-queue path with
|
|
// SMSG_BATTLEFIELD_STATUS_NONE, which is the last word either way.
|
|
if (member.Declined)
|
|
continue;
|
|
|
|
Player* player = ObjectAccessor::FindConnectedPlayer(member.Guid);
|
|
if (!player)
|
|
continue;
|
|
|
|
uint32 const queueSlot = player->GetBattlegroundQueueIndex(m_queueId);
|
|
if (queueSlot >= PLAYER_MAX_BATTLEGROUND_QUEUES)
|
|
continue;
|
|
|
|
uint32 const joinTime = player->GetBattlegroundQueueJoinTime(m_queueId);
|
|
|
|
if (status == BattlegroundProposalStatus::Failed)
|
|
{
|
|
WorldPackets::Battleground::BattlefieldStatusGroupProposalFailed packet;
|
|
BattlegroundMgr::BuildBattlegroundStatusGroupProposalFailed(&packet, player, queueSlot, joinTime, m_queueId, counts);
|
|
player->SendDirectMessage(packet.Write());
|
|
}
|
|
else
|
|
{
|
|
WorldPackets::Battleground::BattlefieldStatusWaitForGroups packet;
|
|
BattlegroundMgr::BuildBattlegroundStatusWaitForGroups(&packet, player, queueSlot, joinTime, m_queueId,
|
|
proposal.MapId, PROPOSAL_ACCEPT_WAIT_TIME, proposal.SlotsPerSide, awaitedPerSide, counts);
|
|
player->SendDirectMessage(packet.Write());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool BattlegroundQueue::ProposalAccept(ObjectGuid guid)
|
|
{
|
|
BattlegroundProposal* proposal = FindProposalFor(guid);
|
|
if (!proposal)
|
|
return false;
|
|
|
|
bool everyoneAccepted = true;
|
|
for (BattlegroundProposalMember& member : proposal->Members)
|
|
{
|
|
if (member.Guid == guid)
|
|
member.Accepted = true;
|
|
|
|
if (!member.Accepted)
|
|
everyoneAccepted = false;
|
|
}
|
|
|
|
if (!everyoneAccepted)
|
|
{
|
|
// Held. The client is told who is still missing rather than being left on a dialog that did nothing.
|
|
SendProposalStatus(*proposal, BattlegroundProposalStatus::Waiting);
|
|
return true;
|
|
}
|
|
|
|
ResolveProposal(proposal->BgInstanceGUID, true);
|
|
return true;
|
|
}
|
|
|
|
bool BattlegroundQueue::ProposalDecline(ObjectGuid guid)
|
|
{
|
|
BattlegroundProposal* proposal = FindProposalFor(guid);
|
|
if (!proposal)
|
|
return false;
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Battleground: {} declined the group proposal over BG instance {}; collapsing it.",
|
|
guid.ToString(), proposal->BgInstanceGUID);
|
|
|
|
// The decliner is marked, NOT removed. Removing them made the role block describe 15 of 16 players, and the
|
|
// client adds Awaited+Secured+Lost per role to get the lobby size: 20 decoded bodies have that sum constant
|
|
// at the team size, so a short sum is a wrong packet. They are counted as a loss (Accepted is cleared) and
|
|
// skipped by the collapse below, because their own removal from the queue is the caller's ordinary
|
|
// leave-queue path and must not happen twice.
|
|
uint32 const instanceId = proposal->BgInstanceGUID;
|
|
for (BattlegroundProposalMember& member : proposal->Members)
|
|
{
|
|
if (member.Guid != guid)
|
|
continue;
|
|
|
|
member.Accepted = false;
|
|
member.Declined = true;
|
|
}
|
|
|
|
ResolveProposal(instanceId, false);
|
|
return true;
|
|
}
|
|
|
|
void BattlegroundQueue::ProposalTimeout(uint32 bgInstanceGuid)
|
|
{
|
|
if (m_Proposals.find(bgInstanceGuid) == m_Proposals.end())
|
|
return; // already resolved - completed, or collapsed by a decline
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Battleground: group proposal over BG instance {} expired; collapsing it.", bgInstanceGuid);
|
|
ResolveProposal(bgInstanceGuid, false);
|
|
}
|
|
|
|
void BattlegroundQueue::ResolveProposal(uint32 bgInstanceGuid, bool accepted)
|
|
{
|
|
std::unordered_map<uint32, BattlegroundProposal>::iterator itr = m_Proposals.find(bgInstanceGuid);
|
|
if (itr == m_Proposals.end())
|
|
return;
|
|
|
|
// Take the proposal out of the map before doing anything that can re-enter this queue: porting a player
|
|
// calls RemovePlayer, which can free the GroupQueueInfo the pools point at.
|
|
BattlegroundProposal proposal = std::move(itr->second);
|
|
m_Proposals.erase(itr);
|
|
|
|
Battleground* bg = sBattlegroundMgr->GetBattleground(proposal.BgInstanceGUID, proposal.BgTypeId);
|
|
|
|
if (accepted)
|
|
{
|
|
if (!bg)
|
|
{
|
|
TC_LOG_ERROR("bg.battleground", "BattlegroundQueue: group proposal over BG instance {} completed but the battleground is gone.", proposal.BgInstanceGUID);
|
|
return;
|
|
}
|
|
|
|
// The client clears its role display when the match forms - that is what the terminal 0x48000D body
|
|
// in the capture is - so say so before the ports start rewriting queue state.
|
|
SendProposalStatus(proposal, BattlegroundProposalStatus::Formed);
|
|
|
|
for (BattlegroundProposalMember const& member : proposal.Members)
|
|
{
|
|
Player* player = ObjectAccessor::FindConnectedPlayer(member.Guid);
|
|
if (!player)
|
|
continue; // logged out between accepting and the last accept
|
|
|
|
uint32 const queueSlot = player->GetBattlegroundQueueIndex(m_queueId);
|
|
if (queueSlot >= PLAYER_MAX_BATTLEGROUND_QUEUES)
|
|
continue;
|
|
|
|
Team const team = member.Side == TEAM_ALLIANCE ? ALLIANCE : HORDE;
|
|
BattlegroundMgr::PortPlayerToBattleground(player, bg, team, m_queueId, queueSlot);
|
|
}
|
|
|
|
TC_LOG_DEBUG("bg.battleground", "Battleground: group proposal over BG instance {} was accepted by all {} members.",
|
|
proposal.BgInstanceGUID, uint32(proposal.Members.size()));
|
|
return;
|
|
}
|
|
|
|
// Collapse. Everyone still listed loses the invite; whether they keep their queue slot depends on whether
|
|
// they accepted - and, for a duo, on how the other half answered.
|
|
SendProposalStatus(proposal, BattlegroundProposalStatus::Failed);
|
|
|
|
// Blitz may be entered as a duo (BattlemasterList 1101, MaxGroupSize 2), and below the proposal a queue
|
|
// ENTRY is atomic: RemovePlayer drops every remaining member of a GroupQueueInfo the moment one of them
|
|
// leaves a rated queue - the recursive branch above, "everyone from the group should leave too". A duo
|
|
// whose halves answered differently can therefore not be split: the accepting half leaves with its
|
|
// partner, and the invariant documented on BattlegroundProposal holds per queue ENTRY, not per player.
|
|
// Which entries are about to break is settled here, before the first removal, instead of being left to
|
|
// fall out of that cascade. Otherwise the accepting member is told BATTLEFIELD_STATUS_QUEUED and then, in
|
|
// the same tick, BATTLEFIELD_STATUS_NONE by the cascade - and which of the two it ends up with depends on
|
|
// the order the two members happen to have in proposal.Members. A member who declined breaks the entry
|
|
// too: its own removal only happens after this function returns, in HandleBattleFieldPortOpcode.
|
|
std::vector<GroupQueueInfo const*> brokenEntries;
|
|
for (BattlegroundProposalMember const& member : proposal.Members)
|
|
if (!member.Accepted)
|
|
if (PlayerQueueInfo const* playerInfo = Trinity::Containers::MapGetValuePtr(m_QueuedPlayers, member.Guid))
|
|
brokenEntries.push_back(playerInfo->GroupInfo);
|
|
|
|
auto entryIsBroken = [&brokenEntries](GroupQueueInfo const* ginfo)
|
|
{
|
|
return std::find(brokenEntries.begin(), brokenEntries.end(), ginfo) != brokenEntries.end();
|
|
};
|
|
|
|
// The invite is revoked for everyone first, and only then is anyone removed: the first removal can delete
|
|
// a GroupQueueInfo that the members listed after it still point at.
|
|
for (BattlegroundProposalMember const& member : proposal.Members)
|
|
{
|
|
// The decliner is already being taken out of the queue by WorldSession::HandleBattleFieldPortOpcode,
|
|
// whose RemovePlayer(guid, true) does this same invited-count bookkeeping. Doing it here as well would
|
|
// decrement it twice.
|
|
if (member.Declined)
|
|
continue;
|
|
|
|
// The invited count was raised once per invited PLAYER, so it has to come back down once per member,
|
|
// while the group's invite flag is a per-entry thing that is only cleared the first time.
|
|
if (PlayerQueueInfo const* playerInfo = Trinity::Containers::MapGetValuePtr(m_QueuedPlayers, member.Guid))
|
|
{
|
|
if (GroupQueueInfo* ginfo = playerInfo->GroupInfo)
|
|
{
|
|
if (bg)
|
|
bg->DecreaseInvitedCount(ginfo->Team);
|
|
|
|
if (ginfo->IsInvitedToBGInstanceGUID == proposal.BgInstanceGUID)
|
|
{
|
|
ginfo->IsInvitedToBGInstanceGUID = 0;
|
|
ginfo->RemoveInviteTime = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Player* player = ObjectAccessor::FindConnectedPlayer(member.Guid))
|
|
player->SetInviteForBattlegroundQueueType(m_queueId, 0);
|
|
}
|
|
|
|
for (BattlegroundProposalMember const& member : proposal.Members)
|
|
{
|
|
if (member.Declined)
|
|
continue;
|
|
|
|
Player* player = ObjectAccessor::FindConnectedPlayer(member.Guid);
|
|
if (!player)
|
|
continue;
|
|
|
|
// Removing one half of a duo takes the other half with it, so a member can already be out of the queue
|
|
// when its own turn comes. That cascade sent it BATTLEFIELD_STATUS_NONE and there is nothing to add.
|
|
PlayerQueueInfo const* playerInfo = Trinity::Containers::MapGetValuePtr(m_QueuedPlayers, member.Guid);
|
|
if (!playerInfo)
|
|
continue;
|
|
|
|
uint32 const queueSlot = player->GetBattlegroundQueueIndex(m_queueId);
|
|
if (queueSlot >= PLAYER_MAX_BATTLEGROUND_QUEUES)
|
|
continue;
|
|
|
|
uint32 const joinTime = player->GetBattlegroundQueueJoinTime(m_queueId);
|
|
|
|
if (!member.Accepted || entryIsBroken(playerInfo->GroupInfo))
|
|
{
|
|
// Never answered, answered no, or accepted while sharing a queue entry with someone who did not.
|
|
// Same outcome a plain invite timeout has: out of the queue. Where this removal cascades onto a
|
|
// partner who declined, that player sees the cascade BATTLEFIELD_STATUS_NONE shortly before the
|
|
// one its own session sends - the same ticket in the same state twice, a repeat rather than the
|
|
// QUEUED-then-NONE contradiction this ordering exists to prevent.
|
|
player->RemoveBattlegroundQueueId(m_queueId);
|
|
RemovePlayer(member.Guid, false);
|
|
|
|
WorldPackets::Battleground::BattlefieldStatusNone packet;
|
|
BattlegroundMgr::BuildBattlegroundStatusNone(&packet, player, queueSlot, joinTime);
|
|
player->SendDirectMessage(packet.Write());
|
|
continue;
|
|
}
|
|
|
|
// Accepted, and is being sent back through no fault of their own. Their GroupQueueInfo is left alone,
|
|
// so JoinTime - and with it the position CheckSoloQueueMatch orders by - is exactly what it was.
|
|
GroupQueueInfo groupInfo;
|
|
bool const hasGroupInfo = GetPlayerGroupInfoData(member.Guid, &groupInfo);
|
|
uint32 const avgWaitTime = hasGroupInfo ? GetAverageQueueWaitTime(&groupInfo, proposal.BracketId) : 0;
|
|
|
|
// AsGroup is derived, not assumed: a Blitz duo (BattlemasterList 1101, MaxGroupSize 2) that both
|
|
// accepted while a third member let the window lapse re-enters here with its queue entry intact, and
|
|
// the very next CMSG_REQUEST_BATTLEFIELD_STATUS would answer true from the same Players.size() - so a
|
|
// hardcoded false made the bit read true, false, true on one ticket within seconds.
|
|
WorldPackets::Battleground::BattlefieldStatusQueued packet;
|
|
BattlegroundMgr::BuildBattlegroundStatusQueued(&packet, player, queueSlot, joinTime, m_queueId, avgWaitTime,
|
|
hasGroupInfo && groupInfo.Players.size() > 1);
|
|
player->SendDirectMessage(packet.Write());
|
|
}
|
|
|
|
// The battleground was created for this proposal only. Drop it, so the next attempt picks a fresh map -
|
|
// which is what retail does across the three proposal runs in the capture.
|
|
if (bg)
|
|
{
|
|
bg->RemoveFromBGFreeSlotQueue();
|
|
bg->SetDeleteThis();
|
|
}
|
|
|
|
sBattlegroundMgr->ScheduleQueueUpdate(0, m_queueId, proposal.BracketId);
|
|
}
|
|
|
|
bool BGQueueProposalTimeoutEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/)
|
|
{
|
|
// No-op when the proposal already resolved - it is looked up by instance id, which is never reused.
|
|
sBattlegroundMgr->GetBattlegroundQueue(m_QueueId).ProposalTimeout(m_BgInstanceGUID);
|
|
return true; //event will be deleted
|
|
}
|
|
|
|
void BGQueueProposalTimeoutEvent::Abort(uint64 /*e_time*/)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
ChrSpecializationRole BattlegroundQueue::GetPlayerRole(ObjectGuid guid) const
|
|
{
|
|
QueuedPlayersMap::const_iterator itr = m_QueuedPlayers.find(guid);
|
|
return itr != m_QueuedPlayers.end() ? itr->second.Role : ChrSpecializationRole::Dps;
|
|
}
|