monitor/resource/sv_playerlist.lua
2025-04-16 22:30:27 +07:00

258 lines
7.9 KiB
Lua

-- Prevent running in monitor mode
if not TX_SERVER_MODE then return end
-- =============================================
-- Server PlayerList handler
-- =============================================
local function logError(x)
txPrint("^1" .. x)
end
local oneSyncConvar = GetConvar('onesync', 'off')
local onesyncEnabled = oneSyncConvar == 'on' or oneSyncConvar == 'legacy'
-- Optimizations
local floor = math.floor
local min = math.min
local sub = string.sub
local tonumber = tonumber
local tostring = tostring
local pairs = pairs
-- Variables & Consts
-- https://www.desmos.com/calculator/dx9f5ko2ge
local refreshMinDelay = 1500
local refreshMaxDelay = 5000
local maxPlayersDelayCeil = 300 --at this number, the delay won't increase more
local intervalYieldLimit = 50
local vTypeMap = {
["nil"] = -1,
["walking"] = 0,
["automobile"] = 1,
["bike"] = 2,
["boat"] = 3,
["heli"] = 4,
["plane"] = 5,
["submarine"] = 6,
["trailer"] = 7,
["train"] = 8,
}
--[[ Wrapper to refresh player list data ]]
local function refreshPlayerList()
-- For each player
local players = GetPlayers()
for yieldCounter, serverID in pairs(players) do
-- Updating player vehicle/health
local health = -1
local vType = -1
local xCoord = nil
local yCoord = nil
if onesyncEnabled == true then
local ped = GetPlayerPed(serverID)
if ped and DoesEntityExist(ped) then
health = GetPedHealthPercent(ped)
local veh = GetVehiclePedIsIn(ped)
if veh ~= 0 and DoesEntityExist(veh) then
vType = vTypeMap[tostring(GetVehicleType(veh))]
else
vType = vTypeMap["walking"]
end
local coords = GetEntityCoords(ped)
xCoord = math.floor(coords.x)
yCoord = math.floor(coords.y)
end
end
-- Updating TX_PLAYERLIST
if type(TX_PLAYERLIST[serverID]) ~= 'table' then
TX_PLAYERLIST[serverID] = {
name = sub(GetPlayerName(serverID) or "unknown", 1, 75),
health = health,
vType = vType,
xCoord = xCoord,
yCoord = yCoord,
}
else
TX_PLAYERLIST[serverID].health = health
TX_PLAYERLIST[serverID].vType = vType
TX_PLAYERLIST[serverID].xCoord = xCoord
TX_PLAYERLIST[serverID].yCoord = yCoord
end
-- Mark as refreshed
TX_PLAYERLIST[serverID].foundLastCheck = true
-- Yield to prevent hitches
if yieldCounter % intervalYieldLimit == 0 then
Wait(0)
end
end --end for players
--Check if player disconnected
local playersOnline = 0
for playerID, playerData in pairs(TX_PLAYERLIST) do
if playerData.foundLastCheck == true then
playersOnline = playersOnline + 1
playerData.foundLastCheck = false
else
TX_PLAYERLIST[playerID] = nil
end
end
return playersOnline
end
--[[ Thread to refresh player list ]]
CreateThread(function()
while true do
-- Attempt to refresh player list
local callSuccess, callOutput = pcall(refreshPlayerList)
local playersOnline = 0
if callSuccess then
playersOnline = callOutput
else
logError("failed to update playerlist")
end
-- DEBUG
-- debugPrint("====================================")
-- print(json.encode(TX_PLAYERLIST, {indent = true}))
-- debugPrint("====================================")
-- Refresh interval with linear function
local hDiff = refreshMaxDelay - refreshMinDelay
local calcDelay = (hDiff / maxPlayersDelayCeil) * (playersOnline) + refreshMinDelay
local delay = floor(min(calcDelay, refreshMaxDelay))
Wait(delay)
end --end while true
end)
--[[ Handle player Join or Leave ]]
AddEventHandler('playerJoining', function(srcString, _oldID)
-- sanity checking source
if source <= 0 then
logError('playerJoining event with source ' .. json.encode(source))
return
end
-- checking if the player was not already dropped
local playerDetectedName = GetPlayerName(source)
if type(playerDetectedName) ~= 'string' then
logError('Received a playerJoining for a player that was already dropped. There is some resource dropping the player at the playerJoining event handler without first waiting for the next tick.')
return
end
local playerData = {
name = sub(playerDetectedName or "unknown", 1, 128),
ids = GetPlayerIdentifiers(source),
hwids = GetPlayerTokens(source),
}
PrintStructuredTrace(json.encode({
type = 'txAdminPlayerlistEvent',
event = 'playerJoining',
id = source,
player = playerData
}))
-- relaying this info to all admins
for adminID, _ in pairs(TX_ADMINS) do
TriggerClientEvent('txcl:plist:updatePlayer', adminID, source, playerData.name)
end
end)
AddEventHandler('playerDropped', function(reason, resource, category)
-- sanity checking source
if source <= 0 then
logError('playerDropped event with source ' .. json.encode(source))
return
end
if resource == 'monitor' and TX_IS_SERVER_SHUTTING_DOWN then
reason = 'server_shutting_down'
end
PrintStructuredTrace(json.encode({
type = 'txAdminPlayerlistEvent',
event = 'playerDropped',
id = source,
reason = reason,
resource = resource,
category = category,
}))
-- relaying this info to all admins
for adminID, _ in pairs(TX_ADMINS) do
TriggerClientEvent('txcl:plist:updatePlayer', adminID, source, false)
end
end)
-- Handle getDetailedPlayerlist
-- This event is only called when the menu "players" tab is opened, and every 5s while the tab is open
-- DEBUG playerlist scroll test stuff
-- math.randomseed(os.time())
-- local fake_playerlist = {}
-- local fake_admins = {1, 10, 21, 61, 91, 141, 281}
-- local function getFakePlayer()
-- return {
-- name = 'fake'..tostring(math.random(999999)),
-- health = 0,
-- vType = math.random(8),
-- }
-- end
-- for serverID=1, 500 do
-- fake_playerlist[serverID] = getFakePlayer()
-- end
RegisterNetEvent('txsv:req:plist:getDetailed', function(getPlayerNames)
if TX_ADMINS[tostring(source)] == nil then
debugPrint('Ignoring unauthenticated getDetailedPlayerlist() by ' .. source)
return
end
local players = {}
--DEBUG replace TX_PLAYERLIST with fake_playerlist and playerData.health with math.random(150)
for playerID, playerData in pairs(TX_PLAYERLIST) do
players[#players + 1] = {
tonumber(playerID),
playerData.health,
playerData.vType,
playerData.xCoord,
playerData.yCoord,
}
if getPlayerNames then
players[#players][6] = playerData.name
end
end
local admins = {}
for adminID, _ in pairs(TX_ADMINS) do
admins[#admins + 1] = tonumber(adminID)
end
--DEBUG replace admins with fake_admins
TriggerClientEvent('txcl:plist:setDetailed', source, players, admins)
end)
-- Sends the initial playlist to a specific admin
-- Triggered by the server after admin auth
function sendInitialPlayerlist(adminID)
local payload = {}
--DEBUG replace TX_PLAYERLIST with fake_playerlist
for playerID, playerData in pairs(TX_PLAYERLIST) do
payload[#payload + 1] = { tonumber(playerID), playerData.name }
end
--DEBUG
-- debugPrint("====================================")
-- print(json.encode(payload, {indent = true}))
-- debugPrint("====================================")
debugPrint('Sending initial playerlist to ' .. adminID)
TriggerClientEvent('txcl:plist:setInitial', adminID, payload)
end