À qui s'adresse cette section
Cette section est pour le développeur de ton serveur : la personne qui écrit du Lua, ajoute un module ou branche une ressource sur Socle. Si tu cherches à régler le serveur sans toucher au code, tout se fait depuis le panel : commence plutôt par Réglages en direct.
Les pages développeurs :
- Architecture (cette page) : les ressources, l'arborescence, le démarrage et les règles du jeu ;
- Les réglages côté code : déclarer une variable du panel et la lire ;
- Référence API : callbacks, events, hooks, points d'interaction, logs, permissions, exports ;
- Créer ton module : un tutoriel complet, du dossier à la checklist ;
- L'interface NUI : ajouter une vue React à l'interface unique ;
- Panel en ligne, agent et licence : ce qui relie ton serveur au panel hébergé.
Les ressources du serveur
La liste exacte et l'ordre de démarrage sont dans server_resources.cfg. Les ressources principales :
| Ressource | Rôle | Tu peux la modifier ? |
|---|---|---|
oxmysql | Accès à la base de données (MariaDB / MySQL) | Non, livrée telle quelle |
ox_lib | Bibliothèque utilitaire. Retouchée : ses écrans (saisie, confirmation, skillcheck, menus, texte d'aide) sont affichés par l'interface Socle | Non |
ox_inventory | Inventaire. Retouché : couleur du serveur, habillages (hud.inventorySkin), points d'interaction des boutiques | Non |
ox_target | Ciblage (œil). Retouché : suit la couleur du serveur | Non |
es_extended | Framework ESX. ESX.ShowNotification est redirigée vers les notifications Socle | Non |
socle | Le cœur : variables du panel, interface NUI unique, permissions, logs, licence, et la plupart des modules de jeu | Oui, dans tes propres dossiers de module |
core | Contenu historique : métiers (police, EMS, mécano…), activités, monde. Utilise les menus RageUI fournis par socle | Avec prudence |
ltd, radio, banking, cardealer, order-system, interactions, territory… | Ressources de contenu livrées avec la base | Avec prudence |
Les retouches faites dans ox_lib, ox_inventory et ox_target sont marquées [Socle] dans leur code : elles ne servent qu'à s'afficher dans le style du serveur. Ne les modifie pas toi-même : une mise à jour de Socle les remplace, et ces bibliothèques sont sous licence libre copyleft : on n'y ajoute pas de code de serveur. Ton code se branche sur elles par leurs exports, depuis ton module.
socle démarre après oxmysql, ox_lib, es_extended et ox_inventory (ses dependencies), et avant core, qui charge @socle/bridges/rageui/client/RageUI.lua.
L'arborescence de socle
resources/socle/
fxmanifest.lua
init.lua ← démarre Core quand ESX est prêt
core/ ← l'objet Core, la console, les exports de base
main.lua ← Core, registerClass, resolveClass, onReady
api.lua ← exports getCoreObject, getVariable, getAccent…
console.lua ← Console:write(niveau, catégorie, message)
client/ server/ ← Core.nui, Core.Player, Core.clamp, Core.isFinite…
handlers/ ← Callback, Event (signés), Hook, Keybind
system/ ← Variable (réglages du panel), Command, Menu, Focus,
Blip, entités
modules/<nom>/ ← un dossier par fonctionnalité de jeu
shared/ server/ client/
bridges/ ← pont avec le panel (HTTP local), RageUI
addons/ ← carburant, kevlar, écran de chargement
utils/ ← Utils.Math, Utils.Table, Utils.Zone…
configs/ ← Config.lang, Config.debug, réglages fixes historiques
data/ ← JSON livrés (emotes, blips, tatouages…)
i18n/ ← Translate.lua + langs/fr.json, en.json, de.json, es.json
ui/ ← l'interface NUI unique (React + Vite) ; dist/ = build
migrations/ ← création et évolution des tables SQL
assets/ ← images, sons, logos servis en nui://socle/assets/…Ton code va dans `modules/<ton_module>/` : ce dossier n'existe pas dans les paquets de mise à jour, il n'est donc jamais touché (voir Mises à jour).
L'ordre de chargement
Le manifeste charge les fichiers par motifs (globs). Côté serveur :
server_scripts {
'core/api.lua',
'@oxmysql/lib/MySQL.lua',
'core/server/**',
'utils/**/server/**',
'esx/server.lua',
'handlers/**/server/**',
-- le catalogue des descriptions AVANT les variables
'system/variables/server/Descriptions.lua',
'system/**/server/**',
'modules/**/server/**',
'addons/**/server/**',
'bridges/**/server/**',
'migrations/*.lua', -- en dernier
}Côté client, même logique : core/client/**, utils, esx/client.lua, handlers, system, bridges, puis modules/**/client/** et addons. Les fichiers partagés (shared_scripts) passent avant tout le reste : core/console.lua, core/main.lua, i18n/Translate.lua, configs/**, puis les dossiers shared ; init.lua est chargé en tout dernier.
Ce que ça implique pour toi :
- n'importe quel fichier de `modules/<nom>/server/` ou `client/` est chargé automatiquement, sans toucher au manifeste ;
- quand ton fichier s'exécute,
Core,Core.Event,Core.Callback,Core.Hook,Core.Variable,Core.Commandexistent déjà ; - ne compte pas sur l'ordre entre deux fichiers d'un même motif. Si tu as besoin d'une valeur, lis-la au moment où tu t'en sers, pas au chargement du fichier ;
- les migrations s'exécutent après les modules : une table créée par une migration n'existe pas encore au chargement de ton module (attends
Core.migrationsDone, voir plus bas).
Core, l'objet global
Core est une table globale partagée par tous les fichiers de la ressource socle.
| Membre | Côté | Rôle |
|---|---|---|
Core.server | les deux | true côté serveur |
Core.resourceName | les deux | "socle" |
Core.ready | les deux | true une fois le démarrage terminé |
Core.onReady(fn) | les deux | appelle fn dès que Core est prêt (tout de suite s'il l'est déjà) |
Core.registerClass(nom, classe) | les deux | enregistre une classe ; appelle classe.onInit() tout de suite, classe.onStop() à l'arrêt de la ressource |
Core.resolveClass(nom) | les deux | attend qu'une classe soit enregistrée et renvoie Core[nom] |
Core.t(clé, vars) | les deux | texte traduit (voir Traductions) |
Core.log(niveau, tag, message, ...) | serveur | écrit dans la console (string.format si arguments) |
Core.isFinite(v), Core.clamp(v, min, max), Core.round(v, déc) | serveur | validation des nombres reçus d'un client |
Core.jsonDecode(texte, défaut), Core.jsonEncode(v) | serveur | JSON sans erreur fatale |
Core.getPlayer(src), Core.getIdentifier(src), Core.getPlayerName(src) | serveur | joueur ESX, identifiant, nom du personnage |
Core.ESX | serveur | l'objet ESX partagé |
Core.nui(action, data), Core.rnui(nom, fn) | client | messages vers l'interface, retours de l'interface |
Core.Player.ped, .coords, .vehicle, .serverId | client | infos du joueur local (ped rafraîchi chaque seconde) |
Un module qui expose des fonctions à ses autres fichiers les range dans une table de Core :
-- modules/monmodule/server/Etat.lua
Core.MonModule = Core.MonModule or {}
function Core.MonModule.estOccupe(src)
return false
end
-- facultatif : onInit / onStop
Core.registerClass("MonModule", {
onStop = function()
-- nettoyage à l'arrêt de la ressource
end,
})Et un autre fichier qui en dépend attend qu'elle existe :
-- bloque (avec Wait) jusqu'à l'enregistrement de la classe, puis renvoie Core.Hook
local Hook = Core.resolveClass("Hook")Depuis une autre ressource, passe par les exports (exports.socle:getVariable(...), exports.socle:Notify(...)…). exports.socle:getCoreObject() (alias getSharedObject) renvoie bien Core, mais chaque appel de fonction à travers un export coûte cher : réserve-le aux appels ponctuels.
Le cycle de démarrage
Côté serveur
migrations/schema.luacrée les tables manquantes et ajoute les colonnes manquantes, puis metCore.migrationsDone = true. Les autres fichiers demigrations/attendent ce drapeau pour créer leurs propres tables.- Les variables du panel sont chargées depuis la table
socle_variables(les valeurs par défaut du code servent tant qu'aucune valeur n'est enregistrée). init.luaattend qu'ESX soit disponible, puis appelleCore.Ready(). Côté serveur,Core.Readyattend en plus la fin des migrations (60 s au plus, puis démarrage forcé avec une erreur en console).- Les fonctions passées à
Core.onReadys'exécutent,Core.readypasse àtrue. - Cinq secondes après les migrations, le schéma des variables est publié en base pour que le panel génère ses formulaires.
Si des tables ou des colonnes ont été créées, la console affiche un grand avertissement « SERVER RESTART STRONGLY RECOMMENDED » : redémarre une fois, proprement.
Côté client
init.luaattend ESX, puisCore.Ready().- Le client demande ses variables au serveur (
core:variables:server:request) ; seules les valeurs non privées lui sont envoyées. - L'interface NUI se charge ;
Core.nuiattend son accusé (ui:load) avant d'envoyer quoi que ce soit.
Les convars lues par socle
| Convar | Où | Rôle |
|---|---|---|
setr network_token <entier> | server.cfg | Signature des events client vers serveur (voir plus bas). setr : le client doit la lire |
set panel_token "…" | server.cfg | Protège le pont HTTP entre le panel et le jeu. set, jamais setr |
set socle_license_key "SOCLE-…" | server.cfg | Clé de licence Socle (voir Licence) |
maintenance_backup_keep | server.cfg | Nombre de sauvegardes bak_ gardées par table lors d'une maintenance (5 par défaut) |
panel_preview_seconds | server.cfg | Durée de l'aperçu des positions en jeu (60 s par défaut) |
configs/_main.lua porte encore Config.lang (langue des textes : fr, en, de ou es) et Config.debug (affiche les messages debug de la console).
Les règles de performance
Objectif : chaque ressource sous 0,02 ms au repos dans resmon. Les règles qui suivent ne sont pas du style : ce sont elles qui gardent le serveur fluide à 100 joueurs.
Pas de boucle par image permanente
Mauvais : une boucle qui tourne à chaque image pour tout le monde, tout le temps.
-- À NE PAS FAIRE : 0,1 ms en permanence, même loin du point
CreateThread(function()
while true do
local dist = #(GetEntityCoords(PlayerPedId()) - vector3(215.0, -810.0, 30.7))
if dist < 2.0 and IsControlJustPressed(0, 38) then
ouvrirMenu()
end
Wait(0)
end
end)Bon : un point d'interaction. Le Socle n'a qu'un seul thread de proximité pour tous les points de la base ; il ne passe par image que quand le joueur est à portée de touche.
-- 0,00 ms loin du point : c'est le thread unique de modules/interact qui gère tout
local pointId = exports.socle:addInteractionPoint({
coords = vector3(215.0, -810.0, 30.7),
dist = 2.0,
key = "E",
message = Core.t("monmodule.point"),
onPress = function() ouvrirMenu() end,
})Quand une boucle par image est vraiment indispensable (une animation qui bloque des contrôles, un marqueur pendant un placement), elle n'existe que pendant l'état et s'arrête seule : c'est ce que fait le portage (modules/carry), dont la boucle ne vit que tant que le joueur est porté.
Attente adaptée à la distance
Si tu dois surveiller quelque chose de loin, adapte l'attente :
CreateThread(function()
while actif do
local dist = #(GetEntityCoords(PlayerPedId()) - cible)
-- loin : on regarde rarement ; près : plus souvent ; jamais Wait(0) ici
Wait(dist > 100.0 and 2000 or dist > 20.0 and 500 or 100)
end
end)Lire les réglages sans les recopier
Dans une boucle chaude côté client, Core.Variable:watch met la valeur en cache et ne la relit qu'après un changement du panel :
local vitesseMax = Core.Variable:watch("monmodule", "vitesseMax", 120)
CreateThread(function()
while actif do
if GetEntitySpeed(veh) * 3.6 > vitesseMax() then --[[ ... ]] end
Wait(250)
end
end)Le reste
- State bags pour un état partagé et persistant d'un joueur ou d'une entité (
Player(src).state:set("carry", role, true)côté serveur) plutôt qu'un event diffusé à tous. - Events ciblés :
Core.Event:send(src, ...)vers le joueur concerné.Core.Event:broadcastest réservé à ce qui concerne vraiment tout le monde. - Écritures SQL groupées : regroupe les insertions (les logs du Socle écrivent par lots toutes les 5 s).
- Une seule interface NUI (voir L'interface NUI) : n'ajoute jamais de
ui_pageà une nouvelle ressource.
Les règles de sécurité
Le client n'est jamais source de vérité. Tout ce qui touche à l'argent, aux objets, aux positions ou aux droits est décidé par le serveur.
Ne jamais croire un montant venu du client
Mauvais :
-- À NE PAS FAIRE : le client choisit le prix, un tricheur enverra -100000
Core.Event:register("monmodule:server:acheter", function(src, item, prix)
local xPlayer = Core.getPlayer(src)
xPlayer.removeAccountMoney("money", prix)
exports.ox_inventory:AddItem(src, item, 1)
end)Bon : le client envoie un identifiant, le serveur retrouve le prix dans les réglages, vérifie la distance, puis encaisse.
Core.Callback:register("monmodule:server:acheter", function(src, cb, index)
index = math.floor(tonumber(index) or 0)
local catalogue = Core.Variable:getOr("monmodule", "catalogue", {})
local article = catalogue[index]
if type(article) ~= "table" then return cb(false, "inconnu") end
-- distance vérifiée par le serveur, position lue par le serveur
local ped = GetPlayerPed(src)
local comptoir = Core.Variable:getOr("monmodule", "comptoir", { 0, 0, 0 })
local pos = vector3(comptoir[1] + 0.0, comptoir[2] + 0.0, comptoir[3] + 0.0)
if ped == 0 or #(GetEntityCoords(ped) - pos) > 3.0 then return cb(false, "trop_loin") end
local prix = math.floor(tonumber(article.prix) or 0)
local xPlayer = Core.getPlayer(src)
if prix <= 0 or not xPlayer or xPlayer.getAccount("money").money < prix then
return cb(false, "pas_assez")
end
if not exports.ox_inventory:CanCarryItem(src, article.item, 1) then return cb(false, "plein") end
xPlayer.removeAccountMoney("money", prix, "Achat " .. article.item)
exports.ox_inventory:AddItem(src, article.item, 1)
cb(true)
end)Les autres règles
- Tout event serveur valide ses paramètres : type, bornes (
Core.clamp), nombre fini (Core.isFinite), existence du joueur visé. - Distance vérifiée par le serveur pour toute action dans l'espace (
GetEntityCoords(GetPlayerPed(src))). - Permission vérifiée par le serveur pour toute action de staff :
Core.Permission:can(src, "admin.xxx"). La vérification côté client ne sert qu'à masquer un bouton. - Requêtes SQL préparées (
?), jamais de concaténation de texte venu du joueur. - Transaction dès qu'une opération de valeur a plusieurs étapes (débiter puis livrer doit réussir ou échouer ensemble) :
MySQL.transaction.await({ ... }). - Valeurs sensibles privées : une adresse de webhook, un mot de passe, une réponse d'examen se déclarent avec
private = trueet ne partent jamais chez les joueurs (voir Les réglages côté code).
La signature des events
Les events envoyés par Event:send (client) et reçus par Core.Event:register (serveur) portent une signature calculée à partir de network_token, de l'identifiant du joueur et d'un compteur. Un TriggerServerEvent « à la main » (exécuteur de triche, autre ressource) vers un event enregistré ainsi est refusé avec un avertissement Rejected '…' (invalid token).