From 122c8640449cdf151b9dfa9870c690d37a35a73c Mon Sep 17 00:00:00 2001 From: Beu Date: Fri, 23 Feb 2024 10:18:50 +0100 Subject: [PATCH] Add EnduroCup script --- TM_EnduroCup.Script.txt | 624 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 624 insertions(+) create mode 100644 TM_EnduroCup.Script.txt diff --git a/TM_EnduroCup.Script.txt b/TM_EnduroCup.Script.txt new file mode 100644 index 0000000..389edfd --- /dev/null +++ b/TM_EnduroCup.Script.txt @@ -0,0 +1,624 @@ +/** + * EnduroCup mode + */ +#Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaRoundsBase.Script.txt" + +#Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" +#Const Version "2024-12-23" +#Const ScriptName "Modes/TM2020-Gamemodes/TM_EnduroCup.Script.txt" + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Libraries +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Include "TextLib" as TL +#Include "MathLib" as ML +#Include "Libs/Nadeo/CMGame/Utils/Semver.Script.txt" as Semver +#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/Utils/Tracking.Script.txt" as Tracking +#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 + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Settings +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Setting S_CheckpointsWithoutElimination -1 as "Number of checkpoint without elimination. Negative value = Nb of Laps" +#Setting S_TimeToReachCheckpoint 30000 as "Default time to reach checkpoint in ms" +#Setting S_TimeReductionEveryCP 500 as "Reduction of time in ms applied to each checkpoint" + +#Setting S_PointsLimit 0 as _("Points limit") +#Setting S_RoundsPerMap 4 as _("Number of rounds per track") ///< Number of round to play on one map before going to the next one +#Setting S_MapsPerMatch 1 as _("Number of tracks per match") ///< Number of maps to play before finishing the match +#Setting S_UseTieBreak False as _("Use tie-break") ///< Continue to play the map until the tie is broken + +#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") + +// Default settings that sould be ignored +#Setting S_InfiniteLaps True +#Setting S_ForceLapsNb 0 + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Constants +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Const C_ModeName "EnduroCup" +//L16N [Rounds] Description of the mode rules +#Const Description "" // TODO CHANGE + +#Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/Trackmania/Modes/Rounds.Script.txt" //< Url of the mania app +#Const C_FakeUsersNb 0 + +// Server can allow up to 500ms of latency from the clients, so the double should be enough +#Const C_LatencyCompensation 1000 +#Const C_CleanCheckDelay 1000 +#Const C_ScriptName_Timer "EnduroCup_Timer" + +#Const C_PointsLimit_NotReached 0 +#Const C_PointsLimit_Reached 1 +#Const C_PointsLimit_Tie 2 + +#Const C_UploadRecord True +#Const C_DisplayRecordGhost False +#Const C_DisplayRecordMedal False +#Const C_CelebrateRecordGhost True +#Const C_CelebrateRecordMedal True + +#Struct K_CheckpointState { + Integer RaceTime; + Integer TimeToReach; +} + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// 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*** +*** +StateMgr::Load(); +*** + +***Match_UnloadLibraries*** +*** +StateMgr::Unload(); +*** + +***Match_Settings*** +*** +MB_Settings_UseDefaultHud = True; +*** + +***Match_AfterLoadHud*** +*** +ClientManiaAppUrl = C_ManiaAppUrl; +Race::SortScores(Race::C_Sort_TotalPoints); +UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_Points); +UIModules_Checkpoint::SetVisibilityTimeDiff(False); +UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_CurrentRace); +UIModules_PauseMenu_Online::SetHelp(Description); +UIModules_Sign16x9Small::SetScoreMode(UIModules_Sign16x9Small::C_ScoreMode_Points); +// Hide SM Overlay +UIManager.UIAll.OverlayHideSpectatorInfos = True; +UIManager.UIAll.OverlayHideCountdown = True; + +// Unload default UI +UIModules::UnloadModules(["UIModule_Rounds_SmallScoresTable"]); + +CreateTimerUI(); +*** + +***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); +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_Rounds, + "", + C_UploadRecord, + C_DisplayRecordGhost, + C_DisplayRecordMedal, + C_CelebrateRecordGhost, + C_CelebrateRecordMedal +); + +declare netwrite K_CheckpointState[Integer] Net_EnduroCup_Checkpoints for Teams[0] = []; +Net_EnduroCup_Checkpoints = []; +*** + +***Match_InitMap*** +*** +declare Integer Map_ValidRoundsNb; +declare Boolean Map_Skipped; + +declare Integer Map_PointsLimit = S_PointsLimit; +declare Integer Map_RoundsPerMap = S_RoundsPerMap; +declare Integer Map_MapsPerMatch = S_MapsPerMatch; + +UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb); +*** + +***Match_StartMap*** +*** +// Set inifinite laps (ignoring server settings) +Race::SetLapsSettings(True, 0); + +Map_Skipped = True; +CarRank::Reset(); + +// Warm up +UIModules_ScoresTable::SetFooterInfo(_("Warm up")); +MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); +*** + +***Match_InitRound*** +*** +declare Integer Round_CheckpointsWithoutElimination; +declare netwrite K_CheckpointState[Integer] Net_EnduroCup_Checkpoints for Teams[0] = []; + +declare Integer Last_CheckAlivePlayers = 0; + +while (Players.count < 2 && MB_MapIsRunning()) { + MB_Yield(); +} +*** + +***Match_StartRound*** +*** +if (S_CheckpointsWithoutElimination < 0) { + Round_CheckpointsWithoutElimination = (Map::GetCheckpointsCount() + 1) * ML::Abs(Round_CheckpointsWithoutElimination); +} else { + Round_CheckpointsWithoutElimination = S_CheckpointsWithoutElimination; +} +UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb); +StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]); +*** + +***Rounds_PlayerSpawned*** +*** +CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); +*** + +***Rounds_AfterSpawningPlayers*** +*** +// Prevent late players to spawn +foreach (Score in Scores) { + Rounds_SetCanSpawn(Score, False); +} +*** + +***Match_PlayLoop*** +*** +// Manage race events +foreach (Event in Race::GetPendingEvents()) { + Race::ValidEvent(Event); + + if (Event.Player == Null) continue; + + // Waypoint + if (Event.Type == Events::C_Type_Waypoint) { + CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); + + if (Event.IsEndLap) { + Scores::UpdatePlayerBestLapIfBetter(Event.Player); + } + + // Update the PrevRace at each checkpoint to be able to sort scores even if someone disconnect or something + Scores::UpdatePlayerPrevRace(Event.Player); + + declare Integer NumberOfCP = Event.Player.RaceWaypointTimes.count; + + if (Round_CheckpointsWithoutElimination < NumberOfCP) { + // Player is first + if (!Net_EnduroCup_Checkpoints.existskey(NumberOfCP) || Net_EnduroCup_Checkpoints[NumberOfCP].RaceTime > Event.RaceTime) { + Net_EnduroCup_Checkpoints[NumberOfCP] = K_CheckpointState { + RaceTime = Event.RaceTime, + TimeToReach = S_TimeToReachCheckpoint - S_TimeReductionEveryCP * (NumberOfCP - Round_CheckpointsWithoutElimination) + }; + + if (Last_CheckAlivePlayers == 0) Last_CheckAlivePlayers = Now + C_CleanCheckDelay; + } else if (Net_EnduroCup_Checkpoints[NumberOfCP].RaceTime + Net_EnduroCup_Checkpoints[NumberOfCP].TimeToReach < Event.RaceTime) { + EliminatePlayer(Event.Player); + } + } + } else if (Event.Type == Events::C_Type_GiveUp) { + EliminatePlayer(Event.Player); + } +} + +// Manage mode events +foreach (Event in PendingEvents) { + if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; + Events::Invalid(Event); +} + +// Check if players need to be unspawned +if (Last_CheckAlivePlayers > 0 && Last_CheckAlivePlayers < Now) { + Last_CheckAlivePlayers = 0; + + foreach (Player in Players) { + if (Player.SpawnStatus != CSmPlayer::ESpawnStatus::Spawned) continue; + + // Use get because it's more optimized + declare K_CheckpointState NextCheckpointState = Net_EnduroCup_Checkpoints.get(Player.RaceWaypointTimes.count + 1, K_CheckpointState{}); + if (NextCheckpointState.RaceTime == 0) continue; + + if (NextCheckpointState.RaceTime + NextCheckpointState.TimeToReach + C_LatencyCompensation < Player.CurrentRaceTime) { + EliminatePlayer(Player); + } + } +} + +// Server info change +if ( + Map_PointsLimit != S_PointsLimit || + Map_RoundsPerMap != S_RoundsPerMap || + Map_MapsPerMatch != S_MapsPerMatch +) { + Map_PointsLimit = S_PointsLimit; + Map_RoundsPerMap = S_RoundsPerMap; + Map_MapsPerMatch = S_MapsPerMatch; + + UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb); +} +*** + +***Rounds_CheckStopRound*** +*** +// End the round +// If All players finished +if (Players.count > 0 && PlayersNbAlive <= 1) { + 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(); +StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); +CarRank::Update(CarRank::C_SortCriteria_CurrentRace); + +Net_EnduroCup_Checkpoints = []; + +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(); + } + MB_SetValidRound(False); +} else { + Map_ValidRoundsNb += 1; + // Get the last round points + ComputePoints(); + Race::SortScores(Race::C_Sort_TotalPoints); + UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; + UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; + MB_Sleep(5000); + // Add them to the total scores + ComputeScores(); + Race::SortScores(Race::C_Sort_TotalPoints); + MB_Sleep(3000); + 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*** +*** +if (MatchIsOver(S_UseTieBreak, S_PointsLimit, MB_GetMapCount(), S_MapsPerMatch, S_RoundsPerMap, Map_Skipped)) MB_StopMatch(); + +if (!MB_MapIsRunning() && MB_MatchIsRunning()) MB_SkipPodiumSequence(); + +Race::SortScores(Race::C_Sort_TotalPoints); +declare CSmScore Winner <=> Scores::GetBestPlayer(Scores::C_Sort_MatchPoints); +Scores::SetPlayerWinner(Winner); + +if (!MB_MatchIsRunning()) { + // Compute ranking for tracking + declare Integer PreviousPoints = 0; + declare Integer Rank = 0; + foreach (Key => Score in Scores) { + if (Key == 0 || Scores::GetPlayerMatchPoints(Score) < PreviousPoints) { + PreviousPoints = Scores::GetPlayerMatchPoints(Score); + Rank = Key + 1; + } + Tracking::SendPlayerMatchResult(UIManager, Score.User, Rank, Winner == Score && Scores.count > 1); + } +} +*** + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Functions +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Update the scores table footer text + * + * @param _PointsLimit The points limit + * @param _RoundsPerMap The number of rounds per map + * @param _MapsPerMatch The number of maps per match + * @param _ValidRoundsNb Number of valid rounds played + */ +Void UpdateScoresTableFooter(Integer _PointsLimit, Integer _RoundsPerMap, Integer _MapsPerMatch, Integer _ValidRoundsNb) { + declare Text[] Parts; + declare Text Message = ""; + if (_PointsLimit > 0) { + if (Parts.count > 0) Message ^= "\n"; + Message ^= """%{{{Parts.count + 1}}}{{{_PointsLimit}}}"""; + //L16N [Rounds] Number of points to reach to win the match. + Parts.add(_("Points limit : ")); + } + if (_RoundsPerMap > 0) { + if (Parts.count > 0) Message ^= "\n"; + Message ^= """%{{{Parts.count + 1}}}{{{ML::Min(_ValidRoundsNb+1, _RoundsPerMap)}}}/{{{_RoundsPerMap}}}"""; + //L16N [Rounds] Number of rounds played during the track. + Parts.add(_("Rounds : ")); + } + if (_MapsPerMatch > 0) { + if (Parts.count > 0) Message ^= "\n"; + Message ^= """%{{{Parts.count + 1}}}{{{MB_GetMapCount()}}}/{{{_MapsPerMatch}}}"""; + //L16N [Rounds] Number of tracks played during the match. + Parts.add(_("Tracks : ")); + } + + 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])); + case 3: UIModules_ScoresTable::SetFooterInfo(TL::Compose(Message, Parts[0], Parts[1], Parts[2])); + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/// Compute round points of the prev race +Void ComputePoints() { + declare Ident[][Integer][Integer] Ranking = []; + + foreach (Score in Scores) { + declare Integer NumberOfCP = Score.PrevRaceTimes.count; + + declare Integer LastTime = 0; + if (NumberOfCP > 0) LastTime = Score.PrevRaceTimes[-1]; + + if (!Ranking.existskey(NumberOfCP)) Ranking[NumberOfCP] = []; + if (!Ranking[NumberOfCP].existskey(LastTime)) Ranking[NumberOfCP][LastTime] = []; + Ranking[NumberOfCP][LastTime].add(Score.Id); + } + + declare Integer[] PointsRepartition = PointsRepartition::GetPointsRepartition(); + declare Integer Index = 0; + + foreach (Times in Ranking.sortkeyreverse()) { + foreach (ScoresId in Times.sortkey()) { + foreach (ScoreId in ScoresId) { + if (!Scores.existskey(ScoreId)) continue; + + declare CSmScore Score <=> Scores[ScoreId]; + + declare Integer Points = 0; + if (PointsRepartition.count > 0) { + if (PointsRepartition.existskey(Index)) { + Points = PointsRepartition[Index]; + } else { + Points = PointsRepartition[-1]; + } + } + Scores::SetPlayerRoundPoints(Score, Points); + Index += 1; + } + } + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/// Compute the map scores +Void ComputeScores() { + Scores::EndRound(); +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** Eliminate Player + * + * @param _Player Player to eliminate + */ + Void EliminatePlayer(CSmPlayer _Player) { + Race::StopSkipOutro(_Player); + + if (_Player.User != Null) { + UIManager.UIAll.SendChat("$f00" ^ _Player.User.Name ^" elimitated"); + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** 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; +} + +Void CreateTimerUI() { + declare Text Manialink = """ + + + + +"""; + + + Layers::Create(C_ScriptName_Timer, Manialink); + Layers::SetType(C_ScriptName_Timer, CUILayer::EUILayerType::Normal); + Layers::Attach(C_ScriptName_Timer); +} \ No newline at end of file