AI Core
Dokumentacja APILib: serwer

Wejście z klienta

Bezpieczne odpowiadanie klientom: onClientCallback, onClientEvent, strażnicy, triggerClientCallback, paski postępu i minigry sprawdzane przez serwer, PendingActions.

Wszystko, co wysyła klient, może zostać podrobione przez oszusta. Każdy serwer ai-* słucha klientów tylko przez bramkę: onClientCallback i onClientEvent. Zanim zadziała twój handler, bramka sprawdza w tej kolejności:

  1. Wysłał to prawdziwy, połączony gracz (a nie inny zasób emitujący tę samą nazwę).
  2. Limit częstotliwości (callbacki 20 wywołań na sekundę na gracza, zdarzenia 10, chyba że ustawisz rate).
  3. Twoich strażników (hasCharacter(), near(...), isAdmin() albo własnych).
  4. Twój parser args, który zamienia surowe wartości w sprawdzone albo odrzuca wywołanie.
  5. Opcjonalnie blokadę: jedno wywołanie naraz na gracza.

Odmowa z powodem jest zapisywana w konsoli serwera z nazwą gracza i nazwą wejścia, najwyżej jedna linia na gracza i wejście co 10 sekund (linie pomiędzy są doliczane do następnej).

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

Czytniki wejścia (readInt, readId, readText, oneArg, asArgs) i fail są opisane na stronie Lib: shared i fivem. Akcja odpowiada ActionResult: { ok: true, ... } albo fail("LANG_KEY").

Bramka

onClientCallback

Odpowiada na triggerServerCallback(name, ...) klienta (albo callAction) i odsyła wartość zwróconą przez handler. Używaj jej dla każdego żądania, w którym klient czeka na odpowiedź: kupowanie, używanie, otwieranie ekranu. Ma dwie formy: bez argumentów (name, handler, options?) i z argumentami (name, { args, ... }, handler), gdzie opcje są pierwsze, więc twój handler zawsze widzi tylko sprawdzone wartości.

Strona: Serwer · Czeka: nie (handler może być async; jego promise jest oczekiwany przed wysłaniem odpowiedzi)

Sygnatura
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

Parametry

Prop

Type

ClientInputOptions

Prop

Type

ParsedInputOptions

Wszystkie pola ClientInputOptions, plus:

Prop

Type

Zwracana wartość

void. Odpowiedź, którą dostaje klient:

  • wartość zwrócona przez handler, gdy zadziałał;
  • niepowodzenie, gdy bramka odrzuciła wywołanie, blokada była zajęta albo handler rzucił błąd. W TypeScript promise triggerServerCallback klienta od razu się odrzuca (callAction pokazuje swój tekst fallback); w Lua aiLib.triggerServerCallback zwraca nil, 'failed'.

Przykład

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

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

Wersja Lua nie ma guards ani args: przyjmuje options = { rate?, lock?, ace? } (ace: gracz potrzebuje tego uprawnienia ACE, ai.admin dla administratorów ai-core), a handler dostaje surowe wartości. Sprawdź sam każdy argument (typ, zakres, własność, odległość), zanim go użyjesz. W TypeScript nigdy nie czytaj wartości klienta poza args.

onClientEvent

Nasłuchuje zdarzenia sieciowego wysyłanego przez klienta, przez tę samą bramkę. Używaj go dla wiadomości w jedną stronę, gdy klient nie czeka na odpowiedź (zamknięcie ekranu, zgłoszona komórka). Nigdy nie używaj gołego onNet dla wejścia z klienta. Te same dwie formy co onClientCallback.

Strona: Serwer · Czeka: nie

Sygnatura
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, ...), opcje { rate?, ace? }

Parametry

Prop

Type

Zwracana wartość

void.

Przykład

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

const openShops = new PlayerMap<string>();

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

Wersja Lua przyjmuje options = { rate?, ace? } (bez lock, guards i args). Lokalne TriggerEvent z innego zasobu nie niesie gracza i jest ignorowane po obu stronach.

Strażnicy

Strażnik to funkcja, która dostaje id serwerowe gracza i odpowiada true (przepuść), false (odrzuć po cichu) albo stringiem (odrzuć i zaloguj string jako powód).

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

Napisz własnego strażnika, gdy sprawdzenie powtarza się w kilku wejściach:

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

Strażnik, który przepuszcza, gdy gracz ma uprawnienie ACE: uprawnienie administratora ai-core ai.admin albo to, które podasz.

Strona: Serwer · Czeka: nie

Sygnatura
isAdmin(ace?: string): Guard
Lua
local allowed = aiLib.isAdmin(source, ace) -- boolean, bezpośrednie sprawdzenie (nie strażnik)

Parametry

Prop

Type

Zwracana wartość

Guard: odrzuca z powodem not allowed (<ace>). Odpowiednik w Lua zwraca bezpośrednio true albo false, a false dla source 0 i niższego.

Przykład

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

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

hasCharacter

Strażnik, który przepuszcza, gdy gracz ma postać w świecie (nie na ekranie wyboru postaci).

Strona: Serwer · Czeka: nie

Sygnatura
hasCharacter(): Guard

Brak odpowiednika w Lua: sprawdź aiLib.online.character(source) ~= nil.

Zwracana wartość

Guard: odrzuca z powodem no character loaded.

Przykład

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

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

near

Strażnik, który przepuszcza, gdy ped gracza jest w odległości metres od punktu, mierzonej na serwerze (klient nie może tu skłamać).

Strona: Serwer · Czeka: nie

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

Brak odpowiednika w Lua: porównaj sam #(GetEntityCoords(GetPlayerPed(source)) - point).

Parametry

Zwracana wartość

Guard: odrzuca z powodem too far (<metres> m).

Przykład

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 serwerowe gracza, który wysłał obsługiwane zdarzenie sieciowe. Wewnątrz onClientCallback i onClientEvent dostajesz source jako pierwszy argument, więc potrzebujesz tego tylko w surowym handlerze onNet albo on z FiveM.

Strona: Serwer · Czeka: nie

Sygnatura
eventSource(): number

Brak odpowiednika w Lua: Lua czyta globalne source.

Zwracana wartość

number: id serwerowe gracza (globalne source z FiveM).

Przykład

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

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

Odczytaj je przed pierwszym await. FiveM resetuje zmienną globalną, gdy handler odda sterowanie, więc późniejszy odczyt daje złego gracza.

Pytanie klienta

triggerClientCallback

Zadaje pytanie klientowi jednego gracza i czeka na odpowiedź. Klient odpowiada przez onServerCallback(name, handler) (Lib: klient). Odpowiedzieć może tylko gracz, którego zapytano.

Strona: Serwer · Czeka: tak (Promise w TypeScript, w Lua wstrzymuje wątek)

Sygnatura
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>
Lua
local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- wywołuj wewnątrz wątku

Parametry

Prop

Type

Zwracana wartość

Promise<unknown>: rozwiązuje się odpowiedzią klienta. Odrzuca się z Error, gdy odpowiedź nie przyszła na czas albo handler klienta zawiódł. Odpowiedź pochodzi od klienta: sparsuj ją, zanim jej użyjesz.

Lua zwraca odpowiedź albo nil i 'timeout' / 'failed'.

Przykład

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

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

Akcje z czasem, którym ufa serwer

progressFor

Uruchamia pasek postępu ai-lib na ekranie gracza i czeka, aż się skończy. Używaj go dla każdej akcji z czasem, której serwer musi ufać (użycie przedmiotu, naprawa, napad): odpowiada true tylko wtedy, gdy pasek dobiegł końca na kliencie i na serwerze minęło dość czasu, więc oszust nie może go pominąć. Nie piszesz żadnego kodu klienta: ai-lib odpowiada za każdy zasób.

Strona: Serwer · Czeka: tak (Promise w TypeScript, w Lua wstrzymuje wątek)

Sygnatura
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- boolean, wywołuj wewnątrz wątku

Parametry

Prop

Type

ProgressForOptions

Prop

Type

Zwracana wartość

Promise<boolean>: true, gdy pasek się skończył. false, gdy gracz anulował, zginął, wsiadł do pojazdu (bez allowVehicle), wyszedł, zmienił postać, odpowiedział za szybko albo żadna odpowiedź nie przyszła w czasie duration plus 15 sekund.

Przykład

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");
    // Świat mógł się zmienić w czasie paska: sprawdź jeszcze raz.
    const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
    if (!vehicle) return fail("TOO_FAR");
    if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
    // Zdrowie pojazdu ustawia klient, który jest właścicielem encji.
    emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
    return { ok: true };
  },
);

Drugie progressFor dla tego samego gracza kończy pierwsze z false. Po await sprawdź ponownie odległość i zapas: świat mógł się zmienić w czasie paska. W Lua wyjście albo zmiana postaci nie są wykrywane wcześniej: wywołanie odpowiada false, gdy upłynie limit czasu (duration + 15 s).

minigameFor

Odtwarza jedną z minigier ai-lib (test umiejętności albo wytrych) na ekranie gracza dla przebiegu serwera i czeka na wynik. Używaj jej do otwierania drzwi wytrychem, łamania sejfu, odpalania na krótko. Wynik to słowo klienta, więc serwer i tak decyduje, co daje wygrana (losowanie szansy, progressFor, własne sprawdzenia).

Strona: Serwer · Czeka: tak (Promise w TypeScript, w Lua wstrzymuje wątek)

Sygnatura
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', wywołuj wewnątrz wątku

Parametry

Prop

Type

MinigameForOptions

Prop

Type

Zwracana wartość

Promise<MinigameOutcome>, gdzie MinigameOutcome = "won" | "lost" | "broken":

  • "won": gracz wygrał (i nie szybciej niż minMs).
  • "lost": gracz chybił, poddał się, wygrał za szybko albo nie odpowiedział w ciągu 120 sekund.
  • "broken": tylko wytrych, ostatni wytrych się złamał. Zabierz przedmiot wytrycha.

Przykład

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

Nigdy nie pisz własnego callbacka minigry klienta dla przebiegu serwera: używaj minigameFor. Bez minMs zmodyfikowany klient może od razu odpowiedzieć "won". 1500 wyżej to przykład: zmierz najszybszą uczciwą rozgrywkę przy swoich ustawieniach.

PendingActions

Trzyma jedną oczekującą akcję na gracza dla przebiegów, w których klient otwiera okno dialogowe, a później wysyła wynik (pisanie notatki, wybór opcji), bez paska z czasem. start wydaje token, który klient odsyła; take kończy akcję tylko wtedy, gdy token się zgadza. Wpisy są zwalniane, gdy gracz wyjdzie albo porzuci postać (zmiana postaci nigdy nie wypłaca akcji starej postaci nowej). Utwórz jedną na poziomie modułu.

Strona: Serwer · Czeka: nie

Brak odpowiednika w Lua: trzymaj tabelę Lua z tokenami na gracza.

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

Parametry konstruktora

Prop

Type

PendingActions.start

Zaczyna akcję gracza, zastępując taką, która jeszcze trwa, i zwraca jej token.

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

Prop

Type

Zwraca number: token, liczba całkowita od 1 do 2147483647. Wyślij go klientowi; odczytaj go z powrotem przez readId.

PendingActions.take

Kończy akcję i zwraca jej dane, gdy token jest tym bieżącym i minęło dość czasu.

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

Zwraca T | null: dane albo null, gdy żadna akcja nie trwa, token się nie zgadza albo odpowiedź przyszła za wcześnie. Zły token zostawia bieżącą akcję w spokoju (stary albo podrobiony token nigdy jej nie anuluje); pasujący kończy ją nawet wtedy, gdy przyszedł za wcześnie.

PendingActions.peek

Sygnatura
peek(source: number): T | null

Zwraca dane bieżącej akcji bez jej kończenia albo null.

PendingActions.cancel

Sygnatura
cancel(source: number): void

Kończy akcję gracza bez wyniku.

PendingActions.size

Sygnatura
readonly size: number

Ile akcji trwa teraz.

Przykład

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

// Krok 1: klient prosi o pisanie; serwer odpowiada tokenem.
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()] },
);

// Krok 2: klient wysyła token i tekst.
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 };
  },
);

Zaawansowane

ClientGate

Klasa za onClientCallback i onClientEvent. Potrzebujesz jej tylko wtedy, gdy budujesz własny rodzaj wejścia na surowym zdarzeniu; skrypty używają dwóch funkcji wyżej.

Strona: Serwer · Czeka: nie

Brak odpowiednika w Lua.

Sygnatura
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 zwraca sprawdzone argumenty (pustą tablicę bez args) albo null, gdy wywołanie nie może się wykonać (nie prawdziwy gracz, limit, strażnik, parser).
  • run uruchamia task pod blokadą na gracza, gdy lock jest włączone, a zamiast tego zwraca busy, gdy inne wywołanie tego gracza jeszcze trwa.
  • release zwalnia wpisy limitu i blokady gracza. Każda bramka jest zwalniana sama przy playerDropped.

Na tej stronie