AI Core
Referencia de la APILib: servidor

Entrada del cliente

Responde a los clientes de forma segura: onClientCallback, onClientEvent, guardas, triggerClientCallback, barras de progreso y minijuegos comprobados por el servidor, PendingActions.

Todo lo que envía un cliente puede falsificarlo un tramposo. Cada servidor ai-* escucha a los clientes solo a través de la puerta: onClientCallback y onClientEvent. Antes de que se ejecute tu handler, la puerta comprueba, en este orden:

  1. Que lo envió un jugador real y conectado (no otro recurso que emite el mismo nombre).
  2. El límite de frecuencia (callbacks: 20 llamadas por segundo por jugador; eventos: 10, salvo que fijes rate).
  3. Tus guardas (hasCharacter(), near(...), isAdmin() o las tuyas).
  4. Tu parser args, que convierte los valores en bruto en valores comprobados o rechaza la llamada.
  5. Opcionalmente un bloqueo: una llamada a la vez por jugador.

Un rechazo con motivo se escribe en la consola del servidor con el nombre del jugador y el nombre de la entrada, como máximo una línea por jugador y entrada cada 10 segundos (las líneas intermedias se cuentan en la siguiente).

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

Los lectores de entrada (readInt, readId, readText, oneArg, asArgs) y fail se documentan en Lib: compartido y fivem. Una acción responde un ActionResult: { ok: true, ... } o fail("LANG_KEY").

Puerta

onClientCallback

Responde a un triggerServerCallback(name, ...) (o callAction) de un cliente y le devuelve el valor de retorno del handler. Úsalo para cada petición en la que el cliente espera una respuesta: comprar, usar, abrir una pantalla. Tiene dos formas: sin argumentos (name, handler, options?), y con argumentos (name, { args, ... }, handler), donde las opciones van primero para que tu handler solo vea valores comprobados.

Lado: Servidor · Espera: no (el handler puede ser async; su promise se espera antes de enviar la respuesta)

Firma
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

Parámetros

Prop

Type

ClientInputOptions

Prop

Type

ParsedInputOptions

Todos los campos de ClientInputOptions, más:

Prop

Type

Devuelve

void. La respuesta que recibe el cliente:

  • el valor de retorno del handler cuando se ejecutó;
  • un fallo cuando la puerta rechazó la llamada, el bloqueo estaba ocupado o el handler lanzó un error. En TypeScript, la promise de triggerServerCallback del cliente se rechaza al momento (callAction muestra su texto fallback); en Lua, aiLib.triggerServerCallback devuelve nil, 'failed'.

Ejemplo

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

// Cliente: 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 versión Lua no tiene guards ni args: acepta options = { rate?, lock?, ace? } (ace: el jugador necesita este permiso ACE, ai.admin para los admins de ai-core) y el handler recibe los valores en bruto. Comprueba tú mismo cada argumento (tipo, rango, propiedad, distancia) antes de usarlo. En TypeScript, nunca leas valores del cliente fuera de args.

onClientEvent

Escucha un evento de red que envía un cliente, a través de la misma puerta. Úsalo para mensajes de un solo sentido en los que el cliente no espera respuesta (cerrar una pantalla, una celda informada). Nunca uses un onNet a secas para la entrada del cliente. Las mismas dos formas que onClientCallback.

Lado: Servidor · Espera: no

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

Parámetros

Prop

Type

Devuelve

void.

Ejemplo

import { onClientEvent, PlayerMap } from "@aicore-fivem/lib/server";

const openShops = new PlayerMap<string>();

// Cliente: emitNet("my-shop:close")
onClientEvent("my-shop:close", (source) => {
  openShops.delete(source);
});

La versión Lua acepta options = { rate?, ace? } (sin lock, sin guards, sin args). Un TriggerEvent local desde otro recurso no lleva jugador y se ignora en ambos lados.

Guardas

Una guarda es una función que recibe el id de servidor del jugador y responde true (pasa), false (rechaza en silencio) o un string (rechaza y registra el string como motivo).

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

Escribe tu propia guarda cuando una comprobación se repite en varias entradas:

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

Una guarda que pasa cuando el jugador tiene un permiso ACE: el permiso de admin de ai-core ai.admin, o el que indiques.

Lado: Servidor · Espera: no

Firma
isAdmin(ace?: string): Guard
Lua
local allowed = aiLib.isAdmin(source, ace) -- boolean, una comprobación directa (no una guarda)

Parámetros

Prop

Type

Devuelve

Guard: rechaza con el motivo not allowed (<ace>). El equivalente en Lua devuelve true o false directamente, y false para source 0 o menor.

Ejemplo

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

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

hasCharacter

Una guarda que pasa cuando el jugador tiene un personaje en el mundo (no en la pantalla de personajes).

Lado: Servidor · Espera: no

Firma
hasCharacter(): Guard

Sin equivalente en Lua: comprueba aiLib.online.character(source) ~= nil.

Devuelve

Guard: rechaza con el motivo no character loaded.

Ejemplo

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

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

near

Una guarda que pasa cuando el ped del jugador está a menos de metres de un punto, medido en el servidor (el cliente no puede mentir al respecto).

Lado: Servidor · Espera: no

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

Sin equivalente en Lua: compara tú mismo #(GetEntityCoords(GetPlayerPed(source)) - point).

Parámetros

Devuelve

Guard: rechaza con el motivo too far (<metres> m).

Ejemplo

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

El id de servidor del jugador que envió el evento de red que se está gestionando. Dentro de onClientCallback y onClientEvent recibes source como primer argumento, así que solo lo necesitas en un handler onNet u on de FiveM a secas.

Lado: Servidor · Espera: no

Firma
eventSource(): number

Sin equivalente en Lua: Lua lee la variable global source.

Devuelve

number: el id de servidor del jugador (la variable global source de FiveM).

Ejemplo

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

on("my-script:serverOnly", () => {
  const source = eventSource(); // léelo primero
  console.log(`event from ${source}`);
});

Léelo antes del primer await. FiveM reinicia la variable global después de que el handler cede, así que una lectura posterior da el jugador equivocado.

Preguntar al cliente

triggerClientCallback

Hace una pregunta al cliente de un jugador y espera la respuesta. El cliente responde con onServerCallback(name, handler) (Lib: cliente). Solo puede responder el jugador al que preguntaste.

Lado: Servidor · Espera: sí (Promise en TypeScript, cede el hilo en Lua)

Firma
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>
Lua
local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- llamar dentro de un hilo

Parámetros

Prop

Type

Devuelve

Promise<unknown>: se resuelve con la respuesta del cliente. Se rechaza con un Error cuando la respuesta no llegó a tiempo o el handler del cliente falló. La respuesta viene de un cliente: procésala antes de usarla.

Lua devuelve la respuesta, o nil y 'timeout' / 'failed'.

Ejemplo

import { addCommand, triggerClientCallback, notify } from "@aicore-fivem/lib/server";
import { readText } from "@aicore-fivem/lib/shared";

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

Acciones temporizadas en las que confía el servidor

progressFor

Ejecuta la barra de progreso de ai-lib en la pantalla del jugador y espera hasta que termine. Úsalo para cualquier acción temporizada en la que deba confiar el servidor (usar un objeto, reparar, robar): solo responde true cuando la barra terminó en el cliente y pasó suficiente tiempo en el servidor, así que un tramposo no puede saltársela. No escribes nada de código de cliente: ai-lib responde por cada recurso.

Lado: Servidor · Espera: sí (Promise en TypeScript, cede el hilo en Lua)

Firma
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- boolean, llamar dentro de un hilo

Parámetros

Prop

Type

ProgressForOptions

Prop

Type

Devuelve

Promise<boolean>: true cuando la barra terminó. false cuando el jugador canceló, murió, se subió a un vehículo (sin allowVehicle), se fue, cambió de personaje, respondió demasiado rápido, o no llegó ninguna respuesta dentro de duration más 15 segundos.

Ejemplo

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");
    // El mundo puede haber cambiado mientras corría la barra: comprueba otra vez.
    const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
    if (!vehicle) return fail("TOO_FAR");
    if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
    // La salud del vehículo se fija en el cliente dueño de la entidad.
    emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
    return { ok: true };
  },
);

Un segundo progressFor para el mismo jugador termina el primero con false. Comprueba de nuevo la distancia y el stock después del await: el mundo puede haber cambiado mientras corría la barra. En Lua, una desconexión o un cambio de personaje no se detecta antes: la llamada responde false cuando se agota el tiempo límite (duration + 15 s).

minigameFor

Juega uno de los minijuegos de ai-lib (una prueba de habilidad o una ganzúa) en la pantalla del jugador para un flujo del servidor y espera el resultado. Úsalo para forzar una puerta, abrir una caja fuerte o hacer un puente a un coche. El resultado es la palabra del cliente, así que el servidor sigue decidiendo qué da una victoria (una tirada de probabilidad, un progressFor, sus propias comprobaciones).

Lado: Servidor · Espera: sí (Promise en TypeScript, cede el hilo en Lua)

Firma
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', llamar dentro de un hilo

Parámetros

Prop

Type

MinigameForOptions

Prop

Type

Devuelve

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

  • "won": el jugador ganó (y no más rápido que minMs).
  • "lost": el jugador falló, se rindió, ganó demasiado rápido o no respondió en 120 segundos.
  • "broken": solo ganzúa, se rompió la última ganzúa. Quita el objeto de ganzúa.

Ejemplo

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

Nunca escribas tu propio callback de minijuego en el cliente para un flujo del servidor: usa minigameFor. Sin minMs, un cliente modificado puede responder "won" al instante. El 1500 de arriba es un ejemplo: mide la partida honesta más rápida con tus ajustes.

PendingActions

Guarda una acción en espera por jugador para los flujos en los que el cliente abre un diálogo y más tarde envía el resultado (escribir una nota, elegir una opción) sin una barra temporizada. start entrega un token que el cliente devuelve; take termina la acción solo cuando ese token coincide. Las entradas se liberan cuando el jugador se desconecta o suelta el personaje (un cambio nunca paga la acción del personaje antiguo al nuevo). Crea uno a nivel de módulo.

Lado: Servidor · Espera: no

Sin equivalente en Lua: guarda una tabla Lua de tokens por jugador.

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

Parámetros del constructor

Prop

Type

PendingActions.start

Inicia la acción del jugador, reemplazando una que siga en marcha, y devuelve su token.

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

Prop

Type

Devuelve number: el token, un número entero de 1 a 2147483647. Envíalo al cliente; léelo de vuelta con readId.

PendingActions.take

Termina la acción y devuelve sus datos cuando token es el que está en marcha y pasó suficiente tiempo.

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

Devuelve T | null: los datos, o null cuando no hay ninguna acción en marcha, el token no coincide o la respuesta llegó demasiado pronto. Un token incorrecto deja en paz la acción en marcha (un token viejo o falsificado nunca la cancela); uno que coincide la termina aunque haya llegado demasiado pronto.

PendingActions.peek

Firma
peek(source: number): T | null

Devuelve los datos de la acción en marcha sin terminarla, o null.

PendingActions.cancel

Firma
cancel(source: number): void

Termina la acción del jugador sin resultado.

PendingActions.size

Firma
readonly size: number

Cuántas acciones hay en marcha ahora.

Ejemplo

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

// Paso 1: el cliente pide escribir; el servidor responde un 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()] },
);

// Paso 2: el cliente envía el token y el texto.
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 };
  },
);

Avanzado

ClientGate

La clase detrás de onClientCallback y onClientEvent. Solo la necesitas cuando construyes tu propio tipo de entrada sobre un evento en bruto; los scripts usan las dos funciones de arriba.

Lado: Servidor · Espera: no

Sin equivalente en Lua.

Firma
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 devuelve los argumentos comprobados (un array vacío sin args) o null cuando la llamada no debe ejecutarse (no es un jugador real, frecuencia, guarda, parser).
  • run ejecuta task bajo el bloqueo por jugador cuando lock está activado, y devuelve busy en su lugar cuando otra llamada de ese jugador sigue en marcha.
  • release libera las entradas de frecuencia y de bloqueo del jugador. Cada puerta se libera sola en playerDropped.

En esta página