mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-03-08 02:00:29 +00:00
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:
@@ -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);
|
||||
@@ -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
|
||||
|
||||
#
|
||||
###################################################################################################
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>)>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
63
src/test/server/game/Time/GameTime.cpp
Normal file
63
src/test/server/game/Time/GameTime.cpp
Normal 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)));
|
||||
}
|
||||
Reference in New Issue
Block a user