KVP, state bags, database, frameworks
This page covers the modules that store and read data: resource KVP (Fx.kvp), state bags (Fx.state), oxmysql (Fx.db) and the player, money and job API over ESX, QBCore, Qbox and standalone (Fx.framework). Each one checks values with a Schema at the boundary and turns failures into tagged errors you can catch with catchTag (Errors). Every function returns an effect unless noted; run it with Fx.yield inside Fx.gen, Fx.runFork or Fx.runAwait (Effects).
| Module | Side |
|---|---|
Fx.kvp |
both |
Fx.state |
both |
Fx.db |
server |
Fx.framework |
server |
KVP
Fx.kvp stores values in the resource's KVP (SetResourceKvp) as JSON. With a schema, values are checked on the way in and on the way out.
Error classes on Fx.kvp:
KvpInvalid { key, message }: a write was refused (failed its schema, cannot be encoded, or would not survive JSON).KvpDecodeError { key, message }: a stored value is not valid JSON or fails its schema.
Fx.kvp.get(key, schema)
Fx.kvp.get(key: string, schema?: Fx.Schema) -> Fx.Effect<any, KvpDecodeError>
- Side: both.
- Parameters:
key;schema, checked against the decoded value. - Returns: the decoded value (as the schema outputs it), or
nilwhen the key is not set. - Fails with:
KvpDecodeErrorwhen the stored string is not JSON (stored value is not valid JSON) or fails the schema (the schema's message). - Notes: values written with plain
SetResourceKvpare read too, so a non-JSON string written by old code fails here.
local Prefs = S.struct { volume = S.number:between(0, 1) }
local prefs = Fx.yield(Fx.kvp.get('radio:prefs', Prefs)) or { volume = 0.5 }
Fx.kvp.set(key, value, schema)
Fx.kvp.set(key: string, value: any, schema?: Fx.Schema) -> Fx.Effect<nil, KvpInvalid>
- Side: both.
- Parameters:
key;value;schema, checked (and applied) before writing. - Returns:
nil. - Fails with:
KvpInvalidwhen:- the value fails
schema(the schema's message); json.encodefails (value cannot be encoded as JSON);- the value would come back different after a JSON round trip: tables mixing array and string keys, arrays with holes, or non-string keys (
value does not survive JSON encoding ...).
- the value fails
- Notes: with a schema, the stored value is the schema's output, so unknown struct keys are stripped.
Fx.yield(Fx.kvp.set('radio:prefs', { volume = 0.4 }, Prefs))
Fx.yield(Fx.kvp.set('bad', { 'a', x = 1 })) -- fails KvpInvalid: mixed keys
Fx.kvp.delete(key)
Fx.kvp.delete(key: string) -> Fx.Effect<nil, never>
- Side: both.
- Returns:
nil. - Fails with: never.
Fx.yield(Fx.kvp.delete('radio:prefs'))
Fx.kvp.keys(prefix)
Fx.kvp.keys(prefix: string) -> Fx.Effect<string[], never>
- Side: both.
- Returns: every key starting with
prefix(full keys, prefix included), sorted. An empty list when there are none. - Fails with: never.
local keys = Fx.yield(Fx.kvp.keys('garage:')) -- { 'garage:ABC123', 'garage:XYZ789' }
Fx.kvp.typed(prefix, schema)
Fx.kvp.typed(prefix: string, schema: Fx.Schema) -> Fx.KvpTyped
- Side: both.
- Parameters:
prefix, prepended to every id;schema, used for every read and write. - Returns: a
Fx.KvpTypedstore withprefix,schemaand theKvpTyped:methods below. Not an effect. - Fails with: never.
local Vehicle = S.struct { model = S.string, fuel = S.number:between(0, 100) }
local Garage = Fx.kvp.typed('garage:', Vehicle)
KvpTyped:get(id)
KvpTyped:get(id: string) -> Fx.Effect<any, KvpDecodeError>
- Side: both.
- Returns: the value at
prefix .. id, ornilwhen unset. - Fails with:
KvpDecodeError.
local car = Fx.yield(Garage:get('ABC123'))
KvpTyped:set(id, value)
KvpTyped:set(id: string, value: any) -> Fx.Effect<nil, KvpInvalid>
- Side: both.
- Returns:
nil. - Fails with:
KvpInvalid(same rules asFx.kvp.set).
Fx.yield(Garage:set('ABC123', { model = 'sultan', fuel = 64 }))
KvpTyped:delete(id)
KvpTyped:delete(id: string) -> Fx.Effect<nil, never>
- Side: both.
- Returns:
nil. - Fails with: never.
Fx.yield(Garage:delete('ABC123'))
KvpTyped:ids()
KvpTyped:ids() -> Fx.Effect<string[], never>
- Side: both.
- Returns: every stored id under the prefix, without the prefix, sorted.
- Fails with: never.
local plates = Fx.yield(Garage:ids()) -- { 'ABC123', 'XYZ789' }
KvpTyped:all()
KvpTyped:all() -> Fx.Effect<table<string, any>, KvpDecodeError>
- Side: both.
- Returns:
{ [id] = value }for every id. - Fails with:
KvpDecodeErroron the first entry that does not decode. One bad entry fails the whole call.
for plate, car in pairs(Fx.yield(Garage:all())) do
print(plate, car.model, car.fuel)
end
State bags
Fx.state puts a schema on a state bag key. Values are checked when you set them and when a change arrives, so a handler never sees a value that fails the schema. On the server, an invalid value a client wrote to its own player or entity bag is dropped and reported as an invalid_state security event (Net and RPC).
Error classes on Fx.state:
StateInvalid { key, bag, message }: a value failed the schema ongetorset.StateTimeout { key, bag, timeoutMs }:awaitran out of time.
A bag is a table { name, state } from one of the bag functions. Bag names: global, player:<serverId>, entity:<netId>, localEntity:<handle>.
A change record passed to handlers:
---@class Fx.StateChange
---@field bag string bag name, e.g. "player:3"
---@field value any the new value, nil when cleared
---@field replicated boolean on the server: true when a client wrote it
---@field player? integer for player bags
---@field entity? integer for entity bags
Fx.state.key(name, schema)
Fx.state.key(name: string, schema: Fx.Schema) -> Fx.StateKey
-- name: the state bag key
- Side: both.
- Returns: a
Fx.StateKeyobject withname,schemaand theStateKey:methods below. Not an effect. - Fails with: never.
- Notes: define the same key with the same schema on both sides, ideally in a shared script.
-- shared.lua
Job = Fx.state.key('job', S.struct { name = S.string, grade = S.int })
Fx.state.global()
Fx.state.global() -> Fx.StateBag
- Side: both.
- Returns: the
GlobalStatebag, namedglobal. Not an effect.
local Weather = Fx.state.key('weather', S.literal('CLEAR', 'RAIN', 'THUNDER'))
Fx.yield(Weather:set(Fx.state.global(), 'RAIN'))
Fx.state.player(src)
Fx.state.player(src?: integer) -> Fx.StateBag
-- src: server id; on the client omit it for the local player
- Side: both.
- Returns: the player's bag (
Player(src).state), namedplayer:<src>. On the client with nosrc,LocalPlayer.statewith the local server id. - Notes: on the server
srcis required.
Fx.yield(Job:set(Fx.state.player(src), { name = 'police', grade = 2 }))
Fx.state.entity(ent)
Fx.state.entity(ent: integer) -> Fx.StateBag
-- ent: entity handle
- Side: both.
- Returns:
Entity(ent).state, namedentity:<netId>on the server or for networked entities on the client,localEntity:<ent>for client-only entities.
local Fuel = Fx.state.key('fuel', S.number:between(0, 100))
Fx.yield(Fuel:set(Fx.state.entity(veh), 75))
StateKey:get(bag)
StateKey:get(bag: Fx.StateBag) -> Fx.Effect<any, StateInvalid>
- Side: both.
- Returns: the current value as the schema outputs it, or
nilwhen unset. - Fails with:
StateInvalidwhen the bag holds a value that fails the schema.
local job = Fx.yield(Job:get(Fx.state.player())) -- client, local player
StateKey:set(bag, value, opts)
StateKey:set(bag: Fx.StateBag, value: any, opts?: { replicated?: boolean }) -> Fx.Effect<nil, StateInvalid>
- Side: both.
- Parameters:
value, checked against the schema (nilis always allowed and clears it);opts.replicated, defaults totrueon the server andfalseon the client. - Returns:
nil. - Fails with:
StateInvalidwhenvaluefails the schema; nothing is written. - Notes: a client may only replicate to bags it owns (its own player bag, entities it controls).
local Bad = Fx.yield(Job:set(Fx.state.player(src), { name = 'police' }):exit()) -- StateInvalid: grade missing
StateKey:clear(bag, opts)
StateKey:clear(bag: Fx.StateBag, opts?: { replicated?: boolean }) -> Fx.Effect<nil, never>
- Side: both.
- Returns:
nil. Same asStateKey:set(bag, nil, opts). - Fails with: never in practice (
nilpasses the check).
Fx.yield(Job:clear(Fx.state.player(src)))
StateKey:onChange(handler, opts)
StateKey:onChange(handler: fun(change: Fx.StateChange): Fx.Effect<any, any>|nil, opts?: { bag?: Fx.StateBag|string }) -> integer
-- returns: cookie for RemoveStateBagChangeHandler
- Side: both.
- Parameters:
handler, called for every valid change of the key;opts.bag, a bag or a bag name to listen to one bag only. - Returns: the cookie from
AddStateBagChangeHandler. Not an effect; the handler is registered right away. - Fails with: never. A handler that raises is reported as a defect.
- Notes:
- A cleared value (
nil) is a valid change;change.valueisnil. - If
handlerreturns an effect, it runs on its own fiber. - An invalid value never reaches the handler. On the server, if a client wrote it (
replicatedis true), it is reported withFx.security.report { kind = 'invalid_state', source, name, message, bag }.sourceis the player for player bags and the entity owner for entity bags. On the client invalid values are dropped silently. - Handlers also end when the resource stops.
- A cleared value (
-- client
Job:onChange(function(change)
Hud.setJob(change.value and change.value.name or 'unemployed')
end, { bag = Fx.state.player() })
StateKey:watch(handler, opts)
StateKey:watch(handler: fun(change: Fx.StateChange): Fx.Effect<any, any>|nil, opts?: { bag?: Fx.StateBag|string }) -> Fx.Effect<nil, never>
-- requires Scope
- Side: both.
- Returns:
nil. The handler is registered when the effect runs and removed when the scope closes. - Fails with: never.
Fx.runFork(Fx.player.extend(src, Job:watch(function(c)
print(('player %s is now %s'):format(c.player, c.value and c.value.name))
end, { bag = Fx.state.player(src) })))
StateKey:await(bag, predicate, opts)
StateKey:await(bag: Fx.StateBag, predicate?: fun(value: any): boolean, opts?: { timeout?: integer }) -> Fx.Effect<any, StateTimeout>
-- predicate: default: value ~= nil
- Side: both.
- Returns: the first valid value (current or future) for which
predicateis true. - Fails with:
StateTimeoutwhenopts.timeoutis set and runs out. Without a timeout it waits forever. - Notes: checks the current value first. If it is invalid or does not match, it waits for changes on that bag. Interrupting it removes the handler.
local Ready = Fx.state.key('ready', S.boolean)
Fx.yield(Ready:await(Fx.state.global(), nil, { timeout = 10000 }))
StateKey:changes(opts)
StateKey:changes(opts?: { bag?: Fx.StateBag|string, buffer?: integer, strategy?: 'sliding'|'dropping' }) -> Fx.Stream<Fx.StateChange>
- Side: both.
- Parameters:
opts.bagas inonChange;opts.buffer(default 1024) andopts.strategy(defaultsliding, drops the oldest;droppingkeeps the oldest) as inFx.Stream.async. - Returns: a stream of valid change records. The handler is registered while the stream runs and removed when it ends.
- Fails with: never.
-- client: react to job changes, but not to every flicker
Fx.runFork(Job:changes({ bag = Fx.state.player() })
:debounce(250)
:runForEach(function(change) return Hud.setJob(change.value) end))
Database
Fx.db runs oxmysql queries as effects. Every failure is a SqlError, and a row that fails its schema is a SqlDecodeError, so you can catch, retry or map them. Queries go to oxmysql unless an Fx.db.Database service is provided.
Error classes on Fx.db:
SqlError { method, sql, message }: the driver reported an error. Fortransaction,sqlis absent.messageis the database's line: FiveM's "An error occurred while calling export" wrapping and oxmysql's query dump are stripped, as are^1-style colour codes.SqlDecodeError { sql, index, message }: rowindex(1-based) failedopts.row, or the scalar failedopts.value(indexis 1).
Fx.db.query(sql, params, opts)
Fx.db.query(sql: string, params?: table, opts?: { row?: Fx.Schema }) -> Fx.Effect<table[], SqlError|SqlDecodeError>
- Side: server.
- Parameters:
params, positional (?) or named, as oxmysql takes them;opts.row, checked against every row. - Returns: the rows (an empty table when there are none), as the schema outputs them.
- Fails with:
SqlError;SqlDecodeErroron the first row that failsopts.row.
local Row = S.struct { id = S.int, name = S.string, money = S.int }
local rich = Fx.yield(Fx.db.query('SELECT id, name, money FROM players WHERE money > ?', { 100000 }, { row = Row })
:retry(Fx.Schedule.exponential(100):take(3)))
Fx.db.single(sql, params, opts)
Fx.db.single(sql: string, params?: table, opts?: { row?: Fx.Schema }) -> Fx.Effect<table|nil, SqlError|SqlDecodeError>
- Side: server.
- Returns: the first row, or
nilwhen there is none (not an error). - Fails with:
SqlError;SqlDecodeErrorwhen the row failsopts.row.
local NotYours = Fx.TaggedError('NotYours')
local owner = Fx.yield(Fx.db.single('SELECT owner, garage FROM owned_vehicles WHERE plate = ?', { plate },
{ row = S.struct { owner = S.string, garage = S.string } }))
if not owner then Fx.yield(NotYours { plate = plate }:fail()) end
Fx.db.scalar(sql, params, opts)
Fx.db.scalar(sql: string, params?: table, opts?: { value?: Fx.Schema }) -> Fx.Effect<any, SqlError|SqlDecodeError>
- Side: server.
- Parameters:
opts.value, checked against the value (note:value, notrow). - Returns: the first column of the first row, or
nil. - Fails with:
SqlError;SqlDecodeError { index = 1 }when the value failsopts.value.
local count = Fx.yield(Fx.db.scalar('SELECT COUNT(*) FROM owned_vehicles WHERE owner = ?', { cid }, { value = S.int }))
Fx.db.insert(sql, params)
Fx.db.insert(sql: string, params?: table) -> Fx.Effect<integer, SqlError>
- Side: server.
- Returns: the new row's id.
- Fails with:
SqlError.
local id = Fx.yield(Fx.db.insert('INSERT INTO bank_logs (cid, amount, reason) VALUES (?, ?, ?)', { cid, 500, 'garage fee' }))
Fx.db.update(sql, params)
Fx.db.update(sql: string, params?: table) -> Fx.Effect<integer, SqlError>
- Side: server.
- Returns: the number of affected rows.
- Fails with:
SqlError.
local n = Fx.yield(Fx.db.update('UPDATE owned_vehicles SET garage = ? WHERE plate = ?', { 'pillbox', plate }))
Fx.db.prepare(sql, params)
Fx.db.prepare(sql: string, params?: table) -> Fx.Effect<any, SqlError>
- Side: server.
- Parameters:
params, one parameter set or a list of sets, as oxmysql'spreparetakes them. - Returns: what oxmysql's
preparereturns. - Fails with:
SqlError.
Fx.yield(Fx.db.prepare('UPDATE players SET last_seen = NOW() WHERE cid = ?', { { 'ABC123' }, { 'XYZ789' } }))
Fx.db.transaction(queries, params)
Fx.db.transaction(queries: table, params?: table) -> Fx.Effect<true, SqlError>
-- queries: list of { query = sql, values = params }, or sql strings with shared params
-- params: shared params
- Side: server.
- Returns:
truewhen committed. - Fails with:
SqlError { method = 'transaction' }when the driver reports an error, or with messagetransaction rolled backwhen it answersfalseornil.
Fx.yield(Fx.db.transaction({
{ query = 'UPDATE players SET money = money - ? WHERE cid = ?', values = { price, cid } },
{ query = 'INSERT INTO owned_vehicles (owner, plate, model) VALUES (?, ?, ?)', values = { cid, plate, model } },
}))
Fx.db.Database
---@type Fx.Tag key 'rivo_fx/Database'
Fx.db.Database
---@alias Fx.DbDone fun(result: any, err?: any)
---@class Fx.DbDriver
---@field query fun(sql: string, params: table|nil, done: Fx.DbDone)
---@field single fun(sql: string, params: table|nil, done: Fx.DbDone)
---@field scalar fun(sql: string, params: table|nil, done: Fx.DbDone)
---@field insert fun(sql: string, params: table|nil, done: Fx.DbDone)
---@field update fun(sql: string, params: table|nil, done: Fx.DbDone)
---@field prepare fun(sql: string, params: table|nil, done: Fx.DbDone)
---@field transaction fun(queries: table, params: table|nil, done: Fx.DbDone)
- Side: server.
- Notes:
- The service tag for the driver. Every
Fx.dbcall looks it up withFx.serviceOption; when it is not provided, the live driverFx.db.oxmysqlis used. - A provided driver replaces oxmysql entirely: a method it does not implement raises when called (a defect, not a
SqlError). - Each method calls
done(result)on success ordone(nil, err)on failure. Anyerrother thannilandfalsebecomes aSqlError; a tableerruseserr.messageorerr.error. paramsis passed as given (it may benil); the oxmysql driver turnsnilinto{}.Fx.db.oxmysqlis the live driver table andFx.db.layerisFx.Layer.succeed(Fx.db.Database, Fx.db.oxmysql), for providing it explicitly.
- The service tag for the driver. Every
-- a fake database for tests
local Fake = Fx.Layer.succeed(Fx.db.Database, {
query = function(sql, params, done) done({ { id = 1, name = 'Vex', money = 500 } }) end,
scalar = function(sql, params, done) done(nil, 'connection lost') end,
})
local rows = Fx.yield(Fx.db.query('SELECT id, name, money FROM players'):provide(Fake))
Framework
Fx.framework gives one player, money and job API over ESX, QBCore, Qbox and standalone. You get it as the Framework service from a layer and read it with Fx.yield(Fx.framework.Framework) (Services and layers).
Error classes on Fx.framework:
PlayerNotFound { source }: no player (or no character loaded) for that id.InsufficientFunds { source, account, need, have }:removeMoneywould go negative, or the framework refused the removal.FrameworkError { message }: an unknown account, a bad amount, a job or grade that does not exist, the framework refusing to add money, or the framework's lookup raising.
Shapes:
---@class Fx.FrameworkJob
---@field name string
---@field label? string
---@field grade? integer
---@field gradeLabel? string
---@class Fx.FrameworkPlayer
---@field source integer
---@field identifier string ESX: identifier, QBCore/Qbox: citizenid, standalone: license identifier
---@field name string ESX: getName(), QBCore/Qbox: charinfo first + last name (else the player name), standalone: player name
---@field job Fx.FrameworkJob standalone: { name = 'unemployed', label = 'Unemployed', grade = 0, gradeLabel = 'Unemployed' }
---@field money { cash: number, bank: number } ESX 'money' account is cash
---@alias Fx.MoneyAccount 'cash'|'bank'
The player table is a snapshot taken when you ask; read money again with getMoney.
Fx.framework.auto
---@type Fx.Layer provides Fx.framework.Framework
Fx.framework.auto
- Side: server.
- Notes: picks the framework with
Fx.framework.detect()when the layer is built. The service for each framework is built once per resource and shared by every layer that provides it (building it registers event handlers).
Fx.setRuntime(Fx.Runtime.make(Fx.framework.auto))
Fx.rpc.register('garage:pay', { input = S.struct {} }, function(src)
return Fx.gen(function()
local fw = Fx.yield(Fx.framework.Framework)
return Fx.yield(fw.removeMoney(src, 'bank', 500, 'garage fee'))
end)
end)
Fx.framework.esx, qbcore, qbox, standalone
---@type Fx.Layer
Fx.framework.esx -- exports.es_extended:getSharedObject()
Fx.framework.qbcore -- exports['qb-core']:GetCoreObject()
Fx.framework.qbox -- exports.qbx_core
Fx.framework.standalone -- no framework
- Side: server.
- Notes:
- Use one to force a framework instead of detecting it.
- Standalone has no characters: every connected player is a player, money lives in memory per session (starts at 0 cash and 0 bank, gone on drop), the job is always
unemployed,setJobalways fails withFrameworkError, and there are no job change events.
Fx.setRuntime(Fx.Runtime.make(Fx.framework.qbox))
Fx.framework.detect()
Fx.framework.detect() -> 'qbox'|'qbcore'|'esx'|'standalone'
- Side: server.
- Returns: the first started resource among
qbx_core(qbox),qb-core(qbcore),es_extended(esx); elsestandalone. Not an effect. - Notes: Qbox is checked before QBCore.
print('framework: ' .. Fx.framework.detect())
Fx.framework.Framework
---@type Fx.Tag key 'rivo_fx/Framework'
Fx.framework.Framework
- Side: server.
- Returns: as an effect (
Fx.yield(Fx.framework.Framework)), the service withname('esx'|'qbcore'|'qbox'|'standalone') and the methods below. - Fails with: dies with
Service not found: rivo_fx/Frameworkwhen no framework layer is provided.
local fw = Fx.yield(Fx.framework.Framework)
print(fw.name)
fw.getPlayer(src)
fw.getPlayer(src: integer) -> Fx.Effect<Fx.FrameworkPlayer, PlayerNotFound|FrameworkError>
- Side: server.
- Returns: the normalized player.
- Fails with:
PlayerNotFoundwhen there is no player or character;FrameworkErrorwhen the framework's lookup raises.
local p = Fx.yield(fw.getPlayer(src))
print(p.name, p.job.name, p.money.bank)
fw.findPlayer(src)
fw.findPlayer(src: integer) -> Fx.Effect<Fx.FrameworkPlayer|nil, FrameworkError>
- Side: server.
- Returns: the player, or
nilwhen they have no character loaded. - Fails with:
FrameworkError.
local p = Fx.yield(fw.findPlayer(src))
if not p then return Fx.succeed(nil) end
fw.getMoney(src, account)
fw.getMoney(src: integer, account: 'cash'|'bank') -> Fx.Effect<number, PlayerNotFound|FrameworkError>
- Side: server.
- Returns: the live balance (read from the framework each time, not from a copy).
- Fails with:
FrameworkErrorfor any account other thancashorbank;PlayerNotFound.
local cash = Fx.yield(fw.getMoney(src, 'cash'))
fw.addMoney(src, account, amount, reason)
fw.addMoney(src: integer, account: 'cash'|'bank', amount: number, reason?: string) -> Fx.Effect<number, PlayerNotFound|FrameworkError>
-- reason: default: the resource name
- Side: server.
- Parameters:
amountmust be a number, at least 0, not NaN and not infinite. - Returns: the new balance.
- Fails with:
FrameworkErrorfor a bad account or amount, orthe framework refused to add moneywhen the framework's add returnsfalse;PlayerNotFound. - Notes: ESX's
addAccountMoneyreturns nothing, so a refusal is not detected on ESX.
local bank = Fx.yield(fw.addMoney(src, 'bank', 2500, 'paycheck'))
fw.removeMoney(src, account, amount, reason)
fw.removeMoney(src: integer, account: 'cash'|'bank', amount: number, reason?: string) -> Fx.Effect<number, InsufficientFunds|PlayerNotFound|FrameworkError>
-- reason: default: the resource name
- Side: server.
- Returns: the new balance.
- Fails with:
InsufficientFunds { source, account, need, have }when the balance is belowamount(nothing is removed) or when the framework's remove returnsfalse;FrameworkErrorfor a bad account or amount;PlayerNotFound.
Fx.yield(fw.removeMoney(src, 'bank', price, 'vehicle purchase')
:catchTag('InsufficientFunds', function(e)
return Fx.net.emit('shop:notify', src, { text = ('You need $%d more'):format(e.need - e.have) })
end))
fw.setJob(src, job, grade)
fw.setJob(src: integer, job: string, grade?: integer) -> Fx.Effect<Fx.FrameworkJob, PlayerNotFound|FrameworkError>
-- grade: default 0
- Side: server.
- Returns: the player's job after the change.
- Fails with:
FrameworkError(job <job> grade <grade> does not exist in <framework>) when the job or grade does not exist, and always on standalone;PlayerNotFound. - Notes: ESX checks
ESX.DoesJobExist(job, tostring(grade))first. QBCore callsFunctions.SetJob(job, tostring(grade)); Qbox callsexports.qbx_core:SetJob(src, job, grade).
local job = Fx.yield(fw.setJob(src, 'police', 1))
print(job.label, job.gradeLabel)
fw.onPlayerLoaded(fn)
fw.onPlayerLoaded(fn: fun(src: integer): Fx.Effect<any, any>|nil) -> fun()
-- returns: removes the handler
- Side: server.
- Returns: a function that removes the handler. Not an effect.
- Notes:
- Fires when a character is loaded:
esx:playerLoaded,QBCore:Server:PlayerLoaded(QBCore and Qbox),playerJoining(standalone). - Players who already had a character when the service was built do not get a loaded event, but are tracked so their unload still fires.
- If
fnreturns an effect, it runs on its own fiber. A handler that raises is reported as a defect.
- Fires when a character is loaded:
fw.onPlayerLoaded(function(src)
return fw.getPlayer(src):flatMap(function(p)
return Fx.logInfo('character loaded', { player = src, cid = p.identifier })
end)
end)
fw.onPlayerUnloaded(fn)
fw.onPlayerUnloaded(fn: fun(src: integer): Fx.Effect<any, any>|nil) -> fun()
-- returns: removes the handler
- Side: server.
- Returns: a function that removes the handler.
- Notes: fires exactly once per loaded character, on logout (
esx:playerDropped,QBCore:Server:OnPlayerUnload) or onplayerDropped, whichever comes first and whatever order resources handle the drop in.
fw.onPlayerUnloaded(function(src) activeDuty[src] = nil end)
fw.onJobChange(fn)
fw.onJobChange(fn: fun(src: integer, job: Fx.FrameworkJob): Fx.Effect<any, any>|nil) -> fun()
-- returns: removes the handler
- Side: server.
- Returns: a function that removes the handler.
- Notes: fires on
esx:setJoborQBCore:Server:OnJobUpdate(QBCore and Qbox) with the normalized job. Never fires on standalone.
fw.onJobChange(function(src, job)
return Job:set(Fx.state.player(src), { name = job.name, grade = job.grade or 0 })
end)