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:
- Um jogador real e conectado o enviou (não outro recurso emitindo o mesmo nome).
- O limite de taxa (callbacks 20 chamadas por segundo por jogador, eventos 10, a menos que você defina
rate). - Seus guards (
hasCharacter(),near(...),isAdmin()ou os seus próprios). - Seu parser de
args, que transforma os valores brutos em valores verificados ou recusa a chamada. - 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).
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)
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, ...): anyParâ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
triggerServerCallbackdo cliente é rejeitada na hora (callActionmostra o textofallback); em LuaaiLib.triggerServerCallbackretornanil, '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
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? }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 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
isAdmin(ace?: string): Guardlocal 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
hasCharacter(): GuardSem 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
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): GuardSem equivalente em Lua: compare #(GetEntityCoords(GetPlayerPed(source)) - point) você mesmo.
Parâmetros
Prop
Type
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
eventSource(): numberSem 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)
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- chame dentro de uma threadParâ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)
progressFor(source: number, options: ProgressForOptions): Promise<boolean>local finished = aiLib.progressFor(source, options) -- boolean, chame dentro de uma threadParâ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)
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', chame dentro de uma threadParâ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 queminMs)."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.
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.
start(source: number, data: T, durationMs: number): numberProp
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.
take(source: number, token: number): T | nullRetorno 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
peek(source: number): T | nullRetorno os dados da ação em andamento sem encerrá-la, ou null.
PendingActions.cancel
cancel(source: number): voidEncerra a ação do jogador sem resultado.
PendingActions.size
readonly size: numberQuantas 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.
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
checkretorna os argumentos verificados (um array vazio semargs) ounullquando a chamada não deve rodar (não é um jogador real, taxa, guard, parser).runrodatasksob o lock por jogador quandolockestá ativo, e retornabusyem vez disso quando outra chamada daquele jogador ainda está rodando.releaselibera as entradas de taxa e de lock do jogador. Todo portão é liberado emplayerDroppedsozinho.