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 :
- Qu’un vrai joueur connecté l’a envoyé (et non une autre ressource qui émet le même nom).
- La limite de fréquence (callbacks 20 appels par seconde et par joueur, événements 10, sauf si vous définissez
rate). - Vos gardes (
hasCharacter(),near(...),isAdmin()ou les vôtres). - Votre analyseur
args, qui transforme les valeurs brutes en valeurs vérifiées ou refuse l’appel. - É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).
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)
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, ...): anyParamè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
triggerServerCallbackdu client est rejetée aussitôt (callActionaffiche son textefallback) ; en Lua,aiLib.triggerServerCallbackrenvoienil, '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
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? }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 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
isAdmin(ace?: string): Guardlocal 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
hasCharacter(): GuardPas 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
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): GuardPas d’équivalent Lua : comparez vous-même #(GetEntityCoords(GetPlayerPed(source)) - point).
Paramètres
Prop
Type
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
eventSource(): numberPas 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)
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- à appeler dans un threadParamè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)
progressFor(source: number, options: ProgressForOptions): Promise<boolean>local finished = aiLib.progressFor(source, options) -- booléen, à appeler dans un threadParamè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)
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', à appeler dans un threadParamètres
Prop
Type
MinigameForOptions
Prop
Type
Retour
Promise<MinigameOutcome> où MinigameOutcome = "won" | "lost" | "broken" :
"won": le joueur a gagné (et pas plus vite queminMs)."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.
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.
start(source: number, data: T, durationMs: number): numberProp
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é.
take(source: number, token: number): T | nullRetour 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
peek(source: number): T | nullRetour les données de l’action en cours sans la terminer, ou null.
PendingActions.cancel
cancel(source: number): voidTermine l’action du joueur sans résultat.
PendingActions.size
readonly size: numberCombien 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.
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
checkrenvoie les arguments vérifiés (un tableau vide sansargs) ounullquand l’appel ne doit pas s’exécuter (pas un vrai joueur, fréquence, garde, analyseur).runexécutetasksous le verrou par joueur quandlockest activé, et renvoiebusyà la place quand un autre appel de ce joueur tourne encore.releaselibère les entrées de fréquence et de verrou du joueur. Chaque porte est libérée d’elle-même surplayerDropped.
Lib : serveur
@aicore-fivem/lib/server et le côté serveur d’aiLib : ce qu’ils sont, comment les charger en TypeScript et en Lua, et toutes les pages thématiques.
Commandes
addCommand : des commandes de chat avec arguments analysés, permissions ACE, suggestions de chat et une ligne dans le panneau d’administration d’ai-core.