Aller au contenu
TLRFRAMEWORK REDM Ma console

Architecture de tlr_core

tlr_core est une seule ressource RedM (fx_version 'cerulean', game 'rdr3', Lua 5.4) qui contient le cœur du framework, tous ses domaines et l'interface NUI. Retour au sommaire.

Dossiers

  • shared/ : règles pures en Lua, sans native, chargées des deux côtés et testées hors jeu (schémas, permissions, métiers, inventaire, console, licence...). C'est là que vit la logique qui doit être vérifiable.
  • server/ : un dossier par domaine (players, economy, jobs, inventory, weapons, horses, shops, bank, law, world, console...). On y trouve aussi les services du cœur : core (état global, démarrage), database (migrations), config (paramètres et réglages des systèmes), permissions, logs, modules, rpc (passerelle et actions du panel dans server/rpc/actions/).
  • client/ : un dossier par domaine côté jeu, plus client/nui (pont avec la page, focus, Échap) et client/admin (commandes /tlr et /admin).
  • web/ : la NUI en HTML, CSS et JS vanilla (modules ES, sans bundler). web/js/pages/ contient une page par section du panel, web/js/screens/ les écrans de jeu, web/js/lib/ les composants, web/css/ les feuilles (dont tokens.css et themes.css), web/console/ la console web, web/loading/ l'écran de chargement.
  • lib/ : la lib tlib (équivalent maison d'ox_lib), utilisable par d'autres ressources avec shared_script '@tlr_core/lib/init.lua'.
  • target/ : le système de cible (Alt maintenu).
  • config/ : fichiers de départ modifiables par le propriétaire du serveur (config.lua, ranch.lua, saloon.lua...). TLR ne les réécrit jamais.
  • locales/ : textes traduits (fr.lua, table TLR.Locale.fr), dont les messages des codes d'erreur ; lus avec TLR.T(code).
  • tests/ : tests hors jeu, lancés par lua tests/run.lua.
  • install/ : schéma SQL et exemple de server.cfg.
  • docs/ : documentation technique ; ce guide est dans docs/guide/.

Cycle de démarrage

Le manifeste charge server/core/boot.lua en dernier, quand tous les services sont définis. Ce fichier exécute dans l'ordre :

  1. database : attend oxmysql puis applique les migrations manquantes (TLR.Migrations.run).
  2. settings : charge les paramètres (TLR.Settings.load).
  3. permissions : groupes et membres.
  4. jobs : métiers.
  5. items : objets.
  6. weapons, world, doors, zones, blips, targets : facultatifs.

Les cinq premières étapes sont indispensables : si l'une échoue, le démarrage s'interrompt, le cœur reste non prêt et les joueurs sont refusés. Chaque étape publie son état avec TLR.SetServiceStatus(nom, 'pending' | 'ok' | 'error', détail), visible dans le tableau de bord et dans tlr_diag.

Ensuite les boucles démarrent (sauvegarde des joueurs, paie, besoins, horloge du monde), TLR.Ready passe à vrai et l'événement serveur tlr:core:ready est déclenché. Les modules s'enregistrent sur cet événement ; il est aussi déclenché après un redémarrage de tlr_core. Les joueurs déjà connectés reçoivent tlr:player:retryReady pour retrouver l'écran des personnages.

RPC : la passerelle de l'interface

Fichier : server/rpc/rpc.lua, objet TLR.RPC. Toute requête de la page NUI de tlr_core passe par l'événement réseau tlr:rpc (envoyé par client/nui/nui.lua depuis le rappel NUI rpc), puis par ces contrôles, dans l'ordre :

  1. qui : la source donnée par le serveur, jamais une donnée du client ;
  2. débit : seau de jetons par joueur (TLRConfig.RateLimit, 20 requêtes par 5 secondes par défaut) ;
  3. action connue, sinon UNKNOWN_ACTION journalisé en sécurité ;
  4. données valides selon le schéma, sinon VALIDATION_FAILED ;
  5. permission, refus par défaut, sinon PERMISSION_DENIED journalisé ;
  6. personnage chargé si l'action l'exige (NO_CHARACTER) ;
  7. exécution du gestionnaire, puis réponse tlr:rpc:result.

Déclarer une action (dans le code de tlr_core) :

local RPC, S = TLR.RPC, TLR.RPC.S

RPC.register('exemple.lire', {
    permission = 'tlr.exemple.read',
    schema = { type = 'object', fields = { id = S.id } },
    character = false,
    handler = function(ctx, data)
        if not ctx.can('tlr.exemple.read') then RPC.fail('PERMISSION_DENIED') end
        return { id = data.id, par = ctx.actor }
    end,
})

Le contexte ctx contient source, player, actor (libellé pour le journal), can(node) et, pour la console web, web = true. RPC.fail(code, détail) interrompt le gestionnaire avec une erreur propre. Schémas courants : TLR.RPC.S.empty, S.id, S.name, S.label, S.license.

Autres appels :

  • RPC.invoke(src, nom, données) : appel interne par le serveur (commandes de l'équipe), mêmes contrôles sans réseau.
  • RPC.invokeWeb(src, nom, données) : appel depuis la console web. Seules les actions protégées par un droit et sans personnage sont permises.
  • RPC.exists(nom).

Côté page : call(action, données) de web/js/rpc.js. Dans la console web, ce même module bascule en HTTP, ce qui permet de réutiliser les pages du panel telles quelles.

Codes d'erreur stables : PERMISSION_DENIED, VALIDATION_FAILED, RATE_LIMITED, NOT_FOUND, CONFLICT, UNKNOWN_ACTION, NOT_READY, NO_CHARACTER, INSUFFICIENT_FUNDS, INTERNAL.

Permissions

Fichier : server/permissions/permissions.lua, objet TLR.Permissions.

  • Une permission est un nœud en points, par exemple tlr.shops.update. Tout ce qui n'est pas accordé est refusé.
  • TLR.Permissions.declareNode(nœud, libellé, catégorie) rend le nœud visible dans la section Permissions.
  • TLR.Permissions.has(src, nœud) : vrai si le joueur a le droit (un nœud nil est toujours permis).
  • TLR.Permissions.profile(src) : { founder, level, permissions, group, groupLabel }.
  • Les groupes ont un niveau de 1 à 99. On ne gère que les groupes de niveau inférieur au sien et on n'accorde que ce qu'on possède.
  • Le fondateur est reconnu par l'ACE tlr.founder et passe au dessus de tout.
  • Les sources virtuelles de la console web (inférieures ou égales à moins 1000) ont les droits du groupe de la licence du compte.

Paramètres : TLR.Settings

Fichier : server/config/settings.lua. Toute fonctionnalité administrable déclare ses paramètres ; le panel génère leurs écrans tout seul (section Paramètres).

TLR.Settings.declare('tlr_core', 'exemple.actif', {
    type = 'boolean', default = true,
    label = 'Exemple actif', category = 'Exemple',
    description = "Active la fonction d'exemple.",
})

local actif = TLR.Settings.get('exemple.actif')
TLR.Settings.onChange('exemple.actif', function(valeur) end)
  • La clé suit le motif domaine.nom en minuscules.
  • Types : boolean, integer, number, string, enum (avec choices), money, object, array. Options : min, max, default, label, description, category, pattern, readPermission, writePermission (par défaut tlr.settings.read et tlr.settings.update), restart = true si le changement ne vaut qu'au redémarrage, check pour une règle entre plusieurs paramètres.
  • Fusion : valeur par défaut, puis config.lua, puis valeur en jeu. TLR.Settings.reset(clé, acteur) efface la valeur en jeu.
  • TLR.Settings.problems() liste les valeurs invalides du fichier.

Réglages des systèmes : TLR.Tuning

Fichier : server/config/tuning.lua. Pour des données riches (tables de recettes, lieux, prix), un système expose sa table vivante au panel :

TLR.Tuning.declare('monsysteme', {
    label = 'Mon système', category = 'Nature', help = "Ce que l'on règle ici",
    root = function() return MonSysteme.cfg end,
    keys = { 'PRIX', 'LIEUX' },
})

Chaque déclaration crée une plaque du panel (tuning_<id>) rangée par catégorie et les permissions tlr.<id>.read et tlr.<id>.update. Les valeurs modifiées sont gardées dans la table tlr_tuning et posées sur la table vivante sans redémarrage. Options : client, onChange, tools (boutons qui lancent une commande en jeu), emptyHelp. Les déclarations du cœur sont dans server/config/tuning_sections.lua.

Migrations : TLR.Migrations.add

Fichier : server/database/migrations.lua. Chaque domaine ajoute ses migrations depuis son propre fichier :

TLR.Migrations.add({ id = 900, name = 'table exemple', sql = {
    [[CREATE TABLE IF NOT EXISTS tlr_exemple (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        label VARCHAR(80) NOT NULL,
        PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4]],
} })
  • Les identifiants sont uniques : une double déclaration arrête le chargement. Les numéros sont réservés par domaine pour éviter les collisions.
  • TLR.Migrations.run() crée tlr_migrations, puis applique dans l'ordre les migrations absentes ; une requête en échec arrête le démarrage.
  • Une migration appliquée n'est jamais rejouée ni modifiée : pour changer une table, on ajoute une nouvelle migration.

Journal : TLR.Log

Fichier : server/logs/logs.lua.

TLR.Log.write('shops', ctx.actor, 'shop.update', { id = 12 }, 'medium')
  • Arguments : catégorie, auteur, action, détail (table encodée en JSON), gravité facultative (low, medium, high, déduite de la catégorie si absente).
  • Catégories : admin, economy, permissions, settings, jobs, world, players, security, error, system, weapons, appearance, storage, horses, shops, bank, bills, law, vip, christmas, crafting.
  • L'écriture n'attend pas la base. Les catégories security et error sont aussi affichées dans la console. Aucune ligne ne s'efface depuis le jeu.
  • TLR.Log.recent(limite, catégorie, gravité, recherche) sert la section Journaux.

Pour aller plus loin

Source : tlr_core/docs/guide/dev_architecture.md