AI Core
API-ReferenzLib: Server

Spieler und Charaktere

online, onCharacter, notify, Spieler in der Nähe, Positionen und Speicher pro Spieler: PlayerMap, Cooldowns, KeyedCooldowns, SourceLock, RateLimit, KeyWatchers.

TypeScript
import { online, onCharacter, notify, playersNear, coordsOf, PlayerMap, Cooldowns } from "@aicore-fivem/lib/server";

Ein Spieler ist ein verbundener Client mit einer Server-ID (source). Ein Charakter ist die Person, die er spielt; ein Spieler auf dem Charakterbildschirm hat keinen Charakter geladen. ai-core besitzt Spieler und Charaktere; diese Helfer lesen sie.

online

online ist die Kopie deiner Ressource von den geladenen Spielern in ai-core: Charaktere, Job- und Gang-Mitgliedschaften mit Rängen, Mitgliederlisten pro Job und Gang und Dienst. Es liest ai-core einmal beim ersten Gebrauch und folgt danach den Events von ai-core, daher ist ein Lesezugriff ein Nachschlagen im Speicher. Nutze es in heißen Pfaden (jede Chatnachricht, jeder Alarm), statt die Exports von ai-core aufzurufen.

In Lua fragt aiLib.online bei jedem Aufruf die Exports von ai-core: in Ordnung für Aktionen, nicht für jeden Tick. Es hat character, name, jobs, gangs, jobGrade, gangGrade, dutyJob und sources; der Rest hat keinen Lua-Zwilling.

online.sources

Die Server-IDs aller Spieler mit geladenem Charakter.

Seite: Server · Wartet: nein

Signatur
online.sources(): IterableIterator<number>
Lua
local list = aiLib.online.sources() -- integer[]

Rückgabewert

IterableIterator<number> (Lua: ein Array). Spieler auf dem Charakterbildschirm sind nicht enthalten.

Beispiel

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

for (const source of online.sources()) notify(source, "Server restart in 5 minutes", "warning", 10_000);

online.size

Wie viele Spieler einen geladenen Charakter haben.

Seite: Server · Wartet: nein

Signatur
readonly online.size: number

Kein Lua-Zwilling: #aiLib.online.sources().

online.character

Der geladene Charakter eines Spielers.

Seite: Server · Wartet: nein

Signatur
online.character(source: number): CharacterInfo | null
Lua
local character = aiLib.online.character(source) -- table oder nil

Prop

Type

Rückgabewert

CharacterInfo (id, citizenid, slot, firstname, lastname, birthdate, sex, nationality), oder null (Lua nil), wenn der Spieler keinen Charakter geladen hat.

Beispiel

const character = online.character(source);
if (character) console.log(`${character.citizenid} is ${character.firstname}`);

online.charId

Die ID des geladenen Charakters des Spielers (die Datenbank-ID, stabil über Reconnects hinweg).

Seite: Server · Wartet: nein

Signatur
online.charId(source: number): number | undefined

Kein Lua-Zwilling: (aiLib.online.character(source) or {}).id.

Rückgabewert: die Charakter-ID, oder undefined, wenn keiner geladen ist. Verknüpfe Daten, die einen Reconnect überleben müssen (Cooldowns, Besitz), mit dieser ID, nicht mit source.

const charId = online.charId(source);
if (charId === undefined) return fail("NO_CHARACTER");

online.name

"Vorname Nachname" des Charakters, oder der FiveM-Spielername, wenn kein Charakter geladen ist.

Seite: Server · Wartet: nein

Signatur
online.name(source: number, options?: { fallback?: boolean }): string
Lua
local name = aiLib.online.name(source, fallback) -- fallback: boolean?

Prop

Type

Rückgabewert: string.

notify(target, `${online.name(source)} sent you a gift`, "success");

online.jobs

Jeder Job, in dem der Spieler Mitglied ist, im Dienst oder nicht. ai-core erlaubt einem Charakter mehrere Jobs (maxJobs im Panel) mit jeweils einem Job im Dienst.

Seite: Server · Wartet: nein

Signatur
online.jobs(source: number): readonly string[]
Lua
local memberships = aiLib.online.jobs(source) -- { { job = 'police', grade = 2 }, ... }

Rückgabewert: die Jobnamen (leer, wenn keiner oder kein Charakter). Der Lua-Zwilling gibt Mitgliedschaftstabellen { job, grade } zurück, keine Namen.

const isMechanic = online.jobs(source).includes("mechanic");

online.gangs

Jede Gang, in der der Spieler ist.

Seite: Server · Wartet: nein

Signatur
online.gangs(source: number): readonly string[]
Lua
local memberships = aiLib.online.gangs(source) -- { { gang = 'ballas', grade = 0 }, ... }

Rückgabewert: die Gang-Namen (leer, wenn keine). Der Lua-Zwilling gibt Mitgliedschaftstabellen { gang, grade } zurück.

if (online.gangs(source).length === 0) return fail("NOT_IN_GANG");

online.jobGrade

Der Rang des Spielers in einem Job.

Seite: Server · Wartet: nein

Signatur
online.jobGrade(source: number, job: string): number | null
Lua
local grade = aiLib.online.jobGrade(source, job) -- integer oder nil

Prop

Type

Rückgabewert: die Rangnummer, oder null (Lua nil), wenn der Spieler kein Mitglied ist. Rang 0 ist ein Mitglied: Vergleiche mit null, nicht mit einer Truthy-Prüfung.

const grade = online.jobGrade(source, "police");
if (grade === null || grade < 3) return fail("RANK_TOO_LOW");

online.gangGrade

Der Rang des Spielers in einer Gang.

Seite: Server · Wartet: nein

Signatur
online.gangGrade(source: number, gang: string): number | null
Lua
local grade = aiLib.online.gangGrade(source, gang) -- integer oder nil

Rückgabewert: der Rang, oder null (Lua nil), wenn kein Mitglied.

const boss = (online.gangGrade(source, "ballas") ?? -1) >= 4;

online.jobMembers

Jeder Spieler mit einer Mitgliedschaft in einem Job, im Dienst oder nicht.

Seite: Server · Wartet: nein

Signatur
online.jobMembers(job: string): ReadonlySet<number>

Kein Lua-Zwilling.

Rückgabewert: ein Live-Set von Server-IDs (leer, wenn keine). Es ändert sich, wenn Spieler dem Job beitreten oder ihn verlassen: Kopiere es ([...set]) vor einem await.

for (const source of online.jobMembers("mechanic")) notify(source, "A customer is waiting", "info");

online.gangMembers

Jeder Spieler in einer Gang.

Seite: Server · Wartet: nein

Signatur
online.gangMembers(gang: string): ReadonlySet<number>

Kein Lua-Zwilling. Rückgabewert: ein Live-Set von Server-IDs; kopiere es vor einem await.

const members = [...online.gangMembers("ballas")];

online.dutyJob

Der Job, für den der Spieler im Dienst ist.

Seite: Server · Wartet: nein

Signatur
online.dutyJob(source: number): string | null
Lua
local job = aiLib.online.dutyJob(source) -- string oder nil

Rückgabewert: der Jobname, oder null (Lua nil), wenn außer Dienst.

if (online.dutyJob(source) !== "ambulance") return fail("NOT_ON_DUTY");

online.onDutyIn

Ob der Spieler für einen von mehreren Jobs im Dienst ist (alle Polizei-Jobs, alle EMS-Jobs).

Seite: Server · Wartet: nein

Signatur
online.onDutyIn(source: number, jobs: readonly string[]): boolean

Kein Lua-Zwilling. Rückgabewert: true, wenn der Job im Dienst in jobs ist.

const POLICE = ["police", "sheriff"];
if (!online.onDutyIn(source, POLICE)) return fail("NOT_POLICE");

online.onDutyFor

Die Spieler, die für einen Job im Dienst sind.

Seite: Server · Wartet: nein

Signatur
online.onDutyFor(job: string): ReadonlySet<number>

Kein Lua-Zwilling. Rückgabewert: ein Live-Set von Server-IDs; kopiere es vor einem await.

for (const medic of online.onDutyFor("ambulance")) notify(medic, "Patient at Pillbox", "info");

online.countOnDuty

Wie viele Spieler über mehrere Jobs hinweg im Dienst sind.

Seite: Server · Wartet: nein

Signatur
online.countOnDuty(jobs: readonly string[]): number

Kein Lua-Zwilling. Rückgabewert: die Gesamtzahl (die Kosten wachsen mit der Anzahl der Jobs, nicht der Spieler).

if (online.countOnDuty(["police", "sheriff"]) < 2) return fail("NOT_ENOUGH_POLICE");

online.allows

Ob der Spieler einen Ort nutzen darf, der durch eine Zugriffsregel gesperrt ist (Jobs, Gangs, Mindestränge, im Dienst). allows auf dem Client nutzt dieselbe Regel, sodass ein Spieler genau das sieht, was der Server ihn nutzen lässt.

Seite: Server · Wartet: nein

Signatur
online.allows(source: number, rule: AccessRule): boolean

Kein Lua-Zwilling.

Prop

Type

Rückgabewert: true, wenn die Listen leer sind, der Spieler in einer der gangs mit deren Mindestrang ist oder in einem der jobs mit dessen Mindestrang (und dafür im Dienst, wenn onDuty gesetzt ist).

import { online } from "@aicore-fivem/lib/server";
import { readAccess, fail } from "@aicore-fivem/lib/shared";

const rule = readAccess(stashRecord); // { jobs: ["police"], gangs: [], grades: { police: 2 }, onDuty: true }
if (!online.allows(source, rule)) return fail("NO_ACCESS");

Lebenszyklus eines Charakters

onCharacter

Führt deinen Code aus, wenn ein Charakter die Welt betritt und wenn er sie verlässt. Es spielt außerdem die Charaktere erneut ab, die schon in der Welt sind, wenn deine Ressource (neu) startet, und deckt Verbindungsabbrüche ab, daher brauchst du daneben keinen playerDropped-Handler.

Seite: Server · Wartet: nein

Signatur
onCharacter(handlers: CharacterHandlers): void
Lua
aiLib.onCharacter({ loaded = function(source, character) end, unloaded = function(source, charId, reason) end })

CharacterHandlers

Prop

Type

Rückgabewert

void.

Beispiel

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

const playtime = new PlayerMap<{ charId: number; since: number }>();

onCharacter({
  loaded(source, character) {
    playtime.set(source, { charId: character.id, since: Date.now() });
  },
  unloaded(source, charId, reason) {
    const entry = playtime.get(source);
    if (entry) console.log(`character ${charId} played ${Date.now() - entry.since} ms (${reason})`);
    playtime.delete(source);
  },
});

Rufe es einmal auf oberster Ebene einer Server-Datei auf. Die PlayerMap oben leert sich beim Verlassen selbst; der unloaded-Handler läuft bei einem Verbindungsabbruch trotzdem zuerst, sodass du dort Daten speichern kannst.

Mit Spielern sprechen

notify

Zeigt eine Benachrichtigungskarte auf dem Bildschirm eines Spielers (ai-lib zeichnet sie, oder die Benachrichtigungs-UI, die der Betreiber im Panel von ai-core gewählt hat).

Seite: Server · Wartet: nein

Signatur
notify(source: number, text: string, kind?: NotificationKind, duration?: number): void
Lua
aiLib.notify(source, text, kind, duration)

Parameter

Prop

Type

Rückgabewert

void.

Beispiel

import { notify, createLocale } from "@aicore-fivem/lib/server";

const Locale = createLocale();
notify(source, Locale("PAID", 250), "success");

Um eine Client-Aktion zu beantworten, gib fail("KEY") zurück, statt notify aufzurufen: callAction auf dem Client zeigt den übersetzten Fehler selbst an.

isOnline

Ob ein Spieler noch verbunden ist. Prüfe es nach einem langen await, bevor du etwas gibst.

Seite: Server · Wartet: nein

Signatur
isOnline(source: number): boolean

Kein Lua-Zwilling: GetPlayerName(source) ~= nil.

Rückgabewert: true, solange der Spieler verbunden ist.

await new Promise((resolve) => setTimeout(resolve, 60_000));
if (!isOnline(source)) return;

Spieler in der Nähe

Diese fragen ai-core, welche Spieler im OneSync-Bereich der anderen sind (etwa 424 Meter), und messen nur diese wenigen, statt jeden Spieler auf dem Server durchzugehen. Ein Radius größer als etwa 424 m findet niemanden weiter weg.

playersNear

Die Spieler innerhalb von radius Metern um einen Spieler.

Seite: Server · Wartet: nein

Signatur
playersNear(source: number, radius: number): number[]
Lua
local list = aiLib.playersNear(source, radius) -- integer[]

Prop

Type

Rückgabewert: ein Array von Server-IDs. TypeScript enthält source selbst; Lua lässt es weg.

for (const other of playersNear(source, 20)) if (other !== source) notify(other, "You hear a scream", "warning");

playersNearEntity

Die Spieler innerhalb von radius Metern um eine vernetzte Entity (ein Fahrzeug, ein Prop): ihr Besitzer und die Spieler im Bereich des Besitzers.

Seite: Server · Wartet: nein

Signatur
playersNearEntity(entity: number, radius: number): number[]
Lua
local list = aiLib.playersNearEntity(entity, radius) -- integer[]

Prop

Type

Rückgabewert: ein Array von Server-IDs. Leer, wenn die Entity nicht existiert oder kein Spieler sie besitzt (eine Entity, die nur der Server besitzt, hat niemanden in ihrer Nähe).

const witnesses = playersNearEntity(vehicle, 30);

nearestPlayerTo

Der nächstgelegene andere Spieler innerhalb von metres um einen Spieler, der deinen Filter besteht.

Seite: Server · Wartet: nein

Signatur
nearestPlayerTo(source: number, metres: number, filter?: (other: number) => boolean): number | null
Lua
local other = aiLib.nearestPlayerTo(source, metres, filter) -- integer oder nil

Prop

Type

Rückgabewert: die Server-ID des nächsten Treffers, oder null (Lua nil), wenn keiner.

const target = nearestPlayerTo(source, 2, (other) => online.character(other) !== null);
if (target === null) return fail("NOBODY_NEAR");

Positionen

Keine Lua-Zwillinge: Lua hat Vektor-Mathematik (GetEntityCoords(GetPlayerPed(source)), #(a - b)).

coordsOf

Die Ped-Position eines Spielers, wie der Server sie sieht.

Seite: Server · Wartet: nein

Signatur
coordsOf(source: number): Vec3

Rückgabewert: ein Vec3 { x, y, z }.

const { x, y, z } = coordsOf(source);

entityCoords

Die Position einer beliebigen Entity (die Server-Kopie des Syncs ihres Besitzers).

Seite: Server · Wartet: nein

Signatur
entityCoords(entity: number): Vec3

Rückgabewert: ein Vec3.

import { distance } from "@aicore-fivem/lib/shared";
if (distance(entityCoords(vehicle), coordsOf(source)) > 5) return fail("TOO_FAR");

isNear

Ob der Ped des Spielers innerhalb von metres um einen Punkt ist. Der Guard near nutzt es.

Seite: Server · Wartet: nein

Signatur
isNear(source: number, point: Vec3, metres: number): boolean

Prop

Type

Rückgabewert: true, wenn in Reichweite.

if (!isNear(source, { x: 25.7, y: -1347.3, z: 29.5 }, 3)) return fail("TOO_FAR");

vehicleOf

Das Fahrzeug, in dem der Spieler sitzt.

Seite: Server · Wartet: nein

Signatur
vehicleOf(source: number, options?: { driver?: boolean }): number

Prop

Type

Rückgabewert: die Fahrzeug-Entity, oder 0 zu Fuß (oder nicht auf dem Fahrersitz bei driver: true).

const vehicle = vehicleOf(source, { driver: true });
if (!vehicle) return fail("NOT_DRIVING");

Speicher pro Spieler

Keiner davon hat einen Lua-Zwilling: Nutze eine Lua-Tabelle und leere sie in playerDropped. Erstelle jeden Speicher einmal auf Modulebene, nie in einem Handler.

PlayerMap

Eine Map mit der Server-ID des Spielers als Schlüssel, die sich selbst leert, wenn der Spieler geht. Nutze sie für alles, was du pro Spieler hältst (ein offener Bildschirm, ein ausgewähltes Item).

Seite: Server · Wartet: nein

Signatur
new PlayerMap<V>(options?: { onDrop?: (source: number, value: V) => void })

Prop

Type

Sie hat jede Methode einer JavaScript-Map (get, set, has, delete, size, Iteration).

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

const carrying = new PlayerMap<string>({
  onDrop: (source, item) => console.log(`${source} left while carrying ${item}`),
});
carrying.set(source, "box");

Cooldowns

Cooldowns pro Spieler auf der Spieluhr, freigegeben, wenn der Spieler geht.

Seite: Server · Wartet: nein

Signatur
new Cooldowns()
ready(source: number): boolean
start(source: number, ms: number): void
take(source: number, ms: number): boolean
left(source: number): number
clear(source: number): void
MethodeWas sie tutRückgabewert
ready(source)Für den Spieler läuft kein Cooldown.boolean
start(source, ms)Startet (oder startet neu) den Cooldown des Spielers für ms Millisekunden.void
take(source, ms)Startet den Cooldown, wenn er bereit ist.true, wenn gestartet, false, solange noch einer läuft
left(source)Verbleibende Millisekunden.number, 0, wenn bereit
clear(source)Beendet den Cooldown des Spielers.void
import { onClientCallback, Cooldowns } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const searchCooldown = new Cooldowns();

onClientCallback("my-bins:search", (source) => {
  if (!searchCooldown.take(source, 30_000)) return fail("WAIT");
  return { ok: true };
});

Ein Spieler kann sich neu verbinden, um einen Cooldowns-Eintrag zurückzusetzen. Ein Cooldown, der einen Reconnect überleben muss, nutzt KeyedCooldowns mit online.charId(source) als Schlüssel.

KeyedCooldowns

Cooldowns nach einem beliebigen Schlüssel, der eine Verbindung überdauert: eine Charakter-ID, eine Entity, eine Tür-ID. Abgelaufene Einträge werden aufgeräumt, wenn die Map wächst.

Seite: Server · Wartet: nein

Signatur
new KeyedCooldowns<K = number>()
ready(key: K): boolean
start(key: K, ms: number): void
take(key: K, ms: number): boolean
left(key: K): number
clear(key: K): void

Dieselben Methoden wie Cooldowns, nach key. start mit ms von 0 oder weniger beendet den Cooldown.

import { KeyedCooldowns, online } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const robbed = new KeyedCooldowns<string>(); // nach Laden-ID
const perCharacter = new KeyedCooldowns(); // nach Charakter-ID

function canRob(source: number, store: string) {
  const charId = online.charId(source);
  if (charId === undefined) return fail("NO_CHARACTER");
  if (!robbed.ready(store)) return fail("STORE_EMPTY");
  if (!perCharacter.take(charId, 15 * 60_000)) return fail("WAIT");
  robbed.start(store, 30 * 60_000);
  return { ok: true };
}

SourceLock

Eine laufende Aktion pro Spieler über mehrere Callbacks hinweg. Das eigene lock: true eines Callbacks deckt nur diesen Callback ab; zwei Aktionen, die sich gegenseitig ausschließen müssen (dasselbe Fahrzeug anbieten und verkaufen), teilen sich ein SourceLock.

Seite: Server · Wartet: nein

Signatur
new SourceLock()
run<T, B>(source: number, busy: B, task: () => Promise<T>): Promise<T | B>
release(source: number): void

Prop

Type

Rückgabewert (run): das Ergebnis der Aufgabe, oder busy. release(source) gibt den Spieler frei; rufe es aus playerDropped auf (eine noch laufende Aufgabe behält ihren eigenen Eintrag, bis sie abgeschlossen ist).

import { onClientCallback, SourceLock } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const dealer = new SourceLock();
on("playerDropped", () => dealer.release(Number(globalThis.source)));

onClientCallback("my-dealer:list", (source) => dealer.run(source, fail("BUSY"), async () => ({ ok: true })));
onClientCallback("my-dealer:sell", (source) => dealer.run(source, fail("BUSY"), async () => ({ ok: true })));

RateLimit

Ein Burst-Limit pro Spieler: höchstens count Aktionen innerhalb eines beliebigen windowMs. Das Gate begrenzt bereits jede Eingabe; nutze dies für ein zusätzliches, langsameres Limit für eine Aktion.

Seite: Server · Wartet: nein

Signatur
new RateLimit(count: number, windowMs: number)
take(source: number): boolean
release(source: number): void
Lua
local limit = aiLib.RateLimit.new(count, windowMs)
local allowed = limit:take(source)
limit:release(source)

Prop

Type

Rückgabewert (take): true und zählt eine Aktion, oder false, wenn der Spieler über dem Limit ist (nicht gezählt). release(source) vergisst den Spieler; rufe es aus playerDropped auf.

import { onClientCallback, RateLimit } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const reports = new RateLimit(3, 60_000);
on("playerDropped", () => reports.release(Number(globalThis.source)));

onClientCallback("my-report:send", (source) => {
  if (!reports.take(source)) return fail("TOO_MANY_REPORTS");
  return { ok: true };
});

KeyWatchers

Die Spieler, die Live-Änderungen eines Schlüssels empfangen (die Autos eines Parkplatzes, ein offenes Charakterblatt). Ein Schlüssel pro Spieler: Einen neuen Schlüssel zu beobachten beendet den alten. Beim Verlassen freigegeben; jede Operation läuft in konstanter Zeit.

Seite: Server · Wartet: nein

Signatur
new KeyWatchers<K>()
watch(source: number, key: K): void
stop(source: number): void
of(key: K): ReadonlySet<number> | undefined
keyOf(source: number): K | undefined
MethodeWas sie tutRückgabewert
watch(source, key)Der Spieler beobachtet key und hört auf, den vorherigen zu beobachten.void
stop(source)Der Spieler hört auf zu beobachten.void
of(key)Die Spieler, die key beobachten.ein Set, oder undefined, wenn niemand beobachtet
keyOf(source)Der Schlüssel, den der Spieler beobachtet.der Schlüssel, oder undefined
import { onClientCallback, onClientEvent, KeyWatchers } from "@aicore-fivem/lib/server";
import { oneArg, readText } from "@aicore-fivem/lib/shared";

const lots = new KeyWatchers<string>();

onClientCallback("my-lot:open", { args: oneArg((v) => readText(v, 32)) }, (source, lot) => {
  lots.watch(source, lot);
  return { ok: true, cars: [] }; // der Snapshot
});
onClientEvent("my-lot:close", (source) => lots.stop(source));

function carChanged(lot: string, car: unknown) {
  for (const source of lots.of(lot) ?? []) emitNet("my-lot:car", source, car); // das Delta
}

Auf dieser Seite