222 lines
		
	
	
		
			No EOL
		
	
	
		
			6 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			No EOL
		
	
	
		
			6 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) | 
