Client input
Answer clients safely: onClientCallback, onClientEvent, guards, triggerClientCallback, server-checked progress bars and minigames, PendingActions.
Everything a client sends can be faked by a cheater. Every ai-* server listens to clients only through the gate: onClientCallback and onClientEvent. Before your handler runs, the gate checks, in this order:
- A real, connected player sent it (not another resource emitting the same name).
- The rate limit (callbacks 20 calls per second per player, events 10, unless you set
rate). - Your guards (
hasCharacter(),near(...),isAdmin()or your own). - Your
argsparser, which turns the raw values into checked ones or refuses the call. - Optionally a lock: one call at a time per player.
A refusal with a reason is written to the server console with the player name and the input name, at most one line per player and input every 10 seconds (the lines in between are counted into the next one).
import { onClientCallback, onClientEvent, hasCharacter, near, isAdmin } from "@aicore-fivem/lib/server";
import { fail, oneArg, readId, readInt, readText } from "@aicore-fivem/lib/shared";The input readers (readInt, readId, readText, oneArg, asArgs) and fail are documented on Lib: shared and fivem. An action answers an ActionResult: { ok: true, ... } or fail("LANG_KEY").
Gate
onClientCallback
Answers a client's triggerServerCallback(name, ...) (or callAction) and sends the handler's return value back. Use it for every request where the client waits for an answer: buying, using, opening a screen. It has two forms: without arguments (name, handler, options?), and with arguments (name, { args, ... }, handler), where the options come first so your handler only ever sees checked values.
Side: Server · Waits: no (the handler may be async; its promise is awaited before the answer is sent)
onClientCallback(name: string, handler: (source: number) => unknown, options?: ClientInputOptions): void
onClientCallback<A>(name: string, options: ParsedInputOptions<A>, handler: (source: number, ...args: A) => unknown): voidaiLib.onClientCallback(name, handler, options) -- handler(source, ...): anyParameters
Prop
Type
ClientInputOptions
Prop
Type
ParsedInputOptions
All fields of ClientInputOptions, plus:
Prop
Type
Returns
void. The answer the client gets:
- the handler's return value when it ran;
- a failure when the gate refused the call, the lock was busy or the handler threw. In TypeScript the client's
triggerServerCallbackpromise rejects at once (callActionshows itsfallbacktext); in LuaaiLib.triggerServerCallbackreturnsnil, 'failed'.
Example
import { onClientCallback, hasCharacter, near, inventory } from "@aicore-fivem/lib/server";
import { fail, oneArg, readInt } from "@aicore-fivem/lib/shared";
const CRATE = { x: 120.5, y: -1050.2, z: 29.2 };
// Client: callAction("my-crates:take", [2], { fallback: "TAKE_FAILED" })
onClientCallback(
"my-crates:take",
{
args: oneArg((v) => readInt(v, 1, 5)),
guards: [hasCharacter(), near(CRATE, 3)],
lock: true,
},
(source, count) => {
if (!inventory.canCarryItem(source, "water", count)) return fail("CANNOT_CARRY");
inventory.addItem(source, "water", count);
return { ok: true };
},
);The Lua version has no guards and no args: it takes options = { rate?, lock?, ace? } (ace: the player needs this ACE permission, ai.admin for ai-core admins) and the handler gets the raw values. Check every argument yourself (type, range, ownership, distance) before using it. In TypeScript, never read client values outside args.
onClientEvent
Listens to a net event a client sends, through the same gate. Use it for one-way messages where the client does not wait for an answer (closing a screen, a reported cell). Never use a bare onNet for client input. Same two forms as onClientCallback.
Side: Server · Waits: no
onClientEvent(name: string, handler: (source: number) => unknown, options?: ClientInputOptions): void
onClientEvent<A>(name: string, options: ParsedInputOptions<A>, handler: (source: number, ...args: A) => unknown): voidaiLib.onClientEvent(name, handler, options) -- handler(source, ...), options { rate?, ace? }Parameters
Prop
Type
Returns
void.
Example
import { onClientEvent, PlayerMap } from "@aicore-fivem/lib/server";
const openShops = new PlayerMap<string>();
// Client: emitNet("my-shop:close")
onClientEvent("my-shop:close", (source) => {
openShops.delete(source);
});The Lua version takes options = { rate?, ace? } (no lock, no guards, no args). A local TriggerEvent from another resource carries no player and is ignored on both sides.
Guards
A guard is a function that gets the player's server id and answers true (pass), false (refuse quietly) or a string (refuse and log the string as the reason).
type Guard = (source: number) => boolean | string;Write your own guard when a check repeats over several inputs:
import type { Guard } from "@aicore-fivem/lib/server";
import { online } from "@aicore-fivem/lib/server";
const onDutyPolice: Guard = (source) => online.onDutyIn(source, ["police"]) || "not on duty";isAdmin
A guard that passes when the player has an ACE permission: ai-core's admin permission ai.admin, or the one you name.
Side: Server · Waits: no
isAdmin(ace?: string): Guardlocal allowed = aiLib.isAdmin(source, ace) -- boolean, a direct check (not a guard)Parameters
Prop
Type
Returns
Guard: refuses with the reason not allowed (<ace>). The Lua twin returns true or false directly, and false for source 0 or below.
Example
import { onClientCallback, isAdmin, medical } from "@aicore-fivem/lib/server";
onClientCallback("my-admin:reviveSelf", (source) => ({ ok: medical.revive(source) }), {
guards: [isAdmin()],
});hasCharacter
A guard that passes when the player has a character in the world (not on the character screen).
Side: Server · Waits: no
hasCharacter(): GuardNo Lua twin: check aiLib.online.character(source) ~= nil.
Returns
Guard: refuses with the reason no character loaded.
Example
import { onClientCallback, hasCharacter, online } from "@aicore-fivem/lib/server";
onClientCallback("my-id:name", (source) => ({ ok: true, name: online.name(source) }), {
guards: [hasCharacter()],
});near
A guard that passes when the player's ped is within metres of a point, measured on the server (the client cannot lie about it).
Side: Server · Waits: no
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): GuardNo Lua twin: compare #(GetEntityCoords(GetPlayerPed(source)) - point) yourself.
Parameters
Prop
Type
Returns
Guard: refuses with the reason too far (<metres> m).
Example
import { onClientCallback, near, PlayerMap } from "@aicore-fivem/lib/server";
import type { Vec3 } from "@aicore-fivem/lib/shared";
const openAt = new PlayerMap<Vec3>();
onClientCallback("my-shop:list", (source) => ({ ok: true, items: [] }), {
guards: [near((source) => openAt.get(source) ?? null, 4)],
});eventSource
The server id of the player who sent the net event being handled. Inside onClientCallback and onClientEvent you get source as the first argument, so you only need this in a raw FiveM onNet or on handler.
Side: Server · Waits: no
eventSource(): numberNo Lua twin: Lua reads the global source.
Returns
number: the player's server id (FiveM's global source).
Example
import { eventSource } from "@aicore-fivem/lib/server";
on("my-script:serverOnly", () => {
const source = eventSource(); // read it first
console.log(`event from ${source}`);
});Read it before the first await. FiveM resets the global after the handler yields, so a later read gives the wrong player.
Asking the client
triggerClientCallback
Asks one player's client a question and waits for the answer. The client answers with onServerCallback(name, handler) (Lib: client). Only the player you asked can answer.
Side: Server · Waits: yes (Promise in TypeScript, yields the thread in Lua)
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- call inside a threadParameters
Prop
Type
Returns
Promise<unknown>: resolves with the client's answer. Rejects with an Error when the answer did not come in time or the client handler failed. The answer comes from a client: parse it before you use it.
Lua returns the answer, or nil and 'timeout' / 'failed'.
Example
import { addCommand, triggerClientCallback, notify } from "@aicore-fivem/lib/server";
import { readText } from "@aicore-fivem/lib/shared";
// Client: onServerCallback("my-admin:street", () => locationName().street)
addCommand(
"street",
{ help: "Shows the street a player stands on", restricted: true, params: [{ name: "id", type: "playerId" }] },
async (source, { id }) => {
const answer = await triggerClientCallback("my-admin:street", id, [], 5000).catch(() => null);
const street = readText(answer, 100);
if (source > 0) notify(source, street ?? "No answer", street ? "info" : "error");
},
);Timed actions the server trusts
progressFor
Runs ai-lib's progress bar on the player's screen and waits until it finishes. Use it for any timed action the server must trust (using an item, repairing, robbing): it only answers true when the bar ran out on the client and enough time passed on the server, so a cheater cannot skip it. You do not write any client code: ai-lib answers for every resource.
Side: Server · Waits: yes (Promise in TypeScript, yields the thread in Lua)
progressFor(source: number, options: ProgressForOptions): Promise<boolean>local finished = aiLib.progressFor(source, options) -- boolean, call inside a threadParameters
Prop
Type
ProgressForOptions
Prop
Type
Returns
Promise<boolean>: true when the bar finished. false when the player cancelled, died, got into a vehicle (without allowVehicle), left, switched character, answered too fast, or no answer came within duration plus 15 seconds.
Example
import { onClientCallback, progressFor, inventory, vehicleNearPlayer, createLocale } from "@aicore-fivem/lib/server";
import { fail, oneArg, readId } from "@aicore-fivem/lib/shared";
const Locale = createLocale();
onClientCallback(
"my-mechanic:repair",
{ args: oneArg(readId), lock: true },
async (source, netId) => {
if (!vehicleNearPlayer(source, netId, { metres: 4 })) return fail("TOO_FAR");
if (!inventory.hasItem(source, "repairkit")) return fail("NO_KIT");
const done = await progressFor(source, {
label: Locale("REPAIRING"),
duration: 8000,
anim: { dict: "mini@repair", clip: "fixing_a_player" },
canCancel: true,
});
if (!done) return fail("CANCELLED");
// The world may have changed while the bar ran: check again.
const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
if (!vehicle) return fail("TOO_FAR");
if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
// Vehicle health is set on the client that owns the entity.
emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
return { ok: true };
},
);A second progressFor for the same player ends the first one with false. Check distance and stock again after the await: the world may have changed while the bar ran. In Lua a drop or character switch is not detected early: the call answers false when the timeout (duration + 15 s) runs out.
minigameFor
Plays one of ai-lib's minigames (a skill check or a lockpick) on the player's screen for a server flow and waits for the result. Use it for lockpicking a door, cracking a safe, hot-wiring. The result is the client's word, so the server still decides what a win gives (a chance roll, a progressFor, its own checks).
Side: Server · Waits: yes (Promise in TypeScript, yields the thread in Lua)
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', call inside a threadParameters
Prop
Type
MinigameForOptions
Prop
Type
Returns
Promise<MinigameOutcome> where MinigameOutcome = "won" | "lost" | "broken":
"won": the player won (and not faster thanminMs)."lost": the player missed, gave up, won too fast, or did not answer within 120 seconds."broken": lockpick only, the last pick broke. Take the lockpick item.
Example
import { onClientCallback, minigameFor, inventory, hasCharacter, near, isNear } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";
const SAFE = { x: 28.2, y: -1339.2, z: 29.5 };
onClientCallback(
"my-heist:pickSafe",
async (source) => {
if (!inventory.hasItem(source, "lockpick")) return fail("NO_LOCKPICK");
const outcome = await minigameFor(source, { game: "lockpick", difficulty: "hard", pins: 3, picks: 2, minMs: 1500 });
if (outcome === "broken") inventory.removeItem(source, "lockpick");
if (outcome !== "won") return fail("LOCKPICK_FAILED");
if (!isNear(source, SAFE, 2)) return fail("TOO_FAR");
inventory.addItem(source, "money_bag");
return { ok: true };
},
{ guards: [hasCharacter(), near(SAFE, 2)], lock: true },
);Never write your own client minigame callback for a server flow: use minigameFor. Without minMs, a modified client can answer "won" at once. The 1500 above is an example: measure the fastest honest play of your settings.
PendingActions
Keeps one waiting action per player for flows where the client opens a dialog and later sends the result (writing a note, picking an option) without a timed bar. start hands out a token the client sends back; take ends the action only when that token matches. Entries are freed when the player drops or lets the character go (a switch never pays the old character's action to the new one). Create one at module level.
Side: Server · Waits: no
No Lua twin: keep a Lua table of tokens per player.
new PendingActions<T>(options?: { minRatio?: number })Constructor parameters
Prop
Type
PendingActions.start
Starts the player's action, replacing one that still runs, and returns its token.
start(source: number, data: T, durationMs: number): numberProp
Type
Returns number: the token, a whole number from 1 to 2147483647. Send it to the client; read it back with readId.
PendingActions.take
Ends the action and returns its data when token is the running one and enough time passed.
take(source: number, token: number): T | nullReturns T | null: the data, or null when no action runs, the token does not match, or the answer came too early. A wrong token leaves the running action alone (a stale or forged token never cancels it); a matching one ends it even when it was too early.
PendingActions.peek
peek(source: number): T | nullReturns the running action's data without ending it, or null.
PendingActions.cancel
cancel(source: number): voidEnds the player's action without a result.
PendingActions.size
readonly size: numberHow many actions run now.
Example
import { onClientCallback, PendingActions, inventory, hasCharacter } from "@aicore-fivem/lib/server";
import { fail, readId, readText } from "@aicore-fivem/lib/shared";
const notes = new PendingActions<{ item: string }>({ minRatio: 0 });
// Step 1: the client asks to write; the server answers a token.
onClientCallback(
"my-notes:start",
(source) => {
if (!inventory.hasItem(source, "paper")) return fail("NO_PAPER");
const token = notes.start(source, { item: "paper" }, 60_000);
return { ok: true, token };
},
{ guards: [hasCharacter()] },
);
// Step 2: the client sends the token and the text.
onClientCallback(
"my-notes:finish",
{
args: ([token, text]) => {
const id = readId(token);
const body = readText(text, 200);
return id !== null && body !== null ? [id, body] : null;
},
lock: true,
},
(source, token, text) => {
const action = notes.take(source, token);
if (!action) return fail("NOTE_EXPIRED");
if (!inventory.removeItem(source, action.item)) return fail("NO_PAPER");
inventory.addItem(source, "note", 1, { text });
return { ok: true };
},
);Advanced
ClientGate
The class behind onClientCallback and onClientEvent. You only need it when you build your own kind of input on top of a raw event; scripts use the two functions above.
Side: Server · Waits: no
No Lua twin.
new ClientGate<A>(name: string, options: ClientInputOptions & { args?: (raw: unknown[]) => A | null }, defaultRate: number)
check(source: number, raw: unknown[]): A | null
run<T, B>(source: number, busy: B, task: () => Promise<T> | T): Promise<T | B>
release(source: number): voidProp
Type
checkreturns the checked arguments (an empty array withoutargs) ornullwhen the call must not run (not a real player, rate, guard, parser).runrunstaskunder the per-player lock whenlockis on, and returnsbusyinstead when another call of that player still runs.releasefrees the player's rate and lock entries. Every gate is released onplayerDroppedby itself.