From 3c369873ad9a651ac066597616f5073ab5b29fe3 Mon Sep 17 00:00:00 2001 From: David Malchin Date: Wed, 27 Dec 2023 20:59:52 +0200 Subject: [PATCH] feat(queue): adaptive card & display time --- config/queue.lua | 160 ++++++++++++++++++++++++++++++++++++++++++++++- locale/en.lua | 2 +- server/queue.lua | 47 ++++++++++++-- 3 files changed, 202 insertions(+), 7 deletions(-) diff --git a/config/queue.lua b/config/queue.lua index fd6197c4d..f590ba578 100644 --- a/config/queue.lua +++ b/config/queue.lua @@ -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, } diff --git a/locale/en.lua b/locale/en.lua index c9d74962c..a00247444 100644 --- a/locale/en.lua +++ b/locale/en.lua @@ -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 = { diff --git a/server/queue.lua b/server/queue.lua index 869915f63..7e27bf191 100644 --- a/server/queue.lua +++ b/server/queue.lua @@ -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 Player license to sub-queue position map. ---@field size number @@ -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 @@ -60,6 +68,7 @@ local function enqueue(license, subQueueIndex) totalQueueSize += 1 playerDatas[license] = { + waitingSeconds = 0, subQueueIndex = subQueueIndex, globalPos = globalPos, } @@ -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 @@ -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