Ввод от клиента
Безопасные ответы клиентам: onClientCallback, onClientEvent, guard-условия, triggerClientCallback, полосы прогресса и мини-игры с проверкой на сервере, PendingActions.
Всё, что отправляет клиент, читер может подделать. Каждый сервер ai-* слушает клиентов только через шлюз: onClientCallback и onClientEvent. До запуска вашего обработчика шлюз проверяет, в таком порядке:
- Это отправил настоящий подключённый игрок (а не другой ресурс, вызвавший то же имя).
- Ограничение частоты (колбэки 20 вызовов в секунду на игрока, события 10, если вы не задали
rate). - Ваши guard-условия (
hasCharacter(),near(...),isAdmin()или ваши собственные). - Ваш парсер
args, который превращает сырые значения в проверенные или отклоняет вызов. - Необязательно блокировку: один вызов за раз на игрока.
Отказ с причиной пишется в консоль сервера с именем игрока и именем ввода, не чаще одной строки на игрока и ввод каждые 10 секунд (строки в промежутке подсчитываются в следующую).
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): voidaiLib.onClientCallback(name, handler, options) -- handler(source, ...): anyПараметры
Prop
Type
ClientInputOptions
Prop
Type
ParsedInputOptions
Все поля ClientInputOptions, плюс:
Prop
Type
Возвращает
void. Ответ, который получает клиент:
- возвращаемое значение обработчика, если он выполнился;
- неудача, если шлюз отклонил вызов, блокировка была занята или обработчик выбросил ошибку. В TypeScript promise клиентского
triggerServerCallbackсразу отклоняется (callActionпоказывает свой текстfallback); в LuaaiLib.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): voidaiLib.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): Guardlocal 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(): GuardLua-аналога нет: проверьте 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): GuardLua-аналога нет: сами сравните #(GetEntityCoords(GetPlayerPed(source)) - point).
Параметры
Prop
Type
Возвращает
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(): numberLua-аналога нет: 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>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>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>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): numberProp
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): voidProp
Type
checkвозвращает проверенные аргументы (пустой массив безargs) илиnull, если вызов не должен выполняться (не настоящий игрок, частота, guard-условие, парсер).runвыполняетtaskпод блокировкой игрока, еслиlockвключён, и вместо этого возвращаетbusy, если ещё выполняется другой вызов этого игрока.releaseосвобождает записи частоты и блокировки игрока. Каждый шлюз освобождается приplayerDroppedсам.