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 +}