Files
Viano-Core/src/server/game/DungeonFinding/LFGQueue.cpp
T
Nico aba5aeba2c Keep queued adventure bots active and preserve browser admission rules
Reuse formation performances while waiting for dungeon, raid or PvP admission, and accept compatible formation invitations without active recruitment or interrupting battleground offers.

Require the full low-population wait for every participating queue entry and acknowledge connected clientless members during native group teleportation.

Add regression coverage and document builds, live Yarumak/Seraellin checks and separately preserved shutdown crash findings.
2026-11-04 14:19:38 +01:00

945 lines
34 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 "ObjectDefines.h"
#include "Containers.h"
#include "DBCStructure.h"
#include "DBCStores.h"
#include "GameTime.h"
#include "Group.h"
#include "LFGQueue.h"
#include "LFGMgr.h"
#include "ClientlessBrowserPolicy.h"
#include "World.h"
#include "Log.h"
namespace lfg
{
/**
Given a list of guids returns the concatenation using | as delimiter
@param[in] check list of guids
@returns Concatenated string
*/
std::string ConcatenateGuids(GuidList const& check)
{
if (check.empty())
return "";
// need the guids in order to avoid duplicates
GuidSet guids(check.begin(), check.end());
std::ostringstream o;
GuidSet::const_iterator it = guids.begin();
o << it->GetRawValue();
for (++it; it != guids.end(); ++it)
o << '|' << it->GetRawValue();
return o.str();
}
char const* GetCompatibleString(LfgCompatibility compatibles)
{
switch (compatibles)
{
case LFG_COMPATIBILITY_PENDING:
return "Pending";
case LFG_COMPATIBLES_BAD_STATES:
return "Compatibles (Bad States)";
case LFG_COMPATIBLES_MATCH:
return "Match";
case LFG_COMPATIBLES_WITH_LESS_PLAYERS:
return "Compatibles (Not enough players)";
case LFG_INCOMPATIBLES_HAS_IGNORES:
return "Has ignores";
case LFG_INCOMPATIBLES_MULTIPLE_LFG_GROUPS:
return "Multiple Lfg Groups";
case LFG_INCOMPATIBLES_NO_DUNGEONS:
return "Incompatible dungeons";
case LFG_INCOMPATIBLES_NO_ROLES:
return "Incompatible roles";
case LFG_INCOMPATIBLES_TOO_MUCH_PLAYERS:
return "Too many players";
case LFG_INCOMPATIBLES_WRONG_GROUP_SIZE:
return "Wrong group size";
default:
return "Unknown";
}
}
LfgQueueData::LfgQueueData(): joinTime(GameTime::GetGameTime())
{
InitializeGroupSetup();
}
void LfgQueueData::InitializeGroupSetup()
{
tanks = 0;
healers = 0;
dps = 0;
if (!dungeons.empty())
{
if (LFGDungeonData const* dungeon = sLFGMgr->GetLFGDungeon(*dungeons.begin()))
{
tanks = dungeon->requiredTanks;
healers = dungeon->requiredHealers;
dps = dungeon->requiredDamageDealers;
}
}
}
std::string LFGQueue::GetDetailedMatchRoles(GuidList const& check) const
{
if (check.empty())
return "";
// need the guids in order to avoid duplicates
GuidSet guids(check.begin(), check.end());
std::ostringstream o;
GuidSet::const_iterator it = guids.begin();
o << it->GetRawValue();
LfgQueueDataContainer::const_iterator itQueue = QueueDataStore.find(*it);
if (itQueue != QueueDataStore.end())
{
// skip leader flag, log only dps/tank/healer
auto role = itQueue->second.roles.find(*it);
if (role != itQueue->second.roles.end())
o << ' ' << GetRolesString(itQueue->second.roles.at(*it) & uint8(~PLAYER_ROLE_LEADER));
}
for (++it; it != guids.end(); ++it)
{
o << '|' << it->GetRawValue();
itQueue = QueueDataStore.find(*it);
if (itQueue != QueueDataStore.end())
{
// skip leader flag, log only dps/tank/healer
auto role = itQueue->second.roles.find(*it);
if (role != itQueue->second.roles.end())
o << ' ' << GetRolesString(itQueue->second.roles.at(*it) & uint8(~PLAYER_ROLE_LEADER));
}
}
return o.str();
}
void LFGQueue::AddToQueue(ObjectGuid guid, bool reAdd)
{
LfgQueueDataContainer::iterator itQueue = QueueDataStore.find(guid);
if (itQueue == QueueDataStore.end())
{
TC_LOG_ERROR("lfg.queue.add", "Queue data not found for [%s]", guid.ToString().c_str());
return;
}
if (reAdd)
AddToFrontCurrentQueue(guid);
else
AddToNewQueue(guid);
}
void LFGQueue::RemoveFromQueue(ObjectGuid guid)
{
RemoveFromNewQueue(guid);
RemoveFromCurrentQueue(guid);
RemoveFromCompatibles(guid);
std::ostringstream o;
o << guid.GetRawValue();
std::string sguid = o.str();
LfgQueueDataContainer::iterator itDelete = QueueDataStore.end();
for (LfgQueueDataContainer::iterator itr = QueueDataStore.begin(); itr != QueueDataStore.end(); ++itr)
if (itr->first != guid)
{
if (std::string::npos != itr->second.bestCompatible.find(sguid))
{
itr->second.bestCompatible.clear();
FindBestCompatibleInQueue(itr);
}
}
else
itDelete = itr;
if (itDelete != QueueDataStore.end())
QueueDataStore.erase(itDelete);
}
void LFGQueue::AddToNewQueue(ObjectGuid guid)
{
newToQueueStore.push_back(guid);
}
void LFGQueue::RemoveFromNewQueue(ObjectGuid guid)
{
newToQueueStore.remove(guid);
}
void LFGQueue::AddToCurrentQueue(ObjectGuid guid)
{
currentQueueStore.push_back(guid);
}
void LFGQueue::AddToFrontCurrentQueue(ObjectGuid guid)
{
currentQueueStore.push_front(guid);
}
void LFGQueue::RemoveFromCurrentQueue(ObjectGuid guid)
{
currentQueueStore.remove(guid);
}
void LFGQueue::AddQueueData(ObjectGuid guid, time_t joinTime, LfgDungeonSet const& dungeons, LfgRolesMap const& rolesMap)
{
QueueDataStore[guid] = LfgQueueData(joinTime, dungeons, rolesMap);
AddToQueue(guid);
}
void LFGQueue::RemoveQueueData(ObjectGuid guid)
{
LfgQueueDataContainer::iterator it = QueueDataStore.find(guid);
if (it != QueueDataStore.end())
QueueDataStore.erase(it);
}
void LFGQueue::UpdateWaitTimeAvg(int32 waitTime, uint32 dungeonId)
{
LfgWaitTime &wt = waitTimesAvgStore[dungeonId];
uint32 old_number = wt.number++;
wt.time = int32((wt.time * old_number + waitTime) / wt.number);
}
void LFGQueue::UpdateWaitTimeTank(int32 waitTime, uint32 dungeonId)
{
LfgWaitTime &wt = waitTimesTankStore[dungeonId];
uint32 old_number = wt.number++;
wt.time = int32((wt.time * old_number + waitTime) / wt.number);
}
void LFGQueue::UpdateWaitTimeHealer(int32 waitTime, uint32 dungeonId)
{
LfgWaitTime &wt = waitTimesHealerStore[dungeonId];
uint32 old_number = wt.number++;
wt.time = int32((wt.time * old_number + waitTime) / wt.number);
}
void LFGQueue::UpdateWaitTimeDps(int32 waitTime, uint32 dungeonId)
{
LfgWaitTime &wt = waitTimesDpsStore[dungeonId];
uint32 old_number = wt.number++;
wt.time = int32((wt.time * old_number + waitTime) / wt.number);
}
/**
Remove from cached compatible dungeons any entry that contains the given guid
@param[in] guid Guid to remove from compatible cache
*/
void LFGQueue::RemoveFromCompatibles(ObjectGuid guid)
{
std::stringstream out;
out << guid.GetRawValue();
std::string strGuid = out.str();
TC_LOG_DEBUG("lfg.queue.data.compatibles.remove", "Removing %s", guid.ToString().c_str());
for (LfgCompatibleContainer::iterator itNext = CompatibleMapStore.begin(); itNext != CompatibleMapStore.end();)
{
LfgCompatibleContainer::iterator it = itNext++;
if (std::string::npos != it->first.find(strGuid))
CompatibleMapStore.erase(it);
}
}
/**
Stores the compatibility of a list of guids
@param[in] key String concatenation of guids (| used as separator)
@param[in] compatibles type of compatibility
*/
void LFGQueue::SetCompatibles(std::string const& key, LfgCompatibility compatibles)
{
LfgCompatibilityData& data = CompatibleMapStore[key];
data.compatibility = compatibles;
}
void LFGQueue::SetCompatibilityData(std::string const& key, LfgCompatibilityData const& data)
{
CompatibleMapStore[key] = data;
}
/**
Get the compatibility of a group of guids
@param[in] key String concatenation of guids (| used as separator)
@return LfgCompatibility type of compatibility
*/
LfgCompatibility LFGQueue::GetCompatibles(std::string const& key)
{
LfgCompatibleContainer::iterator itr = CompatibleMapStore.find(key);
if (itr != CompatibleMapStore.end())
return itr->second.compatibility;
return LFG_COMPATIBILITY_PENDING;
}
LfgCompatibilityData* LFGQueue::GetCompatibilityData(std::string const& key)
{
LfgCompatibleContainer::iterator itr = CompatibleMapStore.find(key);
if (itr != CompatibleMapStore.end())
return &(itr->second);
return nullptr;
}
uint8 LFGQueue::FindGroups()
{
// Bot-only waiting populations must not trigger combinatorial searches on
// every world tick. Keep their queue records intact until a human arrives.
bool hasHuman=false,hasBot=false;
for(auto const& entry:QueueDataStore)for(auto const& member:entry.second.roles)
if(sLFGMgr->GetState(member.first)==LFG_STATE_QUEUED)
if(auto player=ObjectAccessor::FindConnectedPlayer(member.first))
{if(ClientlessBrowserPolicy::Bot(player))hasBot=true;else if(ClientlessBrowserPolicy::Human(player))hasHuman=true;}
if(hasBot && !hasHuman && !ClientlessBrowserPolicy::BotOnlyTests())return 0;
uint8 proposals = 0;
GuidList firstNew;
while (!newToQueueStore.empty())
{
ObjectGuid frontguid = newToQueueStore.front();
TC_LOG_DEBUG("lfg.queue.match.check.new", "Checking [%s] newToQueue(%u), currentQueue(%u)", frontguid.ToString().c_str(),
uint32(newToQueueStore.size()), uint32(currentQueueStore.size()));
firstNew.clear();
firstNew.push_back(frontguid);
RemoveFromNewQueue(frontguid);
GuidList temporalList = currentQueueStore;
LfgCompatibility compatibles = FindNewGroups(firstNew, temporalList);
if (compatibles == LFG_COMPATIBLES_MATCH)
++proposals;
else
AddToCurrentQueue(frontguid); // Lfg group not found, add this group to the queue.
}
// Keep normal TrinityCore matchmaking authoritative. Only after normal
// matching has failed do we permit an underfilled low-population group.
if (sWorld->getBoolConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_ENABLE))
proposals += FindLowPopulationGroups();
return proposals;
}
GuidList LFGQueue::GetQueueGuidsFromKey(std::string const& key) const
{
GuidList result;
for (GuidList::const_iterator itr = currentQueueStore.begin(); itr != currentQueueStore.end(); ++itr)
{
std::ostringstream o;
o << itr->GetRawValue();
std::string token = o.str();
size_t pos = key.find(token);
while (pos != std::string::npos)
{
size_t end = pos + token.length();
bool leftBoundary = pos == 0 || key[pos - 1] == '|';
bool rightBoundary = end == key.length() || key[end] == '|';
if (leftBoundary && rightBoundary)
{
result.push_back(*itr);
break;
}
pos = key.find(token, pos + 1);
}
}
return result;
}
uint8 LFGQueue::FindLowPopulationGroups()
{
uint8 proposals = 0;
uint32 const fallbackDelay =
sWorld->getIntConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_FALLBACK_DELAY);
uint32 const minPlayers =
sWorld->getIntConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS);
time_t const now = GameTime::GetGameTime();
bool createdProposal = true;
while (createdProposal)
{
createdProposal = false;
GuidList queueSnapshot = currentQueueStore;
for (GuidList::const_iterator itrGuid = queueSnapshot.begin(); itrGuid != queueSnapshot.end(); ++itrGuid)
{
LfgQueueDataContainer::iterator itrQueue = QueueDataStore.find(*itrGuid);
if (itrQueue == QueueDataStore.end())
continue;
LfgQueueData& queueData = itrQueue->second;
if (now < queueData.joinTime || uint32(now - queueData.joinTime) < fallbackDelay)
continue;
if (queueData.bestCompatible.empty())
FindBestCompatibleInQueue(itrQueue);
if (queueData.bestCompatible.empty())
continue;
GuidList check = GetQueueGuidsFromKey(queueData.bestCompatible);
if (check.empty())
continue;
uint32 playerCount = 0;
for (GuidList::const_iterator itrCheck = check.begin(); itrCheck != check.end(); ++itrCheck)
{
LfgQueueDataContainer::const_iterator itrData = QueueDataStore.find(*itrCheck);
if (itrData != QueueDataStore.end())
playerCount += uint32(itrData->second.roles.size());
}
// Every participating queue entry must have received the full
// fallback window, including a human joining long-waiting bots.
bool waited = std::all_of(check.begin(), check.end(), [&](ObjectGuid guid)
{
auto entry = QueueDataStore.find(guid);
return entry != QueueDataStore.end() && now >= entry->second.joinTime &&
uint64(now - entry->second.joinTime) >= fallbackDelay;
});
if (playerCount < minPlayers || !waited)
continue;
TC_LOG_DEBUG("lfg.queue.lowpopulation",
"Low population fallback after %u seconds: trying partial group (%s) with %u player(s)",
fallbackDelay, ConcatenateGuids(check).c_str(), playerCount);
if (CheckCompatibility(check, true) == LFG_COMPATIBLES_MATCH)
{
++proposals;
createdProposal = true;
break;
}
}
}
return proposals;
}
/**
Checks que main queue to try to form a Lfg group. Returns first match found (if any)
@param[in] check List of guids trying to match with other groups
@param[in] all List of all other guids in main queue to match against
@return LfgCompatibility type of compatibility between groups
*/
LfgCompatibility LFGQueue::FindNewGroups(GuidList& check, GuidList& all)
{
// Mutable browser switches and human presence are not compatibility-cache
// facts. Evaluate them before using cached native combinations.
bool bot=false,human=false;
unsigned playerCount=0;
LFGDungeonData const* selectedDungeon=nullptr;
auto hasHuman=[&](ObjectGuid guid)
{
auto found=QueueDataStore.find(guid);if(found==QueueDataStore.end())return false;
for(auto const& member:found->second.roles)
if(auto player=ObjectAccessor::FindConnectedPlayer(member.first))
if(ClientlessBrowserPolicy::Human(player))return true;
return false;
};
for(auto guid:check)
{
auto found=QueueDataStore.find(guid);if(found==QueueDataStore.end())return LFG_COMPATIBILITY_PENDING;
playerCount+=found->second.roles.size();
if(!selectedDungeon && !found->second.dungeons.empty())selectedDungeon=sLFGMgr->GetLFGDungeon(*found->second.dungeons.begin());
bool entryBot=false;
for(auto const& member:found->second.roles)
if(ClientlessBrowserPolicy::Bot(ObjectAccessor::FindConnectedPlayer(member.first)))entryBot=true;
bot|=entryBot;human|=hasHuman(guid);
if(entryBot && !found->second.dungeons.empty())
if(auto dungeon=sLFGMgr->GetLFGDungeon(*found->second.dungeons.begin()))
{
auto kind=dungeon->IsRaid()?ClientlessBrowserPolicy::Kind::Raid:ClientlessBrowserPolicy::Kind::Dungeon;
if(!ClientlessBrowserPolicy::QueueEnabled(kind) || !ClientlessBrowserPolicy::JoinEnabled(kind))return LFG_COMPATIBILITY_PENDING;
}
}
if(bot && !human && !ClientlessBrowserPolicy::BotOnlyTests())
{
// Prune a full all-bot branch immediately. Recursing into a sixth queue
// entry would consume the human candidate instead of trying that human
// in the fifth slot on the parent branch. Never cache this decision.
if((selectedDungeon && playerCount>=selectedDungeon->GetMaxGroupSize()) ||
std::none_of(all.begin(),all.end(),hasHuman))return LFG_COMPATIBILITY_PENDING;
}
std::string strGuids = ConcatenateGuids(check);
LfgCompatibility compatibles = GetCompatibles(strGuids);
TC_LOG_DEBUG("lfg.queue.match.check", "Guids: (%s): %s - all(%s)", GetDetailedMatchRoles(check).c_str(), GetCompatibleString(compatibles), GetDetailedMatchRoles(all).c_str());
if (compatibles == LFG_COMPATIBILITY_PENDING) // Not previously cached, calculate
compatibles = CheckCompatibility(check);
if (compatibles == LFG_COMPATIBLES_BAD_STATES && sLFGMgr->AllQueued(check))
{
TC_LOG_DEBUG("lfg.queue.match.check", "Guids: (%s) compatibles (cached) changed from bad states to match", GetDetailedMatchRoles(check).c_str());
SetCompatibles(strGuids, LFG_COMPATIBLES_MATCH);
return LFG_COMPATIBLES_MATCH;
}
if (compatibles != LFG_COMPATIBLES_WITH_LESS_PLAYERS)
return compatibles;
// Try to match with queued groups
while (!all.empty())
{
check.push_back(all.front());
all.pop_front();
LfgCompatibility subcompatibility = FindNewGroups(check, all);
if (subcompatibility == LFG_COMPATIBLES_MATCH)
return LFG_COMPATIBLES_MATCH;
check.pop_back();
}
return compatibles;
}
/**
Check compatibilities between groups. If group is Matched proposal will be created
@param[in] check List of guids to check compatibilities
@return LfgCompatibility type of compatibility
*/
LfgCompatibility LFGQueue::CheckCompatibility(GuidList check, bool allowIncomplete)
{
std::string strGuids = ConcatenateGuids(check);
LfgProposal proposal;
LfgDungeonSet proposalDungeons;
LfgGroupsMap proposalGroups;
LfgRolesMap proposalRoles;
// Check for correct size
if (check.empty())
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s): Size wrong - Not compatibles", GetDetailedMatchRoles(check).c_str());
return LFG_INCOMPATIBLES_WRONG_GROUP_SIZE;
}
// Check all-but-new compatiblitity
if (check.size() > 2)
{
ObjectGuid frontGuid = check.front();
check.pop_front();
// Check all-but-new compatibilities (New, A, B, C, D) --> check(A, B, C, D)
LfgCompatibility child_compatibles = CheckCompatibility(check);
if (child_compatibles < LFG_COMPATIBLES_WITH_LESS_PLAYERS) // Group not compatible
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) child %s not compatibles", strGuids.c_str(), GetDetailedMatchRoles(check).c_str());
SetCompatibles(strGuids, child_compatibles);
return child_compatibles;
}
check.push_front(frontGuid);
}
std::ostringstream o;
// Find compatible dungeons
if (check.size() > 1)
{
GuidList::iterator itguid = check.begin();
proposalDungeons = QueueDataStore[*itguid].dungeons;
for (++itguid; itguid != check.end(); ++itguid)
{
LfgDungeonSet temporal;
LfgDungeonSet& dungeons = QueueDataStore[*itguid].dungeons;
std::set_intersection(proposalDungeons.begin(), proposalDungeons.end(), dungeons.begin(), dungeons.end(), std::inserter(temporal, temporal.begin()));
proposalDungeons = temporal;
}
}
else
{
proposalDungeons = QueueDataStore[check.front()].dungeons;
}
LFGDungeonData const* dungeon = nullptr;
do
{
if (proposalDungeons.empty())
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "LFGQueue::CheckCompatibility: (%s) No compatible dungeons%s", strGuids.c_str(), o.str().c_str());
SetCompatibles(strGuids, LFG_INCOMPATIBLES_NO_DUNGEONS);
return LFG_INCOMPATIBLES_NO_DUNGEONS;
}
proposal.dungeonId = Trinity::Containers::SelectRandomContainerElement(proposalDungeons);
proposalDungeons.erase(proposal.dungeonId);
dungeon = sLFGMgr->GetLFGDungeon(proposal.dungeonId);
} while (!dungeon);
// Check for correct size
if (check.size() > dungeon->GetMaxGroupSize())
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "LFGQueue::CheckCompatibility: (%s): Size wrong - Not compatibles", strGuids.c_str());
return LFG_INCOMPATIBLES_WRONG_GROUP_SIZE;
}
// Check if more than one LFG group and number of players joining
uint8 numPlayers = 0;
uint8 numLfgGroups = 0;
for (GuidList::const_iterator it = check.begin(); it != check.end() && numLfgGroups < 1 && numPlayers <= dungeon->GetMaxGroupSize(); ++it)
{
ObjectGuid guid = *it;
LfgQueueDataContainer::iterator itQueue = QueueDataStore.find(guid);
if (itQueue == QueueDataStore.end())
{
TC_LOG_ERROR("lfg.queue.match.compatibility.check", "Guid: [%s] is not queued but listed as queued!", guid.ToString().c_str());
RemoveFromQueue(guid);
return LFG_COMPATIBILITY_PENDING;
}
// Store group so we don't need to call Mgr to get it later (if it's player group will be 0 otherwise would have joined as group)
for (LfgRolesMap::const_iterator it2 = itQueue->second.roles.begin(); it2 != itQueue->second.roles.end(); ++it2)
proposalGroups[it2->first] = itQueue->first.IsGroup() ? itQueue->first : ObjectGuid::Empty;
numPlayers += itQueue->second.roles.size();
if (sLFGMgr->IsLfgGroup(guid))
{
if (!numLfgGroups)
proposal.group = guid;
++numLfgGroups;
}
}
if (numLfgGroups > 1)
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) More than one Lfggroup (%u)", GetDetailedMatchRoles(check).c_str(), numLfgGroups);
SetCompatibles(strGuids, LFG_INCOMPATIBLES_MULTIPLE_LFG_GROUPS);
return LFG_INCOMPATIBLES_MULTIPLE_LFG_GROUPS;
}
if (numPlayers > dungeon->GetMaxGroupSize())
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) Too many players (%u)", GetDetailedMatchRoles(check).c_str(), numPlayers);
SetCompatibles(strGuids, LFG_INCOMPATIBLES_TOO_MUCH_PLAYERS);
return LFG_INCOMPATIBLES_TOO_MUCH_PLAYERS;
}
// If it's single group no need to check for duplicate players, ignores, bad roles or bad dungeons as it's been checked before joining
if (check.size() > 1)
{
for (GuidList::const_iterator it = check.begin(); it != check.end(); ++it)
{
LfgRolesMap const& roles = QueueDataStore[(*it)].roles;
for (LfgRolesMap::const_iterator itRoles = roles.begin(); itRoles != roles.end(); ++itRoles)
{
LfgRolesMap::const_iterator itPlayer;
for (itPlayer = proposalRoles.begin(); itPlayer != proposalRoles.end(); ++itPlayer)
{
if (itRoles->first == itPlayer->first)
TC_LOG_ERROR("lfg.queue.match.compatibility.check", "Guids: ERROR! Player multiple times in queue! [%s]", itRoles->first.ToString().c_str());
else if (sLFGMgr->HasIgnore(itRoles->first, itPlayer->first))
break;
}
if (itPlayer == proposalRoles.end())
proposalRoles[itRoles->first] = itRoles->second;
}
}
if (uint8 playersize = numPlayers - proposalRoles.size())
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) not compatible, %u players are ignoring each other", GetDetailedMatchRoles(check).c_str(), playersize);
SetCompatibles(strGuids, LFG_INCOMPATIBLES_HAS_IGNORES);
return LFG_INCOMPATIBLES_HAS_IGNORES;
}
LfgRolesMap debugRoles = proposalRoles;
if (!LFGMgr::CheckGroupRoles(proposalRoles, dungeon))
{
std::ostringstream o;
for (LfgRolesMap::const_iterator it = debugRoles.begin(); it != debugRoles.end(); ++it)
o << ", " << it->first.GetRawValue() << ": " << GetRolesString(it->second);
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) Roles not compatible%s", GetDetailedMatchRoles(check).c_str(), o.str().c_str());
SetCompatibles(strGuids, LFG_INCOMPATIBLES_NO_ROLES);
return LFG_INCOMPATIBLES_NO_ROLES;
}
}
else
{
proposalRoles = QueueDataStore[check.front()].roles;
LFGMgr::CheckGroupRoles(proposalRoles, dungeon); // assign new roles
}
// Enough players?
if (numPlayers != dungeon->GetMaxGroupSize() && !allowIncomplete)
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) Compatibles but not enough players(%u)", GetDetailedMatchRoles(check).c_str(), numPlayers);
LfgCompatibilityData data(LFG_COMPATIBLES_WITH_LESS_PLAYERS);
data.roles = proposalRoles;
for (GuidList::const_iterator itr = check.begin(); itr != check.end(); ++itr)
UpdateBestCompatibleInQueue(QueueDataStore.find(*itr), strGuids, data.roles);
SetCompatibilityData(strGuids, data);
return LFG_COMPATIBLES_WITH_LESS_PLAYERS;
}
std::vector<ObjectGuid> participants;
for(auto const& role:proposalRoles)participants.push_back(role.first);
if(!ClientlessBrowserPolicy::CanMatch(participants,dungeon->IsRaid()?ClientlessBrowserPolicy::Kind::Raid:ClientlessBrowserPolicy::Kind::Dungeon))
return LFG_COMPATIBILITY_PENDING; // Prune without consuming alternatives or caching mutable admission.
ObjectGuid gguid = check.front();
proposal.queues = check;
proposal.isNew = numLfgGroups != 1 || sLFGMgr->GetOldState(gguid) != LFG_STATE_DUNGEON;
if (!sLFGMgr->AllQueued(check))
{
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) Group MATCH but can't create proposal!", GetDetailedMatchRoles(check).c_str());
SetCompatibles(strGuids, LFG_COMPATIBLES_BAD_STATES);
return LFG_COMPATIBLES_BAD_STATES;
}
// Create a new proposal
proposal.cancelTime = GameTime::GetGameTime() + LFG_TIME_PROPOSAL;
proposal.state = LFG_PROPOSAL_INITIATING;
proposal.leader.Clear();
bool leader = false;
for (LfgRolesMap::const_iterator itRoles = proposalRoles.begin(); itRoles != proposalRoles.end(); ++itRoles)
{
// Assing new leader
if (itRoles->second & PLAYER_ROLE_LEADER)
{
if (!leader || !proposal.leader || urand(0, 1))
proposal.leader = itRoles->first;
leader = true;
}
else if (!leader && (!proposal.leader || urand(0, 1)))
proposal.leader = itRoles->first;
// Assing player data and roles
LfgProposalPlayer &data = proposal.players[itRoles->first];
data.role = itRoles->second;
data.group = proposalGroups.find(itRoles->first)->second;
if (!proposal.isNew && data.group && data.group == proposal.group) // Player from existing group, autoaccept
data.accept = LFG_ANSWER_AGREE;
}
// A browser group containing humans is always led by one of those humans.
std::vector<ObjectGuid> humans;
bool hasBot=false;
for(auto guid:participants)
if(ClientlessBrowserPolicy::Bot(ObjectAccessor::FindConnectedPlayer(guid)))hasBot=true;
else if(ClientlessBrowserPolicy::Human(ObjectAccessor::FindConnectedPlayer(guid)))humans.push_back(guid);
if(hasBot && !humans.empty())proposal.leader=humans[urand(0,uint32(humans.size()-1))];
// Mark proposal members as not queued (but not remove queue data)
for (GuidList::const_iterator itQueue = proposal.queues.begin(); itQueue != proposal.queues.end(); ++itQueue)
{
ObjectGuid guid = (*itQueue);
RemoveFromNewQueue(guid);
RemoveFromCurrentQueue(guid);
}
sLFGMgr->AddProposal(proposal);
TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) MATCH! Group formed", GetDetailedMatchRoles(check).c_str());
SetCompatibles(strGuids, LFG_COMPATIBLES_MATCH);
return LFG_COMPATIBLES_MATCH;
}
void LFGQueue::UpdateQueueTimers(uint8 queueId, time_t currTime, LfgQueueRoleContainer& rolesPerDungeonId)
{
TC_LOG_TRACE("lfg.queue.timers.update", "Updating queue timers...");
for (LfgQueueDataContainer::iterator itQueue = QueueDataStore.begin(); itQueue != QueueDataStore.end(); ++itQueue)
{
LfgQueueData& queueinfo = itQueue->second;
uint32 dungeonId = (*queueinfo.dungeons.begin());
uint32 queuedTime = uint32(currTime - queueinfo.joinTime);
uint8 role = PLAYER_ROLE_NONE;
int32 waitTime = -1;
int32 wtTank = waitTimesTankStore[dungeonId].time;
int32 wtHealer = waitTimesHealerStore[dungeonId].time;
int32 wtDps = waitTimesDpsStore[dungeonId].time;
int32 wtAvg = waitTimesAvgStore[dungeonId].time;
for (LfgRolesMap::const_iterator itPlayer = queueinfo.roles.begin(); itPlayer != queueinfo.roles.end(); ++itPlayer)
role |= itPlayer->second;
role &= ~PLAYER_ROLE_LEADER;
if (role & PLAYER_ROLE_TANK)
rolesPerDungeonId[dungeonId].currentTanks++;
if (role & PLAYER_ROLE_HEALER)
rolesPerDungeonId[dungeonId].currentHealers++;
if (role & PLAYER_ROLE_DAMAGE)
rolesPerDungeonId[dungeonId].currentDps++;
switch (role)
{
case PLAYER_ROLE_NONE: // Should not happen - just in case
waitTime = -1;
break;
case PLAYER_ROLE_TANK:
waitTime = wtTank;
break;
case PLAYER_ROLE_HEALER:
waitTime = wtHealer;
break;
case PLAYER_ROLE_DAMAGE:
waitTime = wtDps;
break;
default:
waitTime = wtAvg;
break;
}
if (queueinfo.bestCompatible.empty())
FindBestCompatibleInQueue(itQueue);
LfgQueueStatusData queueData(queueId, dungeonId, waitTime, wtAvg, wtTank, wtHealer, wtDps, queuedTime, queueinfo.tanks, queueinfo.healers, queueinfo.dps);
for (LfgRolesMap::const_iterator itPlayer = queueinfo.roles.begin(); itPlayer != queueinfo.roles.end(); ++itPlayer)
{
ObjectGuid pguid = itPlayer->first;
LFGMgr::SendLfgQueueStatus(pguid, queueData);
}
}
}
time_t LFGQueue::GetJoinTime(ObjectGuid guid) const
{
LfgQueueDataContainer::const_iterator itr = QueueDataStore.find(guid);
if (itr != QueueDataStore.end())
return itr->second.joinTime;
return 0;
}
std::string LFGQueue::DumpQueueInfo() const
{
uint32 players = 0;
uint32 groups = 0;
uint32 playersInGroup = 0;
for (uint8 i = 0; i < 2; ++i)
{
GuidList const& queue = i ? newToQueueStore : currentQueueStore;
for (GuidList::const_iterator it = queue.begin(); it != queue.end(); ++it)
{
ObjectGuid guid = *it;
if (guid.IsGroup())
{
groups++;
playersInGroup += sLFGMgr->GetPlayerCount(guid);
}
else
players++;
}
}
std::ostringstream o;
o << "Queued Players: " << players << " (in group: " << playersInGroup << ") Groups: " << groups << "\n";
return o.str();
}
std::string LFGQueue::DumpCompatibleInfo(bool full /* = false */) const
{
std::ostringstream o;
o << "Compatible Map size: " << CompatibleMapStore.size() << "\n";
if (full)
for (LfgCompatibleContainer::const_iterator itr = CompatibleMapStore.begin(); itr != CompatibleMapStore.end(); ++itr)
{
o << "(" << itr->first << "): " << GetCompatibleString(itr->second.compatibility);
if (!itr->second.roles.empty())
{
o << " (";
bool first = true;
for (const auto& role : itr->second.roles)
{
if (!first)
o << "|";
o << role.first.GetRawValue() << " " << GetRolesString(role.second & uint8(~PLAYER_ROLE_LEADER));
first = false;
}
o << ")";
}
o << "\n";
}
return o.str();
}
void LFGQueue::FindBestCompatibleInQueue(LfgQueueDataContainer::iterator itrQueue)
{
TC_LOG_DEBUG("lfg.queue.compatibles.find", "%s", itrQueue->first.ToString().c_str());
std::ostringstream o;
o << itrQueue->first.GetRawValue();
std::string sguid = o.str();
for (LfgCompatibleContainer::const_iterator itr = CompatibleMapStore.begin(); itr != CompatibleMapStore.end(); ++itr)
{
if (itr->second.compatibility == LFG_COMPATIBLES_WITH_LESS_PLAYERS && std::string::npos != itr->first.find(sguid))
{
UpdateBestCompatibleInQueue(itrQueue, itr->first, itr->second.roles);
}
}
}
void LFGQueue::UpdateBestCompatibleInQueue(LfgQueueDataContainer::iterator itrQueue, std::string const& key, LfgRolesMap const& roles)
{
LfgQueueData& queueData = itrQueue->second;
uint8 storedSize = queueData.bestCompatible.empty() ? 0 :
std::count(queueData.bestCompatible.begin(), queueData.bestCompatible.end(), '|') + 1;
uint8 size = std::count(key.begin(), key.end(), '|') + 1;
if (size <= storedSize)
return;
TC_LOG_DEBUG("lfg.queue.compatibles.update", "Changed (%s) to (%s) as best compatible group for %s",
queueData.bestCompatible.c_str(), key.c_str(), itrQueue->first.ToString().c_str());
queueData.bestCompatible = key;
queueData.InitializeGroupSetup();
for (LfgRolesMap::const_iterator it = roles.begin(); it != roles.end(); ++it)
{
uint8 role = it->second;
if (role & PLAYER_ROLE_TANK)
--queueData.tanks;
else if (role & PLAYER_ROLE_HEALER)
--queueData.healers;
else
--queueData.dps;
}
}
} // namespace lfg