From 192149c989aa0290d2803b9a9971b44e6ed45372 Mon Sep 17 00:00:00 2001 From: Beu Date: Wed, 15 Jun 2022 21:55:09 +0200 Subject: [PATCH] Add KnockoutDelayedCountdown gamemode --- TM_KnockoutDelayedCountdown.Script.txt | 1143 ++++++++++++++++++++++++ 1 file changed, 1143 insertions(+) create mode 100644 TM_KnockoutDelayedCountdown.Script.txt diff --git a/TM_KnockoutDelayedCountdown.Script.txt b/TM_KnockoutDelayedCountdown.Script.txt new file mode 100644 index 0000000..a4c06ce --- /dev/null +++ b/TM_KnockoutDelayedCountdown.Script.txt @@ -0,0 +1,1143 @@ +/** + * Official Knockout mode with delayed countdown (Finish Timeout) + */ +#Extends "Libs/Nadeo/TMNext/TrackMania/Modes/TMNextRoundsBase.Script.txt" + +#Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" +#Const Version "2022-04-04" +#Const ScriptName "Modes/TrackMania/TM_Knockout_Online.Script.txt" + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Libraries +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Include "TextLib" as TL +#Include "MathLib" as ML +#Include "Libs/Nadeo/CommonLibs/Common/Semver.Script.txt" as Semver +#Include "Libs/Nadeo/ModeLibs/Common/Utils.Script.txt" as ModeUtils +#Include "Libs/Nadeo/TMNext/TrackMania/Modes/Knockout/StateManager.Script.txt" as StateMgr +#Include "Libs/Nadeo/TMNext/TrackMania/Menu/Constants.Script.txt" as MenuConsts +#Include "ManiaApps/Nadeo/TMNext/TrackMania/Knockout/UIModules/KnockoutInfo_Server.Script.txt" as UIModules_KnockoutInfo +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable +#Include "ManiaApps/Nadeo/TMNext/TrackMania/TimeAttack/UIModules/BestRaceViewer_Server.Script.txt" as UIModules_BestRaceViewer +#Include "ManiaApps/Nadeo/TMNext/TrackMania/Knockout/UIModules/KnockedOutPlayers_Server.Script.txt" as UIModules_KnockedOutPlayers +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage +#Include "ManiaApps/Nadeo/TMNext/TrackMania/Knockout/UIModules/KnockoutReward_Server.Script.txt" as UIModules_KnockoutReward +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Settings +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Setting S_FinishTimeout 5 as _("Finish timeout") +#Setting S_RoundsPerMap -1 as _("Number of rounds per map") ///< Number of round to play on one map before going to the next one +#Setting S_WarmUpNb 0 as _("Number of warm up") +#Setting S_WarmUpDuration 0 as _("Duration of one warm up") +#Setting S_WarmUpTimeout -1 as _("Warm up timeout") +#Setting S_ChatTime 6 +//L16N [Knockout] Setting for the KO mode, for exemple if the setting is "4,16,16" it means : "4 players eliminated per round until 16 players are left (16 is written twice, so two extra players are eliminated), then 2 players eliminated per round until 4 players are left (one extra player), then 1 player eliminated per round (default)". +#Setting S_EliminatedPlayersNbRanks "4,16,16" as _("Nb of players above which one extra elim. /round") +#Setting S_RoundsWithoutElimination 1 as _("Rounds without elimination") +#Setting S_EarlyEndMatchCallback True as "" +#Setting S_MatchPosition -1 as "" // Use to display player's final rank in CotD + +#Setting S_FinishTimeoutPercent 100 as "Percent of finisher to start finish timeout" +#Setting S_FinishTimeoutPercentOnSurvivors True as "Apply percentage only on survivors" + +/* About S_EliminatedPlayersNbRanks + * Example : "8,16" + * 1 to 8 players -> 1 elimination per round + * 9 to 16 players -> 2 eliminations per round + * 17 or more players -> 3 eliminations per round + * + * Example : "8,16,16" + * 1 to 8 players -> 1 eliminations per round + * 9 to 16 players -> 2 eliminations per round + * 17 or more players -> 4 eliminations per round + * + * Example : "0,8" + * 1 to 8 players -> 2 eliminations per round + * 9 or more players -> 3 eliminations per round + * + * Example : "" + * 1 or more players -> 1 elimination per round + */ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Constants +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Const C_ModeName "Knockout" +//L16N [Knockout] Description of the mode +#Const Description _("$zIn $<$t$6F9Knockout$> mode, the goal is to be the last player standing. \n\nYou play a series of races as in Round mode. $<$t$6F9At the end of each race, the last players are eliminated$>!\n\nThe winner is the player who eliminates all of their opponents.") + +#Const C_HudModulePath "" //< Path to the hud module +#Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/TMNext/TrackMania/Knockout/Knockout.Script.txt" //< Url of the mania app +#Const C_FakeUsersNb 0 + +#Const C_Callback_Elimination "Trackmania.Knockout.Elimination" + +// [Knockout] Time remaining before the first round begins (in seconds). %1 is a marker to apply typography. %2 is a digit. e.g. "Next round in 3s" +#Const C_Text_NextRoundTimer _("%1Next round in %2s") + +#Const C_UploadRecord True +#Const C_DisplayRecordGhost False +#Const C_DisplayRecordMedal False +#Const C_CelebrateRecordGhost True +#Const C_CelebrateRecordMedal True + +#Struct K_MatchInfo { + Boolean RegistrationClosed; + Integer[Text] PlayerRanks; // from 1 to ParticipantsNb, 0 means still alive, -1 means player never played but mode tried to eliminate them + Integer KOPlayersNb; + Integer ParticipantsNb; +} + +#Struct K_Callback_Elimination { + Text[] accountids; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Extends +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +***Match_LogVersions*** +*** +Log::RegisterScript(ScriptName, Version); +Log::RegisterScript(Semver::ScriptName, Semver::Version); +Log::RegisterScript(ModeUtils::ScriptName, ModeUtils::Version); +Log::RegisterScript(StateMgr::ScriptName, StateMgr::Version); +*** + +***Match_LoadLibraries*** +*** +// Not used for now but can be useful for players +XmlRpc::RegisterCallback(C_Callback_Elimination, """ +* Name: {{{C_Callback_Elimination}}} +* Type: CallbackArray +* Description: Callback sent at the end of each round with the accountid of eliminated players. +* Data: + - Version >=2.0.0: + ``` + [ + "{ + "accountids": [ + { + "45b9cf1e-3c97-4753-ac63-ac61b48b4bb7" + } + ] + }" + ] + ``` +"""); + +StateMgr::Load(); +*** + +***Match_UnloadLibraries*** +*** +StateMgr::Unload(); + +XmlRpc::UnregisterCallback(C_Callback_Elimination); +*** + +***Match_Settings*** +*** +MB_Settings_UseDefaultHud = (C_HudModulePath == ""); +MB_Settings_UseDefaultPodiumSequence = False; +*** + +***Match_Rules*** +*** +ModeInfo::SetName(C_ModeName); +ModeInfo::SetType(ModeInfo::C_Type_FreeForAll); +ModeInfo::SetRules(Description); +ModeInfo::SetStatusMessage(""); +*** + +***Match_LoadHud*** +*** +if (C_HudModulePath != "") Hud_Load(C_HudModulePath); +*** + +***Match_AfterLoadHud*** +*** +ClientManiaAppUrl = C_ManiaAppUrl; +Race::SortScores(Race::C_Sort_TotalPoints); +UIModules_TimeGap::SetTimeGapMode(UIModules_TimeGap::C_TimeGapMode_CurRace); +UIModules_PauseMenu_Online::SetHelp(Description); +UIModules_Checkpoint::SetVisibilityTimeDiff(False); +UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_CurrentRace); +*** + +***Match_Yield*** +*** +foreach (Event in PendingEvents) { + switch (Event.Type) { + // Initialize players when they join the server + case CSmModeEvent::EType::OnPlayerAdded: { + StateMgr::InitializePlayer(Event.Player); + CarRank::InitializePlayer(Event.Player); + } + } +} + +// Manage XmlRpc events +foreach (Event in XmlRpc.PendingEvents) { + if (Event.Type == CXmlRpcEvent::EType::CallbackArray) { + if (Event.ParamArray1 == "Club.Match.Start") { //@TODO @QG + declare Boolean Match_SkipWarmup for This = False; + Match_SkipWarmup = True; + } else if (Event.ParamArray1 == "Club.Match.Completed") { //@TODO @QG + declare Boolean Match_SkipWarmup for This = False; + Match_SkipWarmup = False; + } + } +} + +StateMgr::Yield(); +*** + +***Match_StartServer*** +*** +// Initialize mode +Clans::SetClansNb(0); +Scores::SaveInScore(Scores::C_Points_Match); +StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); +WarmUp::SetAvailability(True); + +Race::SetupRecord( + MenuConsts::C_ScopeType_Season, + MenuConsts::C_ScopeType_PersonalBest, + MenuConsts::C_GameMode_Knockout, + "", + C_UploadRecord, + C_DisplayRecordGhost, + C_DisplayRecordMedal, + C_CelebrateRecordGhost, + C_CelebrateRecordMedal +); +Race::UseAutomaticDossardColor(False); +*** + +***Match_InitMatch*** +*** +declare Integer Match_CurrentRoundNb; +declare Boolean Match_MatchIsOver; +declare Text[] Match_WinnersLogins; +declare Text[] Match_WinnersAccountIds; +declare Boolean Match_SkipWarmup for This = False; // Must survive CServerPlugin.RestartMap() +*** + +***Match_StartMatch*** +*** +Match_CurrentRoundNb = 0; +Match_WinnersLogins = []; +Match_WinnersAccountIds = []; +declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; +Server_MatchInfo = K_MatchInfo {}; + +OpenNewRegistrations(); +foreach(Player in Players) { + if (Player != Null) RegisterPlayer(Player); +} +UpdateCustomRanking(); +*** + +***Match_InitMap*** +*** +declare Boolean Map_DisplayCustom321Go; + +UpdateScoresTableFooter(Match_CurrentRoundNb, GetTotalRoundNb(Match_CurrentRoundNb, GetAlivePlayers())); +UpdateCustomRanking(); +UIModules_KnockoutInfo::UpdateLiveRanking(); +UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_PrevTime); +*** + +***Match_StartMap*** +*** +Map_DisplayCustom321Go = True; + +// Add bot when necessary +//Users_SetNbFakeUsers(C_FakeUsersNb, 0); + +CarRank::Reset(); + +// Warm up +Race::SortScores(Race::C_Sort_BestRaceTime); +UIModules_ScoresTable::SetCustomRanks([]); +UIModules_ScoresTable::DisplayOnly([]); +UIModules_ScoresTable::SetCustomPoints([]); +if (!Match_SkipWarmup) { // Warmup only before daily match starts + MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); +} + +UpdateCustomRanking(); +*** + +***Match_InitRound*** +*** +declare Text[] Round_EliminatedPlayers = []; // accountids +declare Integer Round_EliminatedPlayersNb; +declare Boolean NeedInfoDisplayUpdate; +declare Integer NBFinished = 0; +declare Integer MinimumFinishers = 0; +*** + +***Rounds_CanSpawn*** +*** +// Register new players while registration are open +if (RegistrationsAreOpen()) { + foreach(Player in Players) { + if (!PlayerIsRegistered(Player.User.WebServicesUserId)) RegisterPlayer(Player); + } +} + +Match_CurrentRoundNb += 1; +declare Integer AlivePlayersNb = GetAlivePlayers(); +Round_EliminatedPlayersNb = GetEliminationsNb(AlivePlayersNb, Match_CurrentRoundNb); +if (Round_EliminatedPlayersNb <= 0) { + CarRank::ResetRanksColors(); +} else { + CarRank::SetRanksColors([ + 1 => Race::C_DossardColor_Default, + AlivePlayersNb - Round_EliminatedPlayersNb + 1 => <0.7, 0., 0.> + ]); +} + +UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers()); + +declare ObjectiveMessage = ""; +if (Round_EliminatedPlayersNb > 1) { + //L16N [Knockout] Announce the number of players that will be eliminated at the end of the round. %1 will be replaced by a number greater than 1. eg: "2 players will be eliminated". + ObjectiveMessage = TL::Compose(_("%1 players will be eliminated"), "$t$i$fff"^TL::ToText(Round_EliminatedPlayersNb)); +} else if (Round_EliminatedPlayersNb == 1){ + //L16N [Knockout] Announce the number of players that will be eliminated at the end of the round. %1 will be replaced by the digit 1. eg: "1 player will be eliminated". + ObjectiveMessage = TL::Compose(_("%1 player will be eliminated"), "$t$i$fff"^TL::ToText(Round_EliminatedPlayersNb)); +} else { + //L16N [Knockout] Announce the number of players that will be eliminated at the end of the round. %1 will be replaced by a marker for Typography + ObjectiveMessage = TL::Compose(_("%1No elimination this round"), "$t$i$fff"); +} +UIModules_BigMessage::SetMessage(ObjectiveMessage, 5500); +UIModules_BigMessage::SetOffset(<0., -40.>); + +if (Map_DisplayCustom321Go) { + UIManager.UIAll.BigMessageSound = CUIConfig::EUISound::Default; + UIManager.UIAll.BigMessageSoundVariant = 0; + MB_Sleep(1000); + UIManager.UIAll.BigMessage = TL::Compose(C_Text_NextRoundTimer, "$i$t", "3"); + MB_Sleep(1000); + UIManager.UIAll.BigMessage = TL::Compose(C_Text_NextRoundTimer, "$i$t", "2"); + MB_Sleep(1000); + UIManager.UIAll.BigMessage = TL::Compose(C_Text_NextRoundTimer, "$i$t", "1"); + MB_Sleep(1000); + UIManager.UIAll.BigMessage = ""; + StartTime = Now + Race::C_SpawnDuration; +} + +// Reset spawn permissions for players and spectators according to Rounds rules +foreach (Score in Scores) { + if (Score == Null) continue; + declare ModeRounds_CanSpawn for Score = True; + declare Knockout_SpawnPermissionRequested for Score = False; + Knockout_SpawnPermissionRequested = False; + if (MM_IsMatchServer()) { + declare Player <=> GetPlayer(Score.User.WebServicesUserId); + ModeRounds_CanSpawn = MM_PlayerIsAllowedToPlay(Player); + } else { + ModeRounds_CanSpawn = True; + } +} +// Check spawn permissions for players according to KO rules +foreach (Player in Players) { + if ( + Player != Null && + Player.Score != Null + ) { + declare Knockout_SpawnPermissionRequested for Player.Score = False; + if (!Knockout_SpawnPermissionRequested) { + Knockout_SpawnPermissionRequested = True; + RequestSpawnPermission(Player); + NeedInfoDisplayUpdate = True; + } + } +} + +if (NeedInfoDisplayUpdate) { + UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers()); + NeedInfoDisplayUpdate = False; +} +*** + +***Match_StartRound*** +*** +// Update UI +UIModules_BestRaceViewer::SetPrevDisplay(False); + +foreach (Score in Scores) { + SetFinishedRace(Score, False); + TagDNF(Score, False); + TagAlive(Score, PlayerIsAlive(Score.User.WebServicesUserId)); +} +UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers()); +NeedInfoDisplayUpdate = False; + +StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]); + +// Define minimum finishers +NBFinished = 0; +declare Integer Minus = 0; +if (S_FinishTimeoutPercentOnSurvivors) { + Minus = Round_EliminatedPlayersNb; +} +MinimumFinishers = ML::Max(1, ML::FloorInteger((GetAlivePlayers() - Minus) * (ML::ToReal(S_FinishTimeoutPercent) / 100))); +*** + +***Rounds_PlayerSpawned*** +*** +CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); +*** + +***Rounds_PlayLoopSpawnPlayers*** +*** +// Spawn allowed players +foreach (Player in Players) { + if ( + Player != Null && + Player.Score != Null + ) { + declare Knockout_SpawnPermissionRequested for Player.Score = False; + if (!Knockout_SpawnPermissionRequested) { + Knockout_SpawnPermissionRequested = True; + RequestSpawnPermission(Player); + NeedInfoDisplayUpdate = True; + } + declare ModeRounds_CanSpawn for Player.Score = True; + if ( + Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned && + MB_RoundIsRunning() && + ModeRounds_CanSpawn && + Race::IsReadyToStart(Player) + ) { + Race::Start(Player, StartTime); + ModeRounds_CanSpawn = False; + TagDNF(Player.Score, False); + +++Rounds_PlayerSpawned+++ + } + } +} +if (NeedInfoDisplayUpdate) { + UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers()); + NeedInfoDisplayUpdate = False; +} + +*** + +***Match_PlayLoop*** +*** +foreach (Event in PendingEvents) { + if (Event.Type == CSmModeEvent::EType::OnPlayerAdded) { + if (Event.Player != Null) { + UIModules_BestRaceViewer::SetPrevDisplay(Event.Player, False); + } + } +} + +// Manage race events +declare RacePendingEvents = Race::GetPendingEvents(); +foreach (Event in RacePendingEvents) { + Race::ValidEvent(Event); + + // Waypoint + if (Event.Type == Events::C_Type_Waypoint) { + CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); + if (Event.Player != Null) { + if (Event.IsEndRace) { + Scores::UpdatePlayerPrevRace(Event.Player); + declare BetterRace = Scores::UpdatePlayerBestRaceIfBetter(Event.Player); + declare BetterLap = Scores::UpdatePlayerBestLapIfBetter(Event.Player); + + // Start the countdown if enough players finished + NBFinished += 1 ; + if (EndTime <= 0 && (MinimumFinishers <= NBFinished)) { + EndTime = GetFinishTimeout(); + } + + SetFinishedRace(Event.Player.Score, True); + UIModules_BestRaceViewer::SetPrevDisplay(Event.Player, True); + UpdateCustomRanking(); + } + if (Event.IsEndLap) { + declare Better = Scores::UpdatePlayerBestLapIfBetter(Event.Player); + } + } + UIModules_KnockoutInfo::UpdateLiveRanking(); + } + // GiveUp + if (Event.Type == Events::C_Type_GiveUp) { + if (Event.Player != Null && Event.Player.Score != Null) { + TagDNF(Event.Player.Score, True); + } + UIModules_KnockoutInfo::UpdateLiveRanking(); + } +} + +// Manage mode events +foreach (Event in PendingEvents) { + if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; + Events::Invalid(Event); +} + +// Trigger Endtime if some players DC, go as Spectator or Give Up +if (Now % 5000 == 0) { + if (EndTime <= 0 && MinimumFinishers <= GetAlivePlayers() - PlayersNbAlive) { + EndTime = GetFinishTimeout(); + } +} +*** + +***Match_EndRound*** +*** +Race::StopSkipOutroAll(); +EndTime = -1; +StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); +CarRank::Update(CarRank::C_SortCriteria_CurrentRace); + +if (Semver::Compare(XmlRpc::GetApiVersion(), ">=", "2.1.1")) { + Scores::XmlRpc_SendScores(Scores::C_Section_PreEndRound, ""); +} + +if (Round_ForceEndRound || Round_SkipPauseRound) { + // Cancel points + foreach (Score in Scores) { + Scores::SetPlayerRoundPoints(Score, 0); + } + // Do not launch the forced end round sequence after a pause + if (!Round_SkipPauseRound) { + ForcedEndRoundSequence(); + } +} else { + // Eliminate players that did not finish in time + declare Ident[] EliminatedPlayersScoresIds = []; // Score.Id + if (Match_CurrentRoundNb > S_RoundsWithoutElimination) { + // Close registrations. Players can join the game any time during no elimination rounds + if (RegistrationsAreOpen()) CloseRegistrations(); + + // Eliminate last players + Race::SortScores(Race::C_Sort_PrevRaceTime); + declare EliminationsNb = GetEliminationsNb(GetAlivePlayers(), Match_CurrentRoundNb); + declare Ident[] ReversedEliminatedPlayersScoresIds; + if (Scores.count > 0) { + for (I, 0, Scores.count-1) { + declare Score <=> Scores[Scores.count-1 - I]; + if (Score != Null) { + if (PlayerIsAlive(Score.User.WebServicesUserId)) { + if (!GetFinishedRace(Score)) { + ReversedEliminatedPlayersScoresIds.add(Score.Id); + EliminationsNb -= 1; + TagDNF(Score, True); + } else if (EliminationsNb > 0) { + ReversedEliminatedPlayersScoresIds.add(Score.Id); + EliminationsNb -= 1; + } + } + } + } + foreach (ScoreId in ReversedEliminatedPlayersScoresIds) { + EliminatedPlayersScoresIds.add(ScoreId); + if ( + Scores.existskey(ScoreId) && + Scores[ScoreId] != Null + ) { + Round_EliminatedPlayers.add(Scores[ScoreId].User.WebServicesUserId); + } + } + } + EliminatePlayers(EliminatedPlayersScoresIds); + } + + // Check if match is over + if (MatchIsOver(Match_CurrentRoundNb)) { + Match_MatchIsOver = True; + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + Match_WinnersAccountIds = []; + Match_WinnersLogins = []; + foreach (AccountId => Status in Server_MatchInfo.PlayerRanks) { + if (Status == 0) Match_WinnersAccountIds.add(AccountId); + } + if (Match_WinnersAccountIds.count > 0) { + declare Ident[] LeaderScoresIds; + foreach (Score in Scores) { + if ( + Score != Null && + Match_WinnersAccountIds.exists(Score.User.WebServicesUserId) + ) { + LeaderScoresIds.add(Score.Id); + Match_WinnersLogins.add(Score.User.Login); + } + } + EliminatePlayers(LeaderScoresIds); // Only one player will be considered as winner + } + } + + // Send Trophies, uses Round_EliminatedPlayers array + +++Match_EndRound_AfterComputeScores+++ + + UIModules_KnockoutInfo::UpdateLiveRanking(); + UpdateCustomRanking(); + UpdateScoresTableFooter(Match_CurrentRoundNb, GetTotalRoundNb(Match_CurrentRoundNb, GetAlivePlayers())); + + UIModules_KnockedOutPlayers::DisplayContent(True); + UIModules_KnockedOutPlayers::DisplayEliminatedPlayer(Round_EliminatedPlayers, GetPlayerRanks(Round_EliminatedPlayers)); + declare PagesToShow = Round_EliminatedPlayers.count/4; + if (Round_EliminatedPlayers.count%4 != 0) PagesToShow += 1; + MB_Sleep(ML::Max(1100 + 350*Round_EliminatedPlayers.count + 1600*PagesToShow + 250, 6000 + 250)); + UIModules_KnockedOutPlayers::DisplayEliminatedPlayer([], []); + UIModules_KnockedOutPlayers::DisplayContent(False); + declare K_Callback_Elimination Callback_Elimination; + foreach(AccountId in Round_EliminatedPlayers) { + Callback_Elimination.accountids.add(AccountId); + } + XmlRpc::SendCallback(C_Callback_Elimination, [Callback_Elimination.tojson()]); + foreach (ScoreId in EliminatedPlayersScoresIds) { + if ( + Scores.existskey(ScoreId) && + Scores[ScoreId] != Null + ) { + declare Player = GetPlayer(Scores[ScoreId].User.Login); + if (Player != Null) { + UIModules_KnockoutReward::SetPlayerEliminated(Player, True); + UIModules_KnockoutReward::SendResult(Player); + } + } + } + foreach (Login in Match_WinnersLogins) { + declare Player = GetPlayer(Login); + if (Player != Null) { + UIModules_KnockoutReward::SetPlayerEliminated(Player, True); // Winner + UIModules_KnockoutReward::SendResult(Player); + } + } + + if (MapIsOver(Match_CurrentRoundNb)) MB_StopMap(); +} +*** + +***Match_EndMap*** +*** +if (MatchIsOver(Match_CurrentRoundNb)) { + MB_StopMatch(); +} else { + MB_Sleep(2500); +} + +if (!MB_MapIsRunning() && MB_MatchIsRunning()) MB_SkipPodiumSequence(); + +Race::SortScores(Race::C_Sort_TotalPoints); +Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_MatchPoints)); +*** + +***Match_PodiumSequence*** +*** +if (!MB_Private_SkipPodiumSequence) { + ModeUtils::PlaySound(CUIConfig::EUISound::EndRound, 0); + + declare WinnerScore <=> Scores::GetPlayerWinner(); + if (WinnerScore != Null) { + UIModules_BigMessage::SetMessage(_("$<%1$> wins the match!"), WinnerScore.User.WebServicesUserId); + } else { + UIModules_BigMessage::SetMessage(_("|Match|Draw")); + } + + // Send the EndMatch callback sooner to speed up the API update + if (S_EarlyEndMatchCallback) { + Scores::EndMatch(); + Scores::XmlRpc_SendScores(Scores::C_Section_EndMatch, ""); // send "Trackmania.Scores" + Log::Log("Send early end match callback"); + } + + declare PrevUISequence = UIManager.UIAll.UISequence; + UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium; + MB_Private_Sleep(10000); + UIModules_BigMessage::SetMessage(""); + UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; + MB_Private_Sleep((S_ChatTime*1000)); + UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; + UIManager.UIAll.UISequence = PrevUISequence; +} +*** + +***Match_AfterPodiumSequence*** +*** +if (!MB_Private_SkipPodiumSequence) { + // End Match and delete data + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + Server_MatchInfo = K_MatchInfo {}; + foreach (Score in Scores) { + if (Score == Null) continue; + TagDNF(Score, False); + TagAlive(Score, False); + UIModules_KnockoutReward::ResetResult(Score); + } + Scores::Clear(); + UpdateScoresTableFooter(Match_CurrentRoundNb, GetTotalRoundNb(Match_CurrentRoundNb, GetAlivePlayers())); + UpdateCustomRanking(); + UIModules_KnockoutInfo::UpdateLiveRanking(); + foreach (Player in AllPlayers) { + if (Player != Null) { + UIModules_KnockoutReward::SetPlayerEliminated(Player, False); + } + } +} +*** + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Functions +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Whether the player is registered or not + * + *@return True if the player is registered + * False Otherwise + */ +Boolean PlayerIsRegistered(Text _AccountId) { + if (_AccountId == "") return False; + declare K_MatchInfo Server_MatchInfo for This; + return Server_MatchInfo.PlayerRanks.existskey(_AccountId); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Whether the player has been eliminated or not + * + *@return True if the player has been eliminated + * False Otherwise + */ +Boolean PlayerIsAlive(Text _AccountId) { + if (_AccountId == "") return False; + declare K_MatchInfo Server_MatchInfo for This; + return ( + PlayerIsRegistered(_AccountId) && + Server_MatchInfo.PlayerRanks[_AccountId] == 0 + ); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Tag a score as finished curr race or not + * + * @param _Score + * @param _IsAlive + */ +Void TagAlive(CSmScore _Score, Boolean _IsAlive) { + if (_Score == Null) return; + declare netwrite Boolean Net_Knockout_PlayerIsAlive for _Score = False; + Net_Knockout_PlayerIsAlive = _IsAlive; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Tag a score as DNF + * + * @param _Score + * @param _DNF + */ +Void TagDNF(CSmScore _Score, Boolean _DNF) { + if (_Score == Null) return; + declare netwrite Boolean Net_Knockout_DNF for _Score = False; + Net_Knockout_DNF = _DNF; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Number of players still playing + * + * @return Number of players still playing + */ +Integer GetAlivePlayers() { + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + return (Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Init player at the begining of the match + * + * @param _Player Player to init + */ +Void RegisterPlayer(CSmPlayer _Player) { + if (_Player == Null) return; + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + if (Server_MatchInfo.RegistrationClosed) return; + if ( + !Server_MatchInfo.PlayerRanks.existskey(_Player.User.WebServicesUserId) && + !Server_MatchInfo.RegistrationClosed + ) { + Server_MatchInfo.PlayerRanks[_Player.User.WebServicesUserId] = 0; + Server_MatchInfo.ParticipantsNb += 1; + UIModules_KnockoutInfo::SetAlivePlayersNb(GetAlivePlayers()); + TagAlive(_Player.Score, True); + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Players can join the match after this function is called + * + */ +Void OpenNewRegistrations() { + declare K_MatchInfo Server_MatchInfo for This; + Server_MatchInfo = K_MatchInfo { + RegistrationClosed = False, + PlayerRanks = [], + KOPlayersNb = 0, + ParticipantsNb = 0 + }; + foreach (Score in Scores) { + UIModules_KnockoutReward::ResetResult(Score); + } + foreach (Player in AllPlayers) { + if (Player != Null) { + UIModules_KnockoutReward::SetPlayerEliminated(Player, False); + } + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Whether the player can join match or not + * + *@return True if the player can join + * False Otherwise + */ +Boolean RegistrationsAreOpen() { + declare K_MatchInfo Server_MatchInfo for This; + return !Server_MatchInfo.RegistrationClosed; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Players can't join the match anymore after this function is called + * + */ +Void CloseRegistrations() { + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + Server_MatchInfo.RegistrationClosed = True; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Eliminate players and assign them their final ranks + * according to the _ScoreIds list order. 1st player is the best ranked + * + * @param _ScoreIds Score Ids of Players to Eliminate + */ +Void EliminatePlayers(Ident[] _ScoreIds) { + if (_ScoreIds.count == 0) return; + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + if (!Server_MatchInfo.RegistrationClosed) return; // Cannot eliminate players if registrations are still open + foreach (ScoreId in _ScoreIds) { + if (Scores.existskey(ScoreId)) { + declare Score <=> Scores[ScoreId]; + declare AccountId = Score.User.WebServicesUserId; + if (!PlayerIsRegistered(AccountId)) { + Server_MatchInfo.PlayerRanks[AccountId] = -1; + UIModules_KnockoutReward::SaveRank(Score, -1); + UIModules_KnockoutReward::SaveCupRank(Score, -1, -1); + } else if (PlayerIsAlive(AccountId)) { + Server_MatchInfo.KOPlayersNb += 1; + Server_MatchInfo.PlayerRanks[AccountId] = Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1; + UIModules_KnockoutReward::SaveRank(Score, Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1); + UIModules_KnockoutReward::SaveCupRank(Score, S_MatchPosition, Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1); + } + TagAlive(Score, False); + } + } +} + +Integer[] GetEliminationsMilestones() { + declare Text_Milestones = TL::Split(",", S_EliminatedPlayersNbRanks); + declare Integer[] Milestones = [1]; + foreach (Text_Milestone in Text_Milestones) { + declare Milestone = TL::ToInteger(Text_Milestone); + if (Milestone > 0) Milestones.add(Milestone); + else if (Milestone == 0) Milestones.add(1); + } + return Milestones; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Players Nb at which Emilinations Nb per round decreases + * + * @param _AlivePlayers Number of players still playing + * @return Players Nb of the milestone + */ +Integer GetNextMilestone(Integer _AlivePlayers) { + if (_AlivePlayers <= 0) return 0; + declare Integer[] Milestones = GetEliminationsMilestones(); + + declare NextMilestone = 0; + foreach (Milestone in Milestones) { + if (Milestone < _AlivePlayers) { + NextMilestone = Milestone; + } + } + return NextMilestone; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Number of players to eliminate this round + * + * @param _AlivePlayers Number of players still playing + * @return Number of eliminations this round + */ +Integer GetEliminationsNb(Integer _AlivePlayers, Integer _RoundNb) { + if (_AlivePlayers <= 1) return 0; + if (_RoundNb <= S_RoundsWithoutElimination) return 0; + declare Milestones = GetEliminationsMilestones(); + declare RoundMinEliminations = Milestones.count + 1; + foreach (Index => Milestone in Milestones) { + if (Milestone < _AlivePlayers) { + RoundMinEliminations = Index + 1; + } + } + return ML::Min(RoundMinEliminations, _AlivePlayers-1); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Estimated number of rounds + * + * @return Estimated number of rounds + */ +Integer GetTotalRoundNb(Integer _CurrentRoundNb, Integer _AlivePlayers) { + declare RoundsWithEliminationsLeft = 0; + declare AlivePlayers = _AlivePlayers; + declare Milestones = GetEliminationsMilestones(); + for (Index, 0, Milestones.count-1) { + declare ReverseIndex = Milestones.count-1 - Index; + declare Milestone = Milestones[ReverseIndex]; + if (AlivePlayers > Milestone && AlivePlayers > 1) { + declare ElimNbPerRound = ReverseIndex + 1; + declare TotalElimNb = AlivePlayers - Milestone; + if (TotalElimNb % ElimNbPerRound > 0) TotalElimNb += ElimNbPerRound - TotalElimNb % ElimNbPerRound; + RoundsWithEliminationsLeft += TotalElimNb / ElimNbPerRound; + AlivePlayers -= TotalElimNb; + } + } + + return ML::Max(_CurrentRoundNb, S_RoundsWithoutElimination + 1) + ML::Max(0, RoundsWithEliminationsLeft -1); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Whether the player finished the race or not + * + * @param _Score Player's score + * @param _HasFinished True if player finished race + */ +Void SetFinishedRace(CSmScore _Score, Boolean _HasFinished) { + declare Knockout_FinishedRace for _Score = False; + Knockout_FinishedRace = _HasFinished; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Whether the player finished the race or not + * + * @param _Score Player's score + * @return True if player finished race + */ +Boolean GetFinishedRace(CSmScore _Score) { + declare Knockout_FinishedRace for _Score = False; + return Knockout_FinishedRace; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Player rank in match + * + * @param _AccountIds Account Ids + * @return Ranks in same order + */ +Integer[] GetPlayerRanks(Text[] _AccountIds) { + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + declare Integer[] Result = []; + foreach (AccountId in _AccountIds) { + if (Server_MatchInfo.PlayerRanks.existskey(AccountId)) { + Result.add(Server_MatchInfo.PlayerRanks[AccountId]); + } else { + Result.add(-2); + } + } + return Result; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Whether the player can be spwaned or not, according to KO rules only + * + *@param _Player The Player + *@return True if the player can be spwaned + * False Otherwise + */ +Boolean RequestSpawnPermission(CSmPlayer _Player) { + if (_Player == Null || _Player.Score == Null) return False; + declare ModeRounds_CanSpawn for _Player.Score = True; + if ( + RegistrationsAreOpen() && + !PlayerIsRegistered(_Player.User.WebServicesUserId) + ) { + RegisterPlayer(_Player); + } + ModeRounds_CanSpawn = ( + ModeRounds_CanSpawn && + PlayerIsAlive(_Player.User.WebServicesUserId) + ); + return ModeRounds_CanSpawn; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Update the Scores Table with hidden custom points + * + */ +Void UpdateCustomRanking() { + declare Text[] AccountIdsToDisplay = []; + declare Text[Text] CustomRanks = []; + declare Text[][Text] CustomPoints = []; + declare Integer[Text] CustomTimes = []; + declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; + Race::SortScores(Race::C_Sort_PrevRaceTime); + declare AlivePlayers = GetAlivePlayers(); + foreach (Index => Score in Scores) { + if (Score == Null) continue; + if (PlayerIsRegistered(Score.User.WebServicesUserId)) { + AccountIdsToDisplay.add(Score.User.WebServicesUserId); + + if (PlayerIsAlive(Score.User.WebServicesUserId)) { + Scores::SetPlayerMatchPoints(Score, Scores.count + Server_MatchInfo.ParticipantsNb - Index); + CustomRanks[Score.User.WebServicesUserId] = "-"; + if (!GetFinishedRace(Score)) { + CustomTimes[Score.User.WebServicesUserId] = 0; //@ prev race is updated automatically at the moment (22/10/20) so we need to use this + } + } else { + declare FinalRank = -1; + if (Server_MatchInfo.PlayerRanks.existskey(Score.User.WebServicesUserId)) { + FinalRank = Server_MatchInfo.PlayerRanks[Score.User.WebServicesUserId]; + Scores::SetPlayerMatchPoints(Score, Server_MatchInfo.ParticipantsNb - FinalRank); + CustomRanks[Score.User.WebServicesUserId] = ""^FinalRank; + } else { + Scores::SetPlayerMatchPoints(Score, 0); + CustomRanks[Score.User.WebServicesUserId] = "-"; + } + //L16N [Knockout] Do not translate if K.O. is understandable. This is the player status. + CustomPoints[Score.User.WebServicesUserId] = [_("|Status|K.O."), "f00"]; + if (FinalRank == 1) { + //L16N [Knockout] + CustomPoints[Score.User.WebServicesUserId] = [_("|Status|Winner"), "0f0"]; + } + } + } + } + Race::SortScores(Race::C_Sort_TotalPoints); + UIModules_ScoresTable::SetCustomRanks(CustomRanks); + UIModules_ScoresTable::DisplayOnly(AccountIdsToDisplay); // Display only registered players + UIModules_ScoresTable::SetCustomPoints(CustomPoints); + UIModules_ScoresTable::SetCustomTimes(CustomTimes); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Get the time left to the players to finish the round after the first player + * + * @return The time left in ms + */ +Integer GetFinishTimeout() { + declare FinishTimeout = 0; + + if (S_FinishTimeout >= 0) { + FinishTimeout = S_FinishTimeout * 1000; + } else { + FinishTimeout = 5000; + if (Map.TMObjective_IsLapRace && Race::GetLapsNb() > 0 && Map.TMObjective_NbLaps > 0) { + FinishTimeout += ((Map.TMObjective_AuthorTime / Map.TMObjective_NbLaps) * Race::GetLapsNb()) / 6; + } else { + FinishTimeout += Map.TMObjective_AuthorTime / 6; + } + } + + return Now + FinishTimeout; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Check if we should go to the next match + * + * @param _RoundNb Current round number + * @return True if it is the case, false otherwise + */ +Boolean MatchIsOver(Integer _RoundNb) { + if (_RoundNb < S_RoundsWithoutElimination || RegistrationsAreOpen() || GetAlivePlayers() > 1) return False; + + return True; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Check if we should go to the next map + * + * @param _RoundNb Current round number + * @return True if it is the case, false otherwise + */ +Boolean MapIsOver(Integer _RoundNb) { + if (MatchIsOver(_RoundNb)) return True; + if (S_RoundsPerMap > 0 && MB_GetRoundCount() >= S_RoundsPerMap) return True; //< There is a rounds limit and it is reached + + return False; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Update the scores table footer text + * + * @param _TotalRounds The estimated number of rounds + * @param _AlivePlayers The number of Alive Players + * @param _RoundKOs The number players to eliminate this round + * @param _Milestone Next number of players at which one less player is eliminated per round + */ +Void UpdateScoresTableFooter(Integer _CurrentRoundNb, Integer _TotalRoundsNb) { + //L16N [Knockout] + declare Message = ""; + if (MatchIsOver(_CurrentRoundNb)) { + Message = _("Match is over"); + } else { + declare AlivePlayersNb = GetAlivePlayers(); + declare Milestone = GetNextMilestone(AlivePlayersNb); + declare CurrentRound = _CurrentRoundNb; + if (CurrentRound == 0) CurrentRound = 1; + declare TotalRoundsNb = _TotalRoundsNb; + if (CurrentRound > TotalRoundsNb) TotalRoundsNb = CurrentRound; + declare EliminationsNb = GetEliminationsNb(AlivePlayersNb, _CurrentRoundNb); + if (EliminationsNb == 0 || Milestone <= 1) { + if (EliminationsNb <= 0) { + if (AlivePlayersNb <= 1) { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 1 OR LESS "No K.O. this round" means that no players will be eliminated, KO stands for "Knockouts". \n is a new line. + Message = TL::Compose(_("Round %1/%2\n%3 Player racing\nNo K.O. this round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb); + } else { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 2 OR MORE "No K.O. this round" means that no players will be eliminated, KO stands for "Knockouts". \n is a new line. + Message = TL::Compose(_("Round %1/%2\n%3 Players racing\nNo K.O. this round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb); + } + } else if (EliminationsNb == 1) { + if (AlivePlayersNb <= 1) { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 1 OR LESS. "1 K.O. per round" is the number of eliminated players, KO stands for "Knockouts". \n is a new line. + Message = TL::Compose(_("Round %1/%2\n%3 Player\n1 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb); + } else { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 2 OR MORE. "1 K.O. per round" is the number of eliminated players, KO stands for "Knockouts". \n is a new line. + Message = TL::Compose(_("Round %1/%2\n%3 Players\n1 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb); + } + } else { + if (AlivePlayersNb <= 1) { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 1 OR LESS. %4 the number of eliminated players, stands for "Knockouts", always greater than 1. \n is a new line. + Message = TL::Compose(_("Round %1/%2\n%3 Player\n%4 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb, ""^EliminationsNb); + } else { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 2 OR MORE. %4 the number of eliminated players, stands for "Knockouts", always greater than 1. \n is a new line. + Message = TL::Compose(_("Round %1/%2\n%3 Players\n%4 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb, ""^EliminationsNb); + } + } + + } else { + //L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round (assumed greater than 1). %4 the number of eliminated players, stands for "Knockouts", greater than 1. %5 is the next number of players at which one less player is eliminated per round. \n is a new line. + Message = TL::Compose(_("Round %1/%2, %3 Players\n%4 K.O. per round until %5 players"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb, ""^EliminationsNb, ""^Milestone); + } + } + + UIModules_ScoresTable::SetFooterInfo(Message); +} + +Void UpdateKnockoutInfoDisplay(Integer _CurrentRoundNb, Integer _Round_EliminatedPlayersNb, Integer _AlivePlayers) { + declare TotalRoundsNb = GetTotalRoundNb(_CurrentRoundNb, _AlivePlayers); + UIModules_KnockoutInfo::SetAlivePlayersNb(_AlivePlayers); + UIModules_KnockoutInfo::SetMapRoundNb(ML::Min(S_RoundsPerMap, MB_GetRoundCount()), ML::Min(S_RoundsPerMap, TotalRoundsNb)); + UIModules_KnockoutInfo::SetRoundNb(_CurrentRoundNb, TotalRoundsNb); + UIModules_KnockoutInfo::SetKOsNumber(_Round_EliminatedPlayersNb, GetNextMilestone(_AlivePlayers)); + UIModules_KnockoutInfo::UpdateLiveRanking(); + UpdateCustomRanking(); + UpdateScoresTableFooter(_CurrentRoundNb, TotalRoundsNb); +} \ No newline at end of file