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:
- Ein echter, verbundener Spieler hat es gesendet (nicht eine andere Ressource, die denselben Namen auslöst).
- Das Ratenlimit (Callbacks 20 Aufrufe pro Sekunde und Spieler, Events 10, außer du setzt
rate). - Deine Guards (
hasCharacter(),near(...),isAdmin()oder eigene). - Dein
args-Parser, der die rohen Werte in geprüfte umwandelt oder den Aufruf ablehnt. - 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).
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)
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, ...): anyParameter
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 (callActionzeigt seinenfallback-Text an); in Lua gibtaiLib.triggerServerCallbacknil, '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
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? }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 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
isAdmin(ace?: string): Guardlocal 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
hasCharacter(): GuardKein 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
near(point: Vec3 | ((source: number) => Vec3 | null), metres: number): GuardKein Lua-Zwilling: Vergleiche #(GetEntityCoords(GetPlayerPed(source)) - point) selbst.
Parameter
Prop
Type
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
eventSource(): numberKein 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)
triggerClientCallback(name: string, source: number, args?: unknown[], timeoutMs?: number): Promise<unknown>local result, err = aiLib.triggerClientCallback(name, source, args, timeout) -- innerhalb eines Threads aufrufenParameter
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)
progressFor(source: number, options: ProgressForOptions): Promise<boolean>local finished = aiLib.progressFor(source, options) -- boolean, innerhalb eines Threads aufrufenParameter
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)
minigameFor(source: number, options: MinigameForOptions): Promise<MinigameOutcome>local outcome = aiLib.minigameFor(source, options) -- 'won' | 'lost' | 'broken', innerhalb eines Threads aufrufenParameter
Prop
Type
MinigameForOptions
Prop
Type
Rückgabewert
Promise<MinigameOutcome> mit MinigameOutcome = "won" | "lost" | "broken":
"won": Der Spieler hat gewonnen (und nicht schneller alsminMs)."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.
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.
start(source: number, data: T, durationMs: number): numberProp
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.
take(source: number, token: number): T | nullRü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
peek(source: number): T | nullRückgabewert: die Daten der laufenden Aktion, ohne sie zu beenden, oder null.
PendingActions.cancel
cancel(source: number): voidBeendet die Aktion des Spielers ohne Ergebnis.
PendingActions.size
readonly size: numberWie 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.
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
checkgibt die geprüften Argumente zurück (ein leeres Array ohneargs) odernull, wenn der Aufruf nicht laufen darf (kein echter Spieler, Rate, Guard, Parser).runführttaskunter der Sperre pro Spieler aus, wennlockan ist, und gibt stattdessenbusyzurück, wenn noch ein anderer Aufruf dieses Spielers läuft.releasegibt die Raten- und Sperreinträge des Spielers frei. Jedes Gate wird beiplayerDroppedvon selbst freigegeben.