mirror of
https://github.com/citizenfx/cfx-server-data.git
synced 2025-12-12 06:14:09 +01:00
gameplay: example money, money fountain, ped money drop and player ID systems
This commit is contained in:
12
resources/[gameplay]/player-data/fxmanifest.lua
Normal file
12
resources/[gameplay]/player-data/fxmanifest.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
version '1.0.0'
|
||||
description 'A basic resource for storing player identifiers.'
|
||||
author 'Cfx.re <pr@fivem.net>'
|
||||
|
||||
fx_version 'bodacious'
|
||||
game 'common'
|
||||
|
||||
server_script 'server.lua'
|
||||
|
||||
provides {
|
||||
'cfx.re/playerData.v1alpha1'
|
||||
}
|
||||
222
resources/[gameplay]/player-data/server.lua
Normal file
222
resources/[gameplay]/player-data/server.lua
Normal file
@@ -0,0 +1,222 @@
|
||||
--- 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)
|
||||
Reference in New Issue
Block a user