local R={}local M=nil local e=nil local l=nil local r={}function Lobby._teleportWithRetry(R,M,e,l,r,Z)local F F=TeleportService.TeleportInitFailed:Connect(function(P,q,k)if P==e then Log.Error.Lobby("\084\101\108\101\112\111\114\116 \102\097\105\108\101\100 \102\111\114 \037\115\044 \084\101\108\101\112\111\114\116\082\101\115\117\108\116\058 \037\115",e.Name,q.Name)if q==Enum.TeleportResult.Failure or q==Enum.TeleportResult.Flooded then F:Disconnect()delay(2,function()Log.Warn.Lobby("\082\101\097\116\116\101\109\112\116\105\110\103 \116\101\108\101\112\111\114\116")TeleportService:TeleportToPrivateServer(R,M,{e},l,r,Z)end)end end end)TeleportService:TeleportToPrivateServer(R,M,{e},l,r,Z)end function Lobby._retryJoin(R)local M=Players:GetPlayerByUserId(R.player_id)if M then Log.Warn.Lobby("\082\101\116\114\121\105\110\103")Lobby._teleportPlayer(M,R.place_id,R.game_mode)else _pendingTickets[R.player_id]=nil end end function Lobby._requestServerList()Log.Debug.Lobby("\114\101\113\117\101\115\116\083\101\114\118\101\114\076\105\115\116")local R={type="\065\078\078\079\085\078\067\069\058\082\069\081\085\069\083\084",lobby=game.JobId}MessagingService:PublishAsync("\083\069\082\086\069\082",R)end function Lobby._getClientServerData()local R={}for M,e in pairs(_serverList)do local l={server=M;slots_available=e.slots_available,nonresponsive=_nonresponsiveServers[M]}table.insert(R,l)end return R end function Lobby._sendServerListToAllClients()local R=Lobby._getClientServerData()e:FireAllClients(R)end function Lobby._sendServerListToClient(R)local M=Lobby._getClientServerData()e:FireClient(R,M)end function Lobby._handleServerAnnounce(M)local e=M.Data local l=e.server local r=e.creation_id Log.Debug.Lobby("\072\101\097\114\100 \102\114\111\109 \115\101\114\118\101\114\058 \037\115",l)Log.Debug.LobbyData(Util.tableToString(M))_serverList[l]=e _nonresponsiveServers[l]=nil if r then R[r]=nil end Lobby._sendServerListToAllClients()end function Lobby._handleServerRemoval(R)local M=R.Data.server Log.Debug.Lobby("\083\101\114\118\101\114 \037\115 \114\101\109\111\118\101\100",M)local e={}_serverList[M]=nil for R,l in pairs(_pendingTickets)do if l.server==M then table.insert(e,l)end end for R,M in ipairs(e)do local e=M.player_id _pendingTickets[e]=nil local l=Players:GetPlayerByUserId(e)Lobby._teleportPlayer(l,M.place_id,M.game_mode)end Lobby._sendServerListToAllClients()end function Lobby._sendJoinRequest(R,M,e,r)local Z=R.server local F={type="\074\079\073\078\058\082\069\081\085\069\083\084";lobby=game.JobId,player=M.UserId;count=1}l:FireClient(M,{status=string.format("\067\111\110\110\101\099\116\105\110\103 \116\111 \037\115",Z)})Log.Debug.Lobby("\074\111\105\110 \114\101\113\117\101\115\116 \115\101\110\116 \116\111 \037\115 \102\111\114 \112\108\097\121\101\114 \037\115",Z,M.UserId)MessagingService:PublishAsync(tostring(Z),F)_pendingTickets[M.UserId]={player_id=M.UserId;place_id=R.place_id;server=Z;game_mode=e;time=tick(),match_key=r}end function Lobby._handleJoinResponse(R)local M=R.Data.status local e=R.Data.token local l=R.Data.player local r=Players:GetPlayerByUserId(l)local Z=_pendingTickets[l]_pendingTickets[l]=nil if M==Util._joinCodes.SUCCESS then if Z then Lobby._teleportWithRetry(Z.place_id,e,r)end else Log.Warn.JoinFailure("\080\108\097\121\101\114 \037\115 \102\097\105\108\101\100 \116\111 \106\111\105\110 \037\115 \040\037\115\041",l,R.Data.server,Util.getJoinCodeText(M))if Z then Lobby._teleportPlayer(r,_pendingTickets.place_id,_pendingTickets.game_mode)end end end function Lobby._handleJoinExpired(R)local M=R.Data.player Log.Error.LobbyJoin("\069\120\112\105\114\101\100 \037\115",M)_pendingTickets[M]=nil end function Lobby._registerHandler(R,M)r[R]=M end function Lobby._dispatchMessage(R)local M=r[R.Data.type]if M then M(R)else end end function Lobby._generateMatchKey(R,M)return string.format("\037\115\058\037\115",R,M)end function Lobby._teleportPlayer(M,e,r)local Z=Lobby._generateMatchKey(e,r)l:FireClient(M,{status="\087\097\105\116\105\110\103\046\046\046"})while true do local M=false local e={}for R,l in pairs(R)do if l.match_key==Z then if tick()-l.time>=_serverCreationTimeout then table.insert(e,R)Log.Error.Lobby("\083\101\114\118\101\114 \099\114\101\097\116\105\111\110 \'\037\115\' \101\120\099\101\101\100\101\100 \116\105\109\101\111\117\116",R)else M=true end end end for M,e in ipairs(e)do R[e]=nil end if not M then break end wait(1)end for R,e in pairs(_serverList)do if e.match_key==Z and(e.slots_available>0 and not Lobby._isServerBlacklisted(M,e.server))then Lobby._sendJoinRequest(e,M,r,Z)return end end l:FireClient(M,{status="\067\114\101\097\116\105\110\103 \102\114\101\115\104 \115\101\114\118\101\114"})local F=TeleportService:ReserveServer(e)Log.Debug.Lobby("\082\101\115\101\114\118\101\100 \037\115 \102\111\114 \112\108\097\099\101 \037\115",F,e)l:FireClient(M,{status="\084\101\108\101\112\111\114\116\105\110\103"})local P={lobby=game.JobId,creation_id=HttpService:GenerateGUID(),token=F,time=tick();match_key=Z,place_id=e,game_mode=r}R[P.creation_id]=P Lobby._teleportWithRetry(e,F,M,nil,P)end function Lobby._detectDeadServers()local R={}local M=false for M,e in pairs(_pendingTickets)do if tick()-e.time>=_serverResponseTimeout then R[M]=e end end for R,e in pairs(R)do _pendingTickets[R]=nil local l=Players:GetPlayerByUserId(R)Lobby._recordServerFailure(l,e.server)M=true Log.Warn.Lobby("\068\101\097\100 \115\101\114\118\101\114 \100\101\116\101\099\116\101\100\058 \037\115",e.server)Lobby._retryJoin(e)end if M then Lobby._sendServerListToAllClients()end end function Lobby._recordServerFailure(R,M)local e=R.UserId local l=_playerServerFailures[e]if not l then l={}_playerServerFailures[e]=l end l[M]=(l[M]or 0)+1 _nonresponsiveServers[M]=(_nonresponsiveServers[M]or 0)+1 end function Lobby._isServerBlacklisted(R,M)local e=_playerServerFailures[R.UserId]return e and((e[M]or 0)>=Conf.lobby_teleportation_retries or _nonresponsiveServers[M])end function Lobby._onJoin(R,M)local e=_pendingTickets[R.UserId]if M.type=="\074\079\073\078"then local r=M.queue_selection or Conf.default_game_mode local Z=Conf.modes[r].temp_queue_place if Conf.lobby_use_boring_flow then TeleportService:Teleport(Z,R)l:FireClient(R,{status="\081\117\101\117\101\105\110\103\046\046\046"})else if e then Log.Warn.Lobby("\085\115\101\114 \037\115 \097\108\114\101\097\100\121 \104\097\115 \112\101\110\100\105\110\103 \106\111\105\110 \114\101\113\117\101\115\116\046",R.Name)return end Log.Debug.Lobby("\080\108\097\121\101\114 \106\111\105\110 \114\101\113\117\101\115\116\101\100")_joinQueue[R.UserId]={player=R,place_id=Z;game_mode=r}TeleportService:Teleport(Z,R)l:FireClient(R,{status="\081\117\101\117\101\105\110\103\046\046\046"})end elseif M.type=="\067\065\078\067\069\076"then _pendingTickets[R.UserId]=nil l:FireClient(R,{canceled=true})elseif M.type=="\070\065\073\076\085\082\069"then Log.Error.Lobby("\067\108\105\101\110\116 \116\101\108\101\112\111\114\116 \102\097\105\108\101\100\058 \037\115 \040\037\115\041",R.Name,M.message)if e then Lobby._recordServerFailure(R,e.server)Lobby._retryJoin(e)end end end function Lobby._onPlayerRemoving(R)_pendingTickets[R.UserId]=nil end function Lobby._onPlayerAdded(R)Lobby._sendServerListToClient(R)end function Lobby._onTeleportFailure(R,M,e)Log.Error.Lobby("\084\101\108\101\112\111\114\116 \070\097\105\108\101\100\058 \037\115 \040\037\115\041",R.UserId,e)end function Lobby._processJoinQueue()local R=_joinQueue _joinQueue={}for R,M in pairs(R)do Lobby._teleportPlayer(M.player,M.place_id,M.game_mode)end end

local _pendingServerCreation = {}

local _joinEvent = nil
local _serverListEvent = nil
local _statusEvent = nil

local _messageHandlers = {}

function Lobby._teleportWithRetry(placeId, token, player, spawnName, teleportData, customLoadingScreen)
	local connection

	connection = TeleportService.TeleportInitFailed:Connect(function(failedPlayer, status, errorMessage)
		if failedPlayer == player then
			Log.Error.Lobby("Teleport failed for %s, TeleportResult: %s", player.Name, status.Name)

			if status == Enum.TeleportResult.Failure or status == Enum.TeleportResult.Flooded then
				connection:Disconnect()

				delay(2, function()
					Log.Warn.Lobby("Reattempting teleport")
					TeleportService:TeleportToPrivateServer(placeId, token, {player}, spawnName, teleportData, customLoadingScreen)
				end)
			end
		end
	end)

	TeleportService:TeleportToPrivateServer(placeId, token, {player}, spawnName, teleportData, customLoadingScreen)
end

function Lobby._retryJoin(pending)
	local player = Players:GetPlayerByUserId(pending.player_id)

	if player then
		Log.Warn.Lobby("Retrying")
		Lobby._teleportPlayer(player, pending.place_id, pending.game_mode)

	else
		-- This player didn't make it.  Long live this player.
		_pendingTickets[pending.player_id] = nil
	end
end

function Lobby._requestServerList()
	Log.Debug.Lobby("requestServerList")

	local req = {
		type = "ANNOUNCE:REQUEST",
		lobby = game.JobId,
	}

	MessagingService:PublishAsync("SERVER", req)
end

function Lobby._getClientServerData()
	local data = {}

	for serverId, serverData in pairs(_serverList) do
		local entry = {
			server = serverId,
			slots_available = serverData.slots_available,
			nonresponsive = _nonresponsiveServers[serverId],
		}

		table.insert(data, entry)
	end

	return data
end

function Lobby._sendServerListToAllClients()
	-- Sending everything all at once right now
	local data = Lobby._getClientServerData()
	_serverListEvent:FireAllClients(data)
end

function Lobby._sendServerListToClient(player)
	local data = Lobby._getClientServerData()
	_serverListEvent:FireClient(player, data)
end

function Lobby._handleServerAnnounce(pub)
	local data = pub.Data
	local server = data.server
	local creationId = data.creation_id

	Log.Debug.Lobby("Heard from server: %s", server)
	Log.Debug.LobbyData(Util.tableToString(pub))

	_serverList[server] = data
	_nonresponsiveServers[server] = nil

	if creationId then
		_pendingServerCreation[creationId] = nil
	end

	Lobby._sendServerListToAllClients()
end

function Lobby._handleServerRemoval(pub)
	local removedServer = pub.Data.server

	Log.Debug.Lobby("Server %s removed", removedServer)

	local removals = {}

	-- Remove the actual server entry
	_serverList[removedServer] = nil

	-- Removing any pending joins to the removed server
	for playerId, pendingData in pairs(_pendingTickets) do
		if pendingData.server == removedServer then
			table.insert(removals, pendingData)
		end
	end

	for _, pendingData in ipairs(removals) do
		local playerId = pendingData.player_id

		_pendingTickets[playerId] = nil

		-- This player was attempting to join to the removed server, so send them to another one.
		local player = Players:GetPlayerByUserId(playerId)
		Lobby._teleportPlayer(player, pendingData.place_id, pendingData.game_mode)
	end

	Lobby._sendServerListToAllClients()
end

function Lobby._sendJoinRequest(serverData, player, mode, destinationKey)
	local server = serverData.server

	local req = {
		type = "JOIN:REQUEST",
		lobby = game.JobId,
		player = player.UserId,
		count = 1,
	}

	_statusEvent:FireClient(player, { status = string.format("Connecting to %s", server) })

	Log.Debug.Lobby("Join request sent to %s for player %s", server, player.UserId)

	MessagingService:PublishAsync(tostring(server), req)

	_pendingTickets[player.UserId] = {
		player_id = player.UserId,
		place_id = serverData.place_id,
		server = server,
		game_mode = mode,
		time = tick(),
		match_key = destinationKey,
	}
end

function Lobby._handleJoinResponse(pub)
	local status = pub.Data.status
	local token = pub.Data.token
	local userId = pub.Data.player
	local player = Players:GetPlayerByUserId(userId)

	local pendingData = _pendingTickets[userId]
	_pendingTickets[userId] = nil

	if status == Util._joinCodes.SUCCESS then
		-- TODO:  Warn on no pending ticket

		if pendingData then
			Lobby._teleportWithRetry(pendingData.place_id, token, player)
		end

	else
		Log.Warn.JoinFailure("Player %s failed to join %s (%s)",
							 userId,
							 pub.Data.server,
							 Util.getJoinCodeText(status))

		-- Join a different server
		-- .If the player was forbidden, may need to store that information somehow so we don't try the same one again
		if pendingData then
			Lobby._teleportPlayer(player, _pendingTickets.place_id, _pendingTickets.game_mode)
		end
	end
end

function Lobby._handleJoinExpired(pub)
	local playerId = pub.Data.player

	Log.Error.LobbyJoin("Expired %s", playerId)

	_pendingTickets[playerId] = nil
end

function Lobby._registerHandler(messageType, handler)
	_messageHandlers[messageType] = handler
end

function Lobby._dispatchMessage(pub)
	local handler = _messageHandlers[pub.Data.type]

	if handler then
		handler(pub)
	else
		-- TODO:  Error
	end
end

function Lobby._generateMatchKey(placeId, mode)
	return string.format("%s:%s", placeId, mode)
end

function Lobby._teleportPlayer(player, destinationId, mode)
	local destinationKey = Lobby._generateMatchKey(destinationId, mode)

	-- If there are pending server creations, we wait until they have completed or timed out
	-- .We could continuously iterate through the server list to see if another server comes online during this time, but for this case that complicates things needlessly.

	-- TODO:  Evaluate whether this needs to be wrapped in a coroutine or similar and whether that should be done implicitly here
	-- .Currently _processJoinQueue is wrapped, but honoring the queue is done in a single thread.
	-- .Also, in that same high-level loop, _detectDeadServers would inherently pause against the completion of _processJoinQueue, which could account for some old stuff.

	_statusEvent:FireClient(player, { status = "Waiting..." })

	while true do
		local waiting = false
		local creationTimeouts = {}

		for creationId, pendingCreation in pairs(_pendingServerCreation) do
			if pendingCreation.match_key == destinationKey then
				if tick() - pendingCreation.time >= _serverCreationTimeout then
					table.insert(creationTimeouts, creationId)
					Log.Error.Lobby("Server creation '%s' exceeded timeout", creationId)
				else
					waiting = true
				end
			end
		end

		for _, creationId in ipairs(creationTimeouts) do
			_pendingServerCreation[creationId] = nil
		end

		if not waiting then
			break
		end

		wait(1)
	end

	for _, serverData in pairs(_serverList) do
		if serverData.match_key == destinationKey and serverData.slots_available > 0 and not Lobby._isServerBlacklisted(player, serverData.server) then
			Lobby._sendJoinRequest(serverData, player, mode, destinationKey)
			return
		end
	end

	_statusEvent:FireClient(player, { status = "Creating fresh server" })

	-- Create a gameplay server
	local token = TeleportService:ReserveServer(destinationId)
	Log.Debug.Lobby("Reserved %s for place %s", token, destinationId)

	_statusEvent:FireClient(player, { status = "Teleporting" })

	-- HACK:  The token is sent along with the teleport data because there's no way to get access to it from the server itself
	local creationData = {
		lobby = game.JobId,
		creation_id = HttpService:GenerateGUID(),
		token = token,
		time = tick(),
		match_key = destinationKey,
		place_id = destinationId,
		game_mode = mode,
	}

	_pendingServerCreation[creationData.creation_id] = creationData

	Lobby._teleportWithRetry(destinationId, token, player, nil, creationData)
end

function Lobby._detectDeadServers()
	local expirations = {}
	local updateClients = false

	for playerId, request in pairs(_pendingTickets) do
		if tick() - request.time >= _serverResponseTimeout then
			expirations[playerId] = request
		end
	end

	for playerId, request in pairs(expirations) do
		_pendingTickets[playerId] = nil

		-- Flag this server as non-responsive until it's heard from again
		local player = Players:GetPlayerByUserId(playerId)

		Lobby._recordServerFailure(player, request.server)
		updateClients = true

		Log.Warn.Lobby("Dead server detected: %s", request.server)

		Lobby._retryJoin(request)
	end

	if updateClients then
		Lobby._sendServerListToAllClients()
	end
end

function Lobby._recordServerFailure(player, server)
	local userId = player.UserId
	local failureList = _playerServerFailures[userId]

	if not failureList then
		failureList = {}
		_playerServerFailures[userId] = failureList
	end

	failureList[server] = (failureList[server] or 0) + 1

	_nonresponsiveServers[server] = (_nonresponsiveServers[server] or 0) + 1
end

function Lobby._isServerBlacklisted(player, server)
	local failureList = _playerServerFailures[player.UserId]
	return failureList and
		((failureList[server] or 0) >= Conf.lobby_teleportation_retries or
			_nonresponsiveServers[server])
end

function Lobby._onJoin(player, data)
	local pending = _pendingTickets[player.UserId]

	if data.type == "JOIN" then
		local mode = data.queue_selection or Conf.default_game_mode
		local destinationId = Conf.modes[mode].temp_queue_place

		if Conf.lobby_use_boring_flow then
			-- Alternate queue place
			TeleportService:Teleport(destinationId, player)
			_statusEvent:FireClient(player, { status = "Queueing..." })
		else
			if pending then
				Log.Warn.Lobby("User %s already has pending join request.", player.Name)
				return
			end

			Log.Debug.Lobby("Player join requested")

			_joinQueue[player.UserId] = {
				player = player,
				place_id = destinationId,
				game_mode = mode,
			}

			TeleportService:Teleport(destinationId, player)
			_statusEvent:FireClient(player, { status = "Queueing..." })
		end

	elseif data.type == "CANCEL" then
		_pendingTickets[player.UserId] = nil
		_statusEvent:FireClient(player, { canceled = true })

	elseif data.type == "FAILURE" then
		-- The client failed during teleportation and we need to retry or blacklist the current server
		Log.Error.Lobby("Client teleport failed: %s (%s)", player.Name, data.message)

		if pending then
			Lobby._recordServerFailure(player, pending.server)
			Lobby._retryJoin(pending)
		end
	end
end

function Lobby._onPlayerRemoving(player)
	_pendingTickets[player.UserId] = nil
	-- TODO:  Should this directly tell any server waiting for this player that they won't be making it?
	-- .Currently that will be handled by the ticket timeout
end

function Lobby._onPlayerAdded(player)
	Lobby._sendServerListToClient(player)
end

function Lobby._onTeleportFailure(player, status, message)
	Log.Error.Lobby("Teleport Failed: %s (%s)", player.UserId, message)
end

function Lobby._processJoinQueue()
	local queue = _joinQueue
	_joinQueue = {}

	for _, joinData in pairs(queue) do
		Lobby._teleportPlayer(joinData.player, joinData.place_id, joinData.game_mode)
	end
end
