5MCustoms

Core-API

5mc_core verwaltet Spieler und Charaktere und vermittelt zwischen den Modulen. Alles andere baut darauf auf.

Exports

Server-seitig stehen diese Exports bereit:

exports['5mc_core'].getCharacter(source);
exports['5mc_core'].isLoaded(source);

exports['5mc_core'].getMetadata(source, key);
exports['5mc_core'].setMetadata(source, key, value);

exports['5mc_core'].setJob(source, name, grade, onDuty);

exports['5mc_core'].addMoney(source, account, amount);
exports['5mc_core'].removeMoney(source, account, amount);

exports['5mc_core'].getLoadedPlayers();

getCharacter liefert den geladenen Charakter oder null. Prüfe immer auf null: ein Spieler kann verbunden sein, ohne einen Charakter gewählt zu haben.

Provider-Registry

Module sollen sich nicht gegenseitig direkt aufrufen. Stattdessen meldet ein Modul eine Umsetzung beim Core an, und andere fragen sie dort ab. Fehlt das Modul, bekommen sie null statt eines Absturzes.

// Anbieten — im Modul, das die Fähigkeit mitbringt
exports['5mc_core'].provide('dispatch', meineLeitstelle);

// Abfragen — sofort, kann null sein
const inventory = exports['5mc_core'].getProvider('inventory');

// Abfragen — wartet, bis das Modul bereit ist (Standard 15 Sekunden)
const dispatch = await requireProvider('dispatch');
if (dispatch) {
  dispatch.report({
    code: '10-53',
    title: 'Fahrzeugaufbruch',
    position,
    jobs: ['police'],
  });
}

Bekannte Provider sind inventory, dispatch, medical und admin. Ein eigenes Modul kann die Liste über Declaration Merging erweitern.

Netz-Events

Das SDK nimmt Events nur mit Schema an. Jeder Payload vom Client wird auf dem Server geprüft, bevor der Handler läuft. Ein Client, der etwas anderes schickt, ist entweder ein Fehler oder ein Betrugsversuch — beides soll nicht durchkommen.

import { onNet, emitClient } from '@5mc/sdk';
import { z } from 'zod';

onNet(
  'meinmodul:aktion',
  z.object({ slot: z.number().int().min(1).max(50) }),
  (source, data) => {
    // data.slot ist hier garantiert eine ganze Zahl zwischen 1 und 50
  },
  { rateLimit: 5 },
);

Alle Events laufen unter dem Namensraum 5mc:*. Ohne eigene Angabe gilt ein Ratenlimit von zehn Aufrufen pro Sekunde und Spieler.

Callbacks

Wenn der Client eine Antwort braucht:

// Server
onCallback('meinmodul:liste', z.object({}), async (source) => {
  return { eintraege: await ladeEintraege(source) };
});

// Client
const antwort = await callback<{ eintraege: Eintrag[] }>('meinmodul:liste');

Der Client-Aufruf bricht nach zehn Sekunden ab, statt hängen zu bleiben.

Datenbank

import { query, single, insert, update, transaction } from '@5mc/sdk/db';

const zeilen = await query<Fahrzeug>('SELECT * FROM vehicles WHERE owner = ?', [id]);
const eine = await single<Fahrzeug>('SELECT * FROM vehicles WHERE plate = ?', [plate]);
const neueId = await insert('INSERT INTO vehicles (owner, plate) VALUES (?, ?)', [id, plate]);
const betroffen = await update('UPDATE vehicles SET fuel = ? WHERE id = ?', [fuel, id]);

await transaction(async (tx) => {
  // alles oder nichts
});

Der Datenbankteil liegt bewusst unter @5mc/sdk/db statt im Hauptpaket: er zieht den Treiber mit und gehört nur in Server-Einstiegspunkte.

Audit

Alles, was ein Team-Mitglied an Spielerdaten ändert, sollte eine Spur hinterlassen:

import { audit } from '@5mc/sdk';

audit({
  actor: source,
  action: 'setmoney',
  target,
  detail: `${account}: ${vorher} -> ${nachher}`,
  meta: { account, from: vorher, to: nachher },
});

Läuft ein Moderationsmodul, landet der Eintrag dort. Ohne eines geht er in die Konsole, verschwindet also nicht stillschweigend.