diff --git a/config/queue.lua b/config/queue.lua new file mode 100644 index 000000000..fad5e0823 --- /dev/null +++ b/config/queue.lua @@ -0,0 +1,8 @@ +return { + ---Amount of seconds to wait before removing a player from the queue after disconnecting while waiting. + timeoutSeconds = 30, + + ---Amount of seconds to wait before removing a player from the queue after disconnecting while installing server data. + ---Notice that an additional ~2 minutes will be waited due to limitations with how FiveM handles joining players. + joiningTimeoutSeconds = 0, +} diff --git a/server/events.lua b/server/events.lua index 67fbfb56b..2b3a3095a 100644 --- a/server/events.lua +++ b/server/events.lua @@ -16,10 +16,13 @@ AddEventHandler('chatMessage', function(_, _, message) end) AddEventHandler('playerJoining', function() - if not serverConfig.checkDuplicateLicense then return end local src = source --[[@as string]] local license = GetPlayerIdentifierByType(src, 'license2') or GetPlayerIdentifierByType(src, 'license') if not license then return end + if queue then + queue.removePlayerJoining(license) + end + if not serverConfig.checkDuplicateLicense then return end if usedLicenses[license] then Wait(0) -- mandatory wait for the drop reason to show up DropPlayer(src, Lang:t('error.duplicate_license')) diff --git a/server/queue.lua b/server/queue.lua index 87cd84997..4b46d48ba 100644 --- a/server/queue.lua +++ b/server/queue.lua @@ -17,6 +17,7 @@ end -- Queue code +local config = require 'config.queue' local maxPlayers = GlobalState.MaxPlayers ---Player license to queue position map. @@ -47,16 +48,17 @@ end ---Map of player licenses that passed the queue and are downloading server content. ---Needs to be saved because these players won't be part of regular player counts such as `GetNumPlayerIndices`. ----@type table +---@type table local joiningPlayers = {} local joiningPlayerCount = 0 +---@param source Source ---@param license string -local function addPlayerJoining(license) +local function updatePlayerJoining(source, license) if not joiningPlayers[license] then joiningPlayerCount += 1 end - joiningPlayers[license] = true + joiningPlayers[license] = { source = source, timestamp = os.time() } end ---@param license string @@ -67,16 +69,54 @@ local function removePlayerJoining(license) joiningPlayers[license] = nil end +---@type table +local timingOut = {} + +---@param license string +---@return boolean shouldDequeue +local function awaitPlayerTimeout(license) + timingOut[license] = true + + Wait(config.timeoutSeconds * 1000) + + -- if timeout data wasn't consumed then the player hasn't reconnected + if timingOut[license] then + timingOut[license] = nil + return true + end + + return false +end + +---@param license string +---@return boolean playerTimingOut +local function isPlayerTimingOut(license) + local playerTimingOut = timingOut[license] or false + timingOut[license] = nil + return playerTimingOut +end + ---@param source Source ---@param license string ---@param deferrals Deferrals local function awaitPlayerQueue(source, license, deferrals) - if playerPositions[license] then + if joiningPlayers[license] then + -- the player was in the middle of joining, so let them in + updatePlayerJoining(source, license) + deferrals.done() + return + end + + local playerTimingOut = isPlayerTimingOut(license) + + if playerPositions[license] and not playerTimingOut then deferrals.done(Lang:t('error.already_in_queue')) return end - enqueue(license) + if not playerTimingOut then + enqueue(license) + end -- wait until the player disconnected or until there are available slots and the player is first in queue while DoesPlayerExist(source --[[@as string]]) and ((GetNumPlayerIndices() + joiningPlayerCount) >= maxPlayers or playerPositions[license] > 1) do @@ -90,22 +130,40 @@ local function awaitPlayerQueue(source, license, deferrals) -- if the player disconnected while waiting in queue if not DoesPlayerExist(source --[[@as string]]) then - dequeue(license) + if awaitPlayerTimeout(license) then + dequeue(license) + end return end - addPlayerJoining(license) + updatePlayerJoining(source, license) dequeue(license) deferrals.done() - -- wait until the player finally joins or disconnects while installing server content - -- this may result in waiting ~2 additional minutes if the player disconnects as FXServer will think that the player exists - while DoesPlayerExist(source --[[@as string]]) do - Wait(1000) + local joiningData + while true do + joiningData = joiningPlayers[license] + + -- wait until the player finally joins or disconnects while installing server content + -- this may result in waiting ~2 additional minutes if the player disconnects as FXServer will think that the player exists + while DoesPlayerExist(source --[[@as string]]) do + Wait(1000) + end + + -- wait until either the player reconnects or was disconnected for too long + while joiningPlayers[license] and joiningPlayers[license].source == joiningData.source and (os.time() - joiningData.timestamp) < config.joiningTimeoutSeconds do + Wait(1000) + end + + -- if the player disconnected for too long stop waiting for them + if joiningPlayers[license] and joiningPlayers[license].source == joiningData.source then + removePlayerJoining(license) + break + end end - removePlayerJoining(license) end return { awaitPlayerQueue = awaitPlayerQueue, + removePlayerJoining = removePlayerJoining, }