Skip to content

Commit

Permalink
feat(queue): adaptive card & display time (#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
D4isDAVID committed Dec 30, 2023
1 parent 9ff7c5f commit 07d6052
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 7 deletions.
160 changes: 159 additions & 1 deletion config/queue.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,174 @@ return {
---Notice that an additional ~2 minutes will be waited due to limitations with how FiveM handles joining players.
joiningTimeoutSeconds = 0,

---@class AdaptiveCardTextOptions
---@field style? 'default' | 'heading' | 'columnHeader'
---@field fontType? 'default' | 'monospace'
---@field size? 'small' | 'default' | 'medium' | 'large' | 'extralarge'
---@field weight? 'lighter' | 'default' | 'bolder'
---@field color? 'default' | 'dark' | 'light' | 'accent' | 'good' | 'warning' | 'attention'
---@field isSubtle? boolean

---@class SubQueueConfig
---@field name string
---@field predicate? fun(source: Source): boolean
---@field cardOptions? AdaptiveCardTextOptions Text options used in the adaptive card

---Sub-queues from most to least prioritized.
---The first sub-queue without a predicate function will be considered the default.
---If a player doesn't pass any predicate and a sub-queue with no predicate does not exist they will not be let into the server unless a player slot is available.
---@type SubQueueConfig[]
subQueues = {
{ name = 'Admin Queue', predicate = function(source) return HasPermission(source, 'admin') end },
{ name = 'Admin Queue', predicate = function(source) return HasPermission(source, 'admin') end, cardOptions = { color = 'good' } },
{ name = 'Regular Queue' },
},

---Cosmetic emojis shown along with the elapsed queue time.
waitingEmojis = {
'🕛',
'🕒',
'🕕',
'🕘',
},

---Use the adaptive card generator that is defined below.
useAdaptiveCard = true,

---@class GenerateCardParams
---@field subQueue SubQueue
---@field globalPos integer
---@field totalQueueSize integer
---@field displayTime string

---Generator function for the adaptive card.
---@param params GenerateCardParams
---@return table
generateCard = function(params)
local subQueue = params.subQueue
local pos = params.globalPos
local size = params.totalQueueSize
local displayTime = params.displayTime

local serverName = GetConvar('sv_projectName', GetConvar('sv_hostname', 'Server'))
local progressAmount = 7 -- amount of progress shown between the queue & server

local playerColumn = pos == 1 and progressAmount or (progressAmount - math.ceil(pos / (size / progressAmount)) + 1)
local progressTextReplacements = {
[1] = {
text = 'Queue',
color = 'good',
},
[playerColumn + 1] = {
text = 'You',
color = 'good',
},
[progressAmount + 2] = {
text = 'Server',
color = 'good',
},
}

local progressColumns = {}
for i = 1, progressAmount + 2 do
local textBlock = {
type = 'TextBlock',
text = '',
horizontalAlignment = 'center',
size = 'extralarge',
weight = 'lighter',
color = 'accent',
}

local replacements = progressTextReplacements[i]
if replacements then
for k, v in pairs(replacements) do
textBlock[k] = v
end
end

local column = {
type = 'Column',
width = 'stretch',
verticalContentAlignment = 'center',
items = {
textBlock,
}
}

progressColumns[i] = column
end

return {
type = 'AdaptiveCard',
version = '1.6',
body = {
{
type = 'TextBlock',
text = 'In Line',
horizontalAlignment = 'center',
size = 'large',
weight = 'bolder',
},
{
type = 'TextBlock',
text = ('Joining %s'):format(serverName),
spacing = 'none',
horizontalAlignment = 'center',
size = 'medium',
weight = 'bolder',
},
{
type = 'ColumnSet',
spacing = 'large',
columns = progressColumns,
},
{
type = 'ColumnSet',
spacing = 'large',
columns = {
{
type = 'Column',
width = 'stretch',
items = {
{
type = 'TextBlock',
text = subQueue.name,
style = subQueue.cardOptions.style,
fontType = subQueue.cardOptions.fontType,
size = subQueue.cardOptions.size or 'medium',
color = subQueue.cardOptions.color,
isSubtle = subQueue.cardOptions.isSubtle,
}
},
},
{
type = 'Column',
width = 'stretch',
items = {
{
type = 'TextBlock',
text = ('%d/%d'):format(pos, size),
horizontalAlignment = 'center',
color = 'good',
size = 'medium',
}
},
},
{
type = 'Column',
width = 'stretch',
items = {
{
type = 'TextBlock',
text = displayTime,
horizontalAlignment = 'right',
size = 'medium',
}
},
},
},
},
},
}
end,
}
2 changes: 1 addition & 1 deletion locale/en.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ local Translations = {
birth_date = 'Birth Date',
select_gender = 'Select your gender...',
confirm_delete = 'Are you sure you wish to delete this character?',
in_queue = '🐌 You are %{queuePos}/%{queueSize} in queue. (%{subQueue})',
in_queue = '🐌 You are %{queuePos}/%{queueSize} in queue. (%{subQueue}) %{displayTime}',
},
command = {
tp = {
Expand Down
47 changes: 42 additions & 5 deletions server/queue.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ end
local config = require 'config.queue'
local maxPlayers = GlobalState.MaxPlayers

-- destructure frequently used config options
local waitingEmojis = config.waitingEmojis
local waitingEmojiCount = #waitingEmojis
local useAdaptiveCard = config.useAdaptiveCard
local generateCard = config.generateCard

---@class SubQueue : SubQueueConfig
---@field positions table<string, number> Player license to sub-queue position map.
---@field size number
Expand All @@ -30,12 +36,14 @@ for i = 1, #config.subQueues do
subQueues[i] = {
name = config.subQueues[i].name,
predicate = config.subQueues[i].predicate,
cardOptions = config.subQueues[i].cardOptions,
positions = {},
size = 0,
}
end

---@class PlayerQueueData
---@field waitingSeconds number
---@field subQueueIndex number
---@field globalPos number

Expand All @@ -60,6 +68,7 @@ local function enqueue(license, subQueueIndex)

totalQueueSize += 1
playerDatas[license] = {
waitingSeconds = 0,
subQueueIndex = subQueueIndex,
globalPos = globalPos,
}
Expand Down Expand Up @@ -175,6 +184,14 @@ local function isPlayerTimingOut(license)
return playerTimingOut
end

---@param waitingSeconds number
---@param waitingEmojiIndex number
local function createDisplayTime(waitingSeconds, waitingEmojiIndex)
local minutes = math.floor(waitingSeconds / 60)
local seconds = waitingSeconds % 60
return ('%02d:%02d %s'):format(minutes, seconds, waitingEmojis[waitingEmojiIndex])
end

---@param source Source
---@param license string
---@param deferrals Deferrals
Expand Down Expand Up @@ -213,15 +230,35 @@ local function awaitPlayerQueue(source, license, deferrals)
data = playerDatas[license]
end

local waitingEmojiIndex = 1 -- for updating the waiting emoji
local subQueue = subQueues[data.subQueueIndex]

-- 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 data.globalPos > 1) do
deferrals.update(Lang:t('info.in_queue', {
queuePos = data.globalPos,
queueSize = totalQueueSize,
subQueue = subQueue.name,
}))
local displayTime = createDisplayTime(data.waitingSeconds, waitingEmojiIndex)

if useAdaptiveCard then
deferrals.presentCard(generateCard({
subQueue = subQueue,
globalPos = data.globalPos,
totalQueueSize = totalQueueSize,
displayTime = displayTime,
}))
else
deferrals.update(Lang:t('info.in_queue', {
queuePos = data.globalPos,
queueSize = totalQueueSize,
subQueue = subQueue.name,
displayTime = displayTime,
}))
end

data.waitingSeconds += 1
waitingEmojiIndex += 1

if waitingEmojiIndex > waitingEmojiCount then
waitingEmojiIndex = 1
end

Wait(1000)
end
Expand Down

0 comments on commit 07d6052

Please sign in to comment.