EasyDB
Description
EasyDB
Extension Arma 3 (Go/cgo, .so Linux) faisant le pont entre SQF et MySQL 8
pour FastRP. Une seule commande générique de requête paramétrée, en
synchrone ou en asynchrone — pas de protocole multi-fichiers façon extDB3.
EasyDB est livré comme un addon Arma complet, server-side only :
@EasyDB/EasyDB_x64.so (l'extension) + @EasyDB/addons/easydb.pbo (les
fonctions EasyDB_fnc_*, auto-compilées via CfgFunctions, aucun script à
coller à la main dans une mission).
Prérequis
- Go 1.21+ avec
CGO_ENABLED=1(nécessite gcc) - armake2 pour empaqueter le PBO (
cargo install --git https://github.com/KoffeinFlummi/armake2 armake2) — outil communautaire Linux, les outils BI officiels (Addon Builder) sont Windows-only - MySQL 8.x
- Linux x86-64 (aucun support Windows prévu)
Build & packaging
make package # -> dist/@EasyDB/EasyDB_x64.so + dist/@EasyDB/addons/easydb.pbo
(make build seul ne produit que le .so, dans build/, si vous voulez
juste tester l'extension sans passer par le PBO.)
Déploiement : copier dist/@EasyDB/ entier sur le serveur, à côté des autres
mods, et charger -mod=@EasyDB uniquement sur le serveur dédié (l'addon
ne fait rien côté client — EasyDB_fnc_init se coupe tout seul si
!isServer). Résultat final, un mod autonome :
@EasyDB/
├── EasyDB_x64.so
├── easydb.json (voir easydb.json.example — pas commité, contient un mot de passe)
└── addons/
└── easydb.pbo
easydb.json se pose directement à la racine du mod, à côté du .so :
l'extension retrouve son propre dossier au chargement (via dladdr sur son
propre symbole) et y cherche easydb.json en priorité — pas besoin de
variable d'environnement ni de dépendre du répertoire de travail du serveur.
EASYDB_CONFIG reste disponible pour pointer ailleurs si besoin, et
./easydb.json (répertoire de travail) sert de dernier recours.
Important : EasyDB_x64.so doit rester à la racine du mod (@EasyDB/),
jamais dans le PBO — Arma ne charge jamais une extension depuis l'intérieur
d'un PBO. Le nom du fichier est sensible à la casse sur Linux et doit
correspondre exactement à "EasyDB" callExtension ... utilisé dans le code.
API SQF
Une fois -mod=@EasyDB chargé, EasyDB_fnc_init s'auto-exécute (postInit)
et enregistre le handler ExtensionCallback — rien à faire côté mission.
Deux fonctions, EasyDB_fnc_query (synchrone) et EasyDB_fnc_queryAsync
(asynchrone, avec callback).
Guide complet avec exemples FastRP (argent, grade, inventaire JSON, gestion d'erreurs, bonnes pratiques) : docs/USAGE.md.
Tests
make test # tests unitaires (sqfarray, jsonconv, chunk, db via sqlmock, dispatch, async)
make vet
Test de bout en bout sans Arma (dlopen le .so compilé et appelle
directement RVExtension/RVExtensionArgs/RVExtensionRegisterCallback) :
cp easydb.json.example easydb.json # renseigner un vrai mot de passe
make smoke # package + dlopen dist/@EasyDB/EasyDB_x64.so + exercice complet de l'API
docker/init.sql crée le schéma easydb (utilisateur dédié + table
d'exemple example_players) à jouer contre un serveur MySQL 8 existant —
utile pour make smoke et pour des tests d'intégration futurs.
Schéma FastRP
Pas encore figé. example_players dans docker/init.sql n'est qu'une table
de démonstration pour valider get/set/increment + colonne JSON ; le schéma
réel de FastRP (joueurs/factions/jobs/économie) viendra dans un second temps
et n'a besoin d'aucun changement côté extension : c'est justement le but
d'une commande de requête générique.
Utilisation
Guide d'utilisation — EasyDB
Ce guide s'adresse à quelqu'un qui écrit du code mission/serveur pour FastRP et veut lire/écrire dans MySQL depuis SQF. Pour builder, packager ou déployer l'addon lui-même, voir le README.
Prérequis
- Le mod
@EasyDBchargé sur le serveur dédié (-mod=@EasyDB), jamais nécessaire côté client — tout ce qui suit ne fonctionne que dans du code qui s'exécute côté serveur (isServer== true). easydb.jsonposé à la racine de@EasyDB/, à côté deEasyDB_x64.so(voireasydb.json.example), avec les identifiants MySQL. L'extension détecte son propre dossier toute seule, rien à configurer côté serveur.
Rien d'autre à initialiser : EasyDB_fnc_init tourne automatiquement au
démarrage de la mission et enregistre ce qu'il faut pour l'async.
Les deux fonctions
EasyDB_fnc_query |
EasyDB_fnc_queryAsync |
|
|---|---|---|
| Usage | lookups rapides | tout ce qui peut être lent ou volumineux |
| Bloquant ? | oui (thread appelant, timeout moteur 1s) | non, retourne immédiatement |
| Résultat | renvoyé directement | livré plus tard via callback |
| Taille du résultat | refusé si > ~10 Ko | pas de limite (découpé automatiquement) |
Règle simple : si c'est un SELECT/UPDATE sur une ligne avec peu de
colonnes (argent, grade, un compteur...), EasyDB_fnc_query convient. Dès
que ça touche un inventaire complet, une liste de plusieurs joueurs, ou un
batch de plusieurs requêtes, utiliser EasyDB_fnc_queryAsync.
Toutes les requêtes sont des statements préparés : _sql contient des
? comme placeholders, les valeurs vont dans le tableau _params. Ne
jamais construire _sql en concaténant des valeurs venant du joueur —
c'est exactement ce que les ? évitent.
EasyDB_fnc_query — synchrone
params ["_sql", ["_params", []]];
// Retourne [true, resultat] ou [false, messageErreur]
Exemple — lire l'argent d'un joueur
private _result = ["SELECT money FROM players WHERE steam_id = ?", [_steamId]] call EasyDB_fnc_query;
_result params ["_ok", "_data"];
if (!_ok) exitWith {
diag_log text format ["[FastRP] échec lecture argent pour %1: %2", _steamId, _data];
};
_data params ["_columns", "_rows"];
if (count _rows == 0) exitWith { /* joueur pas encore en base */ };
private _money = parseNumber ((_rows select 0) select 0); // colonnes = STRING, caster au besoin
Exemple — incrémenter l'argent (opération atomique côté SQL)
private _result = [
"UPDATE players SET money = money + ? WHERE steam_id = ?",
[_montant, _steamId]
] call EasyDB_fnc_query;
_result params ["_ok", "_data"];
// _data = [affectedRows, lastInsertId] pour un UPDATE/INSERT/DELETE
if (_ok && { (_data select 0) == 0 }) then {
diag_log text format ["[FastRP] steam_id %1 introuvable, argent non crédité", _steamId];
};
Faire l'incrément dans le UPDATE (money = money + ?) plutôt que
lire-modifier-écrire depuis SQF évite les races entre deux appels
concurrents sur le même joueur.
Exemple — changer le grade/palier d'un job
["UPDATE players SET grade = ? WHERE steam_id = ?", [_nouveauGrade, _steamId]] call EasyDB_fnc_query;
EasyDB_fnc_queryAsync — asynchrone
params ["_sql", ["_params", []], "_callback"];
// Retourne [true, requestId] immédiatement, ou [false, messageErreur] si
// la requête est rejetée tout de suite (SQL/arguments invalides)
_callback est le nom (STRING) d'une fonction globale déjà compilée
(via CfgFunctions par exemple), pas une code-string. Elle sera appelée
plus tard avec :
params ["_ok", "_data"];
// même format que le retour de EasyDB_fnc_query : [colonnes, lignes] ou
// [affectedRows, lastInsertId] si _ok, message d'erreur sinon
Exemple — charger l'inventaire (JSON) d'un joueur à la connexion
// dans le CfgFunctions du mod FastRP, tag FastRP :
// TAG_fnc_onInventoryLoaded.sqf
params ["_ok", "_data"];
if (!_ok) exitWith { diag_log text format ["[FastRP] échec chargement inventaire: %1", _data]; };
_data params ["_columns", "_rows"];
if (count _rows == 0) exitWith {};
private _inventoryJson = (_rows select 0) select 0; // colonne JSON déjà décodée en tableau SQF
// _inventoryJson est de la forme [["weapon","arifle_MX_F"],["items",[...]]] pour {"weapon":"arifle_MX_F","items":[...]}
// appel, côté connexion joueur :
[
"SELECT inventory FROM players WHERE steam_id = ?",
[_steamId],
"TAG_fnc_onInventoryLoaded"
] call EasyDB_fnc_queryAsync;
Exemple — sauvegarder l'inventaire (écrire du JSON)
Le plus simple est de construire le JSON directement en SQF (ou de le garder tel quel si vous le stockez déjà sous cette forme) et de le passer comme n'importe quel paramètre STRING — MySQL valide et stocke le JSON lui-même :
private _json = "{""weapon"":""arifle_MX_F"",""items"":[""FirstAidKit"",""HandGrenade""]}";
[
"UPDATE players SET inventory = ? WHERE steam_id = ?",
[_json, _steamId],
"TAG_fnc_onInventorySaved"
] call EasyDB_fnc_queryAsync;
(Si vous préférez éviter d'écrire du JSON à la main en SQF, un petit
helper TAG_fnc_arrayToJSON côté mission reste la responsabilité du code
FastRP — EasyDB ne fait la conversion JSON→SQF que dans le sens lecture,
volontairement, pour rester simple.)
Format des valeurs
- Toute valeur scalaire (colonnes non-JSON) revient en STRING, y
compris les nombres — faire
parseNumbersi besoin d'un number SQF. Raison : évite les surprises d'arrondi/precision sur lesDECIMAL/BIGINTet garde le marshaling simple et uniforme. NULL(SQL) devient""(string vide).- Une colonne
JSONest décodée récursivement :- un objet JSON
{"a":1,"b":[2,3]}devient[["a",1],["b",[2,3]]](paires clé/valeur triées par clé, puisque SQF n'a pas de type "map") - un tableau JSON devient un tableau SQF classique
nullJSON devient""
- un objet JSON
Gestion des erreurs
EasyDB_fnc_query/queryAsync retournent toujours [false, message] en
cas de problème, jamais d'exception SQF. Causes possibles :
- SQL invalide ou erreur MySQL (contrainte violée, table inexistante...) → message = l'erreur MySQL brute
- Résultat trop gros pour la voie synchrone →
"result too large for sync query, use queryAsync"(utiliser la version async) - Requête mal formée côté SQF (mauvais types d'arguments) → message explicite, à vérifier en dev
Toujours tester _ok avant d'utiliser _data — ne pas supposer que
l'appel a réussi.
Bonnes pratiques
- Toujours des
?+_params, jamais de concaténation de valeurs joueur dans_sql(injection SQL). - Préférer les opérations atomiques côté SQL (
money = money + ?) à un cycle lire/modifier/écrire depuis SQF, pour éviter les races. - Réserver
EasyDB_fnc_queryaux lookups qui répondent en quelques millisecondes ; dès que le volume de données ou la charge serveur est incertaine, passer en async pour ne pas risquer de geler le serveur pendant 1 seconde (timeout moteur). - Un callback
queryAsyncpeut arriver plusieurs frames après l'appel (découpage en plusieurs morceaux pour les gros résultats) : ne pas supposer qu'il arrive avant la fin du frame courant.
Dépannage
diag_logaffiche[EasyDB] initializedau démarrage si l'addon a bien démarré côté serveur (sinon : mod pas chargé, ou lancé côté client).- Si
EasyDB_fnc_query/queryAsyncrenvoient systématiquement[false, "EasyDB_fnc_query must be called server-side"]: le code est exécuté côté client, il faut le passer côté serveur (ou viaremoteExecciblé serveur si l'appel part d'un client). - Erreur
init error: ...:easydb.jsonintrouvable/mal formé (vérifier qu'il est bien à côté deEasyDB_x64.so, ou queEASYDB_CONFIGpointe au bon endroit), ou connexion MySQL impossible — vérifier les identifiants et les logs du serveur MySQL.