Callbacks let one side ask the other a question and wait for the answer. The client asks the server for a trader's stock, or the server asks a client what it can see. encore.callback handles the request ids, routing and timeouts, so a player who disconnects mid-request can never hang your code.
API
encore.callback is a table you can also call directly as a function for the async form.
Server
| Function | Returns | Notes |
|---|---|---|
encore.callback.register(name, handler) | none | handler(source, ...) returns the answer |
encore.callback.registerAsync(name, handler) | none | handler(source, respond, ...) answers later by calling respond(...) |
encore.callback.await(name, playerId, ...) | the client's return values | Asks one client. Yields until it answers or times out |
encore.callback(name, playerId, cb, ...) | none | Asks one client without yielding. cb(...) receives the answer |
Client
| Function | Returns | Notes |
|---|---|---|
encore.callback.register(name, handler) | none | handler(...) returns the answer |
encore.callback.registerAsync(name, handler) | none | handler(respond, ...) answers later by calling respond(...) |
encore.callback.await(name, ...) | the server's return values | Yields until the server answers or times out |
encore.callback(name, cb, ...) | none | Doesn't yield. cb(...) receives the answer |
Settings
| Field | Default | Meaning |
|---|---|---|
encore.callback.timeout | 15000 | Milliseconds before a request gives up. Set it in your own resource; it only affects that resource |
Return values
- Handlers can return several values, and all of them arrive in order.
nilvalues in the middle are kept, soreturn nil, 'Not enough scrap'arrives asnil, 'Not enough scrap'.- Values cross the network, so return plain data: strings, numbers, booleans and tables of those. Entity handles differ between machines; send network ids instead.
Timeouts and errors
- A timed-out
awaitreturns nothing, so every result isnil. - A timed-out async call runs
cbwith no arguments. - If a handler throws, the error is logged as
Callback "<name>" errored: ...and the caller receives nothing straight away, without waiting for the timeout. - A
registerAsynchandler that never callsrespondis answered with nothing when the timeout passes. Only the firstrespondcounts.
Always handle the nil case:
local stock = encore.callback.await('my-shop:getStock', shopId)
if not stock then
return encore.notify({ description = 'The trader is not answering.', type = 'error' })
endNaming
Every resource shares one callback namespace. Prefix names with your resource: 'my-shop:getStock', 'en-zombies:findSpawnPoints'.
Security
A server callback runs for any client that calls it, with any arguments that client chooses. source is the only value you can trust. Validate every argument before you use it.
Check:
- Types:
type(shopId) == 'string',math.type(amount) == 'integer'. - Ranges: amounts above zero and below a sensible maximum.
- Existence: the shop, item or vehicle actually exists.
- Permission: the player has the job, group, key or item needed.
- Distance: the player is standing where the action makes sense.
The library does protect the transport. When the server asks a client, it only accepts an answer from the player it asked, so one client can't inject an answer into another player's request. But the player you asked still controls what they send back. Treat the results of a server-side await as untrusted too.
Examples
Client asks the server
-- server/main.lua
encore.callback.register('my-shop:getStock', function(source, shopId)
if type(shopId) ~= 'string' then return nil end
local shop = Shops[shopId]
if not shop then return nil end
local ped = GetPlayerPed(source)
if #(GetEntityCoords(ped) - shop.coords) > 10.0 then return nil end
return shop.stock, os.time()
end)
-- client/main.lua
local stock, stamp = encore.callback.await('my-shop:getStock', 'north_camp')
if not stock then
return encore.notify({ description = 'Shop unavailable', type = 'error' })
endReturning a reason on failure
-- server
encore.callback.register('my-crafting:craft', function(source, recipeId)
local recipe = type(recipeId) == 'string' and Recipes[recipeId]
if not recipe then return false, 'Unknown recipe' end
for item, count in pairs(recipe.cost) do
if encore.inventory.getItemCount(source, item) < count then
return false, 'Missing materials'
end
end
if not encore.inventory.canCarry(source, recipe.result, 1) then
return false, 'No room in your pack'
end
for item, count in pairs(recipe.cost) do
encore.inventory.removeItem(source, item, count)
end
encore.inventory.addItem(source, recipe.result, 1)
return true
end)
-- client
local ok, reason = encore.callback.await('my-crafting:craft', 'makeshift_knife')
encore.notify({ description = ok and 'Crafted' or reason or 'No answer', type = ok and 'success' or 'error' })Server asks a client
-- client
encore.callback.register('my-base:getGroundZ', function(x, y)
local found, z = GetGroundZFor_3dCoord(x + 0.0, y + 0.0, 1000.0, false)
return found and z or nil
end)
-- server: the answer comes from the player, so sanity-check it
local z = encore.callback.await('my-base:getGroundZ', source, coords.x, coords.y)
if type(z) ~= 'number' or math.abs(z - coords.z) > 50.0 then
z = coords.z
endAnswering later with registerAsync
Use registerAsync when the answer arrives in another callback, which is how QBCore and ESX scripts are usually written.
-- server
encore.callback.registerAsync('my-stash:load', function(source, respond, stashId)
if type(stashId) ~= 'string' then return respond(nil) end
MySQL.query('SELECT items FROM my_stashes WHERE id = ?', { stashId }, function(rows)
respond(rows[1] and json.decode(rows[1].items) or {})
end)
end)Async form
-- client: don't block this thread
encore.callback('my-shop:getStock', function(stock)
if stock then renderStock(stock) end
end, 'north_camp')How it works
For reference only, you never need to trigger these yourself. Requests travel on the net event __enc_cb:req:<name>, and answers come back on __enc_cb:res:<resource>, carrying the asking resource and a numeric request id. Requests whose resource isn't a string or whose id isn't a number are ignored.