/** * Royal Rounds mode */ #Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaRoundsBase.Script.txt" #Const CompatibleMapTypes "TrackMania\\TM_Royal,TM_Royal" #Const Version "2024-04-05" #Const ScriptName "Modes/TrackMania/TM_RoyalRounds_Online.Script.txt" // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Libraries // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Include "TextLib" as TL #Include "MathLib" as ML #Include "Libs/Nadeo/CMGame/Utils/Semver.Script.txt" as Semver #Include "Libs/Nadeo/CMGame/Modes/Utils.Script.txt" as ModeUtils #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/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage #Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/Chrono_Server.Script.txt" as UIModules_Chrono #Include "Libs/Nadeo/Trackmania/Modes/Rounds/UIModules/SmallScoresTable_Server.Script.txt" as UIModules_SmallScoresTable // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Settings // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Setting S_PointsLimit 100 as _("Points limit") #Setting S_FinishTimeout -1 as _("Finish timeout") #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_MapsPerMatch -1 as _("Number of maps per match") ///< Number of maps to play before finishing the match #Setting S_UseTieBreak True as _("Use tie-break") ///< Continue to play the map until the tie is broken #Setting S_SegmentsPerRound 5 as "Number of segment to end the round" #Setting S_StrictPointDistribution False as "Give points only to players who complete all segments" #Setting S_AllowCam7DuringWaitingScreen False as "" #Setting S_RoundWaitingScreenDuration 20 as _("Round waiting screen duration") //< Maximum time spent waiting for players at the beginning of each round #Setting S_FinisherBonusBase 5 as "Base of Bonus for finishers" #Setting S_FinisherBonusNumber 10 as "if only one player finish, they get S_FinisherBonusBase x This value. if 2, it's This - 1, etc..." #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") // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Constants // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // #Const C_ModeName "RoyalRounds" //L16N [Rounds] Description of the mode rules #Const Description _("$zIn $<$t$6F9RoyalRounds$z$z$> mode, the goal is to win a maximum number of $<$t$6F9points.\n\n$z$>The rounds mode consists of $<$t$6F9a series of races$z$>.\nWhen you finish a race in a good $<$t$6F9position$z$>, you get $<$t$6F9points$z$>, added to your total.\n\nThe $<$t$6F9winner$z$> is the first player whose total reaches the $<$t$6F9point limit$z$> (30 for example).") #Const C_HudModulePath "" //< Path to the hud module #Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/Trackmania/Modes/Rounds.Script.txt" //< Url of the mania app #Const C_FakeUsersNb 0 #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 #Const C_DisableSkipOutro True //< Prevent the players from pressing respawn/give up to cut the finish outro and respawn faster // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // 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); Log::RegisterScript(UIModules_Checkpoint::ScriptName, UIModules_Checkpoint::Version); Log::RegisterScript(UIModules_SmallScoresTable::ScriptName, UIModules_SmallScoresTable::Version); *** ***Match_LoadLibraries*** *** StateMgr::Load(); *** ***Match_UnloadLibraries*** *** StateMgr::Unload(); *** ***Match_Settings*** *** MB_Settings_UseDefaultHud = (C_HudModulePath == ""); MB_Settings_UseDefaultPodiumSequence = False; MB_Settings_UseDefaultIntroSequence = 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::SetVisibleFor(UIModules_Checkpoint::C_Target_None); UIModules_TimeGap::SetTimeGapMode(UIModules_TimeGap::C_TimeGapMode_Hidden); UIModules_PauseMenu_Online::SetHelp(Description); // Hide SM Overlay UIManager.UIAll.OverlayHideSpectatorInfos = True; UIManager.UIAll.OverlayHideCountdown = True; SetML(); *** ***Match_Yield*** *** foreach (Event in PendingEvents) { switch (Event.Type) { // Initialize players when they join the server case CSmModeEvent::EType::OnPlayerAdded: { StateMgr::InitializePlayer(Event.Player); } } } StateMgr::Yield(); *** ***Match_InitServer*** *** declare Integer Server_PointsLimit; declare Integer Server_RoundsPerMap; declare Integer Server_MapsPerMatch; declare Integer Server_SegmentsPerRound; *** ***Match_StartServer*** *** // Initialize mode Clans::SetClansNb(0); Scores::SaveInScore(Scores::C_Points_Match); StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); UsePvECollisions = False; //< Synchronize obstacles between all players Race::UseAutomaticDossardColor(False); Server_PointsLimit = S_PointsLimit - 1; Server_RoundsPerMap = S_RoundsPerMap - 1; Server_MapsPerMatch = S_MapsPerMatch - 1; Server_SegmentsPerRound = S_SegmentsPerRound - 1; *** ***Match_InitMap*** *** declare netwrite Text Net_ScriptEnvironment for Teams[0] = S_ScriptEnvironment; if (Net_ScriptEnvironment != S_ScriptEnvironment) { Net_ScriptEnvironment = S_ScriptEnvironment; } declare Integer Map_ValidRoundsNb; declare Boolean Map_Skipped; UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb, S_SegmentsPerRound); // Find start blocks declare CMapLandmark[] Starts = Map::GetStarts(); declare CMapLandmark[Integer] SortedStarts; foreach (Start in Starts) { SortedStarts[Start.Order] = Start; } SortedStarts = SortedStarts.sortkey(); declare CMapLandmark[] Map_Starts for This; Map_Starts = []; foreach (Start in SortedStarts) { Map_Starts.add(Start); } if (Map_Starts.count > 0) { Map::SetDefaultStart(Map_Starts[0]); } else { StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); UIManager.UIAll.QueueMessage(3000, 1, CUIConfig::EMessageDisplay::Big, _("This map is not valid")); MB_Sleep(3000); MB_StopMap(); } // Waiting screen declare Integer WaitingScreenDuration = 0; if (WaitingScreenDuration >= 0) { if (S_AllowCam7DuringWaitingScreen) ModeUtils::PushAndApplyUISequence(UIManager.UIAll, CUIConfig::EUISequence::Playing); else ModeUtils::PushAndApplyUISequence(UIManager.UIAll, CUIConfig::EUISequence::RollingBackgroundIntro); // Wait for the connection of the first valid player to start the countdown while (MB_MapIsRunning() && AllPlayers.count <= 0) { MB_Sleep(1000); } UIModules_BigMessage::SetMessage("""The map will start in few seconds"""); while (MB_MapIsRunning() && S_RoundWaitingScreenDuration - WaitingScreenDuration > 0) { if (S_RoundWaitingScreenDuration - WaitingScreenDuration <= 3) { ModeUtils::PlaySound(CUIConfig::EUISound::PhaseChange, 0); UIModules_BigMessage::SetMessage("""The map starts in {{{S_RoundWaitingScreenDuration - WaitingScreenDuration}}} seconds"""); } WaitingScreenDuration = WaitingScreenDuration + 1; MB_Sleep(1000); } UIModules_BigMessage::SetMessage(""); ModeUtils::PopAndApplyUISequence(UIManager.UIAll); } *** ***Match_StartMap*** *** // Add bot when necessary Users_SetNbFakeUsers(C_FakeUsersNb, 0); Map_Skipped = True; StartTime = Now + Race::C_SpawnDuration; UIModules_ScoresTable::SetFooterInfo(_("Warm up")); MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); *** ***Match_StartWarmUpRound*** *** declare netwrite Boolean Net_RoyalRounds_WarmUpUI_IsActive for Teams[0]; Net_RoyalRounds_WarmUpUI_IsActive = True; foreach (Player in Players) { declare netwrite Integer Net_RoyalRounds_WarmUpUI_SelectedSegment for Player = 1; Player.LandmarkOrderSelector_Race = Net_RoyalRounds_WarmUpUI_SelectedSegment; } *** ***Match_WarmUpLoop*** *** // Manage Custom UI Events foreach (Event in UIManager.PendingEvents) { Log::Log("[UIManager] Event.CustomEventType: " ^ Event.CustomEventType); if (TL::StartsWith("Request.WarmUp.Segment.", Event.CustomEventType)) { declare Text Target = Event.CustomEventData[0]; declare CSmPlayer Player = GetPlayer(Target); if (Player != Null) { declare CMapLandmark[] Map_Starts for This; declare netwrite Integer Net_RoyalRounds_WarmUpUI_SelectedSegment for Player = 1; declare Integer NewSegment; if (Event.CustomEventType == "Request.WarmUp.Segment.Minus") { if (Net_RoyalRounds_WarmUpUI_SelectedSegment == 1) NewSegment = 5; else NewSegment = Net_RoyalRounds_WarmUpUI_SelectedSegment - 1; } else if (Event.CustomEventType == "Request.WarmUp.Segment.Plus") { if (Net_RoyalRounds_WarmUpUI_SelectedSegment == 5) NewSegment = 1; else NewSegment = Net_RoyalRounds_WarmUpUI_SelectedSegment + 1; } Player.LandmarkOrderSelector_Race = NewSegment; Net_RoyalRounds_WarmUpUI_SelectedSegment = NewSegment; Race::SetPlayerDefaultStart(Player, Map_Starts[NewSegment-1]); Race::StopSkipOutro(Player); } } } *** ***Match_EndWarmUpRound*** *** declare netwrite Boolean Net_RoyalRounds_WarmUpUI_IsActive for Teams[0]; Net_RoyalRounds_WarmUpUI_IsActive = False; *** ***Rounds_SpawnPlayer*** *** declare Integer CurrentSegment for Player.Score = -1; // Init player if newly connected if (CurrentSegment == -1) { CurrentSegment = 1; CurrentRanking[0][Player.User.WebServicesUserId] = 0; if (!UpdateRankingSegments.exists(0)) UpdateRankingSegments.add(0); if (UpdateRankingTimer == 0) UpdateRankingTimer = Now + 1000; } declare Integer Index; if (CurrentSegment > Map_Starts.count) { Index = Map_Starts.count - 1; } else { Index = CurrentSegment - 1; } switch (CurrentSegment) { case 1: Player.Dossard_Color = Race::C_DossardColor_Default; // White case 2: Player.Dossard_Color = <0.063, 0.816, 0.125>; // Green case 3: Player.Dossard_Color = <0.125, 0.251, 0.753>; // Blue case 4: Player.Dossard_Color = <0.8, 0.2, 0.2>; // Red default: Player.Dossard_Color = <0.2, 0.2, 0.2>; // Black } Player.LandmarkOrderSelector_Race = Index + 1; declare Integer SpecificStartTime = Now + Race::C_SpawnDuration; if (IsStartRound) SpecificStartTime = StartTime; Race::Start(Player, Map_Starts[Index] , SpecificStartTime); *** ***Rounds_PlayerSpawned*** *** declare Integer SpecificOffset = Player.StartTime - StartTime; if (IsStartRound) SpecificOffset = 0; UIModules_Chrono::SetTimeOffset(Player, SpecificOffset); *** ***Match_InitRound*** *** declare netwrite Integer Net_RoyalRounds_CheckpointUI_TotalNbSegments for Teams[0]; declare Integer[Text] CustomTimes for This = []; declare Integer[Text][Integer] CurrentRanking for This = []; // CurrentRanking[Segment][AccountId][Time] CurrentRanking[0] = []; // Init white section declare Boolean IsStartRound = True; // Reset players for the race foreach (Score in Scores) { declare Integer CurrentSegment for Score = -1; CurrentSegment = 1; CurrentRanking[0][Score.User.WebServicesUserId] = 0; } declare Integer UpdateRankingTimer = 1; declare Integer[] UpdateRankingSegments; UpdateRankingSegments.add(0); *** ***Match_StartRound*** *** UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb, S_SegmentsPerRound); StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]); IsStartRound = False; *** ***Match_PlayLoop*** *** // Manage race events foreach (Event in Race::GetPendingEvents()) { if (Event.Type == Events::C_Type_SkipOutro && C_DisableSkipOutro) { Race::InvalidEvent(Event); } else { Race::ValidEvent(Event); // Waypoint if (Event.Type == Events::C_Type_Waypoint) { if (Event.Player != Null) { if (Event.IsEndRace) { declare Integer CurrentSegment for Event.Player.Score = -1; Log::Log("""[RacePendingEvents] Player {{{Event.Player.User.Name }}} end the segment {{{CurrentSegment}}} at {{{Event.Player.StartTime - StartTime + Event.RaceTime}}}"""); if (CurrentRanking.existskey(CurrentSegment-1)) CurrentRanking[CurrentSegment-1].removekey(Event.Player.User.WebServicesUserId) ; if (!CurrentRanking.existskey(CurrentSegment)) CurrentRanking[CurrentSegment] = []; CurrentRanking[CurrentSegment][Event.Player.User.WebServicesUserId] = Event.Player.StartTime - StartTime + Event.RaceTime; // Update Ranking of the current players declare Integer Rank = 1; declare Integer I = 1; Rank = CurrentRanking[CurrentSegment].count; while (CurrentRanking.existskey(CurrentSegment + I)) { Rank = Rank + CurrentRanking[CurrentSegment + I].count; I = I + 1; } Event.Player.Dossard_Number = TL::FormatInteger(ML::Clamp(Rank, 0, 99), 2); // Send Rank to Checkpoint UI declare netwrite Integer Net_RoyalRounds_CheckpointUI_Rank for Event.Player; Net_RoyalRounds_CheckpointUI_Rank = Rank; declare netwrite Integer Net_RoyalRounds_CheckpointUI_Update for Event.Player; Net_RoyalRounds_CheckpointUI_Update += 1; declare netwrite Integer Net_RoyalRounds_CheckpointUI_CurrentNbSegments for Event.Player; Net_RoyalRounds_CheckpointUI_CurrentNbSegments = CurrentSegment; if (CurrentSegment < S_SegmentsPerRound) { // TODO Try to keep CP diff a the bottom of the screen declare Boolean ModeRounds_CanSpawn for Event.Player.Score = Rounds_Settings_CanSpawnDefault; ModeRounds_CanSpawn = True; CurrentSegment = CurrentSegment + 1; Race::StopSkipScoresTable(Event.Player); } else { Scores::UpdatePlayerPrevRace(Event.Player); UpdateCustomRanking(Event.Player); Race::SortScores(Race::C_Sort_TotalPoints); if (EndTime <= 0) { EndTime = GetFinishTimeout(S_FinishTimeout); } } // Add to trigger UpdateRanking for all players affected if (!UpdateRankingSegments.exists(CurrentSegment-1)) UpdateRankingSegments.add(CurrentSegment-1); if (UpdateRankingTimer == 0) UpdateRankingTimer = Now + 1000; } } } else if (Event.Type == Events::C_Type_GiveUp) { Log::Log("""[RacePendingEvents] Player {{{Event.Player.User.Name }}} give-up"""); if (Event.Player != Null) { declare Integer CurrentSegment for Event.Player.Score = -1; if (CurrentRanking.existskey(CurrentSegment-1)) CurrentRanking[CurrentSegment-1].removekey(Event.Player.User.WebServicesUserId); while (CurrentSegment >= 1) { if (!UpdateRankingSegments.exists(CurrentSegment-1)) UpdateRankingSegments.add(CurrentSegment-1); CurrentSegment = CurrentSegment - 1; } if (UpdateRankingTimer == 0) UpdateRankingTimer = Now + 1000; } } } } // Manage mode events foreach (Event in PendingEvents) { if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; Events::Invalid(Event); } if (UpdateRankingTimer > 0 && Now > UpdateRankingTimer) { UpdateRankingSegments = UpdateRankingSegments.sortreverse(); foreach (Segment in UpdateRankingSegments) { Log::Log("[UpdateRanking] Updating ranking of players of the segment " ^ Segment); declare Integer I = 1; declare Integer Rank = 1; while (CurrentRanking.existskey(Segment + I)) { Rank = Rank + CurrentRanking[Segment + I].count; I = I + 1; } if (CurrentRanking.existskey(Segment)) { if (Segment == 0) Rank = Rank + CurrentRanking[0].count - 1; foreach (ID => Time in CurrentRanking[Segment]) { declare CSmPlayer Player = ModeUtils::GetPlayerFromAccountId(ID); if (Player != Null) Player.Dossard_Number = TL::FormatInteger(ML::Clamp(Rank, 0, 99), 2); if (Segment != 0) Rank = Rank + 1; } } } UpdateRankingTimer = 0; UpdateRankingSegments.clear(); } // Server info change if ( Server_PointsLimit != S_PointsLimit || Server_RoundsPerMap != S_RoundsPerMap || Server_MapsPerMatch != S_MapsPerMatch || Server_SegmentsPerRound != S_SegmentsPerRound ) { Server_PointsLimit = S_PointsLimit; Server_RoundsPerMap = S_RoundsPerMap; Server_MapsPerMatch = S_MapsPerMatch; Server_SegmentsPerRound = S_SegmentsPerRound; UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb, S_SegmentsPerRound); } if (Net_ScriptEnvironment != S_ScriptEnvironment) { Net_ScriptEnvironment = S_ScriptEnvironment; } if (Net_RoyalRounds_CheckpointUI_TotalNbSegments != S_SegmentsPerRound) { Net_RoyalRounds_CheckpointUI_TotalNbSegments = S_SegmentsPerRound; } *** ***Rounds_CheckStopRound*** *** // End the round // If All players finished if (Players.count > 0 && PlayersNbAlive <= 0) { declare Boolean NoOneCanPlay = True; foreach (Player in Players) { declare Boolean ModeRounds_CanSpawn for Player.Score = Rounds_Settings_CanSpawnDefault; if (ModeRounds_CanSpawn) { NoOneCanPlay = False; break; } } if (NoOneCanPlay) { MB_StopRound(); Round_Skipped = False; } } // 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; // Keep Playing State to keep SmallScoresTable visible StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]); 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 UpdateCustomRanking(Null); Race::SortScores(Race::C_Sort_TotalPoints); UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; MB_Sleep(3000); Race::SortScores(Race::C_Sort_TotalPoints); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; MB_Sleep(3000); // 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(); } } StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); CustomTimes.clear(); CurrentRanking.clear(); UIModules_SmallScoresTable::ResetCustomTimes(); UIModules_SmallScoresTable::ResetCustomResults(); *** ***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); Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_MatchPoints)); *** // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Functions // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Update the scores table footer text * * @param _PointsLimit The points limit * @param _RoundsPerMap The number of round per map * @param _MapsPerMatch The number of maps per match * @param _ValidRoundsNb Number of valid rounds played * @param _SegmentsPerRound The number of segment per round */ Void UpdateScoresTableFooter(Integer _PointsLimit, Integer _RoundsPerMap, Integer _MapsPerMatch, Integer _ValidRoundsNb, Integer _SegmentsPerRound) { declare Text[] Parts; declare Text Message = ""; if (_PointsLimit > 0) { if (Parts.count > 0) Message ^= " | "; 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 ^= " | "; Message ^= """%{{{Parts.count + 1}}}{{{ML::Min(_ValidRoundsNb+1, _RoundsPerMap)}}}/{{{_RoundsPerMap}}}"""; //L16N [Rounds] Number of rounds played during the map. Parts.add(_("Rounds : ")); } if (_MapsPerMatch > 0) { if (Parts.count > 0) Message ^= " | "; Message ^= """%{{{Parts.count + 1}}}{{{MB_GetMapCount()}}}/{{{_MapsPerMatch}}}"""; //L16N [Rounds] Number of maps played during the match. Parts.add(_("Maps : ")); } if (_SegmentsPerRound != 5) { if (Parts.count > 0) Message ^= " | "; Message ^= """%{{{Parts.count + 1}}}{{{_SegmentsPerRound}}}"""; Parts.add("Segments : "); } 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])); case 4: UIModules_ScoresTable::SetFooterInfo(TL::Compose(Message, Parts[0], Parts[1], Parts[2], Parts[3])); } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** Get the time left to the players to finish the round after the first player * * @return The time left in ms */ Integer GetFinishTimeout(Integer _FinishTimeout) { declare Integer FinishTimeout = 0; if (_FinishTimeout >= 0) { FinishTimeout = _FinishTimeout * 1000; } else { FinishTimeout = 5000 + Map.TMObjective_AuthorTime / 6; } return Now + FinishTimeout; } /** Update the Scores Table with Real Time * * @param _Player The Player who end the round */ Void UpdateCustomRanking(CSmPlayer _Player) { declare Integer[Text] CustomTimes for This; declare Text[Text] CustomResult; declare Integer[Text][Integer] CurrentRanking for This = []; declare Integer[] PointsRepartition = PointsRepartition::GetPointsRepartition(); if (_Player != Null) { declare Integer CurrentSegment for _Player.Score; CustomTimes[_Player.User.WebServicesUserId] = CurrentRanking[CurrentSegment][_Player.User.WebServicesUserId]; } else if (!S_StrictPointDistribution) { CustomTimes.clear(); CurrentRanking = CurrentRanking.sortkeyreverse(); declare Integer LastTimeFromThePreviousSegment = 0; declare Integer LastTime = 0; foreach (Segment => DummyVar in CurrentRanking) { if (Segment == 0) break; if (CustomTimes.count > 0) LastTimeFromThePreviousSegment = LastTime; foreach (ID => Time in CurrentRanking[Segment]) { CustomTimes[ID] = Time + LastTimeFromThePreviousSegment; LastTime = Time + LastTimeFromThePreviousSegment; if (LastTimeFromThePreviousSegment > 0) CustomResult[ID] = """({{{Segment - S_SegmentsPerRound}}}) {{{TL::TimeToText(Time, True, True)}}}"""; } } } declare Integer BonusForFinishers = 0; if (S_FinisherBonusBase > 0 && S_FinisherBonusNumber > 0 && CurrentRanking.existskey(S_SegmentsPerRound)) { BonusForFinishers = ML::Max(0, S_FinisherBonusBase * (S_FinisherBonusNumber + 1 - CurrentRanking[S_SegmentsPerRound].count)); if (_Player == Null && BonusForFinishers > 0) {// If End round UIManager.UIAll.SendChat("$<$ff3$> " ^ CurrentRanking[S_SegmentsPerRound].count ^ " players received " ^ BonusForFinishers ^ " bonus points for being the only finishers" ); } } if (PointsRepartition.count > 0) { CustomTimes = CustomTimes.sort(); declare Integer I = 0; foreach (AccountId => DummyVar in CustomTimes) { declare CSmPlayer Player = ModeUtils::GetPlayerFromAccountId(AccountId); if (Player == Null) continue; declare Integer Points = 0; if (PointsRepartition.existskey(I)) { Points = PointsRepartition[I]; } else { Points = PointsRepartition[PointsRepartition.count - 1]; } declare Integer CurrentSegment for Player.Score; if (CurrentSegment == S_SegmentsPerRound && Scores::GetPlayerPrevRaceTime(Player.Score) > 0) { Points += BonusForFinishers; } Scores::SetPlayerRoundPoints(Player.Score, Points); I += 1; } } UIModules_SmallScoresTable::SetCustomTimes(CustomTimes); UIModules_SmallScoresTable::SetCustomResults(CustomResult); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /// Compute the map scores Void ComputeScores() { Scores::EndRound(); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // /** 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) { if (PointsLimitReached(_UseTieBreak, _PointsLimit) == 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; } /** * Set the UI */ Void SetML() { declare Text MLText = """