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

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

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

Gestion des erreurs

EasyDB_fnc_query/queryAsync retournent toujours [false, message] en cas de problème, jamais d'exception SQF. Causes possibles :

Toujours tester _ok avant d'utiliser _data — ne pas supposer que l'appel a réussi.

Bonnes pratiques

Dépannage