feat(Core/LFG): Implement dungeon selection cooldown to prevent repeat assignme… (#24916)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Andrew
2026-02-27 23:55:45 -03:00
committed by GitHub
parent 22078a9de5
commit b14cdea7d2
11 changed files with 215 additions and 6 deletions

View File

@@ -0,0 +1,9 @@
-- Add lfg cooldown command to the command table
DELETE FROM `command` WHERE `name` IN ('lfg cooldown');
INSERT INTO `command` (`name`, `security`, `help`) VALUES
('lfg cooldown', 3, 'Syntax: .lfg cooldown\nClears all LFG dungeon cooldowns for all players.');
-- Add acore_string for cooldown cleared message (English, German, Spanish)
DELETE FROM `acore_string` WHERE `entry` = 11019;
INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES
(11019, 'LFG dungeon cooldowns cleared for all players.', NULL, NULL, 'LFG-Dungeon-Abklingzeiten für alle Spieler zurückgesetzt.', NULL, NULL, 'Tiempos de reutilización de mazmorras LFG eliminados para todos los jugadores.', 'Tiempos de reutilización de mazmorras LFG eliminados para todos los jugadores.', NULL);

View File

@@ -3404,6 +3404,16 @@ DungeonFinder.CastDeserter = 1
DungeonFinder.AllowCompleted = 1
#
# DungeonFinder.DungeonSelectionCooldown
#
# Description: Duration in minutes for the dungeon selection cooldown. Players who complete a
# random dungeon via LFG will not be assigned the same dungeon again for this
# duration. This reduces the chance of getting the same dungeon in a row.
# Default: 0 - (Disabled)
DungeonFinder.DungeonSelectionCooldown = 0
#
###################################################################################################

View File

@@ -165,6 +165,87 @@ namespace lfg
LOG_INFO("server.loading", " ");
}
void LFGMgr::AddDungeonCooldown(ObjectGuid guid, uint32 dungeonId)
{
if (!sWorld->getIntConfig(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN))
return;
DungeonCooldownStore[guid][dungeonId] = GameTime::Now();
}
void LFGMgr::CleanupDungeonCooldowns()
{
if (!sWorld->getIntConfig(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN))
return;
Seconds cooldownDuration = GetDungeonCooldownDuration();
for (auto itPlayer = DungeonCooldownStore.begin(); itPlayer != DungeonCooldownStore.end(); )
{
for (auto itDungeon = itPlayer->second.begin(); itDungeon != itPlayer->second.end(); )
{
if (GameTime::HasElapsed(itDungeon->second, cooldownDuration))
itDungeon = itPlayer->second.erase(itDungeon);
else
++itDungeon;
}
if (itPlayer->second.empty())
itPlayer = DungeonCooldownStore.erase(itPlayer);
else
++itPlayer;
}
}
void LFGMgr::ClearDungeonCooldowns()
{
DungeonCooldownStore.clear();
}
Seconds LFGMgr::GetDungeonCooldownDuration() const
{
return Seconds(sWorld->getIntConfig(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN) * MINUTE);
}
LfgDungeonSet LFGMgr::FilterCooldownDungeons(LfgDungeonSet const& dungeons, LfgRolesMap const& players)
{
if (!sWorld->getIntConfig(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN))
return dungeons;
Seconds cooldownDuration = GetDungeonCooldownDuration();
LfgDungeonSet filtered;
for (uint32 dungeonId : dungeons)
{
bool onCooldown = false;
for (auto const& playerPair : players)
{
auto itPlayer = DungeonCooldownStore.find(playerPair.first);
if (itPlayer != DungeonCooldownStore.end())
{
auto itDungeon = itPlayer->second.find(dungeonId);
if (itDungeon != itPlayer->second.end() && !GameTime::HasElapsed(itDungeon->second, cooldownDuration))
{
onCooldown = true;
break;
}
}
}
if (!onCooldown)
filtered.insert(dungeonId);
}
// If all dungeons are on cooldown, return original set to avoid blocking the queue
if (filtered.empty())
{
LOG_DEBUG("lfg", "LFGMgr::FilterCooldownDungeons: All {} dungeons on cooldown for group, bypassing cooldown filter", dungeons.size());
return dungeons;
}
return filtered;
}
LFGDungeonData const* LFGMgr::GetLFGDungeon(uint32 id)
{
LFGDungeonContainer::const_iterator itr = LfgDungeonStore.find(id);
@@ -329,6 +410,9 @@ namespace lfg
BootsStore.erase(itBoot);
}
}
// Cleanup expired dungeon cooldowns
CleanupDungeonCooldowns();
}
else if (task == 1)
{
@@ -2263,6 +2347,9 @@ namespace lfg
continue;
}
// Record dungeon cooldown for this player (the actual dungeon completed, not the random entry)
AddDungeonCooldown(guid, dungeonId);
Player* player = ObjectAccessor::FindPlayer(guid);
if (!player || player->FindMap() != currMap) // pussywizard: currMap - multithreading crash if on other map (map id check is not enough, binding system is not reliable)
{

View File

@@ -450,6 +450,10 @@ namespace lfg
void LoadRewards();
/// Loads dungeons from dbc and adds teleport coords
void LoadLFGDungeons(bool reload = false);
/// Filters out recently completed dungeons from the proposal set for the given players
LfgDungeonSet FilterCooldownDungeons(LfgDungeonSet const& dungeons, LfgRolesMap const& players);
/// Clears all dungeon cooldowns for all players
void ClearDungeonCooldowns();
// Multiple files
/// Check if given guid applied for random dungeon
@@ -636,6 +640,14 @@ namespace lfg
LfgPlayerDataContainer PlayersStore; ///< Player data
LfgGroupDataContainer GroupsStore; ///< Group data
bool m_Testing;
// Dungeon cooldown system - prevents same dungeon being assigned in a row
typedef std::unordered_map<uint32 /*dungeonId*/, TimePoint /*completionTime*/> LfgDungeonCooldownMap;
typedef std::unordered_map<ObjectGuid /*playerGuid*/, LfgDungeonCooldownMap> LfgDungeonCooldownContainer;
LfgDungeonCooldownContainer DungeonCooldownStore; ///< Stores dungeon cooldowns per player
void AddDungeonCooldown(ObjectGuid guid, uint32 dungeonId);
void CleanupDungeonCooldowns();
[[nodiscard]] Seconds GetDungeonCooldownDuration() const;
};
template <typename T, FMT_ENABLE_IF(std::is_enum_v<T>)>

View File

@@ -415,7 +415,10 @@ namespace lfg
proposal.cancelTime = GameTime::GetGameTime().count() + LFG_TIME_PROPOSAL;
proposal.state = LFG_PROPOSAL_INITIATING;
proposal.leader.Clear();
proposal.dungeonId = Acore::Containers::SelectRandomContainerElement(proposalDungeons);
// Filter out recently completed dungeons to prevent same dungeon in a row
LfgDungeonSet filteredDungeons = sLFGMgr->FilterCooldownDungeons(proposalDungeons, proposalRoles);
proposal.dungeonId = Acore::Containers::SelectRandomContainerElement(filteredDungeons);
uint32 completedEncounters = 0;
bool leader = false;

View File

@@ -1347,6 +1347,8 @@ enum AcoreStrings
LANG_BAN_IP_YOUBANNEDMESSAGE_WORLD = 11017,
LANG_BAN_IP_YOUPERMBANNEDMESSAGE_WORLD = 11018,
LANG_LFG_COOLDOWN_CLEARED = 11019,
LANG_MUTED_PLAYER = 30000, // Mute for player 2 hour
// Instant Flight

View File

@@ -41,6 +41,19 @@ namespace GameTime
/// Uptime
AC_GAME_API Seconds GetUptime();
/// Uptime since a given time point
inline Microseconds Elapsed(TimePoint start)
{
return std::chrono::duration_cast<Microseconds>(Now() - start);
}
/// Check if a duration has elapsed since a given time point
template<class T>
inline bool HasElapsed(TimePoint start, T duration)
{
return (Now() - start) >= duration;
}
/// Update all timers
void UpdateGameTimers();
}

View File

@@ -562,6 +562,7 @@ void WorldConfig::BuildConfigCache()
SetConfigValue<uint32>(CONFIG_LFG_OPTIONSMASK, "DungeonFinder.OptionsMask", 5);
SetConfigValue<bool>(CONFIG_LFG_CAST_DESERTER, "DungeonFinder.CastDeserter", true);
SetConfigValue<bool>(CONFIG_LFG_ALLOW_COMPLETED, "DungeonFinder.AllowCompleted", true);
SetConfigValue<uint32>(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN, "DungeonFinder.DungeonSelectionCooldown", 0);
// DBC_ItemAttributes
SetConfigValue<bool>(CONFIG_DBC_ENFORCE_ITEM_ATTRIBUTES, "DBC.EnforceItemAttributes", true);

View File

@@ -321,6 +321,7 @@ enum ServerConfigs
CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION,
CONFIG_PERSISTENT_CHARACTER_CLEAN_FLAGS,
CONFIG_LFG_OPTIONSMASK,
CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN,
CONFIG_MAX_INSTANCES_PER_HOUR,
CONFIG_WINTERGRASP_PLR_MAX,
CONFIG_WINTERGRASP_PLR_MIN,

View File

@@ -47,11 +47,12 @@ public:
{
static ChatCommandTable lfgCommandTable =
{
{ "player", HandleLfgPlayerInfoCommand, SEC_MODERATOR, Console::No },
{ "group", HandleLfgGroupInfoCommand, SEC_MODERATOR, Console::No },
{ "queue", HandleLfgQueueInfoCommand, SEC_MODERATOR, Console::Yes },
{ "clean", HandleLfgCleanCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "options", HandleLfgOptionsCommand, SEC_GAMEMASTER, Console::Yes },
{ "player", HandleLfgPlayerInfoCommand, SEC_MODERATOR, Console::No },
{ "group", HandleLfgGroupInfoCommand, SEC_MODERATOR, Console::No },
{ "queue", HandleLfgQueueInfoCommand, SEC_MODERATOR, Console::Yes },
{ "clean", HandleLfgCleanCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "options", HandleLfgOptionsCommand, SEC_GAMEMASTER, Console::Yes },
{ "cooldown", HandleLfgCooldownClearCommand, SEC_ADMINISTRATOR, Console::Yes },
};
static ChatCommandTable commandTable =
@@ -126,6 +127,13 @@ public:
sLFGMgr->Clean();
return true;
}
static bool HandleLfgCooldownClearCommand(ChatHandler* handler)
{
sLFGMgr->ClearDungeonCooldowns();
handler->SendSysMessage(LANG_LFG_COOLDOWN_CLEARED);
return true;
}
};
void AddSC_lfg_commandscript()

View File

@@ -0,0 +1,63 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "GameTime.h"
#include "gtest/gtest.h"
#include <thread>
TEST(GameTimeTest, Elapsed)
{
GameTime::UpdateGameTimers();
auto start = GameTime::Now();
std::this_thread::sleep_for(50ms);
GameTime::UpdateGameTimers();
auto elapsed = GameTime::Elapsed(start);
EXPECT_GE(elapsed, 50ms);
}
TEST(GameTimeTest, HasElapsedTrue)
{
GameTime::UpdateGameTimers();
auto start = GameTime::Now();
std::this_thread::sleep_for(50ms);
GameTime::UpdateGameTimers();
EXPECT_TRUE(GameTime::HasElapsed(start, 25ms));
}
TEST(GameTimeTest, HasElapsedFalse)
{
GameTime::UpdateGameTimers();
auto start = GameTime::Now();
EXPECT_FALSE(GameTime::HasElapsed(start, 10s));
}
TEST(GameTimeTest, HasElapsedWithSeconds)
{
GameTime::UpdateGameTimers();
auto start = GameTime::Now();
EXPECT_FALSE(GameTime::HasElapsed(start, 1s));
}
TEST(GameTimeTest, HasElapsedWithMicroseconds)
{
GameTime::UpdateGameTimers();
auto start = GameTime::Now();
std::this_thread::sleep_for(100us);
GameTime::UpdateGameTimers();
EXPECT_TRUE(GameTime::HasElapsed(start, Microseconds(1)));
}