From 5073447ef44a3f5cf14d9ebb1287577f3bde36fb Mon Sep 17 00:00:00 2001 From: blattersturm Date: Sun, 1 Nov 2020 12:50:24 +0100 Subject: [PATCH] gameplay: example money, money fountain, ped money drop and player ID systems --- .../money-fountain-example-map/fxmanifest.lua | 6 + .../money-fountain-example-map/map.lua | 4 + .../[examples]/money-fountain/client.lua | 101 ++++++++ .../[examples]/money-fountain/fxmanifest.lua | 18 ++ .../[examples]/money-fountain/mapdata.lua | 28 +++ .../[examples]/money-fountain/server.lua | 107 +++++++++ .../[gameplay]/[examples]/money/client.lua | 30 +++ .../[examples]/money/fxmanifest.lua | 12 + .../[gameplay]/[examples]/money/server.lua | 119 ++++++++++ .../[examples]/ped-money-drops/client.lua | 41 ++++ .../[examples]/ped-money-drops/fxmanifest.lua | 11 + .../[examples]/ped-money-drops/server.lua | 42 ++++ .../[gameplay]/player-data/fxmanifest.lua | 12 + resources/[gameplay]/player-data/server.lua | 222 ++++++++++++++++++ 14 files changed, 753 insertions(+) create mode 100644 resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua create mode 100644 resources/[gameplay]/[examples]/money-fountain-example-map/map.lua create mode 100644 resources/[gameplay]/[examples]/money-fountain/client.lua create mode 100644 resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua create mode 100644 resources/[gameplay]/[examples]/money-fountain/mapdata.lua create mode 100644 resources/[gameplay]/[examples]/money-fountain/server.lua create mode 100644 resources/[gameplay]/[examples]/money/client.lua create mode 100644 resources/[gameplay]/[examples]/money/fxmanifest.lua create mode 100644 resources/[gameplay]/[examples]/money/server.lua create mode 100644 resources/[gameplay]/[examples]/ped-money-drops/client.lua create mode 100644 resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua create mode 100644 resources/[gameplay]/[examples]/ped-money-drops/server.lua create mode 100644 resources/[gameplay]/player-data/fxmanifest.lua create mode 100644 resources/[gameplay]/player-data/server.lua diff --git a/resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua b/resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua new file mode 100644 index 0000000..412880b --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua @@ -0,0 +1,6 @@ +fx_version 'cerulean' +game 'gta5' + +map 'map.lua' + +dependency 'money-fountain' \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain-example-map/map.lua b/resources/[gameplay]/[examples]/money-fountain-example-map/map.lua new file mode 100644 index 0000000..e7e1626 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain-example-map/map.lua @@ -0,0 +1,4 @@ +money_fountain 'test_fountain' { + vector3(97.334, -973.621, 29.36), + amount = 75 +} \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/client.lua b/resources/[gameplay]/[examples]/money-fountain/client.lua new file mode 100644 index 0000000..1cdc1f1 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/client.lua @@ -0,0 +1,101 @@ +-- add text entries for all the help types we have +AddTextEntry('FOUNTAIN_HELP', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.') +AddTextEntry('FOUNTAIN_HELP_DRAINED', 'This fountain currently contains $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.') +AddTextEntry('FOUNTAIN_HELP_BROKE', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.') +AddTextEntry('FOUNTAIN_HELP_BROKE_N_DRAINED', 'This fountain currently contains $~1~.') +AddTextEntry('FOUNTAIN_HELP_INUSE', 'This fountain currently contains $~1~.~n~You can use it again in ~a~.') + +-- upvalue aliases so that we will be fast if far away +local Wait = Wait +local GetEntityCoords = GetEntityCoords +local PlayerPedId = PlayerPedId + +-- timer, don't tick as frequently if we're far from any money fountain +local relevanceTimer = 500 + +CreateThread(function() + local pressing = false + + while true do + Wait(relevanceTimer) + + local coords = GetEntityCoords(PlayerPedId()) + + for _, data in pairs(moneyFountains) do + -- if we're near this fountain + local dist = #(coords - data.coords) + + -- near enough to draw + if dist < 40 then + -- ensure per-frame tick + relevanceTimer = 0 + + DrawMarker(29, data.coords.x, data.coords.y, data.coords.z, 0, 0, 0, 0.0, 0, 0, 1.0, 1.0, 1.0, 0, 150, 0, 120, false, true, 2, false, nil, nil, false) + else + -- put the relevance timer back to the way it was + relevanceTimer = 500 + end + + -- near enough to use + if dist < 1 then + -- are we able to use it? if not, display appropriate help + local player = LocalPlayer + local nextUse = player.state['fountain_nextUse'] + + -- GetNetworkTime is synced for everyone + if nextUse and nextUse >= GetNetworkTime() then + BeginTextCommandDisplayHelp('FOUNTAIN_HELP_INUSE') + AddTextComponentInteger(GlobalState['fountain_' .. data.id]) + AddTextComponentSubstringTime(math.tointeger(nextUse - GetNetworkTime()), 2 | 4) -- seconds (2), minutes (4) + EndTextCommandDisplayHelp(0, false, false, 1000) + else + -- handle inputs for pickup/place + if not pressing then + if IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) then + TriggerServerEvent('money_fountain:tryPickup', data.id) + pressing = true + elseif IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then + TriggerServerEvent('money_fountain:tryPlace', data.id) + pressing = true + end + else + if not IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) and + not IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then + pressing = false + end + end + + -- decide the appropriate help message + local youCanSpend = (player.state['money_cash'] or 0) >= data.amount + local fountainCanSpend = GlobalState['fountain_' .. data.id] >= data.amount + + local helpName + + if youCanSpend and fountainCanSpend then + helpName = 'FOUNTAIN_HELP' + elseif youCanSpend and not fountainCanSpend then + helpName = 'FOUNTAIN_HELP_DRAINED' + elseif not youCanSpend and fountainCanSpend then + helpName = 'FOUNTAIN_HELP_BROKE' + else + helpName = 'FOUNTAIN_HELP_BROKE_N_DRAINED' + end + + -- and print it + BeginTextCommandDisplayHelp(helpName) + AddTextComponentInteger(GlobalState['fountain_' .. data.id]) + + if fountainCanSpend then + AddTextComponentInteger(data.amount) + end + + if youCanSpend then + AddTextComponentInteger(data.amount) + end + + EndTextCommandDisplayHelp(0, false, false, 1000) + end + end + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua b/resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua new file mode 100644 index 0000000..159b453 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua @@ -0,0 +1,18 @@ +version '1.0.0' +description 'An example money system client containing a money fountain.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'gta5' + +client_script 'client.lua' +server_script 'server.lua' + +shared_script 'mapdata.lua' + +dependencies { + 'mapmanager', + 'money' +} + +lua54 'yes' \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/mapdata.lua b/resources/[gameplay]/[examples]/money-fountain/mapdata.lua new file mode 100644 index 0000000..4eecad8 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/mapdata.lua @@ -0,0 +1,28 @@ +-- define the money fountain list (SHARED SCRIPT) +moneyFountains = {} + +-- index to know what to remove +local fountainIdx = 1 + +AddEventHandler('getMapDirectives', function(add) + -- add a 'money_fountain' map directive + add('money_fountain', function(state, name) + return function(data) + local coords = data[1] + local amount = data.amount or 100 + + local idx = fountainIdx + fountainIdx += 1 + + moneyFountains[idx] = { + id = name, + coords = coords, + amount = amount + } + + state.add('idx', idx) + end + end, function(state) + moneyFountains[state.idx] = nil + end) +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/server.lua b/resources/[gameplay]/[examples]/money-fountain/server.lua new file mode 100644 index 0000000..9b1e238 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/server.lua @@ -0,0 +1,107 @@ +-- track down what we've added to global state +local sentState = {} + +-- money system +local ms = exports['money'] + +-- get the fountain content from storage +local function getMoneyForId(fountainId) + return GetResourceKvpInt(('money:%s'):format(fountainId)) / 100.0 +end + +-- set the fountain content in storage + state +local function setMoneyForId(fountainId, money) + GlobalState['fountain_' .. fountainId] = math.tointeger(money) + + return SetResourceKvpInt(('money:%s'):format(fountainId), math.tointeger(money * 100.0)) +end + +-- get the nearest fountain to the player + ID +local function getMoneyFountain(id, source) + local coords = GetEntityCoords(GetPlayerPed(source)) + + for _, v in pairs(moneyFountains) do + if v.id == id then + if #(v.coords - coords) < 2.5 then + return v + end + end + end + + return nil +end + +-- generic function for events +local function handleFountainStuff(source, id, pickup) + -- if near the fountain we specify + local fountain = getMoneyFountain(id, source) + + if fountain then + -- and we can actually use the fountain already + local player = Player(source) + + local nextUse = player.state['fountain_nextUse'] + if not nextUse then + nextUse = 0 + end + + -- GetGameTimer ~ GetNetworkTime on client + if nextUse <= GetGameTimer() then + -- not rate limited + local success = false + local money = getMoneyForId(fountain.id) + + -- decide the op + if pickup then + -- if the fountain is rich enough to get the per-use amount + if money >= fountain.amount then + -- give the player money + if ms:addMoney(source, 'cash', fountain.amount) then + money -= fountain.amount + success = true + end + end + else + -- if the player is rich enough + if ms:removeMoney(source, 'cash', fountain.amount) then + -- add to the fountain + money += fountain.amount + success = true + end + end + + -- save it and set the player's cooldown + if success then + setMoneyForId(fountain.id, money) + player.state['fountain_nextUse'] = GetGameTimer() + GetConvarInt('moneyFountain_cooldown', 5000) + end + end + end +end + +-- event for picking up fountain->player +RegisterNetEvent('money_fountain:tryPickup') +AddEventHandler('money_fountain:tryPickup', function(id) + handleFountainStuff(source, id, true) +end) + +-- event for donating player->fountain +RegisterNetEvent('money_fountain:tryPlace') +AddEventHandler('money_fountain:tryPlace', function(id) + handleFountainStuff(source, id, false) +end) + +-- listener: if a new fountain is added, set its current money in state +CreateThread(function() + while true do + Wait(500) + + for _, fountain in pairs(moneyFountains) do + if not sentState[fountain.id] then + GlobalState['fountain_' .. fountain.id] = math.tointeger(getMoneyForId(fountain.id)) + + sentState[fountain.id] = true + end + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money/client.lua b/resources/[gameplay]/[examples]/money/client.lua new file mode 100644 index 0000000..e094dd1 --- /dev/null +++ b/resources/[gameplay]/[examples]/money/client.lua @@ -0,0 +1,30 @@ +local moneyTypes = { + cash = `MP0_WALLET_BALANCE`, + bank = `BANK_BALANCE`, +} + +RegisterNetEvent('money:displayUpdate') + +AddEventHandler('money:displayUpdate', function(type, money) + local stat = moneyTypes[type] + if not stat then return end + StatSetInt(stat, math.floor(money)) +end) + +TriggerServerEvent('money:requestDisplay') + +CreateThread(function() + while true do + Wait(0) + + if IsControlJustPressed(0, 20) then + SetMultiplayerBankCash() + SetMultiplayerWalletCash() + + Wait(4350) + + RemoveMultiplayerBankCash() + RemoveMultiplayerWalletCash() + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money/fxmanifest.lua b/resources/[gameplay]/[examples]/money/fxmanifest.lua new file mode 100644 index 0000000..8dd5ed7 --- /dev/null +++ b/resources/[gameplay]/[examples]/money/fxmanifest.lua @@ -0,0 +1,12 @@ +version '1.0.0' +description 'An example money system using KVS.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'gta5' + +client_script 'client.lua' +server_script 'server.lua' + +--dependency 'cfx.re/playerData.v1alpha1' +lua54 'yes' \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money/server.lua b/resources/[gameplay]/[examples]/money/server.lua new file mode 100644 index 0000000..2b2ce96 --- /dev/null +++ b/resources/[gameplay]/[examples]/money/server.lua @@ -0,0 +1,119 @@ +local playerData = exports['cfx.re/playerData.v1alpha1'] + +local validMoneyTypes = { + bank = true, + cash = true, +} + +local function getMoneyForId(playerId, moneyType) + return GetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType)) / 100.0 +end + +local function setMoneyForId(playerId, moneyType, money) + local s = playerData:getPlayerById(playerId) + + TriggerEvent('money:updated', { + dbId = playerId, + source = s, + moneyType = moneyType, + money = money + }) + + return SetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType), math.tointeger(money * 100.0)) +end + +local function addMoneyForId(playerId, moneyType, amount) + local curMoney = getMoneyForId(playerId, moneyType) + curMoney += amount + + if curMoney >= 0 then + setMoneyForId(playerId, moneyType, curMoney) + return true, curMoney + end + + return false, 0 +end + +exports('addMoney', function(playerIdx, moneyType, amount) + amount = tonumber(amount) + + if amount <= 0 or amount > (1 << 30) then + return false + end + + if not validMoneyTypes[moneyType] then + return false + end + + local playerId = playerData:getPlayerId(playerIdx) + local success, money = addMoneyForId(playerId, moneyType, amount) + + if success then + Player(playerIdx).state['money_' .. moneyType] = money + end + + return true +end) + +exports('removeMoney', function(playerIdx, moneyType, amount) + amount = tonumber(amount) + + if amount <= 0 or amount > (1 << 30) then + return false + end + + if not validMoneyTypes[moneyType] then + return false + end + + local playerId = playerData:getPlayerId(playerIdx) + local success, money = addMoneyForId(playerId, moneyType, -amount) + + if success then + Player(playerIdx).state['money_' .. moneyType] = money + end + + return success +end) + +exports('getMoney', function(playerIdx, moneyType) + local playerId = playerData:getPlayerId(playerIdx) + return getMoneyForId(playerId, moneyType) +end) + +-- player display bits +AddEventHandler('money:updated', function(data) + if data.source then + TriggerClientEvent('money:displayUpdate', data.source, data.moneyType, data.money) + end +end) + +RegisterNetEvent('money:requestDisplay') + +AddEventHandler('money:requestDisplay', function() + local source = source + local playerId = playerData:getPlayerId(source) + + for type, _ in pairs(validMoneyTypes) do + local amount = getMoneyForId(playerId, type) + TriggerClientEvent('money:displayUpdate', source, type, amount) + + Player(source).state['money_' .. type] = amount + end +end) + +RegisterCommand('earn', function(source, args) + local type = args[1] + local amount = tonumber(args[2]) + + exports['money']:addMoney(source, type, amount) +end, true) + +RegisterCommand('spend', function(source, args) + local type = args[1] + local amount = tonumber(args[2]) + + if not exports['money']:removeMoney(source, type, amount) then + print('you are broke??') + end +end, true) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/ped-money-drops/client.lua b/resources/[gameplay]/[examples]/ped-money-drops/client.lua new file mode 100644 index 0000000..d6d000c --- /dev/null +++ b/resources/[gameplay]/[examples]/ped-money-drops/client.lua @@ -0,0 +1,41 @@ +AddEventHandler('gameEventTriggered', function(eventName, args) + if eventName == 'CEventNetworkEntityDamage' then + local victim = args[1] + local culprit = args[2] + local isDead = args[4] == 1 + + if isDead then + local origCoords = GetEntityCoords(victim) + local pickup = CreatePickupRotate(`PICKUP_MONEY_VARIABLE`, origCoords.x, origCoords.y, origCoords.z - 0.7, 0.0, 0.0, 0.0, 512, 0, false, 0) + local netId = PedToNet(victim) + + local undoStuff = { false } + + CreateThread(function() + local self = PlayerPedId() + + while not undoStuff[1] do + Wait(50) + + if #(GetEntityCoords(self) - origCoords) < 2.5 and HasPickupBeenCollected(pickup) then + TriggerServerEvent('money:tryPickup', netId) + + RemovePickup(pickup) + break + end + end + + undoStuff[1] = true + end) + + SetTimeout(15000, function() + if not undoStuff[1] then + RemovePickup(pickup) + undoStuff[1] = true + end + end) + + TriggerServerEvent('money:allowPickupNear', netId) + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua b/resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua new file mode 100644 index 0000000..f338973 --- /dev/null +++ b/resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua @@ -0,0 +1,11 @@ +version '1.0.0' +description 'An example money system client.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'gta5' + +client_script 'client.lua' +server_script 'server.lua' + +lua54 'yes' diff --git a/resources/[gameplay]/[examples]/ped-money-drops/server.lua b/resources/[gameplay]/[examples]/ped-money-drops/server.lua new file mode 100644 index 0000000..6876952 --- /dev/null +++ b/resources/[gameplay]/[examples]/ped-money-drops/server.lua @@ -0,0 +1,42 @@ +local safePositions = {} + +RegisterNetEvent('money:allowPickupNear') + +AddEventHandler('money:allowPickupNear', function(pedId) + local entity = NetworkGetEntityFromNetworkId(pedId) + + Wait(250) + + if not DoesEntityExist(entity) then + return + end + + if GetEntityHealth(entity) > 100 then + return + end + + local coords = GetEntityCoords(entity) + safePositions[pedId] = coords +end) + +RegisterNetEvent('money:tryPickup') + +AddEventHandler('money:tryPickup', function(entity) + if not safePositions[entity] then + return + end + + local source = source + local playerPed = GetPlayerPed(source) + local coords = GetEntityCoords(playerPed) + + if #(safePositions[entity] - coords) < 2.5 then + exports['money']:addMoney(source, 'cash', 40) + end + + safePositions[entity] = nil +end) + +AddEventHandler('entityRemoved', function(entity) + safePositions[entity] = nil +end) \ No newline at end of file diff --git a/resources/[gameplay]/player-data/fxmanifest.lua b/resources/[gameplay]/player-data/fxmanifest.lua new file mode 100644 index 0000000..3b6b89b --- /dev/null +++ b/resources/[gameplay]/player-data/fxmanifest.lua @@ -0,0 +1,12 @@ +version '1.0.0' +description 'A basic resource for storing player identifiers.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'common' + +server_script 'server.lua' + +provides { + 'cfx.re/playerData.v1alpha1' +} \ No newline at end of file diff --git a/resources/[gameplay]/player-data/server.lua b/resources/[gameplay]/player-data/server.lua new file mode 100644 index 0000000..a262281 --- /dev/null +++ b/resources/[gameplay]/player-data/server.lua @@ -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: +-- : +-- identifier: +-- : 'true' +-- identifier: +-- : + +-- 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 : gets identifiers for ID') + print('\tplayerData getIdentifier : 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) \ No newline at end of file