Requests that check what a player sends, run one at a time, and know where the player is.
On this page
A player's game can be modified, so anything it sends the server can be made up: a price, a position, a hundred requests at once. tr_core gives every request the same checks, so the server only acts on what it has checked itself.
Requests from players
Every request from a player's game goes through TR.callback.register (or TR.net.on for one that needs no answer), with a schema for what it may send and a rateLimit for how often.
server/main.lua
TR.callback.register('tr_bank:withdraw', function(src, amount) if not TR.guard.isNear(src, Config.bank, 3.0) then return false, 'too_far' end if not TR.player.removeMoney(src, 'bank', amount) then return false, 'no_money' end TR.player.addMoney(src, 'cash', amount) return trueend, { schema = { { type = 'integer', min = 1, max = 100000 } }, rateLimit = { max = 2, window = 1000 },})
client/main.lua
local ok, why = TR.callback.await('tr_bank:withdraw', 500)if not ok then TR.notify.send({ description = TR.locale.t(why), type = 'error' })end
A request that doesn't match its schema never reaches your function: the player gets nil, 'invalid_args' and is flagged (see Flags). One over the rate limit gets nil, 'rate_limited'.
The schema has one entry per argument. Types: any, nil, boolean, number, integer, string, table, array, vector3 and vector4, with min / max, pattern, enum and optional. A short form works too: 'string?' is an optional string.
Numbers and vector parts must be real numbers: NaN and infinity are refused.
A string without its own max takes at most 4096 characters. Give a bigger max where a player may send more.
One at a time
A function that waits for something (the database, or the player's game) can be called again by the same player before the first call is done. Both calls pass the checks before either has saved anything: two purchases, one payment.
Give such a request exclusive = true. While it runs for a player, their next call gets nil, 'in_progress'.
server/main.lua
TR.callback.register('tr_houses:buy', function(src, id) local sql = 'SELECT owner FROM houses WHERE id = ?' if TR.db.scalar(sql, { id }) then return false, 'taken' end if not TR.player.removeMoney(src, 'bank', Config.price) then return false, 'no_money' end TR.db.update('UPDATE houses SET owner = ? WHERE id = ?', { TR.player.getIdentifier(src), id, }) return trueend, { schema = { { type = 'integer', min = 1 } }, rateLimit = { max = 1, window = 2000 }, exclusive = true,})
Add an in_progress line to your locales, for the message the player sees.
It costs nothing on a function that doesn't wait, so when in doubt, add it. When the same work can be started another way too (from a laptop app, say), take the same lock there: it is named after the callback.
server/apps.lua
local ok, why = TR.guard.exclusive(src, 'tr_houses:buy', buy, src, id)
TR.guard.lock(src, name) is the same lock by hand: it returns a function that lets go of it, or nil while the player holds it.
Asking a player's game
The server can ask a player's game for something only it knows, with TR.callback.await(name, src, ...). The answer comes from that game, so check it like a request: awaitChecked takes a schema for the answer.
server/main.lua
local fuel, why = TR.callback.awaitChecked('tr_example:fuel', src, { schema = { 'number' },}, netId)-- why: 'invalid_answer', 'timeout' or 'offline'if not fuel then return end
An answer that doesn't match gives nil, 'invalid_answer' and flags the player (flag = false turns that off). A player who leaves while the server waits gives nil, 'offline' at once; timeout (ms) sets how long to wait.
Where the player is
TR.guard.isNear(src, coords, distance) checks the player's real position on the server, not one their game sent.
Instanced places (a warehouse interior, an apartment) put each copy in its own routing bucket, at the same coordinates. Coordinates don't know about buckets, so give the place's bucket, and a player in another copy isn't near it:
server/main.lua
if not TR.guard.isNear(src, interior.center, 20.0, interior.bucket) then return false, 'not_inside'end
Near an entity (a car, a prop), use TR.guard.isNearEntity(src, entity, distance). It checks the entity's bucket itself.
Flags
When a player's game sends something it never should, flag them:
server/main.lua
TR.guard.flag(src, 'sold more than they had', { item = item, count = count,})
Every flag fires the tr:playerFlagged event, for an anticheat or ban script to act on:
The console and the security Discord channel get a player's first flag in a minute, then one summary of the rest, so a player sending bad data on purpose can't flood them.