/** * Reverse Cup mode * based on the mode by BossBravo (https://github.com/BossBravo/Trackmania2020_LastManStandingCup) */ // #RequireContext CSmMode #Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaRoundsBase.Script.txt" #Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" #Const Version "2023-10-16" #Const ScriptName "Modes/TM2020-Gamemodes/TM_ReverseCup.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 Menu_Const #Include "Libs/Nadeo/Trackmania/Modes/CupCommon/Constants.Script.txt" as CupCommon_Const #Include "Libs/Nadeo/Trackmania/Modes/Cup/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/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_WarmUpNb 0 as _("Number of warm up") #Setting S_WarmUpDuration 20 as _("Duration of one warm up") #Setting S_WarmUpTimeout -1 as _("Warm up timeout") #Setting S_NbOfWinners 1 as _("Number of winners") #Setting S_FinishTimeout -1 as _("Finish timeout") #Setting S_ComplexPointsRepartition "" as "JSON of PointsRepartition depending for the number of the Players Alive" // Example: {"3": [3, 6, 10], "4,5": [1, 3, 6,10]} #Setting S_PointsRepartition "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20" #Setting S_RoundsPerMap 5 as _("Number of rounds per map") ///< Number of round to play on one map before going to the next one #Setting S_PointsStartup 100 as _("Points at start") #Setting S_DisableLastChance False as _("When a player reach 0 points he is automatically eliminated") #Setting S_AllowFastForwardRounds True as _("If whatever the issue of the round, all players will be in Last Chance, the round will be skipped to the next without playing it (all players will be in LastChance).") #Setting S_FastForwardPointsRepartition True as "Accelerate the distribution of points when the number of players alive decreases " #Setting S_DNF_LossPoints 20 as _("Number of points for player that give up a round") #Setting S_LastChance_DNF_Mode 0 as "0 = Every Players in Last Chance who DNF will be eliminated | 1 = Only the Player in Last Chance who passed the less checkpoints and DNF will be eliminated, others will stay alive" #Setting S_NbOfPlayers 0 as "Number of players awaited before starting the match (0 is automatic)" #Setting S_EnableCollisions False // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Constants // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Const C_ModeName "Reverse Cup" #Const Description _("$zThe cup mode consists of $<$t$6F9a series of races on multiple maps$>.\n\nWhen you finish a race in a bad $<$t$6F9position$>, you loose $<$t$6F9points$> substracted from your total.\nServers might propose warmup races to get familiar with a map first.\n\nTo win, you must be the last player with points. Once you are a LastChance, if you finish a race last you will be eliminated.The cup mode ends once there is one player left.") #Const C_HudModulePath "" //< Path to the hud module #Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/Trackmania/Modes/Cup.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 #Const C_Color_LastChance "FF0000" #Const C_Color_Eliminated "FF0000" #Const C_Color_Spectator "48DA36" #Const C_Text_LastChance "Last Chance" #Const C_Text_Eliminated _("|Status|Eliminated") #Const C_Text_Spectator _("|Status|Spectator") #Const C_Points_LastChance -1000 #Const C_Points_Eliminated -2000 #Const C_Points_Spectator -10000 #Struct K_MatchInfo { Boolean RegistrationClosed; Text[] Participants; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Globales // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // declare Integer G_NbOfValidRounds; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Extends // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ***Match_LogVersions*** *** Log::RegisterScript(ScriptName, Version); Log::RegisterScript(Semver::ScriptName, Semver::Version); Log::RegisterScript(StateMgr::ScriptName, StateMgr::Version); *** ***Match_LoadLibraries*** *** StateMgr::Load(); *** ***Match_UnloadLibraries*** *** StateMgr::Unload(); *** ***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_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_Points); UIModules_Checkpoint::SetVisibilityTimeDiff(False); UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_CurrentRace); UIModules_PauseMenu_Online::SetHelp(Description); // Hide SM Overlay UIManager.UIAll.OverlayHideSpectatorInfos = True; UIManager.UIAll.OverlayHideCountdown = True; // Unload default UI UIModules::UnloadModules(["UIModule_Rounds_SmallScoresTable"]); SetManialink_LiveRace(); *** ***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); declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; if (Server_MatchInfo.RegistrationClosed && !Server_MatchInfo.Participants.exists(Event.Player.User.Login)) { Scores::SetPlayerMatchPoints(Event.Player.Score, C_Points_Spectator); // Equivalent of getCustomPoints: declare netwrite Text[][Text] Net_TMGame_ScoresTable_CustomPoints for Teams[0] = []; Net_TMGame_ScoresTable_CustomPoints[Event.Player.User.WebServicesUserId] = [C_Text_Spectator, C_Color_Spectator]; } } } declare netwrite Integer Net_ReverseCup_LiveRanking_Update for Teams[0] = 0; Net_ReverseCup_LiveRanking_Update += 1; } StateMgr::Yield(); *** ***Match_InitServer*** *** declare Integer Server_PointsLimit; declare Integer Server_RoundsPerMap; declare Integer Server_NbOfWinners; declare Integer Server_DNF_LossPoints; declare Text Server_ComplexPointsRepartition; *** ***Match_StartServer*** *** // Initialize mode Clans::SetClansNb(0); Scores::SaveInScore(Scores::C_Points_Match); Scores::EnablePlayerNegativePoints(True, True, True); StateMgr::ForcePlayersStates([CupCommon_Const::C_State_Waiting]); WarmUp::SetAvailability(True); Race::SetupRecord( Menu_Const::C_ScopeType_Season, Menu_Const::C_ScopeType_PersonalBest, Menu_Const::C_GameMode_TimeAttack, "", C_UploadRecord, C_DisplayRecordGhost, C_DisplayRecordMedal, C_CelebrateRecordGhost, C_CelebrateRecordMedal ); Race::UseAutomaticDossardColor(False); Server_PointsLimit = S_PointsStartup; Server_RoundsPerMap = S_RoundsPerMap; Server_NbOfWinners = S_NbOfWinners; Server_DNF_LossPoints = S_DNF_LossPoints; Server_ComplexPointsRepartition = S_ComplexPointsRepartition; *** ***Match_StartMatch*** *** UIModules_ScoresTable::SetCustomPoints([]); UpdateComplexPointsRepartition(S_ComplexPointsRepartition); declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; Server_MatchInfo = K_MatchInfo {}; *** ***Match_InitMap*** *** declare netwrite Text Net_ScriptEnvironment for Teams[0] = S_ScriptEnvironment; if (Net_ScriptEnvironment != S_ScriptEnvironment) { Net_ScriptEnvironment = S_ScriptEnvironment; } UIModules_ScoresTable::DisplayRoundPoints(True); G_NbOfValidRounds = 0; UpdateScoresTableFooter(S_PointsStartup, S_RoundsPerMap, G_NbOfValidRounds, S_NbOfWinners); *** ***Match_StartMap*** *** // Add bot when necessary Users_SetNbFakeUsers(C_FakeUsersNb, 0); CarRank::Reset(); declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; if (!Server_MatchInfo.RegistrationClosed) { if (S_NbOfPlayers != 0) { declare Integer FuturStartTime = 0; declare Integer Last_PlayersNb = -1; while (MB_MapIsRunning() && (FuturStartTime == 0 || FuturStartTime > Now)) { if (Last_PlayersNb != Players.count) { Last_PlayersNb = Players.count; if (Last_PlayersNb < S_NbOfPlayers) { UIModules_BigMessage::SetMessage(_("Waiting for players")); FuturStartTime = 0; } else if (Last_PlayersNb > S_NbOfPlayers) { UIModules_BigMessage::SetMessage("Too many players"); FuturStartTime = 0; } else { UIModules_BigMessage::SetMessage("Match starting in 3 seconds"); ModeUtils::PlaySound(CUIConfig::EUISound::PhaseChange, 0); FuturStartTime = Now + 3000; } } MB_Yield(); } UIModules_BigMessage::SetMessage(""); } Scores::Clear(); foreach (Player in Players) { if (Player.User == Null) continue; if (Player.Score == Null) continue; Server_MatchInfo.Participants.add(Player.User.Login); Scores::SetPlayerMatchPoints(Player.Score, S_PointsStartup); } //UIModules_ScoresTable::DisplayOnly(Server_MatchInfo.Participants); // Bugged foreach (Spectator in Spectators) { if (Spectator.User == Null) continue; if (Spectator.Score == Null) continue; Scores::SetPlayerMatchPoints(Spectator.Score, C_Points_Spectator); } DisplayCustomPoints(); } Server_MatchInfo.RegistrationClosed = True; // Warm up foreach (Score in Scores) { if (Score.User != Null && Server_MatchInfo.Participants.exists(Score.User.Login) && Score.Points >= C_Points_LastChance) { WarmUp::CanPlay(Score, True); } else { WarmUp::CanPlay(Score, False); } } UIModules_ScoresTable::SetFooterInfo(_("Warm up")); MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); *** ***Match_StartWarmUp*** *** declare netwrite Integer Net_ReverseCup_LiveRanking_Update for Teams[0] = 0; Net_ReverseCup_LiveRanking_Update += 1; *** ***Rounds_CheckCanSpawn*** *** if (_Player.User == Null) return False; declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; if (!Server_MatchInfo.Participants.exists(_Player.User.Login)) return False; if (Scores::GetPlayerMatchPoints(_Player.Score) < C_Points_LastChance) return False; *** ***Match_InitRound*** *** if (S_EnableCollisions) { Race::SetNetworkMode(False, False); UsePvPCollisions = True; UsePvECollisions = True; } else { Race::SetNetworkMode( !Race_Settings_IsLocalMode && !S_IsSplitScreen && S_TrustClientSimu, !Race_Settings_IsLocalMode && !S_IsSplitScreen && S_UseCrudeExtrapolation ); UsePvPCollisions = False; UsePvECollisions = False; } ModeUtils::PlaySound(CUIConfig::EUISound::PhaseChange, 0); if (S_RoundsPerMap > 0) { UIModules_BigMessage::SetMessage(_("Round: ") ^ TL::ToText(G_NbOfValidRounds + 1) ^ " / " ^ TL::ToText(S_RoundsPerMap)); } else { UIModules_BigMessage::SetMessage(_("Round: ") ^ TL::ToText(G_NbOfValidRounds + 1)); } MB_Sleep(3000); UIModules_BigMessage::SetMessage(""); declare Integer Round_NbPlayersInThisRound = 0; *** ***Match_StartRound*** *** UpdateScoresTableFooter(S_PointsStartup, S_RoundsPerMap, G_NbOfValidRounds, S_NbOfWinners); declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; foreach (Score in Scores) { if (Score.User == Null) continue; if (Scores::GetPlayerMatchPoints(Score) < C_Points_LastChance) continue; if (!Server_MatchInfo.Participants.exists(Score.User.Login)) continue; Round_NbPlayersInThisRound += 1; } CheckRoundBeforePlay(Round_NbPlayersInThisRound); UpdateDNFLossPoints(Server_DNF_LossPoints); StateMgr::ForcePlayersStates([CupCommon_Const::C_State_Playing]); UpdateLiveRaceUI(); *** ***Match_StartPlayLoop*** *** // Update dossard color foreach (Player in Players) { if (Player.Score != Null && Scores::GetPlayerMatchPoints(Player.Score) == C_Points_LastChance) { Player.Dossard_Color = <0.7, 0., 0.>; } else { Player.Dossard_Color = Race::C_DossardColor_Default; } } *** ***Rounds_PlayerSpawned*** *** CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); *** ***Match_PlayLoop*** *** // 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::UpdatePlayerBestRaceIfBetter(Event.Player); Scores::UpdatePlayerBestLapIfBetter(Event.Player); Scores::UpdatePlayerPrevRace(Event.Player); ComputeLatestRaceScores(Round_NbPlayersInThisRound); Race::SortScores(Race::C_Sort_TotalPoints); // Start the countdown if it's the first player to finish if (EndTime <= 0) { EndTime = GetFinishTimeout(); +++Cup_PlayLoop_FirstPlayerFinishRace+++ } } if (Event.IsEndLap) { Scores::UpdatePlayerBestLapIfBetter(Event.Player); } } } UpdateLiveRaceUI(); } // Manage mode events foreach (Event in PendingEvents) { if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; Events::Invalid(Event); } if (Net_ScriptEnvironment != S_ScriptEnvironment) { Net_ScriptEnvironment = S_ScriptEnvironment; } // Server info change if (Server_PointsLimit != S_PointsStartup || Server_RoundsPerMap != S_RoundsPerMap || Server_NbOfWinners != S_NbOfWinners || Server_DNF_LossPoints != S_DNF_LossPoints) { Server_PointsLimit = S_PointsStartup; Server_RoundsPerMap = S_RoundsPerMap; Server_NbOfWinners = S_NbOfWinners; Server_DNF_LossPoints = S_DNF_LossPoints; UpdateScoresTableFooter(S_PointsStartup, S_RoundsPerMap, G_NbOfValidRounds, S_NbOfWinners); UpdateDNFLossPoints(Server_DNF_LossPoints); } if (Server_ComplexPointsRepartition != S_ComplexPointsRepartition) { Server_ComplexPointsRepartition = S_ComplexPointsRepartition; UpdateComplexPointsRepartition(Server_ComplexPointsRepartition); } *** ***Match_EndRound*** *** Race::StopSkipOutroAll(); EndTime = -1; StateMgr::ForcePlayersStates([CupCommon_Const::C_State_Waiting]); CarRank::Update(CarRank::C_SortCriteria_CurrentRace); UpdateLiveRaceUI(); // Compute round points before the callback ComputeLatestRaceScores(Round_NbPlayersInThisRound); 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); DisplayCustomPoints(); MB_Sleep(3000); } else { +++Cup_EndRound_BeforeScoresUpdate+++ Race::SortScores(Race::C_Sort_TotalPoints); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; MB_Sleep(3000); // Add them to the total scores ComputeScores(); Race::SortScores(Race::C_Sort_TotalPoints); +++Cup_EndRound_AfterScoresUpdate+++ DisplayCustomPoints(); MB_Sleep(3000); +++Cup_EndRound_BeforeScoresTableEnd+++ UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing; UIModules_BigMessage::SetMessage(""); if (MatchIsOver()) { MB_StopMatch(); } else if (MapIsOver()) { MB_StopMap(); } } UpdateLiveRaceUI(); *** ***Match_EndMap*** *** UIModules_ScoresTable::DisplayRoundPoints(False); Race::SortScores(Race::C_Sort_TotalPoints); if (MB_MatchIsRunning()) { MB_SkipPodiumSequence(); } else { declare CSmScore Eliminated <=> Scores::GetBestPlayer(Scores::C_Sort_MatchPoints); Scores::SetPlayerWinner(Eliminated); } *** ***Match_BeforePodiumSequence*** *** ModeUtils::PlaySound(CUIConfig::EUISound::EndRound, 0); if (!MB_Private_SkipPodiumSequence) { declare Text[] WinnersNames; foreach (Player in Players) { if(!Spectators.exists(Player)) { if (Scores::GetPlayerMatchPoints(Player.Score) >= C_Points_LastChance) { if (Player.User.ClubTag == "") { WinnersNames.add(Player.User.Name); } else { WinnersNames.add("[$<"^Player.User.ClubTag^"$>] " ^ Player.User.Name); } } } } if(WinnersNames.count >= 1) { UIModules_BigMessage::SetMessage(TL::Compose(_("$<%1$> wins the match!"), TL::Join(", ", WinnersNames))); UIManager.UIAll.SendChat(TL::Compose(_("$<%1$> wins the match!"), TL::Join(", ", WinnersNames))); } else { UIModules_BigMessage::SetMessage(_("|Match|Draw")); } } *** ***Match_PodiumSequence*** *** declare CUIConfig::EUISequence PrevUISequence = UIManager.UIAll.UISequence; UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium; MB_Private_Sleep((S_ChatTime*1000)/2); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; MB_Private_Sleep((S_ChatTime*1000)/2); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; UIManager.UIAll.UISequence = PrevUISequence; *** ***Match_AfterPodiumSequence*** *** UIModules_BigMessage::SetMessage(""); UIModules_ScoresTable::ResetTrophies(); *** // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Functions // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Void UpdateDNFLossPoints(Integer _DNF_LossPoints) { declare netwrite Integer Net_ReverseCup_DNF_LossPoints for Teams[0] = 0; Net_ReverseCup_DNF_LossPoints = _DNF_LossPoints; } /** Update the scores table footer text * * @param _PointsLimit The points limit * @param _RoundsPerMap The number of rounds per map * @param _ValidRoundsNb Number of valid rounds played * @param _NbOfWinners Number of Winners */ Void UpdateScoresTableFooter(Integer _PointsLimit, Integer _RoundsPerMap, Integer _ValidRoundsNb, Integer _NbOfWinners) { declare Text[] Parts; declare Text Message = ""; if (_PointsLimit > 0) { if (Parts.count > 0) Message ^= "\n"; Message ^= """%{{{Parts.count + 1}}}{{{_PointsLimit}}}"""; //L16N [TM_Cup_Online] Number of points to reach to win the match. Parts.add("Initial health : "); } 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 map. Parts.add(_("Rounds : ")); } if (_NbOfWinners > 1) { if (Parts.count > 0) Message ^= "\n"; Message ^= """%{{{Parts.count + 1}}}{{{_NbOfWinners}}}"""; //L16N [Rounds] Number of rounds played during the map. Parts.add(_("Nb of Winners : ")); } 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])); } } Void UpdateComplexPointsRepartition(Text _ComplexPointsRepartition) { declare Integer[][Integer] Server_ComplexPointsRepartition for This = []; Server_ComplexPointsRepartition = []; declare Integer[][Text] TextComplexPointsRepartition; TextComplexPointsRepartition.fromjson(_ComplexPointsRepartition); foreach (TextMultipleRemainingPlayers => PointsRepartition in TextComplexPointsRepartition) { declare Text[] MultipleRemainingPlayers = TL::Split(",", TextMultipleRemainingPlayers); foreach (TextRemainingPlayers in MultipleRemainingPlayers) { Server_ComplexPointsRepartition[TL::ToInteger(TextRemainingPlayers)] = PointsRepartition; } } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** 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; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Announce a new looser in the chat * * @param _Name The name of the new looser * @param _Rank The rank of the new looser */ Void AnnounceEliminated(Text _Name, Integer _Rank) { declare Text Message = ""; switch (_Rank) { case 1: Message = TL::Compose("$f90$i$<%1$> takes 1st place!", _Name); case 2: Message = TL::Compose("$f90$i$<%1$> takes 2nd place!", _Name); case 3: Message = TL::Compose("$f90$i$<%1$> takes 3rd place!", _Name); default: Message = TL::Compose("$f90$i$<%1$> takes %2th place!", _Name, TL::ToText(_Rank)); } UIManager.UIAll.SendChat(Message); UIModules_BigMessage::SetMessage(Message); } Integer[] GetPointsRepartition(Integer _NbPlayersInThisRound) { declare Integer[] PointsRepartition; declare Integer[][Integer] Server_ComplexPointsRepartition for This = []; if (Server_ComplexPointsRepartition.existskey(_NbPlayersInThisRound)) { PointsRepartition = Server_ComplexPointsRepartition[_NbPlayersInThisRound]; } else { PointsRepartition = PointsRepartition::GetPointsRepartition(); } if (S_FastForwardPointsRepartition && _NbPlayersInThisRound > PointsRepartition.count) { foreach (Score in Scores) { if (_NbPlayersInThisRound <= PointsRepartition.count) break; declare Integer Points = Scores::GetPlayerMatchPoints(Score); if (Points > C_Points_Spectator && Points <= C_Points_Eliminated) { PointsRepartition = PointsRepartition.slice(1, PointsRepartition.count - 1); } } } return PointsRepartition; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /// Compute the latest race scores Void ComputeLatestRaceScores(Integer _NbPlayersInThisRound) { Race::SortScores(Race::C_Sort_PrevRaceTime); // Points distributed between all players declare Integer Key = 0; declare CSmPlayer[] LastChancePlayersDNF_WithWorstCheckpoints; declare Integer MinCheckpointNbPassed = 9999; declare Integer[] PointsRepartition = GetPointsRepartition(_NbPlayersInThisRound); foreach (Score in Scores) { // Skip Spectators and already eliminated players if (Scores::GetPlayerMatchPoints(Score) < C_Points_LastChance) continue; if (Scores::GetPlayerPrevRaceTime(Score) > 0) { declare Integer Points = 0; if (PointsRepartition.count > 0) { if (PointsRepartition.existskey(Key)) { Points = 0 - PointsRepartition[Key]; } else { Points = 0 - PointsRepartition[PointsRepartition.count - 1]; } } Scores::SetPlayerRoundPoints(Score, Points); Key += 1; } else { // Apply DNF penality if Disconnected if (Score.User == Null) { Scores::SetPlayerRoundPoints(Score, 0 - S_DNF_LossPoints); continue; } declare CSmPlayer Player <=> GetPlayer(Score.User.Login); // Apply DNF penality if Disconnected if (Player == Null) { Scores::SetPlayerRoundPoints(Score, 0 - S_DNF_LossPoints); continue; } if(S_LastChance_DNF_Mode == 1) { if(Scores::GetPlayerMatchPoints(Score) == C_Points_LastChance) { if(Player.LapWaypointTimes.count < MinCheckpointNbPassed) { MinCheckpointNbPassed = Player.LapWaypointTimes.count; LastChancePlayersDNF_WithWorstCheckpoints.clear(); LastChancePlayersDNF_WithWorstCheckpoints.add(Player); } else if(Player.LapWaypointTimes.count == MinCheckpointNbPassed) { LastChancePlayersDNF_WithWorstCheckpoints.add(Player); } } else if (Scores::GetPlayerMatchPoints(Score) > C_Points_LastChance) { Scores::SetPlayerRoundPoints(Score, 0 - S_DNF_LossPoints); } } else { Scores::SetPlayerRoundPoints(Score, 0 - S_DNF_LossPoints); } } } if(S_LastChance_DNF_Mode == 1) { foreach(Player in LastChancePlayersDNF_WithWorstCheckpoints) { Scores::SetPlayerRoundPoints(Player.Score, 0 - S_DNF_LossPoints); } } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /// Compute the map scores Void ComputeScores() { declare Boolean RoundIsValid = False; declare Integer NbOfEliminated = 0; declare CSmScore[] NewEliminated; Race::SortScores(Race::C_Sort_TotalPoints); declare Integer MaxRoundPoints = 0; foreach (Score in Scores) { if (MaxRoundPoints > Scores::GetPlayerRoundPoints(Score) && Scores::GetPlayerMatchPoints(Score) > C_Points_Eliminated) MaxRoundPoints = Scores::GetPlayerRoundPoints(Score); } foreach (Score in Scores) { if (Scores::GetPlayerMatchPoints(Score) == C_Points_Spectator) continue; if (!RoundIsValid && Scores::GetPlayerRoundPoints(Score) < 0) RoundIsValid = True; declare Integer NewMatchPoints = Scores::GetPlayerMatchPoints(Score) + Scores::GetPlayerRoundPoints(Score); // Already loose if (NewMatchPoints < C_Points_Eliminated) { NbOfEliminated += 1; continue; } // New LastChance looser else if (NewMatchPoints > C_Points_LastChance && NewMatchPoints <= 0) { if(S_DisableLastChance == False) { Scores::SetPlayerMatchPoints(Score, C_Points_LastChance); } else { NbOfEliminated += 1; NewEliminated.add(Score); continue; } } // New looser else if (NewMatchPoints > C_Points_Eliminated && NewMatchPoints < C_Points_LastChance && Scores::GetPlayerRoundPoints(Score) == MaxRoundPoints) { NbOfEliminated += 1; NewEliminated.add(Score); } // Already LastChance and not last else if (Scores::GetPlayerMatchPoints(Score) == C_Points_LastChance && Scores::GetPlayerRoundPoints(Score) != MaxRoundPoints) { Scores::SetPlayerMatchPoints(Score, C_Points_LastChance); } // Standard round finish else { Scores::AddPlayerMatchPoints(Score, Scores::GetPlayerRoundPoints(Score)); if (NewMatchPoints < C_Points_Eliminated) Scores::SetPlayerMatchPoints(Score, C_Points_Eliminated); } Scores::AddPlayerMapPoints(Score, Scores::GetPlayerRoundPoints(Score)); Scores::SetPlayerRoundPoints(Score, 0); } if(NewEliminated.count > 0) { declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; declare Integer Rank = Server_MatchInfo.Participants.count - NbOfEliminated + 1; foreach (Score in NewEliminated) { Scores::SetPlayerMatchPoints(Score, C_Points_Eliminated - Rank); if (Score.User != Null) { AnnounceEliminated(Score.User.Name, Rank); } } } if (RoundIsValid) G_NbOfValidRounds += 1; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check if we should go to the next map * * @return True if it is the case, false otherwise */ Boolean MapIsOver() { if (S_RoundsPerMap > 0 && G_NbOfValidRounds >= S_RoundsPerMap) return True; return False; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check if we have found all the loosers * * @return True if the match is over, false otherwise */ Boolean MatchIsOver() { Log::Log("[Cup] MatchIsOver() check | S_PointsStartup : "^S_PointsStartup); declare Integer NbOfPlayersActive = 0; foreach (Score in Scores) { if (Scores::GetPlayerMatchPoints(Score) >= C_Points_LastChance) NbOfPlayersActive += 1; } declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {}; if (Server_MatchInfo.Participants.count == 1) { return NbOfPlayersActive == 0; } return S_NbOfWinners >= NbOfPlayersActive; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check round before playing it, if necessary to fast forward it * */ Void DisplayCustomPoints() { // Display Spectator, LastChance & Eliminated UI declare Text[][Text] CustomPoints; foreach (Score in Scores) { if (Score.User == Null) continue; if (Scores::GetPlayerMatchPoints(Score) == C_Points_LastChance) { CustomPoints[Score.User.WebServicesUserId] = [C_Text_LastChance, C_Color_LastChance]; } else if (Scores::GetPlayerMatchPoints(Score) == C_Points_Spectator) { CustomPoints[Score.User.WebServicesUserId] = [C_Text_Spectator, C_Color_Spectator]; } else if (Scores::GetPlayerMatchPoints(Score) <= C_Points_Eliminated) { CustomPoints[Score.User.WebServicesUserId] = [C_Text_Eliminated, C_Color_Eliminated]; } } UIModules_ScoresTable::SetCustomPoints(CustomPoints); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Check round before playing it, if necessary to fast forward it * */ Void CheckRoundBeforePlay(Integer _NbPlayersInThisRound) { if(S_AllowFastForwardRounds == True) { declare Integer[] PointsRepartition = GetPointsRepartition(_NbPlayersInThisRound); declare Boolean IsRoundFastForwardCompatible = True; // Check if no players are LastChance or have more points than the minimum points foreach (Score in Scores) { declare Integer PlayerMatchPoints = Scores::GetPlayerMatchPoints(Score); if (PlayerMatchPoints == C_Points_LastChance || PlayerMatchPoints > PointsRepartition[0]) { IsRoundFastForwardCompatible = False; break; } } // if no one, do : if(IsRoundFastForwardCompatible == True) { foreach (Score in Scores) { declare Integer PlayerMatchPoints = Scores::GetPlayerMatchPoints(Score); if (PlayerMatchPoints > C_Points_Eliminated) { Scores::SetPlayerMatchPoints(Score, C_Points_LastChance); } } ModeUtils::PlaySound(CUIConfig::EUISound::EndRound, 0); UIModules_BigMessage::SetMessage("$c00This round is fast-forwarded. Every remaining players are now in Last Chance."); DisplayCustomPoints(); MB_Sleep(5000); UIModules_BigMessage::SetMessage(""); MB_StopRound(); } } } Void UpdateLiveRaceUI() { declare netwrite Integer Net_ReverseCup_LiveRanking_Update for Teams[0] = 0; Net_ReverseCup_LiveRanking_Update += 1; } Void SetManialink_LiveRace() { declare Text MLText = """