diff --git a/sql/custom/auth/EG_2016_12_05_00_auth_permissions_01.sql b/sql/custom/auth/EG_2016_12_05_00_auth_permissions_01.sql index 7aa8a9ed..9af3acc3 100644 --- a/sql/custom/auth/EG_2016_12_05_00_auth_permissions_01.sql +++ b/sql/custom/auth/EG_2016_12_05_00_auth_permissions_01.sql @@ -1,7 +1,8 @@ -- -DELETE FROM `rbac_permissions` WHERE `id` = 1003; +DELETE FROM `rbac_permissions` WHERE `id` IN (1003, 1004); INSERT INTO `rbac_permissions` (`id`, `name`) VALUES -(1003, 'Allow crossfaction arena team interaction'); +(1003, 'Allow crossfaction arena team interaction'), +(1004, 'Command: tournament'); DELETE FROM `rbac_linked_permissions` WHERE `id` IN (195, 199); INSERT INTO `rbac_linked_permissions` VALUES diff --git a/sql/custom/auth/EG_2016_12_05_00_auth_permissions_04.sql b/sql/custom/auth/EG_2016_12_05_00_auth_permissions_04.sql index f39080bf..1575cb6c 100644 --- a/sql/custom/auth/EG_2016_12_05_00_auth_permissions_04.sql +++ b/sql/custom/auth/EG_2016_12_05_00_auth_permissions_04.sql @@ -324,4 +324,5 @@ INSERT INTO `rbac_linked_permissions` VALUES (196, 879), -- Command: debug poolstatus (196, 881), -- Command: reload vehicle_template (196, 884), -- Command: bg start -(196, 885); -- Command: bg stop +(196, 885), -- Command: bg stop +(196, 1004); -- Command: tournament diff --git a/sql/custom/characters/EG_2026_06_11_00_character_tournament.sql b/sql/custom/characters/EG_2026_06_11_00_character_tournament.sql new file mode 100644 index 00000000..0ef27346 --- /dev/null +++ b/sql/custom/characters/EG_2026_06_11_00_character_tournament.sql @@ -0,0 +1,75 @@ +-- +DROP TABLE IF EXISTS `tournament`; +CREATE TABLE `tournament` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `state` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0 draft, 1 registration, 2 locked, 3 running, 4 ended', + `difficulty` tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '0 normal, 1 heroic', + `ilvlCap` smallint(5) unsigned NOT NULL DEFAULT 213 COMMENT 'max allowed equipped item level', + `startTime` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'unix time the tournament went running', + `endTime` int(10) unsigned NOT NULL DEFAULT 0, + `createdBy` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'admin character guidLow', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `tournament_dungeon`; +CREATE TABLE `tournament_dungeon` ( + `tournamentId` int(10) unsigned NOT NULL, + `slot` tinyint(3) unsigned NOT NULL COMMENT '1..5', + `mapId` smallint(5) unsigned NOT NULL, + `difficulty` tinyint(3) unsigned NOT NULL DEFAULT 1 COMMENT '0 normal, 1 heroic', + `revealed` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT 'hidden until tournament start', + PRIMARY KEY (`tournamentId`,`slot`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `tournament_team`; +CREATE TABLE `tournament_team` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `tournamentId` int(10) unsigned NOT NULL, + `name` varchar(100) NOT NULL, + `status` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0 active, 1 disqualified', + `dqReason` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `idx_tournament` (`tournamentId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `tournament_team_member`; +CREATE TABLE `tournament_team_member` ( + `teamId` int(10) unsigned NOT NULL, + `charGuid` int(10) unsigned NOT NULL COMMENT 'character guidLow', + `accountId` int(10) unsigned NOT NULL DEFAULT 0, + `role` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0 tank, 1 healer, 2 dps', + PRIMARY KEY (`teamId`,`charGuid`), + KEY `idx_char` (`charGuid`) COMMENT 'uniqueness is per tournament, enforced by the core' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `tournament_run`; +CREATE TABLE `tournament_run` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `teamId` int(10) unsigned NOT NULL, + `dungeonSlot` tinyint(3) unsigned NOT NULL, + `mapId` smallint(5) unsigned NOT NULL, + `instanceId` int(10) unsigned NOT NULL DEFAULT 0, + `state` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0 pending, 1 active, 2 completed, 3 void, 4 rejected', + `combatStart` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'unix time, first player in combat', + `bossFinish` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'unix time, end boss dead with all encounters done', + `durationMs` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'millisecond-precise run time', + `rejectReason` varchar(255) NOT NULL DEFAULT '', + `verifiedBy` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'staff character guidLow', + `created` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_team` (`teamId`), + KEY `idx_slot` (`dungeonSlot`), + KEY `idx_instance` (`instanceId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `tournament_run_event`; +CREATE TABLE `tournament_run_event` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `runId` int(10) unsigned NOT NULL, + `ts` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'unix time', + `type` tinyint(3) unsigned NOT NULL DEFAULT 0 COMMENT '0 enter, 1 combatStart, 2 bossKill, 3 wipe, 4 gearViolation, 5 finish, 6 void, 7 rejected', + `detail` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `idx_run` (`runId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 4b673184..64c5daea 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -616,6 +616,32 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_INS_ARENA_1V1, "INSERT INTO character_arena_1v1 (guid, rating, matchMakerRating, weekGames, weekWins, seasonGames, seasonWins, previousOpponent) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_UPD_ARENA_1V1, "UPDATE character_arena_1v1 SET rating = ?, matchMakerRating = ?, weekGames = ?, weekWins = ?, seasonGames = ?, seasonWins = ?, previousOpponent = ? WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_ARENA_1V1, "DELETE FROM character_arena_1v1 WHERE guid = ?", CONNECTION_ASYNC); + + // EG - PvE tournament + PrepareStatement(CHAR_SEL_TOURNAMENT_ALL, "SELECT id, name, state, difficulty, ilvlCap, startTime, endTime, createdBy FROM tournament", CONNECTION_SYNCH); + PrepareStatement(CHAR_INS_TOURNAMENT, "INSERT INTO tournament (id, name, state, difficulty, ilvlCap, startTime, endTime, createdBy) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_TOURNAMENT_STATE, "UPDATE tournament SET state = ?, startTime = ?, endTime = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_TOURNAMENT_ILVL, "UPDATE tournament SET ilvlCap = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_TOURNAMENT, "DELETE FROM tournament WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_TOURNAMENT_DUNGEON_ALL, "SELECT tournamentId, slot, mapId, difficulty, revealed FROM tournament_dungeon", CONNECTION_SYNCH); + PrepareStatement(CHAR_REP_TOURNAMENT_DUNGEON, "REPLACE INTO tournament_dungeon (tournamentId, slot, mapId, difficulty, revealed) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_TOURNAMENT_DUNGEON, "DELETE FROM tournament_dungeon WHERE tournamentId = ? AND slot = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_TOURNAMENT_DUNGEON_ALL, "DELETE FROM tournament_dungeon WHERE tournamentId = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_TOURNAMENT_DUNGEON_REVEAL, "UPDATE tournament_dungeon SET revealed = ? WHERE tournamentId = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_TOURNAMENT_TEAM_ALL, "SELECT id, tournamentId, name, status, dqReason FROM tournament_team", CONNECTION_SYNCH); + PrepareStatement(CHAR_INS_TOURNAMENT_TEAM, "INSERT INTO tournament_team (id, tournamentId, name, status, dqReason) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_TOURNAMENT_TEAM_STATUS, "UPDATE tournament_team SET status = ?, dqReason = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_TOURNAMENT_TEAM, "DELETE FROM tournament_team WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_TOURNAMENT_MEMBER_ALL, "SELECT teamId, charGuid, accountId, role FROM tournament_team_member", CONNECTION_SYNCH); + PrepareStatement(CHAR_INS_TOURNAMENT_MEMBER, "INSERT INTO tournament_team_member (teamId, charGuid, accountId, role) VALUES (?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_TOURNAMENT_MEMBER, "DELETE FROM tournament_team_member WHERE teamId = ? AND charGuid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_TOURNAMENT_MEMBER_BY_TEAM, "DELETE FROM tournament_team_member WHERE teamId = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_TOURNAMENT_RUN_ALL, "SELECT id, teamId, dungeonSlot, mapId, instanceId, state, combatStart, bossFinish, durationMs, rejectReason FROM tournament_run", CONNECTION_SYNCH); + PrepareStatement(CHAR_SEL_TOURNAMENT_RUN_BY_ID, "SELECT id FROM tournament_run WHERE id = ?", CONNECTION_SYNCH); + PrepareStatement(CHAR_INS_TOURNAMENT_RUN, "INSERT INTO tournament_run (id, teamId, dungeonSlot, mapId, instanceId, state, created) VALUES (?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_TOURNAMENT_RUN, "UPDATE tournament_run SET state = ?, combatStart = ?, bossFinish = ?, durationMs = ?, rejectReason = ?, verifiedBy = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_TOURNAMENT_RUN_VERDICT, "UPDATE tournament_run SET state = ?, rejectReason = ?, verifiedBy = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_TOURNAMENT_RUN_EVENT, "INSERT INTO tournament_run_event (runId, ts, type, detail) VALUES (?, ?, ?, ?)", CONNECTION_ASYNC); } CharacterDatabaseConnection::CharacterDatabaseConnection(MySQLConnectionInfo& connInfo) : MySQLConnection(connInfo) diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index 6aa28627..9060ae14 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -532,6 +532,32 @@ enum CharacterDatabaseStatements : uint32 CHAR_UPD_ARENA_1V1, CHAR_DEL_ARENA_1V1, + // EG - PvE tournament + CHAR_SEL_TOURNAMENT_ALL, + CHAR_INS_TOURNAMENT, + CHAR_UPD_TOURNAMENT_STATE, + CHAR_UPD_TOURNAMENT_ILVL, + CHAR_DEL_TOURNAMENT, + CHAR_SEL_TOURNAMENT_DUNGEON_ALL, + CHAR_REP_TOURNAMENT_DUNGEON, + CHAR_DEL_TOURNAMENT_DUNGEON, + CHAR_DEL_TOURNAMENT_DUNGEON_ALL, + CHAR_UPD_TOURNAMENT_DUNGEON_REVEAL, + CHAR_SEL_TOURNAMENT_TEAM_ALL, + CHAR_INS_TOURNAMENT_TEAM, + CHAR_UPD_TOURNAMENT_TEAM_STATUS, + CHAR_DEL_TOURNAMENT_TEAM, + CHAR_SEL_TOURNAMENT_MEMBER_ALL, + CHAR_INS_TOURNAMENT_MEMBER, + CHAR_DEL_TOURNAMENT_MEMBER, + CHAR_DEL_TOURNAMENT_MEMBER_BY_TEAM, + CHAR_SEL_TOURNAMENT_RUN_ALL, + CHAR_SEL_TOURNAMENT_RUN_BY_ID, + CHAR_INS_TOURNAMENT_RUN, + CHAR_UPD_TOURNAMENT_RUN, + CHAR_UPD_TOURNAMENT_RUN_VERDICT, + CHAR_INS_TOURNAMENT_RUN_EVENT, + MAX_CHARACTERDATABASE_STATEMENTS }; diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index a4b76dca..de790a65 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -756,6 +756,7 @@ enum RBACPermissions RBAC_PERM_COMMAND_CUSTOM_CHARACTER_SETTINGS = 1000, RBAC_PERM_FREE_TRANSMOGRIFICATION = 1002, RBAC_PERM_TWO_SIDE_INTERACTION_ARENA = 1003, + RBAC_PERM_COMMAND_TOURNAMENT = 1004, RBAC_PERM_MAX }; diff --git a/src/server/game/Custom/Tournament/TournamentMgr.cpp b/src/server/game/Custom/Tournament/TournamentMgr.cpp new file mode 100644 index 00000000..eb4d26cc --- /dev/null +++ b/src/server/game/Custom/Tournament/TournamentMgr.cpp @@ -0,0 +1,990 @@ +#include "TournamentMgr.h" +#include "DatabaseEnv.h" +#include "GameTime.h" +#include "Item.h" +#include "Log.h" +#include "Map.h" +#include "MapUtils.h" +#include "Player.h" +#include "StringFormat.h" +#include "Timer.h" +#include +#include + +TournamentMember const* TournamentTeam::GetMember(ObjectGuid::LowType guid) const +{ + for (TournamentMember const& member : members) + if (member.charGuid == guid) + return &member; + return nullptr; +} + +bool TournamentTeam::HasRoleComposition() const +{ + if (members.size() != TOURNAMENT_TEAM_SIZE) + return false; + + uint8 tanks = 0; + uint8 healers = 0; + uint8 dps = 0; + for (TournamentMember const& member : members) + { + switch (member.role) + { + case TOURNAMENT_ROLE_TANK: + ++tanks; + break; + case TOURNAMENT_ROLE_HEALER: + ++healers; + break; + case TOURNAMENT_ROLE_DPS: + ++dps; + break; + } + } + + return tanks == 1 && healers == 1 && dps == 3; +} + +TournamentDungeon const* TournamentData::GetDungeonByMap(uint16 mapId, uint8 difficulty) const +{ + for (auto const& pair : dungeons) + if (pair.second.mapId == mapId && pair.second.difficulty == difficulty) + return &pair.second; + return nullptr; +} + +TournamentMgr* TournamentMgr::instance() +{ + static TournamentMgr inst; + return &inst; +} + +void TournamentMgr::LoadFromDB() +{ + uint32 const oldMSTime = getMSTime(); + + std::unique_lock lock(_lock); + + _tournaments.clear(); + _teams.clear(); + _memberIndex.clear(); + _runsByInstance.clear(); + _nextTournamentId = 1; + _nextTeamId = 1; + _nextRunId = 1; + + // tournaments + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_ALL))) + { + do + { + Field* fields = result->Fetch(); + TournamentData data; + data.id = fields[0].GetUInt32(); + data.name = fields[1].GetString(); + data.state = TournamentState(fields[2].GetUInt8()); + data.difficulty = fields[3].GetUInt8(); + data.ilvlCap = fields[4].GetUInt16(); + data.startTime = fields[5].GetUInt32(); + data.endTime = fields[6].GetUInt32(); + data.createdBy = fields[7].GetUInt32(); + _nextTournamentId = std::max(_nextTournamentId, data.id + 1); + _tournaments[data.id] = std::move(data); + } while (result->NextRow()); + } + + // dungeon set + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_DUNGEON_ALL))) + { + do + { + Field* fields = result->Fetch(); + TournamentData* tournament = Trinity::Containers::MapGetValuePtr(_tournaments, fields[0].GetUInt32()); + if (!tournament) + continue; + + TournamentDungeon dungeon; + dungeon.slot = fields[1].GetUInt8(); + dungeon.mapId = fields[2].GetUInt16(); + dungeon.difficulty = fields[3].GetUInt8(); + dungeon.revealed = fields[4].GetBool(); + tournament->dungeons[dungeon.slot] = dungeon; + } while (result->NextRow()); + } + + // teams + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_TEAM_ALL))) + { + do + { + Field* fields = result->Fetch(); + TournamentTeam team; + team.id = fields[0].GetUInt32(); + team.tournamentId = fields[1].GetUInt32(); + team.name = fields[2].GetString(); + team.status = TournamentTeamStatus(fields[3].GetUInt8()); + team.dqReason = fields[4].GetString(); + _nextTeamId = std::max(_nextTeamId, team.id + 1); + _teams[team.id] = std::move(team); + } while (result->NextRow()); + } + + // members + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_MEMBER_ALL))) + { + do + { + Field* fields = result->Fetch(); + uint32 teamId = fields[0].GetUInt32(); + TournamentTeam* team = Trinity::Containers::MapGetValuePtr(_teams, teamId); + if (!team) + continue; + + TournamentMember member; + member.charGuid = fields[1].GetUInt32(); + member.accountId = fields[2].GetUInt32(); + member.role = TournamentRole(fields[3].GetUInt8()); + team->members.push_back(member); + _memberIndex.emplace(member.charGuid, teamId); + } while (result->NextRow()); + } + + // runs: not held in memory, but advance the id counter past the highest stored run + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_RUN_ALL))) + { + do + { + Field* fields = result->Fetch(); + _nextRunId = std::max(_nextRunId, fields[0].GetUInt32() + 1); + } while (result->NextRow()); + } + + TC_LOG_INFO("server.loading", ">> Loaded {} tournament(s), {} team(s) in {} ms", + uint32(_tournaments.size()), uint32(_teams.size()), GetMSTimeDiffToNow(oldMSTime)); +} + +TournamentData const* TournamentMgr::FindActiveTournament() const +{ + TournamentData const* fallback = nullptr; + for (auto const& pair : _tournaments) + { + if (pair.second.state == TOURNAMENT_STATE_RUNNING) + return &pair.second; + if (!fallback || pair.second.id > fallback->id) + fallback = &pair.second; + } + return fallback; +} + +TournamentData const* TournamentMgr::GetActiveTournament() const +{ + std::shared_lock lock(_lock); + return FindActiveTournament(); +} + +TournamentData const* TournamentMgr::GetTournament(uint32 id) const +{ + std::shared_lock lock(_lock); + return Trinity::Containers::MapGetValuePtr(_tournaments, id); +} + +TournamentTeam const* TournamentMgr::GetTeam(uint32 teamId) const +{ + std::shared_lock lock(_lock); + return Trinity::Containers::MapGetValuePtr(_teams, teamId); +} + +TournamentTeam const* TournamentMgr::FindTeamByMember(ObjectGuid::LowType charGuid, uint32 tournamentId) const +{ + auto range = _memberIndex.equal_range(charGuid); + for (auto itr = range.first; itr != range.second; ++itr) + if (TournamentTeam const* team = Trinity::Containers::MapGetValuePtr(_teams, itr->second)) + if (team->tournamentId == tournamentId) + return team; + + return nullptr; +} + +TournamentTeam const* TournamentMgr::GetTeamByMember(ObjectGuid::LowType charGuid, uint32 tournamentId) const +{ + std::shared_lock lock(_lock); + return FindTeamByMember(charGuid, tournamentId); +} + +std::vector TournamentMgr::GetTeams(uint32 tournamentId) const +{ + std::shared_lock lock(_lock); + + std::vector teams; + for (auto const& pair : _teams) + if (pair.second.tournamentId == tournamentId) + teams.push_back(&pair.second); + + std::sort(teams.begin(), teams.end(), [](TournamentTeam const* a, TournamentTeam const* b) { return a->id < b->id; }); + return teams; +} + +uint32 TournamentMgr::CreateTournament(std::string_view name, uint8 difficulty, ObjectGuid::LowType admin) +{ + std::unique_lock lock(_lock); + + TournamentData data; + data.id = _nextTournamentId++; + data.name = name; + data.state = TOURNAMENT_STATE_DRAFT; + data.difficulty = difficulty; + data.ilvlCap = 213; + data.createdBy = admin; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_TOURNAMENT); + stmt->setUInt32(0, data.id); + stmt->setString(1, data.name); + stmt->setUInt8(2, data.state); + stmt->setUInt8(3, data.difficulty); + stmt->setUInt16(4, data.ilvlCap); + stmt->setUInt32(5, data.startTime); + stmt->setUInt32(6, data.endTime); + stmt->setUInt32(7, data.createdBy); + CharacterDatabase.Execute(stmt); + + uint32 const id = data.id; + _tournaments[id] = std::move(data); + return id; +} + +bool TournamentMgr::DeleteTournament(uint32 id) +{ + std::unique_lock lock(_lock); + + auto itr = _tournaments.find(id); + if (itr == _tournaments.end()) + return false; + + // cascade teams (and their members) belonging to this tournament + std::vector teamIds; + for (auto const& pair : _teams) + if (pair.second.tournamentId == id) + teamIds.push_back(pair.first); + for (uint32 teamId : teamIds) + EraseTeam(teamId); + + CharacterDatabasePreparedStatement* delDungeons = CharacterDatabase.GetPreparedStatement(CHAR_DEL_TOURNAMENT_DUNGEON_ALL); + delDungeons->setUInt32(0, id); + CharacterDatabase.Execute(delDungeons); + + CharacterDatabasePreparedStatement* delTournament = CharacterDatabase.GetPreparedStatement(CHAR_DEL_TOURNAMENT); + delTournament->setUInt32(0, id); + CharacterDatabase.Execute(delTournament); + + _tournaments.erase(itr); + return true; +} + +bool TournamentMgr::SetState(uint32 id, TournamentState state) +{ + std::unique_lock lock(_lock); + + TournamentData* data = Trinity::Containers::MapGetValuePtr(_tournaments, id); + if (!data) + return false; + + // only one tournament may be RUNNING, the hooks gate on the single active one + if (state == TOURNAMENT_STATE_RUNNING) + for (auto const& pair : _tournaments) + if (pair.first != id && pair.second.state == TOURNAMENT_STATE_RUNNING) + return false; + + data->state = state; + if (state == TOURNAMENT_STATE_RUNNING && !data->startTime) + data->startTime = uint32(GameTime::GetGameTime()); + if (state == TOURNAMENT_STATE_ENDED) + data->endTime = uint32(GameTime::GetGameTime()); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_STATE); + stmt->setUInt8(0, data->state); + stmt->setUInt32(1, data->startTime); + stmt->setUInt32(2, data->endTime); + stmt->setUInt32(3, id); + CharacterDatabase.Execute(stmt); + + if (state == TOURNAMENT_STATE_RUNNING) + RevealDungeonsOfTournament(*data); + else + { + // live runs only exist while their tournament is RUNNING; void any leftovers + std::vector liveInstances; + for (auto const& pair : _runsByInstance) + if (TournamentTeam const* team = Trinity::Containers::MapGetValuePtr(_teams, pair.second.teamId)) + if (team->tournamentId == id) + liveInstances.push_back(pair.first); + + for (uint32 instanceId : liveInstances) + TerminateRun(instanceId, TOURNAMENT_RUN_VOID, "tournament no longer running"); + } + return true; +} + +bool TournamentMgr::SetIlvlCap(uint32 id, uint16 cap) +{ + std::unique_lock lock(_lock); + + TournamentData* data = Trinity::Containers::MapGetValuePtr(_tournaments, id); + if (!data) + return false; + + data->ilvlCap = cap; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_ILVL); + stmt->setUInt16(0, cap); + stmt->setUInt32(1, id); + CharacterDatabase.Execute(stmt); + return true; +} + +bool TournamentMgr::SetDungeon(uint32 id, uint8 slot, uint16 mapId, uint8 difficulty) +{ + std::unique_lock lock(_lock); + + TournamentData* data = Trinity::Containers::MapGetValuePtr(_tournaments, id); + if (!data || slot < 1 || slot > TOURNAMENT_DUNGEON_NUM) + return false; + + // the same dungeon may not fill two slots + if (TournamentDungeon const* existing = data->GetDungeonByMap(mapId, difficulty)) + if (existing->slot != slot) + return false; + + // runs of a redefined slot would mix timings of different dungeons + if (TournamentDungeon const* current = Trinity::Containers::MapGetValuePtr(data->dungeons, slot)) + if (current->mapId != mapId || current->difficulty != difficulty) + VoidLiveRunsOfSlot(id, slot, "dungeon slot redefined"); + + TournamentDungeon dungeon; + dungeon.slot = slot; + dungeon.mapId = mapId; + dungeon.difficulty = difficulty; + dungeon.revealed = data->state == TOURNAMENT_STATE_RUNNING; // hidden until start; a swap mid-event is immediately playable + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_TOURNAMENT_DUNGEON); + stmt->setUInt32(0, id); + stmt->setUInt8(1, dungeon.slot); + stmt->setUInt16(2, dungeon.mapId); + stmt->setUInt8(3, dungeon.difficulty); + stmt->setBool(4, dungeon.revealed); + CharacterDatabase.Execute(stmt); + + data->dungeons[slot] = dungeon; + return true; +} + +bool TournamentMgr::RemoveDungeon(uint32 id, uint8 slot) +{ + std::unique_lock lock(_lock); + + TournamentData* data = Trinity::Containers::MapGetValuePtr(_tournaments, id); + if (!data || !data->dungeons.count(slot)) + return false; + + VoidLiveRunsOfSlot(id, slot, "dungeon slot removed"); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_TOURNAMENT_DUNGEON); + stmt->setUInt32(0, id); + stmt->setUInt8(1, slot); + CharacterDatabase.Execute(stmt); + + data->dungeons.erase(slot); + return true; +} + +void TournamentMgr::RevealDungeonsOfTournament(TournamentData& data) +{ + for (auto& pair : data.dungeons) + pair.second.revealed = true; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_DUNGEON_REVEAL); + stmt->setBool(0, true); + stmt->setUInt32(1, data.id); + CharacterDatabase.Execute(stmt); +} + +void TournamentMgr::RevealDungeons(uint32 id) +{ + std::unique_lock lock(_lock); + + if (TournamentData* data = Trinity::Containers::MapGetValuePtr(_tournaments, id)) + RevealDungeonsOfTournament(*data); +} + +uint32 TournamentMgr::CreateTeam(uint32 tournamentId, std::string_view name) +{ + std::unique_lock lock(_lock); + + if (!_tournaments.count(tournamentId)) + return 0; + + TournamentTeam team; + team.id = _nextTeamId++; + team.tournamentId = tournamentId; + team.name = name; + team.status = TOURNAMENT_TEAM_ACTIVE; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_TOURNAMENT_TEAM); + stmt->setUInt32(0, team.id); + stmt->setUInt32(1, team.tournamentId); + stmt->setString(2, team.name); + stmt->setUInt8(3, team.status); + stmt->setString(4, team.dqReason); + CharacterDatabase.Execute(stmt); + + uint32 const id = team.id; + _teams[id] = std::move(team); + return id; +} + +void TournamentMgr::VoidLiveRunsOfTeam(uint32 teamId, std::string_view why) +{ + std::vector liveInstances; + for (auto const& pair : _runsByInstance) + if (pair.second.teamId == teamId) + liveInstances.push_back(pair.first); + + for (uint32 instanceId : liveInstances) + TerminateRun(instanceId, TOURNAMENT_RUN_VOID, why); +} + +void TournamentMgr::VoidLiveRunsOfSlot(uint32 tournamentId, uint8 slot, std::string_view why) +{ + std::vector liveInstances; + for (auto const& pair : _runsByInstance) + if (pair.second.dungeonSlot == slot) + if (TournamentTeam const* team = Trinity::Containers::MapGetValuePtr(_teams, pair.second.teamId)) + if (team->tournamentId == tournamentId) + liveInstances.push_back(pair.first); + + for (uint32 instanceId : liveInstances) + TerminateRun(instanceId, TOURNAMENT_RUN_VOID, why); +} + +void TournamentMgr::EraseTeam(uint32 teamId) +{ + auto itr = _teams.find(teamId); + if (itr == _teams.end()) + return; + + VoidLiveRunsOfTeam(teamId, "team deleted"); + + for (TournamentMember const& member : itr->second.members) + Trinity::Containers::MultimapErasePair(_memberIndex, member.charGuid, teamId); + + CharacterDatabasePreparedStatement* delMembers = CharacterDatabase.GetPreparedStatement(CHAR_DEL_TOURNAMENT_MEMBER_BY_TEAM); + delMembers->setUInt32(0, teamId); + CharacterDatabase.Execute(delMembers); + + CharacterDatabasePreparedStatement* delTeam = CharacterDatabase.GetPreparedStatement(CHAR_DEL_TOURNAMENT_TEAM); + delTeam->setUInt32(0, teamId); + CharacterDatabase.Execute(delTeam); + + _teams.erase(itr); +} + +bool TournamentMgr::DeleteTeam(uint32 teamId) +{ + std::unique_lock lock(_lock); + + if (!_teams.count(teamId)) + return false; + + EraseTeam(teamId); + return true; +} + +bool TournamentMgr::AddMember(uint32 teamId, ObjectGuid::LowType charGuid, uint32 accountId, TournamentRole role) +{ + std::unique_lock lock(_lock); + + TournamentTeam* team = Trinity::Containers::MapGetValuePtr(_teams, teamId); + if (!team) + return false; + + // a character may belong to only one team per tournament + if (FindTeamByMember(charGuid, team->tournamentId)) + return false; + + if (team->members.size() >= TOURNAMENT_TEAM_SIZE) + return false; + + TournamentMember member; + member.charGuid = charGuid; + member.accountId = accountId; + member.role = role; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_TOURNAMENT_MEMBER); + stmt->setUInt32(0, teamId); + stmt->setUInt32(1, charGuid); + stmt->setUInt32(2, accountId); + stmt->setUInt8(3, role); + CharacterDatabase.Execute(stmt); + + team->members.push_back(member); + _memberIndex.emplace(charGuid, teamId); + return true; +} + +bool TournamentMgr::RemoveMember(uint32 teamId, ObjectGuid::LowType charGuid) +{ + std::unique_lock lock(_lock); + + TournamentTeam* team = Trinity::Containers::MapGetValuePtr(_teams, teamId); + if (!team) + return false; + + std::vector& members = team->members; + auto memberItr = std::find_if(members.begin(), members.end(), + [charGuid](TournamentMember const& m) { return m.charGuid == charGuid; }); + if (memberItr == members.end()) + return false; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_TOURNAMENT_MEMBER); + stmt->setUInt32(0, teamId); + stmt->setUInt32(1, charGuid); + CharacterDatabase.Execute(stmt); + + members.erase(memberItr); + Trinity::Containers::MultimapErasePair(_memberIndex, charGuid, teamId); + return true; +} + +bool TournamentMgr::DisqualifyTeam(uint32 teamId, std::string_view reason) +{ + std::unique_lock lock(_lock); + + TournamentTeam* team = Trinity::Containers::MapGetValuePtr(_teams, teamId); + if (!team) + return false; + + VoidLiveRunsOfTeam(teamId, "team disqualified"); + + team->status = TOURNAMENT_TEAM_DISQUALIFIED; + team->dqReason = reason; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_TEAM_STATUS); + stmt->setUInt8(0, team->status); + stmt->setString(1, team->dqReason); + stmt->setUInt32(2, teamId); + CharacterDatabase.Execute(stmt); + return true; +} + +bool TournamentMgr::RequalifyTeam(uint32 teamId) +{ + std::unique_lock lock(_lock); + + TournamentTeam* team = Trinity::Containers::MapGetValuePtr(_teams, teamId); + if (!team) + return false; + + team->status = TOURNAMENT_TEAM_ACTIVE; + team->dqReason.clear(); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_TEAM_STATUS); + stmt->setUInt8(0, team->status); + stmt->setString(1, team->dqReason); + stmt->setUInt32(2, teamId); + CharacterDatabase.Execute(stmt); + return true; +} + +TournamentTeam const* TournamentMgr::MatchTeam(std::vector const& memberGuids) const +{ + if (memberGuids.size() != TOURNAMENT_TEAM_SIZE) + return nullptr; + + std::shared_lock lock(_lock); + + // resolve candidate teams via the first member, then require an exact set match + auto range = _memberIndex.equal_range(memberGuids.front()); + for (auto idxItr = range.first; idxItr != range.second; ++idxItr) + if (TournamentTeam const* team = MatchTeamCandidate(Trinity::Containers::MapGetValuePtr(_teams, idxItr->second), memberGuids)) + return team; + + return nullptr; +} + +TournamentTeam const* TournamentMgr::MatchTeamCandidate(TournamentTeam const* team, std::vector const& memberGuids) const +{ + if (!team || team->status != TOURNAMENT_TEAM_ACTIVE) + return nullptr; + + if (team->members.size() != memberGuids.size() || !team->HasRoleComposition()) + return nullptr; + + for (ObjectGuid::LowType guid : memberGuids) + if (!team->GetMember(guid)) + return nullptr; + + return team; +} + +Item const* TournamentMgr::GetEquippedViolation(Player const* player, uint16 ilvlCap) +{ + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + if (Item const* item = player->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + if (item->GetItemLevel() > ilvlCap) + return item; + + return nullptr; +} + +Item const* TournamentMgr::GetContestantEntryViolation(Player const* player, uint16 mapId, uint8 difficulty) const +{ + std::shared_lock lock(_lock); + + TournamentData const* tournament = FindActiveTournament(); + if (!tournament || tournament->state != TOURNAMENT_STATE_RUNNING) + return nullptr; + + if (!tournament->GetDungeonByMap(mapId, difficulty)) + return nullptr; + + TournamentTeam const* team = FindTeamByMember(player->GetGUID().GetCounter(), tournament->id); + if (!team || team->status != TOURNAMENT_TEAM_ACTIVE) + return nullptr; + + return GetEquippedViolation(player, tournament->ilvlCap); +} + +TournamentData const* TournamentMgr::GetRunningTournamentForTeam(uint32 teamId) const +{ + TournamentTeam const* team = Trinity::Containers::MapGetValuePtr(_teams, teamId); + if (!team) + return nullptr; + + TournamentData const* tournament = Trinity::Containers::MapGetValuePtr(_tournaments, team->tournamentId); + if (!tournament || tournament->state != TOURNAMENT_STATE_RUNNING) + return nullptr; + + return tournament; +} + +uint32 TournamentMgr::GetEquipViolationRunId(Player const* player, Item const* item) const +{ + Map const* map = player->GetMap(); + if (!map || !map->IsDungeon()) + return 0; + + TournamentRun const* run = Trinity::Containers::MapGetValuePtr(_runsByInstance, map->GetInstanceId()); + if (!run) + return 0; + + if (run->state != TOURNAMENT_RUN_PENDING && run->state != TOURNAMENT_RUN_ACTIVE) + return 0; + + TournamentData const* tournament = GetRunningTournamentForTeam(run->teamId); + if (!tournament) + return 0; + + TournamentTeam const* team = Trinity::Containers::MapGetValuePtr(_teams, run->teamId); + if (!team || !team->GetMember(player->GetGUID().GetCounter())) + return 0; + + if (item->GetItemLevel() <= tournament->ilvlCap) + return 0; + + return run->id; +} + +bool TournamentMgr::IsContestantEquipViolation(Player const* player, Item const* item) const +{ + std::shared_lock lock(_lock); + return GetEquipViolationRunId(player, item) != 0; +} + +void TournamentMgr::LogEquipViolation(Player const* player, Item const* item) +{ + uint32 runId = 0; + { + std::shared_lock lock(_lock); + runId = GetEquipViolationRunId(player, item); + } + + if (runId) + LogEvent(runId, TOURNAMENT_EVENT_GEAR_VIOLATION, Trinity::StringFormat("{} attempted to equip item {} (ilvl {}), denied", + player->GetName(), item->GetEntry(), item->GetItemLevel())); +} + +void TournamentMgr::OnPlayerCombatStart(Player const* player) +{ + Map const* map = player->GetMap(); + if (!map || !map->IsDungeon()) + return; + + std::unique_lock lock(_lock); + + TournamentRun* run = Trinity::Containers::MapGetValuePtr(_runsByInstance, map->GetInstanceId()); + if (!run) + return; + + if (run->state != TOURNAMENT_RUN_PENDING) + return; + + if (!GetRunningTournamentForTeam(run->teamId)) + return; + + TournamentTeam const* team = Trinity::Containers::MapGetValuePtr(_teams, run->teamId); + if (!team || !team->GetMember(player->GetGUID().GetCounter())) + return; + + StampCombatStart(*run); +} + +uint32 TournamentMgr::CreateRun(uint32 teamId, uint8 dungeonSlot, uint16 mapId, uint32 instanceId) +{ + std::unique_lock lock(_lock); + + if (!GetRunningTournamentForTeam(teamId)) + return 0; + + TournamentRun run; + run.id = _nextRunId++; + run.teamId = teamId; + run.dungeonSlot = dungeonSlot; + run.mapId = mapId; + run.instanceId = instanceId; + run.state = TOURNAMENT_RUN_PENDING; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_TOURNAMENT_RUN); + stmt->setUInt32(0, run.id); + stmt->setUInt32(1, run.teamId); + stmt->setUInt8(2, run.dungeonSlot); + stmt->setUInt16(3, run.mapId); + stmt->setUInt32(4, run.instanceId); + stmt->setUInt8(5, run.state); + stmt->setUInt32(6, uint32(GameTime::GetGameTime())); + CharacterDatabase.Execute(stmt); + + uint32 const id = run.id; + _runsByInstance[instanceId] = std::move(run); + LogEvent(id, TOURNAMENT_EVENT_ENTER, ""); + return id; +} + +TournamentRun const* TournamentMgr::GetRunByInstance(uint32 instanceId) const +{ + std::shared_lock lock(_lock); + return Trinity::Containers::MapGetValuePtr(_runsByInstance, instanceId); +} + +void TournamentMgr::FlagRunFinalizing(uint32 instanceId) +{ + std::unique_lock lock(_lock); + if (TournamentRun* run = Trinity::Containers::MapGetValuePtr(_runsByInstance, instanceId)) + run->finalizing = true; +} + +bool TournamentMgr::IsRunFinalizing(uint32 instanceId) const +{ + std::shared_lock lock(_lock); + TournamentRun const* run = Trinity::Containers::MapGetValuePtr(_runsByInstance, instanceId); + return run && run->finalizing; +} + +void TournamentMgr::StampCombatStart(TournamentRun& run) +{ + if (run.combatStart) + return; + + run.combatStart = uint32(GameTime::GetGameTime()); + run.combatStartMSTime = GameTime::GetGameTimeMS(); + run.state = TOURNAMENT_RUN_ACTIVE; + SaveRun(run); + LogEvent(run.id, TOURNAMENT_EVENT_COMBAT_START, ""); +} + +bool TournamentMgr::TerminateRun(uint32 instanceId, TournamentRunState state, std::string_view why) +{ + auto itr = _runsByInstance.find(instanceId); + if (itr == _runsByInstance.end()) + return false; + + TournamentRun& run = itr->second; + run.state = state; + TournamentRunEventType eventType; + switch (state) + { + case TOURNAMENT_RUN_COMPLETED: + run.bossFinish = uint32(GameTime::GetGameTime()); + run.durationMs = run.combatStartMSTime ? getMSTimeDiff(run.combatStartMSTime, GameTime::GetGameTimeMS()) : 0; + eventType = TOURNAMENT_EVENT_FINISH; + break; + case TOURNAMENT_RUN_REJECTED: + run.rejectReason = why; + eventType = TOURNAMENT_EVENT_REJECTED; + break; + default: + run.rejectReason = why; + eventType = TOURNAMENT_EVENT_VOID; + break; + } + + SaveRun(run); + LogEvent(run.id, eventType, why); + _runsByInstance.erase(itr); + return true; +} + +void TournamentMgr::CompleteRun(uint32 instanceId) +{ + std::unique_lock lock(_lock); + TerminateRun(instanceId, TOURNAMENT_RUN_COMPLETED, ""); +} + +void TournamentMgr::RejectRun(uint32 instanceId, std::string_view why) +{ + std::unique_lock lock(_lock); + TerminateRun(instanceId, TOURNAMENT_RUN_REJECTED, why); +} + +void TournamentMgr::VoidRun(uint32 instanceId, std::string_view why) +{ + std::unique_lock lock(_lock); + TerminateRun(instanceId, TOURNAMENT_RUN_VOID, why); +} + +void TournamentMgr::SaveRun(TournamentRun const& run) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_RUN); + stmt->setUInt8(0, run.state); + stmt->setUInt32(1, run.combatStart); + stmt->setUInt32(2, run.bossFinish); + stmt->setUInt32(3, run.durationMs); + stmt->setString(4, run.rejectReason); + stmt->setUInt32(5, 0); + stmt->setUInt32(6, run.id); + CharacterDatabase.Execute(stmt); +} + +void TournamentMgr::LogEvent(uint32 runId, TournamentRunEventType type, std::string_view detail) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_TOURNAMENT_RUN_EVENT); + stmt->setUInt32(0, runId); + stmt->setUInt32(1, uint32(GameTime::GetGameTime())); + stmt->setUInt8(2, type); + stmt->setStringView(3, detail); + CharacterDatabase.Execute(stmt); +} + +bool TournamentMgr::SetRunVerdict(uint32 runId, TournamentRunState state, std::string_view reason, ObjectGuid::LowType staff) +{ + // completion is engine-driven only: a forced COMPLETED verdict would carry no timings (0 ms would win the slot) + if (state != TOURNAMENT_RUN_VOID && state != TOURNAMENT_RUN_REJECTED) + return false; + + { + // if the run is still live, route through the normal lifecycle so the instance entry is released + std::unique_lock lock(_lock); + for (auto const& pair : _runsByInstance) + { + if (pair.second.id != runId) + continue; + + TerminateRun(pair.second.instanceId, state, reason); + return true; + } + } + + // stored-run verdicts require an existing row, otherwise the event log would reference a run that never was + CharacterDatabasePreparedStatement* checkStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_RUN_BY_ID); + checkStmt->setUInt32(0, runId); + if (!CharacterDatabase.Query(checkStmt)) + return false; + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_TOURNAMENT_RUN_VERDICT); + stmt->setUInt8(0, state); + stmt->setStringView(1, reason); + stmt->setUInt32(2, staff); + stmt->setUInt32(3, runId); + CharacterDatabase.Execute(stmt); + + LogEvent(runId, state == TOURNAMENT_RUN_VOID ? TOURNAMENT_EVENT_VOID : TOURNAMENT_EVENT_REJECTED, reason); + return true; +} + +void TournamentMgr::BuildStandings(uint32 tournamentId, std::vector& standings) const +{ + standings.clear(); + + std::shared_lock lock(_lock); + + std::unordered_map byTeam; + for (auto const& pair : _teams) + if (pair.second.tournamentId == tournamentId && pair.second.status == TOURNAMENT_TEAM_ACTIVE) + byTeam[pair.first].teamId = pair.first; + + if (byTeam.empty()) + return; + + // best completed duration per team per dungeon slot + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_RUN_ALL))) + { + do + { + Field* fields = result->Fetch(); + TournamentStanding* standing = Trinity::Containers::MapGetValuePtr(byTeam, fields[1].GetUInt32()); + if (!standing) + continue; + + if (TournamentRunState(fields[5].GetUInt8()) != TOURNAMENT_RUN_COMPLETED) + continue; + + uint8 slot = fields[2].GetUInt8(); + uint32 durationMs = fields[8].GetUInt32(); + auto slotItr = standing->bestSlotTimeMs.find(slot); + if (slotItr == standing->bestSlotTimeMs.end() || durationMs < slotItr->second) + standing->bestSlotTimeMs[slot] = durationMs; + } while (result->NextRow()); + } + + // only the fastest team of each dungeon slot receives the point, exact ties go to the lower team id + for (uint8 slot = 1; slot <= TOURNAMENT_DUNGEON_NUM; ++slot) + { + TournamentStanding* fastest = nullptr; + uint32 fastestTime = 0; + for (auto& pair : byTeam) + { + auto slotItr = pair.second.bestSlotTimeMs.find(slot); + if (slotItr == pair.second.bestSlotTimeMs.end()) + continue; + + if (!fastest || slotItr->second < fastestTime || (slotItr->second == fastestTime && pair.second.teamId < fastest->teamId)) + { + fastest = &pair.second; + fastestTime = slotItr->second; + } + } + + if (fastest) + ++fastest->points; + } + + for (auto& pair : byTeam) + { + TournamentStanding& standing = pair.second; + standing.completedSlots = uint8(standing.bestSlotTimeMs.size()); + for (auto const& slotPair : standing.bestSlotTimeMs) + standing.totalTimeMs += slotPair.second; + standings.push_back(std::move(standing)); + } + + // rank: points desc, then completed count desc, then combined time asc, exact ties by team id + std::sort(standings.begin(), standings.end(), [](TournamentStanding const& a, TournamentStanding const& b) + { + if (a.points != b.points) + return a.points > b.points; + if (a.completedSlots != b.completedSlots) + return a.completedSlots > b.completedSlots; + if (a.totalTimeMs != b.totalTimeMs) + return a.totalTimeMs < b.totalTimeMs; + return a.teamId < b.teamId; + }); +} diff --git a/src/server/game/Custom/Tournament/TournamentMgr.h b/src/server/game/Custom/Tournament/TournamentMgr.h new file mode 100644 index 00000000..c026bd79 --- /dev/null +++ b/src/server/game/Custom/Tournament/TournamentMgr.h @@ -0,0 +1,230 @@ +#ifndef EG_TOURNAMENT_MGR_H +#define EG_TOURNAMENT_MGR_H + +#include "Define.h" +#include "ObjectGuid.h" +#include +#include +#include +#include +#include + +class Item; +class Player; + +enum TournamentState : uint8 +{ + TOURNAMENT_STATE_DRAFT = 0, + TOURNAMENT_STATE_REGISTRATION, + TOURNAMENT_STATE_LOCKED, + TOURNAMENT_STATE_RUNNING, + TOURNAMENT_STATE_ENDED +}; + +enum TournamentRole : uint8 +{ + TOURNAMENT_ROLE_TANK = 0, + TOURNAMENT_ROLE_HEALER, + TOURNAMENT_ROLE_DPS +}; + +enum TournamentTeamStatus : uint8 +{ + TOURNAMENT_TEAM_ACTIVE = 0, + TOURNAMENT_TEAM_DISQUALIFIED +}; + +enum TournamentRunState : uint8 +{ + TOURNAMENT_RUN_PENDING = 0, + TOURNAMENT_RUN_ACTIVE, + TOURNAMENT_RUN_COMPLETED, + TOURNAMENT_RUN_VOID, + TOURNAMENT_RUN_REJECTED +}; + +enum TournamentRunEventType : uint8 +{ + TOURNAMENT_EVENT_ENTER = 0, + TOURNAMENT_EVENT_COMBAT_START, + TOURNAMENT_EVENT_BOSS_KILL, + TOURNAMENT_EVENT_WIPE, + TOURNAMENT_EVENT_GEAR_VIOLATION, + TOURNAMENT_EVENT_FINISH, + TOURNAMENT_EVENT_VOID, + TOURNAMENT_EVENT_REJECTED +}; + +uint8 constexpr TOURNAMENT_TEAM_SIZE = 5; +uint8 constexpr TOURNAMENT_DUNGEON_NUM = 5; + +struct TournamentMember +{ + ObjectGuid::LowType charGuid = 0; + uint32 accountId = 0; + TournamentRole role = TOURNAMENT_ROLE_DPS; +}; + +struct TournamentTeam +{ + uint32 id = 0; + uint32 tournamentId = 0; + std::string name; + TournamentTeamStatus status = TOURNAMENT_TEAM_ACTIVE; + std::string dqReason; + std::vector members; + + TournamentMember const* GetMember(ObjectGuid::LowType guid) const; + bool HasRoleComposition() const; // exactly 1 tank, 1 healer, 3 dps +}; + +struct TournamentDungeon +{ + uint8 slot = 0; + uint16 mapId = 0; + uint8 difficulty = 0; + bool revealed = false; +}; + +struct TournamentData +{ + uint32 id = 0; + std::string name; + TournamentState state = TOURNAMENT_STATE_DRAFT; + uint8 difficulty = 0; + uint16 ilvlCap = 213; + uint32 startTime = 0; + uint32 endTime = 0; + ObjectGuid::LowType createdBy = 0; + std::unordered_map dungeons; // slot -> dungeon + + TournamentDungeon const* GetDungeonByMap(uint16 mapId, uint8 difficulty) const; +}; + +struct TournamentRun +{ + uint32 id = 0; + uint32 teamId = 0; + uint8 dungeonSlot = 0; + uint16 mapId = 0; + uint32 instanceId = 0; + TournamentRunState state = TOURNAMENT_RUN_PENDING; + uint32 combatStart = 0; // unix time, first player in combat + uint32 bossFinish = 0; // unix time, final boss dead with all encounters done + uint32 durationMs = 0; // millisecond-precise run time, measured on server uptime + std::string rejectReason; + uint32 combatStartMSTime = 0; // transient: GetGameTimeMS() stamp backing durationMs + bool finalizing = false; // transient: final boss credited, completion pending its DONE state +}; + +struct TournamentStanding +{ + uint32 teamId = 0; + uint32 points = 0; + uint64 totalTimeMs = 0; + uint8 completedSlots = 0; + std::unordered_map bestSlotTimeMs; // slot -> best completed duration +}; + +class TC_GAME_API TournamentMgr +{ +public: + static TournamentMgr* instance(); + + void LoadFromDB(); + + // ----- read access ----- + TournamentData const* GetActiveTournament() const; // the single RUNNING/most-recent tournament + TournamentData const* GetTournament(uint32 id) const; + TournamentTeam const* GetTeam(uint32 teamId) const; + TournamentTeam const* GetTeamByMember(ObjectGuid::LowType charGuid, uint32 tournamentId) const; // membership is scoped per tournament + std::vector GetTeams(uint32 tournamentId) const; + + // ----- tournament lifecycle (admin) ----- + uint32 CreateTournament(std::string_view name, uint8 difficulty, ObjectGuid::LowType admin); + bool DeleteTournament(uint32 id); + bool SetState(uint32 id, TournamentState state); + bool SetIlvlCap(uint32 id, uint16 cap); + bool SetDungeon(uint32 id, uint8 slot, uint16 mapId, uint8 difficulty); + bool RemoveDungeon(uint32 id, uint8 slot); + void RevealDungeons(uint32 id); // flips revealed on RUNNING transition + + // ----- team / contestant (admin) ----- + uint32 CreateTeam(uint32 tournamentId, std::string_view name); + bool DeleteTeam(uint32 teamId); + bool AddMember(uint32 teamId, ObjectGuid::LowType charGuid, uint32 accountId, TournamentRole role); + bool RemoveMember(uint32 teamId, ObjectGuid::LowType charGuid); + bool DisqualifyTeam(uint32 teamId, std::string_view reason); + bool RequalifyTeam(uint32 teamId); + + // ----- contestant signalling (LFG) ----- + // the eligible team (active, 1/1/3) whose member set exactly matches the given guids, or nullptr + TournamentTeam const* MatchTeam(std::vector const& memberGuids) const; + + // ----- gear enforcement ----- + // first equipped item above the cap, or nullptr + static Item const* GetEquippedViolation(Player const* player, uint16 ilvlCap); + // offending equipped item gating the LFG teleport of an active contestant into a tournament dungeon + Item const* GetContestantEntryViolation(Player const* player, uint16 mapId, uint8 difficulty) const; + // true when the player would equip an item above the cap inside his team's live run, gates Player::CanEquipItem + bool IsContestantEquipViolation(Player const* player, Item const* item) const; + void LogEquipViolation(Player const* player, Item const* item); + + // ----- run tracking hooks ----- + // the run timer starts when the first contestant enters combat + void OnPlayerCombatStart(Player const* player); + + // ----- run lifecycle (run tracker) ----- + // terminal transitions are keyed by instance id, no-op without a live run and invalidate fetched run pointers + uint32 CreateRun(uint32 teamId, uint8 dungeonSlot, uint16 mapId, uint32 instanceId); + TournamentRun const* GetRunByInstance(uint32 instanceId) const; + void CompleteRun(uint32 instanceId); // all boss states DONE at end-boss + void RejectRun(uint32 instanceId, std::string_view why); // gear/skip/swap violation + void VoidRun(uint32 instanceId, std::string_view why); // no-fault termination + void LogEvent(uint32 runId, TournamentRunEventType type, std::string_view detail); + + // two-stage completion: the encounter credit fires before the final boss state is DONE, so it flags and SetBossState evaluates + void FlagRunFinalizing(uint32 instanceId); + bool IsRunFinalizing(uint32 instanceId) const; + + // staff verdict on a stored run (rejected/void only; completion is engine-driven) + bool SetRunVerdict(uint32 runId, TournamentRunState state, std::string_view reason, ObjectGuid::LowType staff); + + // ----- scoring ----- + // per dungeon slot the fastest completed run wins 1 point, ties broken by summed duration + void BuildStandings(uint32 tournamentId, std::vector& standings) const; + +private: + TournamentMgr() = default; + ~TournamentMgr() = default; + TournamentMgr(TournamentMgr const&) = delete; + TournamentMgr& operator=(TournamentMgr const&) = delete; + + // private helpers are lock-free, public entry points hold _lock + void SaveRun(TournamentRun const& run); + void StampCombatStart(TournamentRun& run); + TournamentData const* FindActiveTournament() const; + TournamentTeam const* FindTeamByMember(ObjectGuid::LowType charGuid, uint32 tournamentId) const; + uint32 GetEquipViolationRunId(Player const* player, Item const* item) const; // 0 when no live-run cap violation + TournamentData const* GetRunningTournamentForTeam(uint32 teamId) const; + TournamentTeam const* MatchTeamCandidate(TournamentTeam const* team, std::vector const& memberGuids) const; + void RevealDungeonsOfTournament(TournamentData& data); + void VoidLiveRunsOfTeam(uint32 teamId, std::string_view why); + void VoidLiveRunsOfSlot(uint32 tournamentId, uint8 slot, std::string_view why); + void EraseTeam(uint32 teamId); + bool TerminateRun(uint32 instanceId, TournamentRunState state, std::string_view why); // true if a live run was terminated + + std::unordered_map _tournaments; // id -> tournament + std::unordered_map _teams; // teamId -> team + std::unordered_multimap _memberIndex; // charGuid -> teamIds (unique per tournament, not globally) + std::unordered_map _runsByInstance; // instanceId -> active run + mutable std::shared_mutex _lock; // guards all manager state, hooks run on map-updater threads while admin commands run on the world thread + + uint32 _nextTournamentId = 1; + uint32 _nextTeamId = 1; + uint32 _nextRunId = 1; +}; + +#define sTournamentMgr TournamentMgr::instance() + +#endif // EG_TOURNAMENT_MGR_H diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index 68209c85..d63f2622 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -16,6 +16,7 @@ */ #include "LFGMgr.h" +#include "Chat.h" #include "Common.h" #include "DatabaseEnv.h" #include "DBCStores.h" @@ -26,6 +27,7 @@ #include "GroupMgr.h" #include "InstanceSaveMgr.h" #include "InstanceScript.h" +#include "Item.h" #include "LFGGroupData.h" #include "LFGPlayerData.h" #include "LFGRandomReward.h" @@ -41,6 +43,7 @@ #include "RBAC.h" #include "SharedDefines.h" #include "SocialMgr.h" +#include "TournamentMgr.h" #include "World.h" #include "WorldSession.h" @@ -1496,6 +1499,14 @@ void LFGMgr::TeleportPlayer(Player* player, bool out, bool fromOpcode /*= false* return; } + // EG - PvE tournament: contestants may not enter a tournament dungeon with equipped items above the cap + if (Item const* violation = sTournamentMgr->GetContestantEntryViolation(player, dungeon->map, uint8(dungeon->difficulty))) + { + ChatHandler(player->GetSession()).PSendSysMessage("Tournament: equipped item '%s' exceeds the allowed item level.", violation->GetTemplate()->Name1.c_str()); + player->GetSession()->SendLfgTeleportError(uint8(LFG_TELEPORTERROR_INVALID_LOCATION)); + return; + } + LfgTeleportError error = LFG_TELEPORTERROR_OK; if (!player->IsAlive()) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 0f828745..3f55fbe3 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -102,6 +102,7 @@ #include "StringConvert.h" #include "TalentPackets.h" #include "TicketMgr.h" +#include "TournamentMgr.h" #include "TradeData.h" #include "Trainer.h" #include "Transport.h" @@ -11145,6 +11146,13 @@ InventoryResult Player::CanEquipItem(uint8 slot, uint16 &dest, Item* pItem, bool // check this only in game if (not_loading) { + // EG - PvE tournament: contestants may not equip items above the cap during a live run + if (sTournamentMgr->IsContestantEquipViolation(this, pItem)) + { + sTournamentMgr->LogEquipViolation(this, pItem); + return EQUIP_ERR_CLIENT_LOCKED_OUT; + } + // May be here should be more stronger checks; STUNNED checked // ROOT, CONFUSED, DISTRACTED, FLEEING this needs to be checked. if (HasUnitState(UNIT_STATE_STUNNED)) @@ -24312,6 +24320,12 @@ void Player::ProcessTerrainStatusUpdate(ZLiquidStatus oldLiquidStatus, Optional< m_MirrorTimerFlags &= ~(UNDERWATER_INWATER | UNDERWATER_INLAVA | UNDERWATER_INSLIME | UNDERWATER_INDARKWATER); } +void Player::AtEnterCombat() +{ + Unit::AtEnterCombat(); + sTournamentMgr->OnPlayerCombatStart(this); +} + void Player::AtExitCombat() { Unit::AtExitCombat(); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index d8f54203..eda41eae 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1787,6 +1787,7 @@ class TC_GAME_API Player : public Unit, public GridObject bool UpdatePosition(float x, float y, float z, float orientation, bool teleport = false) override; bool UpdatePosition(Position const& pos, bool teleport = false) override { return UpdatePosition(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), pos.GetOrientation(), teleport); } void ProcessTerrainStatusUpdate(ZLiquidStatus oldLiquidStatus, Optional const& newLiquidData) override; + void AtEnterCombat() override; void AtExitCombat() override; void SendMessageToSet(WorldPacket const* data, bool self) const override { SendMessageToSetInRange(data, GetVisibilityRange(), self); } diff --git a/src/server/game/Instances/InstanceScript.cpp b/src/server/game/Instances/InstanceScript.cpp index 24aa07e8..37b0fc79 100644 --- a/src/server/game/Instances/InstanceScript.cpp +++ b/src/server/game/Instances/InstanceScript.cpp @@ -34,6 +34,7 @@ #include "RBAC.h" #include "ScriptMgr.h" #include "ScriptReloadMgr.h" +#include "TournamentMgr.h" #include "World.h" #include "WorldSession.h" #include @@ -368,6 +369,26 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) bossInfo->state = state; SaveToDB(); + + // EG - PvE tournament: final boss state settled, complete only if every encounter is DONE, reject otherwise + if (state == DONE && sTournamentMgr->IsRunFinalizing(instance->GetInstanceId())) + { + bool allEncountersDone = true; + for (uint32 i = 0; i < GetEncounterCount(); ++i) + if (GetBossState(i) != DONE) + { + allEncountersDone = false; + break; + } + + if (allEncountersDone) + sTournamentMgr->CompleteRun(instance->GetInstanceId()); + else + sTournamentMgr->RejectRun(instance->GetInstanceId(), "final boss killed before all encounters were cleared"); + } + // EG - PvE tournament: an encounter resetting while the run finalizes means a boss outlived the final one, reject + else if (state != IN_PROGRESS && sTournamentMgr->IsRunFinalizing(instance->GetInstanceId())) + sTournamentMgr->RejectRun(instance->GetInstanceId(), "an encounter was still in progress when the final boss died"); } for (uint32 type = 0; type < MAX_DOOR_TYPES; ++type) @@ -761,6 +782,28 @@ void InstanceScript::UpdateEncounterState(EncounterCreditType type, uint32 credi if (dungeonId) { + // EG - PvE tournament: KILL credits fire before the final boss state is DONE (JustDied runs later), flag only and let SetBossState evaluate + sTournamentMgr->FlagRunFinalizing(instance->GetInstanceId()); + + // EG - PvE tournament: CAST credits can instead fire after the states already settled, evaluate in place then + if (sTournamentMgr->IsRunFinalizing(instance->GetInstanceId())) + { + bool allEncountersDone = true; + bool anyInProgress = false; + for (uint32 i = 0; i < GetEncounterCount(); ++i) + { + if (GetBossState(i) == IN_PROGRESS) + anyInProgress = true; + if (GetBossState(i) != DONE) + allEncountersDone = false; + } + + if (allEncountersDone) + sTournamentMgr->CompleteRun(instance->GetInstanceId()); + else if (!anyInProgress) + sTournamentMgr->RejectRun(instance->GetInstanceId(), "final boss killed before all encounters were cleared"); + } + Map::PlayerList const& players = instance->GetPlayers(); for (auto const& ref : players) { diff --git a/src/server/game/Maps/MapInstanced.cpp b/src/server/game/Maps/MapInstanced.cpp index 6d1f193c..3955a80b 100644 --- a/src/server/game/Maps/MapInstanced.cpp +++ b/src/server/game/Maps/MapInstanced.cpp @@ -26,6 +26,7 @@ #include "ObjectMgr.h" #include "Player.h" #include "ScriptMgr.h" +#include "TournamentMgr.h" #include "VMapFactory.h" #include "VMapManager2.h" #include "World.h" @@ -99,6 +100,10 @@ void MapInstanced::UnloadAll() { i->second->UnloadAll(); + // EG - PvE tournament + if (i->second->IsDungeon()) + sTournamentMgr->VoidRun(i->second->GetInstanceId(), "instance unloaded"); + sScriptMgr->OnDestroyMap(i->second.get()); } @@ -296,6 +301,10 @@ bool MapInstanced::DestroyInstance(InstancedMaps::iterator &itr) Map::UnloadAll(); } + // EG - PvE tournament + if (itr->second->IsDungeon()) + sTournamentMgr->VoidRun(itr->second->GetInstanceId(), "instance destroyed"); + sScriptMgr->OnDestroyMap(itr->second.get()); // Free up the instance id and allow it to be reused diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 59dee217..5fd40ada 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -25,6 +25,7 @@ #include "AddonMgr.h" #include "Arena1v1Mgr.h" #include "ArenaTeamMgr.h" +#include "TournamentMgr.h" #include "AuctionHouseBot.h" #include "AuctionHouseMgr.h" #include "BattlefieldMgr.h" @@ -2104,6 +2105,10 @@ void World::SetInitialWorldSettings() TC_LOG_INFO("server.loading", "Loading 1v1 Arena stats..."); sArena1v1Mgr->LoadFromDB(); + // EG - PvE tournament + TC_LOG_INFO("server.loading", "Loading PvE Tournament..."); + sTournamentMgr->LoadFromDB(); + TC_LOG_INFO("server.loading", "Loading Groups..."); sGroupMgr->LoadGroups(); diff --git a/src/server/scripts/Custom/EG_tournament_commandscript.cpp b/src/server/scripts/Custom/EG_tournament_commandscript.cpp new file mode 100644 index 00000000..b8af753b --- /dev/null +++ b/src/server/scripts/Custom/EG_tournament_commandscript.cpp @@ -0,0 +1,541 @@ +#include "CharacterCache.h" +#include "Chat.h" +#include "ChatCommand.h" +#include "DatabaseEnv.h" +#include "DBCStores.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "StringFormat.h" +#include "TournamentMgr.h" +#include "Util.h" + +using namespace Trinity::ChatCommands; + +class EG_tournament_commandscript : public CommandScript +{ +public: + EG_tournament_commandscript() : CommandScript("EG_tournament_commandscript") { } + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable tournamentDungeonCommandTable = + { + { "set", HandleDungeonSet, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "remove", HandleDungeonRemove, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + }; + + static ChatCommandTable tournamentMemberCommandTable = + { + { "add", HandleMemberAdd, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "remove", HandleMemberRemove, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + }; + + static ChatCommandTable tournamentTeamCommandTable = + { + { "create", HandleTeamCreate, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "delete", HandleTeamDelete, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "info", HandleTeamInfo, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "list", HandleTeamList, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "disqualify", HandleTeamDisqualify, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "requalify", HandleTeamRequalify, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "member", tournamentMemberCommandTable }, + }; + + static ChatCommandTable tournamentRunCommandTable = + { + { "list", HandleRunList, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "reject", HandleRunReject, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "void", HandleRunVoid, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + }; + + static ChatCommandTable tournamentCommandTable = + { + { "create", HandleCreate, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "delete", HandleDelete, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "state", HandleState, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "ilvl", HandleIlvl, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "status", HandleStatus, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "standings", HandleStandings, rbac::RBAC_PERM_COMMAND_TOURNAMENT, Console::Yes }, + { "dungeon", tournamentDungeonCommandTable }, + { "team", tournamentTeamCommandTable }, + { "run", tournamentRunCommandTable }, + }; + + static ChatCommandTable commandTable = + { + { "tournament", tournamentCommandTable }, + }; + + return commandTable; + } + + static char const* StateName(TournamentState state) + { + switch (state) + { + case TOURNAMENT_STATE_DRAFT: return "draft"; + case TOURNAMENT_STATE_REGISTRATION: return "registration"; + case TOURNAMENT_STATE_LOCKED: return "locked"; + case TOURNAMENT_STATE_RUNNING: return "running"; + case TOURNAMENT_STATE_ENDED: return "ended"; + } + return "unknown"; + } + + static char const* RoleName(TournamentRole role) + { + switch (role) + { + case TOURNAMENT_ROLE_TANK: return "tank"; + case TOURNAMENT_ROLE_HEALER: return "healer"; + case TOURNAMENT_ROLE_DPS: return "dps"; + } + return "unknown"; + } + + static char const* RunStateName(TournamentRunState state) + { + switch (state) + { + case TOURNAMENT_RUN_PENDING: return "pending"; + case TOURNAMENT_RUN_ACTIVE: return "active"; + case TOURNAMENT_RUN_COMPLETED: return "completed"; + case TOURNAMENT_RUN_VOID: return "void"; + case TOURNAMENT_RUN_REJECTED: return "rejected"; + } + return "unknown"; + } + + static std::string MapName(uint16 mapId, ChatHandler* handler) + { + if (MapEntry const* mapEntry = sMapStore.LookupEntry(mapId)) + return mapEntry->MapName[handler->GetSessionDbcLocale()]; + return Trinity::StringFormat("", mapId); + } + + static std::string FormatDuration(uint32 durationMs) + { + return Trinity::StringFormat("{}:{:02}.{:03}", durationMs / 60000, (durationMs % 60000) / 1000, durationMs % 1000); + } + + static bool ParseDifficulty(std::string_view text, uint8& difficulty) + { + if (StringEqualI(text, "normal")) + { + difficulty = 0; + return true; + } + if (StringEqualI(text, "heroic")) + { + difficulty = 1; + return true; + } + return false; + } + + // ----- tournament level ----- + + static bool HandleCreate(ChatHandler* handler, QuotedString name, Optional difficultyText) + { + uint8 difficulty = 1; + if (difficultyText && !ParseDifficulty(*difficultyText, difficulty)) + { + handler->SendSysMessage("Invalid difficulty, use: normal | heroic."); + return false; + } + + ObjectGuid::LowType const admin = handler->GetPlayer() ? handler->GetPlayer()->GetGUID().GetCounter() : 0; + uint32 const id = sTournamentMgr->CreateTournament(name, difficulty, admin); + handler->PSendSysMessage("Tournament %u '%s' created (%s), state: draft.", id, name.c_str(), difficulty ? "heroic" : "normal"); + return true; + } + + static bool HandleDelete(ChatHandler* handler, uint32 tournamentId) + { + if (!sTournamentMgr->DeleteTournament(tournamentId)) + { + handler->PSendSysMessage("Tournament %u does not exist.", tournamentId); + return false; + } + + handler->PSendSysMessage("Tournament %u deleted (teams and dungeon set included).", tournamentId); + return true; + } + + static bool HandleState(ChatHandler* handler, uint32 tournamentId, std::string_view stateText) + { + TournamentState state; + if (StringEqualI(stateText, "draft")) + state = TOURNAMENT_STATE_DRAFT; + else if (StringEqualI(stateText, "registration")) + state = TOURNAMENT_STATE_REGISTRATION; + else if (StringEqualI(stateText, "locked")) + state = TOURNAMENT_STATE_LOCKED; + else if (StringEqualI(stateText, "running")) + state = TOURNAMENT_STATE_RUNNING; + else if (StringEqualI(stateText, "ended")) + state = TOURNAMENT_STATE_ENDED; + else + { + handler->SendSysMessage("Invalid state, use: draft | registration | locked | running | ended."); + return false; + } + + if (!sTournamentMgr->SetState(tournamentId, state)) + { + handler->PSendSysMessage("Could not set tournament %u state (unknown tournament, or another one is already running).", tournamentId); + return false; + } + + handler->PSendSysMessage("Tournament %u state set to %s.", tournamentId, StateName(state)); + if (state == TOURNAMENT_STATE_RUNNING) + handler->PSendSysMessage("Dungeon selection is now revealed."); + return true; + } + + static bool HandleIlvl(ChatHandler* handler, uint32 tournamentId, uint16 cap) + { + if (!sTournamentMgr->SetIlvlCap(tournamentId, cap)) + { + handler->PSendSysMessage("Tournament %u does not exist.", tournamentId); + return false; + } + + handler->PSendSysMessage("Tournament %u equipped item level cap set to %u.", tournamentId, cap); + return true; + } + + static bool HandleStatus(ChatHandler* handler, Optional tournamentIdArg) + { + TournamentData const* tournament = tournamentIdArg ? sTournamentMgr->GetTournament(*tournamentIdArg) : sTournamentMgr->GetActiveTournament(); + if (!tournament) + { + handler->SendSysMessage("No tournament found."); + return false; + } + + handler->PSendSysMessage("Tournament %u: '%s' [%s], difficulty: %s, ilvl cap: %u.", + tournament->id, tournament->name.c_str(), StateName(tournament->state), tournament->difficulty ? "heroic" : "normal", tournament->ilvlCap); + if (tournament->startTime) + handler->PSendSysMessage("Started: %s", TimeToTimestampStr(tournament->startTime).c_str()); + if (tournament->endTime) + handler->PSendSysMessage("Ended: %s", TimeToTimestampStr(tournament->endTime).c_str()); + + if (tournament->dungeons.empty()) + handler->SendSysMessage("No dungeons selected."); + else + for (uint8 slot = 1; slot <= TOURNAMENT_DUNGEON_NUM; ++slot) + { + auto itr = tournament->dungeons.find(slot); + if (itr == tournament->dungeons.end()) + continue; + + TournamentDungeon const& dungeon = itr->second; + handler->PSendSysMessage("Dungeon %u: %s (map %u, %s)%s", slot, MapName(dungeon.mapId, handler).c_str(), + dungeon.mapId, dungeon.difficulty ? "heroic" : "normal", dungeon.revealed ? "" : " [hidden]"); + } + + std::vector teams = sTournamentMgr->GetTeams(tournament->id); + handler->PSendSysMessage("%u team(s) registered.", uint32(teams.size())); + return true; + } + + static bool HandleStandings(ChatHandler* handler, Optional tournamentIdArg) + { + TournamentData const* tournament = tournamentIdArg ? sTournamentMgr->GetTournament(*tournamentIdArg) : sTournamentMgr->GetActiveTournament(); + if (!tournament) + { + handler->SendSysMessage("No tournament found."); + return false; + } + + std::vector standings; + sTournamentMgr->BuildStandings(tournament->id, standings); + if (standings.empty()) + { + handler->PSendSysMessage("Tournament %u has no active teams.", tournament->id); + return true; + } + + handler->PSendSysMessage("Standings for tournament %u '%s':", tournament->id, tournament->name.c_str()); + uint32 rank = 0; + for (TournamentStanding const& standing : standings) + { + TournamentTeam const* team = sTournamentMgr->GetTeam(standing.teamId); + handler->PSendSysMessage("%u. %s - %u point(s), %u/%u dungeons, total %s", ++rank, + team ? team->name.c_str() : "", standing.points, standing.completedSlots, + TOURNAMENT_DUNGEON_NUM, FormatDuration(uint32(standing.totalTimeMs)).c_str()); + } + return true; + } + + // ----- dungeon level ----- + + static bool HandleDungeonSet(ChatHandler* handler, uint32 tournamentId, uint8 slot, uint16 mapId, Optional difficultyText) + { + TournamentData const* tournament = sTournamentMgr->GetTournament(tournamentId); + if (!tournament) + { + handler->PSendSysMessage("Tournament %u does not exist.", tournamentId); + return false; + } + + MapEntry const* mapEntry = sMapStore.LookupEntry(mapId); + if (!mapEntry || !mapEntry->IsNonRaidDungeon()) + { + handler->PSendSysMessage("Map %u is not a 5-player dungeon.", mapId); + return false; + } + + uint8 difficulty = tournament->difficulty; + if (difficultyText && !ParseDifficulty(*difficultyText, difficulty)) + { + handler->SendSysMessage("Invalid difficulty, use: normal | heroic."); + return false; + } + + if (!sTournamentMgr->SetDungeon(tournamentId, slot, mapId, difficulty)) + { + handler->PSendSysMessage("Could not set dungeon slot %u (valid slots: 1-%u, no duplicate dungeons).", slot, TOURNAMENT_DUNGEON_NUM); + return false; + } + + handler->PSendSysMessage("Tournament %u dungeon %u set to %s (map %u, %s).", tournamentId, slot, + MapName(mapId, handler).c_str(), mapId, difficulty ? "heroic" : "normal"); + return true; + } + + static bool HandleDungeonRemove(ChatHandler* handler, uint32 tournamentId, uint8 slot) + { + if (!sTournamentMgr->RemoveDungeon(tournamentId, slot)) + { + handler->PSendSysMessage("Tournament %u has no dungeon in slot %u.", tournamentId, slot); + return false; + } + + handler->PSendSysMessage("Tournament %u dungeon slot %u cleared.", tournamentId, slot); + return true; + } + + // ----- team level ----- + + static bool HandleTeamCreate(ChatHandler* handler, uint32 tournamentId, QuotedString name) + { + uint32 const teamId = sTournamentMgr->CreateTeam(tournamentId, name); + if (!teamId) + { + handler->PSendSysMessage("Tournament %u does not exist.", tournamentId); + return false; + } + + handler->PSendSysMessage("Team %u '%s' registered to tournament %u.", teamId, name.c_str(), tournamentId); + return true; + } + + static bool HandleTeamDelete(ChatHandler* handler, uint32 teamId) + { + if (!sTournamentMgr->DeleteTeam(teamId)) + { + handler->PSendSysMessage("Team %u does not exist.", teamId); + return false; + } + + handler->PSendSysMessage("Team %u deleted.", teamId); + return true; + } + + static bool HandleTeamInfo(ChatHandler* handler, uint32 teamId) + { + TournamentTeam const* team = sTournamentMgr->GetTeam(teamId); + if (!team) + { + handler->PSendSysMessage("Team %u does not exist.", teamId); + return false; + } + + handler->PSendSysMessage("Team %u: '%s' (tournament %u), status: %s%s%s.", team->id, team->name.c_str(), team->tournamentId, + team->status == TOURNAMENT_TEAM_ACTIVE ? "active" : "disqualified", + team->dqReason.empty() ? "" : ", reason: ", team->dqReason.c_str()); + + if (team->members.empty()) + { + handler->SendSysMessage("No members."); + return true; + } + + for (TournamentMember const& member : team->members) + { + std::string name; + sCharacterCache->GetCharacterNameByGuid(ObjectGuid::Create(member.charGuid), name); + handler->PSendSysMessage("- %s (guid %u, account %u): %s", name.empty() ? "" : name.c_str(), + member.charGuid, member.accountId, RoleName(member.role)); + } + + if (!team->HasRoleComposition()) + handler->SendSysMessage("WARNING: team does not meet the 1 tank / 1 healer / 3 dps composition yet."); + return true; + } + + static bool HandleTeamList(ChatHandler* handler, Optional tournamentIdArg) + { + TournamentData const* tournament = tournamentIdArg ? sTournamentMgr->GetTournament(*tournamentIdArg) : sTournamentMgr->GetActiveTournament(); + if (!tournament) + { + handler->SendSysMessage("No tournament found."); + return false; + } + + std::vector teams = sTournamentMgr->GetTeams(tournament->id); + handler->PSendSysMessage("Tournament %u '%s': %u team(s).", tournament->id, tournament->name.c_str(), uint32(teams.size())); + for (TournamentTeam const* team : teams) + handler->PSendSysMessage("%u. '%s' - %u member(s), %s", team->id, team->name.c_str(), uint32(team->members.size()), + team->status == TOURNAMENT_TEAM_ACTIVE ? "active" : "disqualified"); + return true; + } + + static bool HandleTeamDisqualify(ChatHandler* handler, uint32 teamId, Tail reason) + { + if (!sTournamentMgr->DisqualifyTeam(teamId, reason)) + { + handler->PSendSysMessage("Team %u does not exist.", teamId); + return false; + } + + handler->PSendSysMessage("Team %u disqualified: %s", teamId, std::string(reason).c_str()); + return true; + } + + static bool HandleTeamRequalify(ChatHandler* handler, uint32 teamId) + { + if (!sTournamentMgr->RequalifyTeam(teamId)) + { + handler->PSendSysMessage("Team %u does not exist.", teamId); + return false; + } + + handler->PSendSysMessage("Team %u requalified.", teamId); + return true; + } + + // ----- member level ----- + + static bool HandleMemberAdd(ChatHandler* handler, uint32 teamId, PlayerIdentifier player, std::string_view roleText) + { + TournamentRole role; + if (StringEqualI(roleText, "tank")) + role = TOURNAMENT_ROLE_TANK; + else if (StringEqualI(roleText, "healer")) + role = TOURNAMENT_ROLE_HEALER; + else if (StringEqualI(roleText, "dps")) + role = TOURNAMENT_ROLE_DPS; + else + { + handler->SendSysMessage("Invalid role, use: tank | healer | dps."); + return false; + } + + TournamentTeam const* team = sTournamentMgr->GetTeam(teamId); + if (!team) + { + handler->PSendSysMessage("Team %u does not exist.", teamId); + return false; + } + + ObjectGuid::LowType const charGuid = player.GetGUID().GetCounter(); + if (TournamentTeam const* existing = sTournamentMgr->GetTeamByMember(charGuid, team->tournamentId)) + { + handler->PSendSysMessage("%s already belongs to team %u '%s' of this tournament.", player.GetName().c_str(), existing->id, existing->name.c_str()); + return false; + } + + uint32 const accountId = sCharacterCache->GetCharacterAccountIdByGuid(player.GetGUID()); + if (!sTournamentMgr->AddMember(teamId, charGuid, accountId, role)) + { + handler->PSendSysMessage("Could not add %s to team %u (team full or character already registered).", player.GetName().c_str(), teamId); + return false; + } + + handler->PSendSysMessage("%s added to team %u as %s.", player.GetName().c_str(), teamId, RoleName(role)); + return true; + } + + static bool HandleMemberRemove(ChatHandler* handler, uint32 teamId, PlayerIdentifier player) + { + if (!sTournamentMgr->RemoveMember(teamId, player.GetGUID().GetCounter())) + { + handler->PSendSysMessage("%s is not a member of team %u.", player.GetName().c_str(), teamId); + return false; + } + + handler->PSendSysMessage("%s removed from team %u.", player.GetName().c_str(), teamId); + return true; + } + + // ----- run level ----- + + static bool HandleRunList(ChatHandler* handler, uint32 teamId) + { + TournamentTeam const* team = sTournamentMgr->GetTeam(teamId); + if (!team) + { + handler->PSendSysMessage("Team %u does not exist.", teamId); + return false; + } + + uint32 count = 0; + if (PreparedQueryResult result = CharacterDatabase.Query(CharacterDatabase.GetPreparedStatement(CHAR_SEL_TOURNAMENT_RUN_ALL))) + { + do + { + Field* fields = result->Fetch(); + if (fields[1].GetUInt32() != teamId) + continue; + + ++count; + TournamentRunState state = TournamentRunState(fields[5].GetUInt8()); + std::string line = Trinity::StringFormat("Run {}: dungeon {} (map {}), {}", fields[0].GetUInt32(), + fields[2].GetUInt8(), fields[3].GetUInt16(), RunStateName(state)); + if (state == TOURNAMENT_RUN_COMPLETED) + line += Trinity::StringFormat(", time {}", FormatDuration(fields[8].GetUInt32())); + std::string reason = fields[9].GetString(); + if (!reason.empty()) + line += Trinity::StringFormat(", reason: {}", reason); + handler->SendSysMessage(line.c_str()); + } while (result->NextRow()); + } + + handler->PSendSysMessage("%u run(s) for team %u '%s'.", count, teamId, team->name.c_str()); + return true; + } + + static bool HandleRunReject(ChatHandler* handler, uint32 runId, Tail reason) + { + ObjectGuid::LowType const staff = handler->GetPlayer() ? handler->GetPlayer()->GetGUID().GetCounter() : 0; + if (!sTournamentMgr->SetRunVerdict(runId, TOURNAMENT_RUN_REJECTED, reason, staff)) + { + handler->PSendSysMessage("Could not reject run %u.", runId); + return false; + } + + handler->PSendSysMessage("Run %u rejected: %s", runId, std::string(reason).c_str()); + return true; + } + + static bool HandleRunVoid(ChatHandler* handler, uint32 runId, Tail reason) + { + ObjectGuid::LowType const staff = handler->GetPlayer() ? handler->GetPlayer()->GetGUID().GetCounter() : 0; + if (!sTournamentMgr->SetRunVerdict(runId, TOURNAMENT_RUN_VOID, reason, staff)) + { + handler->PSendSysMessage("Could not void run %u.", runId); + return false; + } + + handler->PSendSysMessage("Run %u voided: %s", runId, std::string(reason).c_str()); + return true; + } +}; + +void AddSC_EG_tournament_commandscript() +{ + new EG_tournament_commandscript(); +} diff --git a/src/server/scripts/Custom/EG_tournament_scripts.cpp b/src/server/scripts/Custom/EG_tournament_scripts.cpp new file mode 100644 index 00000000..91abd6bb --- /dev/null +++ b/src/server/scripts/Custom/EG_tournament_scripts.cpp @@ -0,0 +1,87 @@ +#include "Group.h" +#include "Item.h" +#include "Log.h" +#include "Map.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "StringFormat.h" +#include "TournamentMgr.h" + +class EG_tournament_player_scripts : public PlayerScript +{ +public: + EG_tournament_player_scripts() : PlayerScript("EG_tournament_player_scripts") { } + + void OnMapChanged(Player* player) override + { + Map const* map = player->GetMap(); + if (!map || !map->IsDungeon()) + return; + + TournamentData const* tournament = sTournamentMgr->GetActiveTournament(); + if (!tournament || tournament->state != TOURNAMENT_STATE_RUNNING) + return; + + TournamentDungeon const* dungeon = tournament->GetDungeonByMap(uint16(map->GetId()), uint8(map->GetDifficulty())); + if (!dungeon || !dungeon->revealed) + return; + + // an already signaled run: enforce roster integrity and the gear backstop + if (TournamentRun const* run = sTournamentMgr->GetRunByInstance(map->GetInstanceId())) + { + if (player->IsGameMaster()) + return; + + TournamentTeam const* team = sTournamentMgr->GetTeam(run->teamId); + if (team && !team->GetMember(player->GetGUID().GetCounter())) + { + sTournamentMgr->RejectRun(map->GetInstanceId(), Trinity::StringFormat("non-registered character {} entered the instance", player->GetName())); + return; + } + + EnforceGearOnEntry(*run, player, tournament->ilvlCap); + return; + } + + Group* group = player->GetGroup(); + if (!group || !group->isLFGGroup() || group->GetMembersCount() != TOURNAMENT_TEAM_SIZE) + return; + + std::vector memberGuids; + memberGuids.reserve(TOURNAMENT_TEAM_SIZE); + for (Group::MemberSlot const& slot : group->GetMemberSlots()) + memberGuids.push_back(slot.guid.GetCounter()); + + TournamentTeam const* team = sTournamentMgr->MatchTeam(memberGuids); + if (!team || team->tournamentId != tournament->id) + return; + + uint32 const runId = sTournamentMgr->CreateRun(team->id, dungeon->slot, uint16(map->GetId()), map->GetInstanceId()); + if (!runId) + return; + + if (TournamentRun const* run = sTournamentMgr->GetRunByInstance(map->GetInstanceId())) + EnforceGearOnEntry(*run, player, tournament->ilvlCap); + + TC_LOG_INFO("tournament", "Team {} '{}' signaled as contestant (run {}, map {}, instance {})", + team->id, team->name, runId, map->GetId(), map->GetInstanceId()); + } + +private: + static void EnforceGearOnEntry(TournamentRun const& run, Player* player, uint16 ilvlCap) + { + Item const* violation = TournamentMgr::GetEquippedViolation(player, ilvlCap); + if (!violation) + return; + + std::string const detail = Trinity::StringFormat("{} entered with item {} (ilvl {})", + player->GetName(), violation->GetEntry(), violation->GetTemplate()->GetBaseItemLevel()); + sTournamentMgr->LogEvent(run.id, TOURNAMENT_EVENT_GEAR_VIOLATION, detail); + sTournamentMgr->RejectRun(run.instanceId, detail); + } +}; + +void AddSC_EG_tournament_scripts() +{ + new EG_tournament_player_scripts(); +} diff --git a/src/server/scripts/Custom/custom_script_loader.cpp b/src/server/scripts/Custom/custom_script_loader.cpp index 3e63bd4e..488a35b8 100644 --- a/src/server/scripts/Custom/custom_script_loader.cpp +++ b/src/server/scripts/Custom/custom_script_loader.cpp @@ -33,6 +33,8 @@ void AddSC_EG_areatrigger_scripts(); void AddSC_EG_commandscript(); void AddSC_EG_go_scripts(); void AddSC_EG_player_scripts(); +void AddSC_EG_tournament_commandscript(); +void AddSC_EG_tournament_scripts(); void AddCustomScripts() @@ -54,4 +56,6 @@ void AddCustomScripts() AddSC_EG_commandscript(); AddSC_EG_go_scripts(); AddSC_EG_player_scripts(); + AddSC_EG_tournament_commandscript(); + AddSC_EG_tournament_scripts(); }