/** 
 *	Matchmaking lobby library
 */

#Const Version		"2015-02-06"
#Const ScriptName	"MatchmakingLobby.Script.txt"

#Include "TextLib" as TL
#Include "MathLib" as ML
#Include "Libs/Nadeo/Message.Script.txt" as Message
#Include "Libs/Nadeo/Manialink.Script.txt" as Manialink
#Include "Libs/Nadeo/XmlRpcCommon.Script.txt" as XmlRpc
#Include "Libs/Nadeo/MatchmakingCommon.Script.txt" as MMCommon

// ---------------------------------- //
// Constants
// ---------------------------------- //
// Lobby phases
#Const C_Lobby_Playing		0	///< Playing phase
#Const C_Lobby_Matchmaking	1	///< Matchmaking phase
// Masters
#Const C_Master_Numbers		20	///< Number of masters displayed
#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
// Ally status
#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
// Ally info
#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
// Default room properties
#Const C_Lobby_DefaultClan	0	///< Default clan when creating a room 
#Const C_Lobby_DefaultSlot	0	///< Default slot when creating a room
// Cancellation configuration
#Const C_AllowMatchCancel			True	///< Legacy : Allow match cancel
#Const C_LimitMatchCancel			0		///< Legacy : -1: infinite cancel, 0 or more: number of cancellations allowed
#Const C_PenalizeSubstituteCancel	False	///< Legacy : Penalize players canceling a replacement
#Const C_WarnPenalty				False	///< Warn player that they will be penalize if they cancel
// Misc
#Const C_ReconnectDuration			5000		///< Duration before sending back a player to the match he left
#Const C_TransfertSafeTime			15000		///< Minimum time after a transfert before a player can be listed as ready
#Const C_RequestRandomDeviation		500			///< Random time margin applied to the live request of the match and lobby server

// ---------------------------------- //
// Globales
// ---------------------------------- //
declare Boolean G_LibMMLobby_DisableUI;					///< Disable the lobby UI
declare Integer G_LibMMLobby_StartTime;					///< Startime of the lobby
declare Integer G_LibMMLobby_EndTime;					///< EndTime of the lobby
declare Integer	G_LibMMLobby_Phase;						///< Current lobby phase : playing/matchmaking
declare Boolean G_LibMMLobby_MatchmakingEnabled;		///< Is the the matchmaking enabled?
declare Integer G_LibMMLobby_PreMatchmakingDuration;	///< Duration of the pre matchmaking sequence
declare Integer G_LibMMLobby_PostMatchmakingDuration;	///< Duration of the post matchmaking sequence

// ---------------------------------- //
// Functions
// ---------------------------------- //
// ---------------------------------- //
// Private
// ---------------------------------- //
// ---------------------------------- //
/** Custom log function
 *
 *	@param	_Message	The message to log
 */
Void Private_Log(Text _Message) {
	log(Now^"> "^_Message);
}

// ---------------------------------- //
/** Convert an echelon to an integer
 *
 *	@param	_Echelon	The echelon to convert
 *
 *	@return		The echelon converted to an Integer
 */
Integer Private_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 a user from its login
 *
 *	@param	_Login		Login of the user to get
 *	
 *	@return				The user if found, Null otherwise
 */
CUser Private_GetUser(Text _Login) {
	if (_Login == "") return Null;
	
	foreach (User in Users) {
		if (User.Login == _Login) return User;
	}
	
	return Null;
}

// ---------------------------------- //
/**	Check if a Trackmania player is spawned
 *
 *	@param	_Player		The player to check
 *	
 *	@return				True if the player is spawned, False otherwise
 */
Boolean Private_PlayerIsSpawned(CTmPlayer _Player) {
	return _Player.IsSpawned;
}

// ---------------------------------- //
/**	Check if a Shootmania player is spawned
 *
 *	@param	_Player		The player to check
 *	
 *	@return				True if the player is spawned, False otherwise
 */
Boolean Private_PlayerIsSpawned(CSmPlayer _Player) {
	return (_Player.SpawnStatus != CSmPlayer::ESpawnStatus::NotSpawned);
}

// ---------------------------------- //
// Public
// ---------------------------------- //
// ---------------------------------- //
/** Return the version number of the script
 *
 *	@return				The version number of the script
 */
Text GetScriptVersion() {
	return Version;
}

// ---------------------------------- //
/** Return the name of the script
 *
 *	@return				The name of the script
 */
Text GetScriptName() {
	return ScriptName;
}

// ---------------------------------- //
/// Unload the library
Void Unload() {
	XmlRpc::UnregisterCallback("Matchmaking_ReadyState");
	
	G_LibMMLobby_DisableUI = False;
	G_LibMMLobby_StartTime = -1;
	G_LibMMLobby_EndTime = -1;
	G_LibMMLobby_Phase = C_Lobby_Playing;
	G_LibMMLobby_MatchmakingEnabled = False;
	G_LibMMLobby_PreMatchmakingDuration = 0;
	G_LibMMLobby_PostMatchmakingDuration = 0;
}

// ---------------------------------- //
/// Load the library
Void Load() {
	Unload();
	
	XmlRpc::RegisterCallback("Matchmaking_ReadyState", """
* Data : An array with the login of the player and its ready state
* Example : ["Login", "True"]
* Note : This callback is sent when the ready state of the player change. It can also be triggered with the `Matchmaking_GetReadyState` method.
* Version : available since  ModeMatchmaking.Script.txt_v2014-10-26
""");
}

// ---------------------------------- //
/** Disable the UI of the lobby
 *
 *	@param	_DisableUI		Disable the UI or not
 */
Void DisableUI(Boolean _DisableUI) {
	G_LibMMLobby_DisableUI = _DisableUI;
}

// ---------------------------------- //
/** Enable or disable the matchmaking in the lobby
 *
 *	@param	_Enable		True to enable, False to disable
 */
Void EnableMatchmaking(Boolean _Enable) {
	G_LibMMLobby_MatchmakingEnabled = _Enable;
}

// ---------------------------------- //
/** Check if the matchmaking is enable in the lobby
 *
 *	@return				True if the matchmaking is enabled, False otherwise
 */
Boolean MatchmakingIsEnabled() {
	return G_LibMMLobby_MatchmakingEnabled;
}

// ---------------------------------- //
/** Set the duration of one round of matchmaking
 *
 *	@param	_Pre		Duration of the pre matchmaking sequence
 *	@para	_Post		Duration of the post matchmaking sequence
 */
Void SetMatchmakingDuration(Integer _Pre, Integer _Post) {
	G_LibMMLobby_PreMatchmakingDuration = _Pre;
	G_LibMMLobby_PostMatchmakingDuration = _Post;
}

// ---------------------------------- //
/** Get the duration of the pre matchmaking sequence
 *
 *	@return				The duration of the sequence
 */
Integer GetPreMatchmakingDuration() {
	return G_LibMMLobby_PreMatchmakingDuration;
}

// ---------------------------------- //
/** Get the duration of the post matchmaking sequence
 *
 *	@return				The duration of the sequence
 */
Integer GetPostMatchmakingDuration() {
	return G_LibMMLobby_PostMatchmakingDuration;
}

// ---------------------------------- //
/** Set the lobby start time
 *
 *	@param	_StartTime	The new lobby start time
 */
Void SetLobbyStartTime(Integer _StartTime) {
	G_LibMMLobby_StartTime = _StartTime;
}

// ---------------------------------- //
/** Get the lobby start time
 *
 *	@return				The lobby start time
 */
Integer GetLobbyStartTime() {
	return G_LibMMLobby_StartTime;
}

// ---------------------------------- //
/** Set the lobby end time
 *
 *	@param	_EndTime	The new lobby end time
 */
Void SetLobbyEndTime(Integer _EndTime) {
	G_LibMMLobby_EndTime = _EndTime + ML::Rand(-C_RequestRandomDeviation, C_RequestRandomDeviation);
}

// ---------------------------------- //
/** Get the lobby end time
 *
 *	@return				The lobby end time
 */
Integer GetLobbyEndTime() {
	return G_LibMMLobby_EndTime;
}

// ---------------------------------- //
/// Get the lobby phase constants
Integer LobbyPhase_Playing() { return C_Lobby_Playing; }
Integer LobbyPhase_Matchmaking() { return C_Lobby_Matchmaking; }

// ---------------------------------- //
/** Set the lobby phase
 *
 *	@param	_Phase		The new lobby phase
 */
Void SetLobbyPhase(Integer _Phase) {
	G_LibMMLobby_Phase = _Phase;
}

// ---------------------------------- //
/** Get the lobby phase
 *
 *	@return				The lobby phase
 */
Integer GetLobbyPhase() {
	return G_LibMMLobby_Phase;
}

// ---------------------------------- //
/** Check if a user is blocked by the matchmaking
 *
 *	@param	_User		The user to check
 *
 *	@return				True if the user is blocked, False otherwise
 */
Boolean IsBlocked(CUser _User) {
	if (_User == Null) return False;
	
	declare Lobby_IsBlocked for _User = False;
	return Lobby_IsBlocked;
}

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

// ---------------------------------- //
/** Check if the UI is synchro
 *
 *	@param	_UI		The UI of the player to check
 */
Boolean IsSynchro(CUIConfig _UI) {
	if (_UI == Null) return True;
	
	declare netread Integer Net_Lobby_ReadySynchroClient for _UI;
	declare netwrite Integer Net_Lobby_ReadySynchroServer for Teams[0];
	
	return (Net_Lobby_ReadySynchroClient == Net_Lobby_ReadySynchroServer);
}

// ---------------------------------- //
/** Send the ready state of a player
 *
 *	@param	_Player		The player to check
 *	@param	_IsReady	The ready state
 */
Void SendReadyState(CPlayer _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 SendReadyState(CPlayer _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;
	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 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
	if (IsBlocked(_User) && _IsReady) return;
	
	declare netwrite Boolean Net_Lobby_Ready for UI;
	Net_Lobby_Ready = _IsReady;
	
	declare CPlayer Player;
	foreach (TmpPlayer in AllPlayers) {
		if (TmpPlayer.Login == _User.Login) {
			Player <=> TmpPlayer;
			break;
		}
	}
	
	if (Player != Null) {
		declare netwrite Boolean Net_Lobby_Ready as ReadyForPlayer for Player;
		ReadyForPlayer = _IsReady;
		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 IsReady(CUser _User) {
	if (_User == Null) return False;
	
	// A blocked player can't be ready
	if (IsBlocked(_User)) 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 UpdateReady(CPlayer _Player) {
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	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 (IsSynchro(UI)) {
			// Don't let a blocked player getting ready
			if (!IsBlocked(_Player.User) || (IsBlocked(_Player.User) && !Net_Lobby_ToggleReady)) {
				Net_Lobby_Ready = Net_Lobby_ToggleReady;
				ReadyForPlayer = Net_Lobby_Ready;
				SendReadyState(_Player, Net_Lobby_Ready);
			}
		}
	}
}

// ---------------------------------- //
/** Inject the ready helper functions into a manialink
 *
 *	@return		The helper functions
 */
Text 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(CPlayer _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;
}
""";
}

// ---------------------------------- //
/// Update the lobby players list
Void 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 ShowVersusML(CPlayer _Player) {
	if (_Player == Null) return;
	declare UI <=> UIManager.GetUI(_Player);
	if (UI == Null) return;
	
	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;
	
	UpdatePlayersList();
}

// ---------------------------------- //
/** Check if a player is on the versus screen
 * 
 *	@param	_Player		The player to check
 *
 *	@return				True if the player is on the versus screen, False otherwise
 */
Boolean IsOnVersusScreen(CPlayer _Player) {
	declare Lobby_OnVersusScreen for _Player = False;
	return Lobby_OnVersusScreen;
}

// ---------------------------------- //
/** Hide the versus manialink from a player
 *
 *	@param	_Player		The player to update
 */
Void HideVersusML(CPlayer _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 = "";
	
	UpdatePlayersList();
}

// ---------------------------------- //
/// Empty the masters list
Void 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 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 <=> Private_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_ToInteger(User.Echelon))
		]);
	}
	
	if (Net_Lobby_Masters.count > C_Master_Numbers) {
		declare RemoveNb = Net_Lobby_Masters.count - C_Master_Numbers;
		while (RemoveNb > 0) {
			declare Removed = Net_Lobby_Masters.removekey(Net_Lobby_Masters.count-1);
			RemoveNb -= 1;
		}
	}
	
	Net_Lobby_MastersUpdate = Now;
}

// ---------------------------------- //
/** Update the karma of an user (manage penalties)
 *
 *	@param	_User	The user to update
 */
Void 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 {
			// Check that player is forced to non ready mode
			if (IsReady(_User)) {
				SetReady(_User, False);
			}
		}
	} else {
		// Block player
		if (Lobby_Penalty >= 0) {
			Lobby_IsBlocked = True;
			SetReady(_User, False);
		}
	}
	
	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;
			
			declare CPlayer Player;
			foreach (TmpPlayer in AllPlayers) {
				if (TmpPlayer.Login == _User.Login) {
					Player <=> TmpPlayer;
					break;
				}
			}
			
			if (Player != Null) {
				declare netwrite Net_Lobby_IsBlocked for Player = False;
				Net_Lobby_IsBlocked = Lobby_IsBlocked;
			}
			
			UpdatePlayersList();
		}
	}
}

// ---------------------------------- //
/** Get an user penalty
 *
 *	@param	_User	The user to get
 *
 *	@return			The user penalty
 */
Integer 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 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 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 PenalizePlayer(CUser _User, Integer _Duration, Text _MatchId, Boolean _IsSubstitute, Boolean _IsReconnect) {
	if (_User == Null) return;
	
	if (_Duration > 0) {
		if (!_IsSubstitute) {
			SetPlayerPenalty(_User, Now + (_Duration * 1000));
			if (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^_User.Login^" > Penalized for "^_Duration^"s");
		}
	} else if (_Duration == 0) {
		if (!_IsSubstitute) {
			SetPlayerPenalty(_User, 0);
			if (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^_User.Login^" > Penalized indefinitely");
		}
	} else {
		CancelPlayerPenalty(_User);
		if (MMCommon::GetLogDisplay("MiscDebug")) Private_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;
}

// ---------------------------------- //
/// Overload PenalizePlayer() function
Void PenalizePlayer(CUser _User, Text _MatchId, Boolean _IsSubstitute, Boolean _IsReconnect) {
	PenalizePlayer(_User, 0, _MatchId, _IsSubstitute, _IsReconnect);
}

// ---------------------------------- //
/// Overload PenalizePlayer() function
Void PenalizePlayer(CUser _User, Integer _Duration) {
	PenalizePlayer(_User, _Duration, "", False, False);
}

// ---------------------------------- //
/** Manage player reconnection after a leave
 *
 *	@param	_Player		The player to reconnect
 */
Void ReconnectToServer(CPlayer _Player) {
	if (_Player == Null) return;
	
	declare Lobby_ReconnectToServer for _Player = "";
	declare Lobby_ReconnectToMatchId for _Player = "";
	
	if (Lobby_ReconnectToServer != "") {
		declare UI <=> UIManager.GetUI(_Player);
		
		if (UI != Null) {
			declare netwrite Net_Lobby_ReconnectToServer for UI = "";
			
			if (Net_Lobby_ReconnectToServer == "") {
				Net_Lobby_ReconnectToServer = Lobby_ReconnectToServer;
				SetReady(_Player.User, False);
				UI.CountdownEndTime = Now + C_ReconnectDuration;
			}
		} else {
			Lobby_ReconnectToServer = "";
			Lobby_ReconnectToMatchId = "";
		}
		
		// Check that players is forced into spectator
		if (IsReady(_Player.User)) {
			SetReady(_Player.User, False);
			// Cancel send back to match server
			if (UI != Null) {
				if (MMCommon::GetLogDisplay("MiscDebug")) {
					Private_Log("[SERVER] "^_Player.Login^" > Cancel reconnection to match on server : \""^Lobby_ReconnectToServer^"\"");
				}
				
				PenalizePlayer(_Player.User, Lobby_ReconnectToMatchId, False, True);
				
				declare netwrite Net_Lobby_ReconnectToServer for UI = "";
				Net_Lobby_ReconnectToServer = "";
				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) {
				MMCommon::SendToServer(_Player, Lobby_ReconnectToServer);
				
				if (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^_Player.Login^" > Reconnected back to match on server : \""^Lobby_ReconnectToServer^"\"");
				
				declare netwrite Net_Lobby_ReconnectToServer for UI = "";
				Net_Lobby_ReconnectToServer = "";
				UI.CountdownEndTime = -1;
				Lobby_ReconnectToServer = "";
				Lobby_ReconnectToMatchId = "";
			}
		}
	}
}

// ---------------------------------- //
/** Let a player cancel a match
 *
 *	@param	_User		The user who want to cancel his match
 */
Void 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 (MMCommon::GetLogDisplay("MiscDebug")) {
			if (CancelIsSubstitute) Private_Log("[SERVER] "^_User.Login^" > Cancel substitute ("^CancelMatchId^") on server: "^CancelMatchServer);
			else Private_Log("[SERVER] "^_User.Login^" > Cancel match ("^CancelMatchId^") on server: "^CancelMatchServer);
		}
		
		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 (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^User.Login^" > My match id : "^Lobby_MatchId^" | The match to cancel : "^CancelMatchId);
				
				if (CancelMatchId != Lobby_MatchId) continue;
				
				if (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^User.Login^" > Match canceled ("^Lobby_MatchId^")");
				
				Lobby_MatchId = "";
				Lobby_MatchServer = "";
				Lobby_IsSubstitute = False;
				
				declare CPlayer Player;
				foreach (TmpPlayer in AllPlayers) {
					if (TmpPlayer.Login == _User.Login) {
						Player <=> TmpPlayer;
						break;
					}
				}
				HideVersusML(Player);
				
				declare UI <=> UIManager.GetUI(User);
				if (UI != Null) {
					UI.SendChat(TL::Compose(_("%1$<%2$> canceled match start."), MMCommon::GetMessagePrefix(), _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 CPlayer Player;
			foreach (TmpPlayer in AllPlayers) {
				if (TmpPlayer.Login == _User.Login) {
					Player <=> TmpPlayer;
					break;
				}
			}
			HideVersusML(Player);
		}
	}
}

// ---------------------------------- //
/** Parse the matches XML and send player to their match server
 *
 *	@param	_MatchesXML			The XML containing the matches
 */
Void GetMatches(Text _MatchesXML) {
	if (MMCommon::GetLogDisplay("APIDebug")) Private_Log("[API] Response: POST /lobby-server/matchmaking-live\n<!--\n"^_MatchesXML^"\n-->");
	// Get matches info from XML
	declare MatchesPlayers = Integer[CPlayer][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 (!G_LibMMLobby_DisableUI && Net_Lobby_ServersAvailable <= 0) {
				Message::SendStatusMessage(
					_("Matchmaking canceled : no server available"),
					GetLobbyEndTime() - 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[CPlayer];
					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);
									declare CurrentMatchFormat = MMCommon::GetCurrentMatchFormat();
									if (CurrentMatchFormat.count > 1) Clan += 1;
									if (Login != "" && Clan >= 0) {
										foreach (Player in AllPlayers) {
											if (Player.Login == 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);
													
													if (!IsReady(Player.User)) {
														MatchesCancelers.add(Player.User.Id);
													}
													
													UnspawnPlayer(Player);
													ShowVersusML(Player);
												}
												break;
											}
										}
									}
								}
							}
						}
					}
				}
			}
			
			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 != "") {
						foreach (Player in AllPlayers) {
							if (Player.Login == Login) {
								if (Player != Null && IsReady(Player.User) && !CantBeSubstitute.exists(Player.Id)) {
									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;
									
									UnspawnPlayer(Player);
									ShowVersusML(Player);
								}
								break;
							}
						}
					}
				}
			}
			
			// 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);
				}
				ClearMasters();
				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 = Private_GetUser(Login);
						PenalizePlayer(UserToPenalize, Penalty);
					}
				}
				
				foreach (User in Users) {
					// Cancel penalties
					if (GetPlayerPenalty(User) >= 0) {
						if (!Penalties.exists(User.Login)) {
							CancelPlayerPenalty(User);
						}
					}
				}
			}
		}
	}
	
	if (MMCommon::GetLogDisplay("APIDebug")) Private_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;
		}
	}
	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)) {
			CancelMatch(Users[UserId]);
		}
	}
}

// ---------------------------------- //
/// Send all players in their matches
Void SendToMatches() {
	declare Matches = Text[][Text];
	
	// Transert player to their servers
	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 != "") {
			MMCommon::SendToServer(Player, Lobby_MatchServer);
		}
		
		if (Lobby_IsSubstitute) {
			UIManager.UIAll.SendChat(TL::Compose(_("%1$<%2$> joined his match as a substitute."), MMCommon::GetMessagePrefix(), Player.Name));
			if (MMCommon::GetLogDisplay("MiscDebug")) Private_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 (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^Player.Login^" > Sent to match ("^Lobby_MatchId^") on server : \""^Lobby_MatchServer^"\"");
		}
	}
	
	// Display message in the chat
	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."), MMCommon::GetMessagePrefix(), PlayersNames));
	}
	
	// Reset values
	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;
	}
}

// ---------------------------------- //
/** Change rooms UI view
 *
 *	@param	_Player		The player to update
 *	@param	_View		The view to display
 */
Void SetMLRoomsView(CPlayer _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;
}

// ---------------------------------- //
/** Change rooms UI view
 *
 *	@param	_User		The user to update
 *	@param	_View		The view to display
 */
Void 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] GetRoom(CPlayer _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 GetRoomId(CPlayer _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[] GetRoomFreeSlot(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);
		}
		
		declare CurrentMatchFormat = MMCommon::GetCurrentMatchFormat();
		foreach (Clan => PlayersNb in CurrentMatchFormat) {
			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[];
}

// ---------------------------------- //
/** Remove a player from all rooms
 *
 *	@param	_Player		The player to remove
 */
Void LeaveRoom(CPlayer _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);
	}
	
	SetMLRoomsView(_Player, "Home");
}

// ---------------------------------- //
/** Create a room and add the player
 *
 *	@param	_Player		The player that creates the room
 */
Void CreateRoom(CPlayer _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]];
	
	SetMLRoomsView(_Player, "Room");
}

// ---------------------------------- //
/** Join a room
 *
 *	@param	_Player		The player that joins the room
 *	@param	_Id			The id of the room to join
 */
Void JoinRoom(CPlayer _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]];
			SetMLRoomsView(_Player, "Room");
		} else {
			declare FreeSlot = GetRoomFreeSlot(_Id);
			if (FreeSlot.count > 0) {
				Lobby_Rooms[_Id][_Player.User.Id] = [C_AllyStatus_Validated, FreeSlot[0], FreeSlot[1]];
				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 InviteRoom(CPlayer _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
		declare MaxPlayers = MMCommon::GetMaxPlayers();
		if (Room.count >= MaxPlayers) {
			foreach (AllyId => AllyInfo in Room) {
				if (AllyInfo[C_AllyInfo_Status] == C_AllyStatus_Disconnected && Lobby_Rooms[_Id].count >= MaxPlayers) {
					declare Removed = Lobby_Rooms[_Id].removekey(AllyId);
				}
			}
		}
		
		// Enough slots left
		if (Lobby_Rooms[_Id].count < MaxPlayers) {
			declare FreeSlot = GetRoomFreeSlot(_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 CancelInviteRoom(CPlayer _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 SwitchSlotRoom(CPlayer _Player, Integer _Clan, Integer _Slot) {
	if (_Player == Null || _Player.User == Null) return;
	
	declare RoomId = 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;
}

// ---------------------------------- //
/** Update the format currently used in the lobby
 *
 *	@param	_Format		The format to set
 */
Void UpdateMatchFormat(Integer[] _Format) {
	declare netwrite Integer Net_Matchmaking_FormatUpdate for Teams[0];
	declare netwrite Integer[] Net_Matchmaking_Format for Teams[0];
	Net_Matchmaking_Format = _Format;
	Net_Matchmaking_FormatUpdate = Now;
}

// ---------------------------------- //
/** Update the maximum number of players in a team
 *
 *	@param	_MaxPlayers		The maximum number of players
 */
Void MM_UpdateMaxPlayers(Integer _MaxPlayers) {
	declare netwrite Integer Net_Matchmaking_MaxPlayers for Teams[0];
	Net_Matchmaking_MaxPlayers = _MaxPlayers;
}

// ---------------------------------- //
/** Parse the player request response and
 *	setup the player accordingly
 *
 *	@param	_PlayerXml	Xml containing the player info
 */
Void 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 CPlayer Player;
				foreach (TmpPlayer in AllPlayers) {
					if (TmpPlayer.Login == Login) {
						Player <=> TmpPlayer;
						break;
					}
				}
				LogPlayerLogin = Login;
				
				if (Player != Null) {
					PenalizePlayer(Player.User, Penalty);
				}
				
				// If the player was in match, send him 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 && GetPlayerPenalty(Player.User) < 0) {
								Lobby_LastPenaltyMatchId = MatchId;
							}
						} else {
							if (MatchId != Lobby_LastPenaltyMatchId && GetPlayerPenalty(Player.User) < 0 && MatchServerLogin != "") {
								declare Lobby_ReconnectToServer for Player = "";
								Lobby_ReconnectToServer = MatchServerLogin;
							}
						}
					}
				}
			}
		}
	}
	if (MMCommon::GetLogDisplay("APIDebug")) Private_Log("[API] Response: GET /lobby-server/player-connection?login="^TL::URLEncode(LogPlayerLogin)^"&lobbylogin="^TL::URLEncode(ServerLogin)^"\n<!--\n"^_PlayerXml^"\n-->");
}

// ---------------------------------- //
/** Check if all the allies of a player are ready
 *
 *	@param	_Player		The player to check
 *
 */
Void AlliesAreReady(CPlayer _Player) {
	if (_Player == Null) return;
	
	declare Integer[Ident] Allies;
	declare CPlayer[] PlayersToUpdate;
	declare Ready = True;
	
	if (MMCommon::IsUniversalServer()) {
		declare RoomId = GetRoomId(_Player);
		declare FreeSlot = GetRoomFreeSlot(RoomId);
		if (FreeSlot.count > 0) Ready = False;
		
		declare Room = 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 < MMCommon::GetMaxPlayers()) Ready = False;
	} else {
		declare Integer[Ident] Lobby_Allies for _Player.User;
		Allies = Lobby_Allies;
		
		// Am i ready ?
		if (!IsReady(_Player.User)) Ready = False;
		PlayersToUpdate.add(_Player);
	}
	
	foreach (AllyId => AllyStatus in Allies) {
		if (AllyStatus == C_AllyStatus_Validated && Users.existskey(AllyId)) {
			declare User <=> Users[AllyId];
			declare CPlayer Player;
			foreach (TmpPlayer in AllPlayers) {
				if (TmpPlayer.Login == User.Login) {
					Player <=> TmpPlayer;
					break;
				}
			}
			if (Player != Null) PlayersToUpdate.add(Player);
			if (!IsReady(User)) Ready = False;
		}
	}
	
	// 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 GetPlayerInfo(CPlayer _Player) {
	if (_Player == Null || Http.SlotsAvailable <= 0) return;
	// Send the request to the API
	declare Request <=> Http.CreateGet(MMCommon::GetApiUrl("/lobby-server/player-connection?login="^TL::URLEncode(_Player.Login))^"&lobbylogin="^TL::URLEncode(ServerLogin), False);
	if (Request != Null) MMCommon::AddPendingRequest(Request.Id, MMCommon::RequestType_GetPlayers());
	
	if (MMCommon::GetLogDisplay("APIDebug")) Private_Log("[API] Request: GET /lobby-server/player-connection?login="^TL::URLEncode(_Player.Login)^"&lobbylogin="^TL::URLEncode(ServerLogin));
}

// ---------------------------------- //
/// Load the allies of the users from a backup
Void LoadAllies() {
	if (MMCommon::IsUniversalServer()) return;
	
	foreach (User in Users) {
		declare Integer[Ident] Lobby_Allies for User;
		declare persistent Integer[Text] Lobby_AlliesBackUp for User;
		
		// Skip allies that are already loaded
		foreach (AllyLogin => AllyStatus in Lobby_AlliesBackUp) {
			declare AllyUser <=> Private_GetUser(AllyLogin);
			if (AllyUser == Null) continue;
			if (Lobby_Allies.existskey(AllyUser.Id)) continue;
			Lobby_Allies[AllyUser.Id] = AllyStatus;
		}
	}
}

// ---------------------------------- //
/** Save the allies of an user in a backup
 *
 *	@param	_User		The user to load
 */
Void SaveAllies(CUser _User) {
	if (_User == Null) return;
	if (MMCommon::IsUniversalServer()) return;
	
	declare Integer[Ident] Lobby_Allies for _User;
	declare persistent Integer[Text] Lobby_AlliesBackUp for _User;
	Lobby_AlliesBackUp.clear();
	
	foreach (AllyId => AllyStatus in Lobby_Allies) {
		if (!Users.existskey(AllyId)) continue;
		declare User <=> Users[AllyId];
		
		Lobby_AlliesBackUp[User.Login] = AllyStatus;
	}
}

// ---------------------------------- //
/// Find the allies of each player
Void 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 (MMCommon::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;
								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;
								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 > MMCommon::GetMaxPlayers()) 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
			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
		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 <=> Private_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 = MMCommon::GetMaxPlayers() - 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
			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
			UpdatePlayersList();
		}
	}
}

// ---------------------------------- //
/** Request an ally
 *
 *	@param	_Player		The player requesting the ally
 *	@param	_Ally		The requested ally
 */
Void RequestAlly(CPlayer _Player, CPlayer _Ally) {
	if (_Player == Null || _Ally == Null) return;
	if (_Player.Id == _Ally.Id) return;
	
	if (MMCommon::IsUniversalServer()) {
		declare MyRoom = 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) {
				CancelInviteRoom(_Ally, GetRoomId(_Player));
			}
			// Send invite if enough slots left
			else {
				InviteRoom(_Ally, GetRoomId(_Player));
			}
		} 
		// Requesting access to a room
		else {
			// Accept an invitation
			declare HisRoom = GetRoom(_Ally);
			if (HisRoom.existskey(_Player.User.Id) && HisRoom[_Player.User.Id][C_AllyInfo_Status] == C_AllyStatus_Sent) {
				JoinRoom(_Player, 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;
			}
		}
	}
	
	ComputeAllies();
}

// ---------------------------------- //
/// Update the timers
Void 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 = GetLobbyEndTime() - GetLobbyStartTime();
	Net_Lobby_StartTime = GetLobbyStartTime();
	
	if (GetLobbyPhase() == C_Lobby_Playing) {
		Net_Lobby_AutoDown = False;
		Net_Lobby_TimeDown = 0;
	}
}

// ---------------------------------- //
/** Revert the timers
 *
 *	@param	_Duration		Duration of the reversal
 */
Void 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;
}

// ---------------------------------- //
/** Start the matchmaker
 *
 *	@param	_ProgressiveActivationWaitingTime		Time before the activation of the progressive matchmaking
 *	@param	_ProgressiveActivationPlayersNbRatio	Number of players before the activation of the progressive matchmaking
 */
Void MatchmakerStart(Integer _ProgressiveActivationWaitingTime, Integer _ProgressiveActivationPlayersNbRatio) {
	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 (MMCommon::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 CPlayer Player;
					foreach (TmpPlayer in AllPlayers) {
						if (TmpPlayer.Login == User.Login) {
							Player <=> TmpPlayer;
							break;
						}
					}
					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}}}
				]
			}""";
				}
			}
			
			MMCommon::SetCurrentMatchFormat(MMCommon::GetMatchFormat());
			
			// 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": {{{MMCommon::GetCurrentMatchFormat()}}},
		"matches": [
			{{{MatchesList}}}
		],
		"waitinglogins": [
			{{{WaitingLogins}}}
		],
		"cancelers": [
			{{{CancelersList}}}	
		],
		"penalties": [
			{{{PenaltiesList}}}
		]
	}""";
			
			declare Request <=> Http.CreatePost(MMCommon::GetApiUrl("/lobby-server/match"), PostData);
			if (Request != Null) Lobby_MatchMakerRequestId = Request.Id;
			
			if (MMCommon::GetLogDisplay("APIDebug")) Private_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;
				
				// Only ready players
				if (!IsReady(Player.User)) continue;
				
				// Update averate waiting time on the lobby
				if (Lobby_ReadySince > 0) {
					AverageWaitingTimeTotal += Now - Lobby_ReadySince;
					AverageWaitingTimeCount += 1;
				}
				
				// Error in transfert time, reset it
				if (MMCommon::GetLastTransfertTime(Player.User) > Now) {
					MMCommon::SetLastTransfertTime(Player.User, -1);
				}
				
				// Wait a bit of time before listing a player as ready when he was transfered to a server
				declare LastTransfertTime = MMCommon::GetLastTransfertTime(Player.User);
				if (LastTransfertTime > 0 && Now - LastTransfertTime < C_TransfertSafeTime) {
					if (MMCommon::GetLogDisplay("MiscDebug")) Private_Log("[SERVER] "^Player.Login^" > Skip ready, waiting transfert since "^Now - 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}}}]
			}""";
			}
			
			MMCommon::SetCurrentMatchFormat(MMCommon::GetMatchFormat());
			
			if (MMCommon::IsProgressiveMatchmaking()) {
				declare RequiredPlayersNb = 0;
				declare netwrite Integer Net_Lobby_PlayersNb for Teams[0];
				
				// How many players are required
				declare MatchFormat = MMCommon::GetMatchFormat();
				foreach (PlayersNb in MatchFormat) {
					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 >= _ProgressiveActivationWaitingTime 
						|| Net_Lobby_PlayersNb < RequiredPlayersNb * _ProgressiveActivationPlayersNbRatio
					)
				) {
					declare PlayingNb = 0;
					declare ProgressiveFormats = MMCommon::GetProgressiveMatchFormats();
					foreach (AvailableFormat in 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;
							MMCommon::SetCurrentMatchFormat(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": {{{MMCommon::GetCurrentMatchFormat()}}},
		"players": [
			{{{PlayersList}}}
		],
		"waitinglogins": [
			{{{WaitingLogins}}}
		],
		"cancelers": [
			{{{CancelersList}}}	
		],
		"penalties": [
			{{{PenaltiesList}}}
		]
	}""";
		
			declare Request <=> Http.CreatePost(MMCommon::GetApiUrl("/lobby-server/matchmaking-live"), PostData);
			if (Request != Null) Lobby_MatchMakerRequestId = Request.Id;
			
			if (MMCommon::GetLogDisplay("APIDebug")) Private_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;
	}
}

// ---------------------------------- //
/** Run the MatchMaker
 *
 *	@return		A list of players to unspawn
 */
Void 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) {
				GetMatches(Request.Result);
			}
			// Fail
			else {
				if (MMCommon::GetLogDisplay("APIError")) {
					if (Request.StatusCode == 401) {
						Private_Log("[ERROR] Matchmaking HTTP API Error 401. Waiting for your server to register on Nadeo master server.");
					} else if (Request.StatusCode == 404) {
						Private_Log("[ERROR] Matchmaking HTTP API Error 404. Maybe the URL in the setting is wrong.");
					} else {
						Private_Log("[ERROR] Matchmaking HTTP Error "^Request.StatusCode^".");
					}
				}
				if (MMCommon::GetErrorMessage() != "") UIManager.UIAll.SendChat(TL::Compose("%1 %2", MMCommon::GetMessagePrefix(), MMCommon::GetErrorMessage()));
			}
			ToRemove = Request.Id;
		}
	}
	if (ToRemove != NullId) {
		Http.Destroy(Http.Requests[ToRemove]);
		Lobby_MatchMakerRequestId = NullId;
	}
}

// ---------------------------------- //
/// Stop the MatchMaker
Void MatchmakerStop() {
	// Destroy any previous request
	declare Ident Lobby_MatchMakerRequestId for This;
	if (Lobby_MatchMakerRequestId != NullId) {
		if (Http.Requests.existskey(Lobby_MatchMakerRequestId)) {
			if (G_LibMMLobby_MatchmakingEnabled) {
				if (MMCommon::GetLogDisplay("APIError")) Private_Log("[ERROR] Matchmaking HTTP Error XXX. API timeout.");
				if (MMCommon::GetErrorMessage() != "") UIManager.UIAll.SendChat(TL::Compose("%1 %2", MMCommon::GetMessagePrefix(), MMCommon::GetErrorMessage()));
			}
			Http.Destroy(Http.Requests[Lobby_MatchMakerRequestId]);
			Lobby_MatchMakerRequestId = NullId;
		}
	}
	
	foreach (Player in AllPlayers) {
		HideVersusML(Player);
	}
	
	// Send players to their match
	SendToMatches();
}

// ---------------------------------- //
/** Start server in lobby mode
 *
 *	@param	_DisableUI			Disable the lobby UI
 */
Void MM_StartServer(Boolean _DisableUI) {
	// ---------------------------------- //
	// Config lobby
	EnableMatchmaking(True);
	DisableUI(_DisableUI);
	
	// ---------------------------------- //
	// Clean the masters list
	ClearMasters();
	
	// ---------------------------------- //
	// Init lobby info
	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;
	
	// ---------------------------------- //
	// Init players
	foreach (Player in AllPlayers) {
		declare Lobby_IsNew for Player = True;
		Lobby_IsNew = True;
	}
}

// ---------------------------------- //
/// Start map in lobby mode
Void MM_StartMap() {
	// ---------------------------------- //
	// 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;
	UIManager.UIAll.OverlayHideSpectatorInfos = True;
	UIManager.UIAll.OverlayHideSpectatorControllers = True;
	
	// ---------------------------------- //
	// Synchro UI
	declare netwrite Net_Lobby_SynchroServer for Teams[0] = 0;
	Net_Lobby_SynchroServer += 1;
}

// ---------------------------------- //
/** Start round in lobby mode
 *
 *	@param	_PreDuration		Duration of the pre matchmaking sequence
 *	@param	_PostDuration		Duration of the post matchmaking sequence
 *	@param	_RoundPerMap		Number of rounds played on a map
 */
Void MM_StartRound(Integer _PreDuration, Integer _PostDuration, Integer _RoundPerMap) {
	SetMatchmakingDuration(_PreDuration, _PostDuration);
	
	// ---------------------------------- //
	// Set lobby phase
	SetLobbyPhase(C_Lobby_Playing);
	
	// ---------------------------------- //
	// Initialize timers
	SetLobbyStartTime(Now);
	SetLobbyEndTime(GetLobbyStartTime() + (GetPreMatchmakingDuration() * 1000) + ML::Rand(-C_RequestRandomDeviation, C_RequestRandomDeviation));
	UpdateTimers();
	
	// ---------------------------------- //
	// Manage match cancellations
	foreach (User in Users) {
		declare Lobby_MatchCancellation for User = 0;
		declare Lobby_RoundsNbBeforeForgiveness for User = _RoundPerMap;
		if (Lobby_MatchCancellation > 0) {
			if (Lobby_RoundsNbBeforeForgiveness > 0) {
				Lobby_RoundsNbBeforeForgiveness -= 1;
			} else {
				Lobby_MatchCancellation -= 1;
				Lobby_RoundsNbBeforeForgiveness = _RoundPerMap;
			}
		} else {
			Lobby_MatchCancellation = 0;
		}
	}
	
	declare netwrite Net_Lobby_AllowMatchCancel for Teams[0] = C_AllowMatchCancel;
	declare netwrite Net_Lobby_LobbyLimitMatchCancel for Teams[0] = C_LimitMatchCancel;
	declare netwrite Net_Lobby_LobbyPenalizeSubstituteCancel for Teams[0] = C_PenalizeSubstituteCancel;
	declare netwrite Net_Lobby_WarnPenalty for Teams[0] = C_WarnPenalty;
	Net_Lobby_AllowMatchCancel = C_AllowMatchCancel;
	Net_Lobby_LobbyLimitMatchCancel = C_LimitMatchCancel;
	Net_Lobby_LobbyPenalizeSubstituteCancel = C_PenalizeSubstituteCancel;
	Net_Lobby_WarnPenalty = C_WarnPenalty;
}

// ---------------------------------- //
/** Manage XmlRpx events for the lobby
 *
 *	@return						An array of actions to execute
 */
Text[] MM_ManageXmlRpcEvents() {
	declare Text[] Actions;
	
	foreach (Event in XmlRpc.PendingEvents) {
		if (Event.Type == CXmlRpcEvent::EType::Callback) {
			switch (Event.Param1) {
				case "Matchmaking_Start": {
					EnableMatchmaking(True);
				}
				case "Matchmaking_Stop": {
					EnableMatchmaking(False);
					if (GetLobbyPhase() == C_Lobby_Matchmaking) {
						SetLobbyEndTime(Now - 1);
					}
				}
				case "Matchmaking_Force": {
					if (GetLobbyPhase() == C_Lobby_Playing) {
						SetLobbyEndTime(Now - 1);
					} else if (GetLobbyPhase() == C_Lobby_Matchmaking) {
						Message::SendStatusMessage(
							_("Matchmaking forced."),
							GetPostMatchmakingDuration() * 1000,
							1
						);
						
						SetTimersAutoDown(GetPostMatchmakingDuration() * 1000);
						SetLobbyEndTime(Now + (GetPostMatchmakingDuration() * 1000));
						
						ComputeAllies();
						Actions.add("StartMatchmaker");
					}
				}
				case "Matchmaking_GetReadyState": {
					declare CPlayer Player;
					foreach (TmpPlayer in AllPlayers) {
						if (TmpPlayer.Login == Event.Param2) {
							Player <=> TmpPlayer;
							break;
						}
					}
					SendReadyState(Player);
				}
			}
		} 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 <=> Private_GetUser(Login);
					declare ReadyState = False;
					if (Ready == "True" || Ready == "true" || Ready == "1") ReadyState = True;
					SetReady(User, ReadyState);
				}
			}
		}
	}
	
	return Actions;
}

// ---------------------------------- //
/// Update the UI
Void UpdateUI() {
	foreach (Player in AllPlayers) {
		declare UI <=> UIManager.GetUI(Player);
		if (UI != Null) {
			// Spectators
			if (!IsReady(Player.User)) {
				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 (Private_PlayerIsSpawned(Player)) {
							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 CPlayer NewAlly;
					foreach (Player in AllPlayers) {
						if (Player.Login == Net_Lobby_RequestAlly) {
							NewAlly <=> Player;
							break;
						}
					}
					if (NewAlly != Null) {
						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": {
							CreateRoom(Player);
						}
						case "LeaveRoom": {
							LeaveRoom(Player);
						}
						case "SwitchSlot": {
							declare netread Integer Net_Lobby_RequestSlot for UI;
							declare netread Integer Net_Lobby_RequestClan for UI;
							SwitchSlotRoom(Player, Net_Lobby_RequestClan, Net_Lobby_RequestSlot);
						}
					}
					
					ComputeAllies();
				}
			}
		}
	}
}

// ---------------------------------- //
/** Play loop of the lobby
 *
 *	@return						True if we need to recompute the allies
 */
Boolean MM_PlayLoop() {
	declare NeedAlliesUpdate = False;
	
	foreach (Player in AllPlayers) {
		declare Lobby_IsNew for Player = True;
		declare Lobby_PrevIsReady for Player = True;
		
		// ---------------------------------- //
		// New player on the lobby
		if (Lobby_IsNew) {
			Lobby_IsNew = False;
			// Players are not ready by default
			SetReady(Player.User, False);
			// Recompute allies when a new player join the lobby
			NeedAlliesUpdate = True;
			// Update players list
			UpdatePlayersList();
			// Request info on the player
			GetPlayerInfo(Player);
			// Choose rooms screen
			if (MMCommon::IsUniversalServer()) {
				if (GetRoomId(Player) != -1) SetMLRoomsView(Player, "Room");
				else SetMLRoomsView(Player, "Home");
			}
			// Save last matchmaker running time
			declare Lobby_LastMatchmakerTime for Player = Now;
			Lobby_LastMatchmakerTime = Now;
		}
		
		// ---------------------------------- //
		// Player switch between ready and unready
		UpdateReady(Player);
		if (Lobby_PrevIsReady != IsReady(Player.User)) {
			Lobby_PrevIsReady = IsReady(Player.User);
			AlliesAreReady(Player);
			declare Integer Lobby_ReadySince for Player = -1;
			
			// See if we need to cancel a match
			if (!IsReady(Player.User)) {
				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 (MMCommon::IsUniversalServer()) {
					if (GetRoomId(Player) < 0) {
						SetReady(Player.User, False);
					}
				}
			}
		}
		
		// ---------------------------------- //
		// Update the karma of the player
		UpdateKarma(Player.User);
		
		// ---------------------------------- //
		// Try to reconnect to the match server after a leave
		ReconnectToServer(Player);
	}
	
	return NeedAlliesUpdate;
}

// ---------------------------------- //
/// End round in lobby mode
Void MM_EndRound() {

}

// ---------------------------------- //
/// End map in lobby mode
Void MM_EndMap() {

}

// ---------------------------------- //
/// End server in lobby mode
Void MM_EndServer() {
	UIManager.UIAll.OverlayHideCountdown = False;
	UIManager.UIAll.AltMenuNoCustomScores = False;
	UIManager.UIAll.AltMenuNoDefaultScores = False;
	UIManager.UIAll.OverlayHideSpectatorInfos = False;
	UIManager.UIAll.OverlayHideSpectatorControllers = False;
}

// ---------------------------------- //
/** Create the header manialink
 *
 *	@return		The manialink
 */
Text GetMLHeader() {
	declare Hidden = "0";
	if (G_LibMMLobby_DisableUI) 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

{{{InjectReadyHelpers()}}}

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 = "";
	declare PrevIsReady = True;
	
	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));
		}
		
		if (PrevMessage != UI.StatusMessage || PrevIsReady != Private_Lobby_IsReady()) {
			PrevIsReady = Private_Lobby_IsReady();
			PrevMessage = UI.StatusMessage;
			if (UI.StatusMessage != "" || !Private_Lobby_IsReady()) Label_Message.Visible = False;
			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 gauge timer manialink
 *
 *	@return		The manialink
 */
Text GetMLGaugeTimer() {
	declare Hidden = "0";
	if (G_LibMMLobby_DisableUI) 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

{{{InjectReadyHelpers()}}}

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. * (GameTime - Net_Lobby_StartTime)) / Net_Lobby_TimerMax;
				if(Ratio < 0.) Ratio = 0.;
				if(Ratio > 1.) Ratio = 1.;
				Gauge_Timer.Ratio = Ratio;
			}
		}
		
		if (!Private_Lobby_IsReady()) {
			Label_WaitTime.Hide();
			WaitingTime = 0;
		} else {
			WaitingTime += Period;
			Label_WaitTime.Show();					
			Label_WaitTime.SetText(TextLib::TimeToText(WaitingTime));
		}
	}
}
--></script>
</manialink>
""";
}

// ---------------------------------- //
/** Create the send to server manialink
 *
 *	@param	_Rules		The rules to display
 *
 *	@return				The manialink
 */
Text GetMLRules(Text _Rules) {
	if (_Rules == "") return "";
	
	return """
<manialink version="1" name="ModeMatchmaking:Rules">
<frame posn="0 0 30" hidden="1" id="Frame_Global">
	{{{_Rules}}}
</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>""";
}

// ---------------------------------- //
/** Create the players list manialink
 *
 *	@return		The manialink
 */
Text 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/Common/Lobbies/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Common/Lobbies/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Common/Lobbies/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Common/Lobbies/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"])}}}

{{{InjectReadyHelpers()}}}

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.>;
		else if (!Private_Lobby_IsReady(_Player)) Quad_Status.BgColor = <0.8, 0., 0.>; 
		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[CPlayer] 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;
		} else if (!Private_Lobby_IsReady(Player)) {
			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);
		}
		
		if (Private_Lobby_IsReady(Player) && Net_Lobby_AlliesAreReady) {
			Lobby_ReadyNb += 1;
		}
	}
	
	declare CPlayer[] 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 == {{{MMCommon::MatchmakingMode_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) {
				declare PrevIsReady for Player = True;
				declare PrevAlliesAreReady for Player = False;
				declare netread Boolean Net_Lobby_AlliesAreReady for Player;
				if (PrevIsReady != Private_Lobby_IsReady(Player) || PrevAlliesAreReady != Net_Lobby_AlliesAreReady) {
					PrevIsReady = Private_Lobby_IsReady(Player);
					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 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/Common/Lobbies/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Common/Lobbies/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Common/Lobbies/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Common/Lobbies/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 waiting screen manialink
 *
 *	@param	_DisplayRules	Allow to display a manialink with the rules of the mode
 *
 *	@return		The manialink
 */
Text GetMLWaitingScreen(Boolean _DisplayRules) {
	declare Hidden = "0";
	if (G_LibMMLobby_DisableUI) 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/Common/Lobbies/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Common/Lobbies/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Common/Lobbies/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Common/Lobbies/button-show.png";
	
	declare HideResumePlayingButton = "";
	if (This is CSmMode) {
		HideResumePlayingButton = "HideResumePlayingButton = True;";
	}
	
	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")}}}" id="Label_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">
		<quad sizen="48 12" halign="center" valign="center" image="{{{ButtonReadyOff}}}" imagefocus="{{{ButtonReadyOn}}}" scriptevents="1" id="Button_Ready" />
		<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"])}}}
{{{InjectReadyHelpers()}}}

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 Frame_PlayerList	<=> (Frame_Global.GetFirstChild("Frame_PlayerList")	as CMlFrame);
	declare Label_Rules			<=> (Frame_Global.GetFirstChild("Label_Rules")		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;
	
	declare PrevIsReady = False;
	declare PrevFormatUpdate = -1;
	declare PrevAlliesLoginUpdate = -1;
	declare PrevIsBlocked = False;
	declare PrevReconnectToServer = "";
	declare PrevMatchmakingMode = -1;
	declare PrevRulesAreVisible = False;
		
	{{{HideResumePlayingButton}}}
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		LibManialink_AnimLoop();
		
		if (PrevRulesAreVisible != Lobby_RulesAreVisible) {
			PrevRulesAreVisible = Lobby_RulesAreVisible;
			
			Frame_PlayerList.Visible = !Lobby_RulesAreVisible;
			if (Lobby_RulesAreVisible) {
				Label_Rules.Value = "{{{_("Allies")}}}";
			} else {
				Label_Rules.Value = "{{{_("Rules")}}}";
			}
		}
		
		if (PrevIsReady != Private_Lobby_IsReady() || PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevIsReady = Private_Lobby_IsReady();
			PrevReconnectToServer = Net_Lobby_ReconnectToServer;
			
			if (!Private_Lobby_IsReady() && Net_Lobby_ReconnectToServer == "") {
				Frame_Global.Visible = True;
			} else {
				Frame_Global.Visible = False;
				Lobby_RulesAreVisible = 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 - GameTime + 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 = !Lobby_RulesAreVisible;
					}
					case "Button_Ready": {
						Private_Lobby_SetReady(True);
					}
					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") {
					if (
						(Net_Lobby_ShowSubstituteML || Net_Lobby_ShowVersusML)
						&& !Net_Lobby_AllowMatchCancel && Private_Lobby_IsReady()
					) {
					
					} else if (!Net_Lobby_ImBlocked) {
						Private_Lobby_ToggleReady();
					}
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the rooms manialink
 *
 *	@return		The manialink
 */
Text GetMLRooms() {
	declare Hidden = "0";
	if (G_LibMMLobby_DisableUI) 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/Common/Lobbies/button-hide-on.png";
	declare ButtonHideOff = "file://Media/Manialinks/Common/Lobbies/button-hide.png";
	declare ButtonShowOn = "file://Media/Manialinks/Common/Lobbies/button-show-on.png";
	declare ButtonShowOff = "file://Media/Manialinks/Common/Lobbies/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>""";
	}
	
	declare HideResumePlayingButton = "";
	if (This is CSmMode) {
		HideResumePlayingButton = "HideResumePlayingButton = True;";
	}
	
	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"])}}}

{{{InjectReadyHelpers()}}}

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") {
						Private_Lobby_ToggleReady();
					} 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;
	declare PrevIsReady = False;
	declare PrevReconnectToServer = "";
	declare PrevFormatUpdate = -1;
	declare PrevRoomsView = "";
	declare PrevRoomLoginsUpdate = -1;
	declare PrevIsBlocked = False;
	
	{{{HideResumePlayingButton}}}
	
	while (True) {
		yield;
		if (InputPlayer == Null || !PageIsVisible) continue;
		
		LibManialink_AnimLoop();
		
		if (PrevIsReady != Private_Lobby_IsReady() || PrevReconnectToServer != Net_Lobby_ReconnectToServer) {
			PrevIsReady = Private_Lobby_IsReady();
			PrevReconnectToServer = Net_Lobby_ReconnectToServer;
			
			if (!Private_Lobby_IsReady() && Net_Lobby_ReconnectToServer == "") {
				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 - GameTime + 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)
						&& !Net_Lobby_AllowMatchCancel && Private_Lobby_IsReady()
					) {
					
					} else if (!Net_Lobby_ImBlocked && Net_Lobby_RoomLogins.count > 0) {
						Private_Lobby_ToggleReady();
					}
				}
			} 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>""";
}

// ---------------------------------- //
/** 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 GetMLLobbyScreen(Boolean _DisplayRules) {
	if (MMCommon::IsUniversalServer()) {
		return GetMLRooms();
	} else {
		return GetMLWaitingScreen(_DisplayRules);
	}
	
	return "";
}

// ---------------------------------- //
/** Create the versus manialink
 *
 *	@return		The manialink
 */
Text 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">
		<quad sizen="35 8" scale="1.3" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" scriptevents="1" id="Button_Cancel" />
		<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;

{{{InjectReadyHelpers()}}}

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;
	declare PrevIsReady = True;
	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;
		}
		
		if (PrevIsReady != Private_Lobby_IsReady()) {
			PrevIsReady = Private_Lobby_IsReady();
			if (!Private_Lobby_IsReady()) {
				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);
				}
				else if (Event.ControlId == "Button_Cancel") {
					Private_Lobby_SetReady(False);
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the substitute manialink
 *
 *	@return		The manialink
 */
Text 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">
		<quad sizen="35 8" scale="1.3" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" scriptevents="1" id="Button_Cancel" />
		<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

{{{InjectReadyHelpers()}}}

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;
	declare PrevIsReady = True;
	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 MatchBestScore = "";
				declare SplittedScores = TL::Split("-", Net_Lobby_MatchScores);
				if (SplittedScores.count == 2) MatchScores = SplittedScores[0]^" - "^SplittedScores[1];
				else if (SplittedScores.count == 1) MatchBestScore = SplittedScores[0];
				
				if (MatchScores != "") Label_SubMessage.Value = TL::Compose("%1 : %2", "{{{_("Current match score")}}}", MatchScores);
				else if (MatchBestScore != "") Label_SubMessage.Value = TL::Compose("%1 : %2", "{{{_("Current match best score")}}}", MatchBestScore);
				else Label_SubMessage.Value = "{{{_("Please wait.")}}}";
			}
		}
		
		if (PrevIsReady != Private_Lobby_IsReady()) {
			PrevIsReady = Private_Lobby_IsReady();
			if (!Private_Lobby_IsReady()) {
				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;
			}
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Cancel") {
					Private_Lobby_SetReady(False);
				}
			}
		}
	}
}
--></script>
</manialink>""";
}

// ---------------------------------- //
/** Create the reconnect manialink
 *
 *	@return		The manialink
 */
Text 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">
		<quad sizen="35 8" scale="1.3" halign="center" valign="center" image="{{{ButtonQuitOff}}}" imagefocus="{{{ButtonQuitOn}}}" scriptevents="1" id="Button_Cancel" />
		<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

{{{InjectReadyHelpers()}}}

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 > GameTime) {
			Label_Countdown.Value = TL::TimeToText(UI.CountdownEndTime - GameTime + 1000);
		}
		
		foreach (Event in PendingEvents) {
			if (Event.Type == CMlEvent::Type::MouseClick) {
				if (Event.ControlId == "Button_Cancel") {
					Private_Lobby_SetReady(True);
				}
			}
		}
	}
}
--></script>
</manialink>""";
}