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:
- Wysłał to prawdziwy, połączony gracz (a nie inny zasób emitujący tę samą nazwę).
- Limit częstotliwości (callbacki 20 wywołań na sekundę na gracza, zdarzenia 10, chyba że ustawisz
rate). - Twoich strażników (
hasCharacter(),near(...),isAdmin()albo własnych). - Twój parser
args, który zamienia surowe wartości w sprawdzone albo odrzuca wywołanie. - 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).
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)
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, ...): anyParametry
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
triggerServerCallbackklienta od razu się odrzuca (callActionpokazuje swój tekstfallback); w LuaaiLib.triggerServerCallbackzwracanil, '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
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, ...), 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 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
isAdmin(ace?: string): Guardlocal 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
hasCharacter(): GuardBrak 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
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): GuardBrak odpowiednika w Lua: porównaj sam #(GetEntityCoords(GetPlayerPed(source)) - point).
Parametry
Prop
Type
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
eventSource(): numberBrak 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)
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- wywołuj wewnątrz wątkuParametry
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)
progressFor(source: number, options: ProgressForOptions): Promise<boolean>local finished = aiLib.progressFor(source, options) -- boolean, wywołuj wewnątrz wątkuParametry
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)
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', wywołuj wewnątrz wątkuParametry
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.
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.
start(source: number, data: T, durationMs: number): numberProp
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.
take(source: number, token: number): T | nullZwraca 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
peek(source: number): T | nullZwraca dane bieżącej akcji bez jej kończenia albo null.
PendingActions.cancel
cancel(source: number): voidKończy akcję gracza bez wyniku.
PendingActions.size
readonly size: numberIle 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.
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
checkzwraca sprawdzone argumenty (pustą tablicę bezargs) albonull, gdy wywołanie nie może się wykonać (nie prawdziwy gracz, limit, strażnik, parser).runuruchamiataskpod blokadą na gracza, gdylockjest włączone, a zamiast tego zwracabusy, gdy inne wywołanie tego gracza jeszcze trwa.releasezwalnia wpisy limitu i blokady gracza. Każda bramka jest zwalniana sama przyplayerDropped.