/** * Time Attack Rounds mode * Quick script ordered by Bergie for Telialigaen */ // #RequireContext CSmMode #Extends "Libs/Nadeo/TMNext/TrackMania/Modes/TMNextRoundsBase.Script.txt" #Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" #Const Version "2022-01-22" #Const ScriptName "Modes/TrackMania/TM_TimeAttackRounds_Online.Script.txt" // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Libraries // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Include "TextLib" as TL #Include "MathLib" as ML #Include "Libs/Nadeo/CommonLibs/Common/Task.Script.txt" as Task #Include "Libs/Nadeo/TMNext/TrackMania/Modes/TimeAttack/StateManager.Script.txt" as StateMgr #Include "Libs/Nadeo/TMNext/TrackMania/Modes/TrophyRanking.Script.txt" as TrophyRanking #Include "Libs/Nadeo/TMNext/TrackMania/Menu/Constants.Script.txt" as MenuConsts #Include "Libs/Nadeo/ModeLibs/Common/Utils.Script.txt" as ModeUtils #Include "Libs/Nadeo/CommonLibs/Common/Semver.Script.txt" as Semver // UI from Race #Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint #Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable #Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online #Include "ManiaApps/Nadeo/TMNext/TrackMania/TimeAttack/UIModules/EndMatchTrophy_Server.Script.txt" as UIModules_EndMatchTrophy // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Settings // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Setting S_MapsPerMatch 3 as _("Number of maps per match") ///< Number of maps to play before finishing the match #Setting S_TimeLimit 300 as _("Time limit") ///< Time limit before going to the next map #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_ForceLapsNb 0 #Setting S_PointsLimit 0 as _("Points limit") #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_UseTieBreak False as _("Use tie-break") ///< Continue to play the map until the tie is broken #Setting S_PointsRepartition "10,9,8,7,6,5,4,3,2,1,0" // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Constants // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Const C_ModeName "Time Attack" //L16N [Time Attack] Description of the mode rules #Const Description _("$zIn $<$t$6F9Time Attack$> mode, the goal is to set the $<$t$6F9best time$>.\n\nYou have as many tries as you want, and you can $<$t$6F9retry$> when you want by pressing the respawn key.\n\nWhen the time is up, the $<$t$6F9winner$> is the player with the $<$t$6F9best time$>.") #Const C_HudModulePath "" //< Path to the hud module #Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/TMNext/TrackMania/TimeAttack/TimeAttack.Script.txt" //< Url of the mania app #Const C_FakeUsersNb 0 #Const C_UploadRecord True #Const C_DisplayRecordGhost True #Const C_DisplayRecordMedal True #Const C_CelebrateRecordGhost True #Const C_CelebrateRecordMedal True #Const C_DisplayWorldTop True #Const C_PointsLimit_NotReached 0 #Const C_PointsLimit_Reached 1 #Const C_PointsLimit_Tie 2 #Const C_TrophyTaskTimeout 5000 #Const C_TrophyAnimationDuration 4000 #Const C_TrophyDisplayDuration 7000 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Extends // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ***Match_LogVersions*** *** Log::RegisterScript(ScriptName, Version); Log::RegisterScript(StateMgr::ScriptName, StateMgr::Version); Log::RegisterScript(Semver::ScriptName, Semver::Version); *** ***Match_LoadLibraries*** *** StateMgr::Load(); *** ***Match_UnloadLibraries*** *** StateMgr::Unload(); *** ***Match_Settings*** *** MB_Settings_UseDefaultTimer = False; MB_Settings_UseDefaultHud = (C_HudModulePath == ""); *** ***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*** *** UIManager.UIAll.ScoreTableOnlyManialink = True; UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_BestRace); ClientManiaAppUrl = C_ManiaAppUrl; Race::SortScores(Race::C_Sort_BestRaceTime); UIModules_TimeGap::SetTimeGapMode(UIModules_TimeGap::C_TimeGapMode_BestRace); UIModules_PauseMenu_Online::SetHelp(Description); UIManager.UIAll.OverlayHideCountdown = True; UIManager.UIAll.OverlayHideSpectatorInfos = True; UIManager.UIAll.OverlayHideChrono = 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); if (Event.Player != Null) { declare Boolean Match_CanForceTrophyRankUpdate for This; TrophyRanking::InitializeUser(Event.Player.User, Match_CanForceTrophyRankUpdate); } } } } StateMgr::Yield(); TrophyRanking::Yield(); *** ***Match_StartServer*** *** // Initialize mode Clans::SetClansNb(0); GiveUpBehaviour_RespawnAfter = True; CrudeExtrapolation_AllowDelay = True; Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_GiveUpBeforeFirstCheckpoint); StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); WarmUp::SetAvailability(True); Race::SetupRecord( MenuConsts::C_ScopeType_Season, MenuConsts::C_ScopeType_PersonalBest, MenuConsts::C_GameMode_TimeAttack, "", C_UploadRecord, C_DisplayRecordGhost, C_DisplayRecordMedal, C_CelebrateRecordGhost, C_CelebrateRecordMedal, C_DisplayWorldTop ); CarRank::Reset(); Scores::SaveInScore(Scores::C_Points_Match); UIModules_Record::SetSpecialVisibility(False); *** ***Match_InitMatch*** *** declare Task::LibCommonTask_K_Task Match_TrophyTask; declare Integer Match_TrophyTaskEndTime; declare Integer Match_MatchDuration; declare Boolean Match_CanForceTrophyRankUpdate for This = False; declare netwrite Text Net_ScriptEnvironment for Teams[0] = S_ScriptEnvironment; *** ***Match_AfterLoadMap*** *** Match_CanForceTrophyRankUpdate = True; *** ***Match_InitMap*** *** declare Integer Map_ValidRoundsNb; declare Boolean Map_Skipped; declare Integer Map_TimeLimit; declare Integer Map_MapStartTime; declare Integer Map_MapsPerMatch; *** ***Match_StartMap*** *** // Add bot when necessary //Users_SetNbFakeUsers(C_FakeUsersNb, 0); // Warm up UIModules_ScoresTable::SetFooterInfo(_("Warmup")); GiveUpBehaviour_RespawnAfter = False; MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); GiveUpBehaviour_RespawnAfter = True; // Initialize race SetScoresTableScoreMode(False); StartTime = Now + Race::C_SpawnDuration; // Spawn players for the race foreach (Player in Players) { Race::Start(Player, StartTime); } StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]); CarRank::Update(CarRank::C_SortCriteria_BestRace); Race::EnableIntroDuringMatch(True); declare netwrite Integer Net_SerialNeedToUpdate for Teams[0]; Net_SerialNeedToUpdate = 0; Map_TimeLimit = S_TimeLimit; Map_MapStartTime = StartTime; Map_MapsPerMatch = S_MapsPerMatch; UpdateScoresTableFooterAndTimeLimit(StartTime, S_TimeLimit, S_MapsPerMatch); *** ***Match_StartRound*** *** StartTime = Now + Race::C_SpawnDuration; UpdateScoresTableFooterAndTimeLimit(StartTime, S_TimeLimit, S_MapsPerMatch); Race::SortScores(Race::C_Sort_BestRaceTime); *** ***Match_InitPlayLoop*** *** Round_Skipped = False; *** ***Match_PlayLoop*** *** // Manage race events declare RacePendingEvents = Race::GetPendingEvents(); foreach (Event in RacePendingEvents) { Race::ValidEvent(Event); // Waypoint if (Event.Type == Events::C_Type_Waypoint) { if (Event.Player != Null) { declare Better = False; declare OldRank = 0; if (Event.IsEndRace) { // Computes old rank before changing Score foreach (Index => Score in Scores) { if (Score.Id == Event.Player.Score.Id) { if (Score.BestRaceTimes.count != 0) { OldRank = Index + 1; } else { OldRank = -123; } break; } } // Change Score Better = Scores::UpdatePlayerBestRaceIfBetter(Event.Player); declare BetterLap = Scores::UpdatePlayerBestLapIfBetter(Event.Player); Scores::UpdatePlayerPrevRace(Event.Player); } else if (Event.IsEndLap) { declare BetterLap = Scores::UpdatePlayerBestLapIfBetter(Event.Player); if (Race::IsIndependentLaps()) { // Computes old rank before changing Score foreach (Index => Score in Scores) { if (Score.Id == Event.Player.Score.Id) { if (Score.BestLapTimes.count != 0) { OldRank = Index + 1; } else { OldRank = -123; } break; } } // Change Score Better = BetterLap; if (Better) Scores::UpdatePlayerBestRace(Event.Player); Scores::UpdatePlayerPrevLap(Event.Player); } } if (Better) { declare NewRank = 0; foreach (Index => Score in Scores) { if (Score.Id == Event.Player.Score.Id) { NewRank = Index + 1; break; } } if (OldRank != NewRank) { if (0 < NewRank && NewRank < 4) { foreach (Player in AllPlayers) { UIModules_DisplayMessage::SendLiveMessage_RankUpdate(Player, Event.Player.User, NewRank); } } else if (0 < NewRank) { UIModules_DisplayMessage::SendLiveMessage_RankUpdate(Event.Player, Event.Player.User, NewRank); } } CarRank::ThrottleUpdate(CarRank::C_SortCriteria_BestRace); } } } } // Manage mode events foreach (Event in PendingEvents) { if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; Events::Invalid(Event); } // Spawn players if (PlayersNbDead > 0) { //< Check for unspawned players only if at least one player is unspawned foreach (Player in Players) { if (Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned && Race::IsReadyToStart(Player)) { Race::Start(Player); } } } // Update the map duration setting if (Map_TimeLimit != S_TimeLimit || Map_MapsPerMatch != S_MapsPerMatch ) { Map_TimeLimit = S_TimeLimit; Map_MapsPerMatch = S_MapsPerMatch; UpdateScoresTableFooterAndTimeLimit(StartTime, S_TimeLimit, S_MapsPerMatch); } if (Net_ScriptEnvironment != S_ScriptEnvironment) { Net_ScriptEnvironment = S_ScriptEnvironment; } *** ***Rounds_CheckStopRound*** *** // If time limit is reached if (EndTime > 0 && Now >= EndTime) { MB_StopRound(); Round_Skipped = False; } // If forced end round or round skipped after pause if (Round_ForceEndRound || Round_SkipPauseRound) { MB_StopRound(); Round_Skipped = False; } *** ***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 || Round_Skipped) { // 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 { Map_ValidRoundsNb += 1; ComputeLatestRaceScores(); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; MB_Sleep(3000); SetScoresTableScoreMode(True); MB_Sleep(3000); Scores::EndRound(); Race::SortScores(Race::C_Sort_TotalPoints); MB_Sleep(3000); SetScoresTableScoreMode(False); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing; if (MapIsOver(S_UseTieBreak, S_PointsLimit, Map_ValidRoundsNb, S_RoundsPerMap)) { Map_Skipped = False; MB_StopMap(); } } *** ***Match_EndMap*** *** EndTime = -1; Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_MatchPoints)); Match_MatchDuration = ML::Max(0, Now - Map_MapStartTime); StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); Race::EnableIntroDuringMatch(False); TrophyRanking::UpdateUsersRank(); CarRank::Update(CarRank::C_SortCriteria_BestRace); if (MatchIsOver(S_UseTieBreak, S_PointsLimit, MB_GetMapCount(), S_MapsPerMatch, S_RoundsPerMap, Map_Skipped)) MB_StopMatch(); if (!MB_MapIsRunning() && MB_MatchIsRunning()) MB_SkipPodiumSequence(); *** ***Match_BeforeUnloadMap*** *** Match_CanForceTrophyRankUpdate = False; *** // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Functions // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /// Select the scores table score mode Void SetScoresTableScoreMode(Boolean _IsPoints) { if (_IsPoints) UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_Points); else UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_BestTime); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Update the time limit * * @param _StartTime The starting time of the map * @param _NewTimeLimit The time limit before going to the next map */ Void UpdateScoresTableFooterAndTimeLimit(Integer _StartTime, Integer _NewTimeLimit, Integer _MapsPerMatch) { Log::Log("[UpdateScoresTableFooterAndTimeLimit] Update Settings Footer"); declare Text[] Parts; declare Text Message = ""; if (_NewTimeLimit <= 0) { if (Parts.count > 0) Message ^= "\n"; Message ^= """%{{{Parts.count + 1}}} -"""; //L16N [Rounds] Number of points to reach to win the match. Parts.add(_("Time Limit : ")); EndTime = -1; UIModules_ScoresTable::SetFooterInfo(TL::Compose("%1 -", _("Time Limit"))); } else { if (Parts.count > 0) Message ^= "\n"; Message ^= """%{{{Parts.count + 1}}}{{{TL::TimeToText(_NewTimeLimit*1000)}}}"""; //L16N [Rounds] Number of points to reach to win the match. Parts.add(_("Time Limit : ")); EndTime = _StartTime + (S_TimeLimit * 1000); UIModules_ScoresTable::SetFooterInfo(TL::Compose("%1 "^TL::TimeToText(_NewTimeLimit*1000), _("Time Limit"))); } if (_MapsPerMatch > 0) { if (Parts.count > 0) Message ^= "\n"; Message ^= """%{{{Parts.count + 1}}}{{{MB_GetMapCount()}}}/{{{_MapsPerMatch}}}"""; //L16N [Rounds] Number of maps played during the match. Parts.add(_("Maps : ")); } switch (Parts.count) { case 0: UIModules_ScoresTable::SetFooterInfo(Message); case 1: UIModules_ScoresTable::SetFooterInfo(TL::Compose(Message, Parts[0])); case 2: UIModules_ScoresTable::SetFooterInfo(TL::Compose(Message, Parts[0], Parts[1])); } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Get the right sort criteria for * the scores * * @return The sort criteria */ Integer GetScoresSortCriteria() { if (Race::IsIndependentLaps()) return Race::C_Sort_BestLapTime; return Race::C_Sort_BestRaceTime; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Get the right sort criteria for * the ladder * * @return The sort criteria */ Integer GetLadderSortCriteria() { if (Race::IsIndependentLaps()) return Scores::C_Sort_BestLapTime; return Scores::C_Sort_BestRaceTime; } Void ComputeLatestRaceScores() { // Points distributed between all players declare Integer I = 0; declare Integer[] PointsRepartition = PointsRepartition::GetPointsRepartition(); foreach (Score in Scores) { if (Scores::GetPlayerBestRaceTime(Score) > 0) { declare Integer Points = 0; if (PointsRepartition.count > 0) { if (PointsRepartition.existskey(I)) { Points = PointsRepartition[I]; } else { Points = PointsRepartition[PointsRepartition.count - 1]; } } Scores::SetPlayerRoundPoints(Score, Points); I += 1; } else { Scores::SetPlayerRoundPoints(Score, 0); } } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check if the points limit was reached * * @param _UseTieBreak Prevent ties or not * @param _PointsLimit Number of points to get to win the match * * @return C_PointsLimit_Reached if the points limit is reached * C_PointsLimit_Tie if there is a tie * C_PointsLimit_NotReached if the points limit is not reached */ Integer PointsLimitReached(Boolean _UseTieBreak, Integer _PointsLimit) { declare Integer MaxScore = -1; declare Boolean Tie = False; foreach (Score in Scores) { declare Integer Points = Scores::GetPlayerMatchPoints(Score); if (Points > MaxScore) { MaxScore = Points; Tie = False; } else if (Points == MaxScore) { Tie = True; } } if (_UseTieBreak && Tie) return C_PointsLimit_Tie; //< There is a tie and it is not allowed if (_PointsLimit > 0 && MaxScore >= _PointsLimit) return C_PointsLimit_Reached; //< There is a points limit and it is reached return C_PointsLimit_NotReached; //< There is no points limit or the points limit is not reached } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check if we should go to the next map * * @param _UseTieBreak Prevent ties or not * @param _PointsLimit Number of points to get to win the match * @param _ValidRoundsNb Number of valid rounds played * @param _RoundsPerMap Number of rounds to play to complete the map * * @return True if it is the case, false otherwise */ Boolean MapIsOver(Boolean _UseTieBreak, Integer _PointsLimit, Integer _ValidRoundsNb, Integer _RoundsPerMap) { declare Integer PointsLimitReached = PointsLimitReached(_UseTieBreak, _PointsLimit); Log::Log("""[Rounds] MapIsOver() > _UseTieBreak: {{{_UseTieBreak}}} | _PointsLimit: {{{_PointsLimit}}} | _ValidRoundsNb: {{{_ValidRoundsNb}}} | _RoundsPerMap: {{{_RoundsPerMap}}} | PointsLimitReached: {{{PointsLimitReached}}}"""); if (PointsLimitReached == C_PointsLimit_Reached) return True; //< There is a points limit and it is reached if (_RoundsPerMap > 0 && _ValidRoundsNb >= _RoundsPerMap) return True; //< There is a rounds limit and it is reached return False; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check if we should go to the next match * * @param _UseTieBreak Prevent ties or not * @param _PointsLimit Number of points to get to win the match * @param _MapsPerMatch Number of maps to play to complete a match * @param _RoundsPerMap Number of rounds to play to complete the map * * @return True if it is the case, false otherwise */ Boolean MatchIsOver(Boolean _UseTieBreak, Integer _PointsLimit, Integer _MapCount, Integer _MapsPerMatch, Integer _RoundsPerMap, Boolean _MapSkipped) { declare Integer PointsLimitReached = PointsLimitReached(_UseTieBreak, _PointsLimit); Log::Log("""[Rounds] MatchIsOver() > _UseTieBreak: {{{_UseTieBreak}}} | _PointsLimit: {{{_PointsLimit}}} | _MapCount: {{{_MapCount}}} | _MapsPerMatch: {{{_MapsPerMatch}}} | _RoundsPerMap: {{{_RoundsPerMap}}} | PointsLimitReached: {{{PointsLimitReached}}} | _MapSkipped : {{{_MapSkipped}}}"""); // If there is a point limit and it is reached, stop the match if (PointsLimitReached == C_PointsLimit_Reached) { return True; } // If there is an explicit maps limit ... else if (_MapsPerMatch >= 1) { if ( (_MapCount >= _MapsPerMatch && PointsLimitReached != C_PointsLimit_Tie) || //< ... stop the match if the maps limit is reached and the match is not a tie (_MapSkipped && _MapsPerMatch == 1 && _MapCount >= _MapsPerMatch) //< ... stop the match if the map was skipped and the match is played on only one map ) { return True; } } // If there is a rounds limit but no maps limit, continue to play until another limit is reached else if (_RoundsPerMap >= 1) { return False; } // If there is neither a points limit nor a rounds limit, always stop the match at the end of the first map, even if there is a tie else { return True; } // In all other cases continue to play return False; }