diff --git a/data/sql/updates/pending_db_characters/active_arena_season.sql b/data/sql/updates/pending_db_characters/active_arena_season.sql
new file mode 100644
index 000000000..726662af3
--- /dev/null
+++ b/data/sql/updates/pending_db_characters/active_arena_season.sql
@@ -0,0 +1,10 @@
+DROP TABLE IF EXISTS `active_arena_season`;
+CREATE TABLE `active_arena_season` (
+ `season_id` TINYINT UNSIGNED NOT NULL,
+ `season_state` TINYINT UNSIGNED NOT NULL COMMENT 'Supported 2 states: 0 - disabled; 1 - in progress.'
+)
+CHARSET = utf8mb4
+COLLATE = utf8mb4_unicode_ci
+ENGINE = InnoDB;
+
+INSERT INTO `active_arena_season` (`season_id`, `season_state`) VALUES (8, 1);
diff --git a/data/sql/updates/pending_db_world/rev_1725301496947122000.sql b/data/sql/updates/pending_db_world/rev_1725301496947122000.sql
new file mode 100644
index 000000000..7a70fd82e
--- /dev/null
+++ b/data/sql/updates/pending_db_world/rev_1725301496947122000.sql
@@ -0,0 +1,95 @@
+DROP TABLE IF EXISTS `arena_season_reward_group`;
+CREATE TABLE `arena_season_reward_group` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `arena_season` TINYINT UNSIGNED NOT NULL,
+ `criteria_type` ENUM('pct', 'abs') NOT NULL DEFAULT 'pct' COMMENT 'Determines how rankings are evaluated: "pct" - percentage-based (e.g., top 20% of the ladder), "abs" - absolute position-based (e.g., top 10 players).',
+ `min_criteria` FLOAT NOT NULL,
+ `max_criteria` FLOAT NOT NULL,
+ `reward_mail_template_id` INT UNSIGNED,
+ `reward_mail_subject` VARCHAR(255),
+ `reward_mail_body` TEXT,
+ `gold_reward` INT UNSIGNED
+)
+CHARSET = utf8mb4
+COLLATE = utf8mb4_unicode_ci
+ENGINE = InnoDB;
+
+-- Season 8
+INSERT INTO `arena_season_reward_group` (`id`, `arena_season`, `criteria_type`, `min_criteria`, `max_criteria`, `reward_mail_template_id`, `reward_mail_subject`, `reward_mail_body`, `gold_reward`) VALUES
+(1, 8, 'abs', 1, 1, 0, '', '', 0),
+(2, 8, 'pct', 0, 0.5, 287, '', '', 0),
+(3, 8, 'pct', 0.5, 3, 0, '', '', 0),
+(4, 8, 'pct', 3, 10, 0, '', '', 0),
+(5, 8, 'pct', 10, 35, 0, '', '', 0),
+-- Season 7
+(6, 7, 'abs', 1, 1, 0, '', '', 0),
+(7, 7, 'pct', 0, 0.5, 286, '', '', 0),
+(8, 7, 'pct', 0.5, 3, 0, '', '', 0),
+(9, 7, 'pct', 3, 10, 0, '', '', 0),
+(10, 7, 'pct', 10, 35, 0, '', '', 0),
+-- Season 6
+(11, 6, 'abs', 1, 1, 0, '', '', 0),
+(12, 6, 'pct', 0, 0.5, 267, '', '', 0),
+(13, 6, 'pct', 0.5, 3, 0, '', '', 0),
+(14, 6, 'pct', 3, 10, 0, '', '', 0),
+(15, 6, 'pct', 10, 35, 0, '', '', 0),
+-- Season 5
+(16, 5, 'abs', 1, 1, 0, '', '', 0),
+(17, 5, 'pct', 0, 0.5, 266, '', '', 0),
+(18, 5, 'pct', 0.5, 3, 0, '', '', 0),
+(19, 5, 'pct', 3, 10, 0, '', '', 0),
+(20, 5, 'pct', 10, 35, 0, '', '', 0);
+
+DROP TABLE IF EXISTS `arena_season_reward`;
+CREATE TABLE `arena_season_reward` (
+ `group_id` INT NOT NULL COMMENT 'id from arena_season_reward_group table',
+ `type` ENUM('achievement', 'item') NOT NULL DEFAULT 'achievement',
+ `entry` INT UNSIGNED NOT NULL COMMENT 'For item type - item entry, for achievement - achevement id.',
+ PRIMARY KEY (`group_id`, `type`, `entry`)
+)
+CHARSET = utf8mb4
+COLLATE = utf8mb4_unicode_ci
+ENGINE = InnoDB;
+
+-- Season 8
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (1, 'achievement', 3336);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (2, 'item', 50435);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (2, 'achievement', 2091);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (3, 'achievement', 2092);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (4, 'achievement', 2093);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (5, 'achievement', 2090);
+-- Season 7
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (6, 'achievement', 3336);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (7, 'item', 47840);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (7, 'achievement', 2091);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (8, 'achievement', 2092);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (9, 'achievement', 2093);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (10, 'achievement', 2090);
+-- Season 6
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (11, 'achievement', 3336);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (12, 'item', 46171);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (12, 'achievement', 2091);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (13, 'achievement', 2092);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (14, 'achievement', 2093);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (15, 'achievement', 2090);
+-- Season 5
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (16, 'achievement', 3336);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (17, 'item', 46708);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (17, 'achievement', 2091);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (18, 'achievement', 2092);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (19, 'achievement', 2093);
+INSERT INTO `arena_season_reward` (`group_id`, `type`, `entry`) VALUES (20, 'achievement', 2090);
+
+DELETE FROM `command` WHERE `name` IN ('arena season start', 'arena season reward', 'arena season set state', 'arena season deleteteams');
+INSERT INTO `command` (`name`, `security`, `help`) VALUES ('arena season start', 3, 'Syntax: .arena season start $season_id\nStarts a new arena season, places the correct vendors, and sets the new season state to IN PROGRESS.');
+INSERT INTO `command` (`name`, `security`, `help`) VALUES ('arena season reward', 3, 'Syntax: .arena season reward $brackets\nBuilds a ladder by combining team brackets and provides rewards from the arena_season_reward table.\nExample usage:\n \n# Combine all brackets, build a ladder, and distribute rewards among them\n.arena season reward all\n \n# Build ladders separately for 2v2, 3v3, and 5v5 brackets so each bracket receives its own rewards\n.arena season reward 2\n.arena season reward 3\n.arena season reward 5\n \n# Combine 2v2 and 3v3 brackets and distribute rewards\n.arena season reward 2,3');
+INSERT INTO `command` (`name`, `security`, `help`) VALUES ('arena season deleteteams', 3, 'Syntax: .arena season deleteteams\nDeletes ALL arena teams.');
+INSERT INTO `command` (`name`, `security`, `help`) VALUES ('arena season set state', 3, 'Syntax: .arena season set state $state\nChanges the state for the current season.\nAvailable states:\n 0 - disabled. Players can\'t queue for the arena.\n 1 - in progress. Players can use arena-related functionality.');
+
+DELETE FROM `achievement_reward` WHERE `ID` IN (3336, 2091, 2092, 2093, 2090);
+INSERT INTO `achievement_reward` (`ID`, `TitleA`, `TitleH`, `ItemID`, `Sender`, `Subject`, `Body`, `MailTemplateID`) VALUES
+(3336, 157, 157, 0, 0, '', '', 0),
+(2091, 42, 42, 0, 0, '', '', 0),
+(2092, 43, 43, 0, 0, '', '', 0),
+(2093, 44, 44, 0, 0, '', '', 0),
+(2090, 45, 45, 0, 0, '', '', 0);
diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist
index df79e3bdc..9fd477413 100644
--- a/src/server/apps/worldserver/worldserver.conf.dist
+++ b/src/server/apps/worldserver/worldserver.conf.dist
@@ -3789,21 +3789,6 @@ Arena.QueueAnnouncer.PlayerOnly = 0
Arena.QueueAnnouncer.Detail = 3
-#
-# Arena.ArenaSeason.ID
-# Description: Current arena season id shown in clients.
-# Default: 8
-
-Arena.ArenaSeason.ID = 8
-
-#
-# Arena.ArenaSeason.InProgress
-# Description: State of current arena season.
-# Default: 1 - (Active)
-# 0 - (Finished)
-
-Arena.ArenaSeason.InProgress = 1
-
#
# Arena.ArenaStartRating
# Description: Start rating for new arena teams.
diff --git a/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonMgr.cpp b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonMgr.cpp
new file mode 100644
index 000000000..e466eb718
--- /dev/null
+++ b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonMgr.cpp
@@ -0,0 +1,224 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 "ArenaSeasonMgr.h"
+#include "ArenaTeamMgr.h"
+#include "ArenaSeasonRewardsDistributor.h"
+#include "BattlegroundMgr.h"
+#include "GameEventMgr.h"
+#include "MapMgr.h"
+#include "Player.h"
+
+ArenaSeasonMgr* ArenaSeasonMgr::instance()
+{
+ static ArenaSeasonMgr instance;
+ return &instance;
+}
+
+void ArenaSeasonMgr::LoadRewards()
+{
+ uint32 oldMSTime = getMSTime();
+
+ std::unordered_map stringToArenaSeasonRewardGroupCriteriaType = {
+ {"pct", ArenaSeasonRewardGroupCriteriaType::ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE},
+ {"abs", ArenaSeasonRewardGroupCriteriaType::ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE}
+ };
+
+ QueryResult result = WorldDatabase.Query("SELECT id, arena_season, criteria_type, min_criteria, max_criteria, reward_mail_template_id, reward_mail_subject, reward_mail_body, gold_reward FROM arena_season_reward_group");
+
+ if (!result)
+ {
+ LOG_WARN("server.loading", ">> Loaded 0 arena season rewards. DB table `arena_season_reward_group` is empty.");
+ LOG_INFO("server.loading", " ");
+ return;
+ }
+
+ std::unordered_map groupsMap;
+
+ do
+ {
+ Field* fields = result->Fetch();
+ uint32 id = fields[0].Get();
+
+ ArenaSeasonRewardGroup group;
+ group.season = fields[1].Get();
+ group.criteriaType = stringToArenaSeasonRewardGroupCriteriaType[fields[2].Get()];
+ group.minCriteria = fields[3].Get();
+ group.maxCriteria = fields[4].Get();
+ group.rewardMailTemplateID = fields[5].Get();
+ group.rewardMailSubject = fields[6].Get();
+ group.rewardMailBody = fields[7].Get();
+ group.goldReward = fields[8].Get();
+
+ groupsMap[id] = group;
+ } while (result->NextRow());
+
+ std::unordered_map stringToArenaSeasonRewardType = {
+ {"achievement", ArenaSeasonRewardType::ARENA_SEASON_REWARD_TYPE_ACHIEVEMENT},
+ {"item", ArenaSeasonRewardType::ARENA_SEASON_REWARD_TYPE_ITEM}
+ };
+
+ result = WorldDatabase.Query("SELECT group_id, type, entry FROM arena_season_reward");
+
+ if (!result)
+ {
+ LOG_WARN("server.loading", ">> Loaded 0 arena season rewards. DB table `arena_season_reward` is empty.");
+ LOG_INFO("server.loading", " ");
+ return;
+ }
+
+ do
+ {
+ Field* fields = result->Fetch();
+ uint32 groupId = fields[0].Get();
+
+ ArenaSeasonReward reward;
+ reward.type = stringToArenaSeasonRewardType[fields[1].Get()];
+ reward.entry = fields[2].Get();
+
+ auto itr = groupsMap.find(groupId);
+ ASSERT(itr != groupsMap.end(), "Unknown arena_season_reward_group ({}) in arena_season_reward", groupId);
+
+ (reward.type == ARENA_SEASON_REWARD_TYPE_ITEM) ?
+ groupsMap[groupId].itemRewards.push_back(reward) :
+ groupsMap[groupId].achievementRewards.push_back(reward);
+
+ } while (result->NextRow());
+
+ for (auto const& itr : groupsMap)
+ _arenaSeasonRewardGroupsStore[itr.second.season].push_back(itr.second);
+
+ LOG_INFO("server.loading", ">> Loaded {} arena season rewards in {} ms", (uint32)groupsMap.size(), GetMSTimeDiffToNow(oldMSTime));
+ LOG_INFO("server.loading", " ");
+}
+
+void ArenaSeasonMgr::LoadActiveSeason()
+{
+ QueryResult result = CharacterDatabase.Query("SELECT season_id, season_state FROM active_arena_season");
+ ASSERT(result, "active_arena_season can't be empty");
+
+ Field* fields = result->Fetch();
+ _currentSeason = fields[0].Get();
+ _currentSeasonState = static_cast(fields[1].Get());
+
+ uint16 eventID = GameEventForArenaSeason(_currentSeason);
+ sGameEventMgr->StartEvent(eventID, true);
+
+ LOG_INFO("server.loading", "Arena Season {} loaded...", _currentSeason);
+ LOG_INFO("server.loading", " ");
+}
+
+void ArenaSeasonMgr::RewardTeamsForTheSeason(std::shared_ptr teamsFilter)
+{
+ ArenaSeasonTeamRewarderImpl rewarder = ArenaSeasonTeamRewarderImpl();
+ ArenaSeasonRewardDistributor distributor = ArenaSeasonRewardDistributor(&rewarder);
+ std::vector rewards = _arenaSeasonRewardGroupsStore[GetCurrentSeason()];
+ ArenaTeamMgr::ArenaTeamContainer filteredTeams = teamsFilter->Filter(sArenaTeamMgr->GetArenaTeams());
+ distributor.DistributeRewards(filteredTeams, rewards);
+}
+
+bool ArenaSeasonMgr::CanDeleteArenaTeams()
+{
+ std::vector rewards = _arenaSeasonRewardGroupsStore[GetCurrentSeason()];
+ if (rewards.empty())
+ return false;
+
+ for (auto const& bg : sBattlegroundMgr->GetActiveBattlegrounds())
+ if (bg->isRated())
+ return false;
+
+ return true;
+}
+
+void ArenaSeasonMgr::DeleteArenaTeams()
+{
+ if (!CanDeleteArenaTeams())
+ return;
+
+ // Cleanup queue first.
+ std::vector arenasQueueTypes = {BATTLEGROUND_QUEUE_2v2, BATTLEGROUND_QUEUE_3v3, BATTLEGROUND_QUEUE_5v5};
+ for (BattlegroundQueueTypeId queueType : arenasQueueTypes)
+ {
+ auto queue = sBattlegroundMgr->GetBattlegroundQueue(queueType);
+ for (auto const& [playerGUID, other] : queue.m_QueuedPlayers)
+ queue.RemovePlayer(playerGUID, true);
+ }
+
+ sArenaTeamMgr->DeleteAllArenaTeams();
+}
+
+void ArenaSeasonMgr::ChangeCurrentSeason(uint8 season)
+{
+ if (_currentSeason == season)
+ return;
+
+ uint16 currentEventID = GameEventForArenaSeason(_currentSeason);
+ sGameEventMgr->StopEvent(currentEventID, true);
+
+ uint16 newEventID = GameEventForArenaSeason(season);
+ sGameEventMgr->StartEvent(newEventID, true);
+
+ _currentSeason = season;
+ _currentSeasonState = ARENA_SEASON_STATE_IN_PROGRESS;
+
+ CharacterDatabase.Execute("UPDATE active_arena_season SET season_id = {}, season_state = {}", _currentSeason, _currentSeasonState);
+
+ BroadcastUpdatedWorldState();
+}
+
+void ArenaSeasonMgr::SetSeasonState(ArenaSeasonState state)
+{
+ if (_currentSeasonState == state)
+ return;
+
+ _currentSeasonState = state;
+
+ CharacterDatabase.Execute("UPDATE active_arena_season SET season_state = {}", _currentSeasonState);
+
+ BroadcastUpdatedWorldState();
+}
+
+uint16 ArenaSeasonMgr::GameEventForArenaSeason(uint8 season)
+{
+ QueryResult result = WorldDatabase.Query("SELECT eventEntry FROM game_event_arena_seasons WHERE season = '{}'", season);
+
+ if (!result)
+ {
+ LOG_ERROR("arenaseasonmgr", "ArenaSeason ({}) must be an existant Arena Season", season);
+ return 0;
+ }
+
+ Field* fields = result->Fetch();
+ return fields[0].Get();
+}
+
+void ArenaSeasonMgr::BroadcastUpdatedWorldState()
+{
+ sMapMgr->DoForAllMaps([](Map* map)
+ {
+ // Ignore instanceable maps, players will get a fresh state once they change the map.
+ if (map->Instanceable())
+ return;
+
+ map->DoForAllPlayers([&](Player* player)
+ {
+ uint32 currZone, currArea;
+ player->GetZoneAndAreaId(currZone, currArea);
+ player->SendInitWorldStates(currZone, currArea);
+ });
+ });
+}
diff --git a/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonMgr.h b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonMgr.h
new file mode 100644
index 000000000..7a1ee97c7
--- /dev/null
+++ b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonMgr.h
@@ -0,0 +1,126 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 .
+ */
+
+#ifndef _ARENASEASONMGR_H
+#define _ARENASEASONMGR_H
+
+#include "Common.h"
+#include "ArenaTeamFilter.h"
+#include
+#include
+
+enum ArenaSeasonState
+{
+ ARENA_SEASON_STATE_DISABLED = 0,
+ ARENA_SEASON_STATE_IN_PROGRESS = 1
+};
+
+enum ArenaSeasonRewardType
+{
+ ARENA_SEASON_REWARD_TYPE_ITEM,
+ ARENA_SEASON_REWARD_TYPE_ACHIEVEMENT
+};
+
+enum ArenaSeasonRewardGroupCriteriaType
+{
+ ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE,
+ ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE
+};
+
+// ArenaSeasonReward represents one reward, it can be an item or achievement.
+struct ArenaSeasonReward
+{
+ ArenaSeasonReward() = default;
+
+ // Item or acheivement entry.
+ uint32 entry{};
+
+ ArenaSeasonRewardType type{ARENA_SEASON_REWARD_TYPE_ITEM};
+
+ // Used in unit tests.
+ bool operator==(const ArenaSeasonReward& other) const
+ {
+ return entry == other.entry && type == other.type;
+ }
+};
+
+struct ArenaSeasonRewardGroup
+{
+ ArenaSeasonRewardGroup() = default;
+
+ uint8 season{};
+
+ ArenaSeasonRewardGroupCriteriaType criteriaType;
+
+ float minCriteria{};
+ float maxCriteria{};
+
+ uint32 rewardMailTemplateID{};
+ std::string rewardMailSubject{};
+ std::string rewardMailBody{};
+ uint32 goldReward{};
+
+ std::vector itemRewards;
+ std::vector achievementRewards;
+
+ // Used in unit tests.
+ bool operator==(const ArenaSeasonRewardGroup& other) const
+ {
+ return minCriteria == other.minCriteria &&
+ maxCriteria == other.maxCriteria &&
+ criteriaType == other.criteriaType &&
+ itemRewards == other.itemRewards &&
+ achievementRewards == other.achievementRewards;
+ }
+};
+
+class ArenaSeasonMgr
+{
+public:
+ static ArenaSeasonMgr* instance();
+
+ using ArenaSeasonRewardGroupsBySeasonContainer = std::unordered_map>;
+
+ // Loading functions
+ void LoadRewards();
+ void LoadActiveSeason();
+
+ // Season management functions
+ void ChangeCurrentSeason(uint8 season);
+ uint8 GetCurrentSeason() { return _currentSeason; }
+
+ void SetSeasonState(ArenaSeasonState state);
+ ArenaSeasonState GetSeasonState() { return _currentSeasonState; }
+
+ // Season completion functions
+ void RewardTeamsForTheSeason(std::shared_ptr teamsFilter);
+ bool CanDeleteArenaTeams();
+ void DeleteArenaTeams();
+
+private:
+ uint16 GameEventForArenaSeason(uint8 season);
+ void BroadcastUpdatedWorldState();
+
+ ArenaSeasonRewardGroupsBySeasonContainer _arenaSeasonRewardGroupsStore;
+
+ uint8 _currentSeason{};
+ ArenaSeasonState _currentSeasonState{};
+};
+
+#define sArenaSeasonMgr ArenaSeasonMgr::instance()
+
+#endif // _ARENASEASONMGR_H
diff --git a/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardsDistributor.cpp b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardsDistributor.cpp
new file mode 100644
index 000000000..dff7961a2
--- /dev/null
+++ b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardsDistributor.cpp
@@ -0,0 +1,166 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 "ArenaSeasonRewardsDistributor.h"
+#include "AchievementMgr.h"
+#include "CharacterDatabase.h"
+#include "Mail.h"
+#include "Player.h"
+#include
+
+constexpr float minPctTeamGamesForMemberToGetReward = 30;
+
+void ArenaSeasonTeamRewarderImpl::RewardTeamWithRewardGroup(ArenaTeam *arenaTeam, const ArenaSeasonRewardGroup &rewardGroup)
+{
+ RewardWithMail(arenaTeam, rewardGroup);
+ RewardWithAchievements(arenaTeam, rewardGroup);
+}
+
+void ArenaSeasonTeamRewarderImpl::RewardWithMail(ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const & rewardGroup)
+{
+ if (rewardGroup.itemRewards.empty() && rewardGroup.goldReward == 0)
+ return;
+
+ const uint32 npcKingDondSender = 18897;
+
+ CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
+ for (auto const& member : arenaTeam->GetMembers())
+ {
+ uint32 teamSeasonGames = arenaTeam->GetStats().SeasonGames;
+ // Avoid division by zero.
+ if (teamSeasonGames == 0)
+ continue;
+
+ float memberParticipationPercentage = (static_cast(member.SeasonGames) / teamSeasonGames) * 100;
+ if (memberParticipationPercentage < minPctTeamGamesForMemberToGetReward)
+ continue;
+
+ Player* player = ObjectAccessor::FindPlayer(member.Guid);
+
+ auto draft = rewardGroup.rewardMailTemplateID > 0 ?
+ MailDraft(rewardGroup.rewardMailTemplateID, false) :
+ MailDraft(rewardGroup.rewardMailSubject, rewardGroup.rewardMailBody);
+
+ if (rewardGroup.goldReward > 0)
+ draft.AddMoney(rewardGroup.goldReward);
+
+ for (auto const& reward : rewardGroup.itemRewards)
+ if (Item* item = Item::CreateItem(reward.entry, 1))
+ {
+ item->SaveToDB(trans);
+ draft.AddItem(item);
+ }
+
+ draft.SendMailTo(trans, MailReceiver(player, member.Guid.GetCounter()), MailSender(npcKingDondSender));
+ }
+ CharacterDatabase.CommitTransaction(trans);
+}
+
+void ArenaSeasonTeamRewarderImpl::RewardWithAchievements(ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const & rewardGroup)
+{
+ if (rewardGroup.achievementRewards.empty())
+ return;
+
+ for (auto const& member : arenaTeam->GetMembers())
+ {
+ uint32 teamSeasonGames = arenaTeam->GetStats().SeasonGames;
+ // Avoid division by zero.
+ if (teamSeasonGames == 0)
+ continue;
+
+ float memberParticipationPercentage = (static_cast(member.SeasonGames) / teamSeasonGames) * 100;
+ if (memberParticipationPercentage < minPctTeamGamesForMemberToGetReward)
+ continue;
+
+ Player* player = ObjectAccessor::FindPlayer(member.Guid);
+ for (auto const& reward : rewardGroup.achievementRewards)
+ {
+ AchievementEntry const* achievement = sAchievementStore.LookupEntry(reward.entry);
+ if (!achievement)
+ continue;
+
+ if (player)
+ player->CompletedAchievement(achievement);
+ else
+ sAchievementMgr->CompletedAchievementForOfflinePlayer(member.Guid.GetCounter(), achievement);
+ }
+ }
+}
+
+ArenaSeasonRewardDistributor::ArenaSeasonRewardDistributor(ArenaSeasonTeamRewarder* rewarder)
+ : _rewarder(rewarder)
+{
+}
+
+void ArenaSeasonRewardDistributor::DistributeRewards(ArenaTeamMgr::ArenaTeamContainer &arenaTeams, std::vector &rewardGroups)
+{
+ std::vector sortedTeams;
+ sortedTeams.reserve(arenaTeams.size());
+
+ static constexpr uint16 minRequiredGames = 30;
+
+ for (auto const& [id, team] : arenaTeams)
+ if (team->GetStats().SeasonGames >= minRequiredGames)
+ sortedTeams.push_back(team);
+
+ std::sort(sortedTeams.begin(), sortedTeams.end(), [](ArenaTeam* a, ArenaTeam* b) {
+ return a->GetRating() > b->GetRating();
+ });
+
+ std::vector pctRewardGroup;
+ std::vector absRewardGroup;
+
+ for (auto const& reward : rewardGroups)
+ {
+ if (reward.criteriaType == ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE)
+ pctRewardGroup.push_back(reward);
+ else
+ absRewardGroup.push_back(reward);
+ }
+
+ size_t totalTeams = sortedTeams.size();
+ for (auto const& rewardGroup : pctRewardGroup)
+ {
+ size_t minIndex = static_cast(rewardGroup.minCriteria * totalTeams / 100);
+ size_t maxIndex = static_cast(rewardGroup.maxCriteria * totalTeams / 100);
+
+ minIndex = std::min(minIndex, totalTeams);
+ maxIndex = std::min(maxIndex, totalTeams);
+
+ for (size_t i = minIndex; i < maxIndex; ++i)
+ {
+ ArenaTeam* team = sortedTeams[i];
+ _rewarder->RewardTeamWithRewardGroup(team, rewardGroup);
+ }
+ }
+
+ for (auto const& rewardGroup : absRewardGroup)
+ {
+ size_t minIndex = rewardGroup.minCriteria-1; // Top 1 team is the team with index 0, so we need make -1.
+ size_t maxIndex = rewardGroup.maxCriteria;
+
+ minIndex = std::max(minIndex, size_t(0));
+ minIndex = std::min(minIndex, totalTeams);
+ maxIndex = std::min(maxIndex, totalTeams);
+
+ for (size_t i = minIndex; i < maxIndex; ++i)
+ {
+ ArenaTeam* team = sortedTeams[i];
+ _rewarder->RewardTeamWithRewardGroup(team, rewardGroup);
+ }
+ }
+}
diff --git a/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardsDistributor.h b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardsDistributor.h
new file mode 100644
index 000000000..144fae5ff
--- /dev/null
+++ b/src/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardsDistributor.h
@@ -0,0 +1,54 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 .
+ */
+
+#ifndef _ARENASEASONREWARDDISTRIBUTOR_H
+#define _ARENASEASONREWARDDISTRIBUTOR_H
+
+#include "ArenaSeasonMgr.h"
+#include "ArenaTeam.h"
+#include "ArenaTeamMgr.h"
+
+class ArenaSeasonTeamRewarder
+{
+public:
+ virtual ~ArenaSeasonTeamRewarder() = default;
+
+ virtual void RewardTeamWithRewardGroup(ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const & rewardGroup) = 0;
+};
+
+class ArenaSeasonTeamRewarderImpl: public ArenaSeasonTeamRewarder
+{
+public:
+ void RewardTeamWithRewardGroup(ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const & rewardGroup) override;
+
+private:
+ void RewardWithMail(ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const & rewardGroup);
+ void RewardWithAchievements(ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const & rewardGroup);
+};
+
+class ArenaSeasonRewardDistributor
+{
+public:
+ ArenaSeasonRewardDistributor(ArenaSeasonTeamRewarder* rewarder);
+
+ void DistributeRewards(ArenaTeamMgr::ArenaTeamContainer& arenaTeams, std::vector& rewardGroups);
+
+private:
+ ArenaSeasonTeamRewarder* _rewarder;
+};
+
+#endif // _ARENASEASONREWARDDISTRIBUTOR_H
diff --git a/src/server/game/Battlegrounds/ArenaSeason/ArenaTeamFilter.h b/src/server/game/Battlegrounds/ArenaSeason/ArenaTeamFilter.h
new file mode 100644
index 000000000..2ac66344f
--- /dev/null
+++ b/src/server/game/Battlegrounds/ArenaSeason/ArenaTeamFilter.h
@@ -0,0 +1,113 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 .
+ */
+
+#ifndef _ARENATEAMFILTER_H
+#define _ARENATEAMFILTER_H
+
+#include "Common.h"
+#include "ArenaTeamMgr.h"
+#include "ArenaTeam.h"
+#include "Tokenize.h"
+#include "StringConvert.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+class ArenaTeamFilter
+{
+public:
+ virtual ~ArenaTeamFilter() = default;
+
+ virtual ArenaTeamMgr::ArenaTeamContainer Filter(ArenaTeamMgr::ArenaTeamContainer teams) = 0;
+};
+
+class ArenaTeamFilterByTypes : public ArenaTeamFilter
+{
+public:
+ ArenaTeamFilterByTypes(std::vector validTypes) : _validTypes(validTypes) {}
+
+ ArenaTeamMgr::ArenaTeamContainer Filter(ArenaTeamMgr::ArenaTeamContainer teams) override
+ {
+ ArenaTeamMgr::ArenaTeamContainer result;
+
+ for (auto const& pair : teams)
+ {
+ ArenaTeam* team = pair.second;
+ for (uint8 arenaType : _validTypes)
+ {
+ if (team->GetType() == arenaType)
+ {
+ result[pair.first] = team;
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+private:
+ std::vector _validTypes;
+};
+
+class ArenaTeamFilterAllTeams : public ArenaTeamFilter
+{
+public:
+ ArenaTeamMgr::ArenaTeamContainer Filter(ArenaTeamMgr::ArenaTeamContainer teams) override
+ {
+ return teams;
+ }
+};
+
+class ArenaTeamFilterFactoryByUserInput
+{
+public:
+ std::unique_ptr CreateFilterByUserInput(std::string userInput)
+ {
+ std::transform(userInput.begin(), userInput.end(), userInput.begin(),
+ [](unsigned char c) { return std::tolower(c); });
+
+ if (userInput == "all")
+ return std::make_unique();
+
+ // Parse the input string (e.g., "2,3") into valid types
+ std::vector validTypes = ParseTypes(userInput);
+
+ if (!validTypes.empty())
+ return std::make_unique(validTypes);
+
+ return nullptr;
+ }
+
+private:
+ std::vector ParseTypes(std::string_view userInput)
+ {
+ std::vector validTypes;
+ auto tokens = Acore::Tokenize(userInput, ',', false);
+
+ for (auto const& token : tokens)
+ if (auto typeOpt = Acore::StringTo(token))
+ validTypes.push_back(*typeOpt);
+
+ return validTypes;
+ }
+};
+
+#endif // _ARENATEAMFILTER_H
diff --git a/src/server/game/Battlegrounds/ArenaTeam.cpp b/src/server/game/Battlegrounds/ArenaTeam.cpp
index 552b229ad..b93822810 100644
--- a/src/server/game/Battlegrounds/ArenaTeam.cpp
+++ b/src/server/game/Battlegrounds/ArenaTeam.cpp
@@ -17,6 +17,7 @@
#include "ArenaTeam.h"
#include "ArenaTeamMgr.h"
+#include "ArenaSeasonMgr.h"
#include "BattlegroundMgr.h"
#include "CharacterCache.h"
#include "Group.h"
@@ -658,7 +659,7 @@ uint32 ArenaTeam::GetPoints(uint32 memberRating)
if (rating <= 1500)
{
- if (sWorld->getIntConfig(CONFIG_ARENA_SEASON_ID) < 6 && !sWorld->getIntConfig(CONFIG_LEGACY_ARENA_POINTS_CALC))
+ if (sArenaSeasonMgr->GetCurrentSeason() < 6 && !sWorld->getIntConfig(CONFIG_LEGACY_ARENA_POINTS_CALC))
points = (float)rating * 0.22f + 14.0f;
else
points = 344;
diff --git a/src/server/game/Battlegrounds/ArenaTeamMgr.cpp b/src/server/game/Battlegrounds/ArenaTeamMgr.cpp
index e36b874ac..8ba0e3cd4 100644
--- a/src/server/game/Battlegrounds/ArenaTeamMgr.cpp
+++ b/src/server/game/Battlegrounds/ArenaTeamMgr.cpp
@@ -125,6 +125,27 @@ void ArenaTeamMgr::RemoveArenaTeam(uint32 arenaTeamId)
ArenaTeamStore.erase(arenaTeamId);
}
+void ArenaTeamMgr::DeleteAllArenaTeams()
+{
+ for (auto const& [id, team] : ArenaTeamStore)
+ {
+ while (team->GetMembersSize() > 0)
+ team->DelMember(team->GetMembers().front().Guid, false);
+
+ delete team;
+ }
+
+ ArenaTeamStore.clear();
+
+ CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
+ trans->Append("DELETE FROM arena_team_member");
+ trans->Append("DELETE FROM arena_team");
+ trans->Append("DELETE FROM character_arena_stats");
+ CharacterDatabase.CommitTransaction(trans);
+
+ NextArenaTeamId = 1;
+}
+
uint32 ArenaTeamMgr::GenerateArenaTeamId()
{
if (NextArenaTeamId >= MAX_ARENA_TEAM_ID)
diff --git a/src/server/game/Battlegrounds/ArenaTeamMgr.h b/src/server/game/Battlegrounds/ArenaTeamMgr.h
index 06ee6bc01..6ca67b5f3 100644
--- a/src/server/game/Battlegrounds/ArenaTeamMgr.h
+++ b/src/server/game/Battlegrounds/ArenaTeamMgr.h
@@ -43,6 +43,8 @@ public:
void AddArenaTeam(ArenaTeam* arenaTeam);
void RemoveArenaTeam(uint32 Id);
+ void DeleteAllArenaTeams();
+
ArenaTeamContainer::iterator GetArenaTeamMapBegin() { return ArenaTeamStore.begin(); }
ArenaTeamContainer::iterator GetArenaTeamMapEnd() { return ArenaTeamStore.end(); }
ArenaTeamContainer& GetArenaTeams() { return ArenaTeamStore; }
diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.cpp b/src/server/game/Battlegrounds/BattlegroundMgr.cpp
index 8e91dd0d4..46bac265d 100644
--- a/src/server/game/Battlegrounds/BattlegroundMgr.cpp
+++ b/src/server/game/Battlegrounds/BattlegroundMgr.cpp
@@ -334,6 +334,18 @@ Battleground* BattlegroundMgr::GetBattlegroundTemplate(BattlegroundTypeId bgType
return bgs.empty() ? nullptr : bgs.begin()->second;
}
+std::vector BattlegroundMgr::GetActiveBattlegrounds()
+{
+ std::vector result;
+
+ for (auto const& [bgType, bgData] : bgDataStore)
+ for (auto const& [id, bg] : bgData._Battlegrounds)
+ if (bg->GetStatus() == STATUS_WAIT_JOIN || bg->GetStatus() == STATUS_IN_PROGRESS)
+ result.push_back(static_cast(bg));
+
+ return result;
+}
+
uint32 BattlegroundMgr::CreateClientVisibleInstanceId(BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id)
{
if (IsArenaType(bgTypeId))
diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.h b/src/server/game/Battlegrounds/BattlegroundMgr.h
index 3b07a9fc7..36c8a30c3 100644
--- a/src/server/game/Battlegrounds/BattlegroundMgr.h
+++ b/src/server/game/Battlegrounds/BattlegroundMgr.h
@@ -82,6 +82,7 @@ public:
Battleground* GetBattleground(uint32 instanceID, BattlegroundTypeId bgTypeId);
Battleground* GetBattlegroundTemplate(BattlegroundTypeId bgTypeId);
Battleground* CreateNewBattleground(BattlegroundTypeId bgTypeId, PvPDifficultyEntry const* bracketEntry, uint8 arenaType, bool isRated);
+ std::vector GetActiveBattlegrounds();
void AddBattleground(Battleground* bg);
void RemoveBattleground(BattlegroundTypeId bgTypeId, uint32 instanceId);
diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index fac531f4b..5a51b10a4 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -21,6 +21,7 @@
#include "ArenaSpectator.h"
#include "ArenaTeam.h"
#include "ArenaTeamMgr.h"
+#include "ArenaSeasonMgr.h"
#include "Battlefield.h"
#include "BattlefieldMgr.h"
#include "BattlefieldWG.h"
@@ -8243,9 +8244,9 @@ void Player::SendInitWorldStates(uint32 zoneid, uint32 areaid)
data << uint32(0x8d4) << uint32(0x0); // 5
data << uint32(0x8d3) << uint32(0x0); // 6
// 7 1 - Arena season in progress, 0 - end of season
- data << uint32(0xC77) << uint32(sWorld->getBoolConfig(CONFIG_ARENA_SEASON_IN_PROGRESS));
+ data << uint32(0xC77) << uint32(sArenaSeasonMgr->GetSeasonState() == ARENA_SEASON_STATE_IN_PROGRESS);
// 8 Arena season id
- data << uint32(0xF3D) << uint32(sWorld->getIntConfig(CONFIG_ARENA_SEASON_ID));
+ data << uint32(0xF3D) << uint32(sArenaSeasonMgr->GetCurrentSeason());
if (mapid == 530) // Outland
{
diff --git a/src/server/game/Events/GameEventMgr.cpp b/src/server/game/Events/GameEventMgr.cpp
index ce529ff24..b380b8399 100644
--- a/src/server/game/Events/GameEventMgr.cpp
+++ b/src/server/game/Events/GameEventMgr.cpp
@@ -1163,34 +1163,6 @@ uint32 GameEventMgr::StartSystem() // return the next
return delay;
}
-void GameEventMgr::StartArenaSeason()
-{
- uint8 season = sWorld->getIntConfig(CONFIG_ARENA_SEASON_ID);
-
- WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_GAME_EVENT_ARENA_SEASON);
- stmt->SetData(0, season);
- PreparedQueryResult result = WorldDatabase.Query(stmt);
-
- if (!result)
- {
- LOG_ERROR("gameevent", "ArenaSeason ({}) must be an existant Arena Season", season);
- return;
- }
-
- Field* fields = result->Fetch();
- uint16 eventId = fields[0].Get();
-
- if (eventId >= _gameEvent.size())
- {
- LOG_ERROR("gameevent", "EventEntry {} for ArenaSeason ({}) does not exists", eventId, season);
- return;
- }
-
- StartEvent(eventId, true);
- LOG_INFO("server.loading", "Arena Season {} started...", season);
- LOG_INFO("server.loading", " ");
-}
-
uint32 GameEventMgr::Update() // return the next event delay in ms
{
time_t currenttime = GameTime::GetGameTime().count();
diff --git a/src/server/game/Events/GameEventMgr.h b/src/server/game/Events/GameEventMgr.h
index 390849269..9cc0adcb6 100644
--- a/src/server/game/Events/GameEventMgr.h
+++ b/src/server/game/Events/GameEventMgr.h
@@ -115,11 +115,10 @@ public:
bool IsActiveEvent(uint16 eventId) { return (_activeEvents.find(eventId) != _activeEvents.end()); }
uint32 StartSystem();
void Initialize();
- void StartArenaSeason();
- void StartInternalEvent(uint16 eventId);
- bool StartEvent(uint16 eventId, bool overwrite = false);
- void StopEvent(uint16 eventId, bool overwrite = false);
- void HandleQuestComplete(uint32 questId); // called on world event type quest completions
+ void StartInternalEvent(uint16 event_id);
+ bool StartEvent(uint16 event_id, bool overwrite = false);
+ void StopEvent(uint16 event_id, bool overwrite = false);
+ void HandleQuestComplete(uint32 quest_id); // called on world event type quest completions
uint32 GetNPCFlag(Creature* cr);
// Load the game event npc vendor table from the DB
void LoadEventVendors();
diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp
index c2aafae81..0735dd2d1 100644
--- a/src/server/game/Handlers/BattleGroundHandler.cpp
+++ b/src/server/game/Handlers/BattleGroundHandler.cpp
@@ -17,6 +17,7 @@
#include "ArenaTeam.h"
#include "ArenaTeamMgr.h"
+#include "ArenaSeasonMgr.h"
#include "Battleground.h"
#include "BattlegroundMgr.h"
#include "Chat.h"
@@ -812,7 +813,7 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData)
if (isRated)
{
// pussywizard: for rated matches check if season is in progress!
- if (!sWorld->getBoolConfig(CONFIG_ARENA_SEASON_IN_PROGRESS))
+ if (sArenaSeasonMgr->GetSeasonState() == ARENA_SEASON_STATE_DISABLED)
return;
ateamId = _player->GetArenaTeamId(arenaslot);
diff --git a/src/server/game/World/IWorld.h b/src/server/game/World/IWorld.h
index 8e6d35335..9cfcfd0b1 100644
--- a/src/server/game/World/IWorld.h
+++ b/src/server/game/World/IWorld.h
@@ -116,7 +116,6 @@ enum WorldBoolConfigs
CONFIG_BATTLEGROUND_TRACK_DESERTERS,
CONFIG_BG_XP_FOR_KILL,
CONFIG_ARENA_AUTO_DISTRIBUTE_POINTS,
- CONFIG_ARENA_SEASON_IN_PROGRESS,
CONFIG_ARENA_QUEUE_ANNOUNCER_ENABLE,
CONFIG_ARENA_QUEUE_ANNOUNCER_PLAYERONLY,
CONFIG_OFFHAND_CHECK_AT_SPELL_UNLEARN,
@@ -325,7 +324,6 @@ enum WorldIntConfigs
CONFIG_ARENA_PREV_OPPONENTS_DISCARD_TIMER,
CONFIG_ARENA_AUTO_DISTRIBUTE_INTERVAL_DAYS,
CONFIG_ARENA_GAMES_REQUIRED,
- CONFIG_ARENA_SEASON_ID,
CONFIG_ARENA_START_RATING,
CONFIG_LEGACY_ARENA_POINTS_CALC,
CONFIG_ARENA_START_PERSONAL_RATING,
diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp
index e891444b9..d3df6afa0 100644
--- a/src/server/game/World/World.cpp
+++ b/src/server/game/World/World.cpp
@@ -24,6 +24,7 @@
#include "AchievementMgr.h"
#include "AddonMgr.h"
#include "ArenaTeamMgr.h"
+#include "ArenaSeasonMgr.h"
#include "AuctionHouseMgr.h"
#include "AutobroadcastMgr.h"
#include "BattlefieldMgr.h"
@@ -1178,12 +1179,10 @@ void World::LoadConfigSettings(bool reload)
_bool_configs[CONFIG_ARENA_AUTO_DISTRIBUTE_POINTS] = sConfigMgr->GetOption("Arena.AutoDistributePoints", false);
_int_configs[CONFIG_ARENA_AUTO_DISTRIBUTE_INTERVAL_DAYS] = sConfigMgr->GetOption("Arena.AutoDistributeInterval", 7); // pussywizard: spoiled by implementing constant day and hour, always 7 now
_int_configs[CONFIG_ARENA_GAMES_REQUIRED] = sConfigMgr->GetOption("Arena.GamesRequired", 10);
- _int_configs[CONFIG_ARENA_SEASON_ID] = sConfigMgr->GetOption("Arena.ArenaSeason.ID", 8);
_int_configs[CONFIG_ARENA_START_RATING] = sConfigMgr->GetOption("Arena.ArenaStartRating", 0);
_int_configs[CONFIG_LEGACY_ARENA_POINTS_CALC] = sConfigMgr->GetOption("Arena.LegacyArenaPoints", 0);
_int_configs[CONFIG_ARENA_START_PERSONAL_RATING] = sConfigMgr->GetOption("Arena.ArenaStartPersonalRating", 0);
_int_configs[CONFIG_ARENA_START_MATCHMAKER_RATING] = sConfigMgr->GetOption("Arena.ArenaStartMatchmakerRating", 1500);
- _bool_configs[CONFIG_ARENA_SEASON_IN_PROGRESS] = sConfigMgr->GetOption("Arena.ArenaSeason.InProgress", true);
_float_configs[CONFIG_ARENA_WIN_RATING_MODIFIER_1] = sConfigMgr->GetOption("Arena.ArenaWinRatingModifier1", 48.0f);
_float_configs[CONFIG_ARENA_WIN_RATING_MODIFIER_2] = sConfigMgr->GetOption("Arena.ArenaWinRatingModifier2", 24.0f);
_float_configs[CONFIG_ARENA_LOSE_RATING_MODIFIER] = sConfigMgr->GetOption("Arena.ArenaLoseRatingModifier", 24.0f);
@@ -2123,9 +2122,10 @@ void World::SetInitialWorldSettings()
LOG_INFO("server.loading", "Initializing Opcodes...");
opcodeTable.Initialize();
- LOG_INFO("server.loading", "Starting Arena Season...");
- LOG_INFO("server.loading", " ");
- sGameEventMgr->StartArenaSeason();
+ LOG_INFO("server.loading", "Loading Arena Season Rewards...");
+ sArenaSeasonMgr->LoadRewards();
+ LOG_INFO("server.loading", "Loading Active Arena Season...");
+ sArenaSeasonMgr->LoadActiveSeason();
LOG_INFO("server.loading", "Loading WorldState...");
sWorldState->Load();
diff --git a/src/server/scripts/Commands/cs_arena.cpp b/src/server/scripts/Commands/cs_arena.cpp
index b556238e2..3e9882bab 100644
--- a/src/server/scripts/Commands/cs_arena.cpp
+++ b/src/server/scripts/Commands/cs_arena.cpp
@@ -23,6 +23,8 @@ Category: commandscripts
EndScriptData */
#include "ArenaTeamMgr.h"
+#include "ArenaSeasonMgr.h"
+#include "ArenaTeamFilter.h"
#include "Chat.h"
#include "CommandScript.h"
#include "Player.h"
@@ -36,6 +38,19 @@ public:
ChatCommandTable GetCommands() const override
{
+ static ChatCommandTable arenaSeasonSetCommandTable =
+ {
+ { "state", HandleArenaSeasonSetStateCommand, SEC_ADMINISTRATOR, Console::Yes }
+ };
+
+ static ChatCommandTable arenaSeasonCommandTable =
+ {
+ { "reward", HandleArenaSeasonRewardCommand, SEC_ADMINISTRATOR, Console::Yes },
+ { "deleteteams", HandleArenaSeasonDeleteTeamsCommand, SEC_ADMINISTRATOR, Console::Yes },
+ { "start", HandleArenaSeasonStartCommand, SEC_ADMINISTRATOR, Console::Yes },
+ { "set", arenaSeasonSetCommandTable }
+ };
+
static ChatCommandTable arenaCommandTable =
{
{ "create", HandleArenaCreateCommand, SEC_ADMINISTRATOR, Console::Yes },
@@ -44,6 +59,7 @@ public:
{ "captain", HandleArenaCaptainCommand, SEC_ADMINISTRATOR, Console::No },
{ "info", HandleArenaInfoCommand, SEC_GAMEMASTER, Console::Yes },
{ "lookup", HandleArenaLookupCommand, SEC_GAMEMASTER, Console::No },
+ { "season", arenaSeasonCommandTable }
};
static ChatCommandTable commandTable =
@@ -229,6 +245,80 @@ public:
return true;
}
+
+ static bool HandleArenaSeasonRewardCommand(ChatHandler* handler, std::string teamsFilterStr)
+ {
+ std::unique_ptr uniqueFilter = ArenaTeamFilterFactoryByUserInput().CreateFilterByUserInput(teamsFilterStr);
+ if (!uniqueFilter)
+ {
+ handler->PSendSysMessage("Invalid filter. Please check your input.");
+ return false;
+ }
+
+ std::shared_ptr sharedFilter = std::move(uniqueFilter);
+
+ if (!sArenaSeasonMgr->CanDeleteArenaTeams())
+ {
+ handler->PSendSysMessage("Cannot proceed. Make sure there are no active arenas and that rewards exist for the current season.");
+ handler->PSendSysMessage("Hint: You can disable the arena queue using the following command: .arena season set state 0");
+ return false;
+ }
+
+ handler->PSendSysMessage("Distributing rewards for arena teams (types: "+teamsFilterStr+")...");
+ sArenaSeasonMgr->RewardTeamsForTheSeason(sharedFilter);
+ handler->PSendSysMessage("Rewards distributed.");
+ return true;
+ }
+
+ static bool HandleArenaSeasonDeleteTeamsCommand(ChatHandler* handler)
+ {
+ handler->PSendSysMessage("Deleting arena teams...");
+ sArenaSeasonMgr->DeleteArenaTeams();
+ handler->PSendSysMessage("Arena teams deleted.");
+ return true;
+ }
+
+ static bool HandleArenaSeasonStartCommand(ChatHandler* handler, uint8 seasonId)
+ {
+ if (seasonId == sArenaSeasonMgr->GetCurrentSeason())
+ {
+ sArenaSeasonMgr->SetSeasonState(ARENA_SEASON_STATE_IN_PROGRESS);
+ handler->PSendSysMessage("Arena season updated.");
+ return true;
+ }
+
+ const uint8 maxSeasonId = 8;
+ if (seasonId > maxSeasonId)
+ {
+ handler->PSendSysMessage("Invalid season id.");
+ return false;
+ }
+
+ sArenaSeasonMgr->ChangeCurrentSeason(seasonId);
+ handler->PSendSysMessage("Arena season changed to season {}.", seasonId);
+ return true;
+ }
+
+ static bool HandleArenaSeasonSetStateCommand(ChatHandler* handler, uint8 state)
+ {
+ ArenaSeasonState seasonState;
+ switch (state) {
+ case ARENA_SEASON_STATE_DISABLED:
+ seasonState = ARENA_SEASON_STATE_DISABLED;
+ break;
+ case ARENA_SEASON_STATE_IN_PROGRESS:
+ seasonState = ARENA_SEASON_STATE_IN_PROGRESS;
+ break;
+ default:
+ handler->PSendSysMessage("Invalid state.");
+ return false;
+ }
+
+ sArenaSeasonMgr->SetSeasonState(seasonState);
+ handler->PSendSysMessage("Arena season updated.");
+ return true;
+ }
+
};
void AddSC_arena_commandscript()
diff --git a/src/test/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardDistributorTest.cpp b/src/test/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardDistributorTest.cpp
new file mode 100644
index 000000000..300cfc4e0
--- /dev/null
+++ b/src/test/server/game/Battlegrounds/ArenaSeason/ArenaSeasonRewardDistributorTest.cpp
@@ -0,0 +1,207 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 "Define.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "ArenaSeasonRewardsDistributor.h"
+
+class MockArenaSeasonTeamRewarder : public ArenaSeasonTeamRewarder
+{
+public:
+ MOCK_METHOD(void, RewardTeamWithRewardGroup, (ArenaTeam* arenaTeam, ArenaSeasonRewardGroup const& rewardGroup), (override));
+};
+
+class ArenaSeasonRewardDistributorTest : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ _mockRewarder = std::make_unique();
+ _distributor = std::make_unique(_mockRewarder.get());
+ }
+
+ std::unique_ptr _mockRewarder;
+ std::unique_ptr _distributor;
+};
+
+ArenaTeam ArenaTeamWithRating(int rating, int gamesPlayed)
+{
+ ArenaTeamStats stats;
+ stats.Rating = rating;
+ stats.SeasonGames = gamesPlayed;
+ ArenaTeam team;
+ team.SetArenaTeamStats(stats);
+ return team;
+}
+
+// This test verifies that a single team receives the correct reward group when multiple percent reward groups are defined.
+TEST_F(ArenaSeasonRewardDistributorTest, SingleTeamMultiplePctRewardDistribution)
+{
+ ArenaTeamMgr::ArenaTeamContainer arenaTeams;
+ std::vector rewardGroups;
+
+ ArenaTeam team = ArenaTeamWithRating(1500, 50);
+ arenaTeams[1] = &team;
+
+ ArenaSeasonRewardGroup rewardGroup;
+ rewardGroup.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup.minCriteria = 0;
+ rewardGroup.maxCriteria = 0.5;
+ rewardGroups.push_back(rewardGroup);
+ ArenaSeasonRewardGroup rewardGroup2;
+ rewardGroup2.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup2.minCriteria = 0.5;
+ rewardGroup2.maxCriteria = 100;
+ rewardGroups.push_back(rewardGroup2);
+
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team, rewardGroup2)).Times(1);
+
+ _distributor->DistributeRewards(arenaTeams, rewardGroups);
+}
+
+// This test verifies that a single team receives the correct reward group when multiple abs percent reward groups are defined.
+TEST_F(ArenaSeasonRewardDistributorTest, SingleTeamMultipleAbsRewardDistribution)
+{
+ ArenaTeamMgr::ArenaTeamContainer arenaTeams;
+ std::vector rewardGroups;
+
+ ArenaTeam team = ArenaTeamWithRating(1500, 50);
+ arenaTeams[1] = &team;
+
+ ArenaSeasonRewardGroup rewardGroup;
+ rewardGroup.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE;
+ rewardGroup.minCriteria = 1;
+ rewardGroup.maxCriteria = 1;
+ rewardGroups.push_back(rewardGroup);
+ ArenaSeasonRewardGroup rewardGroup2;
+ rewardGroup2.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE;
+ rewardGroup2.minCriteria = 2;
+ rewardGroup2.maxCriteria = 10;
+ rewardGroups.push_back(rewardGroup2);
+
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team, rewardGroup)).Times(1);
+
+ _distributor->DistributeRewards(arenaTeams, rewardGroups);
+}
+
+// Input: 1000 teams with incremental ratings and two reward groups with 0% - 0.5% and 0.5% - 3% percentage criteria.
+// Purpose: Ensures that the top 0.5% of teams receive the first reward and the next 3% receive the second reward.
+// Each team should be rewarded only once.
+TEST_F(ArenaSeasonRewardDistributorTest, ManyTeamsTwoRewardsDistribution)
+{
+ ArenaTeamMgr::ArenaTeamContainer arenaTeams;
+ std::vector rewardGroups;
+
+ const int numTeams = 1000;
+ ArenaTeam teams[numTeams + 1]; // used just to prevent teams deletion
+ for (int i = 1; i <= numTeams; i++)
+ {
+ teams[i] = ArenaTeamWithRating(i, 50);
+ arenaTeams[i] = &teams[i];
+ }
+
+ ArenaSeasonRewardGroup rewardGroup1;
+ rewardGroup1.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup1.minCriteria = 0.0; // 0%
+ rewardGroup1.maxCriteria = 0.5; // 0.5% of total teams
+ rewardGroups.push_back(rewardGroup1);
+
+ ArenaSeasonRewardGroup rewardGroup2;
+ rewardGroup2.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup2.minCriteria = 0.5; // 0.5% (the top 0.5% of the teams)
+ rewardGroup2.maxCriteria = 3.0; // 3% of total teams
+ rewardGroups.push_back(rewardGroup2);
+
+ ArenaSeasonRewardGroup rewardGroup3;
+ rewardGroup3.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup3.minCriteria = 3;
+ rewardGroup3.maxCriteria = 10;
+ rewardGroups.push_back(rewardGroup3);
+
+ ArenaSeasonRewardGroup rewardGroup4;
+ rewardGroup4.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup4.minCriteria = 10;
+ rewardGroup4.maxCriteria = 35;
+ rewardGroups.push_back(rewardGroup4);
+
+ // Top 1
+ ArenaSeasonRewardGroup rewardGroup5;
+ rewardGroup5.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_ABSOLUTE_VALUE;
+ rewardGroup5.minCriteria = 1;
+ rewardGroup5.maxCriteria = 1;
+ rewardGroups.push_back(rewardGroup5);
+
+ // Calculate expected reward distributions
+ int expectedTeamsInGroup1 = static_cast(0.005 * numTeams); // 0.5% of 1000 = 5
+ int expectedTeamsInGroup2 = static_cast(0.03 * numTeams); // 3% of 1000 = 30
+ int expectedTeamsInGroup3 = static_cast(0.10 * numTeams); // 10% of 1000 = 100
+ int expectedTeamsInGroup4 = static_cast(0.35 * numTeams); // 35% of 1000 = 350
+
+ int teamsIndexCounter = numTeams;
+
+ // Expectation for rewardGroup1 (top 0.5% of teams)
+ for (; teamsIndexCounter > numTeams - expectedTeamsInGroup1; --teamsIndexCounter)
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup1)).Times(1);
+
+ // Expectation for rewardGroup2 (next 3% - 0.5% teams)
+ for (; teamsIndexCounter > numTeams - expectedTeamsInGroup2; --teamsIndexCounter)
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup2)).Times(1);
+
+ for (; teamsIndexCounter > numTeams - expectedTeamsInGroup3; --teamsIndexCounter)
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup3)).Times(1);
+
+ for (; teamsIndexCounter > numTeams - expectedTeamsInGroup4; --teamsIndexCounter)
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[teamsIndexCounter], rewardGroup4)).Times(1);
+
+ // Top 1
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&teams[numTeams], rewardGroup5)).Times(1);
+
+ _distributor->DistributeRewards(arenaTeams, rewardGroups);
+}
+
+// Input: Three teams where one has fewer than the minimum required games and two have enough games.
+// Purpose: Ensures that only teams meeting the minimum required games threshold are eligible for rewards.
+TEST_F(ArenaSeasonRewardDistributorTest, MinimumRequiredGamesFilter)
+{
+ ArenaTeamMgr::ArenaTeamContainer arenaTeams;
+ std::vector rewardGroups;
+
+ // Creating three teams: one below and two above the minRequiredGames threshold (30 games)
+ ArenaTeam team1 = ArenaTeamWithRating(1500, 50); // Eligible, as it has 50 games
+ ArenaTeam team2 = ArenaTeamWithRating(1100, 20); // Not eligible, as it has only 20 games
+ ArenaTeam team3 = ArenaTeamWithRating(1300, 40); // Eligible, as it has 40 games
+
+ // Adding teams to the container
+ arenaTeams[1] = &team1;
+ arenaTeams[2] = &team2;
+ arenaTeams[3] = &team3;
+
+ // Creating a single reward group covering all teams
+ ArenaSeasonRewardGroup rewardGroup;
+ rewardGroup.criteriaType = ARENA_SEASON_REWARD_CRITERIA_TYPE_PERCENT_VALUE;
+ rewardGroup.minCriteria = 0.0;
+ rewardGroup.maxCriteria = 100;
+ rewardGroups.push_back(rewardGroup);
+
+ // We expect the rewarder to be called for team1 and team3, but not for team2.
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team1, rewardGroup)).Times(1);
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team3, rewardGroup)).Times(1);
+ EXPECT_CALL(*_mockRewarder, RewardTeamWithRewardGroup(&team2, rewardGroup)).Times(0);
+
+ _distributor->DistributeRewards(arenaTeams, rewardGroups);
+}
diff --git a/src/test/server/game/Battlegrounds/ArenaSeason/ArenaTeamFilterTest.cpp b/src/test/server/game/Battlegrounds/ArenaSeason/ArenaTeamFilterTest.cpp
new file mode 100644
index 000000000..9db54f200
--- /dev/null
+++ b/src/test/server/game/Battlegrounds/ArenaSeason/ArenaTeamFilterTest.cpp
@@ -0,0 +1,113 @@
+/*
+ * 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 Affero General Public License as published by the
+ * Free Software Foundation; either version 3 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 Affero 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 "Define.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "ArenaTeamFilter.h"
+#include "ArenaTeamMgr.h"
+#include "ArenaTeam.h"
+#include
+
+// Used to expose Type property.
+class ArenaTeamTest : public ArenaTeam
+{
+public:
+ void SetType(uint8 type)
+ {
+ Type = type;
+ }
+};
+
+ArenaTeam* ArenaTeamWithType(uint8 type)
+{
+ ArenaTeamTest* team = new ArenaTeamTest();
+ team->SetType(type);
+ return team;
+}
+
+// Fixture for ArenaTeamFilter tests
+class ArenaTeamFilterTest : public ::testing::Test
+{
+protected:
+ void SetUp() override
+ {
+ team1 = ArenaTeamWithType(2); // 2v2
+ team2 = ArenaTeamWithType(3); // 3v3
+ team3 = ArenaTeamWithType(5); // 5v5
+
+ arenaTeams[1] = team1;
+ arenaTeams[2] = team2;
+ arenaTeams[3] = team3;
+ }
+
+ void TearDown() override
+ {
+ delete team1;
+ delete team2;
+ delete team3;
+ }
+
+ ArenaTeamMgr::ArenaTeamContainer arenaTeams;
+ ArenaTeam* team1;
+ ArenaTeam* team2;
+ ArenaTeam* team3;
+};
+
+// Test for ArenaTeamFilterAllTeams: it should return all teams without filtering
+TEST_F(ArenaTeamFilterTest, AllTeamsFilter)
+{
+ ArenaTeamFilterAllTeams filter;
+ ArenaTeamMgr::ArenaTeamContainer result = filter.Filter(arenaTeams);
+
+ EXPECT_EQ(result.size(), arenaTeams.size());
+ EXPECT_EQ(result[1], team1);
+ EXPECT_EQ(result[2], team2);
+ EXPECT_EQ(result[3], team3);
+}
+
+// Test for ArenaTeamFilterByTypes: should filter only teams matching the provided types
+TEST_F(ArenaTeamFilterTest, FilterBySpecificTypes)
+{
+ std::vector validTypes = {2, 3}; // Filtering for 2v2 and 3v3
+ ArenaTeamFilterByTypes filter(validTypes);
+
+ ArenaTeamMgr::ArenaTeamContainer result = filter.Filter(arenaTeams);
+
+ EXPECT_EQ(result.size(), 2); // Only 2v2 and 3v3 should pass
+ EXPECT_EQ(result[1], team1); // team1 is 2v2
+ EXPECT_EQ(result[2], team2); // team2 is 3v3
+ EXPECT_EQ(result.find(3), result.end()); // team3 (5v5) should be filtered out
+}
+
+// Test for ArenaTeamFilterFactoryByUserInput: should create the correct filter based on input
+TEST_F(ArenaTeamFilterTest, FabricCreatesFilterByInput)
+{
+ ArenaTeamFilterFactoryByUserInput fabric;
+
+ // Test for "all" input
+ auto allTeamsFilter = fabric.CreateFilterByUserInput("all");
+ ArenaTeamMgr::ArenaTeamContainer allTeamsResult = allTeamsFilter->Filter(arenaTeams);
+ EXPECT_EQ(allTeamsResult.size(), arenaTeams.size()); // All teams should pass
+
+ // Test for "2,3" input
+ auto specificTypesFilter = fabric.CreateFilterByUserInput("2,3");
+ ArenaTeamMgr::ArenaTeamContainer filteredResult = specificTypesFilter->Filter(arenaTeams);
+ EXPECT_EQ(filteredResult.size(), 2); // Only 2v2 and 3v3 teams should pass
+ EXPECT_EQ(filteredResult[1], team1); // 2v2
+ EXPECT_EQ(filteredResult[2], team2); // 3v3
+}