1
0
mirror of https://github.com/citizenfx/cfx-server-data.git synced 2025-03-13 07:17:23 +08:00

222 lines
6.0 KiB
Lua

--- player-data is a basic resource to showcase player identifier storage
--
-- it works in a fairly simple way: a set of identifiers is assigned to an account ID, and said
-- account ID is then returned/added as state bag
--
-- it also implements the `cfx.re/playerData.v1alpha1` spec, which is exposed through the following:
-- - getPlayerId(source: string)
-- - getPlayerById(dbId: string)
-- - getPlayerIdFromIdentifier(identifier: string)
-- - setting `cfx.re/playerData@id` state bag field on the player
-- identifiers that we'll ignore (e.g. IP) as they're low-trust/high-variance
local identifierBlocklist = {
ip = true
}
-- function to check if the identifier is blocked
local function isIdentifierBlocked(identifier)
-- Lua pattern to correctly split
local idType = identifier:match('([^:]+):')
-- ensure it's a boolean
return identifierBlocklist[idType] or false
end
-- our database schema, in hierarchical KVS syntax:
-- player:
-- <id>:
-- identifier:
-- <identifier>: 'true'
-- identifier:
-- <identifier>: <playerId>
-- list of player indices to data
local players = {}
-- list of player DBIDs to player indices
local playersById = {}
-- a sequence field using KVS
local function incrementId()
local nextId = GetResourceKvpInt('nextId')
nextId = nextId + 1
SetResourceKvpInt('nextId', nextId)
return nextId
end
-- gets the ID tied to an identifier in the schema, or nil
local function getPlayerIdFromIdentifier(identifier)
local str = GetResourceKvpString(('identifier:%s'):format(identifier))
if not str then
return nil
end
return msgpack.unpack(str).id
end
-- stores the identifier + adds to a logging list
local function setPlayerIdFromIdentifier(identifier, id)
local str = ('identifier:%s'):format(identifier)
SetResourceKvp(str, msgpack.pack({ id = id }))
SetResourceKvp(('player:%s:identifier:%s'):format(id, identifier), 'true')
end
-- stores any new identifiers for this player ID
local function storeIdentifiers(playerIdx, newId)
for _, identifier in ipairs(GetPlayerIdentifiers(playerIdx)) do
if not isIdentifierBlocked(identifier) then
-- TODO: check if the player already has an identifier of this type
setPlayerIdFromIdentifier(identifier, newId)
end
end
end
-- registers a new player (increments sequence, stores data, returns ID)
local function registerPlayer(playerIdx)
local newId = incrementId()
storeIdentifiers(playerIdx, newId)
return newId
end
-- initializes a player's data set
local function setupPlayer(playerIdx)
-- try getting the oldest-known identity from all the player's identifiers
local defaultId = 0xFFFFFFFFFF
local lowestId = defaultId
for _, identifier in ipairs(GetPlayerIdentifiers(playerIdx)) do
if not isIdentifierBlocked(identifier) then
local dbId = getPlayerIdFromIdentifier(identifier)
if dbId then
if dbId < lowestId then
lowestId = dbId
end
end
end
end
-- if this is the default ID, register. if not, update
local playerId
if lowestId == defaultId then
playerId = registerPlayer(playerIdx)
else
storeIdentifiers(playerIdx, lowestId)
playerId = lowestId
end
-- add state bag field
if Player then
Player(playerIdx).state['cfx.re/playerData@id'] = playerId
end
-- and add to our caching tables
players[playerIdx] = {
dbId = playerId
}
playersById[tostring(playerId)] = playerIdx
end
-- we want to add a player pretty early
AddEventHandler('playerConnecting', function()
local playerIdx = tostring(source)
setupPlayer(playerIdx)
end)
-- and migrate them to a 'joining' ID where possible
RegisterNetEvent('playerJoining')
AddEventHandler('playerJoining', function(oldIdx)
-- resource restart race condition
local oldPlayer = players[tostring(oldIdx)]
if oldPlayer then
players[tostring(source)] = oldPlayer
players[tostring(oldIdx)] = nil
else
setupPlayer(tostring(source))
end
end)
-- remove them if they're dropped
AddEventHandler('playerDropped', function()
local player = players[tostring(source)]
if player then
playersById[tostring(player.dbId)] = nil
end
players[tostring(source)] = nil
end)
-- and when the resource is restarted, set up all players that are on right now
for _, player in ipairs(GetPlayers()) do
setupPlayer(player)
end
-- also a quick command to get the current state
RegisterCommand('playerData', function(source, args)
if not args[1] then
print('Usage:')
print('\tplayerData getId <dbId>: gets identifiers for ID')
print('\tplayerData getIdentifier <identifier>: gets ID for identifier')
return
end
if args[1] == 'getId' then
local prefix = ('player:%s:identifier:'):format(args[2])
local handle = StartFindKvp(prefix)
local key
repeat
key = FindKvp(handle)
if key then
print('result:', key:sub(#prefix + 1))
end
until not key
EndFindKvp(handle)
elseif args[1] == 'getIdentifier' then
print('result:', getPlayerIdFromIdentifier(args[2]))
end
end, true)
-- COMPATIBILITY for server versions that don't export provide
local function getExportEventName(resource, name)
return string.format('__cfx_export_%s_%s', resource, name)
end
function AddExport(name, fn)
if not Citizen.Traits or not Citizen.Traits.ProvidesExports then
AddEventHandler(getExportEventName('cfx.re/playerData.v1alpha1', name), function(setCB)
setCB(fn)
end)
end
exports(name, fn)
end
-- exports
AddExport('getPlayerIdFromIdentifier', getPlayerIdFromIdentifier)
AddExport('getPlayerId', function(playerIdx)
local player = players[tostring(playerIdx)]
if not player then
return nil
end
return player.dbId
end)
AddExport('getPlayerById', function(playerId)
return playersById[tostring(playerId)]
end)