/* * This file is part of the AzerothCore 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 . */ #include "LFGQueue.h" #include "Containers.h" #include "DBCStores.h" #include "GameTime.h" #include "Group.h" #include "InstanceScript.h" #include "LFGMgr.h" #include "Log.h" #include "ObjectMgr.h" #include "Player.h" #include "World.h" namespace lfg { LfgQueueData::LfgQueueData() : joinTime(time_t(GameTime::GetGameTime().count())), lastRefreshTime(joinTime), tanks(LFG_TANKS_NEEDED), healers(LFG_HEALERS_NEEDED), dps(LFG_DPS_NEEDED) { } void LFGQueue::AddToQueue(ObjectGuid guid, bool failedProposal) { LOG_DEBUG("lfg", "ADD AddToQueue: {}, failed proposal: {}", guid.ToString(), failedProposal ? 1 : 0); LfgQueueDataContainer::iterator itQueue = QueueDataStore.find(guid); if (itQueue == QueueDataStore.end()) { LOG_ERROR("lfg", "LFGQueue::AddToQueue: Queue data not found for [{}]", guid.ToString()); return; } LOG_DEBUG("lfg", "AddToQueue success: {}", guid.ToString()); AddToNewQueue(guid, failedProposal); } void LFGQueue::RemoveFromQueue(ObjectGuid guid, bool partial) { LOG_DEBUG("lfg", "REMOVE RemoveFromQueue: {}, partial: {}", guid.ToString(), partial ? 1 : 0); RemoveFromNewQueue(guid); RemoveFromCompatibles(guid); LfgQueueDataContainer::iterator itDelete = QueueDataStore.end(); for (LfgQueueDataContainer::iterator itr = QueueDataStore.begin(); itr != QueueDataStore.end(); ++itr) { if (itr->first != guid) { if (itr->second.bestCompatible.hasGuid(guid)) { LOG_DEBUG("lfg", "CLEAR bestCompatible: {}, because of: {}", itr->second.bestCompatible.toString(), guid.ToString()); itr->second.bestCompatible.clear(); } } else { LOG_DEBUG("lfg", "CLEAR bestCompatible SELF: {}, because of: {}", itr->second.bestCompatible.toString(), guid.ToString()); //itr->second.bestCompatible.clear(); // don't clear here, because UpdateQueueTimers will try to find with every diff update itDelete = itr; } } // xinef: partial if (!partial && itDelete != QueueDataStore.end()) { LOG_DEBUG("lfg", "ERASE QueueDataStore for: {}", guid.ToString()); LOG_DEBUG("lfg", "ERASE QueueDataStore for: {}, itDelete: {},{},{}", guid.ToString(), itDelete->second.dps, itDelete->second.healers, itDelete->second.tanks); QueueDataStore.erase(itDelete); LOG_DEBUG("lfg", "ERASE QueueDataStore for: {} SUCCESS", guid.ToString()); } } void LFGQueue::AddToNewQueue(ObjectGuid guid, bool front) { if (front) { LOG_DEBUG("lfg", "ADD AddToNewQueue at FRONT: {}", guid.ToString()); restoredAfterProposal.push_back(guid); newToQueueStore.push_front(guid); } else { LOG_DEBUG("lfg", "ADD AddToNewQueue at the END: {}", guid.ToString()); newToQueueStore.push_back(guid); } } void LFGQueue::RemoveFromNewQueue(ObjectGuid guid) { LOG_DEBUG("lfg", "REMOVE RemoveFromNewQueue: {}", guid.ToString()); newToQueueStore.remove(guid); restoredAfterProposal.remove(guid); } void LFGQueue::AddQueueData(ObjectGuid guid, time_t joinTime, LfgDungeonSet const& dungeons, LfgRolesMap const& rolesMap) { LOG_DEBUG("lfg", "JOINED AddQueueData: {}", guid.ToString()); QueueDataStore[guid] = LfgQueueData(joinTime, dungeons, rolesMap); AddToQueue(guid); } void LFGQueue::RemoveQueueData(ObjectGuid guid) { LOG_DEBUG("lfg", "LEFT RemoveQueueData: {}", guid.ToString()); 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); } void LFGQueue::RemoveFromCompatibles(ObjectGuid guid) { LOG_DEBUG("lfg", "COMPATIBLES REMOVE for: {}", guid.ToString()); for (LfgCompatibleContainer::iterator it = CompatibleList.begin(); it != CompatibleList.end(); ++it) if (it->hasGuid(guid)) { LOG_DEBUG("lfg", "Removed Compatible: {}, because of: {}", it->toString(), guid.ToString()); it->clear(); // set to 0, this will be removed while iterating in FindNewGroups } for (LfgCompatibleContainer::iterator itr = CompatibleTempList.begin(); itr != CompatibleTempList.end(); ) { LfgCompatibleContainer::iterator it = itr++; if (it->hasGuid(guid)) { LOG_DEBUG("lfg", "Erased Temp Compatible: {}, because of: {}", it->toString(), guid.ToString()); CompatibleTempList.erase(it); } } } void LFGQueue::AddToCompatibles(Lfg5Guids const& key) { LOG_DEBUG("lfg", "COMPATIBLES ADD: {}", key.toString()); CompatibleTempList.push_back(key); } uint8 LFGQueue::FindGroups() { LOG_DEBUG("lfg", "FIND GROUPS!"); uint8 newGroupsProcessed = 0; if (!newToQueueStore.empty()) { ++newGroupsProcessed; ObjectGuid newGuid = newToQueueStore.front(); bool pushCompatiblesToFront = (std::find(restoredAfterProposal.begin(), restoredAfterProposal.end(), newGuid) != restoredAfterProposal.end()); LOG_DEBUG("lfg", "newToQueueStore: {}, front: {}", newGuid.ToString(), pushCompatiblesToFront ? 1 : 0); RemoveFromNewQueue(newGuid); FindNewGroups(newGuid); CompatibleList.splice((pushCompatiblesToFront ? CompatibleList.begin() : CompatibleList.end()), CompatibleTempList); CompatibleTempList.clear(); return newGroupsProcessed; // pussywizard: only one per update, shouldn't be a problem } return newGroupsProcessed; } LfgCompatibility LFGQueue::FindNewGroups(const ObjectGuid& newGuid) { // each combination of dps+heal+tank (tank*8 + heal+4 + dps) has a value assigned 0..15 // first 16 bits of the mask are for marking if such combination was found once, second 16 bits for marking second occurence of that combination, etc uint64 foundMask = 0; uint32 foundCount = 0; LOG_DEBUG("lfg", "FIND NEW GROUPS for: {}", newGuid.ToString()); // we have to take into account that FindNewGroups is called every X minutes if number of compatibles is low! // build set of already present compatibles for this guid std::set currentCompatibles; for (Lfg5GuidsList::iterator it = CompatibleList.begin(); it != CompatibleList.end(); ++it) if (it->hasGuid(newGuid)) { // unset roles here so they are not copied, restore after insertion LfgRolesMap* r = it->roles; it->roles = nullptr; currentCompatibles.insert(*it); it->roles = r; } LfgCompatibility selfCompatibility = LFG_COMPATIBILITY_PENDING; if (currentCompatibles.empty()) { selfCompatibility = CheckCompatibility(Lfg5Guids(), newGuid, foundMask, foundCount, currentCompatibles); if (selfCompatibility != LFG_COMPATIBLES_WITH_LESS_PLAYERS) // group is already compatible (a party of 5 players) return selfCompatibility; } for (Lfg5GuidsList::iterator it = CompatibleList.begin(); it != CompatibleList.end(); ) { Lfg5GuidsList::iterator itr = it++; if (itr->empty()) { LOG_DEBUG("lfg", "ERASE from CompatibleList"); CompatibleList.erase(itr); continue; } LfgCompatibility compatibility = CheckCompatibility(*itr, newGuid, foundMask, foundCount, currentCompatibles); if (compatibility == LFG_COMPATIBLES_MATCH) return LFG_COMPATIBLES_MATCH; if ((foundMask & 0x3FFF3FFF3FFF3FFF) == 0x3FFF3FFF3FFF3FFF) // each combination of dps+heal+tank already found 4 times break; } return selfCompatibility; } LfgCompatibility LFGQueue::CheckCompatibility(Lfg5Guids const& checkWith, const ObjectGuid& newGuid, uint64& foundMask, uint32& foundCount, const std::set& currentCompatibles) { LOG_DEBUG("lfg", "CHECK CheckCompatibility: {}, new guid: {}", checkWith.toString(), newGuid.ToString()); Lfg5Guids check(checkWith, false); // here newGuid is at front Lfg5Guids strGuids(checkWith, false); // here guids are sorted check.force_insert_front(newGuid); strGuids.insert(newGuid); if (!currentCompatibles.empty() && currentCompatibles.find(strGuids) != currentCompatibles.end()) return LFG_INCOMPATIBLES_TOO_MUCH_PLAYERS; LfgProposal proposal; LfgDungeonSet proposalDungeons; LfgGroupsMap proposalGroups; LfgRolesMap proposalRoles; // Check if more than one LFG group and number of players joining uint8 numPlayers = 0; uint8 numLfgGroups = 0; ObjectGuid guid; uint64 addToFoundMask = 0; for (uint8 i = 0; i < 5 && !(guid = check.guids[i]).IsEmpty() && numLfgGroups < 2 && numPlayers <= MAXGROUPSIZE; ++i) { LfgQueueDataContainer::iterator itQueue = QueueDataStore.find(guid); if (itQueue == QueueDataStore.end()) { LOG_ERROR("lfg", "LFGQueue::CheckCompatibility: [{}] is not queued but listed as queued!", guid.ToString()); 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) return LFG_INCOMPATIBLES_MULTIPLE_LFG_GROUPS; // Group with less that MAXGROUPSIZE members always compatible if (!sLFGMgr->IsTesting() && check.size() == 1 && numPlayers < MAXGROUPSIZE) { LfgQueueDataContainer::iterator itQueue = QueueDataStore.find(check.front()); LfgRolesMap roles = itQueue->second.roles; uint8 roleCheckResult = LFGMgr::CheckGroupRoles(roles); strGuids.addRoles(roles); itQueue->second.bestCompatible.clear(); // this may be left after a failed proposal (not cleared, because UpdateQueueTimers would try to generate it with every update) //UpdateBestCompatibleInQueue(itQueue, strGuids); AddToCompatibles(strGuids); if (roleCheckResult && roleCheckResult <= 15) foundMask |= ( (((uint64)1) << (roleCheckResult - 1)) | (((uint64)1) << (16 + roleCheckResult - 1)) | (((uint64)1) << (32 + roleCheckResult - 1)) | (((uint64)1) << (48 + roleCheckResult - 1))); return LFG_COMPATIBLES_WITH_LESS_PLAYERS; } if (numPlayers > MAXGROUPSIZE) 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 (uint8 i = 0; i < 5 && check.guids[i]; ++i) { const LfgRolesMap& roles = QueueDataStore[check.guids[i]].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) { // pussywizard: LFG this means that this player was in two different LfgQueueData (in QueueDataStore), and at least one of them is a group guid, because we do checks so there aren't 2 same guids in current CHECK //LOG_ERROR("lfg", "LFGQueue::CheckCompatibility: ERROR! Player multiple times in queue! [{}]", itRoles->first.ToString()); break; } else if (sLFGMgr->HasIgnore(itRoles->first, itPlayer->first)) break; } if (itPlayer == proposalRoles.end()) proposalRoles[itRoles->first] = itRoles->second; else break; } } if (numPlayers != proposalRoles.size()) return LFG_INCOMPATIBLES_HAS_IGNORES; uint8 roleCheckResult = LFGMgr::CheckGroupRoles(proposalRoles); if (!roleCheckResult || roleCheckResult > 0xF) return LFG_INCOMPATIBLES_NO_ROLES; // now, every combination can occur only 4 times (explained in FindNewGroups) if (foundMask & (((uint64)1) << (roleCheckResult - 1))) { if (foundMask & (((uint64)1) << (16 + roleCheckResult - 1))) { if (foundMask & (((uint64)1) << (32 + roleCheckResult - 1))) { if (foundMask & (((uint64)1) << (48 + roleCheckResult - 1))) { if (foundCount >= 10) // but only after finding at least 10 compatibles (this helps when there are few groups) return LFG_INCOMPATIBLES_NO_ROLES; } else addToFoundMask |= (((uint64)1) << (48 + roleCheckResult - 1)); } else addToFoundMask |= (((uint64)1) << (32 + roleCheckResult - 1)); } else addToFoundMask |= (((uint64)1) << (16 + roleCheckResult - 1)); } else addToFoundMask |= (((uint64)1) << (roleCheckResult - 1)); proposalDungeons = QueueDataStore[check.front()].dungeons; for (uint8 i = 1; i < 5 && check.guids[i]; ++i) { LfgDungeonSet temporal; LfgDungeonSet& dungeons = QueueDataStore[check.guids[i]].dungeons; std::set_intersection(proposalDungeons.begin(), proposalDungeons.end(), dungeons.begin(), dungeons.end(), std::inserter(temporal, temporal.begin())); proposalDungeons = temporal; } if (proposalDungeons.empty()) return LFG_INCOMPATIBLES_NO_DUNGEONS; } else { ObjectGuid gguid = check.front(); const LfgQueueData& queue = QueueDataStore[gguid]; proposalDungeons = queue.dungeons; proposalRoles = queue.roles; LFGMgr::CheckGroupRoles(proposalRoles); // assing new roles } // Enough players? if (!sLFGMgr->IsTesting() && numPlayers != MAXGROUPSIZE) { strGuids.addRoles(proposalRoles); for (uint8 i = 0; i < 5 && check.guids[i]; ++i) { LfgQueueDataContainer::iterator itr = QueueDataStore.find(check.guids[i]); if (!itr->second.bestCompatible.empty()) // update if groups don't have it empty (for empty it will be generated in UpdateQueueTimers) UpdateBestCompatibleInQueue(itr, strGuids); } AddToCompatibles(strGuids); foundMask |= addToFoundMask; ++foundCount; return LFG_COMPATIBLES_WITH_LESS_PLAYERS; } proposal.queues = strGuids; proposal.isNew = numLfgGroups != 1; if (!sLFGMgr->AllQueued(check)) // can't create proposal return LFG_COMPATIBILITY_PENDING; // Create a new proposal proposal.cancelTime = GameTime::GetGameTime().count() + LFG_TIME_PROPOSAL; proposal.state = LFG_PROPOSAL_INITIATING; proposal.leader.Clear(); proposal.dungeonId = Acore::Containers::SelectRandomContainerElement(proposalDungeons); uint32 completedEncounters = 0; 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; if (!completedEncounters && !proposal.isNew) { if (LFGDungeonEntry const* dungeon = sLFGDungeonStore.LookupEntry(proposal.dungeonId)) { if (Player* player = ObjectAccessor::FindConnectedPlayer(itRoles->first)) { if (player->GetMapId() == static_cast(dungeon->MapID)) { if (InstanceScript* instance = player->GetInstanceScript()) { completedEncounters = instance->GetCompletedEncounterMask(); } } } } } } proposal.encounters = completedEncounters; for (uint8 i = 0; i < 5 && proposal.queues.guids[i]; ++i) RemoveFromQueue(proposal.queues.guids[i], true); sLFGMgr->AddProposal(proposal); return LFG_COMPATIBLES_MATCH; } void LFGQueue::UpdateQueueTimers(uint32 diff) { time_t currTime = GameTime::GetGameTime().count(); bool sendQueueStatus = false; if (m_QueueStatusTimer > LFG_QUEUEUPDATE_INTERVAL) { m_QueueStatusTimer = 0; sendQueueStatus = true; } else m_QueueStatusTimer += diff; LOG_DEBUG("lfg", "UPDATE UpdateQueueTimers"); for (Lfg5GuidsList::iterator it = CompatibleList.begin(); it != CompatibleList.end(); ) { Lfg5GuidsList::iterator itr = it++; if (itr->empty()) { LOG_DEBUG("lfg", "UpdateQueueTimers ERASE compatible"); CompatibleList.erase(itr); } } if (!sendQueueStatus) { for (LfgQueueDataContainer::iterator itQueue = QueueDataStore.begin(); itQueue != QueueDataStore.end(); ) { if (currTime - itQueue->second.joinTime > 2 * HOUR) { ObjectGuid guid = itQueue->first; QueueDataStore.erase(itQueue++); sLFGMgr->LeaveAllLfgQueues(guid, true); continue; } if (itQueue->second.bestCompatible.empty()) { uint32 numOfCompatibles = FindBestCompatibleInQueue(itQueue); if (numOfCompatibles /*must be positive, because proposals don't delete QueueQueueData*/ && currTime - itQueue->second.lastRefreshTime >= 60 && numOfCompatibles < (5 - itQueue->second.bestCompatible.roles->size()) * 25) { itQueue->second.lastRefreshTime = currTime; AddToQueue(itQueue->first, false); } } ++itQueue; } return; } // LOG_TRACE("lfg", "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; 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()) { LOG_DEBUG("lfg", "found empty bestCompatible"); FindBestCompatibleInQueue(itQueue); } LfgQueueStatusData queueData(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) { return QueueDataStore[guid].joinTime; } uint32 LFGQueue::FindBestCompatibleInQueue(LfgQueueDataContainer::iterator itrQueue) { uint32 numOfCompatibles = 0; for (LfgCompatibleContainer::const_iterator itr = CompatibleList.begin(); itr != CompatibleList.end(); ++itr) if (itr->hasGuid(itrQueue->first)) { ++numOfCompatibles; UpdateBestCompatibleInQueue(itrQueue, *itr); } return numOfCompatibles; } void LFGQueue::UpdateBestCompatibleInQueue(LfgQueueDataContainer::iterator itrQueue, Lfg5Guids const& key) { LOG_DEBUG("lfg", "UpdateBestCompatibleInQueue: {}", key.toString()); LfgQueueData& queueData = itrQueue->second; uint8 storedSize = queueData.bestCompatible.size(); uint8 size = key.size(); if (size <= storedSize) return; queueData.bestCompatible = key; queueData.tanks = LFG_TANKS_NEEDED; queueData.healers = LFG_HEALERS_NEEDED; queueData.dps = LFG_DPS_NEEDED; for (LfgRolesMap::const_iterator it = key.roles->begin(); it != key.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