/**
 *	Chase mode
 */
#Extends "Modes/TrackMania/Base/ModeTrackmania.Script.txt"

#Const	CompatibleMapTypes	"Race"
#Const	Version							"2017-03-09"
#Const	ScriptName					"Modes/TrackMania/Chase/Chase.Script.txt"

// ---------------------------------- //
// Libraries
// ---------------------------------- //
#Include "TextLib" as TL
#Include "MathLib" as ML
#Include "Libs/Nadeo/TrackMania/WarmUp4.Script.txt" as WarmUp2

// ---------------------------------- //
// Settings
// ---------------------------------- //
#Setting S_TimeLimit						900		as _("Time limit :")
#Setting S_MapPointsLimit			3			as _("Map points limit :")
#Setting S_RoundPointsLimit		-5		as _("Round points limit :")
#Setting S_RoundPointsGap			3			as _("Points gap :")
#Setting S_GiveUpMax						1			as _("Maximum number of give up :")
#Setting S_MinPlayersNb				3			as _("Minimum number of players in a team :")
#Setting S_ForceLapsNb					10		as _("Number of Laps :")
#Setting S_FinishTimeout				-1		as _("Finish timeout :")
#Setting S_DisplayWarning			True	as _("Display a warning message when relay fails :")
#Setting S_CompetitiveMode			False	as _("Use competitive mode :")
#Setting S_PauseBetweenRound		15		as _("Pause duration between rounds :")
#Setting S_WaitingTimeMax			600		as _("Maximum waiting time before next map :")
#Setting S_WaypointEventDelay	500		as "<hidden>" // _("Waypoint event delay :")
#Setting S_WarmUpNb						0			as _("Number of warm up :")
#Setting S_WarmUpDuration			0			as _("Duration of one warm up :")
// Matchmaking
#Setting S_NbPlayersPerTeamMax	3		as "<hidden>" //_("Maximum number of players per team in matchmaking")
#Setting S_NbPlayersPerTeamMin	3		as "<hidden>" //_("Minimum number of players per team in matchmaking")

#Setting S_ScriptEnvironment "production"/*/"development"*/

// ---------------------------------- //
// Commands
// ---------------------------------- //
#Command Command_RoundPointsClan1	(Integer)	as _("Round points for clan 1")
#Command Command_RoundPointsClan2	(Integer)	as _("Round points for clan 2")
#Command Command_SetPause					(Boolean)	as _("Pause the game")
#Command Command_ForceEndRound			(Boolean)	as _("Force round end")

// ---------------------------------- //
// Constants
// ---------------------------------- //
#Const C_WinType_Undefined		0	///< No winning conditions met
#Const C_WinType_PointsGap 	1	///< Win by points gap
#Const C_WinType_PointsLimit	2	///< Win by reaching points limit
#Const C_WinType_Finish			3	///< Win by finishing first
#Const C_WinType_Forfeit			4	///< Win by forfeit
#Const C_WinType_Time				5	///< Time limit
#Const C_WinType_Draw				6	///< Draw

#Const C_EndRoundDuration					5000	///< Duration of the end round sequence
#Const C_SubstituteWaitingTime			15000	///< Max waiting time for substitutes
#Const C_MissingPlayersWaitingTime	45000	///< Max waiting time for missing players
#Const C_NoNameCheckpoint					"-"		///< Default name when no player is selected for next checkpoint

#Const C_HudModulePath "Nadeo/TrackMania/Chase/Hud.Module.Gbx" ///< Path to the hud module
#Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/TrackMania/Chase/Chase.Script.txt"

#Const C_BlueBotsNb	0	///< Number of bots in the blue team
#Const C_RedBotsNb		0	///< Number of bots in the red team

// L16N [Chase] Short description of the Chase game mode
#Const Description _("""$fffIn $f00Chase$fff mode, the goal is to reach the $f00points limit$fff first. To score one $f00point$fff the $f00last$fff player of a team passing a checkpoint must be the $f00first$fff at the next one.""")


// ---------------------------------- //
// Extends
// ---------------------------------- //
***MM_SetupMatchmaking***
***
MM_SetFormat([S_NbPlayersPerTeamMax, S_NbPlayersPerTeamMax]);
***

***Lobby_MatchRulesManialink***
***
ManialinkRules = """<label posn="-60 25" sizen="120 50" autonewline="1" maxline="10" textemboss="1" textsize="2" text="{{{Description}}}" />""";
***

***Match_LogVersion***
***
MB_LogVersion(ScriptName, Version);
***

***Match_Settings***
***
MB_Settings_UseDefaultHud = False;
***

***Match_Rules***
***
ModeInfo::SetName("Chase");
ModeInfo::SetType(ModeInfo::C_Type_Teams);
ModeInfo::SetRules(Description);
ModeInfo::SetStatusMessage("");
***

***Match_LoadHud***
***
ClientManiaAppUrl = C_ManiaAppUrl;
Hud_Load(C_HudModulePath);
MB_SortScores(CTmMode::ETmScoreSortOrder::BestRace_CheckpointsProgress);

// Initialize UI modules
UI::LoadModules([
	UIModules::C_Module_TimeGap,
	UIModules::C_Module_Chrono,
	UIModules::C_Module_CheckpointTime,
	UIModules::C_Module_PrevBestTime,
	UIModules::C_Module_SpeedAndDistance,
	UIModules::C_Module_Countdown,
	UIModules::C_Module_Laps,
	UIModules::C_Module_MapInfo
]);
UI::DisplayTimeDiff(False);

UIManager.UIAll.OverlayHidePosition = True;
***

***Match_InitServer***
***
declare Integer[Integer] RelayStartTime;	///< Start time of the relay
declare Integer PrevTimeLimit;
declare Integer StartTime;
declare Boolean UseCompetitiveMode;
***

***Match_StartServer***
***
// ---------------------------------- //
// Initialize mode
UseClans = True;
PrevTimeLimit = S_TimeLimit;
SetLapsNb(S_ForceLapsNb, StartTime);
StartTime = -1;

// ---------------------------------- //
// Force round/lap synchro of the cars
// In time attack mode the synchro of the car is less strict, 
// creating a small delay between the real position of the player 
// and the position of his car on the screen
UiRounds = True;
UiLaps = True;

// ---------------------------------- //
// Create the advanced warm up
if (S_CompetitiveMode) {
	UseCompetitiveMode = True;
	WarmUp2::Load();
	WarmUp2::CreateGroup("Clan1", S_MinPlayersNb);
	WarmUp2::CreateGroup("Clan2", S_MinPlayersNb);
	WarmUp2::DisplayClanSelection(True);
	WarmUp2::SetLayerPosition(<0., 78.>);
} else {
	UseCompetitiveMode = False;
}

// ---------------------------------- //
// Initialize UI
UI::LoadModules(["TimeGap", "Chrono", "CheckpointTime", "PrevBestTime", "SpeedAndDistance", "Countdown", "Laps"]);
UI::DisplayTimeDiff(False);
Layers::Create("ChaseInfo", GetMLChaseInfo());
UIManager.UIAll.TeamLabelsVisibility = CUIConfig::ELabelsVisibility::WhenInFrustum;
UIManager.UIAll.TeamLabelsShowNames = CUIConfig::EVisibility::ForcedVisible;
UIManager.UIAll.OpposingTeamLabelsVisibility = CUIConfig::ELabelsVisibility::Never;
UIManager.UIAll.OpposingTeamLabelsShowNames = CUIConfig::EVisibility::ForcedVisible;

// ---------------------------------- //
// Register callbacks
/* @XmlRpc
XmlRpc::RegisterCallback("Chase_Pause", """
* Data : An array with one value saying if the mode is in pause or not
* Example : ["True"]
* Note : This callback is sent after using the `Chase_GetPause` method or when the pause status changes.
* Version : available since Combo.Script.txt_v2014-09-15
""");
*/
***

***Match_InitMap***
***
declare Integer MapWinType; ///< How the map was won
declare RoundPointsLimit = GetRoundPointsLimit(); ///< Round points limit

SetLapsNb(S_ForceLapsNb, StartTime);
UpdateScoresTableFooter(RoundPointsLimit);

// ---------------------------------- //
// Initialize scores
Scores_Clear();
//ST2::ClearScores();
declare netwrite Net_Chase_MapPoints for Teams[0] = Integer[Integer];
declare netwrite Net_Chase_RoundPoints for Teams[0] = Integer[Integer];
declare netwrite Net_Chase_PointsGap for Teams[0] = S_RoundPointsGap;
Net_Chase_MapPoints.clear();
Net_Chase_RoundPoints.clear();
Net_Chase_PointsGap = S_RoundPointsGap;
UpdateClanScores();
***

***Match_StartMap***
***
// ---------------------------------- //
// Initialize map
Users_SetNbFakeUsers (C_BlueBotsNb, C_RedBotsNb);
UpdateScoresTableFooter(RoundPointsLimit);
MapWinType = C_WinType_Undefined;

if (MM_IsMatchServer()) {
	MM_SetScores([0, 0]);
} else {
	// ---------------------------------- //
	// Competitive mode
	if (UseCompetitiveMode) {
		WarmUp(False);
	}
	// ---------------------------------- //
	// Warm up
	else {
		declare WarmUpDuration = S_WarmUpDuration * 1000;
		MB_WarmUp(S_WarmUpNb, WarmUpDuration);
	}
}
***

***Match_InitRound***
***
declare Ident[][Integer] RoundPlayers; ///< Ids of the players for this round
declare Ident[][Integer] RoundGiveUp; ///< Number of give up
declare Ident[][Ident][Integer][Integer] Progress; ///< Current progress of the race
declare Ident[][Ident][Integer][Integer] CurrentCheckpoint; ///< Current progress at the current checkpoint
declare Ident[Integer] NextCheckpointPlayerId;	///< Id of the player that must cross the next checkpoint
declare Integer RoundWinType; ///< How the round was won
declare Boolean FirstFinish; ///< First player finishing the map
declare ForceEndRound for This = False; ///< Force the round to end
declare Integer[Text] WaypointEvents_Now; ///< Time at which the event occured
declare Integer[Text] WaypointEvents_RaceTime; ///< Time of the player
declare Ident[Text] WaypointEvents_PlayerId; ///< Id of the player
declare Ident[Text] WaypointEvents_BlockId; ///< Id of the block
declare Boolean[Text] WaypointEvents_IsEndRace; ///< Is the finish
declare Integer[Text] WaypointEvents_CheckpointInRace; ///< Number of checkpoint crossed
***

***Match_StartRound***
***
ForceEndRound = False;

// ---------------------------------- //
// Initialize scores
TM::ResetAllScores();
//ST2::ClearScores();
Net_Chase_RoundPoints.clear();
Net_Chase_PointsGap = S_RoundPointsGap;
RoundWinType = C_WinType_Undefined;
UpdateClanScores();

// ---------------------------------- //
// Initialize checkpoints
declare netwrite Integer Net_Chase_NextPlayerUpdate for Teams[0];
declare netwrite Text[Integer] Net_Chase_NextPlayer for Teams[0];
declare netwrite Integer[Integer] Net_Chase_NextCheckpoint for Teams[0];
declare netwrite Integer[Integer] Net_Chase_RelayTime for Teams[0];
Net_Chase_NextPlayer.clear();
Net_Chase_NextCheckpoint.clear();
Net_Chase_RelayTime.clear();
Net_Chase_NextPlayerUpdate = Now;
foreach (Player in AllPlayers) {
	declare UI <=> UIManager.GetUI(Player);
	UI.MarkersXML = "";
}

// ---------------------------------- //
// Wait minimum number of players in each clan
if (MM_IsMatchServer()) {
	if (!EnoughPlayers()) {
		WaitForPlayers(C_MissingPlayersWaitingTime);
		if (!EnoughPlayers()) {
			RoundWinType = C_WinType_Forfeit;
			MapWinType = C_WinType_Forfeit;
			MB_StopMatch();
		}
	}
} else if (UseCompetitiveMode) {
	WarmUp2::Clean();
	if (
		WarmUp2::GetPlayersNb("Clan1") != WarmUp2::GetSlotsNb("Clan1") ||
		WarmUp2::GetPlayersNb("Clan2") != WarmUp2::GetSlotsNb("Clan1") /*||
		XmlRpc::PauseRequested()*/ //< @pause @xmlrpc
	) {
		WarmUp(True);
		// if (XmlRpc::PauseRequested()) XmlRpc::EndPause(); //< @pause @xmlrpc
	}
} else {
	if (!EnoughPlayers()) {
		WaitForPlayers(S_WaitingTimeMax * 1000);
		if (!EnoughPlayers()) {
			RoundWinType = C_WinType_Draw;
			MapWinType = C_WinType_Draw;
			MB_StopMatch();
		}
	}
}

// ---------------------------------- //
// Initialize race
StartTime = Now + 3000;
SetTimeLimit(StartTime);
FirstFinish = True;
UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;

// ---------------------------------- //
// Spawn players for the round
TM::WaitRaceAll();
foreach (Player in Players) {
	// Skip invalid players in matchmaking or competitive mode
	if (MM_IsMatchServer() && !MM_PlayerIsAllowedToPlay(Player)) continue;
	if (UseCompetitiveMode && WarmUp2::GetPlayerSlot(Player) < 0) continue;
	
	SetPlayerClan(Player, MM_GetRequestedClan(Player));
	TM::StartRace(Player, StartTime);
	if (!RoundPlayers.existskey(Player.CurrentClan)) RoundPlayers[Player.CurrentClan] = Ident[];
	RoundPlayers[Player.CurrentClan].add(Player.Id);
	Player.Score.LadderClan = Player.CurrentClan;
}

UpdateScoresTableFooter(RoundPointsLimit);
Layers::Attach("ChaseInfo");
***

***Match_Yield***
***
// ---------------------------------- //
// Manage command
foreach (Event in PendingEvents) {
	if (Event.Type != CTmModeEvent::EType::OnCommand) continue;
	switch (Event.CommandName) {
		case "Command_RoundPointsClan1": {
			declare netwrite Integer[Integer] Net_Chase_MapPoints for Teams[0];
			Net_Chase_MapPoints[1] = Event.CommandValueInteger;
			ClanScores[1] = Event.CommandValueInteger;
		}
		case "Command_RoundPointsClan2": {
			declare netwrite Integer[Integer] Net_Chase_MapPoints for Teams[0];
			Net_Chase_MapPoints[2] = Event.CommandValueInteger;
			ClanScores[2] = Event.CommandValueInteger;
		}
		case "Command_SetPause": {
			/*if (Event.CommandValueBoolean) XmlRpc::BeginPause();
			else XmlRpc::EndPause();*/ //< @pause @xmlrpc
		}
		case "Command_ForceEndRound": {
			declare ForceEndRound for This = False;
			ForceEndRound = Event.CommandValueBoolean;
		}
	}
}
***

***Match_PlayLoop***
***
// ---------------------------------- //
// Update the map duration setting
if (PrevTimeLimit != S_TimeLimit) {
	PrevTimeLimit = S_TimeLimit;
	SetTimeLimit(StartTime);
}

// ---------------------------------- //
// Check if any player disconnected or gave up
declare Ident[][Integer] PlayersToRemove;
foreach (Clan => PlayersIds in RoundPlayers) {
	foreach (PlayerId in PlayersIds) {
		if (!PlayersRacing.existskey(PlayerId)) {
			if (!PlayersToRemove.existskey(Clan)) PlayersToRemove[Clan] = Ident[];
			PlayersToRemove[Clan].add(PlayerId);
		}
	}
	// Stop round if a clan doesn't have any players left
	if (PlayersIds.count <= 0 || PlayersIds.count < GetMinPlayersNb()) {
		if (RoundWinType == C_WinType_Undefined) {
			RoundWinType = C_WinType_Forfeit;
			MB_StopRound();
		}
	}
}
// Remove players from the round players
foreach (Clan => PlayersIds in PlayersToRemove) {
	foreach (PlayerId in PlayersIds) {
		declare Removed = RoundPlayers[Clan].remove(PlayerId);
		
		// Stop round if too many players give up
		if (Removed) {
			if (!RoundGiveUp.existskey(Clan)) RoundGiveUp[Clan] = Ident[];
			if (!RoundGiveUp[Clan].exists(PlayerId)) RoundGiveUp[Clan].add(PlayerId);
			if (RoundWinType == C_WinType_Undefined && RoundGiveUp[Clan].count > S_GiveUpMax) {
				RoundWinType = C_WinType_Forfeit;
				MB_StopRound();
			}
			
			if (RoundWinType == C_WinType_Undefined) {
				Message::SendStatusMessage(
					TL::Compose("|TeamName player gives up|$<%1$> player gives up", Teams[Clan - 1].ColorizedName),
					3000,
					1
				);
			}
		}
	}
}
// Remove the player from the current checkpoint if he crossed it already
foreach (Clan => PlayersIds in PlayersToRemove) {
	foreach (PlayerId in PlayersIds) {
		declare MinPlayersNb = -1;
		foreach (RoundPlayersIds in RoundPlayers) {
			if (MinPlayersNb < 0 || RoundPlayersIds.count < MinPlayersNb) MinPlayersNb = RoundPlayersIds.count;
		}
	
		declare TmpCurrentCheckpoint = CurrentCheckpoint;
		foreach (CheckpointClan => Laps in TmpCurrentCheckpoint) {
			if (CheckpointClan != Clan) continue;
			declare ClanNextCheckpointPlayerId = NullId;
			if (NextCheckpointPlayerId.existskey(Clan))	ClanNextCheckpointPlayerId = NextCheckpointPlayerId[Clan];
			
			foreach (Lap => CheckpointsIds in Laps) {
				foreach (CheckpointId => PlayersIds in CheckpointsIds) {
					declare Removed = CurrentCheckpoint[CheckpointClan][Lap][CheckpointId].remove(PlayerId);
					
					// Check if a new "next checkpoint player" must be annouced for the team who lost a player
					declare CurrentCheckpointPlayersIds = CurrentCheckpoint[CheckpointClan][Lap][CheckpointId];
					if (
						CurrentCheckpointPlayersIds.count > 0 &&
						CurrentCheckpointPlayersIds.count >= MinPlayersNb
					) {
						if (
							ClanNextCheckpointPlayerId == NullId ||
							!RoundPlayers[Clan].exists(ClanNextCheckpointPlayerId)
						) {
							declare LastPlayerId = CurrentCheckpointPlayersIds[CurrentCheckpointPlayersIds.count-1];
							
							// Annouced the next checkpoint player
							if (AllPlayers.existskey(LastPlayerId)) {
								declare Player <=> AllPlayers[LastPlayerId];
								if (Player.CurRace != Null && Player.CurRace.Checkpoints.count > 0) {
									if (RoundWinType == C_WinType_Undefined) {
										SendMessage(_("|Chase mode|Next checkpoint"), Player.User.Name, Player.CurrentClan, NullId);
									}
									RelayStartTime = SetNextCheckpointPlayer(
										Player.CurrentClan, 
										Player.User.Name, 
										Player.CurRace.Checkpoints.count, 
										Player.CurRace.Checkpoints[Player.CurRace.Checkpoints.count - 1],
										RelayStartTime
									);
									NextCheckpointPlayerId[Clan] = Player.Id;
								}
							}
						}
					} 
					// Cancel the current "next checkpoint player" if necessary
					else if (!RoundPlayers[Clan].exists(ClanNextCheckpointPlayerId)) {
						// Send a message to the players that must wait now
						if (RoundWinType == C_WinType_Undefined) {
							foreach (RoundPlayerId in RoundPlayers[Clan]) {
								if (CurrentCheckpointPlayersIds.exists(RoundPlayerId) && AllPlayers.existskey(RoundPlayerId)) {
									Message::SendBigMessage(AllPlayers[RoundPlayerId], _("|Chase mode|Wait for your teammates"), 3000, 1);
								}
							}
						}
						RelayStartTime = SetNextCheckpointPlayer(Clan, C_NoNameCheckpoint, -1, -1, RelayStartTime);
						NextCheckpointPlayerId[Clan] = NullId;
					}
				}
			}
		}
	}
}

// ---------------------------------- //
// Manage events
foreach (Event in PendingEvents) {
	Events::Valid(Event);
	
	// ---------------------------------- //
	// Waypoint
	if (Event.Type == CTmModeEvent::EType::WayPoint) {
		if (Event.IsEndRace) {
			Scores::SetPlayerPrevRace(Event.Player.Score, Event.Player.CurRace);
			TM::EndRace(Event.Player);
			
			// ---------------------------------- //
			// Start the countdown if it's the first player to finish
			if (FirstFinish) {
				FirstFinish = False;
				CutOffTimeLimit = GetFinishTimeout();
				RoundWinType = C_WinType_Finish;
			}
		}
		
		if (Event.Player.Score !=  Null) {
			Scores::SetPlayerBestRace(Event.Player.Score, Event.Player.CurRace);
			
			// ---------------------------------- //
			// Save the best lap time
			if (Event.IsEndLap) {
				declare Better = Scores::SetPlayerBestLapIfBetter(Event.Player.Score, Event.Player.CurLap, CTmResult::ETmRaceResultCriteria::Time);
			}
		}
		MB_SortScores(CTmMode::ETmScoreSortOrder::BestRace_CheckpointsProgress);
		
		// Delay event
		declare EventId = GetUniqueEventId();
		WaypointEvents_Now[EventId] = Now;
		WaypointEvents_RaceTime[EventId] = Event.RaceTime;
		WaypointEvents_PlayerId[EventId] = Event.Player.Id;
		WaypointEvents_BlockId[EventId] = Event.BlockId;
		WaypointEvents_IsEndRace[EventId] = Event.IsEndRace;
		WaypointEvents_CheckpointInRace[EventId] = Event.CheckpointInRace;
	}
	// ---------------------------------- //
	// GiveUp
	else if (Event.Type == CTmModeEvent::EType::GiveUp) {
		TM::WaitRace(Event.Player);
	}
}

// ---------------------------------- //
// Get delayed event
declare EventsToRemove = Text[];
WaypointEvents_RaceTime = WaypointEvents_RaceTime.sort();
foreach (EventId => RaceTime in WaypointEvents_RaceTime) {
	declare EventNow = WaypointEvents_Now[EventId];
	
	if (EventNow + S_WaypointEventDelay <= Now) {
		EventsToRemove.add(EventId);
	} else {
		break;
	}
}
// ---------------------------------- //
// Update progress
foreach (EventId in EventsToRemove) {
	// Skip events if we have a winner
	if (RoundWinType != C_WinType_Undefined) break;
	
	declare PlayerId = WaypointEvents_PlayerId[EventId];
	if (!AllPlayers.existskey(PlayerId)) continue;
	
	declare Player <=> AllPlayers[PlayerId];
	declare RaceTime = WaypointEvents_RaceTime[EventId];
	declare BlockId = WaypointEvents_BlockId[EventId];
	declare IsEndRace = WaypointEvents_IsEndRace[EventId];
	declare CheckpointInRace = WaypointEvents_CheckpointInRace[EventId];
	
	// Check that the player is authorized to play
	if (!RoundPlayers.existskey(Player.CurrentClan)) continue;
	if (!RoundPlayers[Player.CurrentClan].exists(Player.Id)) continue;
	
	declare NewCheckpoint = False;
	if (!Progress.existskey(Player.CurrentClan)) {
		Progress[Player.CurrentClan] = Ident[][Ident][Integer];
	}
	if (!Progress[Player.CurrentClan].existskey(Player.CurrentNbLaps)) {
		Progress[Player.CurrentClan][Player.CurrentNbLaps] = Ident[][Ident];
	}
	if (!Progress[Player.CurrentClan][Player.CurrentNbLaps].existskey(BlockId)) {
		Progress[Player.CurrentClan][Player.CurrentNbLaps][BlockId] = Ident[];
		NewCheckpoint = True;
	}
	Progress[Player.CurrentClan][Player.CurrentNbLaps][BlockId].add(Player.Id);
	
	declare MinPlayersNb = -1;
	foreach (Clan => PlayersIds in RoundPlayers) {
		if (MinPlayersNb < 0 || PlayersIds.count < MinPlayersNb) MinPlayersNb = PlayersIds.count;
	}
	
	// New checkpoint
	if (NewCheckpoint) {
		// Get the players that already crossed the previous checkpoint
		declare PlayersIds = GetCurrentCheckpointPlayers(CurrentCheckpoint, Player.CurrentClan);
		
		// If it's not the first checkpoint
		if (PlayersIds.count > 0) {
			// Relay successful
			if (
				PlayersIds.count >= MinPlayersNb && 
				NextCheckpointPlayerId.existskey(Player.CurrentClan) &&
				NextCheckpointPlayerId[Player.CurrentClan] == Player.Id
			) {
				if (!Net_Chase_RoundPoints.existskey(Player.CurrentClan)) {
					Net_Chase_RoundPoints[Player.CurrentClan] = 0;
				}
				Net_Chase_PointsGap = S_RoundPointsGap;
				
				// Check points victory conditions
				if (RoundWinType == C_WinType_Undefined) {
					// Only give point if there's no winner yet
					Net_Chase_RoundPoints[Player.CurrentClan] += 1;
					
					declare Points1 = 0;
					declare Points2 = 0;
					if (Net_Chase_RoundPoints.existskey(1)) Points1 = Net_Chase_RoundPoints[1];
					if (Net_Chase_RoundPoints.existskey(2)) Points2 = Net_Chase_RoundPoints[2];
					
					// If a clan reach the round points limit
					if (RoundPointsLimit > 0 && (Points1 >= RoundPointsLimit || Points2 >= RoundPointsLimit)) {
						RoundWinType = C_WinType_PointsLimit;
						MB_StopRound();
					}
					// If a clan reach the round points gap
					else if (ML::Abs(Points1 - Points2) >= S_RoundPointsGap) {
						RoundWinType = C_WinType_PointsGap;
						MB_StopRound();
					}
				}
				
				SendMessage(
					TL::Compose("$0c0%1", _("|Chase mode|Relay successful")), 
					"", 
					Player.CurrentClan, 
					NullId
				);
			}
			// Relay failed 
			else {
				declare WarningMessage = "";
				if (S_DisplayWarning) WarningMessage = _("|Chase mode|Wait for your teammates");
				SendMessage(
					TL::Compose("$f00%1", _("|Chase mode|Relay failed")),
					TL::Compose("$f00%1", WarningMessage), 
					Player.CurrentClan, 
					Player.Id
				);
			}
		}
		
		// Update order
		CurrentCheckpoint[Player.CurrentClan] = Ident[][Ident][Integer];
		CurrentCheckpoint[Player.CurrentClan][Player.CurrentNbLaps] = Ident[][Ident];
		CurrentCheckpoint[Player.CurrentClan][Player.CurrentNbLaps][BlockId] = Ident[];
		CurrentCheckpoint[Player.CurrentClan][Player.CurrentNbLaps][BlockId].add(Player.Id);
		
		RelayStartTime = SetNextCheckpointPlayer(Player.CurrentClan, C_NoNameCheckpoint, CheckpointInRace + 1, RaceTime, RelayStartTime);
		UpdateMarkers(Player, False);
		NextCheckpointPlayerId[Player.CurrentClan] = NullId;
	}
	// Existing checkpoint
	else if (
		CurrentCheckpoint.existskey(Player.CurrentClan) &&
		CurrentCheckpoint[Player.CurrentClan].existskey(Player.CurrentNbLaps) &&
		CurrentCheckpoint[Player.CurrentClan][Player.CurrentNbLaps].existskey(BlockId)
	) {
		CurrentCheckpoint[Player.CurrentClan][Player.CurrentNbLaps][BlockId].add(Player.Id);
		
		declare ClanNextCheckpointPlayerId = NullId;
		if (NextCheckpointPlayerId.existskey(Player.CurrentClan))	ClanNextCheckpointPlayerId = NextCheckpointPlayerId[Player.CurrentClan];
		
		if (CurrentCheckpoint[Player.CurrentClan][Player.CurrentNbLaps][BlockId].count == MinPlayersNb && ClanNextCheckpointPlayerId == NullId) {
			if (!IsEndRace) SendMessage(_("|Chase mode|Next checkpoint"), Player.User.Name, Player.CurrentClan, NullId);
			
			RelayStartTime = SetNextCheckpointPlayer(Player.CurrentClan, Player.User.Name, CheckpointInRace + 1, RaceTime, RelayStartTime);
			UpdateMarkers(Player, True);
			NextCheckpointPlayerId[Player.CurrentClan] = Player.Id;
		} else {
			UpdateNextCheckpointPlayer();
		}
	}
	
	declare Removed = False;
	Removed = WaypointEvents_Now.removekey(EventId);
	Removed = WaypointEvents_RaceTime.removekey(EventId);
	Removed = WaypointEvents_PlayerId.removekey(EventId);
	Removed = WaypointEvents_BlockId.removekey(EventId);
	Removed = WaypointEvents_IsEndRace.removekey(EventId);
	Removed = WaypointEvents_CheckpointInRace.removekey(EventId);
}

// ---------------------------------- //
// Manage XmlRpc events
// @XmlRpc -> register methods Rounds_ForceEndRound
foreach (Event in XmlRpc.PendingEvents) {
	if (Event.Param1 == "Rounds_ForceEndRound") {
		ForceEndRound = True;
	}
}

// ---------------------------------- //
// End the round 
// If All players finished
if (Players.count > 0 && PlayersRacing.count <= 0) {
	MB_StopRound();
}
// If time limit is reached
if (CutOffTimeLimit > 0 && Now >= CutOffTimeLimit) {
	if (RoundWinType == C_WinType_Undefined) RoundWinType = C_WinType_Time;
	MB_StopRound();
}
// If forced end round
if (ForceEndRound) {
	RoundWinType = C_WinType_Draw;
	MB_StopRound();
}
***

***Match_EndRound***
***
// ---------------------------------- //
// Update UI
UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound;
Layers::Detach("ChaseInfo");
Message::CleanAllMessages();
CutOffTimeLimit = -1;
foreach (Player in AllPlayers) {
	declare UI <=> UIManager.GetUI(Player);
	UI.MarkersXML = "";
}

// ---------------------------------- //
// Find round winner
declare RoundWinClan = -1;
switch (RoundWinType) {
	case C_WinType_PointsLimit: {
		declare Points1 = 0;
		declare Points2 = 0;
		if (Net_Chase_RoundPoints.existskey(1)) Points1 = Net_Chase_RoundPoints[1];
		if (Net_Chase_RoundPoints.existskey(2)) Points2 = Net_Chase_RoundPoints[2];
		if (Points1 != Points2) {
			if (Points1 > Points2) RoundWinClan = 1;
			else RoundWinClan = 2;
			UIManager.UIAll.StatusMessage = _("Victory by points");
		}
	}
	case C_WinType_PointsGap: {
		declare MaxPoints = 0;
		foreach (Clan => Points in Net_Chase_RoundPoints) {
			if (Points > MaxPoints) {
				MaxPoints = Points;
				RoundWinClan = Clan;
				UIManager.UIAll.StatusMessage = _("Victory by points gap");
			}
		}
	}
	case C_WinType_Finish: {
		MB_SortScores(CTmMode::ETmScoreSortOrder::BestRace_CheckpointsProgress);
		if (Scores.existskey(0) && Scores[0].BestRace.Time > 0) {
			RoundWinClan = Scores[0].TeamNum;
			UIManager.UIAll.StatusMessage = TL::Compose(_("$<%1$> crossed the finish line!"), Scores[0].User.Name);
		}
	}
	case C_WinType_Forfeit: {
		declare PossibleWinners = Integer[];
		foreach (Clan => PlayersIds in RoundPlayers) {
			if (PlayersIds.count > 0 && PlayersIds.count >= GetMinPlayersNb() && !PossibleWinners.exists(Clan)) PossibleWinners.add(Clan);
		}
		foreach (Clan => PlayersIds in RoundGiveUp) {
			if (PlayersIds.count > S_GiveUpMax) {
				declare Removed = PossibleWinners.remove(Clan);
			}
		}
		if (PossibleWinners.count == 1) {
			RoundWinClan = PossibleWinners[0];
			UIManager.UIAll.StatusMessage = _("Victory by forfeit");
		}
	}
	case C_WinType_Time: {
		// Victory by points advantage
		declare Points1 = 0;
		declare Points2 = 0;
		if (Net_Chase_RoundPoints.existskey(1)) Points1 = Net_Chase_RoundPoints[1];
		if (Net_Chase_RoundPoints.existskey(2)) Points2 = Net_Chase_RoundPoints[2];
		if (Points1 != Points2) {
			if (Points1 > Points2) RoundWinClan = 1;
			else RoundWinClan = 2;
			UIManager.UIAll.StatusMessage = _("Victory by points advantage");
		} 
		// Victory by checkpoints advatange
		else {
			declare Integer[Integer] CheckpointsCount;
			foreach (Score in Scores) {
				if (!CheckpointsCount.existskey(Score.TeamNum)) CheckpointsCount[Score.TeamNum] = 0;
				CheckpointsCount[Score.TeamNum] += Score.BestRace.Checkpoints.count;
			}
			declare MaxCheckpoints = 0;
			foreach (Clan => CheckpointsNb in CheckpointsCount) {
				if (CheckpointsNb > MaxCheckpoints) {
					RoundWinClan = Clan;
					MaxCheckpoints = CheckpointsNb;
				} else if (CheckpointsNb == MaxCheckpoints) {
					RoundWinClan = -1;
				}
			}
			
			if (RoundWinClan != -1) {
				UIManager.UIAll.StatusMessage = _("Victory by checkpoints advantage");
			} 
			// Victory by time advantage
			else {
				MB_SortScores(CTmMode::ETmScoreSortOrder::BestRace_CheckpointsProgress);
				if (Scores.existskey(0) && Scores[0].BestRace.Time > 0) {
					RoundWinClan = Scores[0].TeamNum;
					UIManager.UIAll.StatusMessage = _("Victory by time advantage");
				}
			}
		}
	}
	case C_WinType_Draw: {
		UIManager.UIAll.StatusMessage = _("Forced round end");
	}
}

// ---------------------------------- //
// Update scores
if (RoundWinClan != -1) {
	if (!Net_Chase_MapPoints.existskey(RoundWinClan)) Net_Chase_MapPoints[RoundWinClan] = 0;
	Net_Chase_MapPoints[RoundWinClan] += 1;
}
UpdateClanScores();
//XmlRpc::ScoresReady(); //< @xmlrpc

if (MM_IsMatchServer()) {
	declare AllowSubstitutes = True;
	
	foreach (Points in Net_Chase_MapPoints) {
		if (Points >= S_MapPointsLimit - 1) {
			AllowSubstitutes = False;
			break;
		}
	}
	
	MM_AllowSubstitutes(AllowSubstitutes && MB_MatchIsRunning());
	MM_SetScores([ClanScores[1], ClanScores[2]]);
}

declare EndRoundDuration = S_PauseBetweenRound * 1000;
if (EndRoundDuration < 0) EndRoundDuration = C_EndRoundDuration;

// ---------------------------------- //
// Display round winner
if (Teams.existskey(RoundWinClan - 1)) {
	Message::SendBigMessage(TL::Compose(_("$<%1$> wins the round!"), Teams[RoundWinClan - 1].ColorizedName), EndRoundDuration, 1);
} else {
	Message::SendBigMessage(_("This round is a draw."), EndRoundDuration, 1);
}

// ---------------------------------- //
// Unspawn players
MB_Sleep(250);
TM::WaitRaceAll();

UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
MB_Sleep(EndRoundDuration);
UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
UIManager.UIAll.StatusMessage = "";
Message::CleanAllMessages();

// ---------------------------------- //
// Map points limit reached
foreach (Points in Net_Chase_MapPoints) {
	if (Points >= S_MapPointsLimit) {
		MB_StopMap();
		MB_StopMatch();
		if (MapWinType == C_WinType_Undefined) MapWinType = C_WinType_PointsLimit;
	}
}
***

***Match_EndMap***
***
// ---------------------------------- //
// Bug : spawn players to be able to clear the status message
foreach (Player in Players) {
	Player.IsSpawned = True;
}
UIManager.UIAll.StatusMessage = "";
Message::CleanAllMessages();
MB_Sleep(250);
TM::WaitRaceAll();

declare Master = "";
if (MM_IsMatchServer()) {
	MB_SortScores(CTmMode::ETmScoreSortOrder::BestRace_CheckpointsProgress);
	if (Scores.existskey(0) && Scores[0].BestRace.Time > 0) {
		Master = Scores[0].User.Login;
	}
}
// ---------------------------------- //
// Find map winner
declare WinClan = -1;
if (MapWinType == C_WinType_Forfeit) {
	// Win by forfeit
	declare Integer[Integer] StartingClansPlayersNb;
	foreach (Player in Players) {
		if (!StartingClansPlayersNb.existskey(Player.CurrentClan)) {
			StartingClansPlayersNb[Player.CurrentClan] = 0;
		}
		StartingClansPlayersNb[Player.CurrentClan] += 1;
	}
	for (Clan, 1, 2) {
		if (!StartingClansPlayersNb.existskey(Clan) || StartingClansPlayersNb[Clan] < GetMinPlayersNb()) {
			if (WinClan != -1) {
				WinClan = -1;
				break;
			}
			WinClan = 3 - Clan;
		}
	}
} else {
	// Win by points
	declare MaxPoints = 0;
	foreach (Clan => Points in Net_Chase_MapPoints) {
		if (Points > MaxPoints) {
			WinClan = Clan;
			MaxPoints = Points;
		} else if (Points == MaxPoints) {
			WinClan = -1;
		}
	}
}
Scores::SetDefaultLadderSort(Scores::C_Sort_BestRaceTime);
Scores::SetClanWinner(WinClan);
***

***EndServer***
***
Layers::Destroy("ChaseInfo");

if (UseCompetitiveMode) {
	WarmUp2::Unload();
}
***

// ---------------------------------- //
// Functions
// ---------------------------------- //
// ---------------------------------- //
/** Get a unique event id
 *
 *	@return					The unique event id
 */
Text GetUniqueEventId() {
	declare Chase_UniqueId_PrevNow for This = 0;
	declare Chase_UniqueId_Count for This = 0;
	
	if (Chase_UniqueId_PrevNow != Now) {
		Chase_UniqueId_Count = 0;
	} else {
		Chase_UniqueId_Count += 1;
	}
	Chase_UniqueId_PrevNow = Now;
	
	return Chase_UniqueId_PrevNow^Chase_UniqueId_Count;
}

// ---------------------------------- //
/** Update the footer of the scores table
 *
 *	@param	_RoundPointsLimit	The round points limit
 */
Void UpdateScoresTableFooter(Integer _RoundPointsLimit) {
	if (Hud != Null) Hud.ScoresTable.SetFooterText(TL::Compose("%1 "^_RoundPointsLimit^" | %2 "^S_RoundPointsGap, _("Points limit : "), _("Points gap :")));
}

// ---------------------------------- //
/** Get the minimum number of players in a team
 *
 *	@return					The minimum number of players in a team
 */
Integer GetMinPlayersNb() {
	if (S_MinPlayersNb  >= 2) return S_MinPlayersNb;
	return 2;
}

// ---------------------------------- //
/** Get the time left to the players to finish the map after the first player
 *
 *	@return 				The time left in ms
 */
Integer GetFinishTimeout() {
	declare FinishTimeout = 0;
	
	if (S_FinishTimeout >= 0) {
		FinishTimeout = S_FinishTimeout * 1000;
	} else {
		declare ObjectiveNbLaps = Map.TMObjective_NbLaps;
		if (ObjectiveNbLaps <= 0 || !Map.TMObjective_IsLapRace) ObjectiveNbLaps = 1;
		FinishTimeout = 5000 + (((Map.TMObjective_AuthorTime / ObjectiveNbLaps) * NbLaps) / 6);
	}
	
	return Now + FinishTimeout;
}

// ---------------------------------- //
/** Get the round points limit
 *
 *	@return					The round points limit
 */
Integer GetRoundPointsLimit() {
	if (S_RoundPointsLimit < 0) {
		return ML::Abs((MapCheckpointPos.count + 1) * S_RoundPointsLimit);
	}
	
	return S_RoundPointsLimit;
}

// ---------------------------------- //
/** Set the time limit
 *
 *	@param	_StartTime		The time at which the race started
 */
Void SetTimeLimit(Integer _StartTime) {
	// User define time limit with a setting
	if (S_TimeLimit > 0) {
		CutOffTimeLimit = _StartTime + (S_TimeLimit * 1000);
	} 
	// No time limit
	else if (S_TimeLimit == 0) {
		CutOffTimeLimit = -1;
	} 
	// Time limit auto adjusted
	else {
		declare ObjectiveNbLaps = Map.TMObjective_NbLaps;
		if (ObjectiveNbLaps <= 0) ObjectiveNbLaps = 1;
		declare TimePerLap = ML::NearestInteger((Map.TMObjective_BronzeTime + (Map.TMObjective_BronzeTime * 0.1)) / ObjectiveNbLaps);
		CutOffTimeLimit = _StartTime + (TimePerLap * NbLaps);
	}
}

// ---------------------------------- //
/** Set the number of laps 
 *
 *	@param _LapsNb			The number of laps
 *	@param _StartTime		The time at which the race started
 */
Void SetLapsNb(Integer _LapsNb, Integer _StartTime) {
	if (_LapsNb < 0) NbLaps = -1;
	else NbLaps = _LapsNb;
	SetTimeLimit(_StartTime);
}

// ---------------------------------- //
/** Send a message to a clan
 *
 *	@param _StatusMessage	The status message
 *	@param _BigMessage		The big message
 *	@param _StartTime		The clan that will receive the message
 *	@param _PlayerId		The id of the player that will see the big message
 */
Void SendMessage(Text _StatusMessage, Text _BigMessage, Integer _Clan, Ident _PlayerId) {
	if (_BigMessage == "" && _StatusMessage == "") return;
	
	foreach (Player in AllPlayers) {
		declare Clan = -1;
		// Spectators
		if (Player.RequestsSpectate) {
			declare UI <=> UIManager.GetUI(Player);
			if (UI != Null) {
				declare netread Integer Net_Chase_SpectatingClan for UI;
				Clan = Net_Chase_SpectatingClan;
			}
		} 
		// Players
		else {
			Clan = Player.CurrentClan;
		}
		
		if (Clan == _Clan) {
			if (_StatusMessage != "") Message::SendStatusMessage(Player, _StatusMessage, 3000, 1);
			if (_BigMessage != "") {
				if (_PlayerId == NullId || _PlayerId == Player.Id) Message::SendBigMessage(Player, _BigMessage, 3000, 1);
			}
		}
	}
}

// ---------------------------------- //
/// Force an update in the UI displaying the next checkpoint player
Void UpdateNextCheckpointPlayer() {
	declare netwrite Integer Net_Chase_NextPlayerUpdate for Teams[0];
	Net_Chase_NextPlayerUpdate = Now;
}

// ---------------------------------- //
/** Set the name of the next checkpoint player
 *
 *	@param	_Clan											Clan of the player
 *	@param	_Name											Name of the player
 *	@param	_CheckpointNb							Number of the checkpoint
 *	@param	_RaceTime									Race time if the player
 *	@param	_RelayStartTime						Start time of the relay for each team
 */
Integer[Integer] SetNextCheckpointPlayer(Integer _Clan, Text _Name, Integer _CheckpointNb, Integer _RaceTime, Integer[Integer] _RelayStartTime) {
	declare netwrite Integer Net_Chase_NextPlayerUpdate for Teams[0];
	declare netwrite Text[Integer] Net_Chase_NextPlayer for Teams[0];
	declare netwrite Integer[Integer] Net_Chase_NextCheckpoint for Teams[0];
	declare netwrite Integer[Integer] Net_Chase_RelayTime for Teams[0];
	Net_Chase_NextPlayer[_Clan] = _Name;
	if (_CheckpointNb >= 0) Net_Chase_NextCheckpoint[_Clan] = _CheckpointNb;
	Net_Chase_NextPlayerUpdate = Now;
	
	declare RelayStartTime = _RelayStartTime;
	
	// Save relay duration
	if (_Name == C_NoNameCheckpoint) {
		if (_RaceTime >= 0) RelayStartTime[_Clan] = _RaceTime;
		Net_Chase_RelayTime[_Clan] = -1;
	} else if (RelayStartTime.existskey(_Clan)) {
		Net_Chase_RelayTime[_Clan] = _RaceTime - RelayStartTime[_Clan];
		RelayStartTime[_Clan] = -1;
	}
	
	return RelayStartTime;
}

// ---------------------------------- //
/** Get the ids of the players of a clan that crossed the current checkpoint
 *
 *	@param	_CurrentCheckpoint	The array containing the info about the current checkpoint
 *	@param	_Clan				The clan to get
 *
 *	@return						An array with the ids of the players of the given clan that crossed the current checkpoint
 */
Ident[] GetCurrentCheckpointPlayers(Ident[][Ident][Integer][Integer] _CurrentCheckpoint, Integer _Clan) {
	foreach (Clan => Laps in _CurrentCheckpoint) {
		if (Clan != _Clan) continue;
		foreach (Lap => CheckpointsIds in Laps) {
			foreach (CheckpointId => PlayersIds in CheckpointsIds) {
				return PlayersIds;
			}
		}
	}
	
	return Ident[];
}

// ---------------------------------- //
/** Update the markers
 *
 *	@param	_Player		The player that must cross the checkpoint
 *	@param	_Display	Display the marker or not
 */
Void UpdateMarkers(CTmPlayer _Player, Boolean _Display) {
	// @Tmp
	return;
	
	if (_Player == Null) return;
	
	foreach (Player in AllPlayers) {
		if (Player.CurrentClan != _Player.CurrentClan) continue;
		declare UI <=> UIManager.GetUI(Player);
		
		if (_Display) {
			UI.MarkersXML = """<marker label="$sX" box="0 0.5 0" playerlogin="{{{_Player.User.Login}}}" />""";
		} else {
			UI.MarkersXML = "";
		}
	}
}

// ---------------------------------- //
/// Update the clan scores
Void UpdateClanScores() {
	declare netwrite Integer[Integer] Net_Chase_MapPoints for Teams[0];
	foreach (Clan => Points in Net_Chase_MapPoints) {
		if (ClanScores.existskey(Clan)) ClanScores[Clan] = Points;
	}
}

// ---------------------------------- //
/** Check if there are enough players
 *
 *	@return					True if there are enough players, False otherwise
 */
Boolean EnoughPlayers() {
	declare Integer[Integer] StartingClansPlayersNb;
	foreach (Player in Players) {
		SetPlayerClan(Player, MM_GetRequestedClan(Player));
		
		if (!StartingClansPlayersNb.existskey(Player.CurrentClan)) {
			StartingClansPlayersNb[Player.CurrentClan] = 0;
		}
		StartingClansPlayersNb[Player.CurrentClan] += 1;
	}
	if (
		StartingClansPlayersNb.existskey(1) &&
		StartingClansPlayersNb.existskey(2) &&
		StartingClansPlayersNb[1] >= GetMinPlayersNb() &&
		StartingClansPlayersNb[2] >= GetMinPlayersNb()
	) {
		return True;
	}
	
	return False;
}

// ---------------------------------- //
/** Waiting enough players in each team
 *
 *	@param	_WaitingTime	Maximum waiting time
 */
Void WaitForPlayers(Integer _WaitingTime) {
	if (_WaitingTime > 0) {
		// Add 15 seconds to wait for potential selected
		// substitute to connect to the server
		CutOffTimeLimit = Now + _WaitingTime + 15000;
	} else {
		CutOffTimeLimit = -1;
	}
	
	TM::WaitRaceAll();
	if (EnoughPlayers()) return;
	
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
	UIManager.UIAll.StatusMessage = _("Waiting for players");
	declare WaitSubstitute = False;
	declare EnoughPlayers = False;
	
	while (!ServerShutdownRequested && !MatchEndRequested) {
		MB_Yield();
		
		// ---------------------------------- //
		// Update players
		declare Integer[Integer] ClansPlayersNb;
		foreach (Player in Players) {
			declare RequestedClan = MM_GetRequestedClan(Player);
			if (RequestedClan != Player.CurrentClan) {
				TM::WaitRace(Player);
				SetPlayerClan(Player, RequestedClan);
			}
			
			if (!ClansPlayersNb.existskey(Player.CurrentClan)) {
				ClansPlayersNb[Player.CurrentClan] = 0;
			}
			ClansPlayersNb[Player.CurrentClan] += 1;
			
			if (TM::IsWaiting(Player)) {
				TM::StartRace(Player);
			}
		}
		
		// ---------------------------------- //
		// Manage events
		foreach (Event in PendingEvents) {
			Events::Valid(Event);
			
			// ---------------------------------- //
			// Waypoint
			if (Event.Type == CTmModeEvent::EType::WayPoint) {
				if (Event.IsEndRace) {
					TM::EndRace(Event.Player);
				}
			}
			// ---------------------------------- //
			// GiveUp
			else if (Event.Type == CTmModeEvent::EType::GiveUp) {
				TM::WaitRace(Event.Player);
			}
		}
		
		// ---------------------------------- //
		// Stop when there's enough players
		if (
			ClansPlayersNb.existskey(1) &&
			ClansPlayersNb.existskey(2)
		) {
			declare MissingClan = [1 => GetMinPlayersNb() - ClansPlayersNb[1], 2 => GetMinPlayersNb() - ClansPlayersNb[2]];
			if (MissingClan[1] < 0) MissingClan[1] = 0;
			if (MissingClan[2] < 0) MissingClan[2] = 0;
			UIManager.UIAll.BigMessage = "$<"^Teams[0].ColorizedName^"$> "^MissingClan[1]^" - "^MissingClan[2]^" $<"^Teams[1].ColorizedName^"$>";
			
			if (
				ClansPlayersNb[1] >= GetMinPlayersNb() &&
				ClansPlayersNb[2] >= GetMinPlayersNb()
			) {
				EnoughPlayers = True;
				break;
			}
		}

		// Wait for substitute
		if (CutOffTimeLimit > 0 && !WaitSubstitute && Now >= CutOffTimeLimit - C_SubstituteWaitingTime) {
			WaitSubstitute = True;
			MM_AllowSubstitutes(False);
		}

		// ---------------------------------- //
		// Stop if time limit is reached
		if (CutOffTimeLimit > 0 && Now >= CutOffTimeLimit) break;
	}
	
	UIManager.UIAll.StatusMessage = "";
	if (EnoughPlayers) {
		MB_Sleep(250);
		UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound;
		TM::WaitRaceAll();
		UIManager.UIAll.BigMessage = _("Match starting ...");
		MB_Sleep(4000);
	}
	UIManager.UIAll.BigMessage = "";
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::None;
	TM::WaitRaceAll();
	
	CutOffTimeLimit = -1;
}

// ---------------------------------- //
/** Get the warm up duration for competitive mode
 *
 *	@return					The warm up duration
 */
Integer GetWarmUpDuration() {
	if (S_WarmUpDuration > 0) return S_WarmUpDuration;
	
	return 90;
}

// ---------------------------------- //
/** Manage the warm up sequence
 *
 *	@param	_IsPause		True if it's a pause, False if it's a standards warm-up
 */
Void WarmUp(Boolean _IsPause) {
	//XmlRpc::BeginWarmUp(); @XmlRpc
	
	// Init warm up
	declare PrevSequence = UIManager.UIAll.UISequence;
	UIManager.UIAll.BigMessageSound = CUIConfig::EUISound::PhaseChange;
	if (_IsPause) UIManager.UIAll.BigMessage = _("Pause");
	else UIManager.UIAll.BigMessage = _("Warm-up");
	UIManager.UIAll.StatusMessage = _("Press F6 once you're ready.");
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
	
	WarmUp2::Begin();
	
	// ---------------------------------- //
	// Init players
	foreach (Player in Players) {
		SetPlayerClan(Player, MM_GetRequestedClan(Player));
		if (Player.CurrentClan == 1) WarmUp2::SetPlayerGroup(Player, "Clan1");
		else if (Player.CurrentClan == 2) WarmUp2::SetPlayerGroup(Player, "Clan2");
	}
	WarmUp2::Clean();
	WarmUp2::Fill();
	
	declare PrevWarmUpDuration = GetWarmUpDuration()-1;
	
	while (!WarmUp2::Stop()) {
		MB_Yield();
		
		// Let the server sleep if there's no players on it
		if (AllPlayers.count <= 0) continue;
		
		// Manage players
		foreach (Player in Players) {
			if (Player.CurrentClan != MM_GetRequestedClan(Player)) {
				TM::WaitRace(Player);
				SetPlayerClan(Player, MM_GetRequestedClan(Player));
				if (Player.CurrentClan == 1) WarmUp2::SetPlayerGroup(Player, "Clan1");
				else if (Player.CurrentClan == 2) WarmUp2::SetPlayerGroup(Player, "Clan2");
			}
			if (TM::IsWaiting(Player)) {
				TM::StartRace(Player);
			}
		}
		
		WarmUp2::Loop();
		WarmUp2::ManageEvents();
		
		if (PrevWarmUpDuration != GetWarmUpDuration()) {
			PrevWarmUpDuration = GetWarmUpDuration();
			
			declare LongTimer = GetWarmUpDuration()*1000;
			declare ShortTimer = 5000;
			if (LongTimer <= 0) { LongTimer = 0; ShortTimer = 0; }
			
			WarmUp2::SetGroupTimers("Clan1", [ShortTimer => [-1, S_MinPlayersNb], LongTimer => [1, S_MinPlayersNb]]);
			WarmUp2::SetGroupTimers("Clan2", [ShortTimer => [-1, S_MinPlayersNb], LongTimer => [1, S_MinPlayersNb]]);
		}
	}
	
	WarmUp2::End();
	WarmUp2::Clean();
	WarmUp2::Fill();
	
	TM::WaitRaceAll();
	MB_Sleep(500);
	UIManager.UIAll.BigMessage = "";
	UIManager.UIAll.StatusMessage = "";
	UIManager.UIAll.UISequence = PrevSequence;
	
	//XmlRpc::EndWarmUp(); @XmlRpc
}

// ---------------------------------- //
/** Create the manialink for the next checkpoint player
 *
 *	@return					The manialink
 */
Text GetMLChaseInfo() {
	return """
<manialink version="2" name="Chase:NextPlayer">
<stylesheet>
	<style class="RoundPoints" sizen="10 10" style="TextRaceMessageBig" textsize="6" />
	<style class="MapPoints" sizen="5 10" style="TextRaceMessageBig" textsize="4" />
</stylesheet>
<frame id="Frame_Global">
	<frame posn="0 -68" id="Frame_NextCheckpoint">
		<quad sizen="7 7" halign="right" valign="center" style="Icons64x64_1" substyle="ClipPlay" id="Quad_NextPlayer" />
		<label posn="-6 0.3" sizen="30 6" halign="right" valign="center2" style="TextValueSmallSm" textsize="5" textemboss="1" text="{{{C_NoNameCheckpoint}}}" id="Label_NextPlayer" />
	</frame>
	<frame posn="0 82" id="Frame_NextCheckpointSpectator">
		<frame posn="-40 0">
			<frame id="Frame_NextCheckpointTeam1">
				<quad sizen="6 6" halign="right" valign="center" style="Icons64x64_1" substyle="ClipPlay" id="Quad_NextPlayer" />
				<label posn="-6 0.3" sizen="30 6" halign="right" valign="center2" style="TextValueSmallSm" textsize="3" textemboss="1" text="{{{C_NoNameCheckpoint}}}" id="Label_NextPlayer" />
			</frame>
		</frame>
		<frame posn="42 0">
			<frame id="Frame_NextCheckpointTeam2">
				<quad sizen="6 6" halign="right" valign="center" style="Icons64x64_1" substyle="ClipPlay" id="Quad_NextPlayer" />
				<label posn="-6 0.3" sizen="30 6" halign="right" valign="center2" style="TextValueSmallSm" textsize="3" textemboss="1" text="{{{C_NoNameCheckpoint}}}" id="Label_NextPlayer" />
			</frame>
		</frame>
	</frame>
	<label posn="0 -68" sizen="200 6" halign="center" valign="center2" style="TextValueSmallSm" textsize="5" textemboss="1" id="Label_Distance" />
	<frame posn="0 92" id="Frame_Score">
		<quad sizen="30 14 -1" halign="right" style="UiSMSpectatorScoreBig" substyle="HandleLeft" id="Quad_Score1"/>
		<quad sizen="30 14 -1" style="UiSMSpectatorScoreBig" substyle="HandleRight" id="Quad_Score2" />
		<label posn="-2 -2.8" halign="right" class="RoundPoints" text="0" id="Label_RoundPoints1" />
		<label posn="2 -2.8" class="RoundPoints" text="0" id="Label_RoundPoints2" />
		<label posn="-19 -2.8" class="MapPoints" text="0" id="Label_MapPoints1"/>
		<label posn="19 -2.8" halign="right" class="MapPoints" text="0" id="Label_MapPoints2" />
		<gauge posn="-20 -1.85 -2" sizen="40 5" halign="right" style="BgCard" id="Gauge_Points1" />
		<gauge posn="20 -1.85 -2" sizen="40 5" style="BgCard" id="Gauge_Points2" />
	</frame>
	<frame posn="0 65" id="Frame_RelayTime">
		<label halign="center" style="TextValueSmallSm" id="Label_RelayTime" />
	</frame>
</frame>
<script><!--
#Include "TextLib" as TL
#Include "MathLib" as ML

#Const C_TeamSpectated	0
#Const C_Team1			1
#Const C_Team2			2

Void UpdateNextCheckpoint(CMlFrame _Frame, Text _Name, Integer _CheckpointNb) {
	declare Label_NextPlayer <=> (_Frame.GetFirstChild("Label_NextPlayer") as CMlLabel);
	declare Quad_NextPlayer <=> (_Frame.GetFirstChild("Quad_NextPlayer") as CMlQuad);
	
	if (_Name != "{{{C_NoNameCheckpoint}}}") {
		Quad_NextPlayer.Substyle = "ClipPlay";
		Label_NextPlayer.Value = _Name;
		Label_NextPlayer.TextColor = <1., 1., 1.>;
	} else {
		Quad_NextPlayer.Substyle = "ClipPlay";
		Label_NextPlayer.Value = "-";
		Label_NextPlayer.TextColor = <1., 1., 1.>;
	}
	
	declare NameWidth = Label_NextPlayer.ComputeWidth(Label_NextPlayer.Value);
	if (NameWidth > Label_NextPlayer.Size.X) NameWidth = Label_NextPlayer.Size.X;
	declare Width = NameWidth + Quad_NextPlayer.Size.X;
	_Frame.RelativePosition.X = Width / 2.;
	_Frame.Visible = True;
}

Void UpdateNextCheckpoint(Text _Name, Integer _CheckpointNb) {
	declare Frame_NextCheckpoint <=> (Page.GetFirstChild("Frame_NextCheckpoint") as CMlFrame);
	declare Label_NextPlayer <=> (Frame_NextCheckpoint.GetFirstChild("Label_NextPlayer") as CMlLabel);
	declare Quad_NextPlayer <=> (Frame_NextCheckpoint.GetFirstChild("Quad_NextPlayer") as CMlQuad);
	
	if (GUIPlayer == Null) {
		Frame_NextCheckpoint.Visible = False;
	} else {
		// First checkpoint
		if (_CheckpointNb < 0) {
			Label_NextPlayer.Value = "Go";
			Quad_NextPlayer.Substyle = "ClipPlay";
			Label_NextPlayer.TextColor = <0., 0.7, 0.>;
		} else if (GUIPlayer.CurRace.Checkpoints.count >= _CheckpointNb && _Name == "{{{C_NoNameCheckpoint}}}") {
			Label_NextPlayer.Value = "{{{_("Wait")}}}";
			Quad_NextPlayer.Substyle = "ClipPause";
			Label_NextPlayer.TextColor = <0.9, 0., 0.>;
		} else if (GUIPlayer.CurRace.Checkpoints.count < _CheckpointNb /*&& _Name == "{{{C_NoNameCheckpoint}}}"*/) {
			Quad_NextPlayer.Substyle = "ClipPlay";
			Label_NextPlayer.Value = "Go";
			Label_NextPlayer.TextColor = <0., 0.7, 0.>;
		} else if (_Name != "{{{C_NoNameCheckpoint}}}") {
			if (GUIPlayer.User.Name == _Name) Quad_NextPlayer.Substyle = "ClipPlay";
			else Quad_NextPlayer.Substyle = "ClipRewind";
			Label_NextPlayer.Value = _Name;
			Label_NextPlayer.TextColor = <1., 1., 1.>;
		} else {
			Quad_NextPlayer.Substyle = "ClipPlay";
			Label_NextPlayer.Value = "Go";
			Label_NextPlayer.TextColor = <0., 0.7, 0.>;
		}
		
		declare NameWidth = Label_NextPlayer.ComputeWidth(Label_NextPlayer.Value);
		if (NameWidth > Label_NextPlayer.Size.X) NameWidth = Label_NextPlayer.Size.X;
		declare Width = NameWidth + Quad_NextPlayer.Size.X;
		Frame_NextCheckpoint.RelativePosition.X = Width / 2.;
		Frame_NextCheckpoint.Visible = True;
	}
}

Void UpdateGaugePoints() {
	declare Frame_Score <=> (Page.GetFirstChild("Frame_Score") as CMlFrame);
	declare Gauge_Points1 <=> (Frame_Score.GetFirstChild("Gauge_Points1") as CMlGauge);
	declare Gauge_Points2 <=> (Frame_Score.GetFirstChild("Gauge_Points2") as CMlGauge);
	
	declare netread Integer Net_Chase_PointsGap for Teams[0];
	declare netread Integer[Integer] Net_Chase_RoundPoints for Teams[0];
	declare RoundPoints1 = 0;
	declare RoundPoints2 = 0;
	if (Net_Chase_RoundPoints.existskey(1)) RoundPoints1 = Net_Chase_RoundPoints[1];
	if (Net_Chase_RoundPoints.existskey(2)) RoundPoints2 = Net_Chase_RoundPoints[2];
	declare PointsDifference = RoundPoints1 - RoundPoints2;
	declare Ratio = 0.;
	if (Net_Chase_PointsGap != 0) {
		Ratio = ML::Abs(PointsDifference / (Net_Chase_PointsGap * 1.));
		if (Ratio < 0.) Ratio = 0.;
		else if (Ratio > 1.) Ratio = 1.;
	}
	
	if (PointsDifference > 0) {
		Gauge_Points1.Ratio = Ratio;
		Gauge_Points2.Ratio = 0.;
	} else if (PointsDifference < 0) {
		Gauge_Points1.Ratio = 0.;
		Gauge_Points2.Ratio = Ratio;
	} else {
		Gauge_Points1.Ratio = 0.;
		Gauge_Points2.Ratio = 0.;
	}
}

main() {
	declare Frame_Global		<=> (Page.GetFirstChild("Frame_Global")					as CMlFrame);
	declare Frame_Score			<=> (Page.GetFirstChild("Frame_Score")					as CMlFrame);
	declare Quad_Score1			<=> (Frame_Score.GetFirstChild("Quad_Score1")			as CMlQuad);
	declare Quad_Score2			<=> (Frame_Score.GetFirstChild("Quad_Score2")			as CMlQuad);
	declare Label_RoundPoints1	<=> (Frame_Score.GetFirstChild("Label_RoundPoints1")	as CMlLabel);
	declare Label_RoundPoints2	<=> (Frame_Score.GetFirstChild("Label_RoundPoints2")	as CMlLabel);
	declare Label_MapPoints1	<=> (Frame_Score.GetFirstChild("Label_MapPoints1")		as CMlLabel);
	declare Label_MapPoints2	<=> (Frame_Score.GetFirstChild("Label_MapPoints2")		as CMlLabel);
	declare Gauge_Points1		<=> (Frame_Score.GetFirstChild("Gauge_Points1")			as CMlGauge);
	declare Gauge_Points2		<=> (Frame_Score.GetFirstChild("Gauge_Points2")			as CMlGauge);
	declare Label_RelayTime		<=> (Page.GetFirstChild("Label_RelayTime")				as CMlLabel);
	
	declare Frames_NextCheckpoint = [
		C_TeamSpectated => (Page.GetFirstChild("Frame_NextCheckpoint") as CMlFrame),
		C_Team1 => (Page.GetFirstChild("Frame_NextCheckpointTeam1") as CMlFrame),
		C_Team2 => (Page.GetFirstChild("Frame_NextCheckpointTeam2") as CMlFrame)
	];
	declare Frame_NextCheckpointSpectator <=> (Page.GetFirstChild("Frame_NextCheckpointSpectator") as CMlFrame);
	
	declare netread Integer Net_Chase_NextPlayerUpdate for Teams[0];
	declare netread Text[Integer] Net_Chase_NextPlayer for Teams[0];
	declare netread Integer[Integer] Net_Chase_NextCheckpoint for Teams[0];
	declare netread Integer[Integer] Net_Chase_MapPoints for Teams[0];
	declare netread Integer[Integer] Net_Chase_RoundPoints for Teams[0];
	declare netread Integer[Integer] Net_Chase_RelayTime for Teams[0];
	declare netread Integer Net_Chase_PointsGap for Teams[0];
	declare netwrite Integer Net_Chase_SpectatingClan for UI;
	
	declare RelayHideTime = -1;
	declare PrevNextPlayerUpdate = -1;
	declare PrevSpecClan = -1;
	declare PrevTeamColor = [0 => <0., 0., 0.>, 1 => <1., 1., 1.>];
	declare PrevRoundPoints = [1 => 0, 2 => 0];
	declare PrevMapPoints = [1 => 0, 2 => 0];
	declare PrevPointsGap = -1;
	declare PrevIsSpectatorMode = !IsSpectatorMode;
	
	while (True) {
		sleep(250);
		
		if (!PageIsVisible || InputPlayer == Null) continue;
		
		if (PrevIsSpectatorMode != IsSpectatorMode) {
			PrevIsSpectatorMode = IsSpectatorMode;
			Frame_NextCheckpointSpectator.Visible = IsSpectatorMode;
		}
		
		if (PrevNextPlayerUpdate != Net_Chase_NextPlayerUpdate) {
			PrevNextPlayerUpdate = Net_Chase_NextPlayerUpdate;
			
			if (
				GUIPlayer != Null &&
				Net_Chase_NextPlayer.existskey(GUIPlayer.CurrentClan) &&
				Net_Chase_NextCheckpoint.existskey(GUIPlayer.CurrentClan)
			) {
				UpdateNextCheckpoint(Net_Chase_NextPlayer[GUIPlayer.CurrentClan], Net_Chase_NextCheckpoint[GUIPlayer.CurrentClan]);
			} else {
				UpdateNextCheckpoint("{{{C_NoNameCheckpoint}}}", -1);
			}
			
			for (Clan, 1, 2) {
				if (
					Net_Chase_NextPlayer.existskey(Clan) &&
					Net_Chase_NextCheckpoint.existskey(Clan)
				) {
					UpdateNextCheckpoint(Frames_NextCheckpoint[Clan], Net_Chase_NextPlayer[Clan], Net_Chase_NextCheckpoint[Clan]);
				} else {
					UpdateNextCheckpoint(Frames_NextCheckpoint[Clan], "{{{C_NoNameCheckpoint}}}", -1);
				}
			}
			
			if (
				GUIPlayer != Null &&
				Net_Chase_RelayTime.existskey(GUIPlayer.CurrentClan) &&
				Net_Chase_RelayTime[GUIPlayer.CurrentClan] >= 0
			) {
				declare RelayTime = Net_Chase_RelayTime[GUIPlayer.CurrentClan];
				if (RelayTime < 1000) {
					Label_RelayTime.Value = TL::Compose("%1 : %2ms", "{{{_("Relay duration")}}}", TL::ToText(RelayTime));
					RelayHideTime = Now + 2750;
				} else {
					Label_RelayTime.Value = "";
				}
			} else {
				Label_RelayTime.Value = "";
			}
		}
		
		if (RelayHideTime > 0 && RelayHideTime <= Now) {
			RelayHideTime = -1;
			Label_RelayTime.Value = "";
		}
		
		if (
			GUIPlayer != Null && 
			GUIPlayer.Id != InputPlayer.Id && 
			PrevSpecClan != GUIPlayer.CurrentClan
		) {
			PrevSpecClan = GUIPlayer.CurrentClan;
			Net_Chase_SpectatingClan = GUIPlayer.CurrentClan;
			
			if (Net_Chase_NextPlayer.existskey(GUIPlayer.CurrentClan) && Net_Chase_NextCheckpoint.existskey(GUIPlayer.CurrentClan)) {
				UpdateNextCheckpoint(Net_Chase_NextPlayer[GUIPlayer.CurrentClan], Net_Chase_NextCheckpoint[GUIPlayer.CurrentClan]);
			} else {
				UpdateNextCheckpoint("{{{C_NoNameCheckpoint}}}", -1);
			}
		}
		
		if (
			PrevTeamColor[0] != Teams[0].ColorPrimary ||
			PrevTeamColor[1] != Teams[1].ColorPrimary
		) {
			PrevTeamColor[0] = Teams[0].ColorPrimary;
			PrevTeamColor[1] = Teams[1].ColorPrimary;
			Quad_Score1.ModulateColor = Teams[0].ColorPrimary;
			Quad_Score2.ModulateColor = Teams[1].ColorPrimary;
			Gauge_Points1.Color = Teams[0].ColorPrimary;
			Gauge_Points2.Color = Teams[1].ColorPrimary;
		}
		
		if (PrevPointsGap != Net_Chase_PointsGap) {
			PrevPointsGap = Net_Chase_PointsGap;
			if (Net_Chase_PointsGap != 0) {
				Gauge_Points1.GradingRatio = 1. / Net_Chase_PointsGap;
				Gauge_Points2.GradingRatio = 1. / Net_Chase_PointsGap;
			} else {
				Gauge_Points1.GradingRatio = 1.;
				Gauge_Points2.GradingRatio = 1.;
			}
			UpdateGaugePoints();
		}
		
		for (Clan, 1, 2) {
			if (Net_Chase_RoundPoints.existskey(Clan)) {
				if (PrevRoundPoints[Clan] != Net_Chase_RoundPoints[Clan]) {
					PrevRoundPoints[Clan] = Net_Chase_RoundPoints[Clan];
					if (Clan == 1) Label_RoundPoints1.Value = TL::ToText(Net_Chase_RoundPoints[Clan]);
					else if (Clan == 2) Label_RoundPoints2.Value = TL::ToText(Net_Chase_RoundPoints[Clan]);
					UpdateGaugePoints();
				}
			} else if (PrevRoundPoints[Clan] != 0) {
				PrevRoundPoints[Clan] = 0;
				if (Clan == 1) Label_RoundPoints1.Value = "0";
				else if (Clan == 2) Label_RoundPoints2.Value = "0";
				UpdateGaugePoints();
			}
			
			if (Net_Chase_MapPoints.existskey(Clan)) {
				if (PrevMapPoints[Clan] != Net_Chase_MapPoints[Clan]) {
					PrevMapPoints[Clan] = Net_Chase_MapPoints[Clan];
					if (Clan == 1) Label_MapPoints1.Value = TL::ToText(Net_Chase_MapPoints[Clan]);
					else if (Clan == 2) Label_MapPoints2.Value = TL::ToText(Net_Chase_MapPoints[Clan]);
				}
			} else if (PrevMapPoints[Clan] != 0) {
				PrevMapPoints[Clan] = 0;
				if (Clan == 1) Label_MapPoints1.Value = "0";
				else if (Clan == 2) Label_MapPoints2.Value = "0";
			}
		}
	}
}
--></script>
</manialink>
""";
}