AI Core
API-ReferenzLib: Server

Client-Eingaben

Clients sicher antworten: onClientCallback, onClientEvent, Guards, triggerClientCallback, vom Server geprüfte Fortschrittsbalken und Minispiele, PendingActions.

Alles, was ein Client sendet, kann ein Cheater fälschen. Jeder ai-*-Server hört Clients nur über das Gate zu: onClientCallback und onClientEvent. Bevor dein Handler läuft, prüft das Gate in dieser Reihenfolge:

  1. Ein echter, verbundener Spieler hat es gesendet (nicht eine andere Ressource, die denselben Namen auslöst).
  2. Das Ratenlimit (Callbacks 20 Aufrufe pro Sekunde und Spieler, Events 10, außer du setzt rate).
  3. Deine Guards (hasCharacter(), near(...), isAdmin() oder eigene).
  4. Dein args-Parser, der die rohen Werte in geprüfte umwandelt oder den Aufruf ablehnt.
  5. Optional eine Sperre: ein Aufruf zur Zeit pro Spieler.

Eine Ablehnung mit Grund wird mit dem Spielernamen und dem Eingabenamen in die Server-Konsole geschrieben, höchstens eine Zeile pro Spieler und Eingabe alle 10 Sekunden (die Zeilen dazwischen werden in die nächste mitgezählt).

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

Die Eingabeleser (readInt, readId, readText, oneArg, asArgs) und fail sind auf Lib: Shared und fivem dokumentiert. Eine Aktion antwortet mit einem ActionResult: { ok: true, ... } oder fail("LANG_KEY").

Gate

onClientCallback

Beantwortet ein triggerServerCallback(name, ...) (oder callAction) eines Clients und schickt den Rückgabewert des Handlers zurück. Nutze es für jede Anfrage, bei der der Client auf eine Antwort wartet: kaufen, benutzen, einen Bildschirm öffnen. Es hat zwei Formen: ohne Argumente (name, handler, options?) und mit Argumenten (name, { args, ... }, handler), bei der die Optionen zuerst kommen, damit dein Handler immer nur geprüfte Werte sieht.

Seite: Server · Wartet: nein (der Handler darf async sein; sein Promise wird abgewartet, bevor die Antwort gesendet wird)

Signatur
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

Parameter

Prop

Type

ClientInputOptions

Prop

Type

ParsedInputOptions

Alle Felder von ClientInputOptions, plus:

Prop

Type

Rückgabewert

void. Die Antwort, die der Client bekommt:

  • der Rückgabewert des Handlers, wenn er gelaufen ist;
  • ein Fehlschlag, wenn das Gate den Aufruf abgelehnt hat, die Sperre belegt war oder der Handler geworfen hat. In TypeScript wird das triggerServerCallback-Promise des Clients sofort abgelehnt (callAction zeigt seinen fallback-Text an); in Lua gibt aiLib.triggerServerCallback nil, 'failed' zurück.

Beispiel

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

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

Die Lua-Version hat keine guards und keine args: Sie nimmt options = { rate?, lock?, ace? } (ace: Der Spieler braucht diese ACE-Berechtigung, ai.admin für ai-core-Admins), und der Handler bekommt die rohen Werte. Prüfe jedes Argument selbst (Typ, Bereich, Besitz, Entfernung), bevor du es nutzt. In TypeScript liest du Client-Werte nie außerhalb von args.

onClientEvent

Hört über dasselbe Gate auf ein Net-Event, das ein Client sendet. Nutze es für Einweg-Nachrichten, bei denen der Client nicht auf eine Antwort wartet (einen Bildschirm schließen, eine gemeldete Zelle). Nutze für Client-Eingaben nie ein nacktes onNet. Dieselben zwei Formen wie onClientCallback.

Seite: Server · Wartet: nein

Signatur
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? }

Parameter

Prop

Type

Rückgabewert

void.

Beispiel

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

const openShops = new PlayerMap<string>();

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

Die Lua-Version nimmt options = { rate?, ace? } (kein lock, keine guards, keine args). Ein lokales TriggerEvent aus einer anderen Ressource trägt keinen Spieler und wird auf beiden Seiten ignoriert.

Guards

Ein Guard ist eine Funktion, die die Server-ID des Spielers bekommt und mit true (bestanden), false (still ablehnen) oder einem String antwortet (ablehnen und den String als Grund loggen).

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

Schreibe einen eigenen Guard, wenn sich eine Prüfung über mehrere Eingaben wiederholt:

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

Ein Guard, der besteht, wenn der Spieler eine ACE-Berechtigung hat: die Admin-Berechtigung ai.admin von ai-core, oder die, die du angibst.

Seite: Server · Wartet: nein

Signatur
isAdmin(ace?: string): Guard
Lua
local allowed = aiLib.isAdmin(source, ace) -- boolean, eine direkte Prüfung (kein Guard)

Parameter

Prop

Type

Rückgabewert

Guard: lehnt mit dem Grund not allowed (<ace>) ab. Der Lua-Zwilling gibt direkt true oder false zurück, und false für Source 0 oder darunter.

Beispiel

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

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

hasCharacter

Ein Guard, der besteht, wenn der Spieler einen Charakter in der Welt hat (nicht auf dem Charakterbildschirm).

Seite: Server · Wartet: nein

Signatur
hasCharacter(): Guard

Kein Lua-Zwilling: Prüfe aiLib.online.character(source) ~= nil.

Rückgabewert

Guard: lehnt mit dem Grund no character loaded ab.

Beispiel

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

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

near

Ein Guard, der besteht, wenn der Ped des Spielers innerhalb von metres um einen Punkt ist, gemessen auf dem Server (der Client kann dabei nicht lügen).

Seite: Server · Wartet: nein

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

Kein Lua-Zwilling: Vergleiche #(GetEntityCoords(GetPlayerPed(source)) - point) selbst.

Parameter

Rückgabewert

Guard: lehnt mit dem Grund too far (<metres> m) ab.

Beispiel

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

Die Server-ID des Spielers, der das gerade behandelte Net-Event gesendet hat. In onClientCallback und onClientEvent bekommst du source als erstes Argument, daher brauchst du das nur in einem rohen FiveM-Handler onNet oder on.

Seite: Server · Wartet: nein

Signatur
eventSource(): number

Kein Lua-Zwilling: Lua liest die globale Variable source.

Rückgabewert

number: die Server-ID des Spielers (das globale source von FiveM).

Beispiel

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

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

Lies es vor dem ersten await. FiveM setzt die globale Variable zurück, nachdem der Handler pausiert, daher liefert ein späterer Lesezugriff den falschen Spieler.

Den Client fragen

triggerClientCallback

Stellt dem Client eines Spielers eine Frage und wartet auf die Antwort. Der Client antwortet mit onServerCallback(name, handler) (Lib: Client). Nur der gefragte Spieler kann antworten.

Seite: Server · Wartet: ja (Promise in TypeScript, hält in Lua den Thread an)

Signatur
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>
Lua
local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- innerhalb eines Threads aufrufen

Parameter

Prop

Type

Rückgabewert

Promise<unknown>: wird mit der Antwort des Clients erfüllt. Wird mit einem Error abgelehnt, wenn die Antwort nicht rechtzeitig kam oder der Client-Handler fehlgeschlagen ist. Die Antwort kommt von einem Client: Parse sie, bevor du sie nutzt.

Lua gibt die Antwort zurück, oder nil und 'timeout' / 'failed'.

Beispiel

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

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

Zeitgesteuerte Aktionen, denen der Server vertraut

progressFor

Zeigt den Fortschrittsbalken von ai-lib auf dem Bildschirm des Spielers und wartet, bis er fertig ist. Nutze es für jede zeitgesteuerte Aktion, der der Server vertrauen muss (ein Item benutzen, reparieren, ausrauben): Es antwortet nur mit true, wenn der Balken auf dem Client abgelaufen ist und auf dem Server genug Zeit vergangen ist, sodass ein Cheater ihn nicht überspringen kann. Du schreibst keinen Client-Code: ai-lib antwortet für jede Ressource.

Seite: Server · Wartet: ja (Promise in TypeScript, hält in Lua den Thread an)

Signatur
progressFor(source: number, options: ProgressForOptions): Promise<boolean>
Lua
local finished = aiLib.progressFor(source, options) -- boolean, innerhalb eines Threads aufrufen

Parameter

Prop

Type

ProgressForOptions

Prop

Type

Rückgabewert

Promise<boolean>: true, wenn der Balken fertig wurde. false, wenn der Spieler abgebrochen hat, gestorben ist, in ein Fahrzeug gestiegen ist (ohne allowVehicle), gegangen ist, den Charakter gewechselt hat, zu schnell geantwortet hat oder innerhalb von duration plus 15 Sekunden keine Antwort kam.

Beispiel

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");
    // Die Welt kann sich während des Balkens geändert haben: erneut prüfen.
    const vehicle = vehicleNearPlayer(source, netId, { metres: 4 });
    if (!vehicle) return fail("TOO_FAR");
    if (!inventory.removeItem(source, "repairkit")) return fail("NO_KIT");
    // Die Fahrzeuggesundheit wird auf dem Client gesetzt, dem die Entity gehört.
    emitNet("my-mechanic:applyRepair", NetworkGetEntityOwner(vehicle), netId);
    return { ok: true };
  },
);

Ein zweites progressFor für denselben Spieler beendet das erste mit false. Prüfe Entfernung und Bestand nach dem Await erneut: Die Welt kann sich während des Balkens geändert haben. In Lua werden ein Verbindungsabbruch oder Charakterwechsel nicht früh erkannt: Der Aufruf antwortet mit false, wenn das Timeout (duration + 15 s) abläuft.

minigameFor

Spielt eines der Minispiele von ai-lib (einen Skill-Check oder Schlossknacken) auf dem Bildschirm des Spielers für einen Server-Ablauf und wartet auf das Ergebnis. Nutze es zum Knacken eines Türschlosses, eines Tresors, zum Kurzschließen. Das Ergebnis ist das Wort des Clients, daher entscheidet der Server weiterhin, was ein Sieg bringt (ein Zufallswurf, ein progressFor, eigene Prüfungen).

Seite: Server · Wartet: ja (Promise in TypeScript, hält in Lua den Thread an)

Signatur
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>
Lua
local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', innerhalb eines Threads aufrufen

Parameter

Prop

Type

MinigameForOptions

Prop

Type

Rückgabewert

Promise<MinigameOutcome> mit MinigameOutcome = "won" | "lost" | "broken":

  • "won": Der Spieler hat gewonnen (und nicht schneller als minMs).
  • "lost": Der Spieler hat verfehlt, aufgegeben, zu schnell gewonnen oder nicht innerhalb von 120 Sekunden geantwortet.
  • "broken": nur Schlossknacken, der letzte Dietrich ist zerbrochen. Nimm das Dietrich-Item weg.

Beispiel

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

Schreibe für einen Server-Ablauf nie einen eigenen Client-Minispiel-Callback: Nutze minigameFor. Ohne minMs kann ein modifizierter Client sofort "won" antworten. Die 1500 oben sind ein Beispiel: Miss das schnellste ehrliche Spiel mit deinen Einstellungen.

PendingActions

Hält eine wartende Aktion pro Spieler für Abläufe, bei denen der Client einen Dialog öffnet und später das Ergebnis sendet (eine Notiz schreiben, eine Option wählen), ohne zeitgesteuerten Balken. start gibt ein Token aus, das der Client zurückschickt; take beendet die Aktion nur, wenn dieses Token passt. Einträge werden freigegeben, wenn der Spieler geht oder den Charakter loslässt (ein Wechsel zahlt die Aktion des alten Charakters nie an den neuen aus). Erstelle eins auf Modulebene.

Seite: Server · Wartet: nein

Kein Lua-Zwilling: Halte eine Lua-Tabelle mit Tokens pro Spieler.

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

Konstruktor-Parameter

Prop

Type

PendingActions.start

Startet die Aktion des Spielers, ersetzt eine noch laufende, und gibt ihr Token zurück.

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

Prop

Type

Rückgabewert: number: das Token, eine ganze Zahl von 1 bis 2147483647. Sende es an den Client; lies es mit readId zurück.

PendingActions.take

Beendet die Aktion und gibt ihre Daten zurück, wenn token das laufende ist und genug Zeit vergangen ist.

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

Rückgabewert: T | null: die Daten, oder null, wenn keine Aktion läuft, das Token nicht passt oder die Antwort zu früh kam. Ein falsches Token lässt die laufende Aktion in Ruhe (ein veraltetes oder gefälschtes Token bricht sie nie ab); ein passendes beendet sie, auch wenn es zu früh war.

PendingActions.peek

Signatur
peek(source: number): T | null

Rückgabewert: die Daten der laufenden Aktion, ohne sie zu beenden, oder null.

PendingActions.cancel

Signatur
cancel(source: number): void

Beendet die Aktion des Spielers ohne Ergebnis.

PendingActions.size

Signatur
readonly size: number

Wie viele Aktionen gerade laufen.

Beispiel

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

// Schritt 1: Der Client will schreiben; der Server antwortet mit einem 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()] },
);

// Schritt 2: Der Client sendet das Token und den Text.
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 };
  },
);

Fortgeschritten

ClientGate

Die Klasse hinter onClientCallback und onClientEvent. Du brauchst sie nur, wenn du deine eigene Art von Eingabe auf einem rohen Event aufbaust; Skripte nutzen die zwei Funktionen oben.

Seite: Server · Wartet: nein

Kein Lua-Zwilling.

Signatur
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 gibt die geprüften Argumente zurück (ein leeres Array ohne args) oder null, wenn der Aufruf nicht laufen darf (kein echter Spieler, Rate, Guard, Parser).
  • run führt task unter der Sperre pro Spieler aus, wenn lock an ist, und gibt stattdessen busy zurück, wenn noch ein anderer Aufruf dieses Spielers läuft.
  • release gibt die Raten- und Sperreinträge des Spielers frei. Jedes Gate wird bei playerDropped von selbst freigegeben.

Auf dieser Seite