Luxu Admin API — Connection Queue
Server-side exports and hooks for the built-in connection queue in luxu_admin.
Call these from another resource that starts after luxu_admin. There are no client exports for the queue, and there is no export to move, remove, or admit players — staff do that in the panel (queue.view / queue.manage).
See also: Exports · Server Hooks · Events
Requirements
The queue is enabled with queue.enabled in config/config.json. When it is enabled, LuxU Admin is the only resource that may enforce sv_maxclients: stop hardcap and any other queue resource, otherwise players are rejected twice or admitted past the slot limit. While the queue is enabled, luxu_admin stops the stock hardcap resource automatically.
sv_maxclients is read live, so changing it at runtime immediately changes how many players the queue admits.
When the queue is disabled, players are admitted right after the ban preflight. Priority providers still run (up to 2000 ms, in parallel) and the admitted hook still includes the resolved priority.
Reserved slots
queue.reserved_slots holds back N slots that regular players cannot fill. With sv_maxclients 64 and reserved_slots 4, regular players are admitted while fewer than 60 players (plus players still loading in) are on the server; eligible players are admitted while fewer than 64 are. Set it to 0 to disable.
An attempt is eligible for reserved slots when either of these is true at connection time:
- its resolved priority is
>= queue.reserved_slots_min_priority(default100; priorities are clamped to0..1000, so a threshold above1000disables this path and0makes everyone eligible), or queue.reserved_slots_for_staffistrueand one of the player's identifiers matches a registered staff member'slicense2,discord,steam, orfivem, or aconfig.ownersentry. This uses the in-memory staff list; no database query runs per connection or per tick. Owner entries configured aslicense:also match a connectinglicense2:of the same hash.
Eligibility is computed once when the attempt is admitted to the queue and does not reorder the queue: entries are still ordered by priority (higher first) and arrival. Eligible entries only overtake regular entries when regular capacity is exhausted and a reserved slot is free.
VIP resources mark a player as eligible by returning a priority at or above the threshold from a priority provider. There is no separate export for reserved slots.
Server Exports
registerQueuePriorityProvider(provider)
Registers a callback that assigns a queue priority to a connecting player. Providers run during the connection preflight, before the player is queued or admitted. When several providers are registered, they run in parallel; the highest priority wins and its label is shown to staff and to the player. If two providers return the same priority, the first label is kept unless it is empty and a later provider supplies one.
Parameters:
provider(function) —(identifiers, name) => result
---@alias PriorityProviderResult number | { priority: number, label?: string } | nil | false
---@alias PriorityProvider fun(identifiers: string[], name: string): PriorityProviderResultidentifiers(string[]) — the player's identifiers (license2:,license:,discord:,steam:, …). A copy is passed in, so mutating the array does not affect the queue.ip:identifiers are omitted whendisable_ip_address_usageistrue.name(string) — the connecting player's name.
Returns: function — call it to unregister this provider.
Rules:
- Priority is clamped to
0..1000(non-finite values become0). Higher values connect first.0,nil, andfalsemean "no priority". - Returning a string, or an object without a
priorityfield, is ignored (treated as no result). - Optional
labelis trimmed; empty labels becomenil. Labels longer than 32 characters are truncated. - A priority
>= queue.reserved_slots_min_priority(default100) also makes the player eligible for reserved slots. - Providers may return a promise (or yield in Lua). The queue waits at most 2000 ms per provider, then treats that provider as
0. - Providers that throw or reject are treated as
0and the error is logged to the server console ([Queue] priority provider threw). - A non-function provider throws:
registerQueuePriorityProvider expects a function. - Providers are removed automatically when the resource that registered them stops.
local unregister = exports.luxu_admin:registerQueuePriorityProvider(function(identifiers, name)
for _, identifier in ipairs(identifiers) do
if identifier == "discord:123456789012345678" then
return { priority = 100, label = "VIP" }
end
end
return 0
end)
-- later, if you need to stop providing priority
unregister()// TypeScript / JavaScript resource
const unregister = exports.luxu_admin.registerQueuePriorityProvider(
async (identifiers: string[], name: string) => {
const vip = await lookupVip(identifiers);
return vip ? { priority: vip.tier * 100, label: vip.name } : 0;
}
);unregisterQueuePriorityProvider(provider)
Removes a provider previously passed to registerQueuePriorityProvider. Equivalent to calling the function returned from registration.
Parameters:
provider(function) — the same function reference that was registered
Returns: boolean — true if the provider was registered
local function provider(identifiers, name)
return 0
end
exports.luxu_admin:registerQueuePriorityProvider(provider)
exports.luxu_admin:unregisterQueuePriorityProvider(provider)getQueueSnapshot()
Returns the current queue state. Useful for external dashboards or Discord bots. Safe to call when the queue is disabled (enabled will be false).
Returns:
---@class QueueCharacter
---@field id string -- charId (ESX identifier, QB/QBX citizenid, vRP character id)
---@field name string
---@class QueueSnapshotEntry
---@field id string -- attempt id (`license2:`/`license:` identifier + `#` + generation)
---@field name string
---@field uniqueId string -- `license2:` or `license:` identifier used as the queue key
---@field priority number
---@field priorityLabel string|nil
---@field eligibleForReserved boolean -- may use one of the reserved slots
---@field position number -- 1-based for waiting entries; 0 for released entries that are still loading in
---@field state "waiting"|"reserved"
---@field joinedAt number -- unix ms
---@field characters QueueCharacter[] -- framework characters (max 10); empty while the lookup is pending, when none were found, or for immediately admitted attempts
---@class QueueSnapshot
---@field enabled boolean
---@field maxClients number -- sv_maxclients
---@field players number -- GetNumPlayerIndices()
---@field reserved number -- released attempts still loading in (already count as used slots)
---@field reservedSlots number -- queue.reserved_slots, clamped to maxClients
---@field regularCapacity number -- maxClients - reservedSlots; the limit for non-eligible players
---@field updatedAt number -- unix ms
---@field entries QueueSnapshotEntry[]reserved (attempts loading in) and reservedSlots (slots held back) are unrelated counters.
local snapshot = exports.luxu_admin:getQueueSnapshot()
print(("online=%s loading in=%s reserved slots=%s regular capacity=%s"):format(
snapshot.players,
snapshot.reserved,
snapshot.reservedSlots,
snapshot.regularCapacity
))
for _, entry in ipairs(snapshot.entries) do
print(entry.position, entry.name, entry.priority, entry.state, entry.eligibleForReserved)
endHooks
Queue hooks use the generic registerHook / removeHook exports and the matching server events luxu_admin:server:<hook_name>. These are not networked client events.
Lua resources can listen with AddEventHandler. JavaScript/TypeScript resources should use registerHook (invalid names throw at registration time).
See Server Hooks for the shared registration pattern.
queue_player_admitted
Fires on playerJoining when the queue had reserved a slot for that connection (the deferral was already completed with done(), and the player is loading in). source is the final server id; tempId is the connecting id.
---@class QueuePlayerAdmitted
---@field source number -- final server id at join time
---@field tempId number -- connecting (temporary) id
---@field name string
---@field identifiers string[]
---@field priority number
---@field priorityLabel string|nil
---@field waitedMs number -- milliseconds from enqueue to joinAddEventHandler("luxu_admin:server:queue_player_admitted", function(data)
print(("%s admitted after %d s"):format(data.name, math.floor(data.waitedMs / 1000)))
end)const unregister = exports.luxu_admin.registerHook(
'queue_player_admitted',
(data: {
source: number;
tempId: number;
name: string;
identifiers: string[];
priority: number;
priorityLabel: string | null;
waitedMs: number;
}) => {
console.log(`${data.name} admitted after ${Math.floor(data.waitedMs / 1000)}s`);
}
);queue_player_removed
Fires when an attempt leaves the queue without being admitted.
---@class QueuePlayerRemoved
---@field tempId number
---@field name string
---@field identifiers string[]
---@field reason "admin"|"timeout"|"disconnected"|"replaced"|"queue_full"|"resource_stop"|"preflight_rejected"|"reservation_expired"
---@field staffId number|nil -- set when reason is "admin"; otherwise `nil`/`null`| Reason | When |
|---|---|
admin |
Staff removed the player from the queue (queue.manage) |
timeout |
Waited longer than queue.max_wait_minutes |
disconnected |
The connecting client dropped |
replaced |
A newer connection from the same license replaced this attempt |
queue_full |
queue.max_queue_size was already reached |
resource_stop |
luxu_admin is stopping |
preflight_rejected |
Ban/preflight rejected the connection, including a ban that lands while the player is waiting and is re-checked just before release |
reservation_expired |
A newer attempt from the same license replaced a player who was already released and loading in |
AddEventHandler("luxu_admin:server:queue_player_removed", function(data)
if data.reason == "admin" then
print(("%s removed from the queue by staff %s"):format(data.name, tostring(data.staffId)))
end
end)Ban preflight (config/moderation.lua)
The playerConnecting export in config/moderation.lua is still called for every connection, as a preflight step, before the player is admitted or queued.
The deferrals object it receives is a wrapper owned by the queue (the queue already called deferrals.defer()):
deferrals.update(text)/deferrals.presentCard(...)are forwarded to the player.deferrals.done()with no reason means "preflight passed". It does not admit the player; the queue decides whether to admit or queue them.deferrals.done(reason)rejects the connection with that reason.- Always call
deferrals.done(...)exactly once. If you never call it, the player is rejected after the preflight timeout (30 seconds) with the queue timeout message.
Existing customisations keep working without changes: the old contract (done() = allow, done(reason) = reject) is kept.
If the Lua export is missing, the queue falls back to its own ban check (You are banned: <reason>).