diff --git a/TM_RoyalRounds_Online.Script.txt b/TM_RoyalRounds_Online.Script.txt new file mode 100644 index 0000000..70bb60f --- /dev/null +++ b/TM_RoyalRounds_Online.Script.txt @@ -0,0 +1,718 @@ +/** +* Royal Rounds mode +*/ +#Extends "Libs/Nadeo/TMNext/TrackMania/Modes/TMNextBase.Script.txt" + +#Const CompatibleMapTypes "TrackMania\\TM_Royal,TM_Royal" +#Const Version "2021-08-06" +#Const ScriptName "Modes/TrackMania/TM_RoyalRounds_Online.Script.txt" + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Libraries +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Include "TextLib" as TL +#Include "MathLib" as ML +#Include "Libs/Nadeo/CommonLibs/Common/Semver.Script.txt" as Semver + +#Include "Libs/Nadeo/TMxSM/Race/PointsRepartition.Script.txt" as PointsRepartition +#Include "Libs/Nadeo/TMxSM/Race/Pause.Script.txt" as RacePause +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/TimeGap_Server.Script.txt" as UIModules_TimeGap +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenuOnline +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/SpectatorBase_Server.Script.txt" as UIModules_SpectatorBase + +#Include "Libs/Nadeo/ModeLibs/Common/Utils.Script.txt" as ModeUtils +#Include "Libs/Nadeo/TMNext/TrackMania/Menu/Constants.Script.txt" as MenuConsts +#Include "Libs/Nadeo/TMNext/TrackMania/Modes/Rounds/StateManager.Script.txt" as StateMgr +#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/TMxSM/Race/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/Chrono_Server.Script.txt" as UIModules_Chrono + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// 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_SegmentsPerRounds 5 as "Number of segment to end the round" +#Setting S_RoundWaitingScreenDuration 20 as _("Round waiting screen duration") //< Maximum time spent waiting for players at the beginning of each round +#Setting S_PointsRepartition "" as _("Custom points distribution") //< comma separated points distribution. eg: "10,6,4,3,2,1" + +/*#Setting S_WarmUpNb 0 as _("Number of warm up") // (Impossible at the moment https://forum.nadeo.com/viewtopic.php?f=51&p=8745#p8745) +#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/TMNext/TrackMania/Rounds/Rounds.Script.txt" //< Url of the mania app + +#Const C_FakeUsersNb 0 + +#Const C_PointsRepartition [10, 6, 4, 3, 2, 1] ///< Default points repartition in rounds based modes. Can be overrided by S_PointsRepartition. +#Const C_Method_ForceEndRound "Trackmania.ForceEndRound" + +#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 + +declare Boolean Rounds_Settings_CanSpawnDefault; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// 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(PointsRepartition::ScriptName, PointsRepartition::Version); +Log::RegisterScript(RacePause::ScriptName, RacePause::Version); +Log::RegisterScript(UIModules_Checkpoint::ScriptName, UIModules_Checkpoint::Version); +*** + +***Match_LoadLibraries*** +*** +XmlRpc::RegisterMethod(C_Method_ForceEndRound, """ +* Name: {{{C_Method_ForceEndRound}}} +* Type: TriggerModeScriptEventArray +* Description: Stop the current round. Only available in Cup, Rounds and Team modes. +* Data: + - Version >=2.0.0: + ``` + [] + ``` +"""); + +StateMgr::Load(); +PointsRepartition::Load(); +RacePause::Load(); +*** + +***Match_UnloadLibraries*** +*** +StateMgr::Unload(); +RacePause::Unload(); +PointsRepartition::Unload(); + +XmlRpc::UnregisterMethod(C_Method_ForceEndRound); +*** + +***Match_Settings*** +*** +MB_Settings_UseDefaultHud = (C_HudModulePath == ""); +MB_Settings_UseDefaultPodiumSequence = False; +MB_Settings_UseDefaultIntroSequence = False; + +MB_Settings_UseDefaultTimer = False; +Race_Settings_ResetPlayerRaceBetweenRounds = True; + +Rounds_Settings_CanSpawnDefault = True; +*** + +***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*** +*** +UIModules_TimeGap::SetTimeGapMode(UIModules_TimeGap::C_TimeGapMode_CurRace); +UIModules_PauseMenuOnline::SetAllowPrevReplay(True); + +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_Checkpoint::SetAutoUISequenceFinish(False); +UIModules_PauseMenu_Online::SetHelp(Description); +// Hide SM Overlay +UIManager.UIAll.OverlayHideSpectatorControllers = True; +UIManager.UIAll.OverlayHideSpectatorInfos = True; +UIManager.UIAll.OverlayHideChrono = True; +UIManager.UIAll.OverlayHideCountdown = True; +*** + +***Match_Yield*** +*** +foreach (Event in XmlRpc.PendingEvents) { + if (Event.Type == CXmlRpcEvent::EType::CallbackArray) { + switch (Event.ParamArray1) { + case PointsRepartition::C_Method_SetPointsRepartition: { + declare Integer[] Rounds_PointsRepartitionBackUp for This; + Rounds_PointsRepartitionBackUp = PointsRepartition::GetPointsRepartition(); + } + } + } +} + +declare Rounds_PointsRepartitionSetting for This = S_PointsRepartition; +if (Rounds_PointsRepartitionSetting != S_PointsRepartition) { + Rounds_PointsRepartitionSetting = S_PointsRepartition; + + declare PointsRepartition = C_PointsRepartition; + if (S_PointsRepartition != "") { + declare NewPointsRepartition = PointsRepartition::ConvertPointsRepartition(S_PointsRepartition); + if (NewPointsRepartition.count > 0) { + PointsRepartition = NewPointsRepartition; + } + } + PointsRepartition::SetPointsRepartition(PointsRepartition); + declare Integer[] Rounds_PointsRepartitionBackUp for This; + Rounds_PointsRepartitionBackUp = PointsRepartition::GetPointsRepartition(); +} + +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); + } + } +} + +PointsRepartition::Yield(); +StateMgr::Yield(); +*** + +***Match_InitServer*** +*** +declare Integer Server_PointsLimit; +declare Integer Server_RoundsPerMap; +declare Integer Server_MapsPerMatch; +*** + +***Match_StartServer*** +*** +declare Integer[] Rounds_PointsRepartitionBackUp for This; +// Reload XmlRpc or load default values +if (Rounds_PointsRepartitionBackUp.count > 0) { + PointsRepartition::SetPointsRepartition(Rounds_PointsRepartitionBackUp); +} else { + declare PointsRepartition = C_PointsRepartition; + if (S_PointsRepartition != "") { + declare NewPointsRepartition = PointsRepartition::ConvertPointsRepartition(S_PointsRepartition); + if (NewPointsRepartition.count > 0) { + PointsRepartition = NewPointsRepartition; + } + } + PointsRepartition::SetPointsRepartition(PointsRepartition); + Rounds_PointsRepartitionBackUp = PointsRepartition::GetPointsRepartition(); +} + +// Enable the pause system +Pause::SetAvailability(True); + +// Initialize mode +Clans::SetClansNb(0); +Scores::SaveInScore(Scores::C_Points_Match); +StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); +UsePvECollisions = True; //< Synchronize obstacles between all players +WarmUp::SetAvailability(False); // (Impossible at the moment https://forum.nadeo.com/viewtopic.php?f=51&p=8745#p8745) + +Server_PointsLimit = S_PointsLimit - 1; +Server_RoundsPerMap = S_RoundsPerMap - 1; +Server_MapsPerMatch = S_MapsPerMatch - 1; +*** + +***Match_InitMap*** +*** +declare Integer Map_ValidRoundsNb; + +UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb); + +declare CMapLandmark[] Map_Starts; +declare Integer Map_NextEmptyArmorCheckTime; + +// Find start blocks +declare CMapLandmark[] Starts = Map::GetStarts(); +declare CMapLandmark[Integer] SortedStarts; +foreach (Start in Starts) { + SortedStarts[Start.Order] = Start; +} +SortedStarts = SortedStarts.sortkey(); +foreach (Start in SortedStarts) { + Map_Starts.add(Start); +} + +if (Map_Starts.count > 0) { + Map::SetDefaultStart(Map_Starts[0]); +} + +// We use `>= 0` and not `> 0` here because the waiting screen +// has two steps. First waiting for the presence of at least one player. +// Then waiting the desired amount of time. With `>= 0` we can have +// the first step without the second. +declare Integer WaitingScreenDuration = S_RoundWaitingScreenDuration; +if (WaitingScreenDuration >= 0) { + 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); + } + + // If all players are connected before the end of the countdown start immediatly + declare Integer WaitEndTime = Now + (WaitingScreenDuration * 1000); + + while (WaitingScreenDuration > 0) { + if (WaitingScreenDuration > 3) { + UIModules_BigMessage::SetMessage("""The map starts in {{{WaitingScreenDuration}}} seconds"""); + } else { + UIModules_BigMessage::SetMessage(""); + UIManager.UIAll.BigMessage = """The map starts in {{{WaitingScreenDuration}}} seconds"""; + } + WaitingScreenDuration = WaitingScreenDuration - 1; + MB_Sleep(1000); + } + UIManager.UIAll.BigMessage = ""; + UIModules_BigMessage::SetMessage(""); + + ModeUtils::PopAndApplyUISequence(UIManager.UIAll); +} +*** + +***Match_StartMap*** +*** +// Add bot when necessary +Users_SetNbFakeUsers(C_FakeUsersNb, 0); +StartTime = Now + Race::C_SpawnDuration; + +CarRank::Reset(); + +// Warm up (Impossible at the moment https://forum.nadeo.com/viewtopic.php?f=51&p=8745#p8745) +/*UIModules_ScoresTable::SetFooterInfo(_("Warm up")); +MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); +*/ +*** + +***Match_InitRound*** +*** +declare Boolean Round_ForceEndRound = False; +declare Boolean Round_SkipPauseRound = False; //< Skip the current round after the pause +declare Boolean Round_Skipped = True; //< Round skipped for another reason +*** + +***Match_StartRound*** +*** +// Initialize round +StartTime = Now + Race::C_SpawnDuration; +EndTime = -1; +Round_ForceEndRound = False; +Round_SkipPauseRound = False; + +// Initialize scores +foreach (Score in Scores) { + if (Score.PrevRaceTimes.count > 0) { + Score_ClearPrevRace(Score); + } + Scores::SetPlayerRoundPoints(Score, 0); +} + +// Setup pause +if (Pause::IsActive()) { + StartTime = Now; + +++Rounds_StartPause+++ + while (RacePause::Loop(Pause::IsActive())) { + MB_Yield(); + +++Rounds_PauseLoop+++ + } + StartTime = -1; + +++Rounds_EndPause+++ + + Round_SkipPauseRound = True; + MB_StopRound(); +} + ++++Rounds_WaitForPlayers+++ + +MB_Yield(); //< Yield to wait for everyone to be ready +StartTime = Now + Race::C_SpawnDuration; + +UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb); +StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]); + +// Spawn players for the race +foreach (Player in Players) { + declare Integer CurrentSegment for Player.Score = 1; + CurrentSegment = 1; + Player.LandmarkOrderSelector_Race = CurrentSegment; + declare Boolean ModeRounds_CanSpawn for Player.Score = Rounds_Settings_CanSpawnDefault; + ModeRounds_CanSpawn = False; + Race::Start(Player, Map_Starts[0] , StartTime); + UIModules_Chrono::SetTimeOffset(Player, 0); +} +*** + +***Rounds_PlayerSpawned*** +*** +CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); +*** + +***Match_PlayLoop*** +*** +// Manage XmlRpc events +foreach (Event in XmlRpc.PendingEvents) { + if (Event.Type == CXmlRpcEvent::EType::CallbackArray) { + switch (Event.ParamArray1) { + case C_Method_ForceEndRound: { + Round_ForceEndRound = True; + } + } + } +} + +// Pause activation +if (Pause::IsActive()) { + Round_ForceEndRound = True; +} + +// 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; +} + +// Manage race events +declare RacePendingEvents = Race::GetPendingEvents(); +foreach (Event in RacePendingEvents) { + if (Event.Type == Events::C_Type_SkipOutro && C_DisableSkipOutro) { + Race::InvalidEvent(Event); + } else { + Race::ValidEvent(Event); + + // Waypoint + if (Event.Type == Events::C_Type_Waypoint) { + CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); + if (Event.Player != Null) { + if (Event.IsEndRace) { + declare Integer CurrentSegment for Event.Player.Score = 1; + if (CurrentSegment < S_SegmentsPerRounds) { // TODO Try to keep CP diff a the bottom of the screen + CurrentSegment = CurrentSegment + 1; + Race::StopSkipScoresTable(Event.Player); + declare Boolean ModeRounds_CanSpawn for Event.Player.Score = Rounds_Settings_CanSpawnDefault; + ModeRounds_CanSpawn = True; + } else { + Scores::UpdatePlayerBestRaceIfBetter(Event.Player); + Scores::UpdatePlayerBestLapIfBetter(Event.Player); + Scores::UpdatePlayerPrevRace(Event.Player); + ComputeLatestRaceScores(); + Race::SortScores(Race::C_Sort_TotalPoints); + if (EndTime <= 0) { + EndTime = GetFinishTimeout(S_FinishTimeout); + } + } + } + } + } else if (Event.Type == Events::C_Type_GiveUp) { + if (Event.Player != Null) { + UIModules_SpectatorBase::SetCamModeAndFocus(Event.Player, UIModules_SpectatorBase::C_CamModes_Follow); + } + } + } +} + +// 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 + declare Boolean NoOneCanPlay = True; + foreach (Player in Players) { + declare Boolean ModeRounds_CanSpawn for Player.Score = Rounds_Settings_CanSpawnDefault; + if (Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned && ModeRounds_CanSpawn) { + NoOneCanPlay = False; + if (Race::IsReadyToStart(Player)) { + ModeRounds_CanSpawn = False; + declare Integer CurrentSegment for Player.Score = 1; + declare Integer Index; + if (CurrentSegment > Map_Starts.count) { + Index = Map_Starts.count - 1; + } else { + Index = CurrentSegment - 1; + } + Player.LandmarkOrderSelector_Race = Index + 1; + Race::Start(Player, Map_Starts[Index] , Now + Race::C_SpawnDuration); + UIModules_Chrono::SetTimeOffset(Player, Player.StartTime - StartTime); + } + } + } + if (NoOneCanPlay && PlayersNbAlive <= 0) MB_StopRound(); +} + +// Server info change +if ( + Server_PointsLimit != S_PointsLimit || + Server_RoundsPerMap != S_RoundsPerMap || + Server_MapsPerMatch != S_MapsPerMatch +) { + Server_PointsLimit = S_PointsLimit; + Server_RoundsPerMap = S_RoundsPerMap; + Server_MapsPerMatch = S_MapsPerMatch; + + UpdateScoresTableFooter(S_PointsLimit, S_RoundsPerMap, S_MapsPerMatch, Map_ValidRoundsNb); +} +*** + +***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; + // Get the last round points + ComputeLatestRaceScores(); + 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); + 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)) MB_StopMap(); +} +*** + +***Match_EndMap*** +*** +if (MatchIsOver(S_UseTieBreak, S_PointsLimit, S_MapsPerMatch, S_RoundsPerMap)) 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 + */ +Void UpdateScoresTableFooter(Integer _PointsLimit, Integer _RoundsPerMap, Integer _MapsPerMatch, Integer _ValidRoundsNb) { + 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 : ")); + } + + 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])); + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/** 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; + 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; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/// Compute the latest race scores +Void ComputeLatestRaceScores() { + Race::SortScores(Race::C_Sort_PrevRaceTime); + + // Points distributed between all players + declare Integer I = 0; + declare Integer[] PointsRepartition = PointsRepartition::GetPointsRepartition(); + foreach (Score in Scores) { + if (Scores::GetPlayerPrevRaceTime(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); + } + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +/// 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 _MapsPerMatch, Integer _RoundsPerMap) { + declare Integer PointsLimitReached = PointsLimitReached(_UseTieBreak, _PointsLimit); + + if (_MapsPerMatch > 1 && PointsLimitReached == C_PointsLimit_Tie) return False; //< Ties are allowed if the map was skipped and match is played on one map only + if (PointsLimitReached == C_PointsLimit_Reached) return True; //< There is a points limit and it is reached + if (_MapsPerMatch > 1 && MB_GetMapCount() >= _MapsPerMatch) return True; //< There is a maps limit and it is reached + if (_MapsPerMatch <= 1 && _RoundsPerMap <= 0) return True; + + return False; +} + +// Display a message if the round was skipped +Void ForcedEndRoundSequence() { + declare PrevUISequence = UIManager.UIAll.UISequence; + declare PrevBigMessage = UIManager.UIAll.BigMessage; + declare PrevBigMessageSound = UIManager.UIAll.BigMessageSound; + declare PrevBigMessageSoundVariant = UIManager.UIAll.BigMessageSoundVariant; + UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; + UIManager.UIAll.BigMessage = _("Round skipped"); + UIManager.UIAll.BigMessageSound = CUIConfig::EUISound::EndRound; + UIManager.UIAll.BigMessageSoundVariant = 0; + MB_Sleep(3000); + UIManager.UIAll.BigMessageSoundVariant = PrevBigMessageSoundVariant; + UIManager.UIAll.BigMessageSound = PrevBigMessageSound; + UIManager.UIAll.BigMessage = PrevBigMessage; + UIManager.UIAll.UISequence = PrevUISequence; +} \ No newline at end of file