Admin-Panel
AdminPanel und DataFile: die Einstellungen deines Skripts ins Admin-Panel von ai-core bringen, sie lesen, auf Änderungen reagieren und Panel-Inhalte in data/*.json speichern.
import { AdminPanel, DataFile } from "@aicore-fivem/lib/server";Jedes ai-*-Skript hält die Einstellungen, die ein Serverbetreiber anpasst, im Admin-Panel von ai-core, nicht in einer Konfigurationsdatei. Du beschreibst die Daten und das Formular in resource/server/schema.ts (PanelSchema: Dateien, Tabs, Karten, Widgets; siehe Ein Skript erstellen), erstellst daraus ein AdminPanel und liest die Werte darüber. Die Seite des Betreibers (Panel öffnen, bearbeiten, Berechtigungen) steht unter Admin-Panel.
Kein Lua-Zwilling: Das Admin-Panel gibt es nur in TypeScript.
AdminPanel
Die Panel-Inhalte deiner Ressource. Es lädt jede data/<file>.json einmal beim Start, hält sie im Speicher, prüft jede Panel-Änderung gegen das Schema, speichert die Datei (atomar, eine Reihe schneller Änderungen wird einmal geschrieben) und informiert deinen Code, die offenen Panels und, bei Dateien mit sync: "all", jeden Client. Es schreibt außerdem data/schemas/<file>.schema.json, damit ein Editor eine Handbearbeitung prüfen kann.
Seite: Server · Wartet: nein
new AdminPanel(schema: PanelSchema)Prop
Type
Erstelle genau eins, auf Modulebene, meist in resource/server/content.ts: export const panel = new AdminPanel(schema);. Settings-Dateien bekommen fehlende Abschnitte beim Laden aus defaults. ai-core listet das Panel einen Tick nach der Erstellung.
AdminPanel.get
Der aktuelle Inhalt einer Datei.
get<T = Record<string, unknown>>(file: string): TRückgabewert: das Settings-Objekt (eine settings-Datei) oder { key: record } (eine records-Datei); {} für eine unbekannte Datei. Nur lesen: Ändere das zurückgegebene Objekt nie, nutze commit.
const settings = panel.get<{ prices: { repair: number } }>("settings");AdminPanel.view
Eine Settings-Datei, von deinem Reader geparst und bei jeder Änderung aktuell gehalten (geparst, bevor onChange-Handler laufen).
view<T>(file: string, read: (raw: unknown) => T): { readonly value: T }Rückgabewert: ein Objekt, dessen value immer der neueste geparste Inhalt ist. Lies .value jedes Mal; kopiere es nicht einmalig heraus.
import { int, record } from "@aicore-fivem/lib/shared";
const settings = panel.view("settings", (raw) => ({
repairPrice: int(record(record(raw).prices).repair, 250),
}));
// später: settings.value.repairPriceAdminPanel.records
Eine Records-Datei, Eintrag für Eintrag geparst. Eine Änderung liest nur den geänderten Eintrag neu.
records<T>(file: string, readOne: (key: string, raw: Record<string, unknown>) => T | null): ReadonlyMap<string, T>Rückgabewert: eine Live-Map vom Eintragsschlüssel zu deinem geparsten Wert. Gibt readOne null zurück, wird ein Eintrag ausgelassen.
const shops = panel.records("shops", (key, raw) =>
typeof raw.label === "string" ? { key, label: raw.label } : null,
);
for (const shop of shops.values()) console.log(shop.label);AdminPanel.onChange
Führt deinen Handler aus, nachdem sich eine Datei geändert hat, durch eine Panel-Änderung oder durch dein eigenes commit.
onChange(file: string, handler: (key: string | undefined, value: unknown) => void): voidProp
Type
panel.onChange("shops", (key, value) => {
if (value === null) console.log(`shop ${key} deleted`);
});AdminPanel.rule
Eine feldübergreifende Prüfung, die läuft, bevor eine Änderung gespeichert wird. Das Schema prüft einzelne Werte; eine Regel prüft, wie Werte zusammenpassen.
rule(file: string, rule: (key: string | undefined, next: Record<string, unknown>) => string | null): voidRückgabewert (aus deiner Regel): null zum Akzeptieren, oder ein Sprachschlüssel zum Ablehnen (wird dem Admin angezeigt). Eine Regel pro Datei; ein zweiter Aufruf ersetzt sie.
panel.rule("settings", (_key, next) => {
const prices = next.prices as { min: number; max: number };
return prices.min > prices.max ? "ERR_MIN_ABOVE_MAX" : null;
});AdminPanel.onAction
Läuft, wenn ein Admin ein action-Widget drückt (einen Button: senden, zurücksetzen, kicken). Für den Button wird nichts gespeichert.
onAction(id: string, handler: (admin: number, record: Record<string, unknown>, key: string | undefined) => string | null): voidProp
Type
import { notify } from "@aicore-fivem/lib/server";
panel.onAction("testAlert", (admin) => {
notify(admin, "Test alert sent", "success");
return null;
});AdminPanel.commit
Speichert eine Änderung, die dein Skript selbst gemacht hat (Spielzustand, den das Panel anzeigt), ohne Validierung. Eine memory-Datei hält sie nur im Speicher.
commit(file: string, key: string | undefined, value: unknown): voidProp
Type
panel.commit("vaults", "bank_1", { ...panel.get<Record<string, object>>("vaults").bank_1, robbedAt: Date.now() });AdminPanel.commitAfter
commit, nachdem die aktuelle Änderung das Panel erreicht hat. Nutze es für ein Formular, das sich selbst zurücksetzt, nachdem ein Admin es abgeschickt hat.
commitAfter(file: string, key: string | undefined, value: unknown): voidpanel.onAction("sendBroadcast", (admin, form) => {
if (typeof form.message !== "string" || form.message === "") return "ERR_EMPTY_MESSAGE";
for (const player of getPlayers()) notify(Number(player), form.message, "info", 10_000);
panel.commitAfter("broadcast", undefined, { message: "" }); // das Formular leeren
return null;
});Rufe commit oder commitAfter für dieselbe Datei nie aus ihrem eigenen onChange-Handler auf: Der Commit löst onChange erneut aus, endlos.
AdminPanel.editor
editor: numberDie Server-ID des Admins, dessen Änderung oder Aktion gerade angewendet wird, lesbar in rule-, onChange- und onAction-Handlern; 0 für dein eigenes commit. Wird nur von der Lib gesetzt.
panel.onChange("settings", () => {
if (panel.editor) console.log(`settings changed by ${panel.editor}`);
});Von ai-core genutzt
snapshot(), dataSnapshot(), summary() und apply(admin, patch) ruft ai-core über die Exports auf, die das Panel registriert (AdminPanelSnapshot, AdminPanelData, AdminPanelSummary, AdminPanelApply). Dein Skript ruft sie nicht auf.
Beispiel
import { AdminPanel, aiLog } from "@aicore-fivem/lib/server";
import { int, record } from "@aicore-fivem/lib/shared";
import { schema } from "./schema";
export const panel = new AdminPanel(schema);
export const settings = panel.view("settings", (raw) => ({
repairPrice: int(record(record(raw).prices).repair, 250),
}));
panel.onChange("settings", () => {
aiLog("admin", `repair price is now ${settings.value.repairPrice}`, { source: panel.editor || undefined });
});Lies data/*.json nie selbst und schreibe nie hinein: Das Panel besitzt sie. Daten, die die Welt auf Clients braucht (Orte, Blips, Preise, die Spieler sehen), sind eine Datei mit sync: "all", die auf dem Client mit AdminWorld gelesen wird; alles andere bleibt auf dem Server.
DataFile
Eine data/<name>.json-Datei deiner Ressource mit sicheren Schreibvorgängen. AdminPanel nutzt sie für jede Datei; nutze sie direkt nur für Inhalte außerhalb des Panels. Der Server ist der einzige Leser und Schreiber: Er liest einmal beim Start und hält die Wahrheit im Speicher. Ein Schreibvorgang geht in eine temporäre Datei, die dann die echte ersetzt (ein Absturz hinterlässt nie eine halbe Datei), die vorherige Datei wird als .bak behalten, und eine kaputte Datei fällt beim Start auf die .bak zurück.
Seite: Server · Wartet: nein
Kein Lua-Zwilling.
new DataFile(resource: string, name: string, log: Logger)Prop
Type
DataFile.read
read(): unknownRückgabewert: die geparste Datei ohne ihren $schema-Schlüssel, die .bak, wenn die Datei kaputt ist, oder null, wenn keine von beiden existiert. Parse das Ergebnis, bevor du es nutzt.
DataFile.write
write(value: unknown): voidSpeichert value; der letzte Aufruf gewinnt. Es wird 250 ms nach dem ersten Aufruf einer Reihe auf die Platte geschrieben, in Reihenfolge mit früheren Schreibvorgängen. Ein fehlgeschlagener Schreibvorgang wird geloggt.
DataFile.schemaRef
schemaRef: string | nullWenn gesetzt (./schemas/<name>.schema.json), wird es als erster Schlüssel "$schema" der Datei geschrieben, nie als Teil der Daten.
DataFile.hasSchemaRef
hasSchemaRef(): booleanRückgabewert: ob die Datei auf der Platte ihr Schema nennt.
Beispiel
import { DataFile } from "@aicore-fivem/lib/server";
import { createLogger, isRecord } from "@aicore-fivem/lib/shared";
const resource = GetCurrentResourceName();
const file = new DataFile(resource, "routes", createLogger(resource));
const raw = file.read();
const routes: Record<string, unknown> = isRecord(raw) ? raw : {};
function saveRoute(id: string, route: unknown) {
routes[id] = route;
file.write(routes);
}Spielerdaten kommen nie in eine Datendatei: Sie gehören in die Datenbank (Datenbank). Geheimnisse (Webhooks, API-Schlüssel) kommen in die von git ignorierte data/secrets.json.
Bereichs-Streaming
AreaStream: Weltobjekte (Drops, Pflanzen, Körper, Props), die nur an die Spieler in ihrer Nähe gesendet werden, als ein Snapshot und danach nur die Änderungen.
Datenbank
createDatabaseTables installiert beim Start deine sql/main.sql; WriteQueue führt Datenbank-Schreibvorgänge nacheinander aus.