AI Core
Referência da APILib: servidor

Entrada do cliente

Responda aos clientes com segurança: onClientCallback, onClientEvent, guards, triggerClientCallback, barras de progresso e minigames verificados pelo servidor, PendingActions.

Tudo o que um cliente envia pode ser falsificado por um trapaceiro. Todo servidor ai-* escuta os clientes só pelo portão: onClientCallback e onClientEvent. Antes de o seu handler rodar, o portão verifica, nesta ordem:

  1. Um jogador real e conectado o enviou (não outro recurso emitindo o mesmo nome).
  2. O limite de taxa (callbacks 20 chamadas por segundo por jogador, eventos 10, a menos que você defina rate).
  3. Seus guards (hasCharacter(), near(...), isAdmin() ou os seus próprios).
  4. Seu parser de args, que transforma os valores brutos em valores verificados ou recusa a chamada.
  5. Opcionalmente um lock: uma chamada por vez por jogador.

Uma recusa com motivo é escrita no console do servidor com o nome do jogador e o nome da entrada, no máximo uma linha por jogador e entrada a cada 10 segundos (as linhas no meio são somadas na seguinte).

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

Os leitores de entrada (readInt, readId, readText, oneArg, asArgs) e fail são documentados em Lib: compartilhada e fivem. Uma ação responde um ActionResult: { ok: true, ... } ou fail("LANG_KEY").

Portão

onClientCallback

Responde ao triggerServerCallback(name, ...) (ou callAction) de um cliente e envia de volta o valor de retorno do handler. Use para todo pedido em que o cliente espera uma resposta: comprar, usar, abrir uma tela. Tem duas formas: sem argumentos (name, handler, options?), e com argumentos (name, { args, ... }, handler), em que as opções vêm primeiro para que o seu handler só veja valores verificados.

Lado: Servidor · Espera: não (o handler pode ser async; a promise dele é aguardada antes de a resposta ser enviada)

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

Parâmetros

Prop

Type

ClientInputOptions

Prop

Type

ParsedInputOptions

Todos os campos de ClientInputOptions, mais:

Prop

Type

Retorno

void. A resposta que o cliente recebe:

  • o valor de retorno do handler quando ele rodou;
  • uma falha quando o portão recusou a chamada, o lock estava ocupado ou o handler lançou um erro. Em TypeScript a promise de triggerServerCallback do cliente é rejeitada na hora (callAction mostra o texto fallback); em Lua aiLib.triggerServerCallback retorna nil, 'failed'.

Exemplo

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

A versão Lua não tem guards nem args: ela aceita options = { rate?, lock?, ace? } (ace: o jogador precisa desta permissão ACE, ai.admin para admins do ai-core) e o handler recebe os valores brutos. Verifique cada argumento você mesmo (tipo, faixa, posse, distância) antes de usá-lo. Em TypeScript, nunca leia valores do cliente fora de args.

onClientEvent

Escuta um net event que um cliente envia, pelo mesmo portão. Use para mensagens de mão única em que o cliente não espera resposta (fechar uma tela, uma célula informada). Nunca use um onNet puro para entrada do cliente. Mesmas duas formas de onClientCallback.

Lado: Servidor · Espera: não

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

Parâmetros

Prop

Type

Retorno

void.

Exemplo

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

A versão Lua aceita options = { rate?, ace? } (sem lock, sem guards, sem args). Um TriggerEvent local vindo de outro recurso não carrega jogador e é ignorado nos dois lados.

Guards

Um guard é uma função que recebe o id de servidor do jogador e responde true (passa), false (recusa em silêncio) ou uma string (recusa e registra a string como motivo).

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

Escreva o seu próprio guard quando uma verificação se repete em várias 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

Um guard que passa quando o jogador tem uma permissão ACE: a permissão de admin do ai-core ai.admin, ou a que você indicar.

Lado: Servidor · Espera: não

Signature
isAdmin(ace?: string): Guard
Lua
local allowed = aiLib.isAdmin(source, ace) -- boolean, uma verificação direta (não um guard)

Parâmetros

Prop

Type

Retorno

Guard: recusa com o motivo not allowed (<ace>). O equivalente em Lua retorna true ou false diretamente, e false para source 0 ou menor.

Exemplo

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

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

hasCharacter

Um guard que passa quando o jogador tem um personagem no mundo (não na tela de personagens).

Lado: Servidor · Espera: não

Signature
hasCharacter(): Guard

Sem equivalente em Lua: verifique aiLib.online.character(source) ~= nil.

Retorno

Guard: recusa com o motivo no character loaded.

Exemplo

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

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

near

Um guard que passa quando o ped do jogador está a até metres de um ponto, medido no servidor (o cliente não pode mentir sobre isso).

Lado: Servidor · Espera: não

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

Sem equivalente em Lua: compare #(GetEntityCoords(GetPlayerPed(source)) - point) você mesmo.

Parâmetros

Retorno

Guard: recusa com o motivo too far (<metres> m).

Exemplo

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

O id de servidor do jogador que enviou o net event sendo tratado. Dentro de onClientCallback e onClientEvent você recebe source como primeiro argumento, então só precisa disto em um handler onNet ou on puro do FiveM.

Lado: Servidor · Espera: não

Signature
eventSource(): number

Sem equivalente em Lua: em Lua você lê a global source.

Retorno

number: o id de servidor do jogador (a global source do FiveM).

Exemplo

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

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

Leia antes do primeiro await. O FiveM reseta a global depois que o handler cede a vez, então uma leitura posterior dá o jogador errado.

Perguntando ao cliente

triggerClientCallback

Faz uma pergunta ao cliente de um jogador e espera a resposta. O cliente responde com onServerCallback(name, handler) (Lib: cliente). Só o jogador a quem você perguntou pode responder.

Lado: Servidor · Espera: sim (Promise em TypeScript, pausa a thread em Lua)

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

Parâmetros

Prop

Type

Retorno

Promise<unknown>: resolve com a resposta do cliente. Rejeita com um Error quando a resposta não chegou a tempo ou o handler do cliente falhou. A resposta vem de um cliente: interprete-a antes de usar.

Em Lua retorna a resposta, ou nil e 'timeout' / 'failed'.

Exemplo

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

Ações cronometradas em que o servidor confia

progressFor

Roda a barra de progresso do ai-lib na tela do jogador e espera até ela terminar. Use para qualquer ação cronometrada em que o servidor precisa confiar (usar um item, consertar, roubar): ela só responde true quando a barra terminou no cliente e passou tempo suficiente no servidor, então um trapaceiro não consegue pulá-la. Você não escreve nenhum código de cliente: o ai-lib responde por todo recurso.

Lado: Servidor · Espera: sim (Promise em TypeScript, pausa a thread em Lua)

Signature
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- boolean, chame dentro de uma thread

Parâmetros

Prop

Type

ProgressForOptions

Prop

Type

Retorno

Promise<boolean>: true quando a barra terminou. false quando o jogador cancelou, morreu, entrou em um veículo (sem allowVehicle), saiu, trocou de personagem, respondeu rápido demais, ou nenhuma resposta chegou dentro de duration mais 15 segundos.

Exemplo

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");
    // O mundo pode ter mudado enquanto a barra rodava: verifique de novo.
    const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
    if (!vehicle) return fail("TOO_FAR");
    if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
    // A vida do veículo é definida no cliente dono da entidade.
    emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
    return { ok: true };
  },
);

Um segundo progressFor para o mesmo jogador encerra o primeiro com false. Verifique distância e estoque de novo depois do await: o mundo pode ter mudado enquanto a barra rodava. Em Lua, uma saída ou troca de personagem não é detectada antes: a chamada responde false quando o timeout (duration + 15 s) se esgota.

minigameFor

Toca um dos minigames do ai-lib (um skill check ou um lockpick) na tela do jogador para um fluxo do servidor e espera o resultado. Use para arrombar uma porta, abrir um cofre, fazer ligação direta. O resultado é a palavra do cliente, então o servidor ainda decide o que uma vitória dá (uma rolagem de chance, um progressFor, as próprias verificações).

Lado: Servidor · Espera: sim (Promise em TypeScript, pausa a thread em Lua)

Signature
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', chame dentro de uma thread

Parâmetros

Prop

Type

MinigameForOptions

Prop

Type

Retorno

Promise<MinigameOutcome> em que MinigameOutcome = "won" | "lost" | "broken":

  • "won": o jogador venceu (e não mais rápido que minMs).
  • "lost": o jogador errou, desistiu, venceu rápido demais ou não respondeu em 120 segundos.
  • "broken": só lockpick, a última gazua quebrou. Tire o item de lockpick.

Exemplo

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 escreva o seu próprio callback de minigame no cliente para um fluxo do servidor: use minigameFor. Sem minMs, um cliente modificado pode responder "won" na hora. O 1500 acima é um exemplo: meça a jogada honesta mais rápida com as suas configurações.

PendingActions

Guarda uma ação em espera por jogador para fluxos em que o cliente abre um diálogo e depois envia o resultado (escrever um bilhete, escolher uma opção) sem uma barra cronometrada. start entrega um token que o cliente devolve; take encerra a ação só quando esse token confere. As entradas são liberadas quando o jogador sai ou larga o personagem (uma troca nunca paga a ação do personagem antigo ao novo). Crie uma no nível do módulo.

Lado: Servidor · Espera: não

Sem equivalente em Lua: mantenha uma tabela Lua de tokens por jogador.

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

Parâmetros do construtor

Prop

Type

PendingActions.start

Inicia a ação do jogador, substituindo uma que ainda roda, e retorna o token dela.

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

Prop

Type

Retorno number: o token, um número inteiro de 1 a 2147483647. Envie-o ao cliente; leia-o de volta com readId.

PendingActions.take

Encerra a ação e retorna os dados dela quando token é o que está rodando e passou tempo suficiente.

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

Retorno T | null: os dados, ou null quando nenhuma ação está rodando, o token não confere ou a resposta chegou cedo demais. Um token errado deixa a ação em andamento intacta (um token velho ou forjado nunca a cancela); um que confere a encerra mesmo quando chegou cedo demais.

PendingActions.peek

Signature
peek(source: number): T | null

Retorno os dados da ação em andamento sem encerrá-la, ou null.

PendingActions.cancel

Signature
cancel(source: number): void

Encerra a ação do jogador sem resultado.

PendingActions.size

Signature
readonly size: number

Quantas ações estão rodando agora.

Exemplo

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

// Passo 1: o cliente pede para escrever; o servidor responde um 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()] },
);

// Passo 2: o cliente envia o token e o 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 };
  },
);

Avançado

ClientGate

A classe por trás de onClientCallback e onClientEvent. Você só precisa dela quando constrói o seu próprio tipo de entrada sobre um evento bruto; os scripts usam as duas funções acima.

Lado: Servidor · Espera: não

Sem equivalente em 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 retorna os argumentos verificados (um array vazio sem args) ou null quando a chamada não deve rodar (não é um jogador real, taxa, guard, parser).
  • run roda task sob o lock por jogador quando lock está ativo, e retorna busy em vez disso quando outra chamada daquele jogador ainda está rodando.
  • release libera as entradas de taxa e de lock do jogador. Todo portão é liberado em playerDropped sozinho.

Nesta página