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): adaptive card & display time #294

Merged
merged 1 commit into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading