/** * Official Knockout mode with delayed countdown (Finish Timeout) */ #Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaRoundsBase.Script.txt" #Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" #Const Version "2023-10-16" #Const ScriptName "Modes/TrackMania/TM_Knockout_Online.Script.txt" // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Libraries // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Include "TextLib" as TL #Include "MathLib" as ML #Include "Libs/Nadeo/CMGame/Utils/Semver.Script.txt" as Semver #Include "Libs/Nadeo/CMGame/Modes/Utils.Script.txt" as ModeUtils #Include "Libs/Nadeo/Trackmania/Modes/Knockout/StateManager.Script.txt" as StateMgr #Include "Libs/Nadeo/Trackmania/MainMenu/Constants.Script.txt" as MenuConsts #Include "Libs/Nadeo/Trackmania/Modes/Knockout/UIModules/KnockoutInfo_Server.Script.txt" as UIModules_KnockoutInfo #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable #Include "Libs/Nadeo/Trackmania/Modes/TimeAttack/UIModules/BestRaceViewer_Server.Script.txt" as UIModules_BestRaceViewer #Include "Libs/Nadeo/Trackmania/Modes/Knockout/UIModules/KnockedOutPlayers_Server.Script.txt" as UIModules_KnockedOutPlayers #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage #Include "Libs/Nadeo/Trackmania/Modes/Knockout/UIModules/KnockoutReward_Server.Script.txt" as UIModules_KnockoutReward #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online #Include "Libs/Nadeo/TMGame/Modes/Base/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/Trackmania/Modes/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 Text 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 Boolean ModeRounds_CanSpawn for Score = True; declare Boolean Knockout_SpawnPermissionRequested for Score = False; Knockout_SpawnPermissionRequested = False; if (MM_IsMatchServer()) { declare CSmPlayer 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 Boolean 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 Boolean Knockout_SpawnPermissionRequested for Player.Score = False; if (!Knockout_SpawnPermissionRequested) { Knockout_SpawnPermissionRequested = True; RequestSpawnPermission(Player); NeedInfoDisplayUpdate = True; } declare Boolean 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 Events::K_RaceEvent[] 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); Scores::UpdatePlayerBestRaceIfBetter(Event.Player); 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) { 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(); } MB_SetValidRound(False); } 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 Integer EliminationsNb = GetEliminationsNb(GetAlivePlayers(), Match_CurrentRoundNb); declare Ident[] ReversedEliminatedPlayersScoresIds; if (Scores.count > 0) { for (I, 0, Scores.count-1) { declare CSmScore 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 Integer 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 CSmPlayer Player = GetPlayer(Scores[ScoreId].User.Login); if (Player != Null) { UIModules_KnockoutReward::SetPlayerEliminated(Player, True); UIModules_KnockoutReward::SendResult(Player); } } } foreach (Login in Match_WinnersLogins) { declare CSmPlayer 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 CSmScore WinnerScore <=> Scores::GetPlayerWinner(); if (WinnerScore == Null) { UIModules_BigMessage::SetMessage(_("|Match|Draw")); } else { UIModules_BigMessage::SetMessage(_("$<%1$> wins the match!"), WinnerScore.User.WebServicesUserId); } // 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 CUIConfig::EUISequence 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 = K_MatchInfo {}; 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 = K_MatchInfo {}; 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 = K_MatchInfo {}; 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 = K_MatchInfo {}; 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 CSmScore Score <=> Scores[ScoreId]; declare Text 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; declare Integer Rank = Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1; Server_MatchInfo.PlayerRanks[AccountId] = Rank; UIModules_KnockoutReward::SaveRank(Score, Rank); UIModules_KnockoutReward::SaveCupRank(Score, S_MatchPosition, Rank); } TagAlive(Score, False); } } } Integer[] GetEliminationsMilestones() { declare Text[] Text_Milestones = TL::Split(",", S_EliminatedPlayersNbRanks); declare Integer[] Milestones = [1]; foreach (Text_Milestone in Text_Milestones) { declare Integer 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 Integer 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 Integer[] Milestones = GetEliminationsMilestones(); declare Integer 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 Integer RoundsWithEliminationsLeft = 0; declare Integer AlivePlayers = _AlivePlayers; declare Integer[] Milestones = GetEliminationsMilestones(); for (Index, 0, Milestones.count-1) { declare Integer ReverseIndex = Milestones.count-1 - Index; declare Integer Milestone = Milestones[ReverseIndex]; if (AlivePlayers > Milestone && AlivePlayers > 1) { declare Integer ElimNbPerRound = ReverseIndex + 1; declare Integer 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 Boolean 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 Boolean 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 Boolean 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); 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 Integer 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 Integer 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 Text Message = ""; if (MatchIsOver(_CurrentRoundNb)) { Message = _("Match is over"); } else { declare Integer AlivePlayersNb = GetAlivePlayers(); declare Integer Milestone = GetNextMilestone(AlivePlayersNb); declare Integer CurrentRound = _CurrentRoundNb; if (CurrentRound == 0) CurrentRound = 1; declare Integer TotalRoundsNb = _TotalRoundsNb; if (CurrentRound > TotalRoundsNb) TotalRoundsNb = CurrentRound; declare Integer 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 Integer 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); }