/** * Laps Knockout mode */ // #RequireContext CSmMode #Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaBase.Script.txt" #Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" #Const Version "2023-09-25" #Const ScriptName "Modes/TM2020-Gamemodes/TM_LapsKnockout.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/Laps/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/Trackmania/Modes/LapsCommon/Libs/Constants.Script.txt" as LibLaps_Constants #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_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_NbLapsWithoutKO 0 as "Number of laps without elimination" #Setting S_EliminatedPlayersNbRanks "4,16,16" as _("Nb of players above which one extra elim. /lap. Same setting of Knock") /* About S_EliminatedPlayersNbRanks * Example : "8,16" * 1 to 8 players -> 1 elimination per lap * 9 to 16 players -> 2 eliminations per lap * 17 or more players -> 3 eliminations per lap * * Example : "8,16,16" * 1 to 8 players -> 1 eliminations per lap * 9 to 16 players -> 2 eliminations per lap * 17 or more players -> 4 eliminations per lap * * Example : "0,8" * 1 to 8 players -> 2 eliminations per lap * 9 or more players -> 3 eliminations per lap * * Example : "" * 1 or more players -> 1 elimination per lap */ #Struct K_LapState { Integer NbAliveAfter; Integer NbEliminations; Integer NbLapFinishers; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Constants // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Const C_ModeName "Laps" //L16N [Laps] Description of the mode rules #Const Description _("$zIn $<$t$6F9Laps$> mode, the goal is to drive as far as possible by passing $<$t$6F9checkpoints$>.\n\nThe laps mode takes place on multilap (cyclical) maps, and is played in one go for every map.\n\nWhen the time is up, the $<$t$6F9winner$> is the player who passed the most $<$t$6F9checkpoints$>. In case of draws, the winner is the player who passed the last checkpoint first.") #Const C_HudModulePath "" //< Path to the hud module #Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/Trackmania/Modes/Laps.Script.txt" //< Url of the mania app #Const C_FakeUsersNb 0 #Const C_UploadRecord True #Const C_DisplayRecordGhost False #Const C_DisplayRecordMedal False #Const C_CelebrateRecordGhost True #Const C_CelebrateRecordMedal True // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // 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_BestRaceCheckpointsProgress); 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); UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_Laps); UIModules_ScoresTable::SetHideSpectators(True); SetManialink_Panel(); *** ***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([LibLaps_Constants::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_InitMap*** *** declare Integer Last_NbLapsWithoutKO; declare Integer Last_NumberOfPlayers; declare Text Last_EliminatedPlayersNbRanks; declare K_LapState[Integer] MatchState; declare Integer Last_CooldownDossardUpdate = -1; declare Boolean Last_InCooldownDossardUpdate = False; Race::SetLapsSettings(True, -1); // force infinite lap *** ***Match_StartMap*** *** CarRank::Reset(); UIModules_ScoresTable::SetCustomPoints([]); declare netwrite K_LapState[Integer] Net_LapsKnockout_MatchState for Teams[0]; // Note the NbLapFinishers is not updated Net_LapsKnockout_MatchState = []; // Warm up ---Laps_Warmup--- StartTime = Now + Race::C_SpawnDuration; if (S_DisableGiveUp) { Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_NeverGiveUp); } else { Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); } // Spawn players for the race foreach (Score in Scores) { declare Boolean Laps_CanSpawn for Score = True; Laps_CanSpawn = True; } foreach (Player in Players) { if (Player.Score != Null && Race::IsReadyToStart(Player)) { declare Boolean Laps_CanSpawn for Player.Score = True; Race::Start(Player, StartTime); Laps_CanSpawn = False; } } CarRank::Update(CarRank::C_SortCriteria_BestRace); StateMgr::ForcePlayersStates([LibLaps_Constants::C_State_Playing]); *** ***Laps_Warmup*** *** Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); *** ***Match_StartRound*** *** // Once the round started, no one can join to play Last_NumberOfPlayers = Players.count; Last_NbLapsWithoutKO = S_NbLapsWithoutKO; Last_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; MatchState = ComputeMatchState(Last_NumberOfPlayers); // Update UI declare netwrite K_LapState[Integer] Net_LapsKnockout_MatchState for Teams[0]; // Note the NbLapFinishers is not updated Net_LapsKnockout_MatchState = MatchState; foreach (LapNb => State in MatchState.sortkeyreverse()) { Race::SetLapsSettings(False, LapNb); // Set number of laps with number of break; } // Reset Dossard Color foreach (Player in Players) { Player.Dossard_Color = <1., 1., 1.>; } Last_CooldownDossardUpdate = -1; Last_InCooldownDossardUpdate = False; if (Last_NbLapsWithoutKO == 1) { UIManager.UIAll.SendChat(Last_NbLapsWithoutKO ^ " lap without eliminations"); } else if (Last_NbLapsWithoutKO > 1){ UIManager.UIAll.SendChat(Last_NbLapsWithoutKO ^ " laps without eliminations"); } *** ***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) { if (Event.IsEndLap) { Scores::UpdatePlayerBestLapIfBetter(Event.Player); if (MatchState.existskey(Event.Player.CurrentLapNumber)) { MatchState[Event.Player.CurrentLapNumber].NbLapFinishers += 1; // Proceed kick if (MatchState[Event.Player.CurrentLapNumber].NbLapFinishers >= MatchState[Event.Player.CurrentLapNumber].NbAliveAfter) { foreach (Player in Players) { if (Player.SpawnStatus != CSmPlayer::ESpawnStatus::Spawned) continue; if (Player.CurrentLapNumber < Event.Player.CurrentLapNumber) { EliminatePlayer(Player); } } foreach (LapNb => State in MatchState) { if (LapNb > Event.Player.CurrentLapNumber) { break; } MatchState.removekey(LapNb); } } } } if (Event.IsEndRace) { // waiting GetCustomPoints function in ScoresTable_Server declare netwrite Text[][Text] Net_TMxSM_ScoresTable_CustomPoints for Teams[0]; declare Text[][Text] CustomTimes = Net_TMxSM_ScoresTable_CustomPoints; foreach (Player in Players) { if (Player != Event.Player) { CustomTimes[Player.User.WebServicesUserId] = [_("|Status|K.O."), "f00"]; } } UIModules_ScoresTable::SetCustomPoints(CustomTimes); UIManager.UIAll.SendChat("Player $<$ff6" ^ Event.Player.User.Name ^ "$> $<$6f6wins the match!$>"); MB_StopMatch(); } // Update best race at each checkpoint to sort scores with C_Sort_BestRaceCheckpointsProgress Scores::UpdatePlayerBestRace(Event.Player); CarRank::ThrottleUpdate(CarRank::C_SortCriteria_BestRace); if (Last_CooldownDossardUpdate == -1) { UpdateDossardColors(MatchState); Last_CooldownDossardUpdate = Now + 1000; } else { Last_InCooldownDossardUpdate = True; } } } } // Manage mode events foreach (Event in PendingEvents) { if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; Events::Invalid(Event); } if (Last_CooldownDossardUpdate != -1 && Now > Last_CooldownDossardUpdate) { Last_CooldownDossardUpdate = -1; if (Last_InCooldownDossardUpdate) { Last_InCooldownDossardUpdate = False; UpdateDossardColors(MatchState); } } if (Last_NbLapsWithoutKO != S_NbLapsWithoutKO || Last_EliminatedPlayersNbRanks != S_EliminatedPlayersNbRanks) { Last_NbLapsWithoutKO = S_NbLapsWithoutKO; Last_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; MatchState = ComputeMatchState(Last_NumberOfPlayers); // Update UI declare netwrite K_LapState[Integer] Net_LapsKnockout_MatchState for Teams[0]; // Note the NbLapFinishers is not updated Net_LapsKnockout_MatchState = MatchState; foreach (LapNb => State in MatchState.sortkeyreverse()) { Race::SetLapsSettings(False, LapNb); // Set number of laps with number of break; } } if (Players.count > 0 && PlayersNbAlive <= 0) { MB_StopMatch(); } *** ***Match_EndMap*** *** // Ensure that we stop the match (after a vote for the next map, ...) MB_StopMatch(); EndTime = -1; StateMgr::ForcePlayersStates([LibLaps_Constants::C_State_Waiting]); CarRank::Update(CarRank::C_SortCriteria_BestRace); Race::SortScores(Race::C_Sort_BestRaceCheckpointsProgress); Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_BestRaceCheckpointsProgress)); Race::StopSkipOutroAll(); *** /* * * Functions * */ /** Compute Match State based on S_EliminatedPlayersNbRanks and number of Players * * @return K_LapState[Integer] */ K_LapState[Integer] ComputeMatchState(Integer _NumberOfPlayers) { declare K_LapState[Integer] MatchState; declare Integer[] Eliminations = [0]; foreach (EliminationText in TL::Split(",", S_EliminatedPlayersNbRanks)) { declare Integer Elimination = TL::ToInteger(EliminationText); if (Elimination > 0) { // -1 if error Eliminations.add(Elimination); } } declare Integer KickedPlayers = 0; declare Integer LapNumber = S_NbLapsWithoutKO + 1; while (KickedPlayers <= _NumberOfPlayers) { while (_NumberOfPlayers - KickedPlayers < Eliminations[Eliminations.count - 1]) { Eliminations.removekey(Eliminations.count - 1); } if (_NumberOfPlayers - KickedPlayers - Eliminations.count < Eliminations[Eliminations.count - 1]) { Eliminations.removekey(Eliminations.count - 1); } declare Integer Alive = _NumberOfPlayers - KickedPlayers - Eliminations.count; if (Alive <= 0) break; MatchState[LapNumber] = K_LapState { NbAliveAfter = Alive, NbEliminations = Eliminations.count }; KickedPlayers += Eliminations.count; LapNumber += 1; if (LapNumber > 255) break; // Anti crash } return MatchState; } /** Update Dossard Color of Players depending of the Lap and the Rank * * @return Void */ Void UpdateDossardColors(K_LapState[Integer] _MatchState) { Log::Log("UpdateDossardColors"); declare Integer Rank = 1; foreach (Score in Scores) { if (Score == Null) continue; 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 (_MatchState.existskey(Player.CurrentLapNumber + 1)) { NbAlive = _MatchState[Player.CurrentLapNumber + 1].NbAliveAfter; } else { // get first lap foreach (Value in _MatchState) { 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) { if (_Player == 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$>"); } Void SetManialink_Panel() { declare Text MLText = """