Админ-панель
AdminPanel и DataFile: размещение настроек вашего скрипта в админ-панели ai-core, их чтение, реакция на правки и сохранение содержимого панели в data/*.json.
import { AdminPanel, DataFile } from "@aicore-fivem/lib/server";Каждый скрипт ai-* хранит настройки, которые подстраивает владелец сервера, в админ-панели ai-core, а не в конфиг-файле. Вы описываете данные и форму в resource/server/schema.ts (PanelSchema: файлы, вкладки, карточки, виджеты; см. Создание скрипта), создаёте из него один AdminPanel и читаете значения через него. Сторона владельца (открытие панели, редактирование, права) описана на странице Админ-панель.
Lua-аналога нет: админ-панель есть только в TypeScript.
AdminPanel
Содержимое панели вашего ресурса. Он загружает каждый data/<file>.json один раз при старте, держит его в памяти, проверяет каждую правку из панели по схеме, сохраняет файл (атомарно, серия правок записывается один раз) и сообщает об изменении вашему коду, открытым панелям и, для файлов sync: "all", каждому клиенту. Он также пишет data/schemas/<file>.schema.json, чтобы редактор мог проверить ручную правку.
Сторона: Сервер · Ждёт: нет
new AdminPanel(schema: PanelSchema)Prop
Type
Создайте ровно один на уровне модуля, обычно в resource/server/content.ts: export const panel = new AdminPanel(schema);. Файлы настроек при загрузке получают отсутствующие разделы из defaults. ai-core добавляет панель в список через тик после создания.
AdminPanel.get
Текущее содержимое файла.
get<T = Record<string, unknown>>(file: string): TВозвращает объект настроек (файл settings) или { key: record } (файл records); {} для неизвестного файла. Только для чтения: никогда не меняйте возвращённый объект, используйте commit.
const settings = panel.get<{ prices: { repair: number } }>("settings");AdminPanel.view
Файл настроек, разобранный вашей функцией чтения и обновляемый при каждом изменении (разбирается до запуска обработчиков onChange).
view<T>(file: string, read: (raw: unknown) => T): { readonly value: T }Возвращает объект, value которого всегда содержит последнее разобранное содержимое. Читайте .value каждый раз; не копируйте его один раз.
import { int, record } from "@aicore-fivem/lib/shared";
const settings = panel.view("settings", (raw) => ({
repairPrice: int(record(record(raw).prices).repair, 250),
}));
// позже: settings.value.repairPriceAdminPanel.records
Файл записей, разобранный запись за записью. Изменение заново читает только изменённую запись.
records<T>(file: string, readOne: (key: string, raw: Record<string, unknown>) => T | null): ReadonlyMap<string, T>Возвращает живой Map от ключа записи к вашему разобранному значению. Если readOne возвращает null, запись пропускается.
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
Запускает ваш обработчик после изменения файла правкой из панели или вашим собственным 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
Межполевая проверка, которая выполняется до сохранения правки. Схема проверяет отдельные значения; правило проверяет, как значения сочетаются друг с другом.
rule(file: string, rule: (key: string | undefined, next: Record<string, unknown>) => string | null): voidВозвращает (из вашего правила) null, чтобы принять, или языковой ключ, чтобы отказать (показывается администратору). Одно правило на файл; второй вызов заменяет его.
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
Выполняется, когда администратор нажимает виджет action (кнопка: отправить, сбросить, кикнуть). Для кнопки ничего не сохраняется.
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
Сохраняет изменение, которое сделал сам ваш скрипт (игровое состояние, которое показывает панель), без проверки. Файл memory хранит его только в памяти.
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 после того, как текущее изменение дошло до панели. Используйте для формы, которая сбрасывается после отправки администратором.
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: "" }); // очистить форму
return null;
});Никогда не вызывайте commit или commitAfter для того же файла изнутри его собственного обработчика onChange: commit снова вызывает onChange, и так бесконечно.
AdminPanel.editor
editor: numberСерверный id администратора, чья правка или действие сейчас применяется; доступен внутри обработчиков rule, onChange и onAction; 0 для вашего собственного commit. Задаётся только lib.
panel.onChange("settings", () => {
if (panel.editor) console.log(`settings changed by ${panel.editor}`);
});Используется ai-core
snapshot(), dataSnapshot(), summary() и apply(admin, patch) вызывает ai-core через экспорты, которые регистрирует панель (AdminPanelSnapshot, AdminPanelData, AdminPanelSummary, AdminPanelApply). Ваш скрипт их не вызывает.
Пример
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 });
});Никогда не читайте data/*.json сами и никогда не пишите в них: ими владеет панель. Данные, которые нужны миру на клиентах (места, блипы, цены, которые видят игроки), это файл sync: "all", который читается на клиенте через AdminWorld; всё остальное остаётся на сервере.
DataFile
Один файл data/<name>.json вашего ресурса с безопасной записью. AdminPanel использует его для каждого файла; используйте его напрямую только для содержимого вне панели. Сервер является единственным читателем и писателем: он читает один раз при старте и держит истину в памяти. Запись идёт во временный файл, который затем заменяет настоящий (сбой никогда не оставит половину файла), предыдущий файл сохраняется как .bak, а сломанный файл при старте заменяется на .bak.
Сторона: Сервер · Ждёт: нет
Lua-аналога нет.
new DataFile(resource: string, name: string, log: Logger)Prop
Type
DataFile.read
read(): unknownВозвращает разобранный файл без ключа $schema, .bak, если файл сломан, или null, если нет ни того, ни другого. Разберите результат перед использованием.
DataFile.write
write(value: unknown): voidСохраняет value; побеждает последний вызов. Записывается на диск через 250 мс после первого вызова серии, по порядку с более ранними записями. Неудачная запись логируется.
DataFile.schemaRef
schemaRef: string | nullЕсли задано (./schemas/<name>.schema.json), записывается как первый ключ файла "$schema" и никогда не входит в данные.
DataFile.hasSchemaRef
hasSchemaRef(): booleanВозвращает, указывает ли файл на диске свою схему.
Пример
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);
}Данные игроков никогда не попадают в файл данных: им место в базе данных (База данных). Секреты (вебхуки, API-ключи) хранятся в игнорируемом git файле data/secrets.json.
Потоковая передача областей
AreaStream: объекты мира (выброшенные предметы, растения, тела, пропы), которые отправляются только игрокам рядом с ними, одним снимком, а затем только изменениями.
База данных
createDatabaseTables устанавливает ваш sql/main.sql при старте; WriteQueue выполняет записи в базу данных одну за другой.