Données et réglages
Garder des informations d'une partie à l'autre (scores, argent, niveaux) avec file et le JSON, et donner à l'hôte des réglages qu'il peut changer sans toucher au code (durée de manche, limite de props…) avec les ConVars.
Sauvegarder des fichiers : file
Chaque gamemode et chaque addon a son propre dossier de données. Un script ne voit que celui de son gamemode ou de son addon : il ne peut ni lire les données des autres, ni sortir de son dossier.
| Côté | Dossier |
|---|---|
| Serveur, partie hébergée depuis le jeu | lua_data/<dossier>/ dans les données du jeu (%APPDATA%\Godot\app_userdata\PhysiCore\) |
| Serveur dédié | lua_data/<dossier>/ à côté de server.cfg |
| Client (chaque joueur) | lua_data_client/<dossier>/ dans les données du jeu |
<dossier> vaut gamemode_<nom> pour un gamemode (ex. gamemode_sandbox) et l'identifiant de l'addon pour un addon : local_<nom du dossier> pour un addon local, ws_<numéro> pour un addon du Workshop (qui garde donc ses données après une mise à jour).
| Fonction | Renvoie |
|---|---|
file.Write(nom, texte) | true si écrit (remplace le fichier) |
file.Append(nom, texte) | Ajoute à la fin |
file.Read(nom) | Le texte, ou nil si le fichier n'existe pas |
file.Exists(nom), file.IsDir(nom) | Existe ? Est-ce un dossier ? |
file.Delete(nom) | Supprime un fichier (ou un dossier vide) |
file.CreateDir(nom) | Crée un sous-dossier |
file.Find(motif) | Deux tables, comme GMod : les fichiers puis les dossiers dont le nom correspond, ex. local fichiers, dossiers = file.Find("joueurs/*") |
file.Open(nom, mode) | Un objet File pour lire ou écrire par morceaux (texte ou binaire), ou nil |
file.AsyncRead(nom, chemin, rappel) | Lit le fichier et appelle rappel(nom, chemin, statut, données) à l'image suivante (voir plus bas) |
file.Size(nom), file.Time(nom) | Taille en octets (-1 si absent), date de modification (secondes Unix) |
Règles : noms en minuscules (a-z, chiffres, _, -, .), au plus 4 sous-dossiers (joueurs/76561198000000000.json), extensions .txt, .json, .dat ou .csv pour écrire. Limites : 1 Mio par fichier, 16 Mio et 512 fichiers par dossier. Un nom refusé ou une limite dépassée renvoie false et écrit l'erreur dans la console.
Tables en JSON : util
util.TableToJSON(table, joli) transforme une table en texte JSON (joli = true pour l'indenter), util.JSONToTable(texte) fait l'inverse (nil si le texte est invalide). Les Vector deviennent "[x y z]" et les Color "{r g b a}", et redeviennent des vecteurs et des couleurs à la lecture. Les nombres entiers restent entiers (3, pas 3.0). Les fonctions sont ignorées.
-- Serveur : argent des joueurs, gardé entre les parties.
local function chemin(ply)
return "argent/" .. ply:SteamID() .. ".json"
end
hook.Add("PlayerInitialSpawn", "argent_charger", function(ply)
local data = util.JSONToTable(file.Read(chemin(ply)) or "") or {}
ply:SetNWInt("argent", data.argent or 100)
end)
local function sauver(ply)
file.Write(chemin(ply), util.TableToJSON({ argent = ply:GetNWInt("argent") }))
end
hook.Add("PlayerDisconnected", "argent_sauver", sauver)
timer.Create("argent_sauvegarde", 60, 0, function()
for _, ply in ipairs(player.GetAll()) do sauver(ply) end
end)
En solo et sans Steam, ply:SteamID() vaut "0" : tous les joueurs hors ligne partagent alors le même fichier.
Fichiers ouverts : file.Open
file.Open(nom, mode) ouvre un fichier du dossier de données et renvoie un objet File (ou nil si le nom est refusé, si le fichier à lire n'existe pas ou si une limite est atteinte). Le troisième argument ("DATA"…) est accepté et ignoré, comme pour les autres fonctions file.* : c'est toujours ton dossier.
| Mode | Effet |
|---|---|
"r", "rb" | Lecture. |
"w", "wb" | Écriture : crée ou vide le fichier. |
"a", "ab" | Ajout à la fin du fichier existant. |
| Méthode | Description |
|---|---|
f:Read(n) | n octets lus comme du texte UTF-8 (sans n : tout le reste) ; nil à la fin. |
f:ReadLine() | La ligne suivante, "\n" compris ; nil à la fin. |
f:ReadByte(), f:ReadBool() | Un octet (0 à 255), un booléen (1 octet). |
f:ReadShort(), f:ReadUShort() | Entier 16 bits signé, non signé. |
f:ReadLong(), f:ReadULong() | Entier 32 bits signé, non signé. |
f:ReadUInt64() | Entier 64 bits, renvoyé en texte (un SteamID64 tient dedans). |
f:ReadFloat(), f:ReadDouble() | Nombre à virgule 32 / 64 bits. |
f:Write(texte) | Écrit le texte (UTF-8). |
f:WriteByte, WriteBool, WriteShort, WriteUShort, WriteLong, WriteULong, WriteUInt64(texte), WriteFloat, WriteDouble | Écrivent les mêmes types que les lectures. |
f:Seek(pos), f:Skip(n), f:Tell() | Aller à l'octet pos, avancer de n octets, position actuelle. |
f:Size(), f:EndOfFile() | Taille en octets, fin du fichier atteinte ? |
f:Flush(), f:Close() | Écrire sur le disque, fermer. |
Les nombres sont écrits en petit-boutiste (little endian). Mêmes règles et limites que file.Write (noms, extensions .txt / .json / .dat / .csv pour écrire, 1 Mio par fichier, 16 Mio et 512 fichiers par dossier). 32 fichiers ouverts au plus : pense à f:Close() ; les fichiers encore ouverts sont fermés quand le Lua s'arrête.
-- Serveur : positions enregistrées en binaire.
local f = file.Open("positions.dat", "wb")
if f then
f:WriteUShort(#player.GetAll())
for _, ply in ipairs(player.GetAll()) do
local pos = ply:GetPos()
f:WriteFloat(pos.x) f:WriteFloat(pos.y) f:WriteFloat(pos.z)
end
f:Close()
end
local f = file.Open("positions.dat", "rb")
if f then
for i = 1, f:ReadUShort() do
print(i, Vector(f:ReadFloat(), f:ReadFloat(), f:ReadFloat()))
end
f:Close()
end
file.AsyncRead(nom, chemin, rappel) lit le fichier tout de suite et appelle rappel(nom, chemin, statut, données) à l'image suivante : statut vaut FSASYNC_OK (0) et données le texte, ou FSASYNC_ERR_FILEOPEN (-1) et nil si le fichier n'existe pas. La fonction renvoie aussi ce statut.
file.AsyncRead("config.json", "DATA", function(nom, chemin, statut, donnees)
if statut ~= FSASYNC_OK then return end
PrintTable(util.JSONToTable(donnees) or {})
end)
Base de données : sql
Une base SQLite, comme la bibliothèque sql de GMod. Le serveur utilise sv.db (dans les données du jeu pour une partie hébergée, à côté de server.cfg pour un serveur dédié) ; chaque joueur a sa propre cl.db côté client. Tous les gamemodes et addons partagent la même base : préfixe le nom de tes tables (monaddon_scores).
| Fonction | Renvoie |
|---|---|
sql.Query(requête) | Les lignes ({ { colonne = "valeur", … }, … }), nil s'il n'y en a aucune, false en cas d'erreur |
sql.QueryRow(requête, n) | La ligne n (1 par défaut), nil ou false |
sql.QueryValue(requête) | La première valeur de la première ligne |
sql.SQLStr(texte, sansGuillemets) | Le texte échappé entre apostrophes ('l''eau'), à utiliser pour chaque valeur venant d'un joueur |
sql.TableExists(nom), sql.IndexExists(nom) | La table, l'index existe-t-il ? |
sql.Begin(), sql.Commit() | Regroupe beaucoup d'écritures en une transaction (bien plus rapide) |
sql.LastError() | Message de la dernière erreur, ou nil |
sql.IsAvailable() | L'extension SQLite est-elle chargée ? |
Comme dans GMod, toutes les valeurs reviennent en texte (tonumber pour les nombres) et NULL devient le texte "NULL". Pour la sécurité, ATTACH, DETACH, VACUUM INTO et load_extension sont refusés (ils pourraient lire ou écrire d'autres fichiers) ; une requête fait 1 Mo au plus. SQLite vient de l'extension addons/godot-sqlite (licence MIT), fournie pour Windows et Linux : ailleurs, sql.IsAvailable() vaut false.
-- Serveur : meilleurs scores.
if not sql.TableExists("monaddon_scores") then
sql.Query("CREATE TABLE monaddon_scores ( steamid TEXT PRIMARY KEY, nom TEXT, points INTEGER )")
end
local function enregistrer(ply, points)
sql.Query("REPLACE INTO monaddon_scores VALUES ( " .. sql.SQLStr(ply:SteamID()) .. ", "
.. sql.SQLStr(ply:Nick()) .. ", " .. math.floor(points) .. " )")
end
local function top10()
local lignes = sql.Query("SELECT nom, points FROM monaddon_scores ORDER BY points DESC LIMIT 10")
if lignes == false then print("Erreur SQL :", sql.LastError()) return {} end
return lignes or {}
end
Données par joueur : PData
ply:SetPData(clé, valeur), ply:GetPData(clé, défaut) et ply:RemovePData(clé) gardent une valeur (texte) par joueur dans la table playerpdata de la base (créée au besoin), d'une partie à l'autre. Le joueur est reconnu par son SteamID ; sans Steam (SteamID 0), par "nom:<pseudo>". Les mêmes avec un identifiant au choix : util.SetPData(id, clé, valeur), util.GetPData(id, clé, défaut), util.RemovePData(id, clé). Côté client, c'est la base cl.db du joueur.
-- Serveur : nombre de connexions de chaque joueur.
hook.Add("PlayerInitialSpawn", "compte_visites", function(ply)
local visites = tonumber(ply:GetPData("visites", 0)) + 1
ply:SetPData("visites", visites)
ply:ChatPrint("Bienvenue pour la " .. visites .. "e fois !")
end)
Requêtes web : http
Désactivées par défaut. L'hôte les autorise avec la ConVar sv_allow_http 1 (allow_http dans la section [security] de server.cfg), et peut les limiter à des domaines avec sv_http_whitelist ("api.exemple.fr,exemple.org" : ces domaines et leurs sous-domaines ; vide = tous). Le réglage du serveur vaut aussi pour le Lua client des joueurs. Les adresses du réseau local (localhost, 127.x, 10.x, 192.168.x, 172.16-31.x…) sont toujours refusées. Limites : 8 requêtes en même temps, 4 Mo par réponse, 30 s.
| Fonction | Rôle |
|---|---|
http.Fetch(url, succès, échec, entêtes) | GET. succès(corps, taille, entêtes, code), échec(raison). |
http.Post(url, champs, succès, échec, entêtes) | POST d'un formulaire (champs : table texte → texte). |
HTTP({ url, method, parameters, headers, body, type, success, failed }) | Requête complète (GET, POST, PUT, DELETE, PATCH, HEAD) ; success(code, corps, entêtes). Renvoie false si elle est refusée tout de suite (et appelle failed). |
Les réponses sont du texte (UTF-8). Une requête refusée (HTTP désactivé, domaine non autorisé, trop de requêtes) appelle la fonction d'échec avec la raison.
-- Serveur : envoyer un message sur un webhook quand un joueur arrive.
hook.Add("PlayerInitialSpawn", "webhook", function(ply)
HTTP({
url = "https://exemple.fr/webhook",
method = "POST",
type = "application/json",
body = util.TableToJSON({ content = ply:Nick() .. " a rejoint le serveur" }),
failed = function(raison) print("Webhook :", raison) end,
})
end)
Pour jouer un son téléchargé : sound.PlayURL.
Réglages du serveur : ConVars
Une ConVar est une valeur nommée que l'hôte et les admins peuvent changer pendant la partie. Le gamemode la crée et la lit ; la valeur est envoyée à tous les joueurs.
-- shared.lua (ou un fichier serveur) d'un gamemode
local duree = CreateConVar("cache_duree_manche", 180, FCVAR_ARCHIVE + FCVAR_NOTIFY,
"Durée d'une manche (secondes)", 30, 900)
-- Serveur : au début de la manche
timer.Create("manche", duree:GetInt(), 1, finir_manche)
CreateConVar(nom, défaut, drapeaux, aide, min, max)
Crée (ou retrouve) une ConVar et renvoie son objet. Le nom est en minuscules (a-z, chiffres, _, .), 64 caractères au plus. Avec min et max, la valeur est un nombre gardé entre les deux ; min = 0 et max = 1 en font une case à cocher dans le panneau des réglages. Côté client, CreateConVar renvoie le même objet en lecture seule (avec défaut tant que la valeur du serveur n'est pas arrivée).
| Drapeau | Effet |
|---|---|
FCVAR_ARCHIVE | La valeur est sauvegardée (convars.cfg) et reprise aux parties suivantes |
FCVAR_NOTIFY | Annonce les changements dans le chat (« X a réglé … sur … ») |
FCVAR_REPLICATED | Accepté pour Garry's Mod : toutes les ConVars du serveur sont déjà envoyées aux joueurs |
FCVAR_PROTECTED | Valeur cachée (pas envoyée aux joueurs, masquée dans les listes) : mots de passe, clés |
FCVAR_NONE | Aucun (0) |
Combine-les avec +. Les ConVars d'un gamemode sont oubliées quand une autre partie commence (les valeurs archivées restent dans convars.cfg) ; un rechargement des scripts les garde.
L'objet ConVar
| Méthode | Renvoie |
|---|---|
cvar:GetInt(), GetFloat(), GetBool(), GetString() | La valeur actuelle |
cvar:GetDefault(), GetName(), GetHelpText(), GetMin(), GetMax() | Infos |
cvar:SetInt(n), SetFloat, SetBool, SetString, Revert() | Change la valeur (serveur ; côté client, seulement une ConVar client) |
Aussi : GetConVar(nom) (nil si inconnue), GetConVarNumber(nom), GetConVarString(nom), et RunConsoleCommand(nom, valeur) qui change une ConVar (côté serveur, une commande concommand sinon).
Réagir à un changement
cvars.AddChangeCallback(nom, function(nom, ancienne, nouvelle) … end, identifiant) est appelée des deux côtés quand la valeur change (valeurs en texte). cvars.RemoveChangeCallback(nom, identifiant) la retire.
if SERVER then
cvars.AddChangeCallback("cache_duree_manche", function(nom, ancienne, nouvelle)
PrintMessage("Les prochaines manches dureront " .. nouvelle .. " secondes.")
end, "annonce")
end
Changer une ConVar en jeu
- Menu pause → Réglages du serveur (hôte et admins) : toutes les ConVars avec leur aide, leurs limites et leur valeur par défaut ; Appliquer envoie les changements.
- Chat :
/cvar nom valeur(ou/cvar nompour lire la valeur),/cvars [filtre]pour la liste. Console d'un serveur dédié :cvar nom valeur,cvars. - Il faut la permission
convars(groupesadminetsuperadminpar défaut). Voir Groupes et permissions. - Serveur dédié :
convars.cfg(à côté deserver.cfg, section[convars]) garde les valeurs archivées, et la variable d'environnementPHYSICORE_CVAR_<NOM>(nom en majuscules,.remplacé par_) impose une valeur de départ, pratique dans Pterodactyl.
ConVars intégrées
Elles reflètent les réglages de server.cfg : les changer modifie et enregistre aussi le réglage.
| ConVar | Réglage |
|---|---|
sbox_maxprops | Props max par joueur (0 à 2000) |
sbox_noclip | Noclip autorisé, si le gamemode l'autorise (0 ou 1, au prochain respawn) |
sbox_playershurtplayers | Dégâts entre joueurs, lu par les gamemodes (0 ou 1) |
sv_props_per_second | Props max par seconde et par joueur (1 à 100) |
sv_rp_movement | Déplacements réalistes pour le roleplay (0 ou 1) : inertie, pas de bunny hop, ralenti à l'atterrissage, plus lent en montée, à reculer et de côté |
sv_player_push | Les joueurs poussent les props et se bousculent en marchant (0 ou 1) |
sv_anticheat | Anti-triche des déplacements (0 ou 1, 1 par défaut) : vitesse, vol, noclip interdit, murs traversés. Voir Sécurité |
sv_force_fog | Brouillard imposé (0 ou 1) : l'option Brouillard des joueurs est ignorée, tout le monde voit le brouillard de la carte (et celui des scripts) |
sv_nametag_distance | Distance (m) jusqu'à laquelle on voit le pseudo au-dessus d'un joueur (10 par défaut, 0 = jamais) |
sv_allow_http | Requêtes web du Lua autorisées (0 ou 1, 0 par défaut). Voir Requêtes web |
sv_http_whitelist | Domaines web autorisés, séparés par des virgules (vide = tous) |
sv_allowcslua | Tout joueur peut lancer du Lua sur son propre jeu avec /lua_run_cl (0 ou 1, 0 par défaut) |
ConVars client
CreateClientConVar(nom, défaut, sauver, _, aide, min, max) (client) crée une préférence propre au joueur, gardée sur son ordinateur si sauver vaut true (par défaut) dans lua_client_convars.cfg. Change-la avec cvar:SetBool(…) ou RunConsoleCommand(nom, valeur). Par défaut, le serveur ne la voit pas.
-- cl_init.lua : une option « afficher le chrono » cochable dans un menu.
local chrono = CreateClientConVar("cache_afficher_chrono", 1, true, false, "Afficher le chrono")
local case = vgui.Create("DCheckBoxLabel", fenetre)
case:SetText("Afficher le chrono")
case:SetChecked(chrono:GetBool())
case.OnChange = function(self, coche) chrono:SetBool(coche) end
Préférences envoyées au serveur (userinfo)
Avec true en 4e argument, la ConVar client est « userinfo » : sa valeur est envoyée au serveur à la création et à chaque changement (64 par joueur, 256 caractères). Le serveur la lit avec ply:GetInfo(nom) (texte, "" si absente) ou ply:GetInfoNum(nom, défaut). Côté client, ces deux méthodes lisent sa propre ConVar client.
-- cl_init.lua
CreateClientConVar("monmode_arme_favorite", "pistol", true, true, "Arme donnée à l'apparition")
-- init.lua
function GM:PlayerLoadout(ply)
local arme = ply:GetInfo("monmode_arme_favorite")
if arme == "pistol" or arme == "shotgun" then ply:Give(arme) else ply:Give("crowbar") end
end