Skip to content

Commit 07d6052

Browse files
authored
feat(queue): adaptive card & display time (#294)
1 parent 9ff7c5f commit 07d6052

File tree

3 files changed

+202
-7
lines changed

3 files changed

+202
-7
lines changed

config/queue.lua

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,174 @@ return {
66
---Notice that an additional ~2 minutes will be waited due to limitations with how FiveM handles joining players.
77
joiningTimeoutSeconds = 0,
88

9+
---@class AdaptiveCardTextOptions
10+
---@field style? 'default' | 'heading' | 'columnHeader'
11+
---@field fontType? 'default' | 'monospace'
12+
---@field size? 'small' | 'default' | 'medium' | 'large' | 'extralarge'
13+
---@field weight? 'lighter' | 'default' | 'bolder'
14+
---@field color? 'default' | 'dark' | 'light' | 'accent' | 'good' | 'warning' | 'attention'
15+
---@field isSubtle? boolean
16+
917
---@class SubQueueConfig
1018
---@field name string
1119
---@field predicate? fun(source: Source): boolean
20+
---@field cardOptions? AdaptiveCardTextOptions Text options used in the adaptive card
1221

1322
---Sub-queues from most to least prioritized.
1423
---The first sub-queue without a predicate function will be considered the default.
1524
---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.
1625
---@type SubQueueConfig[]
1726
subQueues = {
18-
{ name = 'Admin Queue', predicate = function(source) return HasPermission(source, 'admin') end },
27+
{ name = 'Admin Queue', predicate = function(source) return HasPermission(source, 'admin') end, cardOptions = { color = 'good' } },
1928
{ name = 'Regular Queue' },
2029
},
30+
31+
---Cosmetic emojis shown along with the elapsed queue time.
32+
waitingEmojis = {
33+
'🕛',
34+
'🕒',
35+
'🕕',
36+
'🕘',
37+
},
38+
39+
---Use the adaptive card generator that is defined below.
40+
useAdaptiveCard = true,
41+
42+
---@class GenerateCardParams
43+
---@field subQueue SubQueue
44+
---@field globalPos integer
45+
---@field totalQueueSize integer
46+
---@field displayTime string
47+
48+
---Generator function for the adaptive card.
49+
---@param params GenerateCardParams
50+
---@return table
51+
generateCard = function(params)
52+
local subQueue = params.subQueue
53+
local pos = params.globalPos
54+
local size = params.totalQueueSize
55+
local displayTime = params.displayTime
56+
57+
local serverName = GetConvar('sv_projectName', GetConvar('sv_hostname', 'Server'))
58+
local progressAmount = 7 -- amount of progress shown between the queue & server
59+
60+
local playerColumn = pos == 1 and progressAmount or (progressAmount - math.ceil(pos / (size / progressAmount)) + 1)
61+
local progressTextReplacements = {
62+
[1] = {
63+
text = 'Queue',
64+
color = 'good',
65+
},
66+
[playerColumn + 1] = {
67+
text = 'You',
68+
color = 'good',
69+
},
70+
[progressAmount + 2] = {
71+
text = 'Server',
72+
color = 'good',
73+
},
74+
}
75+
76+
local progressColumns = {}
77+
for i = 1, progressAmount + 2 do
78+
local textBlock = {
79+
type = 'TextBlock',
80+
text = '',
81+
horizontalAlignment = 'center',
82+
size = 'extralarge',
83+
weight = 'lighter',
84+
color = 'accent',
85+
}
86+
87+
local replacements = progressTextReplacements[i]
88+
if replacements then
89+
for k, v in pairs(replacements) do
90+
textBlock[k] = v
91+
end
92+
end
93+
94+
local column = {
95+
type = 'Column',
96+
width = 'stretch',
97+
verticalContentAlignment = 'center',
98+
items = {
99+
textBlock,
100+
}
101+
}
102+
103+
progressColumns[i] = column
104+
end
105+
106+
return {
107+
type = 'AdaptiveCard',
108+
version = '1.6',
109+
body = {
110+
{
111+
type = 'TextBlock',
112+
text = 'In Line',
113+
horizontalAlignment = 'center',
114+
size = 'large',
115+
weight = 'bolder',
116+
},
117+
{
118+
type = 'TextBlock',
119+
text = ('Joining %s'):format(serverName),
120+
spacing = 'none',
121+
horizontalAlignment = 'center',
122+
size = 'medium',
123+
weight = 'bolder',
124+
},
125+
{
126+
type = 'ColumnSet',
127+
spacing = 'large',
128+
columns = progressColumns,
129+
},
130+
{
131+
type = 'ColumnSet',
132+
spacing = 'large',
133+
columns = {
134+
{
135+
type = 'Column',
136+
width = 'stretch',
137+
items = {
138+
{
139+
type = 'TextBlock',
140+
text = subQueue.name,
141+
style = subQueue.cardOptions.style,
142+
fontType = subQueue.cardOptions.fontType,
143+
size = subQueue.cardOptions.size or 'medium',
144+
color = subQueue.cardOptions.color,
145+
isSubtle = subQueue.cardOptions.isSubtle,
146+
}
147+
},
148+
},
149+
{
150+
type = 'Column',
151+
width = 'stretch',
152+
items = {
153+
{
154+
type = 'TextBlock',
155+
text = ('%d/%d'):format(pos, size),
156+
horizontalAlignment = 'center',
157+
color = 'good',
158+
size = 'medium',
159+
}
160+
},
161+
},
162+
{
163+
type = 'Column',
164+
width = 'stretch',
165+
items = {
166+
{
167+
type = 'TextBlock',
168+
text = displayTime,
169+
horizontalAlignment = 'right',
170+
size = 'medium',
171+
}
172+
},
173+
},
174+
},
175+
},
176+
},
177+
}
178+
end,
21179
}

locale/en.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ local Translations = {
6161
birth_date = 'Birth Date',
6262
select_gender = 'Select your gender...',
6363
confirm_delete = 'Are you sure you wish to delete this character?',
64-
in_queue = '🐌 You are %{queuePos}/%{queueSize} in queue. (%{subQueue})',
64+
in_queue = '🐌 You are %{queuePos}/%{queueSize} in queue. (%{subQueue}) %{displayTime}',
6565
},
6666
command = {
6767
tp = {

server/queue.lua

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ end
2020
local config = require 'config.queue'
2121
local maxPlayers = GlobalState.MaxPlayers
2222

23+
-- destructure frequently used config options
24+
local waitingEmojis = config.waitingEmojis
25+
local waitingEmojiCount = #waitingEmojis
26+
local useAdaptiveCard = config.useAdaptiveCard
27+
local generateCard = config.generateCard
28+
2329
---@class SubQueue : SubQueueConfig
2430
---@field positions table<string, number> Player license to sub-queue position map.
2531
---@field size number
@@ -30,12 +36,14 @@ for i = 1, #config.subQueues do
3036
subQueues[i] = {
3137
name = config.subQueues[i].name,
3238
predicate = config.subQueues[i].predicate,
39+
cardOptions = config.subQueues[i].cardOptions,
3340
positions = {},
3441
size = 0,
3542
}
3643
end
3744

3845
---@class PlayerQueueData
46+
---@field waitingSeconds number
3947
---@field subQueueIndex number
4048
---@field globalPos number
4149

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

6169
totalQueueSize += 1
6270
playerDatas[license] = {
71+
waitingSeconds = 0,
6372
subQueueIndex = subQueueIndex,
6473
globalPos = globalPos,
6574
}
@@ -175,6 +184,14 @@ local function isPlayerTimingOut(license)
175184
return playerTimingOut
176185
end
177186

187+
---@param waitingSeconds number
188+
---@param waitingEmojiIndex number
189+
local function createDisplayTime(waitingSeconds, waitingEmojiIndex)
190+
local minutes = math.floor(waitingSeconds / 60)
191+
local seconds = waitingSeconds % 60
192+
return ('%02d:%02d %s'):format(minutes, seconds, waitingEmojis[waitingEmojiIndex])
193+
end
194+
178195
---@param source Source
179196
---@param license string
180197
---@param deferrals Deferrals
@@ -213,15 +230,35 @@ local function awaitPlayerQueue(source, license, deferrals)
213230
data = playerDatas[license]
214231
end
215232

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

218236
-- wait until the player disconnected or until there are available slots and the player is first in queue
219237
while DoesPlayerExist(source --[[@as string]]) and ((GetNumPlayerIndices() + joiningPlayerCount) >= maxPlayers or data.globalPos > 1) do
220-
deferrals.update(Lang:t('info.in_queue', {
221-
queuePos = data.globalPos,
222-
queueSize = totalQueueSize,
223-
subQueue = subQueue.name,
224-
}))
238+
local displayTime = createDisplayTime(data.waitingSeconds, waitingEmojiIndex)
239+
240+
if useAdaptiveCard then
241+
deferrals.presentCard(generateCard({
242+
subQueue = subQueue,
243+
globalPos = data.globalPos,
244+
totalQueueSize = totalQueueSize,
245+
displayTime = displayTime,
246+
}))
247+
else
248+
deferrals.update(Lang:t('info.in_queue', {
249+
queuePos = data.globalPos,
250+
queueSize = totalQueueSize,
251+
subQueue = subQueue.name,
252+
displayTime = displayTime,
253+
}))
254+
end
255+
256+
data.waitingSeconds += 1
257+
waitingEmojiIndex += 1
258+
259+
if waitingEmojiIndex > waitingEmojiCount then
260+
waitingEmojiIndex = 1
261+
end
225262

226263
Wait(1000)
227264
end

0 commit comments

Comments
 (0)