AI Core
Référence de l’APILib : serveur

Entrées client

Répondre aux clients en sécurité : onClientCallback, onClientEvent, gardes, triggerClientCallback, barres de progression et mini-jeux vérifiés par le serveur, PendingActions.

Tout ce qu’un client envoie peut être falsifié par un tricheur. Chaque serveur ai-* n’écoute les clients qu’à travers la porte (gate) : onClientCallback et onClientEvent. Avant que votre gestionnaire ne s’exécute, la porte vérifie, dans cet ordre :

  1. Qu’un vrai joueur connecté l’a envoyé (et non une autre ressource qui émet le même nom).
  2. La limite de fréquence (callbacks 20 appels par seconde et par joueur, événements 10, sauf si vous définissez rate).
  3. Vos gardes (hasCharacter(), near(...), isAdmin() ou les vôtres).
  4. Votre analyseur args, qui transforme les valeurs brutes en valeurs vérifiées ou refuse l’appel.
  5. Éventuellement un verrou : un seul appel à la fois par joueur.

Un refus accompagné d’une raison est écrit dans la console du serveur avec le nom du joueur et le nom de l’entrée, au plus une ligne par joueur et par entrée toutes les 10 secondes (les lignes intermédiaires sont comptées dans la suivante).

TypeScript
import { onClientCallback, onClientEvent, hasCharacter, near, isAdmin } from "@aicore-fivem/lib/server";
import { fail, oneArg, readId, readInt, readText } from "@aicore-fivem/lib/shared";

Les lecteurs d’entrées (readInt, readId, readText, oneArg, asArgs) et fail sont documentés dans Lib : shared et fivem. Une action répond par un ActionResult : { ok: true, ... } ou fail("LANG_KEY").

Porte

onClientCallback

Répond au triggerServerCallback(name, ...) (ou callAction) d’un client et renvoie la valeur de retour du gestionnaire. Utilisez-la pour chaque requête où le client attend une réponse : acheter, utiliser, ouvrir un écran. Elle a deux formes : sans arguments (name, handler, options?), et avec arguments (name, { args, ... }, handler), où les options viennent en premier pour que votre gestionnaire ne voie jamais que des valeurs vérifiées.

Côté : serveur · Attend : non (le gestionnaire peut être async ; sa promesse est attendue avant l’envoi de la réponse)

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

Paramètres

Prop

Type

ClientInputOptions

Prop

Type

ParsedInputOptions

Tous les champs de ClientInputOptions, plus :

Prop

Type

Retour

void. La réponse que reçoit le client :

  • la valeur de retour du gestionnaire quand il s’est exécuté ;
  • un échec quand la porte a refusé l’appel, que le verrou était occupé ou que le gestionnaire a levé une erreur. En TypeScript, la promesse du triggerServerCallback du client est rejetée aussitôt (callAction affiche son texte fallback) ; en Lua, aiLib.triggerServerCallback renvoie nil, 'failed'.

Exemple

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 };
  },
);

La version Lua n’a ni guards ni args : elle prend options = { rate?, lock?, ace? } (ace : le joueur a besoin de cette permission ACE, ai.admin pour les admins ai-core) et le gestionnaire reçoit les valeurs brutes. Vérifiez vous-même chaque argument (type, plage, propriété, distance) avant de l’utiliser. En TypeScript, ne lisez jamais de valeurs client en dehors de args.

onClientEvent

Écoute un événement réseau envoyé par un client, à travers la même porte. Utilisez-la pour les messages à sens unique où le client n’attend pas de réponse (fermer un écran, une cellule signalée). N’utilisez jamais un simple onNet pour une entrée client. Mêmes deux formes que onClientCallback.

Côté : serveur · Attend : non

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? }

Paramètres

Prop

Type

Retour

void.

Exemple

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);
});

La version Lua prend options = { rate?, ace? } (pas de lock, pas de guards, pas d’args). Un TriggerEvent local venant d’une autre ressource ne porte aucun joueur et est ignoré des deux côtés.

Gardes

Une garde est une fonction qui reçoit l’id serveur du joueur et répond true (passe), false (refus silencieux) ou une chaîne (refus, avec la chaîne journalisée comme raison).

Type
type Guard = (source: number) => boolean | string;

Écrivez votre propre garde quand un contrôle se répète sur plusieurs entrées :

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

Une garde qui passe quand le joueur a une permission ACE : la permission d’admin d’ai-core ai.admin, ou celle que vous indiquez.

Côté : serveur · Attend : non

Signature
isAdmin(ace?: string): Guard
Lua
local allowed = aiLib.isAdmin(source, ace) -- booléen, un contrôle direct (pas une garde)

Paramètres

Prop

Type

Retour

Guard : refuse avec la raison not allowed (<ace>). L’équivalent Lua renvoie directement true ou false, et false pour la source 0 ou en dessous.

Exemple

import { onClientCallback, isAdmin, medical } from "@aicore-fivem/lib/server";

onClientCallback("my-admin:reviveSelf", (source) => ({ ok: medical.revive(source) }), {
  guards: [isAdmin()],
});

hasCharacter

Une garde qui passe quand le joueur a un personnage dans le monde (pas sur l’écran de sélection de personnage).

Côté : serveur · Attend : non

Signature
hasCharacter(): Guard

Pas d’équivalent Lua : vérifiez aiLib.online.character(source) ~= nil.

Retour

Guard : refuse avec la raison no character loaded.

Exemple

import { onClientCallback, hasCharacter, online } from "@aicore-fivem/lib/server";

onClientCallback("my-id:name", (source) => ({ ok: true, name: online.name(source) }), {
  guards: [hasCharacter()],
});

near

Une garde qui passe quand le ped du joueur se trouve à moins de metres d’un point, mesuré sur le serveur (le client ne peut pas mentir à ce sujet).

Côté : serveur · Attend : non

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

Pas d’équivalent Lua : comparez vous-même #(GetEntityCoords(GetPlayerPed(source)) - point).

Paramètres

Retour

Guard : refuse avec la raison too far (<metres> m).

Exemple

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

L’id serveur du joueur qui a envoyé l’événement réseau en cours de traitement. Dans onClientCallback et onClientEvent, vous recevez source comme premier argument, vous n’en avez donc besoin que dans un gestionnaire FiveM brut onNet ou on.

Côté : serveur · Attend : non

Signature
eventSource(): number

Pas d’équivalent Lua : en Lua, on lit la globale source.

Retour

number : l’id serveur du joueur (la globale source de FiveM).

Exemple

import { eventSource } from "@aicore-fivem/lib/server";

on("my-script:serverOnly", () => {
  const source = eventSource(); // à lire en premier
  console.log(`event from ${source}`);
});

Lisez-la avant le premier await. FiveM réinitialise la globale une fois que le gestionnaire cède la main, donc une lecture plus tardive donne le mauvais joueur.

Interroger le client

triggerClientCallback

Pose une question au client d’un joueur et attend la réponse. Le client répond avec onServerCallback(name, handler) (Lib : client). Seul le joueur interrogé peut répondre.

Côté : serveur · Attend : oui (Promise en TypeScript, cède la main (yield) dans le thread en Lua)

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

Paramètres

Prop

Type

Retour

Promise<unknown> : se résout avec la réponse du client. Est rejetée avec une Error quand la réponse n’est pas arrivée à temps ou que le gestionnaire client a échoué. La réponse vient d’un client : analysez-la avant de l’utiliser.

En Lua, la fonction renvoie la réponse, ou nil et 'timeout' / 'failed'.

Exemple

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");
  },
);

Actions chronométrées vérifiées par le serveur

progressFor

Lance la barre de progression d’ai-lib sur l’écran du joueur et attend qu’elle se termine. Utilisez-la pour toute action chronométrée à laquelle le serveur doit pouvoir se fier (utiliser un objet, réparer, braquer) : elle ne répond true que lorsque la barre est arrivée au bout sur le client et qu’assez de temps s’est écoulé sur le serveur, si bien qu’un tricheur ne peut pas la sauter. Vous n’écrivez aucun code client : ai-lib répond pour toutes les ressources.

Côté : serveur · Attend : oui (Promise en TypeScript, cède la main (yield) dans le thread en Lua)

Signature
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- booléen, à appeler dans un thread

Paramètres

Prop

Type

ProgressForOptions

Prop

Type

Retour

Promise<boolean> : true quand la barre s’est terminée. false quand le joueur a annulé, est mort, est monté dans un véhicule (sans allowVehicle), est parti, a changé de personnage, a répondu trop vite, ou qu’aucune réponse n’est arrivée dans les duration plus 15 secondes.

Exemple

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");
    // Le monde a pu changer pendant la barre : vérifiez à nouveau.
    const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
    if (!vehicle) return fail("TOO_FAR");
    if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
    // La santé du véhicule se règle sur le client propriétaire de l'entité.
    emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
    return { ok: true };
  },
);

Un second progressFor pour le même joueur termine le premier avec false. Vérifiez à nouveau la distance et le stock après l’await : le monde a pu changer pendant la barre. En Lua, une déconnexion ou un changement de personnage n’est pas détecté plus tôt : l’appel répond false quand le délai (duration + 15 s) expire.

minigameFor

Lance l’un des mini-jeux d’ai-lib (un test d’adresse ou un crochetage) sur l’écran du joueur pour un flux serveur et attend le résultat. Utilisez-la pour crocheter une porte, percer un coffre-fort, démarrer un véhicule sans clé. Le résultat est la parole du client, donc le serveur décide toujours de ce qu’une victoire rapporte (un tirage au sort, un progressFor, ses propres contrôles).

Côté : serveur · Attend : oui (Promise en TypeScript, cède la main (yield) dans le thread en Lua)

Signature
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', à appeler dans un thread

Paramètres

Prop

Type

MinigameForOptions

Prop

Type

Retour

Promise<MinigameOutcome> où MinigameOutcome = "won" | "lost" | "broken" :

  • "won" : le joueur a gagné (et pas plus vite que minMs).
  • "lost" : le joueur a raté, abandonné, gagné trop vite, ou n’a pas répondu dans les 120 secondes.
  • "broken" : crochetage uniquement, le dernier crochet s’est cassé. Retirez l’objet de crochetage.

Exemple

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 },
);

N’écrivez jamais votre propre callback client de mini-jeu pour un flux serveur : utilisez minigameFor. Sans minMs, un client modifié peut répondre « won » aussitôt. Le 1500 ci-dessus est un exemple : mesurez la partie honnête la plus rapide avec vos réglages.

PendingActions

Garde une action en attente par joueur pour les flux où le client ouvre une boîte de dialogue et envoie le résultat plus tard (écrire une note, choisir une option) sans barre chronométrée. start remet un jeton que le client renvoie ; take ne termine l’action que si ce jeton correspond. Les entrées sont libérées quand le joueur se déconnecte ou quitte son personnage (un changement de personnage ne remet jamais l’action de l’ancien personnage au nouveau). Créez-en une au niveau du module.

Côté : serveur · Attend : non

Pas d’équivalent Lua : gardez une table Lua de jetons par joueur.

Signature
new PendingActions<T>(options?: { minRatio?: number })

Paramètres du constructeur

Prop

Type

PendingActions.start

Démarre l’action du joueur, en remplaçant celle qui tourne encore, et renvoie son jeton.

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

Prop

Type

Retour number : le jeton, un nombre entier de 1 à 2147483647. Envoyez-le au client ; relisez-le avec readId.

PendingActions.take

Termine l’action et renvoie ses données quand token est celui en cours et qu’assez de temps s’est écoulé.

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

Retour T | null : les données, ou null quand aucune action ne tourne, que le jeton ne correspond pas, ou que la réponse est arrivée trop tôt. Un mauvais jeton ne touche pas à l’action en cours (un jeton périmé ou falsifié ne l’annule jamais) ; un jeton correct la termine même quand il arrive trop tôt.

PendingActions.peek

Signature
peek(source: number): T | null

Retour les données de l’action en cours sans la terminer, ou null.

PendingActions.cancel

Signature
cancel(source: number): void

Termine l’action du joueur sans résultat.

PendingActions.size

Signature
readonly size: number

Combien d’actions tournent en ce moment.

Exemple

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 });

// Étape 1 : le client demande à écrire ; le serveur répond par un jeton.
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()] },
);

// Étape 2 : le client envoie le jeton et le texte.
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 };
  },
);

Avancé

ClientGate

La classe derrière onClientCallback et onClientEvent. Vous n’en avez besoin que pour construire votre propre type d’entrée au-dessus d’un événement brut ; les scripts utilisent les deux fonctions ci-dessus.

Côté : serveur · Attend : non

Pas d’équivalent Lua.

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 renvoie les arguments vérifiés (un tableau vide sans args) ou null quand l’appel ne doit pas s’exécuter (pas un vrai joueur, fréquence, garde, analyseur).
  • run exécute task sous le verrou par joueur quand lock est activé, et renvoie busy à la place quand un autre appel de ce joueur tourne encore.
  • release libère les entrées de fréquence et de verrou du joueur. Chaque porte est libérée d’elle-même sur playerDropped.

Sur cette page