Documentation · Développeurs

Réglages côté code

Déclarer et lire les réglages du panel : types, options, lecture à chaud.

SommaireRéglages côté code

Le principe

Dans Socle, rien de réglable n'est écrit en dur. Une distance, un prix, une touche par défaut, une position, une liste d'armes autorisées : tout est une variable déclarée par le code et modifiée depuis le panel. Le panel génère ses formulaires à partir de ces déclarations : tu n'écris jamais de formulaire.

Le circuit complet :

  1. ton fichier Variables.lua déclare la variable (type, valeur par défaut, bornes, description) ;
  2. au démarrage, le Socle publie ces déclarations en base ; le panel affiche le formulaire ;
  3. le fondateur change la valeur dans le panel ; elle est enregistrée dans socle_variables puis appliquée à chaud ;
  4. ton code la lit au moment où il s'en sert (Core.Variable:getOr) ou réagit au changement (hook variable:changed).

Déclarer une catégorie

Les variables sont rangées par catégorie : une catégorie = un écran de réglages dans le panel. Déclare-la côté serveur, en haut de ton Variables.lua :

-- Core.Variable:defineCategory(nom, libellé, parent, icône)
Core.Variable:defineCategory("prime", "Prime de minuit", nil, "money")
ParamètreRôle
nomidentifiant technique, en minuscules, utilisé dans tout le code ("prime")
libellétitre affiché dans le panel
parentcatégorie parente pour ranger l'écran sous une autre (ex. "admin"), ou nil
icônenom d'icône du panel : money, car, user, heart, map, pin, star, music, wrench, badge, bank, wallet, tablet, target, info… Sans icône, le panel affiche un point neutre

Déclarer une variable

-- Core.Variable:define(catégorie, clé, libellé, type, défaut, options)
Core.Variable:define("prime", "montant", "Montant de la prime", "number", 500, {
    unit = "$", min = 0, max = 100000, step = 50,
    description = "Somme versée au joueur qui réclame la prime. Vérifiée et versée par le serveur.",
})

Règles à connaître :

  • define se fait côté serveur, dans un fichier de modules/<nom>/server/, au chargement du fichier (pas dans un CreateThread). Les valeurs enregistrées en base sont chargées après les migrations : une variable déclarée plus tard ne récupérerait pas sa valeur sauvegardée.
  • Déclarer deux fois la même clé ne fait rien : la première déclaration gagne.
  • La clé est l'identité de la variable. Ne la renomme pas une fois en production : l'ancienne valeur enregistrée serait perdue (le panel repartirait du défaut).

Les types

TypeValeur en LuaDans le panel
booleantrue / falseinterrupteur
numbernombrechamp numérique avec min, max, step, unit
stringtextechamp texte (avec sélecteur d'objet ou de média selon le nom, voir plus bas)
choiceune chaîne parmi choicesliste déroulante
color{ r, g, b } (0 à 255)sélecteur de couleur
vector{ x, y, z } ou { x, y, z, cap }trois ou quatre champs, Copier, Coller, Placer en jeu
listtableau de lignes { { champ = valeur, ... }, ... }tableau éditable, une colonne par champ de fields

Côté serveur, number est converti et borné par min / max à chaque écriture, boolean accepte aussi "true" / "1", string est converti en texte. Les autres types sont enregistrés tels quels : c'est à ton code de vérifier ce qu'il lit (une liste peut être vide, une ligne peut avoir un champ manquant).

Toutes les options

OptionTypesRôle
descriptiontoustexte affiché sous le libellé dans le panel. Dis ce que la valeur fait et ce qu'on risque à la changer ; ne répète pas le libellé
min, maxnumberbornes, appliquées par le serveur
stepnumberpas du champ dans le panel
unitnumberunité affichée ("m", "s", "ms", "$", "%", "j"…)
choiceschoiceliste des valeurs possibles, ex. { "money", "bank" }
fieldslistcolonnes d'une ligne : { name, label, type, choices? }
privatetoustrue : la valeur ne part jamais chez les joueurs (voir plus bas)
runtimetousfalse : le panel affiche « Pris en compte au prochain redémarrage du serveur ». À utiliser quand ton code ne lit la valeur qu'une fois (touche par défaut d'un raccourci, commande enregistrée au démarrage)

Exemples de chaque type

local C = "prime"

Core.Variable:define(C, "enabled", "Prime activée", "boolean", true, {
    description = "Coupe ou active la prime de minuit pour tout le serveur.",
})

Core.Variable:define(C, "compte", "Compte crédité", "choice", "money", {
    choices = { "money", "bank" },
    description = "money = argent liquide, bank = compte en banque.",
})

Core.Variable:define(C, "couleurMarqueur", "Couleur du marqueur", "color", { 114, 99, 238 }, {
    description = "Couleur du marqueur posé au sol devant le guichet.",
})

-- quatre composantes : la quatrième est le cap (orientation du PNJ)
Core.Variable:define(C, "guichet", "Position du guichet", "vector", { 215.8, -810.1, 30.7, 160.0 }, {
    description = "Où se tient le PNJ qui verse la prime. Utilise « Placer en jeu » pour la poser.",
})

Core.Variable:define(C, "bonusMetiers", "Bonus par métier", "list", {
    { job = "police", multiplicateur = 1.5 },
    { job = "ambulance", multiplicateur = 1.5 },
}, {
    fields = {
        { name = "job", label = "Métier", type = "string" },
        { name = "multiplicateur", label = "Multiplicateur", type = "number" },
    },
    description = "Les joueurs de ces métiers touchent la prime multipliée.",
})

Les champs d'une liste

Chaque entrée de fields décrit une colonne : name (clé dans la ligne), label (titre de colonne), type. Types de colonne gérés par le panel : string, number, boolean, choice (avec choices), vector, color. Une colonne vector a son propre bouton Placer en jeu.

Le panel choisit aussi un éditeur d'après le nom d'un champ texte (variable string ou colonne de liste) :

Nom du champÉditeur proposé
item, tool, reward, ou finissant par Itemsélecteur d'objet du catalogue ox_inventory
contient image, logo, banner, photo, avatar, thumb, picture, backgroundchoix d'une image de la médiathèque
contient music, sound, audio, songchoix d'un son
contient videochoix d'une vidéo
url ou finissant par urlchoix de n'importe quel média

Nomme donc tes champs en conséquence : rewardItem plutôt que objet, bannerUrl plutôt que lien.

Les positions et « Placer en jeu »

Une variable vector (ou une colonne vector d'une liste) s'enregistre sous forme de tableau : { x, y, z }, ou { x, y, z, cap } si la valeur par défaut a quatre composantes. Le panel affiche alors un quatrième champ « Cap ».

Placer en jeu : un joueur connecté au serveur se place à l'endroit voulu et appuie sur E (Retour arrière pour annuler). S'il y a plusieurs joueurs en jeu, le panel propose de choisir lequel, celui qui a le même compte Discord que la personne du panel en premier (voir Placer une position en jeu) ; la position est lue par le serveur (pas envoyée par le client), arrondie au centième, puis remplie dans le formulaire. Il reste à enregistrer.

Côté code, convertis toujours le tableau en vector3 et méfie-toi d'une valeur incomplète :

-- tableau { x, y, z[, cap] } → vector3 + cap ; nil si la valeur est invalide
local function lirePosition(valeur)
    if type(valeur) ~= "table" or not tonumber(valeur[1]) or not tonumber(valeur[2]) or not tonumber(valeur[3]) then
        return nil
    end
    return vector3(valeur[1] + 0.0, valeur[2] + 0.0, valeur[3] + 0.0), (tonumber(valeur[4]) or 0.0) + 0.0
end

local pos, cap = lirePosition(Core.Variable:getOr("prime", "guichet", nil))

Le bouton d'aperçu du panel affiche en jeu, pendant un temps limité, toutes les positions d'une catégorie (variables vector et colonnes vector des listes).

Les valeurs privées

Core.Variable:define("prime", "webhookUrl", "Webhook Discord des primes", "string", "", {
    private = true,
    description = "Salon Discord où chaque prime versée est signalée. Reste sur le serveur.",
})

Une variable private :

  • n'est jamais envoyée aux clients de jeu (ni au chargement, ni lors d'un changement) ;
  • reste lisible côté serveur avec Core.Variable:get / getOr ;
  • côté client, Core.Variable:getOr renvoie simplement la valeur par défaut que tu passes ;
  • n'est visible dans le menu staff en jeu qu'avec la permission admin.variables.private.

Rends privé tout ce qu'un joueur ne doit pas voir : adresses de webhook, mentions de rôle, réponses d'un examen, seuils anti-triche.

Lire une valeur

Côté serveur

-- valeur brute (nil si la variable n'existe pas)
local montant = Core.Variable:get("prime", "montant")

-- avec repli : la forme à utiliser partout
local montant = tonumber(Core.Variable:getOr("prime", "montant", 500)) or 500

-- déclaration complète (type, bornes, description…)
local entree = Core.Variable:getEntry("prime", "montant")

Côté client

-- non bloquant : renvoie le défaut tant que les variables ne sont pas arrivées
local actif = Core.Variable:getOr("prime", "enabled", true)

-- BLOQUANT : attend l'arrivée des variables (à utiliser dans un thread)
CreateThread(function()
    local guichet = Core.Variable:get("prime", "guichet")
end)

-- lecteur mis en cache pour une boucle chaude, relu seulement après un changement
local distance = Core.Variable:watch("prime", "distance", 2.0)
print(distance())

Core.Variable:version() renvoie un compteur incrémenté à chaque chargement ou changement : pratique pour invalider un cache calculé à partir de plusieurs variables (c'est ce que fait le portage pour sa liste d'armes).

Depuis une autre ressource

-- client ou serveur ; côté client l'appel attend l'arrivée des variables
local montant = exports.socle:getVariable("prime", "montant")

-- couleur du serveur
local rgb = exports.socle:getAccent()      -- { r, g, b }
local hex = exports.socle:getAccentHex()   -- "#7263ee"

Réagir à un changement

Chaque écriture depuis le panel déclenche le hook local variable:changed avec (catégorie, clé, valeur), côté serveur et côté clients (sauf pour une variable privée, qui ne déclenche que le serveur).

-- côté client : reconstruire un point d'interaction quand sa position change
Core.Hook.listen("variable:changed", function(categorie, cle, valeur)
    if categorie ~= "prime" then return end
    if cle == "guichet" or cle == "enabled" or cle == "distance" then
        reconstruirePoint()
    end
end)

Le hook ne se déclenche pas au premier chargement des variables côté client : pour l'état initial, lis les valeurs avec Core.Variable:get dans un thread.

Pour les ressources hors Socle, deux events clients locaux suivent l'apparence : socle:accentChanged (r, g, b) et socle:inventorySkinChanged (nom du style). Voir la référence API.

Les descriptions

Une variable sans description apparaît nue dans le panel, et la console le signale au démarrage (variable sans description : prime.montant). Deux façons d'en donner une :

  1. dans ta déclaration, option description : c'est ce que tu dois faire pour tes modules ;
  2. dans le catalogue central system/variables/server/Descriptions.lua, clé "catégorie.clé". Ce catalogue appartient au Socle et sera remplacé par les mises à jour : n'y ajoute rien.

Si les deux existent, le catalogue central l'emporte. Une clé du catalogue sans variable correspondante est signalée en jaune (description orpheline).

Exemple complet : la prime de minuit

Le fichier de réglages, modules/prime/server/Variables.lua :

-- Réglages de la prime de minuit (exemple de documentation).
-- Toute valeur réglable vit ici ; le reste du module ne contient aucune constante.

local C = "prime"
Core.Variable:defineCategory(C, "Prime de minuit", nil, "money")

local function define(cle, libelle, type, defaut, options)
    Core.Variable:define(C, cle, libelle, type, defaut, options)
end

define("enabled", "Prime activée", "boolean", true, {
    description = "Coupe ou active la prime. Coupée : le guichet disparaît pour tout le monde.",
})
define("guichet", "Position du guichet", "vector", { 215.8, -810.1, 30.7, 160.0 }, {
    description = "Emplacement et orientation du PNJ qui verse la prime. À poser avec « Placer en jeu ».",
})
define("pedModel", "Modèle du PNJ", "string", "a_m_y_business_02", {
    description = "Modèle GTA du PNJ du guichet. Un modèle invalide n'affiche pas de PNJ, le point reste utilisable.",
})
define("distance", "Distance d'interaction", "number", 2.0, {
    unit = "m", min = 1, max = 5, step = 0.5,
    description = "Distance à laquelle la touche apparaît. Le serveur revérifie la distance à la demande.",
})
define("heureDebut", "Heure d'ouverture", "number", 0, {
    unit = "h", min = 0, max = 23, step = 1,
    description = "Heure (du serveur) à partir de laquelle la prime peut être réclamée.",
})
define("heureFin", "Heure de fermeture", "number", 1, {
    unit = "h", min = 1, max = 24, step = 1,
    description = "Heure (du serveur) à partir de laquelle la prime n'est plus versée.",
})
define("montant", "Montant de la prime", "number", 500, {
    unit = "$", min = 0, max = 100000, step = 50,
    description = "Somme versée une fois par jour et par personnage.",
})
define("compte", "Compte crédité", "choice", "money", {
    choices = { "money", "bank" },
    description = "money = argent liquide, bank = compte en banque.",
})
define("bonusMetiers", "Bonus par métier", "list", {
    { job = "police", multiplicateur = 1.5 },
}, {
    fields = {
        { name = "job", label = "Métier", type = "string" },
        { name = "multiplicateur", label = "Multiplicateur", type = "number" },
    },
    description = "Les joueurs de ces métiers touchent la prime multipliée. Multiplicateur borné entre 1 et 5 par le serveur.",
})
define("alerteMontant", "Seuil d'alerte", "number", 2000, {
    unit = "$", min = 0, max = 1000000, step = 100,
    private = true,
    description = "Une prime versée au-dessus de ce montant est journalisée en niveau « warning ».",
})

Et la lecture côté serveur, toujours au moment du calcul :

local function var(cle, defaut) return Core.Variable:getOr("prime", cle, defaut) end

local function montantPour(src)
    local base = math.floor(tonumber(var("montant", 500)) or 500)
    local xPlayer = Core.getPlayer(src)
    local job = xPlayer and xPlayer.job and xPlayer.job.name
    for _, ligne in ipairs(var("bonusMetiers", {}) or {}) do
        if type(ligne) == "table" and ligne.job == job then
            -- la ligne vient du panel : on borne quand même
            return math.floor(base * Core.clamp(tonumber(ligne.multiplicateur) or 1, 1, 5))
        end
    end
    return base
end

Récapitulatif

Tu veux…Utilise
déclarer un écran de réglagesCore.Variable:defineCategory(nom, libellé, parent, icône)
déclarer un réglageCore.Variable:define(cat, clé, libellé, type, défaut, options) (serveur)
lire un réglageCore.Variable:getOr(cat, clé, défaut) (client et serveur)
attendre les réglages côté clientCore.Variable:get(cat, clé) dans un thread
lire dans une boucle chaudeCore.Variable:watch(cat, clé, défaut) (client)
lire depuis une autre ressourceexports.socle:getVariable(cat, clé)
réagir à un changementCore.Hook.listen("variable:changed", fn)
cacher une valeur aux joueursoption private = true