1205 lines
37 KiB
Lua
1205 lines
37 KiB
Lua
if not IsDuplicityVersion() then
|
||
Citizen.CreateThread(function()
|
||
while true do
|
||
Citizen.Wait(0)
|
||
if NetworkIsSessionStarted() then
|
||
TriggerServerEvent("Queue:playerActivated")
|
||
return
|
||
end
|
||
end
|
||
end)
|
||
return
|
||
end
|
||
|
||
local Queue = {}
|
||
-- EDIT THESE IN SERVER.CFG + OTHER OPTIONS IN CONFIG.LUA
|
||
Queue.MaxPlayers = GetConvarInt("sv_maxclients", 30)
|
||
Queue.Debug = GetConvar("sv_debugqueue", "true") == "true" and true or false
|
||
Queue.DisplayQueue = GetConvar("sv_displayqueue", "true") == "true" and true or false
|
||
Queue.InitHostName = GetConvar("sv_hostname")
|
||
|
||
|
||
-- This is needed because msgpack will break when tables are too large
|
||
local _Queue = {}
|
||
_Queue.QueueList = {}
|
||
_Queue.PlayerList = {}
|
||
_Queue.PlayerCount = 0
|
||
_Queue.Priority = {}
|
||
_Queue.Connecting = {}
|
||
_Queue.JoinCbs = {}
|
||
_Queue.TempPriority = {}
|
||
_Queue.JoinDelay = GetGameTimer() + Config.JoinDelay and Config.JoinDelay or 0
|
||
|
||
local tostring = tostring
|
||
local tonumber = tonumber
|
||
local ipairs = ipairs
|
||
local pairs = pairs
|
||
local print = print
|
||
local string_len = string.len
|
||
local string_sub = string.sub
|
||
local string_format = string.format
|
||
local string_lower = string.lower
|
||
local math_abs = math.abs
|
||
local math_floor = math.floor
|
||
local math_random = math.random
|
||
local os_time = os.time
|
||
local table_insert = table.insert
|
||
local table_remove = table.remove
|
||
|
||
Queue.InitHostName = Queue.InitHostName ~= "default FXServer" and Queue.InitHostName or false
|
||
|
||
for id, power in pairs(Config.Priority) do
|
||
_Queue.Priority[string_lower(id)] = power
|
||
end
|
||
|
||
-- ╔════════════════════════════════════════════════════════════════════════════╗
|
||
-- ║ ADVANCED LOGGING SYSTEM ║
|
||
-- ╚════════════════════════════════════════════════════════════════════════════╝
|
||
|
||
local LogLevel = {
|
||
DEBUG = { level = 1, color = "^5", icon = "🔍", name = "DEBUG" },
|
||
INFO = { level = 2, color = "^2", icon = "ℹ️", name = "INFO" },
|
||
WARN = { level = 3, color = "^3", icon = "⚠️", name = "WARN" },
|
||
ERROR = { level = 4, color = "^1", icon = "❌", name = "ERROR" },
|
||
SUCCESS = { level = 2, color = "^2", icon = "✅", name = "SUCCESS" },
|
||
PLAYER = { level = 2, color = "^6", icon = "👤", name = "PLAYER" },
|
||
QUEUE = { level = 2, color = "^4", icon = "📋", name = "QUEUE" },
|
||
PRIORITY = { level = 2, color = "^8", icon = "⭐", name = "PRIORITY" },
|
||
TIMEOUT = { level = 3, color = "^1", icon = "⏱️", name = "TIMEOUT" },
|
||
SYSTEM = { level = 2, color = "^9", icon = "⚙️", name = "SYSTEM" }
|
||
}
|
||
|
||
local function GetTimestamp()
|
||
local time = os.date("*t")
|
||
return string_format("^7[^3%02d:%02d:%02d^7]", time.hour, time.min, time.sec)
|
||
end
|
||
|
||
local function GetFormattedDate()
|
||
return os.date("%Y-%m-%d %H:%M:%S")
|
||
end
|
||
|
||
local function FormatDuration(seconds)
|
||
if seconds < 60 then
|
||
return string_format("%ds", seconds)
|
||
elseif seconds < 3600 then
|
||
return string_format("%dm %ds", math_floor(seconds / 60), seconds % 60)
|
||
else
|
||
return string_format("%dh %dm %ds", math_floor(seconds / 3600), math_floor((seconds % 3600) / 60), seconds % 60)
|
||
end
|
||
end
|
||
|
||
local function CreateSeparator(length, char)
|
||
char = char or "─"
|
||
return string.rep(char, length or 50)
|
||
end
|
||
|
||
local function Log(logType, msg, details)
|
||
if not Queue.Debug then return end
|
||
|
||
local config = LogLevel[logType] or LogLevel.INFO
|
||
local timestamp = GetTimestamp()
|
||
local prefix = string_format("%s %s[%s]^7", timestamp, config.color, config.name)
|
||
local icon = config.icon or "•"
|
||
|
||
local formattedMsg = string_format("%s %s %s^7", prefix, icon, tostring(msg))
|
||
print(formattedMsg)
|
||
|
||
if details and type(details) == "table" then
|
||
for key, value in pairs(details) do
|
||
print(string_format(" ^7├─ ^3%s: ^7%s", tostring(key), tostring(value)))
|
||
end
|
||
end
|
||
end
|
||
|
||
function Queue:DebugPrint(msg)
|
||
Log("DEBUG", msg)
|
||
end
|
||
|
||
function Queue:LogInfo(msg, details)
|
||
Log("INFO", msg, details)
|
||
end
|
||
|
||
function Queue:LogWarn(msg, details)
|
||
Log("WARN", msg, details)
|
||
end
|
||
|
||
function Queue:LogError(msg, details)
|
||
Log("ERROR", msg, details)
|
||
end
|
||
|
||
function Queue:LogSuccess(msg, details)
|
||
Log("SUCCESS", msg, details)
|
||
end
|
||
|
||
function Queue:LogPlayer(msg, details)
|
||
Log("PLAYER", msg, details)
|
||
end
|
||
|
||
function Queue:LogQueue(msg, details)
|
||
Log("QUEUE", msg, details)
|
||
end
|
||
|
||
function Queue:LogPriority(msg, details)
|
||
Log("PRIORITY", msg, details)
|
||
end
|
||
|
||
function Queue:LogTimeout(msg, details)
|
||
Log("TIMEOUT", msg, details)
|
||
end
|
||
|
||
function Queue:LogSystem(msg, details)
|
||
Log("SYSTEM", msg, details)
|
||
end
|
||
|
||
function Queue:PrintHeader(title)
|
||
if not Queue.Debug then return end
|
||
local sep = CreateSeparator(60, "═")
|
||
print("^4╔" .. sep .. "╗^7")
|
||
local padding = math_floor((60 - string_len(title)) / 2)
|
||
print(string_format("^4║^7%s%s%s^4║^7", string.rep(" ", padding), title, string.rep(" ", 60 - padding - string_len(title))))
|
||
print("^4╚" .. sep .. "╝^7")
|
||
end
|
||
|
||
function Queue:PrintStats()
|
||
if not Queue.Debug then return end
|
||
self:PrintHeader("📊 QUEUE STATISTICS")
|
||
print(string_format(" ^2Players Online:^7 %d / %d", Queue:GetPlayerCount(), Queue.MaxPlayers))
|
||
print(string_format(" ^3Queue Size:^7 %d", Queue:GetSize()))
|
||
print(string_format(" ^5Connecting:^7 %d", Queue:ConnectingSize()))
|
||
print(string_format(" ^6Available Slots:^7 %d", math.max(0, Queue.MaxPlayers - Queue:GetPlayerCount() - Queue:ConnectingSize())))
|
||
print("^7" .. CreateSeparator(62, "─"))
|
||
end
|
||
|
||
function Queue:HexIdToSteamId(hexId)
|
||
local cid = math_floor(tonumber(string_sub(hexId, 7), 16))
|
||
local steam64 = math_floor(tonumber(string_sub( cid, 2)))
|
||
local a = steam64 % 2 == 0 and 0 or 1
|
||
local b = math_floor(math_abs(6561197960265728 - steam64 - a) / 2)
|
||
local sid = "steam_0:"..a..":"..(a == 1 and b -1 or b)
|
||
return sid
|
||
end
|
||
|
||
function Queue:IsSteamRunning(src)
|
||
for _, id in ipairs(GetPlayerIdentifiers(src)) do
|
||
if string_sub(id, 1, 5) == "steam" then
|
||
return true
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
function Queue:GetPlayerCount()
|
||
return _Queue.PlayerCount
|
||
end
|
||
|
||
function Queue:GetSize()
|
||
return #_Queue.QueueList
|
||
end
|
||
|
||
function Queue:ConnectingSize()
|
||
return #_Queue.Connecting
|
||
end
|
||
|
||
function Queue:GetQueueList()
|
||
return _Queue.QueueList
|
||
end
|
||
|
||
function Queue:GetPriorityList()
|
||
return _Queue.Priority
|
||
end
|
||
|
||
function Queue:GetPlayerList()
|
||
return _Queue.PlayerList
|
||
end
|
||
|
||
function Queue:GetTempPriorityList()
|
||
return _Queue.TempPriority
|
||
end
|
||
|
||
function Queue:GetConnectingList()
|
||
return _Queue.Connecting
|
||
end
|
||
|
||
function Queue:IsInQueue(ids, rtnTbl, bySource, connecting)
|
||
local connList = Queue:GetConnectingList()
|
||
local queueList = Queue:GetQueueList()
|
||
|
||
for genericKey1, genericValue1 in ipairs(connecting and connList or queueList) do
|
||
local inQueue = false
|
||
|
||
if not bySource then
|
||
for genericKey2, genericValue2 in ipairs(genericValue1.ids) do
|
||
if inQueue then break end
|
||
|
||
for genericKey3, genericValue3 in ipairs(ids) do
|
||
if genericValue3 == genericValue2 then inQueue = true break end
|
||
end
|
||
end
|
||
else
|
||
inQueue = ids == genericValue1.source
|
||
end
|
||
|
||
if inQueue then
|
||
if rtnTbl then
|
||
return genericKey1, connecting and connList[genericKey1] or queueList[genericKey1]
|
||
end
|
||
|
||
return true
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
function Queue:IsPriority(ids)
|
||
local prio = false
|
||
local tempPower, tempEnd = Queue:HasTempPriority(ids)
|
||
local prioList = Queue:GetPriorityList()
|
||
|
||
for _, id in ipairs(ids) do
|
||
id = string_lower(id)
|
||
|
||
if prioList[id] then prio = prioList[id] break end
|
||
|
||
if string_sub(id, 1, 5) == "steam" then
|
||
local steamid = Queue:HexIdToSteamId(id)
|
||
if prioList[steamid] then prio = prioList[steamid] break end
|
||
end
|
||
end
|
||
|
||
if tempPower or prio then
|
||
if tempPower and prio then
|
||
return tempPower > prio and tempPower or prio
|
||
else
|
||
return tempPower or prio
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
function Queue:HasTempPriority(ids)
|
||
local tmpPrio = Queue:GetTempPriorityList()
|
||
|
||
for _, id in pairs(ids) do
|
||
id = string_lower(id)
|
||
|
||
if tmpPrio[id] then return tmpPrio[id].power, tmpPrio[id].endTime, id end
|
||
|
||
if string_sub(id, 1, 5) == "steam" then
|
||
local steamid = Queue:HexIdToSteamId(id)
|
||
if tmpPrio[steamid] then return tmpPrio[steamid].power, tmpPrio[steamid].endTime, id end
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
function Queue:AddToQueue(ids, connectTime, name, src, deferrals)
|
||
if Queue:IsInQueue(ids) then return end
|
||
|
||
local tmp = {
|
||
source = src,
|
||
ids = ids,
|
||
name = name,
|
||
priority = Queue:IsPriority(ids) or (src == "debug" and math_random(0, 15)),
|
||
timeout = 0,
|
||
deferrals = deferrals,
|
||
firstconnect = connectTime,
|
||
queuetime = function() return (os_time() - connectTime) end
|
||
}
|
||
|
||
local _pos = false
|
||
local queueCount = Queue:GetSize() + 1
|
||
local queueList = Queue:GetQueueList()
|
||
|
||
for pos, data in ipairs(queueList) do
|
||
if tmp.priority then
|
||
if not data.priority then
|
||
_pos = pos
|
||
else
|
||
if tmp.priority > data.priority then
|
||
_pos = pos
|
||
end
|
||
end
|
||
|
||
if _pos then
|
||
Queue:LogPriority(string_format("Player prioritized in queue"), {
|
||
Player = tmp.name,
|
||
Identifier = ids[1],
|
||
Position = string_format("%d/%d", _pos, queueCount),
|
||
Priority_Power = tostring(tmp.priority)
|
||
})
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
if not _pos then
|
||
_pos = Queue:GetSize() + 1
|
||
Queue:LogQueue(string_format("Player added to queue"), {
|
||
Player = tmp.name,
|
||
Identifier = ids[1],
|
||
Position = string_format("%d/%d", _pos, queueCount),
|
||
Priority = tmp.priority and tostring(tmp.priority) or "None"
|
||
})
|
||
end
|
||
|
||
table_insert(queueList, _pos, tmp)
|
||
end
|
||
|
||
function Queue:RemoveFromQueue(ids, bySource, byIndex)
|
||
local queueList = Queue:GetQueueList()
|
||
|
||
if byIndex then
|
||
if queueList[byIndex] then
|
||
table_remove(queueList, byIndex)
|
||
end
|
||
|
||
return
|
||
end
|
||
|
||
if Queue:IsInQueue(ids, false, bySource) then
|
||
local pos, data = Queue:IsInQueue(ids, true, bySource)
|
||
table_remove(queueList, pos)
|
||
end
|
||
end
|
||
|
||
function Queue:TempSize()
|
||
local count = 0
|
||
|
||
for _pos, data in pairs(Queue:GetQueueList()) do
|
||
if Queue:HasTempPriority(data.ids) then count = count +1 end
|
||
end
|
||
|
||
return count > 0 and count or false
|
||
end
|
||
|
||
function Queue:IsInConnecting(ids, bySource, refresh)
|
||
local inConnecting, tbl = Queue:IsInQueue(ids, refresh and true or false, bySource and true or false, true)
|
||
|
||
if not inConnecting then return false end
|
||
|
||
if refresh and inConnecting and tbl then
|
||
Queue:GetConnectingList()[inConnecting].timeout = 0
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
function Queue:RemoveFromConnecting(ids, bySource, byIndex)
|
||
local connList = Queue:GetConnectingList()
|
||
|
||
if byIndex then
|
||
if connList[byIndex] then
|
||
table_remove(connList, byIndex)
|
||
end
|
||
|
||
return
|
||
end
|
||
|
||
for genericKey1, genericValue1 in ipairs(connList) do
|
||
local inConnecting = false
|
||
|
||
if not bySource then
|
||
for genericKey2, genericValue2 in ipairs(genericValue1.ids) do
|
||
if inConnecting then break end
|
||
|
||
for genericKey3, genericValue3 in ipairs(ids) do
|
||
if genericValue3 == genericValue2 then inConnecting = true break end
|
||
end
|
||
end
|
||
else
|
||
inConnecting = ids == genericValue1.source
|
||
end
|
||
|
||
if inConnecting then
|
||
table_remove(connList, genericKey1)
|
||
return true
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
function Queue:AddToConnecting(ids, ignorePos, autoRemove, done)
|
||
local function remove()
|
||
if not autoRemove then return end
|
||
|
||
done(Config.Language.connectingerr)
|
||
Queue:RemoveFromConnecting(ids)
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:LogError("Failed to add player to connecting list", {
|
||
Identifier = ids[1] or "Unknown",
|
||
Reason = "Server may be full or position invalid"
|
||
})
|
||
end
|
||
|
||
local connList = Queue:GetConnectingList()
|
||
|
||
if Queue:ConnectingSize() + Queue:GetPlayerCount() + 1 > Queue.MaxPlayers then remove() return false end
|
||
|
||
if ids[1] == "debug" then
|
||
table_insert(connList, {source = ids[1], ids = ids, name = ids[1], firstconnect = ids[1], priority = ids[1], timeout = 0})
|
||
return true
|
||
end
|
||
|
||
if Queue:IsInConnecting(ids) then Queue:RemoveFromConnecting(ids) end
|
||
|
||
local pos, data = Queue:IsInQueue(ids, true)
|
||
if not ignorePos and (not pos or pos > 1) then remove() return false end
|
||
|
||
table_insert(connList, data)
|
||
Queue:RemoveFromQueue(ids)
|
||
|
||
return true
|
||
end
|
||
|
||
function Queue:GetIds(src)
|
||
local ids = GetPlayerIdentifiers(src)
|
||
local ip = GetPlayerEndpoint(src)
|
||
|
||
ids = (ids and ids[1]) and ids or (ip and {"ip:" .. ip} or false)
|
||
ids = ids ~= nil and ids or false
|
||
|
||
if ids and #ids > 1 then
|
||
for k, id in ipairs(ids) do
|
||
if string_sub(id, 1, 3) == "ip:" and not Queue:IsPriority({id}) then table_remove(ids, k) end
|
||
end
|
||
end
|
||
|
||
return ids
|
||
end
|
||
|
||
function Queue:AddPriority(id, power, temp)
|
||
if not id then return false end
|
||
|
||
if type(id) == "table" then
|
||
for _id, power in pairs(id) do
|
||
if _id and type(_id) == "string" and power and type(power) == "number" then
|
||
Queue:GetPriorityList()[_id] = power
|
||
else
|
||
Queue:DebugPrint("Error adding a priority id, invalid data passed")
|
||
return false
|
||
end
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
power = (power and type(power) == "number") and power or 10
|
||
|
||
if temp then
|
||
local tempPower, tempEnd, tempId = Queue:HasTempPriority({id})
|
||
id = tempId or id
|
||
|
||
Queue:GetTempPriorityList()[string_lower(id)] = {power = power, endTime = os_time() + temp}
|
||
else
|
||
Queue:GetPriorityList()[string_lower(id)] = power
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
function Queue:RemovePriority(id)
|
||
if not id then return false end
|
||
id = string_lower(id)
|
||
Queue:GetPriorityList()[id] = nil
|
||
return true
|
||
end
|
||
|
||
function Queue:UpdatePosData(src, ids, deferrals)
|
||
local pos, data = Queue:IsInQueue(ids, true)
|
||
data.source = src
|
||
data.ids = ids
|
||
data.timeout = 0
|
||
data.firstconnect = os_time()
|
||
data.name = GetPlayerName(src)
|
||
data.deferrals = deferrals
|
||
end
|
||
|
||
function Queue:NotFull(firstJoin)
|
||
local canJoin = Queue:GetPlayerCount() + Queue:ConnectingSize() < Queue.MaxPlayers
|
||
if firstJoin and canJoin then canJoin = Queue:GetSize() <= 1 end
|
||
return canJoin
|
||
end
|
||
|
||
function Queue:SetPos(ids, newPos)
|
||
if newPos <= 0 or newPos > Queue:GetSize() then return false end
|
||
|
||
local pos, data = Queue:IsInQueue(ids, true)
|
||
local queueList = Queue:GetQueueList()
|
||
|
||
table_remove(queueList, pos)
|
||
table_insert(queueList, newPos, data)
|
||
end
|
||
|
||
function Queue:CanJoin(src, cb)
|
||
local allow = true
|
||
|
||
for _, data in ipairs(_Queue.JoinCbs) do
|
||
local await = true
|
||
|
||
data.func(src, function(reason)
|
||
if reason and type(reason) == "string" then allow = false cb(reason) end
|
||
await = false
|
||
end)
|
||
|
||
while await do Citizen.Wait(0) end
|
||
|
||
if not allow then return end
|
||
end
|
||
|
||
if allow then cb(false) end
|
||
end
|
||
|
||
function Queue:OnJoin(cb, resource)
|
||
if not cb then return end
|
||
|
||
local tmp = {resource = resource, func = cb}
|
||
table_insert(_Queue.JoinCbs, tmp)
|
||
end
|
||
|
||
exports("GetQueueExports", function()
|
||
return Queue
|
||
end)
|
||
|
||
local function playerConnect(name, setKickReason, deferrals)
|
||
local src = source
|
||
local ids = Queue:GetIds(src)
|
||
local name = GetPlayerName(src)
|
||
local connectTime = os_time()
|
||
local connecting = true
|
||
|
||
deferrals.defer()
|
||
|
||
if Config.AntiSpam then
|
||
for i=Config.AntiSpamTimer,0,-1 do
|
||
deferrals.update(string.format(Config.PleaseWait, i))
|
||
Citizen.Wait(1000)
|
||
end
|
||
end
|
||
|
||
Citizen.CreateThread(function()
|
||
while connecting do
|
||
Citizen.Wait(100)
|
||
if not connecting then return end
|
||
deferrals.update(Config.Language.connecting)
|
||
end
|
||
end)
|
||
|
||
Citizen.Wait(500)
|
||
|
||
local function done(msg, _deferrals)
|
||
connecting = false
|
||
|
||
local deferrals = _deferrals or deferrals
|
||
|
||
if msg then deferrals.update(tostring(msg) or "") end
|
||
|
||
Citizen.Wait(500)
|
||
|
||
if not msg then
|
||
deferrals.done()
|
||
if Config.EnableGrace then Queue:AddPriority(ids[1], Config.GracePower, Config.GraceTime) end
|
||
else
|
||
deferrals.done(tostring(msg) or "") CancelEvent()
|
||
end
|
||
|
||
return
|
||
end
|
||
|
||
local function update(msg, _deferrals)
|
||
local deferrals = _deferrals or deferrals
|
||
connecting = false
|
||
deferrals.update(tostring(msg) or "")
|
||
end
|
||
|
||
if not ids then
|
||
-- prevent joining
|
||
done(Config.Language.idrr)
|
||
CancelEvent()
|
||
Queue:LogError("Player connection rejected - No identifiers", {
|
||
Player = name,
|
||
Source = src,
|
||
Reason = "Could not retrieve any valid identifiers"
|
||
})
|
||
return
|
||
end
|
||
|
||
if Config.RequireSteam and not Queue:IsSteamRunning(src) then
|
||
-- prevent joining
|
||
done(Config.Language.steam)
|
||
CancelEvent()
|
||
return
|
||
end
|
||
|
||
local allow
|
||
|
||
Queue:CanJoin(src, function(reason)
|
||
if reason == nil or allow ~= nil then return end
|
||
if reason == false or #_Queue.JoinCbs <= 0 then allow = true return end
|
||
|
||
if reason then
|
||
-- prevent joining
|
||
allow = false
|
||
done(reason and tostring(reason) or "You were blocked from joining")
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:RemoveFromConnecting(ids)
|
||
Queue:LogWarn("Player blocked from joining", {
|
||
Player = name,
|
||
Identifier = ids[1],
|
||
Reason = tostring(reason),
|
||
Action = "Connection Denied"
|
||
})
|
||
CancelEvent()
|
||
return
|
||
end
|
||
|
||
allow = true
|
||
end)
|
||
|
||
while allow == nil do Citizen.Wait(0) end
|
||
if not allow then return end
|
||
|
||
if Config.PriorityOnly and not Queue:IsPriority(ids) then done(Config.Language.wlonly) return end
|
||
|
||
local rejoined = false
|
||
|
||
if Queue:IsInConnecting(ids, false, true) then
|
||
Queue:RemoveFromConnecting(ids)
|
||
|
||
if Queue:NotFull() then
|
||
-- let them in the server
|
||
|
||
if not Queue:IsInQueue(ids) then
|
||
Queue:AddToQueue(ids, connectTime, name, src, deferrals)
|
||
end
|
||
|
||
local added = Queue:AddToConnecting(ids, true, true, done)
|
||
if not added then CancelEvent() return end
|
||
done()
|
||
|
||
return
|
||
else
|
||
rejoined = true
|
||
end
|
||
end
|
||
|
||
if Queue:IsInQueue(ids) then
|
||
rejoined = true
|
||
Queue:UpdatePosData(src, ids, deferrals)
|
||
Queue:LogPlayer("Player rejoined queue", {
|
||
Player = name,
|
||
Identifier = ids[1],
|
||
Status = "Reconnected after cancel"
|
||
})
|
||
else
|
||
Queue:AddToQueue(ids, connectTime, name, src, deferrals)
|
||
|
||
if rejoined then
|
||
Queue:SetPos(ids, 1)
|
||
rejoined = false
|
||
end
|
||
end
|
||
|
||
local pos, data = Queue:IsInQueue(ids, true)
|
||
|
||
if not pos or not data then
|
||
done(Config.Language.err .. " [1]")
|
||
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:RemoveFromConnecting(ids)
|
||
|
||
CancelEvent()
|
||
return
|
||
end
|
||
|
||
if Queue:NotFull(true) and _Queue.JoinDelay <= GetGameTimer() then
|
||
-- let them in the server
|
||
local added = Queue:AddToConnecting(ids, true, true, done)
|
||
if not added then CancelEvent() return end
|
||
|
||
done()
|
||
Queue:LogSuccess("Player connecting to server (Direct Join)", {
|
||
Player = name,
|
||
Identifier = ids[1],
|
||
Slots_Available = Queue.MaxPlayers - Queue:GetPlayerCount() - Queue:ConnectingSize(),
|
||
Server_Population = string_format("%d/%d", Queue:GetPlayerCount(), Queue.MaxPlayers)
|
||
})
|
||
|
||
return
|
||
end
|
||
|
||
update(string_format(Config.Language.pos .. ((Queue:TempSize() and Config.ShowTemp) and " (" .. Queue:TempSize() .. " temp)" or "00:00:00"), pos, Queue:GetSize(), ""))
|
||
|
||
if rejoined then return end
|
||
|
||
while true do
|
||
Citizen.Wait(500)
|
||
|
||
local pos, data = Queue:IsInQueue(ids, true)
|
||
|
||
local function remove(msg)
|
||
if data then
|
||
if msg then
|
||
update(msg, data.deferrals)
|
||
end
|
||
|
||
Queue:RemoveFromQueue(data.source, true)
|
||
Queue:RemoveFromConnecting(data.source, true)
|
||
else
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:RemoveFromConnecting(ids)
|
||
end
|
||
end
|
||
|
||
if not data or not data.deferrals or not data.source or not pos then
|
||
remove("[Queue] Removed from queue, queue data invalid :(")
|
||
Queue:LogError("Player removed - Invalid queue data", {
|
||
Player = name,
|
||
Identifier = ids[1],
|
||
Reason = "Missing or corrupted queue data",
|
||
Data_Deferrals = data and data.deferrals and "Valid" or "Invalid",
|
||
Data_Source = data and data.source and "Valid" or "Invalid",
|
||
Data_Position = pos and "Valid" or "Invalid"
|
||
})
|
||
return
|
||
end
|
||
|
||
local endPoint = GetPlayerEndpoint(data.source)
|
||
if not endPoint then data.timeout = data.timeout + 0.5 else data.timeout = 0 end
|
||
|
||
if data.timeout >= Config.QueueTimeOut and os_time() - connectTime > 5 then
|
||
remove("[Queue] Removed due to timeout")
|
||
Queue:LogTimeout("Player removed - Queue timeout", {
|
||
Player = name,
|
||
Identifier = ids[1],
|
||
Timeout_Duration = FormatDuration(data.timeout),
|
||
Config_Timeout = FormatDuration(Config.QueueTimeOut),
|
||
Queue_Time = FormatDuration(os_time() - connectTime)
|
||
})
|
||
return
|
||
end
|
||
|
||
if pos <= 1 and Queue:NotFull() and _Queue.JoinDelay <= GetGameTimer() then
|
||
-- let them in the server
|
||
local added = Queue:AddToConnecting(ids)
|
||
|
||
update(Config.Language.joining, data.deferrals)
|
||
Citizen.Wait(500)
|
||
|
||
if not added then
|
||
done(Config.Language.connectingerr)
|
||
CancelEvent()
|
||
return
|
||
end
|
||
|
||
done(nil, data.deferrals)
|
||
|
||
if Config.EnableGrace then Queue:AddPriority(ids[1], Config.GracePower, Config.GraceTime) end
|
||
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:LogSuccess("Player joining server from queue", {
|
||
Player = name,
|
||
Identifier = ids[1],
|
||
Wait_Time = FormatDuration(data.queuetime()),
|
||
Priority = data.priority and tostring(data.priority) or "None",
|
||
Grace_Enabled = Config.EnableGrace and "Yes" or "No"
|
||
})
|
||
return
|
||
end
|
||
|
||
local seconds = data.queuetime()
|
||
local qTime = string_format("%02d", math_floor((seconds % 86400) / 3600)) .. ":" .. string_format("%02d", math_floor((seconds % 3600) / 60)) .. ":" .. string_format("%02d", math_floor(seconds % 60))
|
||
|
||
local msg = string_format(Config.Language.pos .. ((Queue:TempSize() and Config.ShowTemp) and " (" .. Queue:TempSize() .. " temp)" or ""), pos, Queue:GetSize(), qTime)
|
||
update(msg, data.deferrals)
|
||
end
|
||
end
|
||
AddEventHandler("playerConnecting", playerConnect)
|
||
|
||
Citizen.CreateThread(function()
|
||
local function remove(data, pos, msg)
|
||
if data and data.source then
|
||
Queue:RemoveFromQueue(data.source, true)
|
||
Queue:RemoveFromConnecting(data.source, true)
|
||
elseif pos then
|
||
table_remove(Queue:GetQueueList(), pos)
|
||
end
|
||
end
|
||
|
||
while true do
|
||
Citizen.Wait(1000)
|
||
|
||
local i = 1
|
||
|
||
while i <= Queue:ConnectingSize() do
|
||
local data = Queue:GetConnectingList()[i]
|
||
|
||
local endPoint = GetPlayerEndpoint(data.source)
|
||
|
||
data.timeout = data.timeout + 1
|
||
|
||
if ((data.timeout >= 300 and not endPoint) or data.timeout >= Config.ConnectTimeOut) and data.source ~= "debug" and os_time() - data.firstconnect > 5 then
|
||
remove(data)
|
||
Queue:LogTimeout("Player removed - Connection timeout", {
|
||
Player = data.name,
|
||
Identifier = data.ids[1],
|
||
Timeout_Duration = FormatDuration(data.timeout),
|
||
Has_Endpoint = endPoint and "Yes" or "No",
|
||
Connect_Time = FormatDuration(os_time() - data.firstconnect)
|
||
})
|
||
else
|
||
i = i + 1
|
||
end
|
||
end
|
||
|
||
for id, data in pairs(Queue:GetTempPriorityList()) do
|
||
if os_time() >= data.endTime then
|
||
Queue:GetTempPriorityList()[id] = nil
|
||
end
|
||
end
|
||
|
||
Queue.MaxPlayers = GetConvarInt("sv_maxclients", 30)
|
||
Queue.Debug = GetConvar("sv_debugqueue", "true") == "true" and true or false
|
||
Queue.DisplayQueue = GetConvar("sv_displayqueue", "true") == "true" and true or false
|
||
|
||
local qCount = Queue:GetSize()
|
||
|
||
if Queue.DisplayQueue then
|
||
if Queue.InitHostName then
|
||
SetConvar("sv_hostname", (qCount > 0 and "[" .. tostring(qCount) .. "] " or "") .. Queue.InitHostName)
|
||
else
|
||
Queue.InitHostName = GetConvar("sv_hostname")
|
||
Queue.InitHostName = Queue.InitHostName ~= "default FXServer" and Queue.InitHostName or false
|
||
end
|
||
end
|
||
end
|
||
end)
|
||
|
||
RegisterServerEvent("Queue:playerActivated")
|
||
AddEventHandler("Queue:playerActivated", function()
|
||
local src = source
|
||
local ids = Queue:GetIds(src)
|
||
local name = GetPlayerName(src)
|
||
local endpoint = GetPlayerEndpoint(src)
|
||
local ping = GetPlayerPing(src)
|
||
|
||
if not Queue:GetPlayerList()[src] then
|
||
_Queue.PlayerCount = Queue:GetPlayerCount() + 1
|
||
Queue:GetPlayerList()[src] = true
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:RemoveFromConnecting(ids)
|
||
|
||
-- Get all identifiers for logging
|
||
local allIds = GetPlayerIdentifiers(src)
|
||
local steamId, discordId, license, xbl, live, fivem = "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"
|
||
|
||
for _, id in ipairs(allIds) do
|
||
local lowerId = string_lower(id)
|
||
if string.find(lowerId, "^steam:") then
|
||
steamId = id
|
||
elseif string.find(lowerId, "^discord:") then
|
||
discordId = id
|
||
elseif string.find(lowerId, "^license:") then
|
||
license = id
|
||
elseif string.find(lowerId, "^xbl:") then
|
||
xbl = id
|
||
elseif string.find(lowerId, "^live:") then
|
||
live = id
|
||
elseif string.find(lowerId, "^fivem:") then
|
||
fivem = id
|
||
end
|
||
end
|
||
|
||
Queue:LogSuccess("Player fully connected to server", {
|
||
Player = name,
|
||
Source = src,
|
||
Steam = steamId,
|
||
Discord = discordId,
|
||
License = license,
|
||
FiveM = fivem,
|
||
IP = endpoint or "Unknown",
|
||
Ping = ping .. "ms",
|
||
Server_Population = string_format("%d/%d", Queue:GetPlayerCount(), Queue.MaxPlayers),
|
||
Available_Slots = Queue.MaxPlayers - Queue:GetPlayerCount()
|
||
})
|
||
end
|
||
end)
|
||
|
||
AddEventHandler("playerDropped", function(reason)
|
||
local src = source
|
||
local ids = Queue:GetIds(src)
|
||
local name = GetPlayerName(src)
|
||
|
||
if Queue:GetPlayerList()[src] then
|
||
-- Get all identifiers before cleanup
|
||
local allIds = GetPlayerIdentifiers(src)
|
||
local steamId, discordId, license = "N/A", "N/A", "N/A"
|
||
|
||
for _, id in ipairs(allIds) do
|
||
local lowerId = string_lower(id)
|
||
if string.find(lowerId, "^steam:") then
|
||
steamId = id
|
||
elseif string.find(lowerId, "^discord:") then
|
||
discordId = id
|
||
elseif string.find(lowerId, "^license:") then
|
||
license = id
|
||
end
|
||
end
|
||
|
||
_Queue.PlayerCount = Queue:GetPlayerCount() - 1
|
||
Queue:GetPlayerList()[src] = nil
|
||
Queue:RemoveFromQueue(ids)
|
||
Queue:RemoveFromConnecting(ids)
|
||
|
||
local graceStatus = "Disabled"
|
||
if Config.EnableGrace then
|
||
Queue:AddPriority(ids[1], Config.GracePower, Config.GraceTime)
|
||
graceStatus = string_format("Granted (%d power for %s)", Config.GracePower, FormatDuration(Config.GraceTime))
|
||
end
|
||
|
||
Queue:LogPlayer("Player disconnected from server", {
|
||
Player = name or "Unknown",
|
||
Source = src,
|
||
Steam = steamId,
|
||
Discord = discordId,
|
||
License = license,
|
||
Reason = reason or "Unknown",
|
||
Grace_Period = graceStatus,
|
||
Server_Population = string_format("%d/%d", Queue:GetPlayerCount(), Queue.MaxPlayers),
|
||
Available_Slots = Queue.MaxPlayers - Queue:GetPlayerCount()
|
||
})
|
||
end
|
||
end)
|
||
|
||
AddEventHandler("onResourceStop", function(resource)
|
||
if Queue.DisplayQueue and Queue.InitHostName and resource == GetCurrentResourceName() then SetConvar("sv_hostname", Queue.InitHostName) end
|
||
|
||
for k, data in ipairs(_Queue.JoinCbs) do
|
||
if data.resource == resource then
|
||
table_remove(_Queue.JoinCbs, k)
|
||
end
|
||
end
|
||
end)
|
||
|
||
if Config.DisableHardCap then
|
||
Queue:LogSystem("Hardcap resource disabled", {
|
||
Resource = "hardcap",
|
||
Action = "Blocked from starting",
|
||
Reason = "Config.DisableHardCap = true"
|
||
})
|
||
|
||
AddEventHandler("onResourceStarting", function(resource)
|
||
if resource == "hardcap" then CancelEvent() return end
|
||
end)
|
||
|
||
StopResource("hardcap")
|
||
end
|
||
|
||
local testAdds = 0
|
||
local commands = {}
|
||
|
||
commands.addq = function()
|
||
Queue:LogInfo("Debug queue entry added", {
|
||
Test_ID = testAdds,
|
||
Steam_ID = "steam:110000103fd1bb1" .. testAdds
|
||
})
|
||
Queue:AddToQueue({"steam:110000103fd1bb1"..testAdds}, os_time(), "TestAdd: " .. testAdds, "debug")
|
||
testAdds = testAdds + 1
|
||
end
|
||
|
||
commands.removeq = function(args)
|
||
args[1] = tonumber(args[1])
|
||
local name = Queue:GetQueueList()[args[1]] and Queue:GetQueueList()[args[1]].name or nil
|
||
Queue:RemoveFromQueue(nil, nil, args[1])
|
||
Queue:LogInfo("Player removed from queue", {
|
||
Player = tostring(name),
|
||
Position = args[1]
|
||
})
|
||
end
|
||
|
||
commands.printq = function()
|
||
Queue:PrintHeader("📋 CURRENT QUEUE LIST")
|
||
local queueList = Queue:GetQueueList()
|
||
|
||
if #queueList == 0 then
|
||
print(" ^7No players in queue^7")
|
||
else
|
||
for pos, data in ipairs(queueList) do
|
||
print(string_format(" ^3#%d^7 ┃ ^2%s^7", pos, data.name))
|
||
print(string_format(" ├─ ^5Source:^7 %s", data.source))
|
||
print(string_format(" ├─ ^5ID:^7 %s", data.ids[1]))
|
||
print(string_format(" ├─ ^5Priority:^7 %s", tostring(data.priority or "None")))
|
||
print(string_format(" ├─ ^5Last Msg:^7 %s", data.source ~= "debug" and GetPlayerLastMsg(data.source) or "debug"))
|
||
print(string_format(" ├─ ^5Timeout:^7 %s", FormatDuration(data.timeout)))
|
||
print(string_format(" └─ ^5Queue Time:^7 %s", FormatDuration(data.queuetime())))
|
||
if pos < #queueList then print(" ^7" .. CreateSeparator(40, "─")) end
|
||
end
|
||
end
|
||
print("^7" .. CreateSeparator(62, "═"))
|
||
end
|
||
|
||
commands.addc = function()
|
||
Queue:AddToConnecting({"debug"})
|
||
Queue:LogInfo("Debug connecting entry added", {
|
||
Status = "Added to connecting list"
|
||
})
|
||
end
|
||
|
||
commands.removec = function(args)
|
||
args[1] = tonumber(args[1])
|
||
local name = Queue:GetConnectingList()[args[1]] and Queue:GetConnectingList()[args[1]].name or nil
|
||
Queue:RemoveFromConnecting(nil, nil, args[1])
|
||
Queue:LogInfo("Player removed from connecting list", {
|
||
Player = tostring(name),
|
||
Position = args[1]
|
||
})
|
||
end
|
||
|
||
commands.printc = function()
|
||
Queue:PrintHeader("🔗 CURRENT CONNECTING LIST")
|
||
local connList = Queue:GetConnectingList()
|
||
|
||
if #connList == 0 then
|
||
print(" ^7No players connecting^7")
|
||
else
|
||
for pos, data in ipairs(connList) do
|
||
print(string_format(" ^3#%d^7 ┃ ^2%s^7", pos, data.name))
|
||
print(string_format(" ├─ ^5Source:^7 %s", data.source))
|
||
print(string_format(" ├─ ^5ID:^7 %s", data.ids[1]))
|
||
print(string_format(" ├─ ^5Priority:^7 %s", tostring(data.priority or "None")))
|
||
print(string_format(" ├─ ^5Last Msg:^7 %s", data.source ~= "debug" and GetPlayerLastMsg(data.source) or "debug"))
|
||
print(string_format(" └─ ^5Timeout:^7 %s", FormatDuration(data.timeout)))
|
||
if pos < #connList then print(" ^7" .. CreateSeparator(40, "─")) end
|
||
end
|
||
end
|
||
print("^7" .. CreateSeparator(62, "═"))
|
||
end
|
||
|
||
commands.printl = function()
|
||
Queue:PrintHeader("👥 ACTIVE PLAYERS")
|
||
local count = 0
|
||
for k, joined in pairs(Queue:GetPlayerList()) do
|
||
count = count + 1
|
||
print(string_format(" ^2●^7 Source ID: ^3%s^7 | Active: ^2%s^7", k, tostring(joined)))
|
||
end
|
||
print(string_format(" ^7Total: ^2%d^7 players", count))
|
||
print("^7" .. CreateSeparator(62, "═"))
|
||
end
|
||
|
||
commands.printp = function()
|
||
Queue:PrintHeader("⭐ PRIORITY LIST")
|
||
local count = 0
|
||
for id, power in pairs(Queue:GetPriorityList()) do
|
||
count = count + 1
|
||
print(string_format(" ^8★^7 %s ^7=> Power: ^3%s^7", id, tostring(power)))
|
||
end
|
||
print(string_format(" ^7Total: ^3%d^7 priority entries", count))
|
||
print("^7" .. CreateSeparator(62, "═"))
|
||
end
|
||
|
||
commands.printcount = function()
|
||
Queue:PrintStats()
|
||
end
|
||
|
||
commands.printtp = function()
|
||
Queue:PrintHeader("⏳ TEMP PRIORITY LIST")
|
||
local count = 0
|
||
local currentTime = os_time()
|
||
for k, data in pairs(Queue:GetTempPriorityList()) do
|
||
count = count + 1
|
||
local remaining = data.endTime - currentTime
|
||
print(string_format(" ^6◆^7 %s", k))
|
||
print(string_format(" ├─ ^5Power:^7 %s", tostring(data.power)))
|
||
print(string_format(" ├─ ^5Expires In:^7 %s", FormatDuration(math.max(0, remaining))))
|
||
print(string_format(" └─ ^5End Time:^7 %s", os.date("%H:%M:%S", data.endTime)))
|
||
end
|
||
print(string_format(" ^7Total: ^6%d^7 temp priority entries", count))
|
||
print("^7" .. CreateSeparator(62, "═"))
|
||
end
|
||
|
||
commands.removetp = function(args)
|
||
if not args[1] then return end
|
||
|
||
Queue:GetTempPriorityList()[args[1]] = nil
|
||
Queue:LogInfo("Temp priority removed", {
|
||
Identifier = args[1],
|
||
Action = "Removed from temp priority list"
|
||
})
|
||
end
|
||
|
||
commands.setpos = function(args)
|
||
if not args[1] or not args[2] then return end
|
||
|
||
args[1], args[2] = tonumber(args[1]), tonumber(args[2])
|
||
|
||
local data = Queue:GetQueueList()[args[1]]
|
||
|
||
Queue:SetPos(data.ids, args[2])
|
||
|
||
Queue:LogInfo("Queue position updated", {
|
||
Player = data.name,
|
||
Old_Position = args[1],
|
||
New_Position = args[2]
|
||
})
|
||
end
|
||
|
||
commands.setdata = function(args)
|
||
if not args[1] or not args[2] or not args[3] then return end
|
||
args[1] = tonumber(args[1])
|
||
|
||
local num = tonumber(args[3])
|
||
local data = Queue:GetQueueList()[args[1]]
|
||
|
||
if args[2] == "queuetime" then
|
||
local time = data.queuetime()
|
||
local dif = time - num
|
||
|
||
data.firstconnect = data.firstconnect + dif
|
||
data.queuetime = function() return (os_time() - data.firstconnect) end
|
||
else
|
||
data[args[2]] = num and num or args[3]
|
||
end
|
||
|
||
Queue:LogInfo("Player data modified", {
|
||
Player = data.name,
|
||
Field = args[2],
|
||
New_Value = args[3]
|
||
})
|
||
end
|
||
|
||
commands.commands = function()
|
||
Queue:PrintHeader("📚 AVAILABLE COMMANDS")
|
||
print(" ^3addq^7 - Add debug queue entry")
|
||
print(" ^3removeq^7 - Remove from queue by index")
|
||
print(" ^3printq^7 - Print current queue list")
|
||
print(" ^3addc^7 - Add debug connecting entry")
|
||
print(" ^3removec^7 - Remove from connecting by index")
|
||
print(" ^3printc^7 - Print connecting list")
|
||
print(" ^3printl^7 - Print active players")
|
||
print(" ^3printp^7 - Print priority list")
|
||
print(" ^3printcount^7- Print server statistics")
|
||
print(" ^3printtp^7 - Print temp priority list")
|
||
print(" ^3removetp^7 - Remove temp priority")
|
||
print(" ^3setpos^7 - Set queue position")
|
||
print(" ^3setdata^7 - Modify player data")
|
||
print(" ^3commands^7 - Show this help")
|
||
print("^7" .. CreateSeparator(62, "═"))
|
||
end
|
||
|
||
commands.stats = function()
|
||
Queue:PrintStats()
|
||
end
|
||
|
||
AddEventHandler("rconCommand", function(command, args)
|
||
if command == "queue" and commands[args[1]] then
|
||
command = args[1]
|
||
table_remove(args, 1)
|
||
commands[command](args)
|
||
CancelEvent()
|
||
end
|
||
end)
|