/**
 *	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 = """
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<manialink version="3" name="{{{C_ScriptName_Timer}}}">
<frame id="frame-global" pos="0 30" hidden="1">
	<label id="label-timer" halign="center" valign="center" textprefix="$s" textsize="5" textfont="RajdhaniMono" textcolor="ffffff" hidden="1"/>
</frame>
<script><!--
#Include "TextLib" as TL

#Const ScriptName {{{dump(C_ScriptName_Timer)}}}
#Const Version {{{dump(Version)}}}

{{{dumptype(K_CheckpointState)}}}

main() {
	log("Init "^ ScriptName ^ " v"^ Version);
	declare CMlFrame Frame_Global <=> (Page.GetFirstChild("frame-global") as CMlFrame);
	declare CMlLabel Label_Timer <=> (Frame_Global.GetFirstChild("label-timer") as CMlLabel);

	// Wait C++ initialize the player
	wait (InputPlayer != Null);

	declare netread K_CheckpointState[Integer] Net_EnduroCup_Checkpoints for Teams[0] = [];

	while(True) {
		yield;

		if (!PageIsVisible) continue;

		Frame_Global.Visible = (GUIPlayer != Null && GUIPlayer.SpawnStatus == CSmPlayer::ESpawnStatus::Spawned);

		if (!Frame_Global.Visible) continue;

		declare K_CheckpointState CheckpointState = Net_EnduroCup_Checkpoints.get(GUIPlayer.RaceWaypointTimes.count + 1, K_CheckpointState {});

		Label_Timer.Visible = (CheckpointState.RaceTime > 0 && CheckpointState.RaceTime < GUIPlayer.CurrentRaceTime);
		
		if (!Label_Timer.Visible) continue;

		declare Integer TimeLeft = CheckpointState.RaceTime + CheckpointState.TimeToReach - GUIPlayer.CurrentRaceTime;

		if (TimeLeft <= 0) {
			Label_Timer.Value = "$f0000:00.000";
		} else {
			Label_Timer.Value = TL::TimeToText(TimeLeft, True, True);
		}
	}
}
--></script>
</manialink>
""";


	Layers::Create(C_ScriptName_Timer, Manialink);
	Layers::SetType(C_ScriptName_Timer, CUILayer::EUILayerType::Normal);
  Layers::Attach(C_ScriptName_Timer);
}