diff --git a/data/sql/updates/pending_db_world/rev_lfg_cooldown_command.sql b/data/sql/updates/pending_db_world/rev_lfg_cooldown_command.sql
new file mode 100644
index 000000000..84a9f28db
--- /dev/null
+++ b/data/sql/updates/pending_db_world/rev_lfg_cooldown_command.sql
@@ -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);
diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist
index 3bb2c4b85..44da43222 100644
--- a/src/server/apps/worldserver/worldserver.conf.dist
+++ b/src/server/apps/worldserver/worldserver.conf.dist
@@ -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
+
#
###################################################################################################
diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp
index 61724fa25..9da1f9199 100644
--- a/src/server/game/DungeonFinding/LFGMgr.cpp
+++ b/src/server/game/DungeonFinding/LFGMgr.cpp
@@ -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)
{
diff --git a/src/server/game/DungeonFinding/LFGMgr.h b/src/server/game/DungeonFinding/LFGMgr.h
index 644504567..2164de059 100644
--- a/src/server/game/DungeonFinding/LFGMgr.h
+++ b/src/server/game/DungeonFinding/LFGMgr.h
@@ -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 LfgDungeonCooldownMap;
+ typedef std::unordered_map LfgDungeonCooldownContainer;
+ LfgDungeonCooldownContainer DungeonCooldownStore; ///< Stores dungeon cooldowns per player
+ void AddDungeonCooldown(ObjectGuid guid, uint32 dungeonId);
+ void CleanupDungeonCooldowns();
+ [[nodiscard]] Seconds GetDungeonCooldownDuration() const;
};
template )>
diff --git a/src/server/game/DungeonFinding/LFGQueue.cpp b/src/server/game/DungeonFinding/LFGQueue.cpp
index aae5df67a..a7890fc9f 100644
--- a/src/server/game/DungeonFinding/LFGQueue.cpp
+++ b/src/server/game/DungeonFinding/LFGQueue.cpp
@@ -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;
diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h
index 7d76a57cc..7d2b1b9bc 100644
--- a/src/server/game/Miscellaneous/Language.h
+++ b/src/server/game/Miscellaneous/Language.h
@@ -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
diff --git a/src/server/game/Time/GameTime.h b/src/server/game/Time/GameTime.h
index 40d96f6a5..37ffb7bfb 100644
--- a/src/server/game/Time/GameTime.h
+++ b/src/server/game/Time/GameTime.h
@@ -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(Now() - start);
+ }
+
+ /// Check if a duration has elapsed since a given time point
+ template
+ inline bool HasElapsed(TimePoint start, T duration)
+ {
+ return (Now() - start) >= duration;
+ }
+
/// Update all timers
void UpdateGameTimers();
}
diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp
index 62759608b..b47200082 100644
--- a/src/server/game/World/WorldConfig.cpp
+++ b/src/server/game/World/WorldConfig.cpp
@@ -562,6 +562,7 @@ void WorldConfig::BuildConfigCache()
SetConfigValue(CONFIG_LFG_OPTIONSMASK, "DungeonFinder.OptionsMask", 5);
SetConfigValue(CONFIG_LFG_CAST_DESERTER, "DungeonFinder.CastDeserter", true);
SetConfigValue(CONFIG_LFG_ALLOW_COMPLETED, "DungeonFinder.AllowCompleted", true);
+ SetConfigValue(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN, "DungeonFinder.DungeonSelectionCooldown", 0);
// DBC_ItemAttributes
SetConfigValue(CONFIG_DBC_ENFORCE_ITEM_ATTRIBUTES, "DBC.EnforceItemAttributes", true);
diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h
index 3cf19aa21..e6604565f 100644
--- a/src/server/game/World/WorldConfig.h
+++ b/src/server/game/World/WorldConfig.h
@@ -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,
diff --git a/src/server/scripts/Commands/cs_lfg.cpp b/src/server/scripts/Commands/cs_lfg.cpp
index bfdee4288..693ab1585 100644
--- a/src/server/scripts/Commands/cs_lfg.cpp
+++ b/src/server/scripts/Commands/cs_lfg.cpp
@@ -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()
diff --git a/src/test/server/game/Time/GameTime.cpp b/src/test/server/game/Time/GameTime.cpp
new file mode 100644
index 000000000..ed6428d00
--- /dev/null
+++ b/src/test/server/game/Time/GameTime.cpp
@@ -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 .
+ */
+
+#include "GameTime.h"
+#include "gtest/gtest.h"
+
+#include
+
+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)));
+}