mirror of
https://github.com/citizenfx/cfx-server-data.git
synced 2025-02-13 17:53:16 +08:00
gameplay: example money, money fountain, ped money drop and player ID systems
This commit is contained in:
parent
537eae7a00
commit
5073447ef4
@ -0,0 +1,6 @@
|
|||||||
|
fx_version 'cerulean'
|
||||||
|
game 'gta5'
|
||||||
|
|
||||||
|
map 'map.lua'
|
||||||
|
|
||||||
|
dependency 'money-fountain'
|
@ -0,0 +1,4 @@
|
|||||||
|
money_fountain 'test_fountain' {
|
||||||
|
vector3(97.334, -973.621, 29.36),
|
||||||
|
amount = 75
|
||||||
|
}
|
101
resources/[gameplay]/[examples]/money-fountain/client.lua
Normal file
101
resources/[gameplay]/[examples]/money-fountain/client.lua
Normal file
@ -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)
|
@ -0,0 +1,18 @@
|
|||||||
|
version '1.0.0'
|
||||||
|
description 'An example money system client containing a money fountain.'
|
||||||
|
author 'Cfx.re <pr@fivem.net>'
|
||||||
|
|
||||||
|
fx_version 'bodacious'
|
||||||
|
game 'gta5'
|
||||||
|
|
||||||
|
client_script 'client.lua'
|
||||||
|
server_script 'server.lua'
|
||||||
|
|
||||||
|
shared_script 'mapdata.lua'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
'mapmanager',
|
||||||
|
'money'
|
||||||
|
}
|
||||||
|
|
||||||
|
lua54 'yes'
|
28
resources/[gameplay]/[examples]/money-fountain/mapdata.lua
Normal file
28
resources/[gameplay]/[examples]/money-fountain/mapdata.lua
Normal file
@ -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)
|
107
resources/[gameplay]/[examples]/money-fountain/server.lua
Normal file
107
resources/[gameplay]/[examples]/money-fountain/server.lua
Normal file
@ -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)
|
30
resources/[gameplay]/[examples]/money/client.lua
Normal file
30
resources/[gameplay]/[examples]/money/client.lua
Normal file
@ -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)
|
12
resources/[gameplay]/[examples]/money/fxmanifest.lua
Normal file
12
resources/[gameplay]/[examples]/money/fxmanifest.lua
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version '1.0.0'
|
||||||
|
description 'An example money system using KVS.'
|
||||||
|
author 'Cfx.re <pr@fivem.net>'
|
||||||
|
|
||||||
|
fx_version 'bodacious'
|
||||||
|
game 'gta5'
|
||||||
|
|
||||||
|
client_script 'client.lua'
|
||||||
|
server_script 'server.lua'
|
||||||
|
|
||||||
|
--dependency 'cfx.re/playerData.v1alpha1'
|
||||||
|
lua54 'yes'
|
119
resources/[gameplay]/[examples]/money/server.lua
Normal file
119
resources/[gameplay]/[examples]/money/server.lua
Normal file
@ -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)
|
41
resources/[gameplay]/[examples]/ped-money-drops/client.lua
Normal file
41
resources/[gameplay]/[examples]/ped-money-drops/client.lua
Normal file
@ -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)
|
@ -0,0 +1,11 @@
|
|||||||
|
version '1.0.0'
|
||||||
|
description 'An example money system client.'
|
||||||
|
author 'Cfx.re <pr@fivem.net>'
|
||||||
|
|
||||||
|
fx_version 'bodacious'
|
||||||
|
game 'gta5'
|
||||||
|
|
||||||
|
client_script 'client.lua'
|
||||||
|
server_script 'server.lua'
|
||||||
|
|
||||||
|
lua54 'yes'
|
42
resources/[gameplay]/[examples]/ped-money-drops/server.lua
Normal file
42
resources/[gameplay]/[examples]/ped-money-drops/server.lua
Normal file
@ -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)
|
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)
|
Loading…
Reference in New Issue
Block a user