Panel de administración
AdminPanel y DataFile: pon los ajustes de tu script en el panel de administración de ai-core, léelos, reacciona a las ediciones y guarda el contenido del panel en data/*.json.
import { AdminPanel, DataFile } from "@aicore-fivem/lib/server";Cada script ai-* guarda los ajustes que ajusta el dueño del servidor en el panel de administración de ai-core, no en un archivo de configuración. Describes los datos y el formulario en resource/server/schema.ts (PanelSchema: archivos, pestañas, tarjetas, widgets; consulta Crear un script), creas un AdminPanel a partir de él y lees los valores a través de él. El lado del dueño (abrir el panel, editar, permisos) está en Panel de administración.
Sin equivalente en Lua: el panel de administración es solo para TypeScript.
AdminPanel
El contenido del panel de tu recurso. Carga cada data/<file>.json una vez al arrancar, lo mantiene en memoria, comprueba cada edición del panel contra el esquema, guarda el archivo (de forma atómica, una ráfaga de ediciones se escribe una sola vez) y avisa a tu código, a los paneles abiertos y, para los archivos sync: "all", a cada cliente. También escribe data/schemas/<file>.schema.json para que un editor pueda comprobar una edición a mano.
Lado: Servidor · Espera: no
new AdminPanel(schema: PanelSchema)Prop
Type
Crea exactamente uno, a nivel de módulo, normalmente en resource/server/content.ts: export const panel = new AdminPanel(schema);. Los archivos de ajustes reciben al cargar las secciones que faltan desde defaults. ai-core muestra el panel un tick después de crearlo.
AdminPanel.get
El contenido actual de un archivo.
get<T = Record<string, unknown>>(file: string): TDevuelve el objeto de ajustes (un archivo settings) o { key: record } (un archivo records); {} para un archivo desconocido. Solo lectura: nunca cambies el objeto devuelto, usa commit.
const settings = panel.get<{ prices: { repair: number } }>("settings");AdminPanel.view
Un archivo de ajustes procesado por tu lector y mantenido al día en cada cambio (se procesa antes de que se ejecuten los handlers de onChange).
view<T>(file: string, read: (raw: unknown) => T): { readonly value: T }Devuelve un objeto cuyo value es siempre el último contenido procesado. Lee .value cada vez; no lo copies una sola vez.
import { int, record } from "@aicore-fivem/lib/shared";
const settings = panel.view("settings", (raw) => ({
repairPrice: int(record(record(raw).prices).repair, 250),
}));
// más tarde: settings.value.repairPriceAdminPanel.records
Un archivo de registros procesado registro a registro. Un cambio vuelve a leer solo el registro cambiado.
records<T>(file: string, readOne: (key: string, raw: Record<string, unknown>) => T | null): ReadonlyMap<string, T>Devuelve un mapa vivo de la clave del registro a tu valor procesado. Si readOne devuelve null, el registro queda fuera.
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
Ejecuta tu handler después de que un archivo cambió, por una edición del panel o por tu propio 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
Una comprobación entre campos que se ejecuta antes de guardar una edición. El esquema comprueba valores sueltos; una regla comprueba cómo encajan los valores entre sí.
rule(file: string, rule: (key: string | undefined, next: Record<string, unknown>) => string | null): voidDevuelve (desde tu regla) null para aceptar, o una clave de idioma para rechazar (se muestra al admin). Una regla por archivo; una segunda llamada la reemplaza.
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
Se ejecuta cuando un admin pulsa un widget action (un botón: enviar, restablecer, expulsar). No se guarda nada para el botón.
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
Guarda un cambio que hizo tu propio script (estado del juego que muestra el panel), sin validación. Un archivo memory solo lo guarda en memoria.
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 después de que el cambio actual llegó al panel. Úsalo para un formulario que se restablece solo después de que un admin lo envió.
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: "" }); // vaciar el formulario
return null;
});Nunca llames a commit ni a commitAfter para el mismo archivo desde dentro de su propio handler de onChange: el commit dispara onChange otra vez, para siempre.
AdminPanel.editor
editor: numberEl id de servidor del admin cuya edición o acción se está aplicando, legible dentro de los handlers de rule, onChange y onAction; 0 para tu propio commit. Solo lo fija la lib.
panel.onChange("settings", () => {
if (panel.editor) console.log(`settings changed by ${panel.editor}`);
});Lo usa ai-core
snapshot(), dataSnapshot(), summary() y apply(admin, patch) son lo que ai-core llama mediante los exports que registra el panel (AdminPanelSnapshot, AdminPanelData, AdminPanelSummary, AdminPanelApply). Tu script no los llama.
Ejemplo
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 });
});Nunca leas data/*.json tú mismo ni escribas en él: el panel es su dueño. Los datos que el mundo necesita en los clientes (lugares, blips, precios que ven los jugadores) son un archivo sync: "all" que se lee en el cliente con AdminWorld; todo lo demás se queda en el servidor.
DataFile
Un archivo data/<name>.json de tu recurso con escrituras seguras. AdminPanel lo usa para cada archivo; úsalo directamente solo para contenido fuera del panel. El servidor es el único que lee y escribe: lee una vez al arrancar y mantiene la verdad en memoria. Una escritura va a un archivo temporal que luego reemplaza al real (un crash nunca deja medio archivo), el archivo anterior se guarda como .bak, y un archivo dañado recurre al .bak al arrancar.
Lado: Servidor · Espera: no
Sin equivalente en Lua.
new DataFile(resource: string, name: string, log: Logger)Prop
Type
DataFile.read
read(): unknownDevuelve el archivo procesado sin su clave $schema, el .bak cuando el archivo está dañado, o null cuando no existe ninguno de los dos. Procesa el resultado antes de usarlo.
DataFile.write
write(value: unknown): voidGuarda value; gana la última llamada. Se escribe en disco 250 ms después de la primera llamada de una ráfaga, en orden con las escrituras anteriores. Una escritura fallida se registra en el log.
DataFile.schemaRef
schemaRef: string | nullCuando se fija (./schemas/<name>.schema.json), se escribe como primera clave del archivo "$schema", nunca como parte de los datos.
DataFile.hasSchemaRef
hasSchemaRef(): booleanDevuelve si el archivo en disco indica su esquema.
Ejemplo
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);
}Los datos de los jugadores nunca van a un archivo de datos: pertenecen a la base de datos (Base de datos). Los secretos (webhooks, claves de API) van en data/secrets.json, que git ignora.
Streaming por áreas
AreaStream: objetos del mundo (objetos tirados, plantas, cuerpos, props) enviados solo a los jugadores cercanos, como un snapshot y después solo los cambios.
Base de datos
createDatabaseTables instala tu sql/main.sql al arrancar; WriteQueue ejecuta las escrituras en la base de datos una tras otra.