AI Core
Справочник APILib: сервер

Ввод от клиента

Безопасные ответы клиентам: onClientCallback, onClientEvent, guard-условия, triggerClientCallback, полосы прогресса и мини-игры с проверкой на сервере, PendingActions.

Всё, что отправляет клиент, читер может подделать. Каждый сервер ai-* слушает клиентов только через шлюз: onClientCallback и onClientEvent. До запуска вашего обработчика шлюз проверяет, в таком порядке:

  1. Это отправил настоящий подключённый игрок (а не другой ресурс, вызвавший то же имя).
  2. Ограничение частоты (колбэки 20 вызовов в секунду на игрока, события 10, если вы не задали rate).
  3. Ваши guard-условия (hasCharacter(), near(...), isAdmin() или ваши собственные).
  4. Ваш парсер args, который превращает сырые значения в проверенные или отклоняет вызов.
  5. Необязательно блокировку: один вызов за раз на игрока.

Отказ с причиной пишется в консоль сервера с именем игрока и именем ввода, не чаще одной строки на игрока и ввод каждые 10 секунд (строки в промежутке подсчитываются в следующую).

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

Функции чтения ввода (readInt, readId, readText, oneArg, asArgs) и fail описаны на странице Lib: общее и fivem. Действие отвечает ActionResult: { ok: true, ... } или fail("LANG_KEY").

Шлюз

onClientCallback

Отвечает на triggerServerCallback(name, ...) (или callAction) клиента и отправляет обратно значение, которое вернул обработчик. Используйте для каждого запроса, где клиент ждёт ответа: покупка, использование, открытие экрана. Есть две формы: без аргументов (name, handler, options?) и с аргументами (name, { args, ... }, handler), где опции идут первыми, чтобы ваш обработчик видел только проверенные значения.

Сторона: Сервер · Ждёт: нет (обработчик может быть async; его promise ожидается перед отправкой ответа)

Сигнатура
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

Параметры

Prop

Type

ClientInputOptions

Prop

Type

ParsedInputOptions

Все поля ClientInputOptions, плюс:

Prop

Type

Возвращает

void. Ответ, который получает клиент:

  • возвращаемое значение обработчика, если он выполнился;
  • неудача, если шлюз отклонил вызов, блокировка была занята или обработчик выбросил ошибку. В TypeScript promise клиентского triggerServerCallback сразу отклоняется (callAction показывает свой текст fallback); в Lua aiLib.triggerServerCallback возвращает nil, 'failed'.

Пример

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

// Клиент: 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 };
  },
);

У версии для Lua нет guards и args: она принимает options = { rate?, lock?, ace? } (ace: игроку нужно это право ACE, ai.admin для администраторов ai-core), а обработчик получает сырые значения. Проверяйте каждый аргумент сами (тип, диапазон, владение, расстояние) перед использованием. В TypeScript никогда не читайте значения клиента вне args.

onClientEvent

Слушает сетевое событие, которое отправляет клиент, через тот же шлюз. Используйте для односторонних сообщений, где клиент не ждёт ответа (закрытие экрана, сообщённая ячейка). Никогда не используйте голый onNet для ввода от клиента. Те же две формы, что и у onClientCallback.

Сторона: Сервер · Ждёт: нет

Сигнатура
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? }

Параметры

Prop

Type

Возвращает

void.

Пример

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

const openShops = new PlayerMap<string>();

// Клиент: emitNet("my-shop:close")
onClientEvent("my-shop:close", (source) => {
  openShops.delete(source);
});

Версия для Lua принимает options = { rate?, ace? } (без lock, guards и args). Локальный TriggerEvent из другого ресурса не несёт игрока и игнорируется на обеих сторонах.

Guard-условия

Guard-условие это функция, которая получает серверный id игрока и отвечает true (пропустить), false (тихо отклонить) или строкой (отклонить и залогировать строку как причину).

Тип
type Guard = (source: number) => boolean | string;

Напишите своё guard-условие, когда проверка повторяется в нескольких вводах:

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

Guard-условие, которое пропускает, если у игрока есть право ACE: право администратора ai-core ai.admin или то, которое вы укажете.

Сторона: Сервер · Ждёт: нет

Сигнатура
isAdmin(ace?: string): Guard
Lua
local allowed = aiLib.isAdmin(source, ace) -- boolean, прямая проверка (не guard-условие)

Параметры

Prop

Type

Возвращает

Guard: отклоняет с причиной not allowed (<ace>). Lua-аналог возвращает true или false напрямую, и false для source 0 и ниже.

Пример

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

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

hasCharacter

Guard-условие, которое пропускает, если у игрока есть персонаж в мире (не на экране выбора персонажа).

Сторона: Сервер · Ждёт: нет

Сигнатура
hasCharacter(): Guard

Lua-аналога нет: проверьте aiLib.online.character(source) ~= nil.

Возвращает

Guard: отклоняет с причиной no character loaded.

Пример

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

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

near

Guard-условие, которое пропускает, если пед игрока находится в пределах metres от точки, с измерением на сервере (клиент не может об этом солгать).

Сторона: Сервер · Ждёт: нет

Сигнатура
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): Guard

Lua-аналога нет: сами сравните #(GetEntityCoords(GetPlayerPed(source)) - point).

Параметры

Возвращает

Guard: отклоняет с причиной too far (<metres> m).

Пример

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

Серверный id игрока, который отправил обрабатываемое сетевое событие. Внутри onClientCallback и onClientEvent вы получаете source первым аргументом, поэтому это нужно только в голом обработчике FiveM onNet или on.

Сторона: Сервер · Ждёт: нет

Сигнатура
eventSource(): number

Lua-аналога нет: Lua читает глобальный source.

Возвращает

number: серверный id игрока (глобальный source FiveM).

Пример

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

on("my-script:serverOnly", () => {
  const source = eventSource(); // сначала прочитайте его
  console.log(`event from ${source}`);
});

Читайте его до первого await. FiveM сбрасывает глобальную переменную после того, как обработчик уступает выполнение, поэтому более позднее чтение даст не того игрока.

Вопросы клиенту

triggerClientCallback

Задаёт вопрос клиенту одного игрока и ждёт ответа. Клиент отвечает через onServerCallback(name, handler) (Lib: клиент). Ответить может только тот игрок, которого вы спросили.

Сторона: Сервер · Ждёт: да (Promise в TypeScript, в Lua приостанавливает поток)

Сигнатура
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>
Lua
local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- вызывайте внутри потока

Параметры

Prop

Type

Возвращает

Promise<unknown>: выполняется с ответом клиента. Отклоняется с Error, если ответ не пришёл вовремя или обработчик клиента завершился ошибкой. Ответ приходит от клиента: разберите его перед использованием.

Lua возвращает ответ или nil и 'timeout' / 'failed'.

Пример

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

// Клиент: 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");
  },
);

Действия с таймером, которым доверяет сервер

progressFor

Запускает полосу прогресса ai-lib на экране игрока и ждёт её завершения. Используйте для любого действия с таймером, которому должен доверять сервер (использование предмета, ремонт, ограбление): она отвечает true, только если полоса дошла до конца на клиенте и на сервере прошло достаточно времени, поэтому читер не может её пропустить. Клиентский код писать не нужно: ai-lib отвечает за каждый ресурс.

Сторона: Сервер · Ждёт: да (Promise в TypeScript, в Lua приостанавливает поток)

Сигнатура
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- boolean, вызывайте внутри потока

Параметры

Prop

Type

ProgressForOptions

Prop

Type

Возвращает

Promise<boolean>: true, если полоса завершилась. false, если игрок отменил, умер, сел в транспорт (без allowVehicle), ушёл, сменил персонажа, ответил слишком быстро или ответ не пришёл в течение duration плюс 15 секунд.

Пример

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");
    // Мир мог измениться, пока шла полоса: проверьте снова.
    const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
    if (!vehicle) return fail("TOO_FAR");
    if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
    // Здоровье транспорта задаётся на клиенте, которому принадлежит сущность.
    emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
    return { ok: true };
  },
);

Второй progressFor для того же игрока завершает первый с false. После await снова проверьте расстояние и запасы: мир мог измениться, пока шла полоса. В Lua отключение или смена персонажа не обнаруживаются заранее: вызов отвечает false, когда истекает тайм-аут (duration + 15 с).

minigameFor

Запускает одну из мини-игр ai-lib (проверку навыка или взлом отмычкой) на экране игрока для серверного сценария и ждёт результата. Используйте для взлома двери, вскрытия сейфа, запуска двигателя без ключа. Результат это слово клиента, поэтому сервер всё равно решает, что даёт победа (бросок шанса, progressFor, собственные проверки).

Сторона: Сервер · Ждёт: да (Promise в TypeScript, в Lua приостанавливает поток)

Сигнатура
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', вызывайте внутри потока

Параметры

Prop

Type

MinigameForOptions

Prop

Type

Возвращает

Promise<MinigameOutcome>, где MinigameOutcome = "won" | "lost" | "broken":

  • "won": игрок победил (и не быстрее minMs).
  • "lost": игрок промахнулся, сдался, победил слишком быстро или не ответил в течение 120 секунд.
  • "broken": только взлом отмычкой, сломалась последняя отмычка. Заберите предмет отмычки.

Пример

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

Никогда не пишите собственный клиентский колбэк мини-игры для серверного сценария: используйте minigameFor. Без minMs модифицированный клиент может сразу ответить "won". 1500 выше это пример: измерьте самую быструю честную игру с вашими настройками.

PendingActions

Хранит одно ожидающее действие на игрока для сценариев, где клиент открывает диалог и позже отправляет результат (написание записки, выбор варианта) без полосы с таймером. start выдаёт токен, который клиент отправляет обратно; take завершает действие, только если этот токен совпадает. Записи освобождаются, когда игрок отключается или отпускает персонажа (смена персонажа никогда не передаёт действие старого персонажа новому). Создайте один на уровне модуля.

Сторона: Сервер · Ждёт: нет

Lua-аналога нет: храните таблицу Lua с токенами для каждого игрока.

Сигнатура
new PendingActions<T>(options?: { minRatio?: number })

Параметры конструктора

Prop

Type

PendingActions.start

Запускает действие игрока, заменяя ещё выполняющееся, и возвращает его токен.

Сигнатура
start(source: number, data: T, durationMs: number): number

Prop

Type

Возвращает number: токен, целое число от 1 до 2147483647. Отправьте его клиенту; прочитайте обратно через readId.

PendingActions.take

Завершает действие и возвращает его данные, если token совпадает с текущим и прошло достаточно времени.

Сигнатура
take(source: number, token: number): T | null

Возвращает T | null: данные или null, если действие не выполняется, токен не совпадает или ответ пришёл слишком рано. Неверный токен не трогает текущее действие (устаревший или поддельный токен никогда его не отменяет); совпадающий завершает его, даже если ответ пришёл слишком рано.

PendingActions.peek

Сигнатура
peek(source: number): T | null

Возвращает данные текущего действия, не завершая его, или null.

PendingActions.cancel

Сигнатура
cancel(source: number): void

Завершает действие игрока без результата.

PendingActions.size

Сигнатура
readonly size: number

Сколько действий выполняется сейчас.

Пример

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

// Шаг 1: клиент просит разрешения написать; сервер отвечает токеном.
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()] },
);

// Шаг 2: клиент отправляет токен и текст.
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 };
  },
);

Продвинутое

ClientGate

Класс, на котором построены onClientCallback и onClientEvent. Он нужен, только если вы строите собственный вид ввода поверх голого события; скрипты используют две функции выше.

Сторона: Сервер · Ждёт: нет

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): void

Prop

Type

  • check возвращает проверенные аргументы (пустой массив без args) или null, если вызов не должен выполняться (не настоящий игрок, частота, guard-условие, парсер).
  • run выполняет task под блокировкой игрока, если lock включён, и вместо этого возвращает busy, если ещё выполняется другой вызов этого игрока.
  • release освобождает записи частоты и блокировки игрока. Каждый шлюз освобождается при playerDropped сам.

На этой странице