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 :
- ton fichier
Variables.luadéclare la variable (type, valeur par défaut, bornes, description) ; - au démarrage, le Socle publie ces déclarations en base ; le panel affiche le formulaire ;
- le fondateur change la valeur dans le panel ; elle est enregistrée dans
socle_variablespuis appliquée à chaud ; - ton code la lit au moment où il s'en sert (
Core.Variable:getOr) ou réagit au changement (hookvariable: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ètre | Rôle |
|---|---|
nom | identifiant technique, en minuscules, utilisé dans tout le code ("prime") |
libellé | titre affiché dans le panel |
parent | catégorie parente pour ranger l'écran sous une autre (ex. "admin"), ou nil |
icône | nom 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 :
definese fait côté serveur, dans un fichier demodules/<nom>/server/, au chargement du fichier (pas dans unCreateThread). 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
| Type | Valeur en Lua | Dans le panel |
|---|---|---|
boolean | true / false | interrupteur |
number | nombre | champ numérique avec min, max, step, unit |
string | texte | champ texte (avec sélecteur d'objet ou de média selon le nom, voir plus bas) |
choice | une chaîne parmi choices | liste 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 |
list | tableau 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
| Option | Types | Rôle |
|---|---|---|
description | tous | texte 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, max | number | bornes, appliquées par le serveur |
step | number | pas du champ dans le panel |
unit | number | unité affichée ("m", "s", "ms", "$", "%", "j"…) |
choices | choice | liste des valeurs possibles, ex. { "money", "bank" } |
fields | list | colonnes d'une ligne : { name, label, type, choices? } |
private | tous | true : la valeur ne part jamais chez les joueurs (voir plus bas) |
runtime | tous | false : 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 Item | sélecteur d'objet du catalogue ox_inventory |
contient image, logo, banner, photo, avatar, thumb, picture, background | choix d'une image de la médiathèque |
contient music, sound, audio, song | choix d'un son |
contient video | choix d'une vidéo |
url ou finissant par url | choix 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:getOrrenvoie 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 :
- dans ta déclaration, option
description: c'est ce que tu dois faire pour tes modules ; - 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
endRécapitulatif
| Tu veux… | Utilise |
|---|---|
| déclarer un écran de réglages | Core.Variable:defineCategory(nom, libellé, parent, icône) |
| déclarer un réglage | Core.Variable:define(cat, clé, libellé, type, défaut, options) (serveur) |
| lire un réglage | Core.Variable:getOr(cat, clé, défaut) (client et serveur) |
| attendre les réglages côté client | Core.Variable:get(cat, clé) dans un thread |
| lire dans une boucle chaude | Core.Variable:watch(cat, clé, défaut) (client) |
| lire depuis une autre ressource | exports.socle:getVariable(cat, clé) |
| réagir à un changement | Core.Hook.listen("variable:changed", fn) |
| cacher une valeur aux joueurs | option private = true |