/**
 * Mode Matchmaking
 *
 * Add matchmaking functionnalities to your mode
 */
#Extends "Modes/ShootMania/ModeBase.Script.txt"

#Const ModeMatchmakingVersion		"2014-11-21"
#Const ModeMatchmakingScriptName	"ModeMatchmaking.Script.txt"

#Include "TextLib" as TL
#Include "MathLib" as ML
#Include "Libs/Nadeo/Layers2.Script.txt" as Layers
#Include "Libs/Nadeo/Message.Script.txt" as Message
#Include "Libs/Nadeo/VoteMap.Script.txt" as VoteMap
#Include "Libs/Nadeo/Manialink.Script.txt" as Manialink
#Include "Libs/Nadeo/ShootMania/SpawnScreen.Script.txt" as SpawnScreen

// ---------------------------------- //
// Settings
// ---------------------------------- //
#Setting S_MatchmakingAPIUrl		"https://matchmaking.maniaplanet.com/v8"	as "<hidden>"	///< URL of the matchmaking API
#Setting S_MatchmakingMode			0			as "<hidden>"	///< 0 = Off, 1 = Lobby, 2 = Match, 3 = Universal lobby, 4 = Universal match
#Setting S_MatchmakingRematchRatio	-1.			as "<hidden>"	///< Minimum ratio of players agreeing for a rematch to occur
#Setting S_MatchmakingRematchNbMax	2			as "<hidden>"	///< Maxium number of consecutive rematch
#Setting S_MatchmakingVoteForMap	False		as "<hidden>"	///< Allow players to vote for the next played map
#Setting S_MatchmakingProgressive	False		as "<hidden>"	///< Can start a match with less players than the required number
#Setting S_LobbyRoundPerMap  		60			as "<hidden>"	///< "Nb of rounds per map in lobby mode")
#Setting S_LobbyMatchmakerPerRound	6			as "<hidden>"	///< Number of matchmaker call in a "King of the Lobby" round
#Setting S_LobbyMatchmakerWait		2			as "<hidden>"	///< Waiting time before the next call to the matchmaking function
#Setting S_LobbyMatchmakerTime		8			as "<hidden>"	///< Time allocated to the matchmaking (seconds)
#Setting S_LobbyInstagib  			False		as "<hidden>"	///< Laser mode in lobby
#Setting S_LobbyDisplayMasters		True		as "<hidden>"	///< Display the masters list
#Setting S_LobbyDisableUI			False		as "<hidden>"	///< Disable lobby UI
#Setting S_MatchmakingErrorMessage	_("An error occured in the matchmaking API. If the problem persist please try to contact this server administrator.") as "<hidden>" ///< Send a message in the chat if an error occured
#Setting S_MatchmakingLogAPIError	False		as "<hidden>"	///< Log the API requests errors
#Setting S_MatchmakingLogAPIDebug	False		as "<hidden>"	///< Log all the api requests and responses
#Setting S_MatchmakingLogMiscDebug	False		as "<hidden>"	///< Log different kinds of debug info

#Setting S_ProgressiveActivation_WaitingTime	180000	as "<hidden>"	///< Average waiting time before progressive matchmaking activate
#Setting S_ProgressiveActivation_PlayersNbRatio	1		as "<hidden>"	///< Multiply the required players nb by this, if there's less player in the lobby activate progressive

// ---------------------------------- //
// Constant
// ---------------------------------- //
#Const C_Matchmaking_Off			0	///< Matchmaking off on this server
#Const C_Matchmaking_Lobby			1	///< Is lobby server
#Const C_Matchmaking_Match			2	///< Is match  server
#Const C_Matchmaking_UniversalLobby	3	///< Is universal lobby
#Const C_Matchmaking_UniversalMatch	4	///< Is universal match

#Const C_LobbyAllowMatchCancel			True	///< Legacy : Allow match cancel
#Const C_LobbyLimitMatchCancel			0		///< Legacy : -1: infinite cancel, 0 or more: number of cancellations allowed
#Const C_LobbyPenalizeSubstituteCancel	False	///< Legacy : Penalize players canceling a replacement

#Const C_Lobby_Playing		0	///< Playing phase
#Const C_Lobby_Matchmaking	1	///< Matchmaking phase

#Const C_Lobby_TransfertSafeTime	15000	///< Minimum time after a transfert before a player can be listed as ready
#Const C_Lobby_WarnPenalty			False	///< Warn player that they will be penalize if they cancel
#Const C_Lobby_ReconnectDuration	5000	///< Duration before sending back a player to the match he left
#Const C_Lobby_MastersNb			20		///< Number of masters displayed
#Const C_Lobby_BotsNb				0		///< Number of bots in the lobby

#Const C_MatchStatus_Waiting	0	///< Waiting for a match to start
#Const C_MatchStatus_Starting	1	///< Match starting
#Const C_MatchStatus_Playing	2	///< Match running
#Const C_MatchStatus_Substitute	3	///< Waiting for a substitute
#Const C_MatchStatus_Ending		4	///< Match ending

#Const C_Match_PingInterval				60000	///< Time interval between each ping
#Const C_Match_PreparationDuration		20000	///< Maximum duration of the match preparation once there's at least one player
#Const C_Match_DelayBeforeTransfert		10000	///< Maximum delay before sending back the player to the lobby at the end of the match
#Const C_Match_EndingDuration			15000	///< Maximum duration of the match ending
#Const C_Match_EmptyTimeBeforeRestart	300000	///< Time before restarting a match on an empty server
#Const C_Match_RematchVoteDuration		10000	///< Vote duration for the rematch

#Const C_PlayerStatus_Waiting	0	///< Player waiting approval
#Const C_PlayerStatus_Valid		1	///< Player approved
#Const C_PlayerStatus_Invalid	2	///< Player rejected

#Const C_AllyStatus_Validated		0	///< Ally validated
#Const C_AllyStatus_Sent			1	///< Ally request sent to this player
#Const C_AllyStatus_Disconnected	2	///< Ally disconnected

#Const C_AllyInfo_Status	0	///< Current status of the user in the room
#Const C_AllyInfo_Clan		1	///< Current clan of the user in the room
#Const C_AllyInfo_Slot		2	///< Current slot of the user in the room

#Const C_Lobby_DefaultClan	0	///< Default clan when creating a room 
#Const C_Lobby_DefaultSlot	0	///< Default slot when creating a room

#Const C_PlayerInfo_Clan	0	///< Clan of the player
#Const C_PlayerInfo_Slot	1	///< Slot of the player

#Const C_MissingInfo_Clan	0	///< Clan of the missing player
#Const C_MissingInfo_Kicked	1	///< Kick status of the missing player
#Const C_MissingInfo_Since	2	///< Time when the player gone missing

#Const C_MissingPlayerGracePeriod	90000	///< Time before searching a substitute for a missing player

#Const C_Request_GetPlayers		0	///< /lobby-server/player-connection?login=somelogin&lobbylogin=anotherlogin
#Const C_Request_PostPlayers	1	///< /lobby-server/matchmaking-live
#Const C_Request_GetMatches		2	///< /match-server/match?serverlogin=somelogin
#Const C_Request_PostStatus		3	///< /match-server/live
#Const C_Request_PostResults	4	///< /match-server/result
#Const C_Request_PostMatches	5	///< /lobby-server/match

#Const C_RequestRandomDeviation	500	///< Random time margin applied to the live request of the match and lobby server

#Const C_Master_Name		0	///< Name of the master
#Const C_Master_Country		1	///< Country of the master
#Const C_Master_Echelon		2	///< Echelon of the master

#Const C_MessagePrefix		"$000»$09f"	///< Prefix used before the messages sent in the chat by the matchmaking

// ---------------------------------- //
// Globales
// ---------------------------------- //
declare Integer		G_Matchmaking_Mode;				///< Current matchmaking mode, see S_MatchmakingMode
declare Integer[]	G_Matchmaking_Format;			///< Format of the match making [NbPlayersTeam1, NbPlayersTeam2, ..., NbPlayersTeamN]
declare Integer[]	G_Matchmaking_CurrentFormat;	///< Format currently used
declare Integer[][]	G_Matchmaking_ProgressiveFormats; ///< Formats available
declare Integer		G_Matchmaking_MaxPlayers;		///< Maximum number of players in the biggest clan
declare Integer[Ident] G_Matchmaking_RequestsIds;	///< Ident and type of the requests sent by the match making
declare Integer		G_MMLobby_BestCombo;			///< Current best combo in lobby
declare Ident		G_MMLobby_WinnerId;				///< Current round winner
declare Integer		G_MMLobby_StartTime;			///< Lobby start time
declare Integer		G_MMLobby_EndTime;				///< Lobby end time
declare Integer		G_MMLobby_Phase;				///< Current lobby phase : playing/matchmaking
declare Boolean		G_MMLobby_MatchmakingEnabled;	///< Is the the matchmaking enabled?
declare Integer		G_MMMatch_Status;				///< Current match status
declare Integer[][Text] G_MMMatch_Players;			///< Logins, clans and order of the allowed players in the match
declare Integer[Text] G_MMMatch_Clans;				///< Clans assigned to the players by the matchmaking
declare Text		G_MMMatch_Id;					///< Id of the match
declare Text		G_MMMatch_LobbyLogin;			///< Login of the lobby server where to send back the players
declare Integer		G_MMMatch_NextPing;				///< Time of the next ping of the match server to the API
declare Boolean 	G_MMMatch_AllowSubstitutes;		///< Allow the matchmaking to search substitutes to missing players
declare Integer[Integer][Text] G_MMMatch_MissingPlayers; ///< List of missing players login, if they were kicked or not and their clan
declare Text[]		G_MMMatch_KickedPlayers;		///< List of kicked players
declare Boolean		G_MMMatch_NewMissingPlayers;	///< True if there's some new missing players not already sent to the API
declare Integer		G_MMMatch_EmptySince;			///< Time since when the server is empty
declare Integer[Text] G_MMMatch_GhostPlayers;		///< Logins and clans list of the ghost players
declare Integer		G_MMMatch_GhostPlayersIndex;	///< Next index to use when creating a chost players
declare Integer[]	G_MMMatch_Scores;				///< Scores of the current match

// ---------------------------------- //
// Extend
// ---------------------------------- //
***LogVersion***
***
MB_LogVersion(ModeMatchmakingScriptName, ModeMatchmakingVersion);
MB_LogVersion(Layers::GetScriptName(), Layers::GetScriptVersion());
MB_LogVersion(Message::GetScriptName(), Message::GetScriptVersion());
MB_LogVersion(VoteMap::GetScriptName(), VoteMap::GetScriptVersion());
MB_LogVersion(SpawnScreen::GetScriptName(), SpawnScreen::GetScriptVersion());
***

// ---------------------------------- //
// Matchmaking
// ---------------------------------- //
***StartServer***
***
Layers::Create("StartingMatch", Private_MM_GetMLStartingMatch());
Layers::Create("SendToServer", Private_Lobby_GetMLSendToServer());
Layers::Create("RematchVote", Private_Lobby_GetMLRematchVote());
Layers::SetType("StartingMatch", CUILayer::EUILayerType::CutScene);
Layers::Attach("SendToServer");

VoteMap::Load();

G_MMMatch_AllowSubstitutes = True;

// @Debug
foreach (Player in AllPlayers) {
	declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
	Matchmaking_PlayerStatus = C_PlayerStatus_Waiting;
}
***

***InitMap***
***
// Matchmaking already have an intro, skip the default one
if (MM_IsMatchServer()) MB_UseIntro = False;
else MB_UseIntro = True;
***

***PlayLoop***
***
Private_MM_PlayLoop();
***

***EndServer***
***
Layers::Destroy("StartingMatch");
Layers::Destroy("SendToServer");
Layers::Destroy("RematchVote");

VoteMap::Unload();
***

***Matchmaking***
***
MM_SetMode(S_MatchmakingMode);

if (MM_IsLobbyServer()) {
	Admin_SetLobbyInfo(True, AllPlayers.count, 255, 0.);
	---Lobby---
} else {
	Admin_SetLobbyInfo(False, AllPlayers.count, 0, 0.);
}
***

// ---------------------------------- //
// Lobby
// ---------------------------------- //
***Lobby***
***
+++LobbyInitServer+++
+++LobbyStartServer+++
XmlRpc::Load();
Clublink::Load(MB_UsePlayerClublinks);

// ---------------------------------- //
// Match sequence start
// ---------------------------------- //
while (
	!ServerShutdownRequested
	&& !MB_StopServer
) {
	// Match initialization
	MB_CurrentSection = "LobbyStartMatch";
	MB_SectionMatchNb	+= 1;
	MB_SectionMapNb		= 0;
	MB_StopMatch		= False;
	MB_XmlRpcCheck();
	MB_NeutralEmblemUpdate();
	
	if (MB_UseLogging) MB_Log("LobbyStartMatch");
	
	+++LobbyInitMatch+++
	
	// Check for map restart
	declare persistent MB_MapRestarted = False;
	XmlRpc::BeginMatch(MB_SectionMatchNb, MB_MapRestarted);
		
	+++LobbyStartMatch+++
	Clublink::Update();

// ---------------------------------- //
// Map sequence start
// ---------------------------------- //
	while (
		!ServerShutdownRequested
		&& !MB_StopServer
		&& !MB_StopMatch
	) {
		// Map initialization
		MB_CurrentSection = "LobbyStartMap";
		MB_SectionMapNb += 1;
		MB_SectionRoundNb = 0;
		MB_StopMap = False;
		MatchEndRequested = False;
		MB_XmlRpcCheck();
		MB_NeutralEmblemUpdate();
		Private_Lobby_SynchroReady();
		
		+++LobbyBeforeLoadMap+++
		
		XmlRpc::LoadingMap(MB_SectionMapNb);
		Mode::LoadMap();
		
		if (MB_UseLogging) MB_Log("LobbyStartMap");

		+++LobbyInitMap+++
		
		// Check for map restart
		XmlRpc::BeginMap(MB_SectionMapNb, MB_MapRestarted);
			
		MM_Synchro_DoBarrier();
		
		// Play mediatracker intro
		if (MB_UseIntro) {
			---MapIntro---
		}
		
		+++LobbyStartMap+++
		Clublink::Update();
		MB_MapRestarted = True;
		
// ---------------------------------- //
// Round sequence start
// ---------------------------------- //				
		while (!ServerShutdownRequested
			&& !MB_StopServer
			&& !MatchEndRequested
			&& !MB_StopMatch
			&& !MB_StopMap
			&& !MB_StopSubmatch
		) {
			// Round initialization
			MB_CurrentSection = "LobbyStartRound";
			MB_XmlRpcCheck();
			MB_NeutralEmblemUpdate();
			+++LobbyInitRound+++
			MB_StopRound = False;
			if (MB_UseSectionRound) {
				MB_SectionRoundNb += 1;
				if (MB_UseLogging) MB_Log("LobbyStartRound");
				
				XmlRpc::BeginRound(MB_SectionRoundNb);
				+++LobbyStartRound+++
				Clublink::Update();
			}
			MB_SectionTurnNb = 0;
			
			MB_CurrentSection = "LobbyPlayLoop";
// ---------------------------------- //
// Play loop
// ---------------------------------- //					
			while (!ServerShutdownRequested
				&& !MB_StopServer
				&& !MatchEndRequested
				&& !MB_StopMatch
				&& !MB_StopMap
				&& !MB_StopSubmatch
				&& !MB_StopRound
				&& !MB_StopTurn
			) {
				MM_Yield();
				+++LobbyPlayLoop+++
			}
// ---------------------------------- //
// Round end
// ---------------------------------- //
			MB_CurrentSection = "LobbyEndRound";
			MB_XmlRpcCheck();
			MB_NeutralEmblemUpdate();
			if (MB_UseSectionRound) {
				if (MB_UseLogging) MB_Log("LobbyEndRound");
				
				XmlRpc::EndRound(MB_SectionRoundNb);
				+++LobbyEndRound+++
				XmlRpc::SendScores();
			}
		}
// ---------------------------------- //
// Map end
// ---------------------------------- //
		MB_CurrentSection = "LobbyEndMap";
		MB_XmlRpcCheck();
		MB_NeutralEmblemUpdate();
		if (MB_UseLogging) MB_Log("LobbyEndMap");
		
		XmlRpc::EndMap(MB_SectionMapNb);
		
		// Play mediatracker outro
		if (MB_UseOutro) {
			---MapOutro---
		}
		
		+++LobbyEndMap+++
		XmlRpc::SendScores();
		MB_MapRestarted = False;
		Mode::UnloadMap();
		
		+++LobbyAfterUnloadMap+++
	}
// ---------------------------------- //
// Match end
// ---------------------------------- //
	MB_CurrentSection = "LobbyEndMatch";
	MB_XmlRpcCheck();
	MB_NeutralEmblemUpdate();
	if (MB_UseLogging) MB_Log("LobbyEndMatch");
	
	XmlRpc::EndMatch(MB_SectionMatchNb);
	+++LobbyEndMatch+++
	XmlRpc::SendScores();
	MB_MapRestarted = False;
}

XmlRpc::Unload();
Clublink::Unload();
+++LobbyEndServer+++
***

***LobbyStartServer***
***
// ---------------------------------- //
// Check settings
if (S_MatchmakingAPIUrl == "") MB_Log("[ERROR] You must set a matchmaking API URL in your settings.");
assert(S_MatchmakingAPIUrl != "", "You must set a matchmaking API URL in your settings.");

// ---------------------------------- //
// Config lobby
UseAllies = True;
UseClans = False;
MB_UseIntro = False;
MB_UseOutro = False;
MB_UseSectionRound = True;
declare PrevLobbyDisplayMasters = True;
G_MMLobby_MatchmakingEnabled = True;

// ---------------------------------- //
// Clean the masters list
Private_MM_ClearMasters();

declare netwrite Integer Net_Lobby_PlayersNb for Teams[0];
declare netwrite Integer Net_Lobby_AverageWaitingTime for Teams[0];
Net_Lobby_PlayersNb = 0;
Net_Lobby_AverageWaitingTime = -1;

// ---------------------------------- //
// Load rules manialink
declare MLRules = Private_Lobby_GetMLRules();
declare DisplayRules = True;
if (MLRules == "") DisplayRules = False;

Layers::Create("RulesReminder", Private_Lobby_GetMLRulesReminder());
Layers::Create("GaugeTimer", Private_Lobby_GetMLGaugeTimer());
Layers::Create("LobbyScreen", Private_Lobby_GetMLLobbyScreen(DisplayRules));
Layers::Create("PlayersList", Private_Lobby_GetMLPlayersList());
Layers::Create("MastersList", Private_Lobby_GetMLMastersList());
Layers::Create("Header", Private_Lobby_GetMLHeader());
Layers::Create("SendToServer", Private_Lobby_GetMLSendToServer());
Layers::Create("Rules", Private_Lobby_GetMLRules());
Layers::Create("Versus", Private_Lobby_GetMLVersus());
Layers::Create("Substitute", Private_Lobby_GetMLSubstitute());
Layers::Create("Reconnect", Private_Lobby_GetMLReconnect());
Layers::Attach("RulesReminder");
Layers::Attach("GaugeTimer");
Layers::Attach("LobbyScreen");
Layers::Attach("PlayersList");
Layers::Attach("MastersList");
Layers::Attach("Header");
Layers::Attach("SendToServer");
Layers::Attach("Rules");
Layers::Attach("Versus");
Layers::Attach("Substitute");
Layers::Attach("Reconnect");
Layers::SetType("RulesReminder", CUILayer::EUILayerType::CutScene);
Layers::SetType("GaugeTimer", CUILayer::EUILayerType::CutScene);
Layers::SetType("LobbyScreen", CUILayer::EUILayerType::CutScene);
Layers::SetType("PlayersList", CUILayer::EUILayerType::CutScene);
Layers::SetType("MastersList", CUILayer::EUILayerType::CutScene);
Layers::SetType("Header", CUILayer::EUILayerType::CutScene);
Layers::SetType("Rules", CUILayer::EUILayerType::CutScene);
Layers::SetType("Versus", CUILayer::EUILayerType::CutScene);
Layers::SetType("Substitute", CUILayer::EUILayerType::CutScene);
Layers::SetType("Reconnect", CUILayer::EUILayerType::CutScene);

foreach (Player in AllPlayers) {
	declare Lobby_IsNew for Player = True;
	Lobby_IsNew = True;
}
***

***LobbyStartMap***
***
// ---------------------------------- //
// Initialize UI
Message::CleanBigMessages();
UIManager.UIAll.AlliesLabelsVisibility = CUIConfig::ELabelsVisibility::WhenVisible;
UIManager.UIAll.OpposingTeamLabelsVisibility = CUIConfig::ELabelsVisibility::WhenVisible;
UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedHidden;
UIManager.UIAll.OverlayHideCountdown = True;
UIManager.UIAll.AltMenuNoCustomScores = True;
UIManager.UIAll.AltMenuNoDefaultScores = True;

// ---------------------------------- //
// Initialize bots
Users_SetNbFakeUsers(C_Lobby_BotsNb, 0);

// ---------------------------------- //
// Initialize rules
declare ModeName = _("Lobby");
SpawnScreen::ResetRulesSection();
SpawnScreen::AddSubsection(
	_("Objectives"),
	TL::Compose("$<%1%2$>\n\n%3\n$<%11. $>%4\n$<%12. $>%5", "$"^SpawnScreen::GetModeColor(), _("You will soon be redirected to a match server."), _("While waiting, try to become the King Of The Lobby!"), _("Perform as many hits as possible: the points you earn for each one will increase."), _("Your combo falls back to 1 when you are eliminated.")),
	0.
);
SpawnScreen::AddSubsection(_("Type"), _("Free for all"), 60.);
SpawnScreen::CreatePrettyRules(ModeName, False);

// ---------------------------------- //
// Initialize timers
StartTime = Now + 3000;
EndTime = -1;

// ---------------------------------- //
// Synchro UI
declare netwrite Net_Lobby_SynchroServer for Teams[0] = 0;
Net_Lobby_SynchroServer += 1;
***

***LobbyInitRound***
***
declare MatchmakingRunNb = 0;
***

***LobbyStartRound***
***
G_MMLobby_BestCombo = 0;
G_MMLobby_WinnerId = NullId;
G_MMLobby_Phase = C_Lobby_Playing;

// ---------------------------------- //
// Initialize timers
G_MMLobby_StartTime = Now;
G_MMLobby_EndTime = G_MMLobby_StartTime + (S_LobbyMatchmakerWait * 1000) + ML::Rand(-C_RequestRandomDeviation, C_RequestRandomDeviation);
Private_Lobby_UpdateTimers();

// ---------------------------------- //
// Initialize scores
ClearScores();
foreach (Score in Scores) {
	Score.Points = 0;
	Score.RoundPoints = 0;
}

// ---------------------------------- //
// Manage match cancellations
foreach (User in Users) {
	declare Lobby_MatchCancellation for User = 0;
	declare Lobby_RoundsNbBeforeForgiveness for User = S_LobbyRoundPerMap;
	if (Lobby_MatchCancellation > 0) {
		if (Lobby_RoundsNbBeforeForgiveness > 0) {
			Lobby_RoundsNbBeforeForgiveness -= 1;
		} else {
			Lobby_MatchCancellation -= 1;
			Lobby_RoundsNbBeforeForgiveness = S_LobbyRoundPerMap;
		}
	} else {
		Lobby_MatchCancellation = 0;
	}
}

declare netwrite Net_Lobby_AllowMatchCancel for Teams[0] = C_LobbyAllowMatchCancel;
declare netwrite Net_Lobby_LobbyLimitMatchCancel for Teams[0] = C_LobbyLimitMatchCancel;
declare netwrite Net_Lobby_LobbyPenalizeSubstituteCancel for Teams[0] = C_LobbyPenalizeSubstituteCancel;
declare netwrite Net_Lobby_WarnPenalty for Teams[0] = C_Lobby_WarnPenalty;
Net_Lobby_AllowMatchCancel = C_LobbyAllowMatchCancel;
Net_Lobby_LobbyLimitMatchCancel = C_LobbyLimitMatchCancel;
Net_Lobby_LobbyPenalizeSubstituteCancel = C_LobbyPenalizeSubstituteCancel;
Net_Lobby_WarnPenalty = C_Lobby_WarnPenalty;
***

***LobbyPlayLoop***
***
Private_Lobby_UpdateUI();
Private_Lobby_SpawnPlayers();

if (PrevLobbyDisplayMasters != S_LobbyDisplayMasters) {
	PrevLobbyDisplayMasters = S_LobbyDisplayMasters;
	Layers::SetVisibility("MastersList", S_LobbyDisplayMasters);
}

// ---------------------------------- //
// Manage events
foreach (Event in PendingEvents) {
	if (Event.Type == CSmModeEvent::EType::OnHit) {
		if (Event.Shooter != Null && Event.Victim != Null && Event.Shooter != Event.Victim) {
			declare PointsEarned = Private_Lobby_NotifyHit(Event.Shooter);
			Event.ShooterPoints = PointsEarned;
			PassOn(Event);
		} else {
			Discard(Event);
		}
	} else if (Event.Type == CSmModeEvent::EType::OnArmorEmpty) {
		if (Event.Victim != Null) {
			Private_Lobby_ResetPlayer(Event.Victim);
		}
		PassOn(Event);
	} else if (Event.Type == CSmModeEvent::EType::OnPlayerRequestRespawn) {
		if (Event.Player != Null) {
			Private_Lobby_ResetPlayer(Event.Player);
		}
		PassOn(Event);
	} else {
		PassOn(Event);
	}
}

// ---------------------------------- //
// Manage XmlRpc events
foreach (Event in XmlRpc.PendingEvents) {
	if (Event.Type == CXmlRpcEvent::EType::Callback) {
		switch (Event.Param1) {
			case "Matchmaking_Start": {
				G_MMLobby_MatchmakingEnabled = True;
			}
			case "Matchmaking_Stop": {
				G_MMLobby_MatchmakingEnabled = False;
				if (G_MMLobby_Phase == C_Lobby_Matchmaking) {
					G_MMLobby_EndTime = Now - 1;
				}
			}
			case "Matchmaking_Force": {
				if (G_MMLobby_Phase == C_Lobby_Playing) {
					G_MMLobby_EndTime = Now - 1;
				} else if (G_MMLobby_Phase == C_Lobby_Matchmaking) {
					Message::SendStatusMessage(
						_("Matchmaking forced."),
						S_LobbyMatchmakerTime * 1000,
						1
					);
					
					Private_Lobby_SetTimersAutoDown(S_LobbyMatchmakerTime * 1000);
					G_MMLobby_EndTime = Now + (S_LobbyMatchmakerTime * 1000);
					
					Private_MM_ComputeAllies();
					MM_MatchMakerStart();
				}
			}
			case "Matchmaking_GetReadyState": {
				Private_Lobby_SendReadyState(SM::GetPlayer(Event.Param2));
			}
		}
	} else if (Event.Type == CXmlRpcEvent::EType::CallbackArray) {
		switch (Event.ParamArray1) {
			case "Matchmaking_SetReadyState": {
				declare Login = "";
				declare Ready = "False";
				if (Event.ParamArray2.existskey(0)) Login = Event.ParamArray2[0];
				if (Event.ParamArray2.existskey(1)) Ready = Event.ParamArray2[1];
				
				declare User <=> SM::GetUser(Login);
				declare ReadyState = False;
				if (Ready == "True" || Ready == "true" || Ready == "1") ReadyState = True;
				Private_Lobby_SetReady(User, ReadyState);
			}
		}
	}
}

// ---------------------------------- //
// Change phase
switch (G_MMLobby_Phase) {
	// ---------------------------------- //
	// Start Matchmaking
	case C_Lobby_Playing: {
		if (G_MMLobby_EndTime > 0 && Now > G_MMLobby_EndTime) {
			G_MMLobby_Phase = C_Lobby_Matchmaking;
			Message::CleanAllMessages();
			Private_Lobby_ReinitPlayers();
			MatchmakingRunNb += 1;
			
			if (MatchmakingRunNb >= S_LobbyMatchmakerPerRound) {
				// ---------------------------------- //
				// Find the winner
				declare BestScore = G_MMLobby_BestCombo;
				if (G_MMLobby_WinnerId == NullId || !Users.existskey(G_MMLobby_WinnerId)) {
					// Recompute the winner
					G_MMLobby_WinnerId = NullId;
					BestScore = 0;
					foreach (Score in Scores) {
						if (Score.Points > BestScore) {
							BestScore = Score.Points;
							G_MMLobby_WinnerId = Score.User.Id;
						}
					}
				}
				
				// ---------------------------------- //
				// Announce the winner
				if (G_MMLobby_WinnerId != NullId && Users.existskey(G_MMLobby_WinnerId)) {
					Message::SendBigMessage(
						TL::Compose(_("$<%1$> is King of the Lobby!"), Users[G_MMLobby_WinnerId].Name),
						5000,
						3
					);
				} else {
					Message::SendBigMessage(
						_("|Match|Draw"),
						5000,
						3
					);
				}
				Mode::PlaySound(CUIConfig::EUISound::EndRound, 0);
			}
			
			// ---------------------------------- //
			// Start matchmaking
			if (!S_LobbyDisableUI) {
				declare Message = _("Matchmaking in progress...");
				if (!G_MMLobby_MatchmakingEnabled) Message = _("Matchmaking disabled.");
				Message::SendStatusMessage(
					Message,
					S_LobbyMatchmakerTime * 1000,
					1
				);
			}
			
			Private_Lobby_SetTimersAutoDown(S_LobbyMatchmakerTime * 1000);
			G_MMLobby_EndTime = Now + (S_LobbyMatchmakerTime * 1000);
			
			Private_MM_ComputeAllies();
			MM_MatchMakerStart();
		}
	}
	// ---------------------------------- //
	// Start playing
	case C_Lobby_Matchmaking: {
		MM_MatchMakerRun();
			
		if (G_MMLobby_EndTime > 0 && Now > G_MMLobby_EndTime) {
			MM_MatchMakerStop();
			Message::CleanAllMessages();
			
			if (MatchmakingRunNb >= S_LobbyMatchmakerPerRound) {
				if (MB_SectionRoundNb >= S_LobbyRoundPerMap) {
					MB_StopMap();
				} else {
					MB_StopRound();
				}
			} else {
				G_MMLobby_Phase = C_Lobby_Playing;
				G_MMLobby_StartTime = Now;
				G_MMLobby_EndTime = G_MMLobby_StartTime + (S_LobbyMatchmakerWait * 1000) + ML::Rand(-C_RequestRandomDeviation, C_RequestRandomDeviation);
				Private_Lobby_UpdateTimers();
			}
		}
	}
}
***

***LobbyEndMap***
***
SM::UnspawnAllPlayers();
Message::SendBigMessage(_("Going to next map"), 3000, 1, CUIConfig::EUISound::EndMatch, 0);
MM_Sleep(3000);
Message::CleanAllMessages();
MM_Sleep(500);
***

***LobbyEndServer***
***
UseAllies = False;
UIManager.UIAll.OverlayHideCountdown = False;
UIManager.UIAll.AltMenuNoCustomScores = False;

Layers::Destroy("RulesReminder");
Layers::Destroy("GaugeTimer");
Layers::Destroy("LobbyScreen");
Layers::Destroy("PlayersList");
Layers::Destroy("MastersList");
Layers::Destroy("Header");
Layers::Destroy("SendToServer");
Layers::Destroy("Rules");
Layers::Destroy("Versus");
Layers::Destroy("Substitute");
Layers::Destroy("Reconnect");
***

***MatchMakerStart***
***
declare Ident Lobby_MatchMakerRequestId for This;
// Destroy any previous request
if (Lobby_MatchMakerRequestId != NullId) {
	if (Http.Requests.existskey(Lobby_MatchMakerRequestId)) {
		Http.Destroy(Http.Requests[Lobby_MatchMakerRequestId]);
	}
}

declare AverageWaitingTimeTotal = 0;
declare AverageWaitingTimeCount = 0;

// Send the request to the API
if (Http.SlotsAvailable > 0) {
	declare PostData = "";
	
	// ---------------------------------- //
	// Common lobby	
	declare CancelersList = "";
	declare FirstCanceler = True;
	foreach (User in Users) {
		declare Lobby_CancelerMatchId for User = "";
		declare Lobby_CancelerIsSubstitute for User = False;
		declare Lobby_CancelerIsReconnect for User = False;
		if (Lobby_CancelerMatchId != "") {
			declare MatchId = TL::ToInteger(Lobby_CancelerMatchId);

			if (FirstCanceler) FirstCanceler = False;
			else CancelersList ^= ",";
			
			declare IsSubstitute = "false";
			if (Lobby_CancelerIsSubstitute) IsSubstitute = "true";
			
			declare IsReconnect = "false";
			if (Lobby_CancelerIsReconnect) IsReconnect = "true";
			
			CancelersList ^= """
		{
			"login": "{{{User.Login}}}",
			"matchid": {{{MatchId}}},
			"issubstitute": {{{IsSubstitute}}},
			"isreconnect": {{{IsReconnect}}}
		}""";
			
			Lobby_CancelerMatchId = "";
		}
	}
	declare PenaltiesList = "";
	foreach (Player in AllPlayers) {
		declare Lobby_LastMatchmakerTime for Player = Now;
		declare Lobby_IsApiBlocked for Player.User = False;
		
		if (Lobby_IsApiBlocked) {
			declare PenaltyRemove = ((Now - Lobby_LastMatchmakerTime) / 1000) + 1;
			if (PenaltyRemove < 0) PenaltyRemove = 0;
			
			if (PenaltiesList != "") PenaltiesList ^= ",";
			
			PenaltiesList ^= """
			{
				"login": "{{{Player.Login}}}",
				"remove": {{{PenaltyRemove}}}
			}""";
			
			Lobby_IsApiBlocked = False;
		}
		
		// Save last matchmaker running time
		Lobby_LastMatchmakerTime = Now;
	}
	
	// ---------------------------------- //
	// Universal lobby
	if (MM_IsUniversalServer()) {
		declare MatchesList = "";
		declare FirstMatch = True;
		declare WaitingLogins = "";
		
		declare Integer[][Ident][Integer] Lobby_Rooms for This;
		declare Text[] ReadyPlayers;
		
		foreach (Room in Lobby_Rooms) {
			declare PlayersList = "";
			declare FirstPlayer = True;
			declare Valid = True;
			
			foreach (AllyId => AllyInfo in Room) {
				declare AllyStatus = AllyInfo[C_AllyInfo_Status];
				if (AllyStatus != C_AllyStatus_Validated) continue;
				if (!Users.existskey(AllyId)) continue;
				
				declare User <=> Users[AllyId];
				ReadyPlayers.add(User.Login);
				
				// Update averate waiting time on the lobby
				declare Player <=> SM::GetPlayer(User.Login);
				if (Player != Null) {
					declare Integer Lobby_ReadySince for Player = -1;
					if (Lobby_ReadySince > 0) {
						AverageWaitingTimeTotal += Now - Lobby_ReadySince;
						AverageWaitingTimeCount += 1;
					}
				}
			
				declare Lobby_RoomIsReady for User = False;
				if (!Lobby_RoomIsReady) {
					Valid = False;
					break;
				}
				
				declare Clan = AllyInfo[C_AllyInfo_Clan];
				declare Order = AllyInfo[C_AllyInfo_Slot];
				
				if (FirstPlayer) FirstPlayer = False;
				else PlayersList ^= ",";
				
				PlayersList ^= """
				{
					"login": "{{{TL::MLEncode(User.Login)}}}",
					"ladderpoints": {{{ML::NearestInteger(User.LadderPoints)}}},
					"clan": {{{Clan}}},
					"order": {{{Order}}}
				}""";
			}
			
			if (Valid && PlayersList != "") {
				if (FirstMatch) FirstMatch = False;
				else MatchesList ^= ",";
				
				MatchesList ^= """
		{
			"players": [
				{{{PlayersList}}}
			]
		}""";
			}
		}
		
		G_Matchmaking_CurrentFormat = G_Matchmaking_Format;
		
		// Waiting players
		foreach (Player in AllPlayers) {
			if (ReadyPlayers.exists(Player.Login)) continue;
			if (WaitingLogins != "") WaitingLogins ^= ",";
			WaitingLogins ^= "\""^Player.Login^"\"";
		}
		
		PostData = """
{
	"lobby": "{{{TL::MLEncode(ServerLogin)}}}",
	"gamemode": "{{{TL::MLEncode(ServerModeName)}}}",
	"titleid": "{{{TL::MLEncode(LoadedTitle.TitleId)}}}",
	"format": {{{G_Matchmaking_CurrentFormat}}},
	"matches": [
		{{{MatchesList}}}
	],
	"waitinglogins": [
		{{{WaitingLogins}}}
	],
	"cancelers": [
		{{{CancelersList}}}	
	],
	"penalties": [
		{{{PenaltiesList}}}
	]
}""";
		
		declare Request <=> Http.CreatePost(Private_MM_BuildAPIUrl("/lobby-server/match"), PostData);
		if (Request != Null) Lobby_MatchMakerRequestId = Request.Id;
		
		if (S_MatchmakingLogAPIDebug) MB_Log("[API] Request: POST /lobby-server/match\n<!--\n"^PostData^"\n-->");

	// ---------------------------------- //
	// Standard playing
	} else {
		declare PlayersList = "";
		declare FirstPlayer = True;
		declare AvailablePlayersNb = 0;
		declare WaitingLogins = "";
		declare Ident[] ReadyPlayers;
		
		foreach (Player in AllPlayers) {
			declare Integer Lobby_ReadySince for Player = -1;
			declare Integer[Ident] Lobby_Allies for Player.User;
			declare Lobby_AlliesAreReady for Player = False;
			declare Lobby_LastTransfertTime for Player.User = -1;
			
			// Only ready players
			// @specstart valid
			//if (Player.RequestsSpectate) continue;
			if (!Private_Lobby_IsReady(Player.User)) continue;
			// @specend
			
			// Update averate waiting time on the lobby
			if (Lobby_ReadySince > 0) {
				AverageWaitingTimeTotal += Now - Lobby_ReadySince;
				AverageWaitingTimeCount += 1;
			}
			
			// Wait a bit of time before listing a player as ready when he was transfered to a server
			if (Lobby_LastTransfertTime > 0 && Now - Lobby_LastTransfertTime < C_Lobby_TransfertSafeTime) {
				if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^Player.Login^" > Skip ready, waiting transfert since "^Now - Lobby_LastTransfertTime^"ms");
				continue;
			}
			
			// If one of the player ally is not ready don't send him in a match
			if (Lobby_Allies.count > 0 && !Lobby_AlliesAreReady) continue;
			
			if (FirstPlayer) FirstPlayer = False;
			else PlayersList ^= ",";
			
			declare AlliesList = "";
			declare FirstAlly = True;
			foreach (AllyId => AllyStatus in Lobby_Allies) {
				if (AllyStatus != C_AllyStatus_Validated) continue;
				if (!Users.existskey(AllyId)) continue;
				
				if (FirstAlly) FirstAlly = False;
				else AlliesList ^= ",";
				AlliesList ^= "\""^TL::MLEncode(Users[AllyId].Login)^"\"";
			}
			
			AvailablePlayersNb += 1;

			ReadyPlayers.add(Player.Id);
			
			PlayersList ^= """
		{
			"login": "{{{TL::MLEncode(Player.Login)}}}",
			"ladderpoints": {{{ML::NearestInteger(Player.User.LadderPoints)}}},
			"readytime": {{{Now - Lobby_ReadySince}}},
			"allies": [{{{AlliesList}}}]
		}""";
		}
		
		G_Matchmaking_CurrentFormat = G_Matchmaking_Format;
		
		if (MM_MatchmakingIsProgressive()) {
			declare RequiredPlayersNb = 0;
			declare netwrite Integer Net_Lobby_PlayersNb for Teams[0];
			
			// How many players are required
			foreach (PlayersNb in G_Matchmaking_Format) {
				RequiredPlayersNb += PlayersNb;
			}
			
			/* Activate progressive mode if there's not enough players ready on the lobby
			 * and there's not enough players in the matchmaking infrastructure
			 * or players are waiting since a long time
			 * We try to find the format that would allow a maximum of players to play
			 */
			declare AverageWaitingTime = 0;
			if (AverageWaitingTimeCount != 0) AverageWaitingTime = AverageWaitingTimeTotal / AverageWaitingTimeCount;
			if (
				AvailablePlayersNb < RequiredPlayersNb 
				&& (
					AverageWaitingTime >= S_ProgressiveActivation_WaitingTime 
					|| Net_Lobby_PlayersNb < RequiredPlayersNb * S_ProgressiveActivation_PlayersNbRatio
				)
			) {
				declare PlayingNb = 0;
				foreach (AvailableFormat in G_Matchmaking_ProgressiveFormats) {
					// How many players are required in this format ?
					RequiredPlayersNb = 0;
					foreach (PlayersNb in AvailableFormat) {
						RequiredPlayersNb += PlayersNb;
					}
					
					// If there's enough players for this format and more playing players than in one of the previously selected format
					if (RequiredPlayersNb <= AvailablePlayersNb && RequiredPlayersNb > PlayingNb) {
						PlayingNb = RequiredPlayersNb;
						G_Matchmaking_CurrentFormat = AvailableFormat;
					}
				}
			}
		}
		
		// Waiting players
		foreach (Player in AllPlayers) {
			if (ReadyPlayers.exists(Player.Id)) continue;
			if (WaitingLogins != "") WaitingLogins ^= ",";
			WaitingLogins ^= "\""^Player.Login^"\"";
		}
	
		PostData = """
{
	"lobby": "{{{TL::MLEncode(ServerLogin)}}}",
	"gamemode": "{{{TL::MLEncode(ServerModeName)}}}",
	"titleid": "{{{TL::MLEncode(LoadedTitle.TitleId)}}}",
	"format": {{{G_Matchmaking_CurrentFormat}}},
	"players": [
		{{{PlayersList}}}
	],
	"waitinglogins": [
		{{{WaitingLogins}}}
	],
	"cancelers": [
		{{{CancelersList}}}	
	],
	"penalties": [
		{{{PenaltiesList}}}
	]
}""";
	
		declare Request <=> Http.CreatePost(Private_MM_BuildAPIUrl("/lobby-server/matchmaking-live"), PostData);
		if (Request != Null) Lobby_MatchMakerRequestId = Request.Id;
		
		if (S_MatchmakingLogAPIDebug) MB_Log("[API] Request: POST /lobby-server/matchmaking-live\n<!--\n"^PostData^"\n-->");
	}
}

declare netwrite Integer Net_Lobby_AverageWaitingTime for Teams[0];
if (AverageWaitingTimeTotal > 0 && AverageWaitingTimeCount > 0) {
	Net_Lobby_AverageWaitingTime = AverageWaitingTimeTotal / AverageWaitingTimeCount;
} else {
	Net_Lobby_AverageWaitingTime = -1;
}
***

***MatchMakerRun***
***
declare Ident Lobby_MatchMakerRequestId for This;
declare Ident ToRemove;
foreach (Request in Http.Requests) {
	if (Request.Id != Lobby_MatchMakerRequestId) continue;
	
	if (Request.IsCompleted) {
		// Success
		if (Request.StatusCode == 200) {
			MM_GetMatches(Request.Result);
		}
		// Fail
		else {
			if (S_MatchmakingLogAPIError) {
				if (Request.StatusCode == 401) {
					MB_Log("[ERROR] Matchmaking HTTP API Error 401. Waiting for your server to register on Nadeo master server.");
				} else if (Request.StatusCode == 404) {
					MB_Log("[ERROR] Matchmaking HTTP API Error 404. Maybe the URL in the setting is wrong.");
				} else {
					MB_Log("[ERROR] Matchmaking HTTP Error "^Request.StatusCode^".");
				}
			}
			if (S_MatchmakingErrorMessage != "") UIManager.UIAll.SendChat(TL::Compose("%1 %2", C_MessagePrefix, S_MatchmakingErrorMessage));
		}
		ToRemove = Request.Id;
	}
}
if (ToRemove != NullId) {
	Http.Destroy(Http.Requests[ToRemove]);
	Lobby_MatchMakerRequestId = NullId;
}
***

***MatchMakerStop***
***
// Destroy any previous request
declare Ident Lobby_MatchMakerRequestId for This;
if (Lobby_MatchMakerRequestId != NullId) {
	if (Http.Requests.existskey(Lobby_MatchMakerRequestId)) {
		if (G_MMLobby_MatchmakingEnabled) {
			if (S_MatchmakingLogAPIError) MB_Log("[ERROR] Matchmaking HTTP Error XXX. API timeout.");
			if (S_MatchmakingErrorMessage != "") UIManager.UIAll.SendChat(TL::Compose("%1 %2", C_MessagePrefix, S_MatchmakingErrorMessage));
		}
		Http.Destroy(Http.Requests[Lobby_MatchMakerRequestId]);
		Lobby_MatchMakerRequestId = NullId;
	}
}

foreach (Player in AllPlayers) {
	Private_MM_HideVersusML(Player);
}

// Send players to their match
MM_SendToMatches();
***

// ---------------------------------- //
// Functions
// ---------------------------------- //
// ---------------------------------- //
/** Inject the ready helper functions into a manialink
 *
 *	@return		The helper functions
 */
Text Private_Lobby_InjectReadyHelpers() {
	return """
Void Private_Lobby_ToggleReady() {
	declare netwrite Integer Net_Lobby_ReadySynchroClient for UI;
	declare netread Integer Net_Lobby_ReadySynchroServer for Teams[0];
	declare netread Boolean Net_Lobby_Ready for UI;
	declare netwrite Boolean Net_Lobby_ToggleReady for UI;
	declare netwrite Integer Net_Lobby_ToggleReadyUpdate for UI;
	
	Net_Lobby_ReadySynchroClient = Net_Lobby_ReadySynchroServer;
	Net_Lobby_ToggleReady = !Net_Lobby_Ready;
	Net_Lobby_ToggleReadyUpdate = Now;
}

Void Private_Lobby_SetReady(Boolean _IsReady) {
	declare netwrite Integer Net_Lobby_ReadySynchroClient for UI;
	declare netread Integer Net_Lobby_ReadySynchroServer for Teams[0];
	declare netwrite Boolean Net_Lobby_ToggleReady for UI;
	declare netwrite Integer Net_Lobby_ToggleReadyUpdate for UI;
	
	Net_Lobby_ReadySynchroClient = Net_Lobby_ReadySynchroServer;
	Net_Lobby_ToggleReady = _IsReady;
	Net_Lobby_ToggleReadyUpdate = Now;
}

Boolean Private_Lobby_IsReady() {
	declare netread Boolean Net_Lobby_ImBlocked for UI;
	if (Net_Lobby_ImBlocked) return False;
	
	if (LocalUser.IsFakeUser) return True;
	
	declare netread Boolean Net_Lobby_Ready for UI;
	return Net_Lobby_Ready;
}

Boolean Private_Lobby_IsReady(CSmPlayer _Player) {
	if (_Player == Null) return False;
	
	declare netread Net_Lobby_IsBlocked for _Player = False;
	if (Net_Lobby_IsBlocked) return False;
	
	if (_Player.User.IsFakeUser) return True;
	
	declare netread Boolean Net_Lobby_Ready for _Player;
	return Net_Lobby_Ready;
}
""";
}

// ---------------------------------- //
/// Synchronize server and client
Void Private_Lobby_SynchroReady() {
	declare netwrite Net_Lobby_ReadySynchroServer for Teams[0] = 0;
	Net_Lobby_ReadySynchroServer += 1;
}

// ---------------------------------- //
/** Send the ready state of a player
 *
 *	@param	_Player		The player to check
 *	@param	_IsReady	The ready state
 */
Void Private_Lobby_SendReadyState(CSmPlayer _Player, Boolean _IsReady) {
	if (!XmlRpc::CallbackIsAllowed("Matchmaking_ReadyState")) return;
	
	declare ReadyState = "False";
	if (_IsReady) ReadyState = "True";
	XmlRpc::SendCallbackArray("Matchmaking_ReadyState", [_Player.Login, ReadyState]);
}

// ---------------------------------- //
/** Find and send the ready state of a player
 *
 *	@param	_Player		The player to check
 */
Void Private_Lobby_SendReadyState(CSmPlayer _Player) {
	if (!XmlRpc::CallbackIsAllowed("Matchmaking_ReadyState")) return;
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	declare netwrite Boolean Net_Lobby_Ready for UI;
	Private_Lobby_SendReadyState(_Player, Net_Lobby_Ready);
}

// ---------------------------------- //
/** Set the ready state of an user
 *
 *	@param	_User		The user to update
 *	@param	_IsReady	The new ready state
 */
Void Private_Lobby_SetReady(CUser _User, Boolean _IsReady) {
	if (_User == Null) return;
	declare UI <=> UIManager.GetUI(_User);
	if (UI == Null) return;
	
	// Don't let a blocked player getting ready
	declare Lobby_IsBlocked for _User = False;
	if (Lobby_IsBlocked && _IsReady) return;
	
	declare netwrite Boolean Net_Lobby_Ready for UI;
	Net_Lobby_Ready = _IsReady;
	
	declare Player <=> SM::GetPlayer(_User.Login);
	if (Player != Null) {
		declare netwrite Boolean Net_Lobby_Ready as ReadyForPlayer for Player;
		ReadyForPlayer = _IsReady;
		Private_Lobby_SendReadyState(Player, Net_Lobby_Ready);
	}
}

// ---------------------------------- //
/** Check if an user is ready
 *
 *	@param	_User		The user to check
 *
 *	@return				True if the user is ready, false otherwise
 */
Boolean Private_Lobby_IsReady(CUser _User) {
	if (_User == Null) return False;
	
	// A blocked player can't be ready
	declare Lobby_IsBlocked for _User = False;
	if (Lobby_IsBlocked) return False;
	// A bot is always ready
	if (_User.IsFakeUser) return True;
	
	declare UI <=> UIManager.GetUI(_User);
	if (UI == Null) return False;
	
	declare netwrite Boolean Net_Lobby_Ready for UI;
	return Net_Lobby_Ready;
}

// ---------------------------------- //
/** Check ready update from the client
 *
 *	@param	_Player		The player to check
 */
Void Private_Lobby_UpdateReady(CSmPlayer _Player) {
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	declare netread Integer Net_Lobby_ReadySynchroClient for UI;
	declare netwrite Integer Net_Lobby_ReadySynchroServer for Teams[0];
	declare netwrite Boolean Net_Lobby_Ready for UI;
	declare netwrite Boolean Net_Lobby_Ready as ReadyForPlayer for _Player;
	declare netread Boolean Net_Lobby_ToggleReady for UI;
	declare netread Integer Net_Lobby_ToggleReadyUpdate for UI;
	
	declare Lobby_PrevToggleReadyUpdate for _Player = 0;
	
	if (Lobby_PrevToggleReadyUpdate != Net_Lobby_ToggleReadyUpdate) {
		Lobby_PrevToggleReadyUpdate = Net_Lobby_ToggleReadyUpdate;
		
		if (Net_Lobby_ReadySynchroServer == Net_Lobby_ReadySynchroClient) {
			// Don't let a blocked player getting ready
			declare Lobby_IsBlocked for _Player.User = False;
			if (!Lobby_IsBlocked || (Lobby_IsBlocked && !Net_Lobby_ToggleReady)) {
				Net_Lobby_Ready = Net_Lobby_ToggleReady;
				ReadyForPlayer = Net_Lobby_Ready;
				Private_Lobby_SendReadyState(_Player, Net_Lobby_Ready);
			}
		}
	}
}

// ---------------------------------- //
/** Convert an echelon to an integer
 *
 *	@param	_Echelon	The echelon to convert
 *
 *	@return		The echelon converted to an Integer
 */
Integer Private_Lobby_ToInteger(CUser::EEchelon _Echelon) {
	switch (_Echelon) {
		case CUser::EEchelon::Bronze1	: return 1;
		case CUser::EEchelon::Bronze2	: return 2;
		case CUser::EEchelon::Bronze3	: return 3;
		case CUser::EEchelon::Silver1	: return 4;
		case CUser::EEchelon::Silver2	: return 5;
		case CUser::EEchelon::Silver3	: return 6;
		case CUser::EEchelon::Gold1		: return 7;
		case CUser::EEchelon::Gold2		: return 8;
		case CUser::EEchelon::Gold3		: return 9;
	}
	
	return 0;
}

// ---------------------------------- //
/** Get the duration of one round
 *
 *	@return		the duration of one round
 */
Integer Private_Lobby_GetRoundDuration() {
	return S_LobbyMatchmakerPerRound * (S_LobbyMatchmakerWait + S_LobbyMatchmakerTime) * 1000;
}

// ---------------------------------- //
/** Build the url to call the API
 *
 *	@param	_Path		The endpoint to call
 */
Text Private_MM_BuildAPIUrl(Text _Path) {
	return S_MatchmakingAPIUrl^_Path;
}

// ---------------------------------- //
/// Update the lobby players list
Void Private_Lobby_UpdatePlayersList() {
	declare netwrite Integer Net_Lobby_PlayersListUpdate for Teams[0];
	Net_Lobby_PlayersListUpdate = Now;
}

// ---------------------------------- //
/** Show the versus manialink to a player
 *
 *	@param	_Player		The player to update
 */
Void Private_MM_ShowVersusML(CSmPlayer _Player) {
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	UnspawnPlayer(_Player);
	
	declare Lobby_IsSubstitute for _Player.User = False;
	if (Lobby_IsSubstitute) {
		declare netwrite Net_Lobby_ShowSubstituteML for UI = False;
		Net_Lobby_ShowSubstituteML = True;
		
		declare Lobby_MatchScores for _Player.User = "";
		declare netwrite Net_Lobby_MatchScores for UI = "";
		Net_Lobby_MatchScores = Lobby_MatchScores;
	} else {
		declare netwrite Net_Lobby_ShowVersusML for UI = False;
		Net_Lobby_ShowVersusML = True;
	}
	
	declare netwrite Net_Lobby_SelectedForMatch for _Player = False;
	Net_Lobby_SelectedForMatch = True;
	
	declare Lobby_OnVersusScreen for _Player = False;
	Lobby_OnVersusScreen = True;
	
	Private_Lobby_UpdatePlayersList();
}

// ---------------------------------- //
/** Hide the versus manialink from a player
 *
 *	@param	_Player		The player to update
 */
Void Private_MM_HideVersusML(CSmPlayer _Player) {
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	declare netwrite Net_Lobby_ShowSubstituteML for UI = False;
	declare netwrite Net_Lobby_ShowVersusML for UI = False;
	Net_Lobby_ShowSubstituteML = False;
	Net_Lobby_ShowVersusML = False;
	
	declare netwrite Net_Lobby_SelectedForMatch for _Player = False;
	Net_Lobby_SelectedForMatch = False;
	
	declare Lobby_OnVersusScreen for _Player = False;
	Lobby_OnVersusScreen = False;
	
	declare Lobby_MatchScores for _Player.User = "";
	declare netwrite Net_Lobby_MatchScores for UI = "";
	Lobby_MatchScores = "";
	Net_Lobby_MatchScores = "";
	
	Private_Lobby_UpdatePlayersList();
}

// ---------------------------------- //
/// Empty the masters list
Void Private_MM_ClearMasters() {
	declare netwrite Integer Net_Lobby_MastersUpdate for Teams[0];
	declare netwrite Text[Integer][] Net_Lobby_Masters for Teams[0];
	Net_Lobby_Masters.clear();
	Net_Lobby_MastersUpdate = Now;
}

// ---------------------------------- //
/** Add several players to the masters list
 *
 *	@param	_Player		The player to add
 *	@param	_Timestamp	The last time this player was a master
 */
Void Private_MM_AddMasters(Text[] _Logins) {
	declare netwrite Integer Net_Lobby_MastersUpdate for Teams[0];
	declare netwrite Text[Integer][] Net_Lobby_Masters for Teams[0];
	
	foreach (Login in _Logins) {
		declare User <=> SM::GetUser(Login);
		if (User == Null) continue;
		Net_Lobby_Masters.add([
			C_Master_Name		=> User.Name,
			C_Master_Country	=> User.CountryFlagUrl,
			C_Master_Echelon	=> TL::ToText(Private_Lobby_ToInteger(User.Echelon))
		]);
	}
	
	if (Net_Lobby_Masters.count > C_Lobby_MastersNb) {
		declare RemoveNb = Net_Lobby_Masters.count - C_Lobby_MastersNb;
		while (RemoveNb > 0) {
			declare Removed = Net_Lobby_Masters.removekey(Net_Lobby_Masters.count-1);
			RemoveNb -= 1;
		}
	}
	
	Net_Lobby_MastersUpdate = Now;
}

// ---------------------------------- //
/** Get the match id
 *
 *	@return		The match id
 */
Text MM_GetMatchId() {
	return G_MMMatch_Id;
}

// ---------------------------------- //
/** Set the new matchmaking mode on this server
 *
 *	@param	_NewMode		The new mode
 */
Void MM_SetMode(Integer _NewMode) {
	switch (_NewMode) {
		case C_Matchmaking_Lobby			: G_Matchmaking_Mode = C_Matchmaking_Lobby;
		case C_Matchmaking_Match			: G_Matchmaking_Mode = C_Matchmaking_Match;
		case C_Matchmaking_UniversalLobby	: G_Matchmaking_Mode = C_Matchmaking_UniversalLobby;
		case C_Matchmaking_UniversalMatch	: G_Matchmaking_Mode = C_Matchmaking_UniversalMatch;
		default								: G_Matchmaking_Mode = C_Matchmaking_Off;
	}
	
	declare netwrite Integer Net_Matchmaking_Mode for Teams[0];
	Net_Matchmaking_Mode = G_Matchmaking_Mode;
}

// ---------------------------------- //
/** Check if a server is in match mode
 *
 *	@return		True if it's a match server, false otherwise
 */
Boolean MM_IsMatchServer() {
	return (G_Matchmaking_Mode == C_Matchmaking_Match || G_Matchmaking_Mode == C_Matchmaking_UniversalMatch);
}

// ---------------------------------- //
/** Check if a server is in lobby mode
 *
 *	@return		True if it's a lobby server, false otherwise
 */
Boolean MM_IsLobbyServer() {
	return (G_Matchmaking_Mode == C_Matchmaking_Lobby || G_Matchmaking_Mode == C_Matchmaking_UniversalLobby);
}

// ---------------------------------- //
/** Check if we are in universal mode
 *
 *	@return		True if it's an universal server, false otherwise
 */
Boolean MM_IsUniversalServer() {
	return (G_Matchmaking_Mode == C_Matchmaking_UniversalLobby || G_Matchmaking_Mode == C_Matchmaking_UniversalMatch);
}

// ---------------------------------- //
/** Check if we are on a matchmaking server
 *
 *	@return		True if it's a matchmaking server, false otherwise
 */
Boolean MM_IsMatchmakingServer() {
	return (G_Matchmaking_Mode != C_Matchmaking_Off);
}

// ---------------------------------- //
/** Check if the matchmaking is progressive
 *
 *	@return		True if the matchmaking is progressive, False otherwise
 */
Boolean MM_MatchmakingIsProgressive() {
	return (S_MatchmakingProgressive && !MM_IsUniversalServer());
}

// ---------------------------------- //
/** Create a link to join another server in the same title
 *
 *	@param	_ServerLogin	The login of the server to join
 */
Text MM_GetServerJoinLink(Text _ServerLogin) {
	return "maniaplanet://#qjoin="^_ServerLogin^"@"^LoadedTitle.TitleId;
}

// ---------------------------------- //
/** Update the karma of an user (manage penalties)
 *
 *	@param	_User	The user to update
 */
Void MM_UpdateKarma(CUser _User) {
	declare Lobby_Penalty for _User = -1;
	declare Lobby_IsBlocked for _User = False;
	declare Lobby_MatchCancellation for _User = 0;
	
	if (Lobby_IsBlocked) {
		// Unblock player
		if (Lobby_Penalty < 0 || (Now >= Lobby_Penalty && Lobby_Penalty > 0)) {
			Lobby_IsBlocked = False;
			Lobby_Penalty = -1;
		} else {
			// @specstart
			// Check that players is forced into spectator
			/*if (!_User.RequestsSpectate) {
				Users_RequestSwitchToSpectator(_User);
			}*/
			// Check that player is forced to non ready mode
			if (Private_Lobby_IsReady(_User)) {
				Private_Lobby_SetReady(_User, False);
			}
			// @specend
		}
	} else {
		// Block player
		if (Lobby_Penalty >= 0) {
			Lobby_IsBlocked = True;
			// @specstart
			//Users_RequestSwitchToSpectator(_User);
			Private_Lobby_SetReady(_User, False);
			// @specend
		}
	}
	
	declare UI <=> UIManager.GetUI(_User);
	if (UI != Null) {
		declare netwrite Net_Lobby_Penalty for UI = -1;
		declare netwrite Net_Lobby_ImBlocked for UI = False;
		declare netwrite Net_Lobby_MatchCancellation for UI = 0;
		
		Net_Lobby_MatchCancellation = Lobby_MatchCancellation;
		
		if (Net_Lobby_Penalty != Lobby_Penalty || Net_Lobby_ImBlocked != Lobby_IsBlocked) {
			Net_Lobby_Penalty = Lobby_Penalty;
			Net_Lobby_ImBlocked = Lobby_IsBlocked;
			
			// @specstart valid
			/*if (Lobby_IsBlocked) {
				UI.ForceSpectator = True;
			} else {
				UI.ForceSpectator = False;
			}*/
			// @specend
			
			declare Player <=> SM::GetPlayer(_User.Login);
			if (Player != Null) {
				declare netwrite Net_Lobby_IsBlocked for Player = False;
				Net_Lobby_IsBlocked = Lobby_IsBlocked;
			}
			
			Private_Lobby_UpdatePlayersList();
		}
	}
}

// ---------------------------------- //
/** Get an user penalty
 *
 *	@param	_User	The user to get
 *
 *	@return			The user penalty
 */
Integer MM_GetPlayerPenalty(CUser _User) {
	if (_User == Null) return -1;
	declare Lobby_Penalty for _User = -1;
	return Lobby_Penalty;
}

// ---------------------------------- //
/** Set an user penalty
 *
 *	@param	_User		The user penalty to set
 *	@param	_EndTime	The end time of the penalty
 */
Void MM_SetPlayerPenalty(CUser _User, Integer _EndTime) {
	if (_User == Null) return;
	declare Lobby_Penalty for _User = -1;
	Lobby_Penalty = _EndTime;
}

// ---------------------------------- //
/** Reset an user penalty
 *
 *	@param	_User	The user to reset
 */
Void MM_CancelPlayerPenalty(CUser _User) {
	if (_User == Null) return;
	declare Lobby_Penalty for _User = -1;
	Lobby_Penalty = -1;
}

// ---------------------------------- //
/** Penalize a player
 *
 *	@param	_User			The user to penalize
 *	@param	_Duration		Duration of the penalty (0 is infinite) (in seconds)
 *	@param	_MatchId		Id of the match canceled if any
 *	@param	_IsSubsitute	The player was being sent as a substitute
 *	@param	_IsReconnect	The player was reconnecting to a match
 */
Void Private_PenalizePlayer(CUser _User, Integer _Duration, Text _MatchId, Boolean _IsSubstitute, Boolean _IsReconnect) {
	if (_User == Null) return;
	
	if (_Duration > 0) {
		if (!_IsSubstitute) {
			MM_SetPlayerPenalty(_User, Now + (_Duration * 1000));
			if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^_User.Login^" > Penalized for "^_Duration^"s");
		}
	} else if (_Duration == 0) {
		if (!_IsSubstitute) {
			MM_SetPlayerPenalty(_User, 0);
			if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^_User.Login^" > Penalized indefinitely");
		}
	} else {
		MM_CancelPlayerPenalty(_User);
		if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^_User.Login^" > Penalty canceled");
	}
	
	// Mark player as canceler
	if (_MatchId != "") {
		declare Lobby_CancelerMatchId for _User = "";
		declare Lobby_CancelerIsSubstitute for _User = False;
		declare Lobby_CancelerIsReconnect for _User = False;
		Lobby_CancelerMatchId = _MatchId;
		Lobby_CancelerIsSubstitute = _IsSubstitute;
		Lobby_CancelerIsReconnect = _IsReconnect;
	}
	
	declare Lobby_IsApiBlocked for _User = False;
	Lobby_IsApiBlocked = True;
}

Void MM_PenalizePlayer(CUser _User, Text _MatchId, Boolean _IsSubstitute, Boolean _IsReconnect) {
	Private_PenalizePlayer(_User, 0, _MatchId, _IsSubstitute, _IsReconnect);
}

Void MM_PenalizePlayer(CUser _User, Integer _Duration) {
	Private_PenalizePlayer(_User, _Duration, "", False, False);
}

// ---------------------------------- //
/** Send a player to another server
 *
 *	@param	_Player			The player to send
 *	@param	_ServerLogin	The server login where to send the player
 */
Void MM_SendToServer(CPlayer _Player, Text _ServerLogin) {
	if (_Player == Null || _ServerLogin == "") return;
	
	declare Lobby_LastTransfertTime for _Player.User = -1;
	Lobby_LastTransfertTime = Now;
	
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	declare netwrite Integer Net_Lobby_JoinLinkUpdate for UI;
	declare netwrite Text Net_Lobby_JoinLink for UI;
	Net_Lobby_JoinLinkUpdate = Now;
	Net_Lobby_JoinLink = MM_GetServerJoinLink(_ServerLogin);
}

// ---------------------------------- //
/** Manage player reconnection after a leave
 *
 *	@param	_Player		The player to reconnect
 */
Void MM_ReconnectToServer(CPlayer _Player) {
	if (_Player == Null) return;
	
	declare Lobby_ReconnectToServer for _Player = "";
	declare Lobby_ReconnectToMatchId for _Player = "";
	
	declare UI <=> UIManager.GetUI(_Player);
		
	if (Lobby_ReconnectToServer != "") {
		if (UI != Null) {
			declare netwrite Net_Lobby_ReconnectToServer for UI = "";
			
			if (Net_Lobby_ReconnectToServer == "") {
				Net_Lobby_ReconnectToServer = Lobby_ReconnectToServer;
				// @specstart valid
				//Users_RequestSwitchToSpectator(_Player.User);
				//UI.ForceSpectator = True;
				Private_Lobby_SetReady(_Player.User, False);
				// @specend
				UI.CountdownEndTime = Now + C_Lobby_ReconnectDuration;
			}
		} else {
			Lobby_ReconnectToServer = "";
			Lobby_ReconnectToMatchId = "";
		}
		
		// Check that players is forced into spectator
		// @specstart valid
		/*if (!_Player.User.RequestsSpectate) {
			Users_RequestSwitchToSpectator(_Player.User);*/
		if (Private_Lobby_IsReady(_Player.User)) {
			Private_Lobby_SetReady(_Player.User, False);
		// @specend
			// Cancel send back to match server
			if (UI != Null) {
				if (S_MatchmakingLogMiscDebug) {
					MB_Log("[SERVER] "^_Player.Login^" > Cancel reconnection to match on server : \""^Lobby_ReconnectToServer^"\"");
				}
				
				MM_PenalizePlayer(_Player.User, Lobby_ReconnectToMatchId, False, True);
				
				declare netwrite Net_Lobby_ReconnectToServer for UI = "";
				Net_Lobby_ReconnectToServer = "";
				// @specstart valid
				//UI.ForceSpectator = False;
				// @specend
				UI.CountdownEndTime = -1;
				Lobby_ReconnectToServer = "";
				Lobby_ReconnectToMatchId = "";
			}
		}
		
		// Send back player to server at the end of the countdown
		if (UI != Null) {
			if (UI.CountdownEndTime > 0 && Now >= UI.CountdownEndTime) {
				MM_SendToServer(_Player, Lobby_ReconnectToServer);
				
				if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^_Player.Login^" > Reconnected back to match on server : \""^Lobby_ReconnectToServer^"\"");
				
				declare netwrite Net_Lobby_ReconnectToServer for UI = "";
				Net_Lobby_ReconnectToServer = "";
				// @specstart valid
				//UI.ForceSpectator = False;
				// @specend
				UI.CountdownEndTime = -1;
				Lobby_ReconnectToServer = "";
				Lobby_ReconnectToMatchId = "";
			}
		}
	}
}

// ---------------------------------- //
/** Let a player cancel a match
 *
 *	@param	_User		The user who want to cancel his match
 */
Void MM_CancelMatch(CUser _User) {
	if (_User == Null) return;
	
	declare Lobby_MatchId as Lobby_CancelMatchId for _User = "";
	declare Lobby_MatchServer as Lobby_CancelMatchServer for _User = "";
	declare Lobby_IsSubstitute as Lobby_CancelIsSubstitute for _User = False;

	declare CancelMatchId = Lobby_CancelMatchId;
	declare CancelMatchServer = Lobby_CancelMatchServer;
	declare CancelIsSubstitute = Lobby_CancelIsSubstitute;

	if (CancelMatchServer != "") {
		if (S_MatchmakingLogMiscDebug) {
			if (CancelIsSubstitute) MB_Log("[SERVER] "^_User.Login^" > Cancel substitute ("^CancelMatchId^") on server: "^CancelMatchServer);
			else MB_Log("[SERVER] "^_User.Login^" > Cancel match ("^CancelMatchId^") on server: "^CancelMatchServer);
		}
		
		MM_PenalizePlayer(_User, CancelMatchId, CancelIsSubstitute, False);
	}
	
	if (CancelMatchId != "") {
		// Normal match
		if (!CancelIsSubstitute) {
			foreach (User in Users) {
				declare Lobby_MatchId for User = "";
				declare Lobby_MatchServer for User = "";
				declare Lobby_IsSubstitute for User = False;
	
				if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^User.Login^" > My match id : "^Lobby_MatchId^" | The match to cancel : "^CancelMatchId);
				
				if (CancelMatchId != Lobby_MatchId) continue;
				
				if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^User.Login^" > Match canceled ("^Lobby_MatchId^")");
				
				Lobby_MatchId = "";
				Lobby_MatchServer = "";
				Lobby_IsSubstitute = False;
				
				declare Player <=> SM::GetPlayer(User.Login);
				Private_MM_HideVersusML(Player);
				
				declare UI <=> UIManager.GetUI(User);
				if (UI != Null) {
					UI.SendChat(TL::Compose(_("%1$<%2$> canceled match start."), C_MessagePrefix, _User.Name));
				}
			}
		} 
		// Substitute or player join the lobby
		else {
			declare Lobby_MatchId for _User = "";
			declare Lobby_MatchServer for _User = "";
			declare Lobby_IsSubstitute for _User = False;
			Lobby_MatchId = "";
			Lobby_MatchServer = "";
			Lobby_IsSubstitute = False;
			declare Player <=> SM::GetPlayer(_User.Login);
			Private_MM_HideVersusML(Player);
		}
	}
}

// ---------------------------------- //
/** Parse the matches XML and send player to their match server
 *
 *	@param	_MatchesXML			The XML containing the matches
 */
Void MM_GetMatches(Text _MatchesXML) {
	if (S_MatchmakingLogAPIDebug) MB_Log("[API] Response: POST /lobby-server/matchmaking-live\n<!--\n"^_MatchesXML^"\n-->");
	// Get matches info from XML
	declare MatchesPlayers = Integer[CSmPlayer][Text];
	declare MatchesLogins = Integer[Text][Text];
	declare MatchesUsersIds = Ident[][Integer][Text];
	declare MatchesCancelers = Ident[];
	declare XmlDoc <=> Xml.Create(_MatchesXML);
	if (XmlDoc != Null && XmlDoc.Root != Null) {
		declare NodeStatus <=> XmlDoc.Root.GetFirstChild("status");
		if (NodeStatus != Null) {
			declare netwrite Integer Net_Lobby_PlayersNb for Teams[0];
			declare netwrite Integer Net_Lobby_ServersAvailable for Teams[0];
			Net_Lobby_PlayersNb = NodeStatus.GetAttributeInteger("playersnb", 0);
			Net_Lobby_ServersAvailable = NodeStatus.GetAttributeInteger("availableservers", 0);
			
			declare MatchPlayersNb = 0;
			declare SubstitutePlayersNb = 0;
			declare CantBeSubstitute = Ident[];
			
			// No match server available
			if (!S_LobbyDisableUI && Net_Lobby_ServersAvailable <= 0) {
				Message::SendStatusMessage(
					_("Matchmaking canceled : no server available"),
					G_MMLobby_EndTime - Now,
					1
				);
			}
			
			declare NodeMatches <=> XmlDoc.Root.GetFirstChild("matches");
			if (NodeMatches != Null) {
				foreach (NodeMatch in NodeMatches.Children) {
					declare MatchId = NodeMatch.GetAttributeText("id", "");
					if (MatchId == "") continue;
					MatchesPlayers[MatchId] = Integer[CSmPlayer];
					MatchesLogins[MatchId] = Integer[Text];
					MatchesUsersIds[MatchId] = Ident[][Integer];
					// Match
					if (NodeMatch.Name == "match") {
						declare MatchServerLogin = "";
						foreach (Node in NodeMatch.Children) {
							// Server
							if (Node.Name == "server") {
								MatchServerLogin = Node.GetAttributeText("match", "");
							} 
							// Players
							else if (Node.Name == "players" && MatchServerLogin != "") {
								// Player
								foreach (NodePlayer in Node.Children) {
									declare Login = NodePlayer.GetAttributeText("login", "");
									declare Clan = NodePlayer.GetAttributeInteger("clan", -1);
									if (G_Matchmaking_CurrentFormat.count > 1) Clan += 1;
									if (Login != "" && Clan >= 0) {
										declare Player <=> SM::GetPlayer(Login);
										if (Player != Null) {
											declare Lobby_MatchId for Player.User = "";
											declare Lobby_MatchServer for Player.User = "";
											declare Lobby_IsSubstitute for Player.User = False;
											Lobby_MatchId = MatchId;
											Lobby_MatchServer = MatchServerLogin;
											Lobby_IsSubstitute = False;
											MatchesPlayers[MatchId][Player] = Clan;
											MatchesLogins[MatchId][Player.Login] = Clan;
											if (!MatchesUsersIds[MatchId].existskey(Clan)) MatchesUsersIds[MatchId][Clan] = Ident[];
											MatchesUsersIds[MatchId][Clan].add(Player.User.Id);
											MatchPlayersNb += 1;
											CantBeSubstitute.add(Player.Id);
											
											// @specstart valid
											//if (Player.RequestsSpectate) {
											if (!Private_Lobby_IsReady(Player.User)) {
											// @specend
												MatchesCancelers.add(Player.User.Id);
											}
										}
									}
								}
							}
						}
					}
				}
			}
			
			declare NodeSubstitutes <=> XmlDoc.Root.GetFirstChild("substitutes");
			if (NodeSubstitutes != Null) {
				foreach (NodePlayer in NodeSubstitutes.Children) {
					declare Login = NodePlayer.GetAttributeText("login", "");
					declare MatchServerLogin = NodePlayer.GetAttributeText("server", "");
					declare MatchId = NodePlayer.GetAttributeText("matchid", "");
					declare MatchScores = NodePlayer.GetAttributeText("scores", "");
					if (Login != "" && MatchServerLogin != "") {
						declare Player <=> SM::GetPlayer(Login);
						
						// @specstart valid
						//if (Player != Null && !Player.RequestsSpectate && !CantBeSubstitute.exists(Player.Id)) {
						if (Player != Null && Private_Lobby_IsReady(Player.User) && !CantBeSubstitute.exists(Player.Id)) {
						// @specend
							declare Lobby_MatchId for Player.User = "";
							declare Lobby_MatchServer for Player.User = "";
							declare Lobby_MatchScores for Player.User = "";
							declare Lobby_IsSubstitute for Player.User = False;
							Lobby_MatchId = MatchId;
							Lobby_MatchServer = MatchServerLogin;
							Lobby_MatchScores = MatchScores;
							Lobby_IsSubstitute = True;
							SubstitutePlayersNb += 1;
							
							Private_MM_ShowVersusML(Player);
						}
					}
				}
			}
			
			// Determine average LP amount on the lobby
			declare AverageLP = 0.;
			declare AverageLPTotal = 0.;
			declare AverageLPCount = 0.;
			foreach (Player in AllPlayers) {
				if (Player.User == Null) continue;
				AverageLPTotal += Player.User.LadderPoints;
				AverageLPCount += 1.;
			}
			if (AverageLPCount > 0.) AverageLP = AverageLPTotal / AverageLPCount;
			
			// Update the number of players on the lobby
			declare TotalPlayers = Net_Lobby_PlayersNb + AllPlayers.count;
			Admin_SetLobbyInfo(True, TotalPlayers, 255, AverageLP);
			// Pre-add the selected players in the "in match" count
			Net_Lobby_PlayersNb += MatchPlayersNb + SubstitutePlayersNb;
			
			declare NodeMasters <=> XmlDoc.Root.GetFirstChild("masters");
			if (NodeMasters != Null) {
				declare Masters = Text[];
				foreach (NodeMaster in NodeMasters.Children) {
					declare Login = NodeMaster.GetAttributeText("login", "");
					if (Login != "") Masters.add(Login);
				}
				Private_MM_ClearMasters();
				Private_MM_AddMasters(Masters);
			}
			
			declare NodePenalties <=> XmlDoc.Root.GetFirstChild("penalties");
			if (NodePenalties != Null) {
				declare Penalties = Text[];
				foreach (NodePenalty in NodePenalties.Children) {
					declare Login = NodePenalty.GetAttributeText("login", "");
					declare Penalty = NodePenalty.GetAttributeInteger("penalty", -1);
					if (Penalty >= 0 && Login != "") {
						Penalties.add(Login);
						declare UserToPenalize = SM::GetUser(Login);
						MM_PenalizePlayer(UserToPenalize, Penalty);
					}
				}
				
				foreach (User in Users) {
					// Cancel penalties
					if (MM_GetPlayerPenalty(User) >= 0) {
						if (!Penalties.exists(User.Login)) {
							MM_CancelPlayerPenalty(User);
						}
					}
				}
			}
		}
	}
	
	if (S_MatchmakingLogAPIDebug) MB_Log("[API] Match to execute: "^MatchesLogins);
	
	// Send match to player UI
	declare netwrite Boolean[Text] Net_Lobby_VersusAllied for Teams[0];
	declare netwrite Integer Net_Lobby_VersusAlliedUpdate for Teams[0];
	Net_Lobby_VersusAllied.clear();
	foreach (MatchId => MatchPlayers in MatchesPlayers) {
		foreach (Player => Clan in MatchPlayers) {
			declare UI <=> UIManager.GetUI(Player);
			if (UI == Null) continue;
			
			if (Player != Null) {
				declare Integer[Ident] Lobby_Allies for Player.User;
				foreach (AllyId => AllyStatus in Lobby_Allies) {
					if (AllyStatus != C_AllyStatus_Validated) continue;
					if (MatchesUsersIds[MatchId][Clan].exists(AllyId)) Net_Lobby_VersusAllied[Player.Login] = True;
				}
			}
			
			declare netwrite Integer Net_Lobby_VersusPlayersUpdate for UI;
			declare netwrite Integer[Text] Net_Lobby_VersusPlayers for UI;
			declare netwrite Text Net_Lobby_MatchId for UI;
			Net_Lobby_VersusPlayers = MatchesLogins[MatchId];
			Net_Lobby_MatchId = MatchId;
			Net_Lobby_VersusPlayersUpdate = Now;
			
			Private_MM_ShowVersusML(Player);
		}
	}
	Net_Lobby_VersusAlliedUpdate = Now;
	
	// Player was ready before sending the players list to the API but unready when receiving the response
	foreach (UserId in MatchesCancelers) {
		if (Users.existskey(UserId)) {
			MM_CancelMatch(Users[UserId]);
		}
	}
}

// ---------------------------------- //
/// Send all players in their matches
Void MM_SendToMatches() {
	declare Matches = Text[][Text];
	foreach (Player in AllPlayers) {
		declare Lobby_MatchServer for Player.User = "";
		declare Lobby_MatchId for Player.User = "";
		declare Lobby_IsSubstitute for Player.User = False;
		if (Lobby_MatchServer != "") {
			MM_SendToServer(Player, Lobby_MatchServer);
		}
		
		if (Lobby_IsSubstitute) {
			UIManager.UIAll.SendChat(TL::Compose(_("%1$<%2$> joined his match as a substitute."), C_MessagePrefix, Player.Name));
			if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^Player.Login^" > Sent as substitute ("^Lobby_MatchId^") on server : \""^Lobby_MatchServer^"\"");
		} else {
			if (Lobby_MatchId != "") {
				if (!Matches.existskey(Lobby_MatchId)) Matches[Lobby_MatchId] = Text[];
				Matches[Lobby_MatchId].add(Player.Name);
			}
			if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] "^Player.Login^" > Sent to match ("^Lobby_MatchId^") on server : \""^Lobby_MatchServer^"\"");
		}
	}
	
	foreach (MatchId => MatchPlayers in Matches) {
		declare FirstPlayer = True;
		declare PlayersNames = "";
		
		foreach (PlayerName in MatchPlayers) {
			if (FirstPlayer) FirstPlayer = False;
			else PlayersNames ^= " &";
			PlayersNames ^= " $<"^PlayerName^"$>";
		}
		
		UIManager.UIAll.SendChat(TL::Compose(_("%1$<%2$> join a match."), C_MessagePrefix, PlayersNames));
	}
	
	foreach (User in Users) {
		declare Lobby_MatchId for User = "";
		declare Lobby_MatchServer for User = "";
		declare Lobby_IsSubstitute for User = False;
		Lobby_MatchId = "";
		Lobby_MatchServer = "";
		Lobby_IsSubstitute = False;
	}
}

// ---------------------------------- //
/// Start the MatchMaker
Void MM_MatchMakerStart() {
	---MatchMakerStart---
}

// ---------------------------------- //
/// Run the MatchMaker
Void MM_MatchMakerRun() {
	---MatchMakerRun---
}

// ---------------------------------- //
/// Stop the MatchMaker
Void MM_MatchMakerStop() {
	---MatchMakerStop---
}

// ---------------------------------- //
/** Check if a player is allowed to play by the matchmaking
 *
 *	@param	_Player		The player to check
 *
 *	@return			True if the player is allowed to play, false otherwise
 */
Boolean MM_PlayerIsValid(CSmPlayer _Player) {
	if (_Player == Null) return False;
	
	declare Matchmaking_PlayerStatus for _Player = C_PlayerStatus_Waiting;
	return (Matchmaking_PlayerStatus == C_PlayerStatus_Valid);
}

// ---------------------------------- //
/// Send the server status to the API
Void Private_MM_PostMatchStatus() {
	if (Http.SlotsAvailable > 0) {
		declare MissingPlayers = "";
		if ((G_MMMatch_Status == C_MatchStatus_Substitute || G_MMMatch_Status == C_MatchStatus_Playing) && G_MMMatch_MissingPlayers.count > 0) {
			foreach (Login => MissingInfo in G_MMMatch_MissingPlayers) {
				// Skip players that are missing since less than 90 seconds
				if (MissingInfo[C_MissingInfo_Since] + C_MissingPlayerGracePeriod > Now) continue;
				
				if (MissingPlayers != "") MissingPlayers ^= ",";
				
				declare PlayerKicked = "false";
				if (MissingInfo[C_MissingInfo_Kicked] != 0) PlayerKicked = "true";
				
				declare Clan = MissingInfo[C_MissingInfo_Clan];
				if (G_Matchmaking_CurrentFormat.count > 1) Clan -= 1;
				
				MissingPlayers ^= """
{
	"login":"{{{TL::MLEncode(Login)}}}",
	"kicked": {{{PlayerKicked}}},
	"clan": {{{Clan}}}
}""";
			}
		}
		declare MatchId = -1;
		if (G_MMMatch_Id != "") MatchId = TL::ToInteger(G_MMMatch_Id);
		declare MatchScores = "";
		foreach (MatchScore in G_MMMatch_Scores) {
			if (MatchScores != "") MatchScores ^= ",";
			MatchScores ^= MatchScore;
		}
		declare PostData = """
{
	"serverlogin": "{{{TL::MLEncode(ServerLogin)}}}",
	"status": {{{G_MMMatch_Status}}},
	"matchid": {{{MatchId}}},
	"scores": [{{{MatchScores}}}],
	"missingplayers": [{{{MissingPlayers}}}]
}""";
		declare Request <=> Http.CreatePost(Private_MM_BuildAPIUrl("/match-server/live"), PostData);
		if (Request != Null) {
			G_Matchmaking_RequestsIds[Request.Id] = C_Request_PostStatus;
		}
		
		if (S_MatchmakingLogAPIDebug) MB_Log("[API] Request: POST /match-server/live\n<!--\n"^PostData^"\n-->");
	}
}

// ---------------------------------- //
/** Ping the API
 *
 *	@param	_Forced		Skip the time interval and force the ping
 */
Void Private_MM_Ping(Boolean _Forced) {
	if (Now >= G_MMMatch_NextPing || _Forced) {
		G_MMMatch_NextPing = Now + C_Match_PingInterval + ML::Rand(-C_RequestRandomDeviation, C_RequestRandomDeviation);
		
		if (G_MMMatch_Status == C_MatchStatus_Playing || G_MMMatch_Status == C_MatchStatus_Substitute) {
			declare NeedRestart = (UseClans && (ClansNbPlayers[1] <= 0 || ClansNbPlayers[2] <= 0)) || (Players.count <= 1);
			
			if (G_MMMatch_EmptySince <= 0 && NeedRestart) {
				G_MMMatch_EmptySince = Now;
			} else if (G_MMMatch_EmptySince > 0 && !NeedRestart) {
				G_MMMatch_EmptySince = -1;
			} else if (G_MMMatch_EmptySince > 0 && Now >= G_MMMatch_EmptySince + C_Match_EmptyTimeBeforeRestart) {
				MM_RestartMatchmaking = True;
				MatchEndRequested = True;
				if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] Server doesn't have enough players since "^(Now - G_MMMatch_EmptySince)^"ms. Restart matchmaking.");
			}
		}
		
		Private_MM_PostMatchStatus();
	}
}

// ---------------------------------- //
/** Change the match status
 *
 *	@param	_MatchStatus		The new match status
 */
Void Private_MM_SetMatchStatus(Integer _MatchStatus) {
	G_MMMatch_Status = _MatchStatus;
	Private_MM_Ping(True);
}

// ---------------------------------- //
/// Send the get match request to the API
Void Private_MM_GetMatch() {
	if (!G_Matchmaking_RequestsIds.exists(C_Request_GetMatches)) {
		declare Request <=> Http.CreateGet(Private_MM_BuildAPIUrl("/match-server/match?serverlogin="^ServerLogin), False);
		if (Request != Null) {
			G_Matchmaking_RequestsIds[Request.Id] = C_Request_GetMatches;
		}
		
		if (S_MatchmakingLogAPIDebug) MB_Log("[API] Request: Get /match-server/match?serverlogin="^ServerLogin);
	}
}

// ---------------------------------- //
/** Allow or not the mode to request substitutes
 *
 *	@param	_AllowSubstitutes		True to allow, false otherwise
 */
Void MM_AllowSubstitutes(Boolean _AllowSubstitutes) {
	if (G_MMMatch_AllowSubstitutes == _AllowSubstitutes) return;
	
	G_MMMatch_AllowSubstitutes = _AllowSubstitutes;
	
	if (G_MMMatch_Status == C_MatchStatus_Substitute) {
		Private_MM_SetMatchStatus(C_MatchStatus_Playing);
	}
}

// ---------------------------------- //
/** Create a ghost player
 *
 *	@param	_Clan	The clan of the ghost player
 *
 *	@return			The login of th eghost player
 */
Text Private_MM_CreateGhostPlayer(Integer _Clan) {
	declare GhostName = "*player"^G_MMMatch_GhostPlayersIndex^"*";
	G_MMMatch_GhostPlayers[GhostName] = _Clan;
	G_MMMatch_GhostPlayersIndex += 1;
	return GhostName;
}

// ---------------------------------- //
/// Manage players authorization
Void Private_MM_ManagePlayers() {
	declare PresentPlayers = Real[Text];
	declare ToKick = CUser[];
	
	foreach (Player in AllPlayers) {
		declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
		
		// Change the status of the player
		if (Matchmaking_PlayerStatus == C_PlayerStatus_Waiting) {
			Private_MM_GetMatch();
			SetPlayerClan(Player, 0);
		}
		
		// Kick the invalid players
		if (Matchmaking_PlayerStatus == C_PlayerStatus_Invalid) {
			if (!Player.User.IsFakeUser) {
				ToKick.add(Player.User);
			} else {
				declare UI <=> UIManager.GetUI(Player);
				if (UI != Null && !UI.ForceSpectator) {
					UI.ForceSpectator = True;
				}
				Users_RequestSwitchToSpectator(Player.User);
			}
		} 
		// Remove valid players from the missing list
		else if (Matchmaking_PlayerStatus == C_PlayerStatus_Valid) {
			if (G_MMMatch_MissingPlayers.existskey(Player.Login)) {
				declare Removed = G_MMMatch_MissingPlayers.removekey(Player.Login);
				G_MMMatch_NewMissingPlayers = True;
			}
		}
		
		// Count players in each clan
		if (G_MMMatch_Players.existskey(Player.Login)) {
			PresentPlayers[Player.Login] = Player.User.LadderPoints;
		}
	}
	
	foreach (User in ToKick) {
		declare Kicked = Admin_KickUser(User, _("This matchmaking server is hosting a match. You can't join at the moment."));
	}
	
	// Search missing players only if substitutes are allowed
	//if (G_MMMatch_AllowSubstitutes) {
		foreach (Login => PlayerInfo in G_MMMatch_Players) {
			// Missing
			if (!PresentPlayers.existskey(Login)) {
				// New
				if (!G_MMMatch_MissingPlayers.existskey(Login)) {
					// Kicked from match?
					declare Kicked = 0;
					if (G_MMMatch_KickedPlayers.exists(Login)) {
						declare Removed = G_MMMatch_KickedPlayers.remove(Login);
						Kicked = 1;
					}
					declare PlayerClan = PlayerInfo[C_PlayerInfo_Clan];
					G_MMMatch_MissingPlayers[Login] = [C_MissingInfo_Clan => PlayerClan, C_MissingInfo_Kicked => Kicked, C_MissingInfo_Since => Now];
					G_MMMatch_NewMissingPlayers = True;
				}
			}
		}
	//}
	
	declare MissingPlayersCount = 0;
	foreach (Login => MissingInfo in G_MMMatch_MissingPlayers) {
		if (MissingInfo[C_MissingInfo_Since] + C_MissingPlayerGracePeriod <= Now) MissingPlayersCount += 1;
	}
	
	// Change match status
	declare ApiInformed = False;
	if (MissingPlayersCount <= 0 && G_MMMatch_Status == C_MatchStatus_Substitute) {
		Private_MM_SetMatchStatus(C_MatchStatus_Playing);
		ApiInformed = True;
	} else if (MissingPlayersCount > 0 && G_MMMatch_Status == C_MatchStatus_Playing && G_MMMatch_AllowSubstitutes) {
		Private_MM_SetMatchStatus(C_MatchStatus_Substitute);
		ApiInformed = True;
	}
	
	// Inform the API of the missing players
	if (G_MMMatch_NewMissingPlayers && (G_MMMatch_Status == C_MatchStatus_Playing || G_MMMatch_Status == C_MatchStatus_Substitute)) {
		if (!ApiInformed) Private_MM_Ping(True);
		G_MMMatch_NewMissingPlayers = False;
	}
}

// ---------------------------------- //
/** Parse the match request response and
 *	setup a match when necessary
 *
 *	@param	_MatchXml	Xml containing the match settings
 */
Void Private_MM_SetupMatch(Text _MatchXml) {
	if (S_MatchmakingLogAPIDebug) MB_Log("[API] Response: GET /match-server/match?serverlogin="^ServerLogin^"\n<!--\n"^_MatchXml^"\n-->");
	if (_MatchXml != "") {
		declare XmlDoc <=> Xml.Create(_MatchXml);
		if (XmlDoc != Null && XmlDoc.Root != Null) {
			declare NodeMatch <=> XmlDoc.Root.GetFirstChild("match");
			
			if (NodeMatch != Null) {
				declare NodeServer <=> NodeMatch.GetFirstChild("server");
				declare NodeFormat <=> NodeMatch.GetFirstChild("format");
				declare NodePlayers <=> NodeMatch.GetFirstChild("players");
				declare MatchId = "";
				declare MatchPlayers = Integer[][Text];
				declare MatchLobbyLogin = "";
				
				// Setup the maximum number of ghost players we might need
				declare GhostPlayersCount = Integer[Integer];
				if (MM_MatchmakingIsProgressive()) {
					foreach (Key => PlayersNb in G_Matchmaking_Format) {
						declare Clan = Key;
						if (G_Matchmaking_Format.count > 1) Clan += 1;
						GhostPlayersCount[Clan] = PlayersNb;
					}
				}
				
				MatchId = NodeMatch.GetAttributeText("id", "");
				
				if (NodeServer != Null) {
					MatchLobbyLogin = NodeServer.GetAttributeText("lobby", "");
				}
				
				if (NodeFormat != Null) {
					declare Format = Integer[];
					foreach (NodeClan in NodeFormat.Children) {
						declare PlayersNb = NodeClan.GetAttributeInteger("number", -1);
						if (PlayersNb > 0) Format.add(PlayersNb);
					}
					G_Matchmaking_CurrentFormat = Format;
				}
				
				if (NodePlayers != Null) {
					foreach (NodePlayer in NodePlayers.Children) {
						declare Login = NodePlayer.GetAttributeText("login", "");
						declare Clan = NodePlayer.GetAttributeInteger("clan", -1);
						declare Order = NodePlayer.GetAttributeInteger("order", -1);
						declare Replaced = NodePlayer.GetAttributeBoolean("replaced", False);
						
						if (G_Matchmaking_CurrentFormat.count > 1) Clan += 1;
						if (Login != "" && Clan >= 0) G_MMMatch_Clans[Login] = Clan;
						
						if (Replaced) {
							declare Removed = G_MMMatch_MissingPlayers.removekey(Login);
							Removed = G_MMMatch_GhostPlayers.removekey(Login);
						} else {
							if (Login != "" && Clan >= 0) {
								MatchPlayers[Login] = [Clan, Order];
								if (MM_MatchmakingIsProgressive() && GhostPlayersCount.existskey(Clan)) {
									GhostPlayersCount[Clan] -= 1;
								}
							}
						}
					}
				}
				
				// Match is valid, start preparation
				if (MatchId != "" && MatchPlayers.count > 0 && MatchLobbyLogin != "") {
					G_MMMatch_Id = MatchId;
					G_MMMatch_Players = MatchPlayers;
					G_MMMatch_LobbyLogin = MatchLobbyLogin;
					
					Private_MM_SetMatchStatus(C_MatchStatus_Starting);
				}
				
				// Complete the match with ghost players if necessary
				if (MM_MatchmakingIsProgressive()) {
					foreach (Clan => PlayersNb in GhostPlayersCount) {
						for (I, 1, PlayersNb) {
							declare GhostLogin = Private_MM_CreateGhostPlayer(Clan);
							G_MMMatch_Players[GhostLogin] = [Clan, -1];
							G_MMMatch_Clans[GhostLogin] = Clan;
						}
					}
				}
			}
		}
	}
	
	// Kick all players from the server if there's no match starting
	if (G_MMMatch_Status != C_MatchStatus_Starting) {
		foreach (Player in AllPlayers) {
			declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
			Matchmaking_PlayerStatus = C_PlayerStatus_Invalid;
		}
	}
	// Validate players if the match is starting
	else {
		foreach (Player in AllPlayers) {
			declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
			if (G_MMMatch_Players.existskey(Player.Login)) {
				Matchmaking_PlayerStatus = C_PlayerStatus_Valid;
			} else {
				Matchmaking_PlayerStatus = C_PlayerStatus_Invalid;
			}
		}
	}
}

// ---------------------------------- //
/** Parse the match request response and
 *	setup the substitutes if necessary
 *
 *	@param	_MatchXml	Xml containing the match settings
 */
Void Private_MM_SetupSubstitute(Text _MatchXml) {
	if (S_MatchmakingLogAPIDebug) MB_Log("[API] Response: GET /match-server/match?serverlogin="^ServerLogin^"\n<!--\n"^_MatchXml^"\n-->");
	if (_MatchXml != "") {
		declare XmlDoc <=> Xml.Create(_MatchXml);
		if (XmlDoc != Null && XmlDoc.Root != Null) {
			declare NodeMatch <=> XmlDoc.Root.GetFirstChild("match");
			
			if (NodeMatch != Null) {
				declare NodePlayers <=> NodeMatch.GetFirstChild("players");
				declare MatchId = "";
				declare MatchPlayers = Integer[][Text];
				
				MatchId = NodeMatch.GetAttributeText("id", "");
				
				if (MatchId == G_MMMatch_Id) {
					if (NodePlayers != Null) {
						foreach (NodePlayer in NodePlayers.Children) {
							declare Login = NodePlayer.GetAttributeText("login", "");
							declare Clan = NodePlayer.GetAttributeInteger("clan", -1);
							declare Order = NodePlayer.GetAttributeInteger("order", -1);
							declare Replaced = NodePlayer.GetAttributeBoolean("replaced", False);
							
							if (G_Matchmaking_CurrentFormat.count > 1) Clan += 1;
							if (Login != "" && Clan >= 0) G_MMMatch_Clans[Login] = Clan;
						
							if (Replaced) {
								declare Removed = G_MMMatch_MissingPlayers.removekey(Login);
								Removed = G_MMMatch_GhostPlayers.removekey(Login);
							} else {
								if (Login != "" && Clan >= 0) {
									MatchPlayers[Login] = [Clan, Order];
								}
							}
						}
					}
					
					G_MMMatch_Players = MatchPlayers;
					
					if (MM_MatchmakingIsProgressive()) {
						foreach (GhostLogin => GhostClan in G_MMMatch_GhostPlayers) {
							G_MMMatch_Players[GhostLogin] = [GhostClan, -1];
							G_MMMatch_Clans[GhostLogin] = GhostClan;
						}
					}
				}
			}
		}
	}
	
	// Set invalid players
	foreach (Player in AllPlayers) {
		declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
		if (G_MMMatch_Players.existskey(Player.Login)) {
			Matchmaking_PlayerStatus = C_PlayerStatus_Valid;
		} else {
			Matchmaking_PlayerStatus = C_PlayerStatus_Invalid;
		}
	}
}

// ---------------------------------- //
/** Parse the player request response and
 *	setup the player accordingly
 *
 *	@param	_PlayerXml	Xml containing the player info
 */
Void Private_MM_SetupPlayer(Text _PlayerXml) {
	declare LogPlayerLogin = "Log_PlayerNotFound";
	if (_PlayerXml != "") {
		declare XmlDoc <=> Xml.Create(_PlayerXml);
		if (XmlDoc != Null && XmlDoc.Root != Null) {
			declare NodePlayer <=> XmlDoc.Root.GetFirstChild("player");
			if (NodePlayer != Null) {
				declare Login = NodePlayer.GetAttributeText("login", "");
				declare Penalty = NodePlayer.GetAttributeInteger("penalty", -1);
				declare Player <=> SM::GetPlayer(Login);
				LogPlayerLogin = Login;
				
				if (Player != Null) {
					MM_PenalizePlayer(Player.User, Penalty);
				}
				
				// If the player was in match, send in back to his server
				declare NodeServer <=> NodePlayer.GetFirstChild("server");
				if (NodeServer != Null) {
					declare MatchServerLogin = NodeServer.GetAttributeText("match", "");
					declare Replaced = NodeServer.GetAttributeBoolean("replaced", False);
					declare MatchId = NodeServer.GetAttributeText("matchid", "");
					if (Player != Null) {
						declare Lobby_LastPenaltyMatchId for Player.User = "";
						declare Lobby_ReconnectToMatchId for Player = "";
						Lobby_ReconnectToMatchId = MatchId;
						
						if (Replaced) {
							if (MatchId != Lobby_LastPenaltyMatchId && MM_GetPlayerPenalty(Player.User) < 0) {
								Lobby_LastPenaltyMatchId = MatchId;
							}
						} else {
							if (MatchId != Lobby_LastPenaltyMatchId && MM_GetPlayerPenalty(Player.User) < 0 && MatchServerLogin != "") {
								declare Lobby_ReconnectToServer for Player = "";
								Lobby_ReconnectToServer = MatchServerLogin;
							}
						}
					}
				}
			}
		}
	}
	if (S_MatchmakingLogAPIDebug) MB_Log("[API] Response: GET /lobby-server/player-connection?login="^TL::URLEncode(LogPlayerLogin)^"&lobbylogin="^TL::URLEncode(ServerLogin)^"\n<!--\n"^_PlayerXml^"\n-->");
}

// ---------------------------------- //
/// Manage the HTTP response from the API
Void Private_MM_ManageHttpResponses() {
	declare Ident[] ToRemove;
	foreach (Request in Http.Requests) {
		if (!G_Matchmaking_RequestsIds.existskey(Request.Id)) continue;
		
		if (Request.IsCompleted) {
			ToRemove.add(Request.Id);
		}
	}
	
	// Destroy the completed requests
	foreach (RequestId in ToRemove) {
		if (!Http.Requests.existskey(RequestId)) continue;
		declare Request <=> Http.Requests[RequestId];
		
		// Success
		if (Request.StatusCode == 200) {
			declare RequestType = G_Matchmaking_RequestsIds[Request.Id];
			switch (RequestType) {
				case C_Request_GetPlayers: {
					Private_MM_SetupPlayer(Request.Result);
				}
				case C_Request_GetMatches: {
					if (G_MMMatch_Status == C_MatchStatus_Waiting) {
						Private_MM_SetupMatch(Request.Result);
					} else {
						Private_MM_SetupSubstitute(Request.Result);
					}
				}
				default: {
					if (S_MatchmakingLogAPIDebug) MB_Log("[API] Response: "^Request.Url^"\nStatus : "^Request.StatusCode^"\nResult : "^Request.Result);
				}
			}
		}
		// Fail
		else {
			if (S_MatchmakingLogAPIError) {
				if (Request.StatusCode == 401) {
					MB_Log("[ERROR] Matchmaking HTTP API Error 401. Waiting for your server to register on Nadeo master server.");
				} else if (Request.StatusCode == 404) {
					MB_Log("[ERROR] Matchmaking HTTP API Error 404. Maybe the URL in the setting is wrong.");
				} else {
					MB_Log("[ERROR] Matchmaking HTTP Error "^Request.StatusCode^".");
				}
			}
			if (S_MatchmakingErrorMessage != "") UIManager.UIAll.SendChat(TL::Compose("%1 %2", C_MessagePrefix, S_MatchmakingErrorMessage));
		}
			
		declare Removed = G_Matchmaking_RequestsIds.removekey(RequestId);
		Http.Destroy(Http.Requests[RequestId]);
	}
}

// ---------------------------------- //
/** Change rooms UI view
 *
 *	@param	_User		The user to update
 *	@param	_View		The view to display
 */
Void Private_MM_SetMLRoomsView(CUser _User, Text _View) {
	if (_User == Null) return;
	declare UI <=> UIManager.GetUI(_User);
	if (UI == Null) return;
	
	declare netwrite Net_Lobby_RoomsViews for UI = "Home";
	Net_Lobby_RoomsViews = _View;
}

// ---------------------------------- //
/** Get the room in which the player is
 *
 *	@param	_Player		The player to get
 *
 *	@return		The room of the player
 */
Integer[][Ident] Private_MM_GetRoom(CSmPlayer _Player) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	
	foreach (Room in Lobby_Rooms) {
		if (Room.existskey(_Player.User.Id)) {
			if (Room[_Player.User.Id][C_AllyInfo_Status] == C_AllyStatus_Validated) return Room;
		}
	}
	
	return Integer[][Ident];
}

// ---------------------------------- //
/** Get the room id in which the player is
 *
 *	@param	_Player		The player to get
 *
 *	@return		The room id of the player if found, -1 otherwise
 */
Integer Private_MM_GetRoomId(CSmPlayer _Player) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	
	foreach (Id => Room in Lobby_Rooms) {
		if (Room.existskey(_Player.User.Id)) {
			if (Room[_Player.User.Id][C_AllyInfo_Status] == C_AllyStatus_Validated) return Id;
		}
	}
	
	return -1;
}

// ---------------------------------- //
/** Find a free slot in the room
 *
 *	@param	_Id		Id of the room to scan
 *
 *	@return			An array with the clan and free slot numbers [Clan, Slot]
 */
Integer[] Private_MM_GetFreeSlot(Integer _Id) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	if (_Id >= 0 && Lobby_Rooms.existskey(_Id)) {
		declare OccupiedSlots = Integer[][Integer];
		declare Room = Lobby_Rooms[_Id];
		
		foreach (AllyId => AllyInfo in Room) {
			declare Clan = AllyInfo[C_AllyInfo_Clan];
			declare Slot = AllyInfo[C_AllyInfo_Slot];
			if (!OccupiedSlots.existskey(Clan)) OccupiedSlots[Clan] = Integer[];
			OccupiedSlots[Clan].add(Slot);
		}
		
		foreach (Clan => PlayersNb in G_Matchmaking_CurrentFormat) {
			if (!OccupiedSlots.existskey(Clan)) {
				return [Clan, C_Lobby_DefaultSlot];
			} else {
				for (I, 0, PlayersNb-1) {
					if (!OccupiedSlots[Clan].exists(I)) {
						return [Clan, I];
					}
				}
			}
		}
	}
	
	return Integer[];
}

// ---------------------------------- //
/** Check if all the allies of a player are ready
 *
 *	@param	_Player		The player to check
 *
 */
Void Private_MM_AlliesAreReady(CSmPlayer _Player) {
	if (_Player == Null) return;
	
	declare Integer[Ident] Allies;
	declare CPlayer[] PlayersToUpdate;
	declare Ready = True;
	
	if (MM_IsUniversalServer()) {
		declare RoomId = Private_MM_GetRoomId(_Player);
		declare FreeSlot = Private_MM_GetFreeSlot(RoomId);
		if (FreeSlot.count > 0) Ready = False;
		
		declare Room = Private_MM_GetRoom(_Player);
		declare Count = 0;
		foreach (AllyId => AllyInfo in Room) {
			declare AllyStatus = AllyInfo[C_AllyInfo_Status];
			Allies[AllyId] = AllyStatus;
			if (AllyStatus == C_AllyStatus_Validated && Users.existskey(AllyId)) {
				Count += 1;
			}
		}
		if (Count < G_Matchmaking_MaxPlayers) Ready = False;
	} else {
		declare Integer[Ident] Lobby_Allies for _Player.User;
		Allies = Lobby_Allies;
		
		// Am i ready ?
		// @specstart valid
		//if (_Player.User.RequestsSpectate) Ready = False;
		if (!Private_Lobby_IsReady(_Player.User)) Ready = False;
		// @specend
		PlayersToUpdate.add(_Player);
	}
	
	foreach (AllyId => AllyStatus in Allies) {
		if (AllyStatus == C_AllyStatus_Validated && Users.existskey(AllyId)) {
			declare User <=> Users[AllyId];
			declare Player <=> SM::GetPlayer(User.Login);
			if (Player != Null) PlayersToUpdate.add(Player);
			// @specstart valid
			//if (User.RequestsSpectate) Ready = False;
			if (!Private_Lobby_IsReady(User)) Ready = False;
			// @specend
		}
	}
	
	// Set ready state
	foreach (Player in PlayersToUpdate) {
		declare Lobby_RoomIsReady for Player.User = False;
		declare Lobby_AlliesAreReady for Player = False;
		Lobby_AlliesAreReady = Ready;
		Lobby_RoomIsReady = Ready;
			
		declare netwrite Net_Lobby_AlliesAreReady for Player = False;
		Net_Lobby_AlliesAreReady = Ready;
	}
}

// ---------------------------------- //
/** Request info about a connecting player
 *
 *	@param	_Player		The player who joined the server
 */
Void Private_Lobby_GetPlayerInfo(CSmPlayer _Player) {
	if (_Player == Null || Http.SlotsAvailable <= 0) return;
	// Send the request to the API
	declare Request <=> Http.CreateGet(Private_MM_BuildAPIUrl("/lobby-server/player-connection?login="^TL::URLEncode(_Player.Login))^"&lobbylogin="^TL::URLEncode(ServerLogin), False);
	if (Request != Null) G_Matchmaking_RequestsIds[Request.Id] = C_Request_GetPlayers;
	
	if (S_MatchmakingLogAPIDebug) MB_Log("[API] Request: GET /lobby-server/player-connection?login="^TL::URLEncode(_Player.Login)^"&lobbylogin="^TL::URLEncode(ServerLogin));
}

// ---------------------------------- //
/// Find the allies of each player
Void Private_MM_ComputeAllies() {
	// Mark connected user
	foreach (User in Users) {
		declare Lobby_IsConnected for User = False;
		declare Lobby_RoomLogins for User = Integer[][Text];
		Lobby_IsConnected = False;
		Lobby_RoomLogins.clear();
	}
	foreach (Player in AllPlayers) {
		declare Lobby_IsConnected for Player.User = False;
		Lobby_IsConnected = True;
	}
	
	if (MM_IsUniversalServer()) {
		declare Integer[][Ident][Integer] Lobby_Rooms for This;
		declare Integer[][Ident][Integer] NewRooms;
		
		foreach (Id => Room in Lobby_Rooms) {
			declare NewRoom = Integer[Ident];
			
			foreach (AllyId => AllyInfo in Room) {
				if (Users.existskey(AllyId)) {
					declare AllyStatus = AllyInfo[C_AllyInfo_Status];
					declare Ally <=> Users[AllyId];
					declare Lobby_IsConnected for Ally = False;
					declare Lobby_TmpClan for Ally = -1;
					declare Lobby_TmpSlot for Ally = -1;
					Lobby_TmpClan = AllyInfo[C_AllyInfo_Clan];
					Lobby_TmpSlot = AllyInfo[C_AllyInfo_Slot];
					
					switch (AllyStatus) {
						case C_AllyStatus_Validated: {
							if (!Lobby_IsConnected) {
								NewRoom[AllyId] = C_AllyStatus_Disconnected;
							} else {
								NewRoom[AllyId] = C_AllyStatus_Validated;
								Private_MM_SetMLRoomsView(Ally, "Room");
							}
						}
						case C_AllyStatus_Sent: {
							if (Lobby_IsConnected) {
								NewRoom[AllyId] = C_AllyStatus_Sent;
							}
						}
						case C_AllyStatus_Disconnected: {
							if (Lobby_IsConnected) {
								NewRoom[AllyId] = C_AllyStatus_Validated;
								Private_MM_SetMLRoomsView(Ally, "Room");
							} else {
								NewRoom[AllyId] = C_AllyStatus_Disconnected;
							}
						}
					}
				}
			}
			
			if (NewRoom.exists(C_AllyStatus_Validated) || NewRoom.exists(C_AllyStatus_Disconnected)) {
				NewRoom = NewRoom.sort();
				
				// Remove allies
				declare RoomLogins = Integer[][Text];
				declare AlliesLogins = Integer[Text];
				declare Count = 0;
				declare ToRemove = Ident[];
				foreach (AllyId => AllyStatus in NewRoom) {
					Count += 1;
					if (Count > G_Matchmaking_MaxPlayers) ToRemove.add(AllyId);
					else if (
						(AllyStatus == C_AllyStatus_Validated || AllyStatus == C_AllyStatus_Sent) 
						&& Users.existskey(AllyId)
					) {
						declare User <=> Users[AllyId];
						declare Lobby_TmpClan for User = -1;
						declare Lobby_TmpSlot for User = -1;
						RoomLogins[User.Login] = [AllyStatus, Lobby_TmpClan, Lobby_TmpSlot];
						AlliesLogins[User.Login] = AllyStatus;
					}
				}
				
				foreach (AllyId in ToRemove) {
					declare Removed = NewRoom.removekey(AllyId);
				}
				
				foreach (AllyId => AllyStatus in NewRoom) {
					declare User <=> Users[AllyId];
					declare Lobby_TmpClan for User = -1;
					declare Lobby_TmpSlot for User = -1;
					
					if (!NewRooms.existskey(Id)) NewRooms[Id] = Integer[][Ident];
					NewRooms[Id][AllyId] = [AllyStatus, Lobby_TmpClan, Lobby_TmpSlot];
					
					if (AllyStatus == C_AllyStatus_Validated) {
						declare Lobby_RoomLogins for User = Integer[][Text];
						declare Lobby_AlliesLogins for User = Integer[Text];
						Lobby_RoomLogins = RoomLogins;
						Lobby_AlliesLogins = AlliesLogins;
					}
				}
			}
		}
		
		// Update rooms
		Lobby_Rooms = NewRooms;
		
		foreach (Player in AllPlayers) {
			// Check if my allies are ready
			Private_MM_AlliesAreReady(Player);
		
			// Send allies logins to UI
			declare UI <=> UIManager.GetUI(Player);
			if (UI != Null) {
				declare Lobby_RoomLogins for Player.User = Integer[][Text];
				declare Lobby_AlliesLogins for Player.User = Integer[Text];
				
				declare netwrite Integer[][Text] Net_Lobby_RoomLogins for UI;
				declare netwrite Integer Net_Lobby_RoomLoginsUpdate for UI;
				Net_Lobby_RoomLogins = Lobby_RoomLogins;
				Net_Lobby_RoomLoginsUpdate = Now;
				
				declare netwrite Integer[Text] Net_Lobby_AlliesLogins for UI;
				declare netwrite Integer Net_Lobby_AlliesLoginsUpdate for UI;
				Net_Lobby_AlliesLogins = Lobby_AlliesLogins;
				Net_Lobby_AlliesLoginsUpdate = Now;
			}
		}
		
		// Update players list
		Private_Lobby_UpdatePlayersList();
	} else {
		foreach (Player in AllPlayers) {
			declare Integer[Ident] Lobby_Allies as MyAllies for Player.User;
			
			// Add global allies to the local allies array
			foreach (AllyLogin in Player.User.AlliesConnected) {
				declare Ally <=> SM::GetUser(AllyLogin);
				if (Ally != Null) {
					MyAllies[Ally.Id] = C_AllyStatus_Validated;
				}
			}
			
			// Update ally status
			declare ToDisconnect = Ident[];
			declare ToValidate = Ident[];
			declare ToRemove = Ident[];
			foreach (AllyId => AllyStatus in MyAllies) {
				if (!Users.existskey(AllyId)) {
					ToRemove.add(AllyId);
				} else {
					declare Ally <=> Users[AllyId];
					declare Lobby_IsConnected for Ally = False;
					declare Integer[Ident] Lobby_Allies as HisAllies for Ally;
					
					switch (AllyStatus) {
						case C_AllyStatus_Validated: {
							if (!Lobby_IsConnected) {
								ToDisconnect.add(AllyId);
							} else if (HisAllies.existskey(Player.User.Id)) {
								HisAllies[Player.User.Id] = C_AllyStatus_Validated;
							} else {
								ToRemove.add(AllyId);
							}
						}
						case C_AllyStatus_Sent: {
							if (!Lobby_IsConnected) {
								ToRemove.add(AllyId);
							} else if (HisAllies.existskey(Player.User.Id)) {
								HisAllies[Player.User.Id] = C_AllyStatus_Validated;
								ToValidate.add(AllyId);
							}
						}
						case C_AllyStatus_Disconnected: {
							if (Lobby_IsConnected) {
								HisAllies[Player.User.Id] = C_AllyStatus_Validated;
								ToValidate.add(AllyId);
							}
						}
					}
				}
			}
			
			foreach (AllyId in ToDisconnect) {
				MyAllies[AllyId] = C_AllyStatus_Disconnected;
			}
			foreach (AllyId in ToValidate) {
				MyAllies[AllyId] = C_AllyStatus_Validated;
			}
			foreach (AllyId in ToRemove) {
				declare Removed = MyAllies.removekey(AllyId);
			}
			
			ToDisconnect.clear();
			ToRemove.clear();
			
			// Sort allies, Validated and Sent first
			MyAllies = MyAllies.sort();
			
			// Remove allies
			declare AlliesCount = 0;
			declare TotalCount = 0;
			declare MaxAllies = G_Matchmaking_MaxPlayers - 1;
			foreach (AllyId => AllyStatus in MyAllies) {
				TotalCount += 1;
				if (AllyStatus == C_AllyStatus_Validated || AllyStatus == C_AllyStatus_Sent) AlliesCount += 1;
				if (TotalCount > MaxAllies) ToRemove.add(AllyId);
			}
			
			declare Boolean Lobby_AlliesFull for Player;
			if (AlliesCount >= MaxAllies) Lobby_AlliesFull = True;
			else Lobby_AlliesFull = False;
			
			foreach (AllyId in ToRemove) {
				declare Removed = MyAllies.removekey(AllyId);
			}
			
			// Check if my allies are ready
			Private_MM_AlliesAreReady(Player);
			
			declare Count = 0;
			declare UI <=> UIManager.GetUI(Player);
			if (UI != Null) {
				declare netwrite Integer[Text] Net_Lobby_AlliesLogins for UI;
				declare netwrite Integer Net_Lobby_AlliesLoginsUpdate for UI;
				Net_Lobby_AlliesLogins.clear();
				foreach (AllyId => AllyStatus in MyAllies) {
					if (
						(AllyStatus == C_AllyStatus_Validated || AllyStatus == C_AllyStatus_Sent)
						&& Users.existskey(AllyId)
					) {
						Net_Lobby_AlliesLogins[Users[AllyId].Login] = AllyStatus;
						Count += 1;
						if (Count >= MaxAllies) break;
					}
				}
				Net_Lobby_AlliesLoginsUpdate = Now;
			}
			
			// Update players list
			Private_Lobby_UpdatePlayersList();
		}
	}
}

// ---------------------------------- //
/** Change rooms UI view
 *
 *	@param	_Player		The player to update
 *	@param	_View		The view to display
 */
Void Private_MM_SetMLRoomsView(CSmPlayer _Player, Text _View) {
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	declare netwrite Net_Lobby_RoomsViews for UI = "Home";
	Net_Lobby_RoomsViews = _View;
}

// ---------------------------------- //
/** Remove a player from all rooms
 *
 *	@param	_Player		The player to remove
 */
Void Private_MM_LeaveRoom(CSmPlayer _Player) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	
	declare ToRemove = Integer[];
	foreach (Id => Room in Lobby_Rooms) {
		if (Room.existskey(_Player.User.Id)) {
			if (Room[_Player.User.Id][C_AllyInfo_Status] == C_AllyStatus_Validated) ToRemove.add(Id);
		}
	}
	
	foreach (Id in ToRemove) {
		declare Removed = Lobby_Rooms[Id].removekey(_Player.User.Id);
	}
	
	Private_MM_SetMLRoomsView(_Player, "Home");
}

// ---------------------------------- //
/** Create a room and add the player
 *
 *	@param	_Player		The player that creates the room
 */
Void Private_MM_CreateRoom(CSmPlayer _Player) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	Lobby_Rooms = Lobby_Rooms.sortkey();
	declare NewKey = 0;
	foreach (Key => Room in Lobby_Rooms) {
		if (Key != NewKey) break;
		NewKey += 1;
	}
	
	Lobby_Rooms[NewKey] = [_Player.User.Id => [C_AllyStatus_Validated, C_Lobby_DefaultClan, C_Lobby_DefaultSlot]];
	
	Private_MM_SetMLRoomsView(_Player, "Room");
}

// ---------------------------------- //
/** Join a room
 *
 *	@param	_Player		The player that joins the room
 *	@param	_Id			The id of the room to join
 */
Void Private_MM_JoinRoom(CSmPlayer _Player, Integer _Id) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	
	if (_Id >= 0 && Lobby_Rooms.existskey(_Id)) {
		declare Room = Lobby_Rooms[_Id];
		if (Room.existskey(_Player.User.Id)) {
			declare AllyInfo = Room[_Player.User.Id];
			Lobby_Rooms[_Id][_Player.User.Id] = [C_AllyStatus_Validated, AllyInfo[C_AllyInfo_Clan], AllyInfo[C_AllyInfo_Slot]];
			Private_MM_SetMLRoomsView(_Player, "Room");
		} else {
			declare FreeSlot = Private_MM_GetFreeSlot(_Id);
			if (FreeSlot.count > 0) {
				Lobby_Rooms[_Id][_Player.User.Id] = [C_AllyStatus_Validated, FreeSlot[0], FreeSlot[1]];
				Private_MM_SetMLRoomsView(_Player, "Room");
			}
		}
	}
}

// ---------------------------------- //
/** Invite a player to a room
 *
 *	@param	_Player		The player to invite
 *	@param	_Id			The id of the room to join
 */
Void Private_MM_InviteRoom(CSmPlayer _Player, Integer _Id) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	
	if (_Id >= 0 && Lobby_Rooms.existskey(_Id)) {
		declare Room = Lobby_Rooms[_Id];
		
		// Remove disconnected players
		if (Room.count >= G_Matchmaking_MaxPlayers) {
			foreach (AllyId => AllyInfo in Room) {
				if (AllyInfo[C_AllyInfo_Status] == C_AllyStatus_Disconnected && Lobby_Rooms[_Id].count >= G_Matchmaking_MaxPlayers) {
					declare Removed = Lobby_Rooms[_Id].removekey(AllyId);
				}
			}
		}
		
		// Enough slots left
		if (Lobby_Rooms[_Id].count < G_Matchmaking_MaxPlayers) {
			declare FreeSlot = Private_MM_GetFreeSlot(_Id);
			if (FreeSlot.count > 0) {
				Lobby_Rooms[_Id][_Player.User.Id] = [C_AllyStatus_Sent, FreeSlot[0], FreeSlot[1]];
			}
		}
	}
}

// ---------------------------------- //
/** Cancel player invitation
 *
 *	@param	_Player		The player to cancel
 *	@param	_Id			The id of the room
 */
Void Private_MM_CancelInviteRoom(CSmPlayer _Player, Integer _Id) {
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	
	if (_Id >= 0 && Lobby_Rooms.existskey(_Id)) {
		if (Lobby_Rooms[_Id].existskey(_Player.User.Id) && Lobby_Rooms[_Id][_Player.User.Id][C_AllyInfo_Status] == C_AllyStatus_Sent) {
			declare Removed = Lobby_Rooms[_Id].removekey(_Player.User.Id);
		}
	}
}

// ---------------------------------- //
/** Switch slot in a room
 *
 *	@param	_Player		The player to switch
 *	@param	_Clan		The new clan
 *	@param	_Slot		The slot to use
 */
Void Private_MM_SwitchSlotRoom(CSmPlayer _Player, Integer _Clan, Integer _Slot) {
	if (_Player == Null || _Player.User == Null) return;
	
	declare RoomId = Private_MM_GetRoomId(_Player);
	if (RoomId < 0) return;
	
	declare Integer[][Ident][Integer] Lobby_Rooms for This;
	declare Room = Lobby_Rooms[RoomId];
	
	declare Ident SlotUserId;
	
	declare PrevClan = -1;
	declare PrevSlot = -1;
	if (Room.existskey(_Player.User.Id)) {
		PrevClan = Room[_Player.User.Id][C_AllyInfo_Clan];
		PrevSlot = Room[_Player.User.Id][C_AllyInfo_Slot];
	}
	
	// New slot occupied ?
	foreach (AllyId => AllyInfo in Room) {
		if (AllyInfo[C_AllyInfo_Clan] == _Clan && AllyInfo[C_AllyInfo_Slot] == _Slot) {
			SlotUserId = AllyId;
			break;
		}
	}
	// Leave old slot
	
	// Give old slot
	if (SlotUserId != NullId && Room.existskey(SlotUserId)) {
		Room[SlotUserId][C_AllyInfo_Clan] = PrevClan;
		Room[SlotUserId][C_AllyInfo_Slot] = PrevSlot;
	}
	// Take new slot
	if (Room.existskey(_Player.User.Id)) {
		Room[_Player.User.Id][C_AllyInfo_Clan] = _Clan;
		Room[_Player.User.Id][C_AllyInfo_Slot] = _Slot;
	}
	
	// Save room
	Lobby_Rooms[RoomId] = Room;
}

// ---------------------------------- //
/** Request an ally
 *
 *	@param	_Player		The player requesting the ally
 *	@param	_Ally		The requested ally
 */
Void Private_MM_RequestAlly(CSmPlayer _Player, CSmPlayer _Ally) {
	if (_Player == Null || _Ally == Null) return;
	if (_Player.Id == _Ally.Id) return;
	
	if (MM_IsUniversalServer()) {
		declare MyRoom = Private_MM_GetRoom(_Player);
		
		// Inviting to a room
		if (MyRoom.count > 0) {
			// Already validted in the room
			if (MyRoom.existskey(_Ally.User.Id) && MyRoom[_Ally.User.Id][C_AllyInfo_Status] == C_AllyStatus_Validated) {
				// Do nothing ...
			}
			// Cancel invite
			else if (MyRoom.existskey(_Ally.User.Id) && MyRoom[_Ally.User.Id][C_AllyInfo_Status] == C_AllyStatus_Sent) {
				Private_MM_CancelInviteRoom(_Ally, Private_MM_GetRoomId(_Player));
			}
			// Send invite if enough slots left
			else {
				Private_MM_InviteRoom(_Ally, Private_MM_GetRoomId(_Player));
			}
		} 
		// Requesting access to a room
		else {
			// Accept an invitation
			declare HisRoom = Private_MM_GetRoom(_Ally);
			if (HisRoom.existskey(_Player.User.Id) && HisRoom[_Player.User.Id][C_AllyInfo_Status] == C_AllyStatus_Sent) {
				Private_MM_JoinRoom(_Player, Private_MM_GetRoomId(_Ally));
			}
		}
	} else {
		declare Integer[Ident] Lobby_Allies as MyAllies for _Player.User;
		declare Integer[Ident] Lobby_Allies as HisAllies for _Ally.User;
		declare Boolean Lobby_AlliesFull for _Player;
		
		// This ally exists in my list
		if (MyAllies.existskey(_Ally.User.Id) || HisAllies.existskey(_Player.User.Id)) {
			// Cancel my ally request
			if (MyAllies.existskey(_Ally.User.Id) && MyAllies[_Ally.User.Id] == C_AllyStatus_Sent) {
				declare Boolean Removed;
				Removed = MyAllies.removekey(_Ally.User.Id);
				Removed = HisAllies.removekey(_Player.User.Id);
			} 
			// Accept his ally request
			else if (HisAllies.existskey(_Player.User.Id) && HisAllies[_Player.User.Id] == C_AllyStatus_Sent) {
				// If I've already filled my allies slot I can't accept the request
				if (!Lobby_AlliesFull) {
					MyAllies[_Ally.User.Id] = C_AllyStatus_Validated;
					HisAllies[_Player.User.Id] = C_AllyStatus_Validated;
				}
			}
			// Cancel an ally agreement
			else if (MyAllies.existskey(_Ally.User.Id) && MyAllies[_Ally.User.Id] == C_AllyStatus_Validated) {
				declare Boolean Removed;
				Removed = MyAllies.removekey(_Ally.User.Id);
				HisAllies[_Player.User.Id] = C_AllyStatus_Sent;
			}
		}
		// This ally doesn't exists in my list
		else {
			// Send the request if I have enough allies slots left
			if (!Lobby_AlliesFull) {
				MyAllies[_Ally.User.Id] = C_AllyStatus_Sent;
			}
		}
	}
	
	Private_MM_ComputeAllies();
}

// ---------------------------------- //
/// Matchmaking play loop
Void Private_MM_PlayLoop() {
	if (MM_IsLobbyServer()) {
		Message::Loop();
		
		declare NeedAlliesUpdate = False;
		foreach (Player in AllPlayers) {
			declare Lobby_IsNew for Player = True;
			// @specstart valid
			//declare Lobby_PrevRequestsSpectate for Player = False;
			declare Lobby_PrevIsReady for Player = True;
			// @specend
			
			// ---------------------------------- //
			// New player on the lobby
			if (Lobby_IsNew) {
				Lobby_IsNew = False;
				// @specstart valid
				// Not ready players are sent in spectator mode
				//Users_RequestSwitchToSpectator(Player.User);
				// Players are not ready by default
				Private_Lobby_SetReady(Player.User, False);
				// @specend
				// Recompute allies when a new player join the lobby
				NeedAlliesUpdate = True;
				// Update players list
				Private_Lobby_UpdatePlayersList();
				// Request info on the player
				Private_Lobby_GetPlayerInfo(Player);
				// Choose rooms screen
				if (MM_IsUniversalServer()) {
					if (Private_MM_GetRoomId(Player) != -1) Private_MM_SetMLRoomsView(Player, "Room");
					else Private_MM_SetMLRoomsView(Player, "Home");
				}
				// Save last matchmaker running time
				declare Lobby_LastMatchmakerTime for Player = Now;
				Lobby_LastMatchmakerTime = Now;
			}
			
			// ---------------------------------- //
			// Player switch between ready and unready
			Private_Lobby_UpdateReady(Player);
			// @specstart valid
			/*if (Lobby_PrevRequestsSpectate != Player.RequestsSpectate) {
				Lobby_PrevRequestsSpectate = Player.RequestsSpectate;*/
			if (Lobby_PrevIsReady != Private_Lobby_IsReady(Player.User)) {
				Lobby_PrevIsReady = Private_Lobby_IsReady(Player.User);
			// @specend
				//Private_Lobby_UpdatePlayersList();
				Private_MM_AlliesAreReady(Player);
				declare Integer Lobby_ReadySince for Player = -1;
				
				// See if we need to cancel a match
				// @specstart valid
				//if (Player.RequestsSpectate) {
				if (!Private_Lobby_IsReady(Player.User)) {
				// @specend
					MM_CancelMatch(Player.User);
					Lobby_ReadySince = -1;
				} else {
					Lobby_ReadySince = Now;
					
					// Can't be ready in universal lobby if you're not in a room
					if (MM_IsUniversalServer()) {
						if (Private_MM_GetRoomId(Player) < 0) {
							// @specstart valid
							//Users_RequestSwitchToSpectator(Player.User);
							Private_Lobby_SetReady(Player.User, False);
							// @specend
						}
					}
				}
			}
			
			// ---------------------------------- //
			// Update the karma of the player
			MM_UpdateKarma(Player.User);
			
			// ---------------------------------- //
			// Try to reconnect to the match server after a leave
			MM_ReconnectToServer(Player);
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CSmModeEvent::EType::OnPlayerRemoved) {
				NeedAlliesUpdate = True;
				Private_Lobby_UpdatePlayersList();
				MM_CancelMatch(Event.User);
			} else if (Event.Type == CSmModeEvent::EType::OnPlayerAdded) {
				NeedAlliesUpdate = True;
				Private_Lobby_UpdatePlayersList();
			}
		}
		
		if (NeedAlliesUpdate) {
			Private_MM_ComputeAllies();
		}
	}
	else if (MM_IsMatchServer()) {
		foreach (Event in PendingEvents) {
			if (Event.Type == CSmModeEvent::EType::OnPlayerRemoved) {
				if (!Event.PlayerWasInLadderMatch) {
					if (!G_MMMatch_KickedPlayers.exists(Event.User.Login)) G_MMMatch_KickedPlayers.add(Event.User.Login);
				}
			}
		}
		Private_MM_Ping(False);
		Private_MM_ManagePlayers();
	}
	
	Private_MM_ManageHttpResponses();
	if (MM_IsMatchmakingServer()) {
		+++MatchmakingYield+++
	}
}

// ---------------------------------- //
/// Matchmaking yield
Void MM_Yield() {
	MB_Yield();
	Private_MM_PlayLoop();
}

// ---------------------------------- //
/** Get the clan selected by the matchmaking for the player
 *	If the server is not in matchmaking mode, return the default requested clan
 *
 *	@param	_Player		The player to check
 *
 *	@return		The matchmaking clan of the player
 */
Integer MM_GetRequestedClan(CSmPlayer _Player) {
	if (_Player == Null) return 0;
	
	if (MM_IsMatchServer()) {
		if (G_MMMatch_Players.existskey(_Player.Login)) {
			return G_MMMatch_Players[_Player.Login][C_PlayerInfo_Clan];
		}
	}
	
	return _Player.RequestedClan;
}

// ---------------------------------- //
/** Get the clan selected by the matchmaking for a login
 *
 *	@param	_Login		The login to check
 *
 *	@return		The matchmaking clan of the login
 */
Integer MM_GetAssignedClan(Text _Login) {
	if (MM_IsMatchServer()) {
		if (G_MMMatch_Clans.existskey(_Login)) {
			return G_MMMatch_Clans[_Login];
		}
	}
	
	return -1;
}

// ---------------------------------- //
/** Get the slot selected by the matchmaking for the player
 *
 *	@param	_Player		The player to check
 *
 *	@return		The matchmaking slot of the player if found, -1 otherwise
 */
Integer MM_GetRequestedSlot(CSmPlayer _Player) {
	if (_Player == Null) return -1;
	
	if (MM_IsMatchServer()) {
		if (G_MMMatch_Players.existskey(_Player.Login)) {
			return G_MMMatch_Players[_Player.Login][C_PlayerInfo_Slot];
		}
	}
	
	return -1;
}

// ---------------------------------- //
/** Get the status of a player
 *	One of these values can be returned
 *	- C_PlayerStatus_Waiting
 *	- C_PlayerStatus_Valid
 *	- C_PlayerStatus_Invalid
 *
 *	@param	_Player		The player to check
 *
 *	@return		The player status
 */
Integer MM_GetPlayerStatus(CSmPlayer _Player) {
	if (_Player == Null) return C_PlayerStatus_Invalid;
	
	declare Matchmaking_PlayerStatus for _Player = C_PlayerStatus_Waiting;
	return Matchmaking_PlayerStatus;
}

// ---------------------------------- //
/// Do a synchronization
Void MM_Synchro_DoBarrier() {
	declare Barrier = Synchro_AddBarrier();
	while (!Synchro_BarrierReached(Barrier) && !ServerShutdownRequested) MM_Yield();
}

// ---------------------------------- //
/** Wait for all players to be ready on the server
 *
 *	@param	_MaxDuration	Maximum duration of the synchro (ms)
 */
Void MM_WaitPlayers(Integer _MaxDuration) {
	StartTime = Now;
	UIManager.UIAll.CountdownEndTime = -1;
	
	MM_Synchro_DoBarrier();
	
	declare ReqPlayersNb = 0;
	foreach (PlayersNb in G_Matchmaking_CurrentFormat) ReqPlayersNb += PlayersNb;
	
	declare netwrite Integer Net_MM_ReadyPlayersNb for Teams[0];
	declare netwrite Integer Net_MM_ReadyPlayersMax for Teams[0];
	Net_MM_ReadyPlayersNb = 0;
	Net_MM_ReadyPlayersMax = ReqPlayersNb;
	
	Layers::Attach("StartingMatch");
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::RollingBackgroundIntro;
	
	while (!ServerShutdownRequested && !MatchEndRequested) {
		MM_Yield();
		
		declare ReadyPlayersNb = 0;
		foreach (Player in AllPlayers) {
			declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
			if (Matchmaking_PlayerStatus == C_PlayerStatus_Valid) {
				ReadyPlayersNb += 1;
			}
		}
		if (Net_MM_ReadyPlayersNb != ReadyPlayersNb) Net_MM_ReadyPlayersNb = ReadyPlayersNb;
		
		if (UIManager.UIAll.CountdownEndTime < 0 && ReadyPlayersNb > 0) {
			UIManager.UIAll.CountdownEndTime = StartTime + _MaxDuration;
		}
		
		+++MatchmakingWaitPlayers+++
		
		if ((UIManager.UIAll.CountdownEndTime >= 0 && Now >= UIManager.UIAll.CountdownEndTime) || ReadyPlayersNb >= ReqPlayersNb) {
			break;
		}
	}
	
	Layers::Detach("StartingMatch");
	StartTime = -1;
	UIManager.UIAll.CountdownEndTime = -1;
}

// ---------------------------------- //
/** Vote to select the next map
 *
 *	@param	_ForceLoadMap		If True, load the selected map after the vote.
 *								If False, only modify the NextMapIndex variable.
 */
Void MM_VoteForNextMap(Boolean _ForceLoadMap) {
	if (!S_MatchmakingVoteForMap) return;
	
	VoteMap::Begin();
	
	while (!VoteMap::CanStop()) {
		MM_Yield();
		VoteMap::Loop();
		
		+++MatchmakingMatchVoteForNextMap+++
	}
	
	VoteMap::End();
	
	if (_ForceLoadMap) {
		declare NextMapInfo <=> VoteMap::GetNextMapInfo();
		if (NextMapInfo != Null && NextMapInfo.Id != Map.MapInfo.Id) {
			MB_UnloadMap();
			MB_LoadMap();
			MM_Synchro_DoBarrier();
		}
	}
}

// ---------------------------------- //
/// Prepare a new match
Void Private_MM_MatchPrepare() {
	StartTime = Now;
	UIManager.UIAll.CountdownEndTime = StartTime + C_Match_PreparationDuration;
	
	declare ReqPlayersNb = 0;
	foreach (PlayersNb in G_Matchmaking_CurrentFormat) ReqPlayersNb += PlayersNb;
	
	declare netwrite Integer Net_MM_ReadyPlayersNb for Teams[0];
	declare netwrite Integer Net_MM_ReadyPlayersMax for Teams[0];
	Net_MM_ReadyPlayersNb = 0;
	Net_MM_ReadyPlayersMax = ReqPlayersNb;
	
	Layers::Attach("StartingMatch");
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::RollingBackgroundIntro;
	
	while (G_MMMatch_Status == C_MatchStatus_Starting && !ServerShutdownRequested && !MatchEndRequested) {
		MM_Yield();
		
		declare ReadyPlayersNb = 0;
		foreach (Player in AllPlayers) {
			declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
			if (Matchmaking_PlayerStatus == C_PlayerStatus_Valid) {
				ReadyPlayersNb += 1;
			}
		}
		if (Net_MM_ReadyPlayersNb != ReadyPlayersNb) Net_MM_ReadyPlayersNb = ReadyPlayersNb;
		
		+++MatchmakingMatchPrepare+++
		
		if (Now >= UIManager.UIAll.CountdownEndTime || ReadyPlayersNb >= ReqPlayersNb) {
			Private_MM_SetMatchStatus(C_MatchStatus_Playing);
		}
	}
	
	G_Matchmaking_CurrentFormat = G_Matchmaking_Format;
	
	Layers::Detach("StartingMatch");
	StartTime = -1;
	UIManager.UIAll.CountdownEndTime = -1;
}

// ---------------------------------- //
/// Wait for a new match
Void MM_MatchWait() {
	G_MMMatch_Players.clear();
	G_MMMatch_Clans.clear();
	G_MMMatch_Id = "";
	G_MMMatch_LobbyLogin = "";
	G_MMMatch_MissingPlayers.clear();
	G_MMMatch_KickedPlayers.clear();
	G_MMMatch_GhostPlayers.clear();
	G_MMMatch_GhostPlayersIndex = 0;
	G_MMMatch_NewMissingPlayers = False;
	G_MMMatch_EmptySince = -1;
	G_MMMatch_Scores.clear();
	
	foreach (Player in AllPlayers) {
		declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
		Matchmaking_PlayerStatus = C_PlayerStatus_Waiting;
	}
	
	Private_MM_SetMatchStatus(C_MatchStatus_Waiting);
	
	// Waiting to get a match
	while (G_MMMatch_Status == C_MatchStatus_Waiting && !ServerShutdownRequested && !MatchEndRequested) {
		MM_Yield();
		
		+++MatchmakingMatchWait+++
	}
	
	// Got a match, now prepare it
	Private_MM_MatchPrepare();
}

// ---------------------------------- //
/** Launch a vote to see if the players want a rematch
 *
 *	@return		True if the players want a rematch, False otherwise
 */
Boolean MM_VoteForRematch() {
	if (S_MatchmakingRematchRatio < 0.) return False;
	
	Layers::Attach("RematchVote");
	Layers::Create("RematchVoteResults");
	Layers::Attach("RematchVoteResults");
	
	declare netwrite Net_MM_RematchVote_SynchroServer for Teams[0] = 10;
	Net_MM_RematchVote_SynchroServer += 1;
	
	declare StopVote = False;
	declare Result = False;
	declare PlayersVotes = Boolean[Ident];
	
	foreach (Player in Players) {
		declare MM_RematchVoted for Player = False;
				
		if (Player.User.IsFakeUser) MM_RematchVoted = True;
		else MM_RematchVoted = False;
	}
	
	declare VoteEndTime = Now + C_Match_RematchVoteDuration;
	declare netwrite Net_MM_RematchVote_EndTime for Teams[0] = 0;
	Net_MM_RematchVote_EndTime = VoteEndTime;
	
	while (!StopVote && Now < VoteEndTime) {
		MM_Yield();
		
		+++MatchmakingMatchVoteForRematch+++
		
		StopVote = True;
		
		foreach (Player in Players) {
			declare UI <=> UIManager.GetUI(Player);
			if (UI != Null) {
				declare MM_RematchVoted for Player = False;
				declare netread Integer Net_MM_RematchVote_Update for UI;
				declare MM_PrevVoteUpdate for Player = -1;
				if (MM_PrevVoteUpdate != Net_MM_RematchVote_Update) {
					MM_PrevVoteUpdate = Net_MM_RematchVote_Update;
					
					declare netread Integer Net_MM_RematchVote_SynchroClient for UI;
					if (Net_MM_RematchVote_SynchroClient == Net_MM_RematchVote_SynchroServer) {
						declare netread Boolean Net_MM_RematchVote_Value for UI;
						PlayersVotes[Player.User.Id] = Net_MM_RematchVote_Value;
						MM_RematchVoted = True;
						Message::SendBigMessage(Player, _("Waiting for other players."), VoteEndTime - Now, 1);
						
						declare Logins = [True => "*", False => "*"];
						foreach (UserId => Vote in PlayersVotes) {
							if (!Users.existskey(UserId)) continue;
							declare User <=> Users[UserId];
							if (Logins[Vote] != "") Logins[Vote] ^= ",";
							Logins[Vote] ^= User.Login;
						}
						Layers::Update("RematchVoteResults", """<frame posn="0 6 10"><playerlist posn="-37 0" scale="0.8" halign="center" substyle="Medium" logins="{{{Logins[True]}}}" lines="50" columns="1" /><playerlist posn="37.5 0" scale="0.8" halign="center" substyle="Medium" logins="{{{Logins[False]}}}" lines="50" columns="1" /></frame>""");
					}
				}
				
				if (!MM_RematchVoted) StopVote = False;
			}
		}
		
		// Check vote ratio
		declare VoteYes = 0;
		declare VoteNo = Players.count;
		foreach (UserId => Vote in PlayersVotes) {
			if (Vote) {
				VoteYes += 1;
				VoteNo -= 1;
			}
		}
		declare VoteRatio = 0.;
		if (PlayersVotes.count > 0) VoteRatio = (VoteYes*1.) / (Players.count*1.);
		
		if (VoteRatio >= S_MatchmakingRematchRatio) {
			Result = True;
			StopVote = True;
		}
	}
	
	Layers::Detach("RematchVoteResults");
	Layers::Destroy("RematchVoteResults");
	Layers::Detach("RematchVote");
	Message::CleanAllMessages();
	
	if (Result) {
		Message::SendStatusMessage(_("Enough players agreed to play a rematch."), 3000, 2);
		Message::SendBigMessage(_("We are starting a new match."), 3000, 2);
	} else {
		Message::SendStatusMessage(_("Not enough players agreed to play a rematch."), 3000, 2);
		Message::SendBigMessage(_("We are sending you back to the lobby."), 3000, 2);
	}
	declare End = Now + 3000;
	while (Now < End) {
		MM_Yield();
		+++MatchmakingMatchVoteForRematch+++
	}
	
	Message::CleanAllMessages();
	
	return Result;
}

// ---------------------------------- //
/** Send the result to the API
 *
 *	@param	_Master		The master of the match
 */
Void MM_MatchEnd(Text _MasterLogin) {
	Private_MM_SetMatchStatus(C_MatchStatus_Ending);
	
	if (Http.SlotsAvailable > 0) {
		declare MatchId = -1;
		if (G_MMMatch_Id != "") MatchId = TL::ToInteger(G_MMMatch_Id);
		declare MatchScores = "";
		foreach (MatchScore in G_MMMatch_Scores) {
			if (MatchScores != "") MatchScores ^= ",";
			MatchScores ^= MatchScore;
		}
		declare PostData = """
{
	"matchid": {{{MatchId}}},
	"master": "{{{TL::MLEncode(_MasterLogin)}}}",
	"scores": [{{{MatchScores}}}]
}""";
		declare Request <=> Http.CreatePost(Private_MM_BuildAPIUrl("/match-server/result"), PostData);
		if (Request != Null) {
			G_Matchmaking_RequestsIds[Request.Id] = C_Request_PostResults;
		}
		
		if (S_MatchmakingLogAPIDebug) MB_Log("[API] Request: POST /match-server/result\n<!--\n"^PostData^"\n-->");
	}
}

// ---------------------------------- //
/// End a match
Void MM_MatchToLobby() {
	UIManager.UIAll.SendChat(TL::Compose("%1 %2", C_MessagePrefix, _("Match over. You will be transferred back.")));
	
	if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] Waiting API to receive status 4");
	
	StartTime = Now;
	declare APIReceived = False;
	declare Transfered = False;
	declare TransfertTime = Now + C_Match_DelayBeforeTransfert;
	UIManager.UIAll.CountdownEndTime = StartTime + C_Match_EndingDuration;
	while (UIManager.UIAll.CountdownEndTime > Now) {
		MM_Yield();
		
		if (!APIReceived && !Transfered && !G_Matchmaking_RequestsIds.exists(C_Request_PostStatus)) {
			TransfertTime = Now + 250;
			APIReceived = True;
			if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] API received status 4, send back players to lobby");
		}
		
		if (!Transfered && Now >= TransfertTime) {
			foreach (Player in AllPlayers) {
				declare Matchmaking_PlayerStatus for Player = C_PlayerStatus_Waiting;
				if (Matchmaking_PlayerStatus == C_PlayerStatus_Valid) {
					MM_SendToServer(Player, G_MMMatch_LobbyLogin);
				}
			}
			Transfered = True;
			if (S_MatchmakingLogMiscDebug) MB_Log("[SERVER] Players sent back to lobby");
		}
	}
	StartTime = -1;
	UIManager.UIAll.CountdownEndTime = -1;
	
	// Kick all remaining players
	// @Tmp, waiting for the CanKick() function before trying to kick everyone
	foreach (User in Users) {
		declare Kicked = Admin_KickUser(User, _("Match over. You should have been transferred back to the lobby."));
	}
	
	G_MMMatch_LobbyLogin = "";
}

// ---------------------------------- //
/** Set the scores to send to the api
 *
 *	@param	_Scores		The scores to send
 */
Void MM_SetScores(Integer[] _Scores) {
	G_MMMatch_Scores = _Scores;
}

// ---------------------------------- //
/** Custom sleep function
 *
 *	@param	_Duration	The time to spend sleeping in ms
 */
Void MM_Sleep(Integer _Duration) {
	declare End = Now + _Duration;
	while (Now < End && !ServerShutdownRequested && !MatchEndRequested) {
		MM_Yield();
		+++SleepLoop+++
		+++MatchmakingSleepLoop+++
	}
}

// ---------------------------------- //
/// Do the player presentation sequence (aka versus screen)
Void MM_PlayersPresentationSequence(Integer _Duration) {
	Clublink::SyncUpdate();
	
	declare End = Now + _Duration;
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::PlayersPresentation;
	while (Now < End && !ServerShutdownRequested && !MatchEndRequested) {
		MM_Yield();
		+++PlayersPresentationLoop+++
		+++MatchmakingPlayersPresentationLoop+++
	}
}

// ---------------------------------- //
/// Overload of the MM_PlayersPresentationSequence() function
Void MM_PlayersPresentationSequence() {
	MM_PlayersPresentationSequence(C_PlayersPresentationTime);
}

// ---------------------------------- //
/** Load a scores table style from an XML file
 *
 *	@param	_Path		Path to the XML file
 */
Void MM_SetScoresTableStyleFromXml(Text _Path) {
	if (_Path == "") return;
	
	declare Loaded = ST2::RequestStyleFromXml(_Path);
	if (Loaded) {
		while (ST2::WaitStyleFromXml() && !ServerShutdownRequested && !MatchEndRequested) {
			MM_Yield();
		}
		Loaded = ST2::SetStyleFromXml(True);
	}
}

// ---------------------------------- //
/** Reset the combo of a player
 *
 *	@param	_Player		The player to reset
 */
Void Private_Lobby_ResetPlayer(CSmPlayer _Player) {
	if (_Player == Null) return;
	declare Integer Lobby_CurrentCombo for _Player = 0;
	Lobby_CurrentCombo  = 0;
}

// ---------------------------------- //
/// Reinitialize all the players
Void Private_Lobby_ReinitPlayers() {
	foreach (Player in AllPlayers) {
		Private_Lobby_ResetPlayer(Player);
		Player.Armor = Player.ArmorMax;
	}
}

// ---------------------------------- //
/// Spawn the players
Void Private_Lobby_SpawnPlayers() {
	if (MapLandmarks_PlayerSpawn.count <= 0) return;
	
	foreach (Player in Players) {
		// Spawn ready players
		if (Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned && Private_Lobby_IsReady(Player.User)) {
			declare Boolean SpawnThePlayer = True;
			
			declare UI <=> UIManager.GetUI(Player);
			if (UI != Null) {
				declare netread Boolean Net_Lobby_RollingIntro for UI;				
				SpawnThePlayer = !Net_Lobby_RollingIntro;
			}
			
			declare Lobby_OnVersusScreen for Player = False;
			SpawnThePlayer = !Lobby_OnVersusScreen;
			
			if (SpawnThePlayer || UI == Null) {
				if (S_LobbyInstagib) {
					SetPlayerWeapon(Player, CSmMode::EWeapon::Laser, False);
				} else {
					SetPlayerWeapon(Player, CSmMode::EWeapon::Rocket, True);
				}
				SM::SpawnPlayer(Player, 0, MapLandmarks_PlayerSpawn[ML::Rand(0, MapLandmarks_PlayerSpawn.count - 1)].PlayerSpawn, Now);
				Private_Lobby_ResetPlayer(Player);
			}
		} 
		// Unspawn unready players
		else if (Player.SpawnStatus == CSmPlayer::ESpawnStatus::Spawned && !Private_Lobby_IsReady(Player.User)) {
			UnspawnPlayer(Player);
		}
	}
}

// ---------------------------------- //
/** Display a message to a player when he beats his best combo
 *
 *	@param	_Player		The player who'll receive the message
 */
Void Private_Lobby_ShowScore(CSmPlayer _Player) {
	if (_Player == Null || _Player.Score == Null) return;

	declare Message = "";
	if (_Player.Score.Points <= 1) {
		Message = TL::Compose( _("%1 point (Best score: %2)"), TL::ToText(_Player.Score.Points), ""^G_MMLobby_BestCombo);
	} else {
		Message = TL::Compose( _("%1 points (Best score: %2)"), TL::ToText(_Player.Score.Points), ""^G_MMLobby_BestCombo);
	}
	
	Message::SendStatusMessage(_Player, Message, 3000, 2);
}

// ---------------------------------- //
/** Count points and display an hit notice to the shooter
 *
 *	@param	_Shooter	The player who shot and hit
 *
 *	@return		The number of points earned on this hit
 */
Integer Private_Lobby_NotifyHit(CSmPlayer _Shooter)  {
	if (_Shooter == Null || _Shooter.Score == Null) return 0;
	
	declare Lobby_CurrentCombo for _Shooter = 0;
	declare PointsEarned = 1 + (Lobby_CurrentCombo / 2);
	_Shooter.Score.Points += PointsEarned;
	Lobby_CurrentCombo += 1;
	
	if (_Shooter.Score.Points > G_MMLobby_BestCombo) {
		G_MMLobby_BestCombo = _Shooter.Score.Points;
		G_MMLobby_WinnerId = _Shooter.User.Id;
	}
	
	Private_Lobby_ShowScore(_Shooter);
	
	return PointsEarned;
}

// ---------------------------------- //
/// Update the timers
Void Private_Lobby_UpdateTimers() {
	declare netwrite Integer	Net_Lobby_TimerMax	for Teams[0];
	declare netwrite Integer	Net_Lobby_StartTime	for Teams[0];
	declare netwrite Boolean	Net_Lobby_AutoDown	for Teams[0];
	declare netwrite Integer	Net_Lobby_TimeDown	for Teams[0];
	
	Net_Lobby_TimerMax = G_MMLobby_EndTime - G_MMLobby_StartTime;
	Net_Lobby_StartTime = G_MMLobby_StartTime;
	
	if (G_MMLobby_Phase == C_Lobby_Playing) {
		Net_Lobby_AutoDown = False;
		Net_Lobby_TimeDown = 0;
	}
}

// ---------------------------------- //
/** Revert the timers
 *
 *	@param	_Duration		Duration of the reversal
 */
Void Private_Lobby_SetTimersAutoDown(Integer _Duration) {
	declare netwrite Integer	Net_Lobby_TimerMax	for Teams[0];
	declare netwrite Boolean	Net_Lobby_AutoDown	for Teams[0];
	declare netwrite Integer	Net_Lobby_TimeDown	for Teams[0];
	
	Net_Lobby_TimerMax = 0;
	Net_Lobby_AutoDown = True;
	Net_Lobby_TimeDown = _Duration;
}

// ---------------------------------- //
/// Update the UI
Void Private_Lobby_UpdateUI() {
	foreach (Player in AllPlayers) {
		declare UI <=> UIManager.GetUI(Player);
		if (UI != Null) {
			// Spectators
			// @specstart
			//if (Player.RequestsSpectate) {
			if (!Private_Lobby_IsReady(Player.User)) {
			// @specend
				declare netwrite Boolean Net_Lobby_ShowRules for UI;
				Net_Lobby_ShowRules = False;
	
				if (UI.UISequence != CUIConfig::EUISequence::RollingBackgroundIntro) {
					UI.UISequence = CUIConfig::EUISequence::RollingBackgroundIntro;
				}
			} 
			// Players
			else {
				declare netwrite Boolean Net_Lobby_ShowRules for UI;
				declare netread Boolean Net_Lobby_RollingIntro for UI;
				Net_Lobby_ShowRules = Net_Lobby_RollingIntro;
				
				if (!Net_Lobby_RollingIntro) {
					if (UI.UISequence != CUIConfig::EUISequence::Playing) {
						UI.UISequence = CUIConfig::EUISequence::Playing;
					}
				} else {
					if (UI.UISequence != CUIConfig::EUISequence::RollingBackgroundIntro) {
						UI.UISequence = CUIConfig::EUISequence::RollingBackgroundIntro;
						if (Player.SpawnStatus != CSmPlayer::ESpawnStatus::NotSpawned) {
							UnspawnPlayer(Player);
						}
					}
				}
			}
			
			// All
			// Request ally from the UI
			declare netread Integer Net_Lobby_RequestAllyUpdate for UI;
			declare Lobby_PrevRequestAllyUpdate for Player = -1;
			if (Lobby_PrevRequestAllyUpdate != Net_Lobby_RequestAllyUpdate) {
				Lobby_PrevRequestAllyUpdate = Net_Lobby_RequestAllyUpdate;
				
				declare netwrite Net_Lobby_SynchroServer for Teams[0] = 0;
				declare netread Text Net_Lobby_RequestAlly for UI;
				declare netread Integer Net_Lobby_SynchroRequestAlly for UI;
				if (
					Net_Lobby_RequestAlly != "" 
					&& Net_Lobby_RequestAlly != Player.Login
					&& Net_Lobby_SynchroRequestAlly == Net_Lobby_SynchroServer
				) {
					declare NewAlly <=> SM::GetPlayer(Net_Lobby_RequestAlly);
					if (NewAlly != Null) {
						Private_MM_RequestAlly(Player, NewAlly);
					}
				}
			}
			
			// Action from rooms screen
			declare netread Integer Net_Lobby_ClientRoomsActionUpdate for UI;
			declare netwrite Integer Net_Lobby_ServerRoomsActionsUpdate for UI;
			
			if (Net_Lobby_ServerRoomsActionsUpdate != Net_Lobby_ClientRoomsActionUpdate) {
				Net_Lobby_ServerRoomsActionsUpdate = Net_Lobby_ClientRoomsActionUpdate;
				
				declare netwrite Net_Lobby_SynchroServer for Teams[0] = 0;
				declare netread Integer Net_Lobby_SynchroRooms for UI;
				
				declare netread Text Net_Lobby_ClientRoomsAction for UI;
				if (Net_Lobby_SynchroServer == Net_Lobby_SynchroRooms && Net_Lobby_ClientRoomsAction != "") {
					switch (Net_Lobby_ClientRoomsAction) {
						case "CreateRoom": {
							Private_MM_CreateRoom(Player);
						}
						case "LeaveRoom": {
							Private_MM_LeaveRoom(Player);
						}
						case "SwitchSlot": {
							declare netread Integer Net_Lobby_RequestSlot for UI;
							declare netread Integer Net_Lobby_RequestClan for UI;
							Private_MM_SwitchSlotRoom(Player, Net_Lobby_RequestClan, Net_Lobby_RequestSlot);
						}
					}
					
					Private_MM_ComputeAllies();
				}
			}
		}
	}
}

// ---------------------------------- //
/** Create the rules reminder manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLRulesReminder() {
	declare Text ImgBaseDir			= "file://Media/Manialinks/Shootmania/Common/";
	declare Text WelcomeBgImage		= ImgBaseDir^"WelcomeBg.dds";

	declare Text TitleText			= _("Waiting for your match to start");
	
	declare Text RulesReminder1		= TL::Compose("$<%1%2$>", "$"^SpawnScreen::GetModeColor(), _("You will soon be redirected to a match server."));
	declare Text RulesReminder2		= TL::Compose("%1", _("While waiting, try to become the King Of The Lobby!"));
	declare Text RulesReminder3		= TL::Compose("$<%11. $>%2", "$"^SpawnScreen::GetModeColor(), _("Perform as many hits as possible: the points you earn for each one will increase."));
	declare Text RulesReminder4		= TL::Compose("$<%12. $>%2", "$"^SpawnScreen::GetModeColor(), _("Your combo falls back to 1 when you are eliminated."));
	
	
	declare Text DoNotShowAgain		= _("Do Not Show Again");
	declare Text Close				= _("Close");
	
	declare Integer	WindowWidth		= 192;
	declare Integer	WindowHeight	= 38;
	declare Real	WindowX			= 0.;
	declare Real	WindowY			= 5.;
	
	return """
<manialink version="1" name="ModeMatchmaking:RulesReminder">
<frame id="RulesReminderMainFrame" posn="{{{WindowX}}} {{{WindowY}}} 0" hidden="true" >
	<format  textemboss="1" />
	<quad posn="0 -2" sizen="210 65" halign="center" valign="center" image="{{{WelcomeBgImage}}}" />
	<label posn="0 {{{(WindowHeight/2)-3}}}" sizen="{{{WindowWidth-4}}} 4" halign="center" valign="center" text="{{{TitleText}}}" textsize="5" />
	<label posn="{{{-(WindowWidth/2)+2}}} {{{(WindowHeight/2)-12}}}" sizen="{{{WindowWidth-4}}} 4" valign="center" text="{{{RulesReminder1}}}" textsize="2"/>
	<label posn="{{{-(WindowWidth/2)+2}}} {{{(WindowHeight/2)-20}}}" sizen="{{{WindowWidth-4}}} 4" valign="center" text="{{{RulesReminder2}}}" textsize="2"/>
	<label posn="{{{-(WindowWidth/2)+2}}} {{{(WindowHeight/2)-24}}}" sizen="{{{WindowWidth-4}}} 4" valign="center" text="{{{RulesReminder3}}}" textsize="2"/>
	<label posn="{{{-(WindowWidth/2)+2}}} {{{(WindowHeight/2)-28}}}" sizen="{{{WindowWidth-4}}} 4" valign="center" text="{{{RulesReminder4}}}" textsize="2"/>
	<label posn="{{{(WindowWidth/2)-2}}} {{{-(WindowHeight/2)+2}}}" halign="right" valign="center" text="{{{DoNotShowAgain}}}" style="CardButtonSmall" ScriptEvents="true" id="Button_DoNotShowAgain" />
	<label posn="{{{(WindowWidth/2)-42}}} {{{-(WindowHeight/2)+2}}}" halign="right" valign="center" text="{{{Close}}}" style="CardButtonSmall" ScriptEvents="true" id="Button_Close" />
</frame>
<script><!--
main() {
	while (InputPlayer == Null) yield;
	
	// for the "do not show again" feature
	declare persistent Boolean NadeoKoL_PersistentShowRulesReminder for This = True;
	//NadeoKoL_PersistentShowRulesReminder = True;
	
	declare netwrite Boolean Net_Lobby_RollingIntro for UI;
	declare netread Boolean Net_Lobby_ShowRules for UI;
	declare netread Boolean Net_Lobby_ShowSubstituteML for UI;
	declare netread Boolean Net_Lobby_ShowVersusML for UI;
	declare netread Text Net_Lobby_ReconnectToServer for UI;
	
	if (!NadeoKoL_PersistentShowRulesReminder) {
		Net_Lobby_RollingIntro = False;
		return;
	}
	Net_Lobby_RollingIntro = True;
	
	declare Button_DoNotShowAgain <=> (Page.GetFirstChild("Button_DoNotShowAgain") as CMlLabel);
	declare Button_Close <=> (Page.GetFirstChild("Button_Close") as CMlLabel);
	declare RulesReminderMainFrame <=> (Page.GetFirstChild("RulesReminderMainFrame") as CMlFrame);

	while (True) {
		yield;
		
		if (Net_Lobby_ShowRules && !Net_Lobby_ShowVersusML && !Net_Lobby_ShowSubstituteML && Net_Lobby_ReconnectToServer == "") {
			RulesReminderMainFrame.Show();
		} else {
			RulesReminderMainFrame.Hide();
		}

		foreach (Event in PendingEvents) {
			switch (Event.Type){
				case CMlEvent::Type::MouseClick: {
					if (Event.ControlId == "Button_DoNotShowAgain") {
						NadeoKoL_PersistentShowRulesReminder = False;
						Net_Lobby_RollingIntro = False;
					}
					if (Event.ControlId == "Button_Close") {
						Net_Lobby_RollingIntro = False;
					}
				}
			}
		}
	}
}
--></script>
</manialink>
""";
}

// ---------------------------------- //
/** Create the gauge timer manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLGaugeTimer() {
	declare Hidden = "0";
	if (S_LobbyDisableUI) Hidden = "1";
	
	return """
<manialink version="1" name="ModeMatchmaking:GaugeTimer">
<frame posn="0 69 2" hidden="{{{Hidden}}}">
	<gauge posn="0 6.3" sizen="90 7" halign="center" valign="center" drawbg="false" id="Gauge_Timer" />
	<label posn="0 0" sizen="90 7" halign="center" valign="center" style="TextRaceChrono" textsize="6" textcolor="f90d" id="Label_WaitTime" />
</frame>
<script><!--
#Include "TextLib" as TextLib

// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

main() {
	declare Gauge_Timer		<=> (Page.GetFirstChild("Gauge_Timer")		as CMlGauge);
	declare Label_WaitTime	<=> (Page.GetFirstChild("Label_WaitTime")	as CMlLabel);
	
	declare netread Integer	Net_Lobby_TimerMax	for Teams[0];
	declare netread Integer	Net_Lobby_StartTime	for Teams[0];
	declare netread Boolean	Net_Lobby_AutoDown	for Teams[0];
	declare netread Integer	Net_Lobby_TimeDown	for Teams[0];
	
	declare Integer WaitingTime = 0;
	
	declare Integer Period;
	declare Integer PrevNow = Now;
	declare PrevTimerMax = -1;
	declare PrevTimeDown = -1;
	
	while (True) {
		sleep(50);
		Period = Now - PrevNow;
		PrevNow = Now;
		
		if (PrevTimerMax != Net_Lobby_TimerMax || PrevTimeDown != Net_Lobby_TimeDown) {
			PrevTimerMax = Net_Lobby_TimerMax;
			PrevTimeDown = Net_Lobby_TimeDown;
			
			if (Net_Lobby_AutoDown) Gauge_Timer.Ratio = 1.;
			else Gauge_Timer.Ratio = 0.;
		}
		
		if (Net_Lobby_AutoDown && Net_Lobby_TimeDown != 0) {
			declare Real SpeedDown = (1. * Period)  / (1.* Net_Lobby_TimeDown);
			if ((Gauge_Timer.Ratio - SpeedDown) < 0) Gauge_Timer.Ratio = 0.;
			else Gauge_Timer.Ratio -= SpeedDown;
		} else {
			if (Net_Lobby_TimerMax > 0) {
				declare Real Ratio = (1. * (ArenaNow - Net_Lobby_StartTime)) / Net_Lobby_TimerMax;
				if(Ratio < 0.) Ratio = 0.;
				if(Ratio > 1.) Ratio = 1.;
				Gauge_Timer.Ratio = Ratio;
			}
		}
		
		// @specstart
		//if (IsSpectatorMode) {
		if (!Private_Lobby_IsReady()) {
		// @specend
			Label_WaitTime.Hide();
			WaitingTime = 0;
		} else {
			WaitingTime += Period;
			Label_WaitTime.Show();					
			Label_WaitTime.SetText(TextLib::TimeToText(WaitingTime));
		}
	}
}
--></script>
</manialink>
""";
}

// ---------------------------------- //
/** Create the waiting screen manialink
 *
 *	@param	_DisplayRules		Allow to display a manialink with the rules of the mode
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLWaitingScreen(Boolean _DisplayRules) {
	declare Hidden = "0";
	if (S_LobbyDisableUI) Hidden = "1";
	
	declare SizeX = 141.;
	declare SizeY = 99.;
	
	declare Background_Global = "file://Media/Manialinks/Common/Lobbies/main-bg.png";
	declare Background_OccupiedSlot = "file://Media/Manialinks/Common/Lobbies/PlayerCardBg.dds";
	declare Echelon0 = "file://Media/Manialinks/Common/Echelons/echelon0.dds";
	declare ButtonQuitOn = "file://Media/Manialinks/Common/Lobbies/small-button-RED-ON.dds";
	declare ButtonQuitOff = "file://Media/Manialinks/Common/Lobbies/small-button-RED.dds";
	declare ButtonRulesOn = "file://Media/Manialinks/Common/Lobbies/small-button-YELLOW-ON.dds";
	declare ButtonRulesOff = "file://Media/Manialinks/Common/Lobbies/small-button-YELLOW.dds";
	declare ButtonReadyOn = "file://Media/Manialinks/Common/Lobbies/ready-button-GREEN-ON.dds";
	declare ButtonReadyOff = "file://Media/Manialinks/Common/Lobbies/ready-button-GREEN.dds";
	declare ButtonHideOn = "file://Media/Manialinks/Shootmania/Common/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Shootmania/Common/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Shootmania/Common/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Shootmania/Common/button-show.png";
	
	declare ButtonRules = "";
	if (_DisplayRules) {
		ButtonRules = """
<frame posn="0 -25 1" id="Frame_Rules">
	<quad sizen="35 10" halign="center" valign="center" image="{{{ButtonRulesOff}}}" imagefocus="{{{ButtonRulesOn}}}" scriptevents="1" id="Button_Rules" />
	<label sizen="35 10" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" text="{{{_("Rules")}}}" />
</frame>""";
	}
	
	declare PlayersList = "";
	declare SlotsNb = 6;
	for (I, 0, SlotsNb-1) {
		declare PosY = I * -23;
		PlayersList^= """
<frame class="Frame_PlayerCard" hidden="1" id="{{{I+1}}}">
	<frameinstance modelid="Framemodel_EmptySlot" />
	<frameinstance modelid="Framemodel_OccupiedSlot" />
</frame>""";
	}
	
	return """
<manialink version="1" name="ModeMatchmaking:WaitingScreen">
<framemodel id="Framemodel_EmptySlot">
	<frame id="Frame_EmptySlot">
		<quad sizen="80 20 -1" halign="center" valign="center" style="Bgs1" substyle="BgListLine" />
		<label sizen="75 4" halign="center" valign="center2" style="TextButtonSmall" text="{{{_("Picked by matchmaking")}}}" />
	</frame>
</framemodel>
<framemodel id="Framemodel_OccupiedSlot">
	<frame posn="-40 10" hidden="1" id="Frame_OccupiedSlot">
		<quad sizen="80 20 -1" image="{{{Background_OccupiedSlot}}}" />
		<quad posn="0.75 -10 1" sizen="18.5 18.5" valign="center" bgcolor="aaa" id="Quad_Avatar" />
		<label posn="22 -4" sizen="38 4" style="TextRaceMessage" textsize="3.5" id="Label_Name" />
		<label posn="27 -15" sizen="30 4" valign="center2" style="TextRaceMessage" textsize="1" id="Label_Rank" />
		<quad posn="22 -15" sizen="4 4" valign="center" id="Quad_CountryFlag" />
		<frame posn="72 0 1" scale="1.13">
			<quad sizen="14.1551 17.6938" halign="center" image="{{{Echelon0}}}" id="Quad_Echelon" />
			<label posn="0 -3.6" sizen="14 0" halign="center" style="TextRaceMessage" textsize="0.5" text="Echelon" />
			<label posn="0 -10.6" sizen="10 10" halign="center" valign="center" style="TextRaceMessageBig" text="0" id="Label_Echelon" />
		</frame>
		<quad posn="0 0 3" sizen="80 20" bgcolor="333a" hidden="1" id="Quad_AllyNotValidated" />
		<quad posn="0 0" sizen="80 20" scriptevents="1" id="Button_Player" />
	</frame>
</framemodel>
<frame hidden="{{{Hidden}}}">
<frame posn="0 0 10" id="Frame_Global">
	<quad sizen="{{{SizeX}}} {{{SizeY}}} -1" halign="center" valign="center" image="{{{Background_Global}}}" />
	<label posn="0 38" sizen="{{{SizeX}}} 4" scale="0.9" halign="center" valign="top" textsize="5" textemboss="1" autonewline="1" maxline="2" opacity="0.9" id="Label_Title" />
	<frame posn="{{{SizeX*0.37}}} {{{SizeY*0.47}}} -2" id="Frame_ShowHide">
		<quad sizen="5 6" rot="-90" image="{{{ButtonHideOff}}}" imagefocus="{{{ButtonHideOn}}}" scriptevents="1" id="Button_ShowHide" />
	</frame>
	<frame posn="0 18 1" scale="0.6" id="Frame_PlayerList">
		{{{PlayersList}}}
	</frame>
	<frame posn="0 4 1" id="Frame_Pager">
		<quad posn="-52" sizen="9 9" halign="right" valign="center" style="Icons64x64_1" substyle="ArrowPrev" scriptevents="1" id="Button_PagerPrev" />
		<quad posn="52 0" sizen="9 9" valign="center" style="Icons64x64_1" substyle="ArrowNext" scriptevents="1" id="Button_PagerNext" />
	</frame>
	<frame posn="-47 -36 1" id="Frame_ButtonQuit">
		<quad sizen="35 10" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" action="maniaplanet:quitserver" />
		<label sizen="35 10" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" text="{{{_("Quit")}}}" id="Label_Quit" />
	</frame>
	{{{ButtonRules}}}
	<frame posn="0 -36 1" id="Frame_Ready">
		<!-- @specstart -->
		<quad sizen="48 12" halign="center" valign="center" image="{{{ButtonReadyOff}}}" imagefocus="{{{ButtonReadyOn}}}" scriptevents="1" id="Button_Ready" />
		<!-- @specend -->
		<label sizen="48 12" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2.5" text="{{{_("Ready (F6)")}}}" />
	</frame>
	<quad posn="0 -90" sizen="80 20" halign="center" valign="bottom" id="Quad_Logo" />
</frame>
</frame>
<script><!--
#Include "TextLib" as TL
#Include "MathLib" as ML

declare Integer G_PageStart;
declare Integer G_PlayersMax;

{{{Manialink::Animations(["EaseOutExp"])}}}
// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

Integer GetEchelon(CUser::EEchelon _Echelon) {
	switch (_Echelon) {
		case CUser::EEchelon::Bronze1	: return 1;
		case CUser::EEchelon::Bronze2	: return 2;
		case CUser::EEchelon::Bronze3	: return 3;
		case CUser::EEchelon::Silver1	: return 4;
		case CUser::EEchelon::Silver2	: return 5;
		case CUser::EEchelon::Silver3	: return 6;
		case CUser::EEchelon::Gold1		: return 7;
		case CUser::EEchelon::Gold2		: return 8;
		case CUser::EEchelon::Gold3		: return 9;
	}
	
	return 0;
}

Text GetEchelonPath(CUser::EEchelon _Echelon) {
	return "file://Media/Manialinks/Common/Echelons/echelon"^GetEchelon(_Echelon)^".dds";
}

CUser GetUser(Text _Login) {
	foreach (Player in Players) {
		if (Player.Login == _Login) return Player.User;
	}
	
	return Null;
}

Void UpdateOccupiedSlot(CMlFrame _Frame, CUser _User, Text _FallbackLogin) {
	if (_Frame == Null) return;
	
	declare Quad_Avatar			<=> (_Frame.GetFirstChild("Quad_Avatar")		as CMlQuad);
	declare Label_Name			<=> (_Frame.GetFirstChild("Label_Name")			as CMlLabel);
	declare Label_Rank			<=> (_Frame.GetFirstChild("Label_Rank")			as CMlLabel);
	declare Quad_CountryFlag	<=> (_Frame.GetFirstChild("Quad_CountryFlag")	as CMlQuad);
	declare Quad_Echelon		<=> (_Frame.GetFirstChild("Quad_Echelon")		as CMlQuad);
	declare Label_Echelon		<=> (_Frame.GetFirstChild("Label_Echelon")		as CMlLabel);
	declare Button_Player		<=> (_Frame.GetFirstChild("Button_Player")		as CMlQuad);
	
	if (_User != Null) {
		if (Quad_Avatar != Null)		Quad_Avatar.ImageUrl = "file://Avatars/"^_User.Login^"/Default";
		if (Label_Name != Null)			Label_Name.Value = _User.Name;
		if (Quad_CountryFlag != Null)	Quad_CountryFlag.ImageUrl = _User.CountryFlagUrl;
		if (Quad_Echelon != Null)		Quad_Echelon.ImageUrl = GetEchelonPath(_User.Echelon);
		if (Label_Echelon != Null)		Label_Echelon.Value = TL::ToText(GetEchelon(_User.Echelon));
		if (Label_Rank != Null) {
			declare Zone = _("Other");
			declare ZoneArray = TL::Split("|", _User.LadderZoneName);
			if (ZoneArray.existskey(2)) Zone = ZoneArray[2];
			if (_User.LadderRank > 0) Label_Rank.Value = TL::Compose("%1: %2", Zone, TL::ToText(_User.LadderRank));
			else Label_Rank.Value = TL::Compose("%1: %2", Zone, _("Not ranked"));
		}
		
		declare Text Lobby_PlayerLogin for Button_Player;
		Lobby_PlayerLogin = _User.Login;
	} else {
		if (Quad_Avatar != Null)		Quad_Avatar.ImageUrl = "file://Avatars/"^_FallbackLogin^"/Default";
		if (Label_Name != Null)			Label_Name.Value = _FallbackLogin;
		if (Quad_CountryFlag != Null)	Quad_CountryFlag.ImageUrl = "";
		if (Quad_Echelon != Null)		Quad_Echelon.ImageUrl = GetEchelonPath(CUser::EEchelon::None);
		if (Label_Echelon != Null)		Label_Echelon.Value = "0";
		if (Label_Rank != Null)			Label_Rank.Value = "";
		
		declare Text Lobby_PlayerLogin for Button_Player;
		Lobby_PlayerLogin = _FallbackLogin;
	}
}

Void UpdateAllies() {
	declare netread Integer[Text] Net_Lobby_AlliesLogins for UI;
	
	declare Logins = Text[];
	declare Statuses = Integer[];
	foreach (Login => Status in Net_Lobby_AlliesLogins) {
		Logins.add(Login);
		Statuses.add(Status);
	}
	
	declare Frame_PlayerList <=> (Page.GetFirstChild("Frame_PlayerList") as CMlFrame);
	Page.GetClassChildren("Frame_PlayerCard", Frame_PlayerList, True);
	foreach (Control in Page.GetClassChildren_Result) {
		declare Count = G_PageStart + TL::ToInteger(Control.ControlId);
		
		declare Frame_PlayerCard	<=> (Control as CMlFrame);
		declare Frame_EmptySlot		<=> (Frame_PlayerCard.GetFirstChild("Frame_EmptySlot")		as CMlFrame);
		declare Frame_OccupiedSlot	<=> (Frame_PlayerCard.GetFirstChild("Frame_OccupiedSlot")	as CMlFrame);
		declare Quad_AllyNotValidated	<=> (Frame_OccupiedSlot.GetFirstChild("Quad_AllyNotValidated")	as CMlQuad);
		
		if (Count == 1) {
			Frame_EmptySlot.Visible = False;
			Frame_OccupiedSlot.Visible = True;
			Quad_AllyNotValidated.Visible = False;
			UpdateOccupiedSlot(Frame_OccupiedSlot, InputPlayer.User, InputPlayer.Login);
		} else if (Logins.existskey(Count-2)) {
			Frame_EmptySlot.Visible = False;
			Frame_OccupiedSlot.Visible = True;
			if (Statuses[Count-2] == {{{C_AllyStatus_Validated}}}) {
				Quad_AllyNotValidated.Visible = False;
			} else {
				Quad_AllyNotValidated.Visible = True;
			}
			UpdateOccupiedSlot(Frame_OccupiedSlot, GetUser(Logins[Count-2]), Logins[Count-2]);
		} else if (Count > G_PlayersMax) {
			Frame_EmptySlot.Visible = False;
			Frame_OccupiedSlot.Visible = False;
		} else {
			Frame_EmptySlot.Visible = True;
			Frame_OccupiedSlot.Visible = False;
		}
	}
}

Void UpdatePager(Integer _Shift) {
	declare Frame_Pager <=> (Page.GetFirstChild("Frame_Pager") as CMlFrame);
	if (G_PlayersMax > {{{SlotsNb}}}) {
		Frame_Pager.Visible = True;
	} else if (G_PlayersMax <= {{{SlotsNb}}}) {
		Frame_Pager.Visible = False;
		G_PageStart = 0;
	}
	
	declare NewPageStart = G_PageStart + (_Shift * {{{SlotsNb}}});
	if (NewPageStart < 0) NewPageStart = 0;
	else if (NewPageStart > G_PlayersMax - 1) NewPageStart = G_PageStart;
	G_PageStart = NewPageStart;
	if (G_PageStart > G_PlayersMax) G_PageStart = G_PlayersMax - 1;
	
	declare Button_PagerNext <=> (Frame_Pager.GetFirstChild("Button_PagerNext") as CMlQuad);
	declare Button_PagerPrev <=> (Frame_Pager.GetFirstChild("Button_PagerPrev") as CMlQuad);
	if (G_PageStart <= 0) Button_PagerPrev.Substyle = "ArrowDisabled";
	else Button_PagerPrev.Substyle = "ArrowPrev";
	if (G_PageStart + {{{SlotsNb}}} >= G_PlayersMax) Button_PagerNext.Substyle = "ArrowDisabled";
	else Button_PagerNext.Substyle = "ArrowNext";
	
	UpdateAllies();
}

Void UpdateFormat(Integer[] _Format) {
	declare netread Integer Net_Matchmaking_MaxPlayers for Teams[0];
	G_PlayersMax = Net_Matchmaking_MaxPlayers;
	
	declare Frame_PlayerList <=> (Page.GetFirstChild("Frame_PlayerList") as CMlFrame);
	Page.GetClassChildren("Frame_PlayerCard", Frame_PlayerList, True);
	declare X = 0;
	declare Y = 0;
	foreach (Control in Page.GetClassChildren_Result) {
		declare Count = TL::ToInteger(Control.ControlId);
		
		if (Count > G_PlayersMax) Control.Visible = False;
		else Control.Visible = True;
		
		declare PosX = -41.5 + (X * 82.);
		declare PosY = Y * -23.;
		
		if (Count == G_PlayersMax && Count % 2 != 0) {
			PosX = 0.;
		}
		
		Control.RelativePosition.X = PosX;
		Control.RelativePosition.Y = PosY;
		
		X += 1;
		if (Count % 2 == 0) {
			Y += 1;
			X = 0;
		}
	}
	
	UpdatePager(0);
}

Void DisplayFrame(Boolean _Displayed) {
	declare Button_ShowHide <=> (Page.GetFirstChild("Button_ShowHide") as CMlQuad);
	
	if (_Displayed) {
		Button_ShowHide.ImageUrl = "{{{ButtonHideOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonHideOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="0 0 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	} else {
		Button_ShowHide.ImageUrl = "{{{ButtonShowOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonShowOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="0 -133 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	}
}

main() {
	declare Frame_Global	<=> (Page.GetFirstChild("Frame_Global")			as CMlFrame);
	declare Frame_Ready		<=> (Frame_Global.GetFirstChild("Frame_Ready")	as CMlFrame);
	declare Label_Title		<=> (Frame_Global.GetFirstChild("Label_Title")	as CMlLabel);
	
	declare netread Integer Net_Matchmaking_FormatUpdate for Teams[0];
	declare netread Integer[] Net_Matchmaking_Format for Teams[0];
	declare netread Integer Net_Lobby_AlliesLoginsUpdate for UI;
	declare netread Boolean Net_Lobby_ImBlocked for UI;
	declare netread Integer Net_Lobby_Penalty for UI;
	declare netread Integer Net_Lobby_MatchCancellation for UI;
	declare netread Boolean Net_Lobby_AllowMatchCancel for Teams[0];
	declare netread Integer Net_Lobby_LobbyLimitMatchCancel for Teams[0];
	declare netread Boolean Net_Lobby_WarnPenalty for Teams[0];
	declare netread Boolean Net_Lobby_ShowSubstituteML for UI;
	declare netread Boolean Net_Lobby_ShowVersusML for UI;
	declare netread Text Net_Lobby_ReconnectToServer for UI;
	declare netread Integer Net_Lobby_SynchroServer for Teams[0];
	
	declare netwrite Integer Net_Lobby_RequestAllyUpdate for UI;
	declare netwrite Text Net_Lobby_RequestAlly for UI;
	declare netwrite Integer Net_Lobby_SynchroRequestAlly for UI;
	
	declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
	if (!MM_FrameIsDisplayed.existskey("WaitingScreen")) MM_FrameIsDisplayed["WaitingScreen"] = True;
	DisplayFrame(MM_FrameIsDisplayed["WaitingScreen"]);
	
	G_PageStart = 0;
	G_PlayersMax = {{{SlotsNb}}};
	
	declare Lobby_RulesAreVisible for UI = False;
	Lobby_RulesAreVisible = False;
	
	// @specstart
	//declare PrevIsSpectatorMode = True;
	declare PrevIsReady = False;
	// @specend
	declare PrevFormatUpdate = -1;
	declare PrevAlliesLoginUpdate = -1;
	declare PrevIsBlocked = False;
	declare PrevReconnectToServer = "";
	declare PrevMatchmakingMode = -1;
		
	HideResumePlayingButton = True;
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		LibManialink_AnimLoop();
		
		// @specstart
		/*if (PrevIsSpectatorMode != IsSpectatorMode || PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevIsSpectatorMode = IsSpectatorMode;*/
		if (PrevIsReady != Private_Lobby_IsReady() || PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevIsReady = Private_Lobby_IsReady();
		// @specend
			PrevReconnectToServer = Net_Lobby_ReconnectToServer;
			
			// @specstart
			//if (IsSpectatorMode && Net_Lobby_ReconnectToServer == "") {
			if (!Private_Lobby_IsReady() && Net_Lobby_ReconnectToServer == "") {
			// @specend
				Frame_Global.Visible = True;
			} else {
				Frame_Global.Visible = False;
			}
		}
		
		if (Frame_Global.Visible && PrevFormatUpdate != Net_Matchmaking_FormatUpdate) {
			PrevFormatUpdate = Net_Matchmaking_FormatUpdate;
			UpdateFormat(Net_Matchmaking_Format);
		}
		
		if (Frame_Global.Visible && PrevAlliesLoginUpdate != Net_Lobby_AlliesLoginsUpdate) {
			PrevAlliesLoginUpdate = Net_Lobby_AlliesLoginsUpdate;
			UpdatePager(0);
		}
		
		if (Frame_Global.Visible && PrevIsBlocked != Net_Lobby_ImBlocked) {
			PrevIsBlocked = Net_Lobby_ImBlocked;
			
			if (Net_Lobby_ImBlocked) {
				Frame_Ready.Visible = False;
			} else {
				Frame_Ready.Visible = True;
			}
		}
		
		if (Net_Lobby_Penalty > 0) {
			declare Duration = Net_Lobby_Penalty - ArenaNow + 1000;
			Label_Title.Value = TL::Compose("%1 - %2", "{{{_("You are suspended")}}}", TL::TimeToText(Duration, False));
		} else if (Net_Lobby_Penalty == 0) {
			Label_Title.Value = "{{{_("You are suspended")}}}";
		} else if (Net_Lobby_Penalty < 0) {
			if (Net_Lobby_WarnPenalty && Net_Lobby_AllowMatchCancel && Net_Lobby_LobbyLimitMatchCancel >= 0) {
				declare CancellationRemaing = Net_Lobby_LobbyLimitMatchCancel - Net_Lobby_MatchCancellation;
				if (CancellationRemaing <= 0) {
					Label_Title.Value = _("You will be suspended if you cancel your next match.");
				} else if (CancellationRemaing == 1) {
					Label_Title.Value = TL::Compose(_("You can cancel 1 match."), TL::ToText(CancellationRemaing));
				} else {
					Label_Title.Value = TL::Compose(_("|%1 is the number of matches|You can cancel %1 matches."), TL::ToText(CancellationRemaing));
				}
			} else if (Label_Title.Value != "") {
				Label_Title.Value = "";
			}
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				switch (Event.ControlId) {
					case "Button_Rules": {
						Lobby_RulesAreVisible = True;
					}
					case "Button_Ready": {
						// @specstart
						//if (!Net_Lobby_ImBlocked) IsSpectatorMode = False;
						Private_Lobby_SetReady(True);
						// @specend
					}
					case "Button_Player": {
						declare Text Lobby_PlayerLogin for Event.Control;
						Net_Lobby_SynchroRequestAlly = Net_Lobby_SynchroServer;
						Net_Lobby_RequestAllyUpdate = Now;
						Net_Lobby_RequestAlly = Lobby_PlayerLogin;
					}
					case "Button_PagerNext": {
						UpdatePager(1);
					}
					case "Button_PagerPrev": {
						UpdatePager(-1);
					}
					case "Button_ShowHide": {
						declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
						MM_FrameIsDisplayed["WaitingScreen"] = !MM_FrameIsDisplayed["WaitingScreen"];
						DisplayFrame(MM_FrameIsDisplayed["WaitingScreen"]);
					}
				}
			} else if (Event.Type == CMlEvent::Type::KeyPress) {
				if (Event.KeyName == "F6") {
					// @specstart
					/*if (
						(Net_Lobby_ShowSubstituteML || Net_Lobby_ShowVersusML)
						&& !Net_Lobby_AllowMatchCancel && !IsSpectatorMode
					) {
					
					} else if (!Net_Lobby_ImBlocked) {
						IsSpectatorMode = !IsSpectatorMode;
					}*/
					if (
						(Net_Lobby_ShowSubstituteML || Net_Lobby_ShowVersusML)
						&& !Net_Lobby_AllowMatchCancel && Private_Lobby_IsReady()
					) {
					
					} else if (!Net_Lobby_ImBlocked) {
						Private_Lobby_ToggleReady();
					}
					// @specend
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the reconnect manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLReconnect() {
	declare ButtonQuitOn = "file://Media/Manialinks/Common/Lobbies/small-button-RED-ON.dds";
	declare ButtonQuitOff = "file://Media/Manialinks/Common/Lobbies/small-button-RED.dds";
	
	return """
<manialink version="1" name="ModeMatchmaking:Reconnect">
<frame posn="0 10" hidden="1" id="Frame_Global">
	<label posn="0 59" sizen="90 7" halign="center" valign="center" style="TextRaceChrono" textsize="6" textcolor="f90d" id="Label_Countdown" />
	<label halign="center" valign="center" style="TextRaceMessageBig" textsize="6" text="{{{_("We are sending you back to the match you left.")}}}" />
	<label posn="0 -8" halign="center" valign="center" style="TextRaceMessageBig" textsize="4" text="{{{_("Please wait.")}}}" />
	<frame posn="0 -18" hidden="0" id="Frame_Cancel">
		<!-- @specstart -->
		<quad sizen="35 8" scale="1.3" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" scriptevents="1" id="Button_Cancel" />
		<!-- @specend -->
		<label sizen="35 8" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" text="{{{_("F6 to cancel")}}}" id="Label_Quit" />
	</frame>
</frame>
<script><!--
#Include "TextLib" as TL

// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

main() {
	declare Frame_Global	<=> (Page.GetFirstChild("Frame_Global")		as CMlFrame);
	declare Frame_Cancel	<=> (Page.GetFirstChild("Frame_Cancel")		as CMlFrame);
	declare Label_Countdown	<=> (Page.GetFirstChild("Label_Countdown")	as CMlLabel);
	
	declare netread Text Net_Lobby_ReconnectToServer for UI;
	declare netread Integer Net_Lobby_MatchCancellation for UI;
	declare netread Boolean Net_Lobby_AllowMatchCancel for Teams[0];
	declare netread Integer Net_Lobby_LobbyLimitMatchCancel for Teams[0];
	
	declare PrevReconnectToServer = "";
	declare PrevMatchCancellation = -1;
	declare PrevAllowMatchCancel = True;
	declare PrevLobbyLimitMatchCancel = -1;
	
	while (True) {
		yield;
		
		if (!PageIsVisible || InputPlayer == Null) continue;
		
		if (PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevReconnectToServer = Net_Lobby_ReconnectToServer;
			
			if (Net_Lobby_ReconnectToServer != "") {
				Frame_Global.Visible = True;
			} else {
				Frame_Global.Visible = False;
			}
		}
		
		if (PrevAllowMatchCancel != Net_Lobby_AllowMatchCancel) {
			PrevAllowMatchCancel = Net_Lobby_AllowMatchCancel;
			Frame_Cancel.Visible = Net_Lobby_AllowMatchCancel;
		}
		
		if (
			Net_Lobby_AllowMatchCancel &&
			(PrevMatchCancellation != Net_Lobby_MatchCancellation || PrevLobbyLimitMatchCancel != Net_Lobby_LobbyLimitMatchCancel)
		) {
			PrevMatchCancellation = Net_Lobby_MatchCancellation;
			PrevLobbyLimitMatchCancel = Net_Lobby_LobbyLimitMatchCancel;
			
			if (Net_Lobby_MatchCancellation < Net_Lobby_LobbyLimitMatchCancel) {
				Frame_Cancel.Visible = True;
			} else {
				Frame_Cancel.Visible = False;
			}
		}
		
		if (UI.CountdownEndTime > ArenaNow) {
			Label_Countdown.Value = TL::TimeToText(UI.CountdownEndTime - ArenaNow + 1000);
		}
		
		// @specstart
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Cancel") {
					Private_Lobby_SetReady(True);
				}
			}
		}
		// @specend
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the players list manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLPlayersList() {
	declare SizeX = 52.;
	declare SizeY = 127.;
	
	declare Background = "file://Media/Manialinks/Common/Lobbies/side-frame.png";
	declare Echelon0 = "file://Media/Manialinks/Common/Echelons/echelon0.dds";
	declare ButtonHideOn = "file://Media/Manialinks/Shootmania/Common/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Shootmania/Common/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Shootmania/Common/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Shootmania/Common/button-show.png";
	
	declare PlayersNb = 20;
	declare PlayersList = "";
	declare PCSizeY = 5.;
	for (I, 1, PlayersNb) {
		declare PosY = (I - 1) * -PCSizeY;
		PlayersList ^= """<frameinstance posn="0 {{{PosY}}}" hidden="1" modelid="Framemodel_PlayerCard" id="{{{I}}}" />""";
	}
	
	return """
<manialink version="1" name="ModeMatchmaking:PlayersList">
<framemodel class="Frame_PlayerCard" id="Framemodel_PlayerCard">
	<quad posn="0 0 -1" sizen="46 {{{PCSizeY}}}" valign="center" bgcolorfocus="ccc8" scriptevents="1" id="Button_Player" />
	<quad sizen="2 {{{PCSizeY}}}" valign="center" bgcolor="c00" opacity="0.8" id="Quad_Status" />
	<quad posn="3 0" sizen="{{{PCSizeY}}} {{{PCSizeY}}}" scale="0.9" valign="center" id="Quad_CountryFlag" />
	<label posn="8 0" sizen="34 4" valign="center2" textsize="1" textemboss="1" id="Label_Name" />
	<frame posn="{{{SizeX-8.5}}} {{{PCSizeY/2.}}} 1" scale="0.29">
		<quad sizen="14.1551 17.6938" halign="center" image="{{{Echelon0}}}" id="Quad_Echelon" />
		<label posn="0 -10" sizen="10 10" halign="center" valign="center" style="TextRaceMessageBig" text="0" id="Label_Echelon" />
	</frame>
</framemodel>
<frame posn="164 0 10" id="Frame_Global">
	<quad posn="0 0 -1" sizen="{{{SizeX}}} {{{SizeY}}}" halign="right" valign="center" image="{{{Background}}}" />
	<frame posn="{{{-SizeX*0.99}}} {{{SizeY*0.42}}} -2" id="Frame_ShowHide">
		<quad sizen="5 6" rot="180" image="{{{ButtonHideOff}}}" imagefocus="{{{ButtonHideOn}}}" scriptevents="1" id="Button_ShowHide" />
	</frame>
	<frame posn="{{{-SizeX/2.-1.}}} {{{SizeY/2.}}}">
		<frame id="Frame_Title">
			<label posn="0 -4.5" sizen="{{{SizeX-8}}} 4" halign="center" style="TextRaceMessage" textopacity="0.9" text="{{{_("Players")}}}" />
			<label posn="0 -13" sizen="{{{SizeX-8}}} 4" halign="center" valign="bottom" style="TextTips" textsize="1" textopacity="0.75" text="{{{_("Click on a player to set them as ally.")}}}" id="Label_Help" />
		</frame>
		<frame posn="{{{-SizeX/2.+3.2}}} -18 2" id="Frame_PlayersList">
			{{{PlayersList}}}
		</frame>
		<frame posn="0 -119.2" hidden="1" id="Frame_Pager">
			<quad posn="-5 0" sizen="9 9" halign="right" valign="center" style="Icons64x64_1" substyle="ArrowPrev" scriptevents="1" id="Button_PagerPrev" />
			<quad posn="5 0" sizen="9 9" valign="center" style="Icons64x64_1" substyle="ArrowNext" scriptevents="1" id="Button_PagerNext" />
			<label sizen="10 0" halign="center" valign="center2" id="Label_Pager" />
		</frame>
	</frame>
</frame>
<script><!--
#Include "TextLib" as TL
#Include "MathLib" as ML

declare Integer G_PageStart;

{{{Manialink::Animations(["EaseOutExp"])}}}
// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

Integer GetEchelon(CUser::EEchelon _Echelon) {
	switch (_Echelon) {
		case CUser::EEchelon::Bronze1	: return 1;
		case CUser::EEchelon::Bronze2	: return 2;
		case CUser::EEchelon::Bronze3	: return 3;
		case CUser::EEchelon::Silver1	: return 4;
		case CUser::EEchelon::Silver2	: return 5;
		case CUser::EEchelon::Silver3	: return 6;
		case CUser::EEchelon::Gold1		: return 7;
		case CUser::EEchelon::Gold2		: return 8;
		case CUser::EEchelon::Gold3		: return 9;
	}
	
	return 0;
}

Text GetEchelonPath(CUser::EEchelon _Echelon) {
	return "file://Media/Manialinks/Common/Echelons/echelon"^GetEchelon(_Echelon)^".dds";
}

Void UpdatePlayerCard(CMlFrame _Frame, CPlayer _Player) {
	if (_Frame == Null || _Player == Null) return;
	
	declare Button_Player 		<=> (_Frame.GetFirstChild("Button_Player")		as CMlQuad);
	declare Quad_Status			<=> (_Frame.GetFirstChild("Quad_Status")		as CMlQuad);
	declare Quad_CountryFlag	<=> (_Frame.GetFirstChild("Quad_CountryFlag")	as CMlQuad);
	declare Label_Name			<=> (_Frame.GetFirstChild("Label_Name")			as CMlLabel);
	declare Quad_Echelon		<=> (_Frame.GetFirstChild("Quad_Echelon")		as CMlQuad);
	declare Label_Echelon		<=> (_Frame.GetFirstChild("Label_Echelon")		as CMlLabel);
	
	if (Button_Player != Null) {
		declare netread Integer[Text] Net_Lobby_AlliesLogins for UI;
		if (Net_Lobby_AlliesLogins.count > 0 && _Player.Login == InputPlayer.Login) {
			Button_Player.BgColor = <0.15, 0.3, 0.15>;
			Button_Player.Opacity = 0.7;
		} else if (Net_Lobby_AlliesLogins.existskey(_Player.Login) && Net_Lobby_AlliesLogins[_Player.Login] == {{{C_AllyStatus_Validated}}}) {
			Button_Player.BgColor = <0.15, 0.3, 0.15>;
			Button_Player.Opacity = 0.7;
		} else if (Net_Lobby_AlliesLogins.existskey(_Player.Login) && Net_Lobby_AlliesLogins[_Player.Login] == {{{C_AllyStatus_Sent}}}) {
			Button_Player.BgColor = <0.3, 0.15, 0.>;
			Button_Player.Opacity = 0.7;
		} else {
			Button_Player.BgColor = <0.2, 0.2, 0.2>;
			Button_Player.Opacity = 0.5;
		}
		
		declare Text Lobby_PlayerLogin for Button_Player;
		Lobby_PlayerLogin = _Player.User.Login;
	}
	if (Quad_Status != Null) {
		declare netread Boolean Net_Lobby_AlliesAreReady for _Player;
		declare netread Boolean Net_Lobby_IsBlocked for _Player;
		declare netread Boolean Net_Lobby_SelectedForMatch for _Player;
		
		if (Net_Lobby_IsBlocked) Quad_Status.BgColor = <0., 0., 0.>;
		// @specstart
		//else if (_Player.User.RequestsSpectate) Quad_Status.BgColor = <0.8, 0., 0.>;
		else if (!Private_Lobby_IsReady((_Player as CSmPlayer))) Quad_Status.BgColor = <0.8, 0., 0.>; 
		// @specend
		else if (Net_Lobby_SelectedForMatch) Quad_Status.BgColor = <1., 0.9, 0.>;
		else if (!Net_Lobby_AlliesAreReady) Quad_Status.BgColor = <0.8, 0.3, 0.>;
		else Quad_Status.BgColor = <0., 0.8, 0.>;
	}
	if (Quad_CountryFlag != Null) Quad_CountryFlag.ImageUrl = _Player.User.CountryFlagUrl;
	if (Label_Name != Null) Label_Name.Value = _Player.User.Name;
	if (Quad_Echelon != Null) Quad_Echelon.ImageUrl = GetEchelonPath(_Player.User.Echelon);
	if (Label_Echelon != Null) Label_Echelon.Value = TL::ToText(GetEchelon(_Player.User.Echelon));
}

Void UpdatePlayersList() {
	declare Integer[CSmPlayer] ToSort;
	declare Lobby_ReadyNb for UI = 0;
	Lobby_ReadyNb = 0;
	foreach (Player in Players) {
		declare netread Boolean Net_Lobby_AlliesAreReady for Player;
		declare netread Boolean Net_Lobby_IsBlocked for Player;
		declare netread Boolean Net_Lobby_SelectedForMatch for Player;
		declare netread Integer[Text] Net_Lobby_AlliesLogins for UI;
		
		if (Player.Login == InputPlayer.Login) {
			ToSort[Player] = 0;
		} else if (Net_Lobby_AlliesLogins.existskey(Player.Login)) {
			if (Net_Lobby_AlliesLogins[Player.Login] == {{{C_AllyStatus_Validated}}}) ToSort[Player] = 1;
			else ToSort[Player] = 2;
		// @specstart
		//} else if (Player.User.RequestsSpectate) {
		} else if (!Private_Lobby_IsReady(Player)) {
		// @specend
			ToSort[Player] = 6;
		} else if (!Net_Lobby_AlliesAreReady) {
			ToSort[Player] = 5;
		} else if (Net_Lobby_IsBlocked) {
			ToSort[Player] = 7;
		} else if (Net_Lobby_SelectedForMatch) {
			ToSort[Player] = 3;
		} else {
			ToSort[Player] = 4;
		}
		
		if (Player.User.IsFakeUser) {
			ToSort[Player] = ToSort[Player] * 1000000 - GetEchelon(Player.User.Echelon) * 10000;
		} else {
			ToSort[Player] = ToSort[Player] * 1000000 - ML::NearestInteger(Player.User.LadderPoints);
		}
		
		// @specstart
		//if (!Player.User.RequestsSpectate && Net_Lobby_AlliesAreReady) {
		if (Private_Lobby_IsReady(Player) && Net_Lobby_AlliesAreReady) {
		// @specend
			Lobby_ReadyNb += 1;
		}
	}
	
	declare CSmPlayer[] SortedPlayers;
	ToSort = ToSort.sort();
	foreach (Player => Status in ToSort) {
		SortedPlayers.add(Player);
	}
	
	declare Frame_PlayersList <=> (Page.GetFirstChild("Frame_PlayersList") as CMlFrame);
	Page.GetClassChildren("Frame_PlayerCard", Frame_PlayersList, False);
	foreach (Control in Page.GetClassChildren_Result) {
		declare Frame_PlayerCard <=> (Control as CMlFrame);
		declare Count = G_PageStart + TL::ToInteger(Frame_PlayerCard.ControlId);
		
		if (SortedPlayers.existskey(Count-1)) {
			Frame_PlayerCard.Visible = True;
			UpdatePlayerCard(Frame_PlayerCard, SortedPlayers[Count-1]);
		} else {
			Frame_PlayerCard.Visible = False;
		}
	}
}

Void UpdatePager(Integer _Shift) {
	declare NewPageStart = G_PageStart + (_Shift * {{{PlayersNb}}});
	if (NewPageStart < 0) NewPageStart = 0;
	else if (NewPageStart > Players.count - 1) NewPageStart = G_PageStart;
	G_PageStart = NewPageStart;
	if (G_PageStart > Players.count) G_PageStart = Players.count - 1;
	
	declare Frame_Pager <=> (Page.GetFirstChild("Frame_Pager") as CMlFrame);
	if (Players.count > {{{PlayersNb}}}) {
		Frame_Pager.Visible = True;
	} else {
		Frame_Pager.Visible = False;
		G_PageStart = 0;
	}
	
	declare Button_PagerNext <=> (Frame_Pager.GetFirstChild("Button_PagerNext") as CMlQuad);
	declare Button_PagerPrev <=> (Frame_Pager.GetFirstChild("Button_PagerPrev") as CMlQuad);
	if (G_PageStart <= 0) Button_PagerPrev.Substyle = "ArrowDisabled";
	else Button_PagerPrev.Substyle = "ArrowPrev";
	if (G_PageStart + {{{PlayersNb}}} >= Players.count) Button_PagerNext.Substyle = "ArrowDisabled";
	else Button_PagerNext.Substyle = "ArrowNext";
	
	UpdatePlayersList();
}

Void UpdateHelpMessage(Integer _Mode) {
	declare Label_Help <=> (Page.GetFirstChild("Label_Help") as CMlLabel);
	
	declare netread Integer[Text] Net_Lobby_AlliesLogins for UI;
	
	if (_Mode == {{{C_Matchmaking_UniversalLobby}}}) {
		if (Net_Lobby_AlliesLogins.count > 0) {
			Label_Help.Value = "{{{_("Click on a player to invite them to your party.")}}}";
		} else {
			Label_Help.Value = "{{{_("Click on a player to join their party.")}}}";
		}
	} else {
		Label_Help.Value = "{{{_("Click on a player to set them as ally.")}}}";
	}
}

Void DisplayFrame(Boolean _Displayed) {
	declare Button_ShowHide <=> (Page.GetFirstChild("Button_ShowHide") as CMlQuad);
	
	if (_Displayed) {
		Button_ShowHide.ImageUrl = "{{{ButtonHideOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonHideOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="164 0 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	} else {
		Button_ShowHide.ImageUrl = "{{{ButtonShowOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonShowOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="210 0 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	}
}

main() {
	declare netread Integer Net_Lobby_SynchroServer for Teams[0];
	declare netread Integer Net_Matchmaking_Mode for Teams[0];
	declare netread Integer Net_Lobby_PlayersListUpdate for Teams[0];
	declare netwrite Integer Net_Lobby_RequestAllyUpdate for UI;
	declare netwrite Text Net_Lobby_RequestAlly for UI;
	declare netwrite Integer Net_Lobby_SynchroRequestAlly for UI;
	
	declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
	if (!MM_FrameIsDisplayed.existskey("PlayersList")) MM_FrameIsDisplayed["PlayersList"] = True;
	DisplayFrame(MM_FrameIsDisplayed["PlayersList"]);
	
	declare Lobby_ReadyNb for UI = 0;
	Lobby_ReadyNb = 0;
	
	declare PrevPlayersListUpdate = -1;
	declare PrevMatchamkingMode = -1;
	
	declare NextUpdate = 0;
	
	G_PageStart = 0;
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		LibManialink_AnimLoop();
		
		if (PrevPlayersListUpdate != Net_Lobby_PlayersListUpdate) {
			PrevPlayersListUpdate = Net_Lobby_PlayersListUpdate;
			UpdatePager(0);
		}
		
		if (PrevMatchamkingMode != Net_Matchmaking_Mode) {
			PrevMatchamkingMode = Net_Matchmaking_Mode;
			UpdateHelpMessage(Net_Matchmaking_Mode);
		}
		
		if (Now >= NextUpdate) {
			NextUpdate = Now + 1000;
			foreach (Player in Players) {
				// @specstart
				//declare PrevRequestsSpectate for Player = False;
				declare PrevIsReady for Player = True;
				// @specend
				declare PrevAlliesAreReady for Player = False;
				declare netread Boolean Net_Lobby_AlliesAreReady for Player;
				// @specstart
				/*if (PrevRequestsSpectate != Player.User.RequestsSpectate || PrevAlliesAreReady != Net_Lobby_AlliesAreReady) {
					PrevRequestsSpectate = Player.User.RequestsSpectate;*/
				if (PrevIsReady != Private_Lobby_IsReady(Player) || PrevAlliesAreReady != Net_Lobby_AlliesAreReady) {
					PrevIsReady = Private_Lobby_IsReady(Player);
				// @specend
					PrevAlliesAreReady = Net_Lobby_AlliesAreReady;
					UpdatePager(0);
				}
			}
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Player") {
					declare Text Lobby_PlayerLogin for Event.Control;
					Net_Lobby_SynchroRequestAlly = Net_Lobby_SynchroServer;
					Net_Lobby_RequestAllyUpdate = Now;
					Net_Lobby_RequestAlly = Lobby_PlayerLogin;
				} else if (Event.ControlId == "Button_PagerNext") {
					UpdatePager(1);
				} else if (Event.ControlId == "Button_PagerPrev") {
					UpdatePager(-1);
				} else if (Event.ControlId == "Button_ShowHide") {
					declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
					MM_FrameIsDisplayed["PlayersList"] = !MM_FrameIsDisplayed["PlayersList"];
					DisplayFrame(MM_FrameIsDisplayed["PlayersList"]);
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the masters list manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLMastersList() {
	declare SizeX = 52.;
	declare SizeY = 127.;
	
	declare Background = "file://Media/Manialinks/Common/Lobbies/side-frame.png";
	declare Echelon0 = "file://Media/Manialinks/Common/Echelons/echelon0.dds";
	declare ButtonHideOn = "file://Media/Manialinks/Shootmania/Common/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Shootmania/Common/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Shootmania/Common/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Shootmania/Common/button-show.png";
	
	declare PlayersNb = 20;
	declare PlayersList = "";
	declare PCSizeY = 5.;
	for (I, 1, PlayersNb) {
		declare PosY = (I - 1) * -PCSizeY;
		PlayersList ^= """<frameinstance posn="0 {{{PosY}}}" hidden="1" modelid="Framemodel_PlayerCard" id="{{{I}}}" />""";
	}
	
	return """
<manialink version="1" name="ModeMatchmaking:MastersList">
<framemodel class="Frame_PlayerCard" id="Framemodel_PlayerCard">
	<quad posn="0 0 -1" sizen="45 {{{PCSizeY}}}" valign="center" bgcolor="3337" bgcolorfocus="9997" scriptevents="1" id="Button_Player" />
	<quad sizen="2 {{{PCSizeY}}}" valign="center" bgcolor="0007" opacity="0.8" id="Quad_Status" />
	<quad posn="3 0" sizen="{{{PCSizeY}}} {{{PCSizeY}}}" scale="0.9" valign="center" id="Quad_CountryFlag" />
	<label posn="8 0" sizen="32 4" valign="center2" textsize="1" textemboss="1" id="Label_Name" />
	<frame posn="{{{SizeX-9.2}}} {{{PCSizeY/2.}}} 1" scale="0.29">
		<quad sizen="14.1551 17.6938" halign="center" image="{{{Echelon0}}}" id="Quad_Echelon" />
		<label posn="0 -10" sizen="10 10" halign="center" valign="center" style="TextRaceMessageBig" text="0" id="Label_Echelon" />
	</frame>
</framemodel>
<frame posn="-165 0 10" id="Frame_Global">
	<quad posn="0 0 -1" sizen="{{{SizeX}}} {{{SizeY}}}" valign="center" image="{{{Background}}}" />
	<frame posn="{{{SizeX*0.99}}} {{{SizeY*0.42}}} -2" id="Frame_ShowHide">
		<quad sizen="5 6" image="{{{ButtonHideOff}}}" imagefocus="{{{ButtonHideOn}}}" scriptevents="1" id="Button_ShowHide" />
	</frame>
	<frame posn="{{{SizeX/2.+1.}}} {{{SizeY/2.}}}">
		<frame id="Frame_Title">
			<label posn="0 -6" sizen="{{{SizeX-8}}} 4" halign="center" style="TextRaceMessage" textopacity="0.9" text="Masters" />
		</frame>
		<frame posn="{{{-SizeX/2.+4.}}} -18 2" id="Frame_PlayersList">
			{{{PlayersList}}}
		</frame>
		<frame posn="0 -119.2" hidden="1" id="Frame_Pager">
			<quad posn="-5 0" sizen="9 9" halign="right" valign="center" style="Icons64x64_1" substyle="ArrowPrev" scriptevents="1" id="Button_PagerPrev" />
			<quad posn="5 0" sizen="9 9" valign="center" style="Icons64x64_1" substyle="ArrowNext" scriptevents="1" id="Button_PagerNext" />
			<label sizen="10 0" halign="center" valign="center2" id="Label_Pager" />
		</frame>
	</frame>
</frame>
<script><!--
#Include "TextLib" as TL
#Include "MathLib" as ML

{{{Manialink::Animations(["EaseOutExp"])}}}

Text GetEchelonPath(Text _Echelon) {
	return "file://Media/Manialinks/Common/Echelons/echelon"^_Echelon^".dds";
}

Void UpdatePlayerCard(Integer _Slot, CMlFrame _Frame) {
	declare netread Text[Integer][] Net_Lobby_Masters for Teams[0];
	
	declare Key = _Slot;
	if (_Frame == Null || !Net_Lobby_Masters.existskey(Key)) return;
	
	declare Quad_CountryFlag<=> (_Frame.GetFirstChild("Quad_CountryFlag")	as CMlQuad);
	declare Label_Name		<=> (_Frame.GetFirstChild("Label_Name")			as CMlLabel);
	declare Quad_Echelon	<=> (_Frame.GetFirstChild("Quad_Echelon")		as CMlQuad);
	declare Label_Echelon	<=> (_Frame.GetFirstChild("Label_Echelon")		as CMlLabel);
	
	if (Quad_CountryFlag != Null) Quad_CountryFlag.ImageUrl = Net_Lobby_Masters[Key][{{{C_Master_Country}}}];
	if (Label_Name != Null) Label_Name.Value = Net_Lobby_Masters[Key][{{{C_Master_Name}}}];
	if (Quad_Echelon != Null) Quad_Echelon.ImageUrl = GetEchelonPath(Net_Lobby_Masters[Key][{{{C_Master_Echelon}}}]);
	if (Label_Echelon != Null) Label_Echelon.Value = Net_Lobby_Masters[Key][{{{C_Master_Echelon}}}];
	
	_Frame.Visible = True;
}

Void UpdateMastersList() {
	declare Frame_PlayersList <=> (Page.GetFirstChild("Frame_PlayersList") as CMlFrame);
	
	foreach (Key => Control in Frame_PlayersList.Controls) {
		Control.Visible = False;
		UpdatePlayerCard(Key, (Control as CMlFrame));
	}
}

Void DisplayFrame(Boolean _Displayed) {
	declare Button_ShowHide <=> (Page.GetFirstChild("Button_ShowHide") as CMlQuad);
	
	if (_Displayed) {
		Button_ShowHide.ImageUrl = "{{{ButtonHideOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonHideOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="-164 0 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	} else {
		Button_ShowHide.ImageUrl = "{{{ButtonShowOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonShowOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="-210 0 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	}
}

main() {
	declare netread Integer Net_Lobby_MastersUpdate for Teams[0];
	
	declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
	if (!MM_FrameIsDisplayed.existskey("MastersList")) MM_FrameIsDisplayed["MastersList"] = True;
	DisplayFrame(MM_FrameIsDisplayed["MastersList"]);
	
	declare PrevMastersUpdate = -1;
	
	while (True) {
		yield;
		if (!PageIsVisible || InputPlayer == Null) continue;
		
		LibManialink_AnimLoop();
		
		if (PrevMastersUpdate != Net_Lobby_MastersUpdate) {
			PrevMastersUpdate = Net_Lobby_MastersUpdate;
			UpdateMastersList();
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_ShowHide") {
					declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
					MM_FrameIsDisplayed["MastersList"] = !MM_FrameIsDisplayed["MastersList"];
					DisplayFrame(MM_FrameIsDisplayed["MastersList"]);
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the header manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLHeader() {
	declare Hidden = "0";
	if (S_LobbyDisableUI) Hidden = "1";
	
	declare SizeX = 91.;
	declare SizeY = 27.;
	
	declare Background = "file://Media/Manialinks/Common/Lobbies/header.png";
	
	return """
<manialink version="1" name="ModeMatchmaking:Header">
<frame posn="0 86" id="Frame_Global" hidden="{{{Hidden}}}">
	<quad posn="0 0 -1" sizen="{{{SizeX}}} {{{SizeY}}}" halign="center" image="{{{Background}}}" />
	<label posn="0 -4" sizen="{{{SizeX-7}}} {{{SizeY}}}" halign="center" style="TextRaceMessage" textsize="3" id="Label_ServerName" />
	<label posn="{{{SizeX/2.-5.}}} -16" sizen="{{{SizeX/3.}}} 4" halign="right" valign="center" style="TextRaceMessage" textsize="2" opacity="0.75" id="Label_WaitingTime" />
	<label posn="{{{SizeX/2.-5.}}} -18" sizen="{{{SizeX/2.}}} 4" halign="right" valign="top" style="TextRaceMessage" scale="0.5" opacity="0.5" text="{{{_("Average waiting time")}}}" />
	<label posn="{{{-SizeX/2.+5.}}} -16" sizen="{{{SizeX/3.}}} 4" valign="center" style="TextRaceMessage" textsize="2" opacity="0.75" id="Label_PlayersNb" />
	<label posn="{{{-SizeX/2.+5.}}} -18" sizen="{{{SizeX/2.}}} 4" valign="top" style="TextRaceMessage" scale="0.5" opacity="0.5" id="Label_ReadyNb" />
	<label posn="0 -27" halign="center" style="TextRaceMessageBig" textsize="3" id="Label_Message" />
</frame>
<script><!--
#Include "TextLib" as TL

// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

main() {
	declare Label_ServerName	<=> (Page.GetFirstChild("Label_ServerName")		as CMlLabel);
	declare Label_PlayersNb		<=> (Page.GetFirstChild("Label_PlayersNb")		as CMlLabel);
	declare Label_ReadyNb		<=> (Page.GetFirstChild("Label_ReadyNb")		as CMlLabel);
	declare Label_WaitingTime	<=> (Page.GetFirstChild("Label_WaitingTime")	as CMlLabel);
	declare Label_Message		<=> (Page.GetFirstChild("Label_Message")		as CMlLabel);
	
	declare netread Integer Net_Lobby_PlayersNb for Teams[0];
	declare netread Integer Net_Lobby_AverageWaitingTime for Teams[0];
	
	declare Lobby_ReadyNb for UI = 0;
	
	declare PrevServerName = "";
	declare PrevPlayersNb = -1;
	declare PrevReadyNb = -1;
	declare PrevAverageWaitingTime = -2;
	declare PrevAlliesAreReady = True;
	declare PrevMessage = "";
	// @specstart
	//declare PrevIsSpectatorMode = False;
	declare PrevIsReady = True;
	// @specend
	
	while (True) {
		sleep(100);
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		if (PrevServerName != CurrentServerName) {
			PrevServerName = CurrentServerName;
			Label_ServerName.Value = CurrentServerName;
		}
		
		if (PrevPlayersNb != Net_Lobby_PlayersNb) {
			PrevPlayersNb = Net_Lobby_PlayersNb;
			Label_PlayersNb.Value = TL::Compose("%1 playing", TL::ToText(Net_Lobby_PlayersNb));
		}
		
		if (PrevReadyNb != Lobby_ReadyNb) {
			PrevReadyNb = Lobby_ReadyNb;
			Label_ReadyNb.Value = TL::Compose("%1 ready", TL::ToText(Lobby_ReadyNb));
		}
		
		if (PrevAverageWaitingTime != Net_Lobby_AverageWaitingTime) {
			PrevAverageWaitingTime = Net_Lobby_AverageWaitingTime;
			if (Net_Lobby_AverageWaitingTime <= 0) Label_WaitingTime.Value = "- min";
			else if (Net_Lobby_AverageWaitingTime < 60000) Label_WaitingTime.Value = TL::Compose("%1 sec", TL::ToText(Net_Lobby_AverageWaitingTime/1000+1));
			else Label_WaitingTime.Value = TL::Compose("%1 min", TL::ToText(Net_Lobby_AverageWaitingTime/60000));
		}
		
		// @specstart
		/*if (PrevMessage != UI.StatusMessage || PrevIsSpectatorMode != IsSpectatorMode) {
			PrevIsSpectatorMode = IsSpectatorMode;*/
		if (PrevMessage != UI.StatusMessage || PrevIsReady != Private_Lobby_IsReady()) {
			PrevIsReady = Private_Lobby_IsReady();
		// @specend
			PrevMessage = UI.StatusMessage;
			// @specstart
			//if (UI.StatusMessage != "" || IsSpectatorMode) Label_Message.Visible = False;
			if (UI.StatusMessage != "" || !Private_Lobby_IsReady()) Label_Message.Visible = False;
			// @specend
			else Label_Message.Visible = True;
		}
		
		declare netread Boolean Net_Lobby_AlliesAreReady for InputPlayer;
		if (PrevAlliesAreReady != Net_Lobby_AlliesAreReady) {
			PrevAlliesAreReady = Net_Lobby_AlliesAreReady;
			if (Net_Lobby_AlliesAreReady) Label_Message.Value = "";
			else Label_Message.Value = "{{{_("Waiting for your allies to be ready.")}}}";
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the send to server manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLSendToServer() {
	return """
<script><!--
declare netread Integer Net_Lobby_JoinLinkUpdate for UI;
declare netread Text Net_Lobby_JoinLink for UI;

declare PrevUpdate = Net_Lobby_JoinLinkUpdate;

while (True) {
	yield;
	if (InputPlayer == Null || !PageIsVisible) continue;
	
	if (PrevUpdate != Net_Lobby_JoinLinkUpdate) {
		PrevUpdate = Net_Lobby_JoinLinkUpdate;
		
		if ({{{S_MatchmakingLogMiscDebug}}}) {
			log(Now^"> [CLIENT] "^InputPlayer.Login^" > OpenLink: "^Net_Lobby_JoinLink);
		}
		
		if (Net_Lobby_JoinLink != "") {
			OpenLink(Net_Lobby_JoinLink, CMlScript::LinkType::ManialinkBrowser);
		}
	}
}
--></script>""";
}

// ---------------------------------- //
/** Create the send to server manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLRules() {
	declare ManialinkRules = "";
	---ManialinkRules---
	
	declare ML = "";
	if (ManialinkRules != "") {
		ML ^= """
<manialink version="1" name="ModeMatchmaking:Rules">
<frame posn="0 0 30" hidden="1" id="Frame_Global">
	{{{ManialinkRules}}}
</frame>
<script><!--
main() {
	declare Frame_Global <=> (Page.GetFirstChild("Frame_Global") as CMlFrame);
	
	declare Lobby_RulesAreVisible for UI = False;
	Lobby_RulesAreVisible = False;
	
	declare PrevRulesAreVisible = False;
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		if (PrevRulesAreVisible != Lobby_RulesAreVisible) {
			PrevRulesAreVisible = Lobby_RulesAreVisible;
			
			Frame_Global.Visible = Lobby_RulesAreVisible;
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Back") {
					Lobby_RulesAreVisible = False;
				}
			}
		}
	}
}
--></script>
</manialink>""";
	}
	
	return ML;
}

// ---------------------------------- //
/** Create the starting match manialink
 *
 *	@return		The manialink
 */
Text Private_MM_GetMLStartingMatch() {
	declare ReqPlayersNb = 0;
	foreach (PlayersNb in G_Matchmaking_Format) ReqPlayersNb += PlayersNb;
	
	declare ImgPlayer = "file://Media/Manialinks/Shootmania/Common/DefendersLeft.dds";
	declare ImgSize = 8.;
	declare XMargin = 1.;
	
	declare PlayersList = "";
	for(I, 0, ReqPlayersNb-1) {
		declare PosX = (-I*(ImgSize + XMargin)) - (ImgSize/2.);
		declare PosY = -ImgSize/2.;
		
		PlayersList ^= """<quad posn="{{{PosX}}} {{{PosY}}}" sizen="{{{ImgSize}}} {{{ImgSize}}}" halign="center" valign="center" image="{{{ImgPlayer}}}" colorize="777" scale="0.5" class="Quad_Player" id="{{{I}}}" />""";
	}
	
	return """
<manialink version="1" name="ModeMatchmaking:StartingMatch">
<frame posn="155 -79" id="Frame_Waiting">
	<label halign="right" valign="bottom" textsize="3" textemboss="1" id="Label_Waiting" />
	<frame>
		{{{PlayersList}}}
	</frame>
</frame>
<frame posn="155 -86" hidden="1" id="Frame_Ready">
	<label halign="right" valign="bottom" textsize="3" textemboss="1" text="{{{_("The match will begin shortly ...")}}}" />
</frame>
<script><!--
#Include "TextLib" as TL

main() {
	declare Label_Waiting	<=> (Page.GetFirstChild("Label_Waiting")	as CMlLabel);
	declare Frame_Waiting	<=> (Page.GetFirstChild("Frame_Waiting")	as CMlFrame);
	declare Frame_Ready		<=> (Page.GetFirstChild("Frame_Ready")		as CMlFrame);
	
	declare CMlQuad[Integer] Quads_Player;
	Page.GetClassChildren("Quad_Player", Frame_Waiting, True);
	foreach (Control in Page.GetClassChildren_Result) {
		declare Key = TL::ToInteger(Control.ControlId);
		Quads_Player[Key] = (Control as CMlQuad);
	}
	
	Label_Waiting.SetText(_("Waiting for players"));
	
	declare netread Integer Net_MM_ReadyPlayersNb for Teams[0];
	declare netread Integer Net_MM_ReadyPlayersMax for Teams[0];
	declare PrevReadyPlayersNb = -1;
	declare PrevReadyPlayersMax = -1;
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		if (PrevReadyPlayersMax != Net_MM_ReadyPlayersMax) {
			PrevReadyPlayersMax = Net_MM_ReadyPlayersMax;
			
			foreach (Key => Quad_Player in Quads_Player) {
				if (Key <= Net_MM_ReadyPlayersMax - 1) {
					Quad_Player.Visible = True;
				} else {
					Quad_Player.Visible = False;
				}
			}
		}
		
		if (PrevReadyPlayersNb != Net_MM_ReadyPlayersNb) {
			PrevReadyPlayersNb = Net_MM_ReadyPlayersNb;
			
			foreach (Key => Quad_Player in Quads_Player) {
				if (Key <= Net_MM_ReadyPlayersNb - 1) {
					Quad_Player.Colorize = <1., 1., 1.>;
					Quad_Player.RelativeScale = 1.;
				} else {
					Quad_Player.Colorize = <0.5, 0.5, 0.5>;
					Quad_Player.RelativeScale = 0.5;
				}
			}
			
			if (Net_MM_ReadyPlayersNb >= Net_MM_ReadyPlayersMax) {
				PlayUiSound(::EUISound::Custom4, 1, 0.75);
			} else {
				PlayUiSound(::EUISound::Custom4, 0, 0.75);
			}
		}
		
		if (!Frame_Waiting.Visible && Net_MM_ReadyPlayersNb < Net_MM_ReadyPlayersMax) {
			Frame_Waiting.Show();
			Frame_Ready.Hide();
		} else if (!Frame_Ready.Visible && Net_MM_ReadyPlayersNb >= Net_MM_ReadyPlayersMax) {
			Frame_Waiting.Hide();
			Frame_Ready.Show();
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the versus manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLVersus() {
	declare Background = "file://Media/Manialinks/Common/Lobbies/versus-bg.dds";
	declare Background_PlayerCard = "file://Media/Manialinks/Common/Lobbies/PlayerCardBg.dds";
	declare ButtonQuitOn = "file://Media/Manialinks/Common/Lobbies/small-button-RED-ON.dds";
	declare ButtonQuitOff = "file://Media/Manialinks/Common/Lobbies/small-button-RED.dds";
	declare Echelon0 = "file://Media/Manialinks/Common/Echelons/echelon0.dds";
	declare IsAlly = "file://Media/Manialinks/Common/AllyYes.dds";
	
	declare SlotsNb = 3;
	declare PlayersList = "";
	for (I, 0, SlotsNb-1) {
		declare PosY = -18. * I;
		PlayersList ^= """<frameinstance posn="0 {{{PosY}}}" scale="0.7" modelid="Framemodel_PlayerCard" id="{{{I}}}" />""";
	}
	
	return """
<manialink version="1" name="ModeMatchmaking:VersusScreen">
<framemodel id="Framemodel_PlayerCard">
	<frame posn="-40 10" hidden="1" id="Frame_PlayerCard">
		<quad sizen="80 20 -1" image="{{{Background_PlayerCard}}}" />
		<quad posn="0.75 -10 1" sizen="18.5 18.5" valign="center" bgcolor="aaa" id="Quad_Avatar" />
		<label posn="22 -4" sizen="38 4" style="TextRaceMessage" textsize="3.5" id="Label_Name" />
		<frame posn="22 -10.6 1" hidden="1" id="Frame_Ally">
			<label posn="5 0" sizen="30 4" valign="center2" style="TextRaceMessage" textsize="1" text="{{{_("Ally")}}}" />
			<quad sizen="4 4" valign="center" image="{{{IsAlly}}}" />
		</frame>
		<label posn="27 -15" sizen="30 4" valign="center2" style="TextRaceMessage" textsize="1" id="Label_Rank" />
		<quad posn="22 -15 1" sizen="4 4" valign="center" id="Quad_CountryFlag" />
		<frame posn="72 0 1" scale="1.13">
			<quad sizen="14.1551 17.6938" halign="center" image="{{{Echelon0}}}" id="Quad_Echelon" />
			<label posn="0 -3.6" sizen="14 0" halign="center" style="TextRaceMessage" textsize="0.5" text="Echelon" />
			<label posn="0 -10.6" sizen="10 10" halign="center" valign="center" style="TextRaceMessageBig" text="0" id="Label_Echelon" />
		</frame>
	</frame>
</framemodel>
<frame posn="0 -12" hidden="1" id="Frame_Global">
	<frame posn="0 0 -1">
		<quad sizen="190 190" halign="center" valign="center" image="{{{Background}}}" />
	</frame>
	<frame>
		<label posn="0 40" sizen="200 20" halign="center" valign="center2" style="TextRaceMessageBig" textsize="2" id="Label_Info" />
		<label posn="0 40" sizen="200 20" halign="center" valign="center2" style="TextRaceMessageBig" textsize="2" text="{{{_("You are being transferred. Please wait.")}}}" id="Label_Transfert" />
		<frame posn="-45 25" id="Frame_Clan1">
			{{{PlayersList}}}
		</frame>
		<frame posn="45 25" id="Frame_Clan2">
			{{{PlayersList}}}
		</frame>
		<frame posn="0 6" hidden="1" id="Frame_Pager">
			<quad posn="-1 0" sizen="9 9" halign="right" valign="center" style="Icons64x64_1" substyle="ArrowUp" scriptevents="1" id="Button_PagerPrev" />
			<quad posn="1 0" sizen="9 9" valign="center" style="Icons64x64_1" substyle="ArrowDown" scriptevents="1" id="Button_PagerNext" />
		</frame>
		<frame posn="88 -35">
			<label sizen="50 6" halign="right" opacity="0.7" textsize="1" textemboss="1" id="Label_MatchId" />
		</frame>
	</frame>
	<frame posn="0 -50" hidden="1" id="Frame_Cancel">
		<!-- @specstart -->
		<quad sizen="35 8" scale="1.3" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" scriptevents="1" id="Button_Cancel" />
		<!-- @specend -->
		<label sizen="35 8" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" text="{{{_("F6 to cancel")}}}" id="Label_Quit" />
	</frame>
</frame>
<script><!--
#Include "TextLib" as TL

declare Integer G_PageStart;

// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

CUser GetUser(Text _Login) {
	foreach (Player in Players) {
		if (Player.Login == _Login) return Player.User;
	}
	
	return Null;
}

Integer GetEchelon(CUser::EEchelon _Echelon) {
	switch (_Echelon) {
		case CUser::EEchelon::Bronze1	: return 1;
		case CUser::EEchelon::Bronze2	: return 2;
		case CUser::EEchelon::Bronze3	: return 3;
		case CUser::EEchelon::Silver1	: return 4;
		case CUser::EEchelon::Silver2	: return 5;
		case CUser::EEchelon::Silver3	: return 6;
		case CUser::EEchelon::Gold1		: return 7;
		case CUser::EEchelon::Gold2		: return 8;
		case CUser::EEchelon::Gold3		: return 9;
	}
	
	return 0;
}

Text GetEchelonPath(CUser::EEchelon _Echelon) {
	return "file://Media/Manialinks/Common/Echelons/echelon"^GetEchelon(_Echelon)^".dds";
}

Void UpdatePlayerSlot(CMlFrame _Frame, CUser _User) {
	if (_Frame == Null) return;
	
	if (_User == Null) {
		_Frame.Visible = False;
		return;
	} else {
		_Frame.Visible = True;
	}
	
	declare Quad_Avatar			<=> (_Frame.GetFirstChild("Quad_Avatar")		as CMlQuad);
	declare Label_Name			<=> (_Frame.GetFirstChild("Label_Name")			as CMlLabel);
	declare Label_Rank			<=> (_Frame.GetFirstChild("Label_Rank")			as CMlLabel);
	declare Quad_CountryFlag	<=> (_Frame.GetFirstChild("Quad_CountryFlag")	as CMlQuad);
	declare Quad_Echelon		<=> (_Frame.GetFirstChild("Quad_Echelon")		as CMlQuad);
	declare Label_Echelon		<=> (_Frame.GetFirstChild("Label_Echelon")		as CMlLabel);
	declare Frame_Ally			<=> (_Frame.GetFirstChild("Frame_Ally")			as CMlFrame);
	
	if (Quad_Avatar != Null)		Quad_Avatar.ImageUrl = "file://Avatars/"^_User.Login^"/Default";
	if (Label_Name != Null)			Label_Name.Value = _User.Name;
	if (Quad_CountryFlag != Null)	Quad_CountryFlag.ImageUrl = _User.CountryFlagUrl;
	if (Quad_Echelon != Null)		Quad_Echelon.ImageUrl = GetEchelonPath(_User.Echelon);
	if (Label_Echelon != Null)		Label_Echelon.Value = TL::ToText(GetEchelon(_User.Echelon));
	if (Label_Rank != Null) {
		declare Zone = _("Other");
		declare ZoneArray = TL::Split("|", _User.LadderZoneName);
		if (ZoneArray.existskey(2)) Zone = ZoneArray[2];
		if (_User.LadderRank > 0) Label_Rank.Value = TL::Compose("%1: %2", Zone, TL::ToText(_User.LadderRank));
		else Label_Rank.Value = TL::Compose("%1: %2", Zone, _("Not ranked"));
	}
	
	declare netread Boolean[Text] Net_Lobby_VersusAllied for Teams[0];
	if (Net_Lobby_VersusAllied.existskey(_User.Login)) {
		if (Net_Lobby_VersusAllied[_User.Login]) {
			Frame_Ally.Visible = True;
		} else {
			Frame_Ally.Visible = False;
		}
	} else {
		Frame_Ally.Visible = False;
	}
}

Void UpdatePlayersList() {
	declare netread Integer[Text] Net_Lobby_VersusPlayers for UI;
	
	declare ClansUsers = Real[CUser][Integer];
	foreach (Login => Clan in Net_Lobby_VersusPlayers) {
		if (!ClansUsers.existskey(Clan)) ClansUsers[Clan] = Real[CUser];
		declare CUser User;
		declare Real LadderPoints;
		
		User <=> GetUser(Login);
		if (User != Null) {
			LadderPoints = User.LadderPoints;
			
			// @Debug
			if (User.IsFakeUser) LadderPoints = GetEchelon(User.Echelon) * 10000.;
			
			ClansUsers[Clan][User] = -LadderPoints;
		}
	}
	
	declare Frames_Clan = [
		0 => (Page.GetFirstChild("Frame_Clan1") as CMlFrame), 
		1 => (Page.GetFirstChild("Frame_Clan2") as CMlFrame)
	];
	
	for (I, 0, 1) {
		if (!ClansUsers.existskey(I+1)) continue;
		
		declare ClansUsersSorted = ClansUsers[I+1].sort();
		
		declare ClanUsers = CUser[];
		foreach (User => LadderPoints in ClansUsersSorted) {
			ClanUsers.add(User);
		}
		
		foreach (Control in Frames_Clan[I].Controls) {
			declare Frame_PlayerCard <=> ((Control as CMlFrame).GetFirstChild("Frame_PlayerCard") as CMlFrame);
			declare Slot = G_PageStart + TL::ToInteger(Control.ControlId);
			
			if (ClanUsers.existskey(Slot)) UpdatePlayerSlot(Frame_PlayerCard, ClanUsers[Slot]);
			else UpdatePlayerSlot(Frame_PlayerCard, Null);
		}
	}
	
	declare Label_MatchId <=> (Page.GetFirstChild("Label_MatchId") as CMlLabel);
	declare netread Text Net_Lobby_MatchId for UI;
	Label_MatchId.Value = "match id : #"^Net_Lobby_MatchId;
}

Void UpdatePager(Integer _Shift) {
	declare netread Integer[Text] Net_Lobby_VersusPlayers for UI;
	declare MaxPlayers = Integer[Integer];
	declare Max = 0;
	foreach (Login => Clan in Net_Lobby_VersusPlayers) {
		if (!MaxPlayers.existskey(Clan)) MaxPlayers[Clan] = 0;
		MaxPlayers[Clan] += 1;
		if (MaxPlayers[Clan] > Max) Max = MaxPlayers[Clan];
	}
	
	declare Frame_Pager <=> (Page.GetFirstChild("Frame_Pager") as CMlFrame);
	if (Max > {{{SlotsNb}}}) {
		Frame_Pager.Visible = True;
	} else if (Max <= {{{SlotsNb}}}) {
		Frame_Pager.Visible = False;
		G_PageStart = 0;
	}
	
	declare NewPageStart = G_PageStart + (_Shift * {{{SlotsNb}}});
	if (NewPageStart < 0) NewPageStart = 0;
	else if (NewPageStart > Max - 1) NewPageStart = G_PageStart;
	G_PageStart = NewPageStart;
	if (G_PageStart > Max) G_PageStart = Max - 1;
	
	declare Button_PagerNext <=> (Frame_Pager.GetFirstChild("Button_PagerNext") as CMlQuad);
	declare Button_PagerPrev <=> (Frame_Pager.GetFirstChild("Button_PagerPrev") as CMlQuad);
	if (G_PageStart <= 0) Button_PagerPrev.Substyle = "ArrowDisabled";
	else Button_PagerPrev.Substyle = "ArrowUp";
	if (G_PageStart + {{{SlotsNb}}} >= Max) Button_PagerNext.Substyle = "ArrowDisabled";
	else Button_PagerNext.Substyle = "ArrowDown";
	
	UpdatePlayersList();
}

Void UpdateFormat() {
	declare netread Integer[] Net_Matchmaking_Format for Teams[0];
	
	declare Frames_Clan = [
		0 => (Page.GetFirstChild("Frame_Clan1") as CMlFrame), 
		1 => (Page.GetFirstChild("Frame_Clan2") as CMlFrame)
	];
	
	if (Net_Matchmaking_Format.count == 1) {
	
	} else if (Net_Matchmaking_Format.count == 2) {
		Frames_Clan[0].RelativePosition.X = -45.;
		Frames_Clan[1].RelativePosition.X = 45.;
		for (I, 0, 1) {
			switch (Net_Matchmaking_Format[I]) {
				case 1	: {
					Frames_Clan[I].RelativePosition.Y = 25. - 18.;
				}
				case 2	: {
					Frames_Clan[I].RelativePosition.Y = 25. - 9.;
				}
				default	: {
					Frames_Clan[I].RelativePosition.Y = 25.;
				}
			}
			
			foreach (Control in Frames_Clan[I].Controls) {
				declare Frame_PlayerCard <=> ((Control as CMlFrame).GetFirstChild("Frame_PlayerCard") as CMlFrame);
				if (TL::ToInteger(Control.ControlId) >= Net_Matchmaking_Format[I]) Frame_PlayerCard.Visible = False;
				else Frame_PlayerCard.Visible = True;
			}
		}
	} else {
		
	}
	
	UpdatePager(0);
}

main() {
	declare Frame_Global <=> (Page.GetFirstChild("Frame_Global") as CMlFrame);
	declare Frame_Cancel <=> (Page.GetFirstChild("Frame_Cancel") as CMlFrame);
	
	declare netread Integer Net_Matchmaking_FormatUpdate for Teams[0];
	declare netread Boolean Net_Lobby_ShowVersusML for UI;
	declare netread Integer Net_Lobby_VersusPlayersUpdate for UI;
	declare netread Integer Net_Lobby_VersusAlliedUpdate for Teams[0];
	declare netread Integer Net_Lobby_MatchCancellation for UI;
	declare netread Boolean Net_Lobby_AllowMatchCancel for Teams[0];
	declare netread Integer Net_Lobby_LobbyLimitMatchCancel for Teams[0];
	
	G_PageStart = 0;
	
	declare PrevFormatUpdate = -1;
	declare PrevShowVersusML = False;
	declare PrevVersusPlayersUpdate = -1;
	declare PrevVersusAlliedUpdate = -1;
	// @specstart
	//declare PrevIsSpectatorMode = False;
	declare PrevIsReady = True;
	// @specend
	declare PrevAllowMatchCancel = False;
	declare PrevMatchCancellation = -1;
	declare PrevLobbyLimitMatchCancel = -1;
		
	while (True) {
		yield;
		if (!PageIsVisible || InputPlayer == Null) continue;
		
		if (PrevShowVersusML != Net_Lobby_ShowVersusML) {
			PrevShowVersusML = Net_Lobby_ShowVersusML;
			Frame_Global.Visible = Net_Lobby_ShowVersusML;
		}
		
		// @specstart
		/*if (PrevIsSpectatorMode != IsSpectatorMode) {
			PrevIsSpectatorMode = IsSpectatorMode;
			if (IsSpectatorMode) {*/
		if (PrevIsReady != Private_Lobby_IsReady()) {
			PrevIsReady = Private_Lobby_IsReady();
			if (!Private_Lobby_IsReady()) {
		// @specend
				Frame_Global.Visible = False;
			} else {
				Frame_Global.Visible = Net_Lobby_ShowVersusML;
			}
		}
		
		if (!Frame_Global.Visible) continue;
		
		if (PrevAllowMatchCancel != Net_Lobby_AllowMatchCancel) {
			PrevAllowMatchCancel = Net_Lobby_AllowMatchCancel;
			Frame_Cancel.Visible = Net_Lobby_AllowMatchCancel;
		}
		
		if (
			Net_Lobby_AllowMatchCancel &&
			(PrevMatchCancellation != Net_Lobby_MatchCancellation || PrevLobbyLimitMatchCancel != Net_Lobby_LobbyLimitMatchCancel)
		) {
			PrevMatchCancellation = Net_Lobby_MatchCancellation;
			PrevLobbyLimitMatchCancel = Net_Lobby_LobbyLimitMatchCancel;
			
			if (Net_Lobby_MatchCancellation < Net_Lobby_LobbyLimitMatchCancel) {
				Frame_Cancel.Visible = True;
			} else {
				Frame_Cancel.Visible = False;
			}
		}
		
		if (PrevFormatUpdate != Net_Matchmaking_FormatUpdate) {
			PrevFormatUpdate = Net_Matchmaking_FormatUpdate;
			UpdateFormat();
		}
		
		if (PrevVersusPlayersUpdate != Net_Lobby_VersusPlayersUpdate) {
			PrevVersusPlayersUpdate = Net_Lobby_VersusPlayersUpdate;
			UpdatePager(0);
		}
		
		if (PrevVersusAlliedUpdate != Net_Lobby_VersusAlliedUpdate) {
			PrevVersusAlliedUpdate = Net_Lobby_VersusAlliedUpdate;
			UpdatePager(0);
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_PagerNext") {
					UpdatePager(1);
				} else if (Event.ControlId == "Button_PagerPrev") {
					UpdatePager(-1);
				}
				// @specstart
				else if (Event.ControlId == "Button_Cancel") {
					Private_Lobby_SetReady(False);
				}
				// @specend
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the substitute manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLSubstitute() {
	declare ButtonQuitOn = "file://Media/Manialinks/Common/Lobbies/small-button-RED-ON.dds";
	declare ButtonQuitOff = "file://Media/Manialinks/Common/Lobbies/small-button-RED.dds";
	
	return """
<manialink version="1" name="ModeMatchmaking:Substitute">
<frame posn="0 10" hidden="1" id="Frame_Global">
	<label halign="center" valign="center" style="TextRaceMessageBig" textsize="6" text="{{{_("You are being transferred as a substitute.")}}}" />
	<label posn="0 -8" halign="center" valign="center" style="TextRaceMessageBig" textsize="4" text="{{{_("Please wait.")}}}" id="Label_SubMessage" />
	<frame posn="0 -18" hidden="1" id="Frame_Cancel">
		<!-- @specstart -->
		<quad sizen="35 8" scale="1.3" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" scriptevents="1" id="Button_Cancel" />
		<!-- @specend -->
		<label sizen="35 8" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" text="{{{_("F6 to cancel")}}}" id="Label_Quit" />
	</frame>
</frame>
<script><!--
#Include "TextLib" as TL

// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

main() {
	declare Frame_Global <=> (Page.GetFirstChild("Frame_Global") as CMlFrame);
	declare Frame_Cancel <=> (Page.GetFirstChild("Frame_Cancel") as CMlFrame);
	declare Label_SubMessage <=> (Page.GetFirstChild("Label_SubMessage") as CMlLabel);
	
	declare netread Boolean Net_Lobby_ShowSubstituteML for UI;
	declare netread Integer Net_Lobby_MatchCancellation for UI;
	declare netread Text Net_Lobby_MatchScores for UI;
	declare netread Boolean Net_Lobby_AllowMatchCancel for Teams[0];
	declare netread Integer Net_Lobby_LobbyLimitMatchCancel for Teams[0];
	declare netread Boolean Net_Lobby_LobbyPenalizeSubstituteCancel for Teams[0];
	
	declare PrevShowSubstituteML = False;
	// @specstart
	//declare PrevIsSpectatorMode = False;
	declare PrevIsReady = True;
	// @specend
	declare PrevMatchCancellation = -1;
	declare PrevAllowMatchCancel = False;
	declare PrevLobbyLimitMatchCancel = -1;
	
	while (True) {
		yield;
		if (!PageIsVisible || InputPlayer == Null) continue;
		
		if (PrevShowSubstituteML != Net_Lobby_ShowSubstituteML) {
			PrevShowSubstituteML = Net_Lobby_ShowSubstituteML;
			Frame_Global.Visible = Net_Lobby_ShowSubstituteML;
			
			if (Net_Lobby_MatchScores == "") {
				Label_SubMessage.Value = "{{{_("Please wait.")}}}";
			} else {
				declare MatchScores = "";
				declare SplittedScores = TL::Split("-", Net_Lobby_MatchScores);
				if (SplittedScores.count == 2) MatchScores = SplittedScores[0]^" - "^SplittedScores[1];
				
				if (MatchScores != "") Label_SubMessage.Value = TL::Compose("%1 : %2", "{{{_("Current match score")}}}", MatchScores);
				else Label_SubMessage.Value = "{{{_("Please wait.")}}}";
			}
		}
		
		// @specstart
		/*if (PrevIsSpectatorMode != IsSpectatorMode) {
			PrevIsSpectatorMode = IsSpectatorMode;
			if (IsSpectatorMode) {*/
		if (PrevIsReady != Private_Lobby_IsReady()) {
			PrevIsReady = Private_Lobby_IsReady();
			if (!Private_Lobby_IsReady()) {
		// @specend
				Frame_Global.Visible = False;
			} else {
				Frame_Global.Visible = Net_Lobby_ShowSubstituteML;
			}
		}
		
		if (PrevAllowMatchCancel != Net_Lobby_AllowMatchCancel) {
			PrevAllowMatchCancel = Net_Lobby_AllowMatchCancel;
			Frame_Cancel.Visible = Net_Lobby_AllowMatchCancel;
		}
		
		if (
			Net_Lobby_AllowMatchCancel &&
			(PrevMatchCancellation != Net_Lobby_MatchCancellation || PrevLobbyLimitMatchCancel != Net_Lobby_LobbyLimitMatchCancel)
		) {
			PrevMatchCancellation = Net_Lobby_MatchCancellation;
			PrevLobbyLimitMatchCancel = Net_Lobby_LobbyLimitMatchCancel;
			
			if (Net_Lobby_MatchCancellation < Net_Lobby_LobbyLimitMatchCancel || !Net_Lobby_LobbyPenalizeSubstituteCancel) {
				Frame_Cancel.Visible = True;
			} else {
				Frame_Cancel.Visible = False;
			}
		}
		
		// @specstart
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Cancel") {
					Private_Lobby_SetReady(False);
				}
			}
		}
		// @specend
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the rooms manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLRooms() {
	declare Hidden = "0";
	if (S_LobbyDisableUI) Hidden = "1";
	
	declare SizeX = 141.;
	declare SizeY = 99.;
	
	declare Background_Global = "file://Media/Manialinks/Common/Lobbies/main-bg.png";
	declare Background_OccupiedSlot = "file://Media/Manialinks/Common/Lobbies/PlayerCardBg.dds";
	declare Echelon0 = "file://Media/Manialinks/Common/Echelons/echelon0.dds";
	declare ButtonQuitOn = "file://Media/Manialinks/Common/Lobbies/small-button-RED-ON.dds";
	declare ButtonQuitOff = "file://Media/Manialinks/Common/Lobbies/small-button-RED.dds";
	declare ButtonTopOn = "file://Media/Manialinks/Common/Lobbies/small-button-YELLOW-ON.dds";
	declare ButtonTopOff = "file://Media/Manialinks/Common/Lobbies/small-button-YELLOW.dds";
	declare ButtonBottomOn = "file://Media/Manialinks/Common/Lobbies/ready-button-GREEN-ON.dds";
	declare ButtonBottomOff = "file://Media/Manialinks/Common/Lobbies/ready-button-GREEN.dds";
	declare ButtonHideOn = "file://Media/Manialinks/Shootmania/Common/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Shootmania/Common/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Shootmania/Common/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Shootmania/Common/button-show.png";
	
	declare PlayersList = "";
	declare SlotsNb = 6;
	for (I, 0, SlotsNb-1) {
		declare PosY = I * -23;
		PlayersList^= """
<frame class="Frame_PlayerCard" hidden="1" id="{{{I}}}">
	<frameinstance modelid="Framemodel_EmptySlot" />
	<frameinstance modelid="Framemodel_OccupiedSlot" />
</frame>""";
	}
	
	return """
<manialink version="1" name="ModeMatchmaking:Rooms">
<framemodel id="Framemodel_EmptySlot">
	<frame id="Frame_EmptySlot">
		<quad sizen="80 20 -1" halign="center" valign="center" style="Bgs1" substyle="BgListLine" />
		<label sizen="75 4" halign="center" valign="center2" style="TextButtonSmall" text="{{{_("Take this slot")}}}" />
		<quad posn="0 0" sizen="80 20" halign="center" valign="center" scriptevents="1" class="Button_EmptySlot" />
	</frame>
</framemodel>
<framemodel id="Framemodel_OccupiedSlot">
	<frame posn="-40 10" hidden="1" id="Frame_OccupiedSlot">
		<quad sizen="80 20 -1" image="{{{Background_OccupiedSlot}}}" />
		<quad posn="0.75 -10 1" sizen="18.5 18.5" valign="center" bgcolor="aaa" id="Quad_Avatar" />
		<label posn="22 -4" sizen="38 4" style="TextRaceMessage" textsize="3.5" id="Label_Name" />
		<label posn="27 -15" sizen="30 4" valign="center2" style="TextRaceMessage" textsize="1" id="Label_Rank" />
		<quad posn="22 -15 1" sizen="4 4" valign="center" id="Quad_CountryFlag" />
		<frame posn="72 0 1" scale="1.13">
			<quad sizen="14.1551 17.6938" halign="center" image="{{{Echelon0}}}" id="Quad_Echelon" />
			<label posn="0 -3.6" sizen="14 0" halign="center" style="TextRaceMessage" textsize="0.5" text="Echelon" />
			<label posn="0 -10.6" sizen="10 10" halign="center" valign="center" style="TextRaceMessageBig" text="0" id="Label_Echelon" />
		</frame>
		<quad posn="0 0 3" sizen="80 20" bgcolor="333a" hidden="1" id="Quad_AllyNotValidated" />
		<quad posn="0 0" sizen="80 20" scriptevents="1" class="Button_OccupiedSlot" />
	</frame>
</framemodel>
<frame hidden="{{{Hidden}}}">
<frame posn="0 0 10" id="Frame_Global">
	<quad sizen="{{{SizeX}}} {{{SizeY}}} -1" halign="center" valign="center" image="{{{Background_Global}}}" />
	<frame posn="{{{SizeX*0.37}}} {{{SizeY*0.47}}} -2" id="Frame_ShowHide">
		<quad sizen="5 6" rot="-90" image="{{{ButtonHideOff}}}" imagefocus="{{{ButtonHideOn}}}" scriptevents="1" id="Button_ShowHide" />
	</frame>
	<frame class="View" id="Home" hidden="1">
		<label posn="0 30" sizen="{{{SizeX-10}}} 4" scale="0.9" halign="center" valign="top" textsize="5" textemboss="1" autonewline="1" maxline="3" opacity="0.9" text="{{{_("You can create your own party by clicking on the create button below.")}}}" />
		<label posn="0 10" sizen="{{{SizeX-10}}} 4" scale="0.9" halign="center" valign="top" textsize="5" textemboss="1" autonewline="1" maxline="3" opacity="0.9" text="{{{_("Or join an existing party by clicking on a player name on the right.")}}}" />
	</frame>
	<frame class="View" id="Room" hidden="1">
		<label posn="0 40" sizen="{{{SizeX}}} 4" halign="center" valign="top" textsize="3" textemboss="1" opacity="0.9" id="Label_Title" />
		<label posn="0 32" sizen="{{{SizeX}}} 4" halign="center" valign="top" textsize="5" textemboss="1" opacity="0.9" id="Label_Clan" />
		<frame posn="0 18 1" scale="0.6" id="Frame_PlayerList">
			{{{PlayersList}}}
		</frame>
		<frame posn="0 4 1" id="Frame_Pager">
			<quad posn="-52" sizen="9 9" halign="right" valign="center" style="Icons64x64_1" substyle="ArrowPrev" scriptevents="1" id="Button_PagerPrev" />
			<quad posn="52 0" sizen="9 9" valign="center" style="Icons64x64_1" substyle="ArrowNext" scriptevents="1" id="Button_PagerNext" />
		</frame>
	</frame>
	<frame class="View" id="List" hidden="1">
		
	</frame>
	<frame posn="-47 -36 1" id="Frame_ButtonQuit">
		<quad sizen="35 10" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" action="maniaplanet:quitserver" />
		<label sizen="35 10" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" text="{{{_("Quit")}}}" id="Label_Quit" />
	</frame>
	<frame posn="0 -25 1" id="Frame_ButtonTop">
		<quad sizen="35 10" halign="center" valign="center" image="{{{ButtonTopOff}}}" imagefocus="{{{ButtonTopOn}}}" scriptevents="1" id="Button_Top" />
		<label sizen="35 10" scale="0.95" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2" id="Label_ButtonTop" />
	</frame>
	<frame posn="0 -36 1" id="Frame_ButtonBottom">
		<quad sizen="48 12" halign="center" valign="center" image="{{{ButtonBottomOff}}}" imagefocus="{{{ButtonBottomOn}}}" scriptevents="1" id="Button_Bottom" />
		<label sizen="48 12" halign="center" valign="center2" style="TextRaceMessageBig" opacity="0.8" textsize="2.5" id="Label_ButtonBottom" />
	</frame>
</frame>
</frame>
<script><!--
#Include "TextLib" as TL
#Include "MathLib" as ML

declare Text G_CurrentView;
declare Integer[Integer] G_PageFormat;
declare Integer G_PageStart;
declare Integer G_PageClan;
declare Integer G_PageCurrent;
declare Integer G_PlayersMax;
declare CMlFrame Frame_ButtonBottom;

{{{Manialink::Animations(["EaseOutExp"])}}}
// @specstart
{{{Private_Lobby_InjectReadyHelpers()}}}
// @specend

Integer GetEchelon(CUser::EEchelon _Echelon) {
	switch (_Echelon) {
		case CUser::EEchelon::Bronze1	: return 1;
		case CUser::EEchelon::Bronze2	: return 2;
		case CUser::EEchelon::Bronze3	: return 3;
		case CUser::EEchelon::Silver1	: return 4;
		case CUser::EEchelon::Silver2	: return 5;
		case CUser::EEchelon::Silver3	: return 6;
		case CUser::EEchelon::Gold1		: return 7;
		case CUser::EEchelon::Gold2		: return 8;
		case CUser::EEchelon::Gold3		: return 9;
	}
	
	return 0;
}

Text GetEchelonPath(CUser::EEchelon _Echelon) {
	return "file://Media/Manialinks/Common/Echelons/echelon"^GetEchelon(_Echelon)^".dds";
}

CUser GetUser(Text _Login) {
	foreach (Player in Players) {
		if (Player.Login == _Login) return Player.User;
	}
	
	return Null;
}

Void UpdateOccupiedSlot(CMlFrame _Frame, CUser _User, Text _FallbackLogin) {
	if (_Frame == Null) return;
	
	declare Quad_Avatar			<=> (_Frame.GetFirstChild("Quad_Avatar")		as CMlQuad);
	declare Label_Name			<=> (_Frame.GetFirstChild("Label_Name")			as CMlLabel);
	declare Label_Rank			<=> (_Frame.GetFirstChild("Label_Rank")			as CMlLabel);
	declare Quad_CountryFlag	<=> (_Frame.GetFirstChild("Quad_CountryFlag")	as CMlQuad);
	declare Quad_Echelon		<=> (_Frame.GetFirstChild("Quad_Echelon")		as CMlQuad);
	declare Label_Echelon		<=> (_Frame.GetFirstChild("Label_Echelon")		as CMlLabel);
	
	if (_User != Null) {
		if (Quad_Avatar != Null)		Quad_Avatar.ImageUrl = "file://Avatars/"^_User.Login^"/Default";
		if (Label_Name != Null)			Label_Name.Value = _User.Name;
		if (Quad_CountryFlag != Null)	Quad_CountryFlag.ImageUrl = _User.CountryFlagUrl;
		if (Quad_Echelon != Null)		Quad_Echelon.ImageUrl = GetEchelonPath(_User.Echelon);
		if (Label_Echelon != Null)		Label_Echelon.Value = TL::ToText(GetEchelon(_User.Echelon));
		if (Label_Rank != Null) {
			declare Zone = _("Other");
			declare ZoneArray = TL::Split("|", _User.LadderZoneName);
			if (ZoneArray.existskey(2)) Zone = ZoneArray[2];
			if (_User.LadderRank > 0) Label_Rank.Value = TL::Compose("%1: %2", Zone, TL::ToText(_User.LadderRank));
			else Label_Rank.Value = TL::Compose("%1: %2", Zone, _("Not ranked"));
		}
	} else {
		if (Quad_Avatar != Null)		Quad_Avatar.ImageUrl = "file://Avatars/"^_FallbackLogin^"/Default";
		if (Label_Name != Null)			Label_Name.Value = _FallbackLogin;
		if (Quad_CountryFlag != Null)	Quad_CountryFlag.ImageUrl = "";
		if (Quad_Echelon != Null)		Quad_Echelon.ImageUrl = GetEchelonPath(CUser::EEchelon::None);
		if (Label_Echelon != Null)		Label_Echelon.Value = "0";
		if (Label_Rank != Null)			Label_Rank.Value = "";
	}
}

Void UpdateCardPositionning(Integer _SlotsNb) {
	declare Frame_PlayerList <=> (Page.GetFirstChild("Frame_PlayerList") as CMlFrame);
	Page.GetClassChildren("Frame_PlayerCard", Frame_PlayerList, True);
	declare X = 0;
	declare Y = 0;
	
	declare LinesNb = ((_SlotsNb - 1) / 2) + 1;
	declare YMargin = 0.;
	if (LinesNb >= 3) YMargin = 0.;
	else if (LinesNb == 2) YMargin = -11.5;
	else YMargin = -23.;
	
	foreach (Control in Page.GetClassChildren_Result) {
		declare Count = TL::ToInteger(Control.ControlId) + 1;
		
		if (Count > _SlotsNb) Control.Visible = False;
		else Control.Visible = True;
		
		declare PosX = -41.5 + (X * 82.);
		declare PosY = (Y * -23.) + YMargin;
		
		if (Count == _SlotsNb && Count % 2 != 0) {
			PosX = 0.;
		}
		
		Control.RelativePosition.X = PosX;
		Control.RelativePosition.Y = PosY;
		
		X += 1;
		if (Count % 2 == 0) {
			Y += 1;
			X = 0;
		}
	}
}

Void UpdateAllies() {
	declare Label_Clan <=> (Page.GetFirstChild("Label_Clan") as CMlLabel);
	Label_Clan.Value = TL::Compose("%1 %2", "{{{_("Clan")}}}", TL::ToText(G_PageClan+1));
	
	declare netread Integer[] Net_Matchmaking_Format for Teams[0];
	declare CountMax = Net_Matchmaking_Format[G_PageClan] - 1;
	UpdateCardPositionning(CountMax - G_PageStart + 1);
	
	declare netread Integer[][Text] Net_Lobby_RoomLogins for UI;
	declare Logins = Text[Integer];
	declare Statuses = Integer[Integer];
	foreach (Login => Info in Net_Lobby_RoomLogins) {
		declare Clan = Info[{{{C_AllyInfo_Clan}}}];
		if (G_PageClan != Clan) continue;
		
		declare Slot = Info[{{{C_AllyInfo_Slot}}}];
		if (Slot < G_PageStart || Slot - G_PageStart >= {{{SlotsNb}}}) continue;
		
		declare Status = Info[{{{C_AllyInfo_Status}}}];
		Logins[Slot] = Login;
		Statuses[Slot] = Status;
	}
	
	declare Frame_PlayerList <=> (Page.GetFirstChild("Frame_PlayerList") as CMlFrame);
	Page.GetClassChildren("Frame_PlayerCard", Frame_PlayerList, True);
	foreach (Control in Page.GetClassChildren_Result) {
		declare Count = G_PageStart + TL::ToInteger(Control.ControlId);
		
		declare Frame_PlayerCard		<=> (Control as CMlFrame);
		declare Frame_EmptySlot			<=> (Frame_PlayerCard.GetFirstChild("Frame_EmptySlot")			as CMlFrame);
		declare Frame_OccupiedSlot		<=> (Frame_PlayerCard.GetFirstChild("Frame_OccupiedSlot")		as CMlFrame);
		declare Quad_AllyNotValidated	<=> (Frame_OccupiedSlot.GetFirstChild("Quad_AllyNotValidated")	as CMlQuad);
		
		if (Logins.existskey(Count)) {
			Frame_EmptySlot.Visible = False;
			Frame_OccupiedSlot.Visible = True;
			if (Statuses[Count] == {{{C_AllyStatus_Validated}}}) {
				Quad_AllyNotValidated.Visible = False;
			} else {
				Quad_AllyNotValidated.Visible = True;
			}
			UpdateOccupiedSlot(Frame_OccupiedSlot, GetUser(Logins[Count]), Logins[Count]);
		} else if (Count > CountMax) {
			Frame_EmptySlot.Visible = False;
			Frame_OccupiedSlot.Visible = False;
		} else {
			Frame_EmptySlot.Visible = True;
			Frame_OccupiedSlot.Visible = False;
		}
	}
}

Void UpdatePager(Integer _Shift) {
	declare Frame_Pager <=> (Page.GetFirstChild("Frame_Pager") as CMlFrame);
	if (G_PlayersMax > {{{SlotsNb}}} || G_PageFormat.count > 1) {
		Frame_Pager.Visible = True;
	} else if (G_PlayersMax <= {{{SlotsNb}}}) {
		Frame_Pager.Visible = False;
		G_PageStart = 0;
	}
	
	if (G_PageFormat.count <= 0) return;
	if (!G_PageFormat.existskey(G_PageClan)) G_PageClan = 0;
	if (G_PageCurrent < 0 || G_PageCurrent > G_PageFormat[G_PageClan]) G_PageCurrent = 0;
	
	declare NextPageCurrent = G_PageCurrent + _Shift;
	declare NextPageClan = G_PageClan;
	if (NextPageCurrent < 0) {
		NextPageClan -= 1;
		if (G_PageFormat.existskey(NextPageClan)) {
			G_PageClan = NextPageClan;
			G_PageCurrent = G_PageFormat[NextPageClan];
		}
	} else if (NextPageCurrent > G_PageFormat[G_PageClan]) {
		NextPageClan += 1;
		if (G_PageFormat.existskey(NextPageClan)) {
			G_PageClan = NextPageClan;
			G_PageCurrent = 0;
		}
	} else {
		G_PageCurrent = NextPageCurrent;
	}
	
	G_PageStart = G_PageCurrent * {{{SlotsNb}}};
	
	declare Button_PagerNext <=> (Frame_Pager.GetFirstChild("Button_PagerNext") as CMlQuad);
	declare Button_PagerPrev <=> (Frame_Pager.GetFirstChild("Button_PagerPrev") as CMlQuad);
	if (G_PageClan <= 0 && G_PageCurrent <= 0) Button_PagerPrev.Substyle = "ArrowDisabled";
	else Button_PagerPrev.Substyle = "ArrowPrev";
	if (G_PageClan >= G_PageFormat.count-1 && G_PageCurrent >= G_PageFormat[G_PageClan]) Button_PagerNext.Substyle = "ArrowDisabled";
	else Button_PagerNext.Substyle = "ArrowNext";
	
	UpdateAllies();
}

Void UpdateFormat() {
	declare netread Integer[] Net_Matchmaking_Format for Teams[0];
	G_PlayersMax = 0;
	G_PageFormat.clear();
	foreach (Clan => PlayersNb in Net_Matchmaking_Format) {
		G_PlayersMax += PlayersNb;
		
		declare PageNb = 0;
		if (PlayersNb > 0) PageNb = ((PlayersNb-1) / {{{SlotsNb}}});
		G_PageFormat[Clan] = PageNb;
	}
	
	UpdatePager(0);
}

Void ChangeView(Text _View) {
	G_CurrentView = _View;
	
	Page.GetClassChildren("View", Page.MainFrame, True);
	foreach (Control in Page.GetClassChildren_Result) {
		if (Control.ControlId == _View) {
			Control.Visible = True;
		} else {
			Control.Visible = False;
		}
	}
	
	declare Frame_ButtonTop		<=> (Page.GetFirstChild("Frame_ButtonTop")					as CMlFrame);
	declare Label_ButtonTop		<=> (Frame_ButtonTop.GetFirstChild("Label_ButtonTop")		as CMlLabel);
	declare Label_ButtonBottom	<=> (Frame_ButtonBottom.GetFirstChild("Label_ButtonBottom")	as CMlLabel);
	
	switch (G_CurrentView) {
		case "Home": {
			Frame_ButtonTop.Visible = False;
			Frame_ButtonBottom.Visible = True;
			Label_ButtonTop.Value = "{{{_("Join")}}}";
			Label_ButtonBottom.Value = "{{{_("Create")}}}";
		}
		case "Room": {
			Frame_ButtonTop.Visible = True;
			Frame_ButtonBottom.Visible = True;
			Label_ButtonTop.Value = "{{{_("Leave")}}}";
			Label_ButtonBottom.Value = "{{{_("Ready")}}}";
		}
		case "List": {
			Frame_ButtonTop.Visible = True;
			Frame_ButtonBottom.Visible = True;
			Label_ButtonTop.Value = "{{{_("Back")}}}";
			Label_ButtonBottom.Value = "{{{_("Create")}}}";
		}
	}
}

Void ExecuteAction(Text _Action) {
	declare netread Integer Net_Lobby_SynchroServer for Teams[0];
	declare netwrite Integer Net_Lobby_SynchroRooms for UI;
	Net_Lobby_SynchroRooms = Net_Lobby_SynchroServer;
	
	declare netwrite Integer Net_Lobby_ClientRoomsActionUpdate for UI;
	declare netwrite Text Net_Lobby_ClientRoomsAction for UI;
	Net_Lobby_ClientRoomsAction = _Action;
	Net_Lobby_ClientRoomsActionUpdate = Now;
}

Void UpdateView() {
	switch (G_CurrentView) {
		case "Home": {
			foreach (Event in PendingEvents) {
				if (Event.Type == CMlEvent::Type::MouseClick) {
					if (Event.ControlId == "Button_Bottom") {
						ExecuteAction("CreateRoom");
					}
				}
			}
		}
		case "Room": {
			foreach (Event in PendingEvents) {
				if (Event.Type == CMlEvent::Type::MouseClick) {
					if (Event.ControlId == "Button_Top") {
						ExecuteAction("LeaveRoom");
					} else if (Event.ControlId == "Button_Bottom") {
						// @specstart
						//IsSpectatorMode = !IsSpectatorMode;
						Private_Lobby_ToggleReady();
						// @specend
					} else if (Event.ControlId == "Button_PagerPrev") {
						UpdatePager(-1);
					} else if (Event.ControlId == "Button_PagerNext") {
						UpdatePager(1);
					} else if (Event.Control.HasClass("Button_EmptySlot")) {
						declare SlotNb for Event.Control = -1;
						declare netwrite Integer Net_Lobby_RequestSlot for UI;
						declare netwrite Integer Net_Lobby_RequestClan for UI;
						Net_Lobby_RequestSlot = (G_PageCurrent * {{{SlotsNb}}}) + SlotNb;
						Net_Lobby_RequestClan = G_PageClan;
						ExecuteAction("SwitchSlot");
					} else if (Event.Control.HasClass("Button_OccupiedSlot")) {
						declare SlotNb for Event.Control = -1;
						declare netwrite Integer Net_Lobby_RequestSlot for UI;
						declare netwrite Integer Net_Lobby_RequestClan for UI;
						Net_Lobby_RequestSlot = (G_PageCurrent * {{{SlotsNb}}}) + SlotNb;
						Net_Lobby_RequestClan = G_PageClan;
						ExecuteAction("SwitchSlot");
					}
				}
			}
			
			declare netread Boolean Net_Lobby_ImBlocked for UI;
			Frame_ButtonBottom.Visible = !Net_Lobby_ImBlocked;
		}
		case "List": {
			foreach (Event in PendingEvents) {
				if (Event.Type == CMlEvent::Type::MouseClick) {
					if (Event.ControlId == "Button_Top") {
						
					} else if (Event.ControlId == "Button_Bottom") {
						
					}
				}
			}
		}
	}
}

Void DisplayFrame(Boolean _Displayed) {
	declare Button_ShowHide <=> (Page.GetFirstChild("Button_ShowHide") as CMlQuad);
	
	if (_Displayed) {
		Button_ShowHide.ImageUrl = "{{{ButtonHideOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonHideOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="0 0 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	} else {
		Button_ShowHide.ImageUrl = "{{{ButtonShowOff}}}";
		Button_ShowHide.ImageUrlFocus = "{{{ButtonShowOn}}}";
		LibManialink_Anim({{{Manialink::Inject("""<frame posn="0 -133 10" id="Frame_Global" />""")}}}, 300, "EaseOutExp");
	}
}

main() {
	declare Frame_Global	<=> (Page.GetFirstChild("Frame_Global")					as CMlFrame);
	declare Label_Title		<=> (Frame_Global.GetFirstChild("Label_Title")			as CMlLabel);
	Frame_ButtonBottom		<=> (Frame_Global.GetFirstChild("Frame_ButtonBottom")	as CMlFrame);
	
	declare Count = 0;
	Page.GetClassChildren("Button_EmptySlot", Frame_Global, True);
	foreach (Control in Page.GetClassChildren_Result) {
		declare SlotNb for Control = -1;
		SlotNb = Count;
		Count += 1;
	}
	Count = 0;
	Page.GetClassChildren("Button_OccupiedSlot", Frame_Global, True);
	foreach (Control in Page.GetClassChildren_Result) {
		declare SlotNb for Control = -1;
		SlotNb = Count;
		Count += 1;
	}
	
	declare netread Text Net_Lobby_ReconnectToServer for UI;
	declare netread Integer Net_Matchmaking_FormatUpdate for Teams[0];
	declare netread Text Net_Lobby_RoomsViews for UI;
	declare netread Integer Net_Lobby_RoomLoginsUpdate for UI;
	declare netread Integer[][Text] Net_Lobby_RoomLogins for UI;
	
	declare netread Boolean Net_Lobby_ImBlocked for UI;
	declare netread Boolean Net_Lobby_ShowSubstituteML for UI;
	declare netread Boolean Net_Lobby_ShowVersusML for UI;
	declare netread Integer Net_Lobby_Penalty for UI;
	declare netread Integer Net_Lobby_MatchCancellation for UI;
	declare netread Boolean Net_Lobby_AllowMatchCancel for Teams[0];
	declare netread Integer Net_Lobby_LobbyLimitMatchCancel for Teams[0];
	declare netread Boolean Net_Lobby_WarnPenalty for Teams[0];
	
	declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
	if (!MM_FrameIsDisplayed.existskey("Rooms")) MM_FrameIsDisplayed["Rooms"] = True;
	DisplayFrame(MM_FrameIsDisplayed["Rooms"]);
	
	G_PageFormat = Integer[Integer];
	G_PageStart = 0;
	G_PageClan = 0;
	G_PageCurrent = 0;
	G_PlayersMax = {{{SlotsNb}}};
	
	declare FrameIsDisplayed = True;
	// @specstart
	//declare PrevIsSpectatorMode = True;
	declare PrevIsReady = False;
	// @specend
	declare PrevReconnectToServer = "";
	declare PrevFormatUpdate = -1;
	declare PrevRoomsView = "";
	declare PrevRoomLoginsUpdate = -1;
	declare PrevIsBlocked = False;
	
	HideResumePlayingButton = True;
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		LibManialink_AnimLoop();
		
		// @specstart
		/*if (PrevIsSpectatorMode != IsSpectatorMode || PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevIsSpectatorMode = IsSpectatorMode;*/
		if (PrevIsReady != Private_Lobby_IsReady() || PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevIsReady = Private_Lobby_IsRead();
		// @specend
			PrevReconnectToServer = Net_Lobby_ReconnectToServer;
			
			// @specstart
			//if (IsSpectatorMode && Net_Lobby_ReconnectToServer == "") {
			if (!Private_Lobby_IsReady() && Net_Lobby_ReconectToServer == "") {
			// @specend
				Frame_Global.Visible = True;
			} else {
				Frame_Global.Visible = False;
			}
		}
			
		if (Frame_Global.Visible) {
			if (PrevFormatUpdate != Net_Matchmaking_FormatUpdate) {
				PrevFormatUpdate = Net_Matchmaking_FormatUpdate;
				UpdateFormat();
			}
			
			if (PrevRoomLoginsUpdate != Net_Lobby_RoomLoginsUpdate) {
				PrevRoomLoginsUpdate = Net_Lobby_RoomLoginsUpdate;
				UpdatePager(0);
			}
			
			if (PrevRoomsView != Net_Lobby_RoomsViews) {
				PrevRoomsView = Net_Lobby_RoomsViews;
				ChangeView(Net_Lobby_RoomsViews);
			}
			
			if (Net_Lobby_Penalty > 0) {
				declare Duration = Net_Lobby_Penalty - ArenaNow + 1000;
				Label_Title.Value = TL::Compose("%1 - %2", "{{{_("You are suspended")}}}", TL::TimeToText(Duration, False));
			else if (Net_Lobby_Penalty == 0) {
				Label_Title.Value = "{{{_("You are suspended")}}}";
			} else if (Net_Lobby_Penalty < 0) {
				if (Net_Lobby_WarnPenalty && Net_Lobby_AllowMatchCancel && Net_Lobby_LobbyLimitMatchCancel >= 0) {
					declare CancellationRemaing = Net_Lobby_LobbyLimitMatchCancel - Net_Lobby_MatchCancellation;
					if (CancellationRemaing <= 0) {
						Label_Title.Value = _("You will be suspended if you cancel your next match.");
					} else if (CancellationRemaing == 1) {
						Label_Title.Value = TL::Compose(_("You can cancel 1 match."), TL::ToText(CancellationRemaing));
					} else {
						Label_Title.Value = TL::Compose(_("|%1 is the number of matches|You can cancel %1 matches."), TL::ToText(CancellationRemaing));
					}
				} else if (Label_Title.Value != "") {
					Label_Title.Value = "";
				}
			}
		
			UpdateView();
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::KeyPress) {
				if (Event.KeyName == "F6") {
					if (
						(Net_Lobby_ShowSubstituteML || Net_Lobby_ShowVersusML)
						// @specstart 
						//&& !Net_Lobby_AllowMatchCancel && !IsSpectatorMode
						&& !Net_Lobby_AllowMatchCancel && Private_Lobby_IsReady()
						// @specend
					) {
					
					} else if (!Net_Lobby_ImBlocked && Net_Lobby_RoomLogins.count > 0) {
						// @specstart
						//IsSpectatorMode = !IsSpectatorMode;
						Private_Lobby_ToggleReady();
						// @specend
					}
				}
			} else if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_ShowHide") {
					declare persistent Boolean[Text] MM_FrameIsDisplayed for This;
					MM_FrameIsDisplayed["Rooms"] = !MM_FrameIsDisplayed["Rooms"];
					DisplayFrame(MM_FrameIsDisplayed["Rooms"]);
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the rematch vote manialink
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLRematchVote() {
	declare SizeX = 130.;
	declare SizeY = SizeX * 0.3;
	
	declare Background = "file://Media/Manialinks/Common/Lobbies/header.png";
	declare ButtonNoOn = "file://Media/Manialinks/Common/Lobbies/small-button-RED-ON.dds";
	declare ButtonNoOff = "file://Media/Manialinks/Common/Lobbies/small-button-RED.dds";
	declare ButtonYesOn = "file://Media/Manialinks/Common/Lobbies/ready-button-GREEN-ON.dds";
	declare ButtonYesOff = "file://Media/Manialinks/Common/Lobbies/ready-button-GREEN.dds";
	
	return """
<manialink version="1" name="ModeMatchmaking:RematchVote">
<frame posn="0 40 10" id="Frame_Global">
	<quad posn="0 0 -1" sizen="{{{SizeX}}} {{{SizeY}}}" halign="center" image="{{{Background}}}" />
	<label posn="0 -7" sizen="{{{SizeX-7}}} {{{SizeY}}}" halign="center" style="TextRaceMessage" textsize="4" text="{{{_("Do you want to play a rematch?")}}}" />
	<label posn="0 -21" sizen="20 4" halign="center" style="TextRaceChrono" textsize="6" opacity="0.75" id="Label_RemainingTime" />
	<label posn="-43 -21" sizen="30 4" halign="center" style="TextButtonBig" textsize="5" opacity="0.75" text="{{{_("Yes")}}}" scriptevents="1" id="Button_Yes" />
	<label posn="43 -21" sizen="30 4" halign="center" style="TextButtonBig" textsize="5" opacity="0.75" text="{{{_("No")}}}" scriptevents="1" id="Button_No"/>
</frame>
<script><!--
#Include "TextLib" as TL

Void SendVote(Boolean _Value) {
	declare netread Integer Net_MM_RematchVote_SynchroServer for Teams[0];
	declare netwrite Integer Net_MM_RematchVote_SynchroClient for UI;
	declare netwrite Integer Net_MM_RematchVote_Update for UI;
	declare netwrite Boolean Net_MM_RematchVote_Value for UI;
	
	Net_MM_RematchVote_SynchroClient = Net_MM_RematchVote_SynchroServer;
	Net_MM_RematchVote_Value = _Value;
	Net_MM_RematchVote_Update = Now;
	
	declare Button_Yes <=> (Page.GetFirstChild("Button_Yes") as CMlLabel);
	declare Button_No <=> (Page.GetFirstChild("Button_No") as CMlLabel);
	if (_Value) {
		Button_Yes.Scale = 1.1;
		Button_Yes.TextColor = <0.1, 0.9, 0.1>;
		Button_No.Scale = 1.;
		Button_No.TextColor = <1., 1., 1.>;
	} else {
		Button_No.Scale = 1.1;
		Button_No.TextColor = <0.9, 0.1, 0.1>;
		Button_Yes.Scale = 1.;
		Button_Yes.TextColor = <1., 1., 1.>;
	}
}

main() {
	declare Label_RemainingTime	<=> (Page.GetFirstChild("Label_RemainingTime") as CMlLabel);
	
	declare netread Integer Net_MM_RematchVote_EndTime for Teams[0];
	
	while (True) {
		yield;
		if (!PageIsVisible || InputPlayer == Null) continue;
		
		if (Net_MM_RematchVote_EndTime >= ArenaNow) {
			Label_RemainingTime.Value = TL::TimeToText(Net_MM_RematchVote_EndTime - ArenaNow);
		} else {
			Label_RemainingTime.Value = TL::TimeToText(0);
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Yes") {
					SendVote(True);
				} else if (Event.ControlId == "Button_No") {
					SendVote(False);
				}
			}
		}
	}
}
--></script>
</manialink>
""";
}

// ---------------------------------- //
/** Select the right manialink depending on the king of lobby
 *
 *	@param	_DisplayRules		Allow to display a manialink with the rules of the mode
 *
 *	@return		The manialink
 */
Text Private_Lobby_GetMLLobbyScreen(Boolean _DisplayRules) {
	if (MM_IsUniversalServer()) {
		return Private_Lobby_GetMLRooms();
	} else {
		return Private_Lobby_GetMLWaitingScreen(_DisplayRules);
	}
	
	return "";
}

// ---------------------------------- //
/** Set the new matchmaking format on this server
 *
 *	@param	_NewFormat		The new format
 */
Void MM_SetFormat(Integer[] _NewFormat) {
	G_Matchmaking_Format = _NewFormat;
	G_Matchmaking_CurrentFormat = _NewFormat;
	
	declare Max = 0;
	if (MM_IsUniversalServer()) {
		foreach (PlayersNb in G_Matchmaking_Format) {
			Max += PlayersNb;
		}
	} else {
		foreach (PlayersNb in G_Matchmaking_Format) {
			if (PlayersNb > Max) Max = PlayersNb;
		}
	}
	
	G_Matchmaking_MaxPlayers = Max;
	
	declare netwrite Integer Net_Matchmaking_FormatUpdate for Teams[0];
	declare netwrite Integer[] Net_Matchmaking_Format for Teams[0];
	declare netwrite Integer Net_Matchmaking_MaxPlayers for Teams[0];
	Net_Matchmaking_FormatUpdate = Now;
	Net_Matchmaking_Format = G_Matchmaking_Format;
	Net_Matchmaking_MaxPlayers = G_Matchmaking_MaxPlayers;
	
	Layers::Update("StartingMatch", Private_MM_GetMLStartingMatch());
}

// ---------------------------------- //
/** Set the progressive matchmaking formats on this server
 *
 *	@param	_AvailableFormats		List of available progressive formats
 */
Void MM_SetProgressiveFormats(Integer[][] _ProgressiveFormats) {
	G_Matchmaking_ProgressiveFormats = _ProgressiveFormats;
}

// ---------------------------------- //
/** Initialize the match server for matchmaking
 *
 *	@param	_Format		The  match format
 */
Void MM_Init(Integer[] _Format) {
	MM_SetFormat(_Format);
	UseForcedClans = True;
}

// ---------------------------------- //
/// Send the match id to the ladder server to validate 100K matches
Void MM_SetLadderMatchId() {
	declare MatchId = TL::ToInteger(G_MMMatch_Id);
	if (MatchId < 0) return;
	
	Ladder_SetMatchMakingMatchId(MatchId);
}