feat(Core/Arena): Add support for arena seasons completion with progression in runtime. (#19858)

Co-authored-by: Winfidonarleyan <dowlandtop@yandex.com>
This commit is contained in:
Anton Popovichenko 2025-02-12 11:09:31 +01:00 committed by GitHub
parent 24dd7dfc21
commit f6a0433297
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1250 additions and 59 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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.

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<std::string, ArenaSeasonRewardGroupCriteriaType> 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<uint32, ArenaSeasonRewardGroup> groupsMap;
do
{
Field* fields = result->Fetch();
uint32 id = fields[0].Get<uint32>();
ArenaSeasonRewardGroup group;
group.season = fields[1].Get<uint8>();
group.criteriaType = stringToArenaSeasonRewardGroupCriteriaType[fields[2].Get<std::string>()];
group.minCriteria = fields[3].Get<float>();
group.maxCriteria = fields[4].Get<float>();
group.rewardMailTemplateID = fields[5].Get<uint32>();
group.rewardMailSubject = fields[6].Get<std::string>();
group.rewardMailBody = fields[7].Get<std::string>();
group.goldReward = fields[8].Get<uint32>();
groupsMap[id] = group;
} while (result->NextRow());
std::unordered_map<std::string, ArenaSeasonRewardType> 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<uint32>();
ArenaSeasonReward reward;
reward.type = stringToArenaSeasonRewardType[fields[1].Get<std::string>()];
reward.entry = fields[2].Get<uint32>();
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<uint8>();
_currentSeasonState = static_cast<ArenaSeasonState>(fields[1].Get<uint8>());
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<ArenaTeamFilter> teamsFilter)
{
ArenaSeasonTeamRewarderImpl rewarder = ArenaSeasonTeamRewarderImpl();
ArenaSeasonRewardDistributor distributor = ArenaSeasonRewardDistributor(&rewarder);
std::vector<ArenaSeasonRewardGroup> rewards = _arenaSeasonRewardGroupsStore[GetCurrentSeason()];
ArenaTeamMgr::ArenaTeamContainer filteredTeams = teamsFilter->Filter(sArenaTeamMgr->GetArenaTeams());
distributor.DistributeRewards(filteredTeams, rewards);
}
bool ArenaSeasonMgr::CanDeleteArenaTeams()
{
std::vector<ArenaSeasonRewardGroup> 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<BattlegroundQueueTypeId> 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<uint16>();
}
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);
});
});
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef _ARENASEASONMGR_H
#define _ARENASEASONMGR_H
#include "Common.h"
#include "ArenaTeamFilter.h"
#include <vector>
#include <unordered_map>
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<ArenaSeasonReward> itemRewards;
std::vector<ArenaSeasonReward> 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<uint8, std::vector<ArenaSeasonRewardGroup>>;
// 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<ArenaTeamFilter> 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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ArenaSeasonRewardsDistributor.h"
#include "AchievementMgr.h"
#include "CharacterDatabase.h"
#include "Mail.h"
#include "Player.h"
#include <algorithm>
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<float>(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<float>(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<ArenaSeasonRewardGroup> &rewardGroups)
{
std::vector<ArenaTeam*> 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<ArenaSeasonRewardGroup> pctRewardGroup;
std::vector<ArenaSeasonRewardGroup> 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<size_t>(rewardGroup.minCriteria * totalTeams / 100);
size_t maxIndex = static_cast<size_t>(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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<ArenaSeasonRewardGroup>& rewardGroups);
private:
ArenaSeasonTeamRewarder* _rewarder;
};
#endif // _ARENASEASONREWARDDISTRIBUTOR_H

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef _ARENATEAMFILTER_H
#define _ARENATEAMFILTER_H
#include "Common.h"
#include "ArenaTeamMgr.h"
#include "ArenaTeam.h"
#include "Tokenize.h"
#include "StringConvert.h"
#include <sstream>
#include <string>
#include <memory>
#include <unordered_map>
#include <algorithm>
#include <cctype>
class ArenaTeamFilter
{
public:
virtual ~ArenaTeamFilter() = default;
virtual ArenaTeamMgr::ArenaTeamContainer Filter(ArenaTeamMgr::ArenaTeamContainer teams) = 0;
};
class ArenaTeamFilterByTypes : public ArenaTeamFilter
{
public:
ArenaTeamFilterByTypes(std::vector<uint8> 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<uint8> _validTypes;
};
class ArenaTeamFilterAllTeams : public ArenaTeamFilter
{
public:
ArenaTeamMgr::ArenaTeamContainer Filter(ArenaTeamMgr::ArenaTeamContainer teams) override
{
return teams;
}
};
class ArenaTeamFilterFactoryByUserInput
{
public:
std::unique_ptr<ArenaTeamFilter> 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<ArenaTeamFilterAllTeams>();
// Parse the input string (e.g., "2,3") into valid types
std::vector<uint8> validTypes = ParseTypes(userInput);
if (!validTypes.empty())
return std::make_unique<ArenaTeamFilterByTypes>(validTypes);
return nullptr;
}
private:
std::vector<uint8> ParseTypes(std::string_view userInput)
{
std::vector<uint8> validTypes;
auto tokens = Acore::Tokenize(userInput, ',', false);
for (auto const& token : tokens)
if (auto typeOpt = Acore::StringTo<uint8>(token))
validTypes.push_back(*typeOpt);
return validTypes;
}
};
#endif // _ARENATEAMFILTER_H

View File

@ -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;

View File

@ -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)

View File

@ -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; }

View File

@ -334,6 +334,18 @@ Battleground* BattlegroundMgr::GetBattlegroundTemplate(BattlegroundTypeId bgType
return bgs.empty() ? nullptr : bgs.begin()->second;
}
std::vector<Battleground const*> BattlegroundMgr::GetActiveBattlegrounds()
{
std::vector<Battleground const*> 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<const Battleground*>(bg));
return result;
}
uint32 BattlegroundMgr::CreateClientVisibleInstanceId(BattlegroundTypeId bgTypeId, BattlegroundBracketId bracket_id)
{
if (IsArenaType(bgTypeId))

View File

@ -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<Battleground const*> GetActiveBattlegrounds();
void AddBattleground(Battleground* bg);
void RemoveBattleground(BattlegroundTypeId bgTypeId, uint32 instanceId);

View File

@ -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
{

View File

@ -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<uint8>();
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();

View File

@ -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();

View File

@ -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);

View File

@ -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,

View File

@ -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<bool>("Arena.AutoDistributePoints", false);
_int_configs[CONFIG_ARENA_AUTO_DISTRIBUTE_INTERVAL_DAYS] = sConfigMgr->GetOption<uint32>("Arena.AutoDistributeInterval", 7); // pussywizard: spoiled by implementing constant day and hour, always 7 now
_int_configs[CONFIG_ARENA_GAMES_REQUIRED] = sConfigMgr->GetOption<uint32>("Arena.GamesRequired", 10);
_int_configs[CONFIG_ARENA_SEASON_ID] = sConfigMgr->GetOption<uint32>("Arena.ArenaSeason.ID", 8);
_int_configs[CONFIG_ARENA_START_RATING] = sConfigMgr->GetOption<uint32>("Arena.ArenaStartRating", 0);
_int_configs[CONFIG_LEGACY_ARENA_POINTS_CALC] = sConfigMgr->GetOption<uint32>("Arena.LegacyArenaPoints", 0);
_int_configs[CONFIG_ARENA_START_PERSONAL_RATING] = sConfigMgr->GetOption<uint32>("Arena.ArenaStartPersonalRating", 0);
_int_configs[CONFIG_ARENA_START_MATCHMAKER_RATING] = sConfigMgr->GetOption<uint32>("Arena.ArenaStartMatchmakerRating", 1500);
_bool_configs[CONFIG_ARENA_SEASON_IN_PROGRESS] = sConfigMgr->GetOption<bool>("Arena.ArenaSeason.InProgress", true);
_float_configs[CONFIG_ARENA_WIN_RATING_MODIFIER_1] = sConfigMgr->GetOption<float>("Arena.ArenaWinRatingModifier1", 48.0f);
_float_configs[CONFIG_ARENA_WIN_RATING_MODIFIER_2] = sConfigMgr->GetOption<float>("Arena.ArenaWinRatingModifier2", 24.0f);
_float_configs[CONFIG_ARENA_LOSE_RATING_MODIFIER] = sConfigMgr->GetOption<float>("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();

View File

@ -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<ArenaTeamFilter> uniqueFilter = ArenaTeamFilterFactoryByUserInput().CreateFilterByUserInput(teamsFilterStr);
if (!uniqueFilter)
{
handler->PSendSysMessage("Invalid filter. Please check your input.");
return false;
}
std::shared_ptr<ArenaTeamFilter> 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()

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<MockArenaSeasonTeamRewarder>();
_distributor = std::make_unique<ArenaSeasonRewardDistributor>(_mockRewarder.get());
}
std::unique_ptr<MockArenaSeasonTeamRewarder> _mockRewarder;
std::unique_ptr<ArenaSeasonRewardDistributor> _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<ArenaSeasonRewardGroup> 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<ArenaSeasonRewardGroup> 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<ArenaSeasonRewardGroup> 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<int>(0.005 * numTeams); // 0.5% of 1000 = 5
int expectedTeamsInGroup2 = static_cast<int>(0.03 * numTeams); // 3% of 1000 = 30
int expectedTeamsInGroup3 = static_cast<int>(0.10 * numTeams); // 10% of 1000 = 100
int expectedTeamsInGroup4 = static_cast<int>(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<ArenaSeasonRewardGroup> 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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "Define.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ArenaTeamFilter.h"
#include "ArenaTeamMgr.h"
#include "ArenaTeam.h"
#include <memory>
// 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<uint8> 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
}