mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-03-08 10:10:28 +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.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", " ");
|
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)
|
LFGDungeonData const* LFGMgr::GetLFGDungeon(uint32 id)
|
||||||
{
|
{
|
||||||
LFGDungeonContainer::const_iterator itr = LfgDungeonStore.find(id);
|
LFGDungeonContainer::const_iterator itr = LfgDungeonStore.find(id);
|
||||||
@@ -329,6 +410,9 @@ namespace lfg
|
|||||||
BootsStore.erase(itBoot);
|
BootsStore.erase(itBoot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup expired dungeon cooldowns
|
||||||
|
CleanupDungeonCooldowns();
|
||||||
}
|
}
|
||||||
else if (task == 1)
|
else if (task == 1)
|
||||||
{
|
{
|
||||||
@@ -2263,6 +2347,9 @@ namespace lfg
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record dungeon cooldown for this player (the actual dungeon completed, not the random entry)
|
||||||
|
AddDungeonCooldown(guid, dungeonId);
|
||||||
|
|
||||||
Player* player = ObjectAccessor::FindPlayer(guid);
|
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)
|
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();
|
void LoadRewards();
|
||||||
/// Loads dungeons from dbc and adds teleport coords
|
/// Loads dungeons from dbc and adds teleport coords
|
||||||
void LoadLFGDungeons(bool reload = false);
|
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
|
// Multiple files
|
||||||
/// Check if given guid applied for random dungeon
|
/// Check if given guid applied for random dungeon
|
||||||
@@ -636,6 +640,14 @@ namespace lfg
|
|||||||
LfgPlayerDataContainer PlayersStore; ///< Player data
|
LfgPlayerDataContainer PlayersStore; ///< Player data
|
||||||
LfgGroupDataContainer GroupsStore; ///< Group data
|
LfgGroupDataContainer GroupsStore; ///< Group data
|
||||||
bool m_Testing;
|
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>)>
|
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.cancelTime = GameTime::GetGameTime().count() + LFG_TIME_PROPOSAL;
|
||||||
proposal.state = LFG_PROPOSAL_INITIATING;
|
proposal.state = LFG_PROPOSAL_INITIATING;
|
||||||
proposal.leader.Clear();
|
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;
|
uint32 completedEncounters = 0;
|
||||||
bool leader = false;
|
bool leader = false;
|
||||||
|
|||||||
@@ -1347,6 +1347,8 @@ enum AcoreStrings
|
|||||||
LANG_BAN_IP_YOUBANNEDMESSAGE_WORLD = 11017,
|
LANG_BAN_IP_YOUBANNEDMESSAGE_WORLD = 11017,
|
||||||
LANG_BAN_IP_YOUPERMBANNEDMESSAGE_WORLD = 11018,
|
LANG_BAN_IP_YOUPERMBANNEDMESSAGE_WORLD = 11018,
|
||||||
|
|
||||||
|
LANG_LFG_COOLDOWN_CLEARED = 11019,
|
||||||
|
|
||||||
LANG_MUTED_PLAYER = 30000, // Mute for player 2 hour
|
LANG_MUTED_PLAYER = 30000, // Mute for player 2 hour
|
||||||
|
|
||||||
// Instant Flight
|
// Instant Flight
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ namespace GameTime
|
|||||||
/// Uptime
|
/// Uptime
|
||||||
AC_GAME_API Seconds GetUptime();
|
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
|
/// Update all timers
|
||||||
void UpdateGameTimers();
|
void UpdateGameTimers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ void WorldConfig::BuildConfigCache()
|
|||||||
SetConfigValue<uint32>(CONFIG_LFG_OPTIONSMASK, "DungeonFinder.OptionsMask", 5);
|
SetConfigValue<uint32>(CONFIG_LFG_OPTIONSMASK, "DungeonFinder.OptionsMask", 5);
|
||||||
SetConfigValue<bool>(CONFIG_LFG_CAST_DESERTER, "DungeonFinder.CastDeserter", true);
|
SetConfigValue<bool>(CONFIG_LFG_CAST_DESERTER, "DungeonFinder.CastDeserter", true);
|
||||||
SetConfigValue<bool>(CONFIG_LFG_ALLOW_COMPLETED, "DungeonFinder.AllowCompleted", true);
|
SetConfigValue<bool>(CONFIG_LFG_ALLOW_COMPLETED, "DungeonFinder.AllowCompleted", true);
|
||||||
|
SetConfigValue<uint32>(CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN, "DungeonFinder.DungeonSelectionCooldown", 0);
|
||||||
|
|
||||||
// DBC_ItemAttributes
|
// DBC_ItemAttributes
|
||||||
SetConfigValue<bool>(CONFIG_DBC_ENFORCE_ITEM_ATTRIBUTES, "DBC.EnforceItemAttributes", true);
|
SetConfigValue<bool>(CONFIG_DBC_ENFORCE_ITEM_ATTRIBUTES, "DBC.EnforceItemAttributes", true);
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ enum ServerConfigs
|
|||||||
CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION,
|
CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION,
|
||||||
CONFIG_PERSISTENT_CHARACTER_CLEAN_FLAGS,
|
CONFIG_PERSISTENT_CHARACTER_CLEAN_FLAGS,
|
||||||
CONFIG_LFG_OPTIONSMASK,
|
CONFIG_LFG_OPTIONSMASK,
|
||||||
|
CONFIG_LFG_DUNGEON_SELECTION_COOLDOWN,
|
||||||
CONFIG_MAX_INSTANCES_PER_HOUR,
|
CONFIG_MAX_INSTANCES_PER_HOUR,
|
||||||
CONFIG_WINTERGRASP_PLR_MAX,
|
CONFIG_WINTERGRASP_PLR_MAX,
|
||||||
CONFIG_WINTERGRASP_PLR_MIN,
|
CONFIG_WINTERGRASP_PLR_MIN,
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ public:
|
|||||||
{
|
{
|
||||||
static ChatCommandTable lfgCommandTable =
|
static ChatCommandTable lfgCommandTable =
|
||||||
{
|
{
|
||||||
{ "player", HandleLfgPlayerInfoCommand, SEC_MODERATOR, Console::No },
|
{ "player", HandleLfgPlayerInfoCommand, SEC_MODERATOR, Console::No },
|
||||||
{ "group", HandleLfgGroupInfoCommand, SEC_MODERATOR, Console::No },
|
{ "group", HandleLfgGroupInfoCommand, SEC_MODERATOR, Console::No },
|
||||||
{ "queue", HandleLfgQueueInfoCommand, SEC_MODERATOR, Console::Yes },
|
{ "queue", HandleLfgQueueInfoCommand, SEC_MODERATOR, Console::Yes },
|
||||||
{ "clean", HandleLfgCleanCommand, SEC_ADMINISTRATOR, Console::Yes },
|
{ "clean", HandleLfgCleanCommand, SEC_ADMINISTRATOR, Console::Yes },
|
||||||
{ "options", HandleLfgOptionsCommand, SEC_GAMEMASTER, Console::Yes },
|
{ "options", HandleLfgOptionsCommand, SEC_GAMEMASTER, Console::Yes },
|
||||||
|
{ "cooldown", HandleLfgCooldownClearCommand, SEC_ADMINISTRATOR, Console::Yes },
|
||||||
};
|
};
|
||||||
|
|
||||||
static ChatCommandTable commandTable =
|
static ChatCommandTable commandTable =
|
||||||
@@ -126,6 +127,13 @@ public:
|
|||||||
sLFGMgr->Clean();
|
sLFGMgr->Clean();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool HandleLfgCooldownClearCommand(ChatHandler* handler)
|
||||||
|
{
|
||||||
|
sLFGMgr->ClearDungeonCooldowns();
|
||||||
|
handler->SendSysMessage(LANG_LFG_COOLDOWN_CLEARED);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void AddSC_lfg_commandscript()
|
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