/** * CP Knockout V2 mode * Similar to TM_CPKnockout.Script.txt but allow multiple rounds, and can finish before the finish if not enough players */ // #RequireContext CSmMode #Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaRoundsBase.Script.txt" #Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" #Const Version "2023-11-27" #Const ScriptName "Modes/TM2020-Gamemodes/TM_CPKnockout.Script.txt" // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Libraries // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Include "TextLib" as TL #Include "MathLib" as ML #Include "Libs/Nadeo/Trackmania/MainMenu/Constants.Script.txt" as MenuConsts #Include "Libs/Nadeo/Trackmania/Modes/Rounds/StateManager.Script.txt" as StateMgr #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/TimeGap_Server.Script.txt" as UIModules_TimeGap #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Settings // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Setting S_DisableGiveUp True as _("Disable give up") #Setting S_NumberOfFinishers 8 #Setting S_FinishTimeout -1 as _("Finish timeout") #Setting S_WarmUpNb 1 as _("Number of warm up") #Setting S_WarmUpDuration 30 as _("Duration of one warm up") #Setting S_WarmUpTimeout -1 as _("Warm up timeout") #Setting S_EliminatedPlayersNbRanks "2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0" as "Number of eliminated by checkpoint sorted from the finish to the start" // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Constants // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Const C_ModeName "CP Knockout" //L16N [Laps] Description of the mode rules #Const Description "Knockout per checkpoint game mode" #Const C_HudModulePath "" //< Path to the hud module #Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/Trackmania/Modes/TimeAttack.Script.txt" //< Url of the mania app #Const C_UploadRecord True #Const C_DisplayRecordGhost False #Const C_DisplayRecordMedal False #Const C_CelebrateRecordGhost True #Const C_CelebrateRecordMedal True #Struct K_State { Integer NbAliveAfter; Integer NbFinishers; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Extends // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ***Match_LogVersions*** *** Log::RegisterScript(ScriptName, Version); Log::RegisterScript(StateMgr::ScriptName, StateMgr::Version); *** ***Match_LoadLibraries*** *** StateMgr::Load(); *** ***Match_UnloadLibraries*** *** StateMgr::Unload(); *** ***Match_Settings*** *** MB_Settings_UseDefaultHud = (C_HudModulePath == ""); MB_Settings_UseDefaultTimer = False; *** ***Match_Rules*** *** ModeInfo::SetName(C_ModeName); ModeInfo::SetType(ModeInfo::C_Type_FreeForAll); ModeInfo::SetRules(Description); ModeInfo::SetStatusMessage(_("TYPE: Free for all\nOBJECTIVE: Set the best time on the track.")); *** ***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_BestRace); UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_BestRace); UIModules_Checkpoint::SetVisibilityTimeDiff(False, True); UIModules_PauseMenu_Online::SetHelp(Description); Scores::SaveInScore(Scores::C_Points_Match); UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_PrevTime); UIModules_ScoresTable::SetHideSpectators(True); *** ***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); } } } StateMgr::Yield(); *** ***Match_StartServer*** *** // Initialize mode Clans::SetClansNb(0); StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); WarmUp::SetAvailability(True); Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); Race::SetupRecord( MenuConsts::C_ScopeType_Season, MenuConsts::C_ScopeType_PersonalBest, MenuConsts::C_GameMode_Laps, "", C_UploadRecord, C_DisplayRecordGhost, C_DisplayRecordMedal, C_CelebrateRecordGhost, C_CelebrateRecordMedal ); Race::UseAutomaticDossardColor(False); *** ***Match_InitMatch*** *** foreach (Score in Scores) { declare Boolean IsAlive for Score = False; IsAlive = False; } UIModules_ScoresTable::SetCustomPoints([]); UIModules_ScoresTable::SetCustomTimes([]); declare Boolean Match_InitPlayers = True; declare Integer Match_PlayersEliminated; declare Integer Match_Players; *** ***Match_StartMap*** *** CarRank::Reset(); if (S_WarmUpNb > 0) { foreach (Score in Scores) { WarmUp::CanPlay(Score, True); } MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); } if (Match_InitPlayers) { Match_InitPlayers = False; foreach (Score in Scores) { declare Boolean IsAlive for Score = False; IsAlive = True; Match_Players += 1; if (Score.User == Null) continue; } } StartTime = Now + Race::C_SpawnDuration; if (S_DisableGiveUp) { Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_NeverGiveUp); } else { Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); } *** ***Match_InitRound*** *** declare Integer Round_NumberOfPlayers = 0; declare Integer Round_DossardsUpdateCooldown = 0; declare Text Round_EliminatedPlayersNbRanks = ""; declare K_State[Integer] Round_State; declare Ident[] Round_EliminatedScores; *** ***Match_StartRound*** *** Round_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; declare Integer[Text] CustomTimes = []; foreach (Player in Players) { Player.Dossard_Color = <1., 1., 1.>; if (Player.Score == Null || Player.User == Null) continue; declare Boolean IsAlive for Player.Score = False; if (IsAlive) { Scores::SetPlayerMatchPoints(Player.Score, Match_Players); CustomTimes[Player.User.WebServicesUserId] = 0; Round_NumberOfPlayers += 1; } } UIModules_ScoresTable::SetCustomTimes(CustomTimes); Round_State = ComputeState(Round_NumberOfPlayers); log("Round_State: " ^ Round_State); EndTime = -1; *** ***Match_PlayLoop*** *** // Manage race events foreach (Event in Race::GetPendingEvents()) { Race::ValidEvent(Event); // Waypoint if (Event.Type == Events::C_Type_Waypoint) { if (Event.Player != Null) { declare Integer NBOfCP = Event.Player.RaceWaypointTimes.count; if (Round_State.existskey(NBOfCP)) { Round_State[NBOfCP].NbFinishers += 1; // Proceed kick if (Round_State[NBOfCP].NbFinishers >= Round_State[NBOfCP].NbAliveAfter) { foreach (Player in Players) { if (Player.SpawnStatus != CSmPlayer::ESpawnStatus::Spawned) continue; if (Player.RaceWaypointTimes.count < NBOfCP) { EliminatePlayer(Player, Match_PlayersEliminated); Match_PlayersEliminated += 1; Round_EliminatedScores.add(Player.Score.Id); } } foreach (CPNb => State in Round_State) { if (CPNb > Event.Player.RaceWaypointTimes.count) { break; } } } } if (Event.IsEndRace) { Scores::UpdatePlayerPrevRace(Event.Player); declare Integer[Text] CustomTimes = UIModules_ScoresTable::GetCustomTimes(); CustomTimes.removekey(Event.Player.User.WebServicesUserId); UIModules_ScoresTable::SetCustomTimes(CustomTimes); if (EndTime <= 0) { EndTime = Race::GetFinishTimeout(S_FinishTimeout, Race::GetLapsNb(), Map); } } // Update best race at each checkpoint to sort scores with C_Sort_BestRaceCheckpointsProgress Scores::UpdatePlayerBestRace(Event.Player); CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); if (Round_DossardsUpdateCooldown == 0) { UpdateDossardColors(Round_State); Round_DossardsUpdateCooldown = Now + 1000; } } } else if (Event.Type == Events::C_Type_SkipOutro) { if (Event.Player != Null) { Race::StopSkipOutro(Event.Player); } } } // Manage mode events foreach (Event in PendingEvents) { if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; Events::Invalid(Event); } if (Round_DossardsUpdateCooldown > 0 && Round_DossardsUpdateCooldown < Now) { Round_DossardsUpdateCooldown = 0; UpdateDossardColors(Round_State); } if (Round_EliminatedPlayersNbRanks != S_EliminatedPlayersNbRanks) { Round_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; Round_State = ComputeState(Round_NumberOfPlayers); } if (Players.count > 0 && Round_NumberOfPlayers > 0 && PlayersNbAlive <= S_NumberOfFinishers) { MB_StopMatch(); } *** ***Match_EndRound*** *** Race::StopSkipOutroAll(); EndTime = -1; StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); CarRank::Update(CarRank::C_SortCriteria_CurrentRace); if (Round_ForceEndRound || Round_SkipPauseRound) { declare Text[][Text] CustomPoints = UIModules_ScoresTable::GetCustomPoints(); foreach (Score in Scores) { if (!Round_EliminatedScores.exists(Score.Id)) continue; declare Boolean IsAlive for Score = False; IsAlive = True; if (Score.User == Null || !CustomPoints.existskey(Score.User.WebServicesUserId)) continue; CustomPoints.removekey(Score.User.WebServicesUserId); } UIModules_ScoresTable::SetCustomPoints(CustomPoints); // 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 { UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; MB_Sleep(S_ChatTime / 2); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing; MB_Sleep(S_ChatTime / 2); } *** ***Match_EndMap*** *** // Ensure that we stop the match (after a vote for the next map, ...) MB_StopMatch(); EndTime = -1; StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); CarRank::Update(CarRank::C_SortCriteria_CurrentRace); Race::SortScores(Race::C_Sort_TotalPoints); Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_MatchPoints)); Race::StopSkipOutroAll(); *** ***Rounds_CanSpawn*** *** foreach (Score in Scores) { declare Boolean ModeRounds_CanSpawn for Score = True; ModeRounds_CanSpawn = CanSpawn(Score); } *** ***Rounds_CheckCanSpawn*** *** // During PlayLoop Check declare Boolean IsAlive for _Player.Score = False; return IsAlive; ---PouleParty_Rounds_CanSpawn--- *** Boolean CanSpawn(CSmScore _Score) { // Before PlayLoop Check declare Boolean IsAlive for _Score = False; return IsAlive; } /* * * Functions * */ /** Compute Match State based on S_EliminatedPlayersNbRanks and number of Players * * @return K_State[Integer] */ K_State[Integer] ComputeState(Integer _NbOfPlayer) { declare K_State[Integer] State; declare Text[] KORepartition = TL::Split(",", S_EliminatedPlayersNbRanks); declare Integer NBOfCP = Map::GetCheckpointsCount() + 1; declare Integer NbAliveAfter = _NbOfPlayer; for (I, 0 , NBOfCP) { declare Integer CPIndex = NBOfCP - I; if (KORepartition.existskey(CPIndex) && KORepartition[CPIndex] != "0") { NbAliveAfter -= TL::ToInteger(KORepartition[CPIndex]); State[I] = K_State{ NbAliveAfter = NbAliveAfter }; } } return State; } /** Update Dossard Color of Players depending of the CP and the Rank * * @return Void */ Void UpdateDossardColors(K_State[Integer] _Round_State) { Log::Log("UpdateDossardColors"); declare Integer Rank = 1; foreach (Score in Scores) { if (Score.User == Null) continue; declare CSmPlayer Player = GetPlayer(Score.User.Login); if (Player == Null) continue; if (Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned) continue; declare Integer NbAlive; if (_Round_State.existskey(Player.CurrentLapNumber + 1)) { NbAlive = _Round_State[Player.CurrentLapNumber + 1].NbAliveAfter; } else { // get first CP foreach (Value in _Round_State) { NbAlive = Value.NbAliveAfter; break; } } if (Rank > NbAlive) { Player.Dossard_Color = <1., 0., 0.>; } else { Player.Dossard_Color = <1., 1., 1.>; } Rank += 1; } } /** Eliminate Player and send a message in a Chat * * @return Void */ Void EliminatePlayer(CSmPlayer _Player, Integer _Eliminated) { if (_Player == Null || _Player.Score == Null) return; if (_Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned) return; log("EliminatePlayer: " ^ _Player.User.Name ^ " (" ^ _Player.User.Login ^ ")"); Race::StopSkipOutro(_Player); UIManager.UIAll.SendChat("Player $<$ff6" ^ _Player.User.Name ^ "$> is $<$f00eliminated$>"); declare Text[][Text] CustomPoints = UIModules_ScoresTable::GetCustomPoints(); CustomPoints[_Player.User.WebServicesUserId] = [_("|Status|K.O."), "f00"]; UIModules_ScoresTable::SetCustomPoints(CustomPoints); Scores::SetPlayerMatchPoints(_Player.Score, _Eliminated); declare Boolean IsAlive for _Player.Score = False; IsAlive = False; }