Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(queue): add timeouts #285

Merged
merged 4 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config/queue.lua
Original file line number Diff line number Diff line change
@@ -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,
Comment on lines +3 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by the nuance difference here. It seems like the first one is the timeout while the player is in the queue and the second is the timeout when the player is effectively first in line -- that is there is an open slot and they are downloading the assets to be let in? Is it worth specifying both of these config options? Why would someone want to set these to different values?

Copy link
Member Author

@D4isDAVID D4isDAVID Dec 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason I added this is that connectqueue also has it (link), but connectqueue waits until the client sends a custom event to the server - meaning after the player downloaded the server content and passed the loading screen, however this system waits for playerJoining, so I don't account for players that disconnect in the loading screen.

Currently the way the queue works is that after the player is first in line, when there is an available slot, the player is let into the server and dequeued, so now they are installing server content and technically they are no longer first in line; they're already past that.

Obviously this can change if need be.

}
5 changes: 4 additions & 1 deletion server/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
91 changes: 77 additions & 14 deletions server/queue.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ end

-- Queue code

local config = require 'config.queue'
local maxPlayers = GlobalState.MaxPlayers

---Player license to queue position map.
Expand Down Expand Up @@ -47,16 +48,42 @@ 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<string, true>
---@type table<string, { source: Source, timestamp: integer }>
local joiningPlayers = {}
local joiningPlayerCount = 0

---@param license string
local function addPlayerJoining(license)
local function awaitPlayerJoinsOrDisconnects(license)
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(joiningData.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
end

---@param source Source
---@param license string
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
Expand All @@ -67,16 +94,54 @@ local function removePlayerJoining(license)
joiningPlayers[license] = nil
end

---@type table<string, true>
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
Expand All @@ -90,22 +155,20 @@ 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)
end
removePlayerJoining(license)
awaitPlayerJoinsOrDisconnects(license)
end

return {
awaitPlayerQueue = awaitPlayerQueue,
}
removePlayerJoining = removePlayerJoining,
}
Loading