AI Core

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:

  1. A real, connected player sent it (not another resource emitting the same name).
  2. The rate limit (callbacks 20 calls per second per player, events 10, unless you set rate).
  3. Your guards (hasCharacter(), near(...), isAdmin() or your own).
  4. Your args parser, which turns the raw values into checked ones or refuses the call.
  5. 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).

TypeScript
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)

Signature
onClientCallback(name: string, handler: (source: number) => unknown, options?: ClientInputOptions): void
onClientCallback<A>(name: string, options: ParsedInputOptions<A>, handler: (source: number, ...args: A) => unknown): void
Lua
aiLib.onClientCallback(name, handler, options) -- handler(source, ...): any

Parameters

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 triggerServerCallback promise rejects at once (callAction shows its fallback text); in Lua aiLib.triggerServerCallback returns nil, '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

Signature
onClientEvent(name: string, handler: (source: number) => unknown, options?: ClientInputOptions): void
onClientEvent<A>(name: string, options: ParsedInputOptions<A>, handler: (source: number, ...args: A) => unknown): void
Lua
aiLib.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
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

Signature
isAdmin(ace?: string): Guard
Lua
local 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

Signature
hasCharacter(): Guard

No 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

Signature
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): Guard

No Lua twin: compare #(GetEntityCoords(GetPlayerPed(source)) - point) yourself.

Parameters

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

Signature
eventSource(): number

No 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)

Signature
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>
Lua
local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- call inside a thread

Parameters

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)

Signature
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- boolean, call inside a thread

Parameters

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)

Signature
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', call inside a thread

Parameters

Prop

Type

MinigameForOptions

Prop

Type

Returns

Promise<MinigameOutcome> where MinigameOutcome = "won" | "lost" | "broken":

  • "won": the player won (and not faster than minMs).
  • "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.

Signature
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.

Signature
start(source: number, data: T, durationMs: number): number

Prop

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.

Signature
take(source: number, token: number): T | null

Returns 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

Signature
peek(source: number): T | null

Returns the running action's data without ending it, or null.

PendingActions.cancel

Signature
cancel(source: number): void

Ends the player's action without a result.

PendingActions.size

Signature
readonly size: number

How 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.

Signature
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): void

Prop

Type

  • check returns the checked arguments (an empty array without args) or null when the call must not run (not a real player, rate, guard, parser).
  • run runs task under the per-player lock when lock is on, and returns busy instead when another call of that player still runs.
  • release frees the player's rate and lock entries. Every gate is released on playerDropped by itself.

On this page