diff --git a/CHECKLIST.md b/CHECKLIST.md index 2d89c21..435d3f3 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -1,6 +1,6 @@ # Checklist: mysql_samp vs MySQL R41-4 -Coverage of the MySQL R41-4 (BlueG / maddinat0r) Pawn API by **mysql_samp 1.1.1**. Source of truth: [`include/mysql_samp.inc.in`](include/mysql_samp.inc.in) and [`src/lib.rs`](src/lib.rs). +Coverage of the MySQL R41-4 (BlueG / maddinat0r) Pawn API by **mysql_samp**. Source of truth: [`include/mysql_samp.inc.in`](include/mysql_samp.inc.in) and [`src/lib.rs`](src/lib.rs). Current plugin version lives in [`Cargo.toml`](Cargo.toml). ## Connection diff --git a/README.md b/README.md index 1e01622..7997904 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mysql_samp -> MySQL plugin for SA-MP written in Rust — by [NullSablex](https://github.com/NullSablex) +> MySQL plugin for SA-MP and Open Multiplayer, written in Rust — by [NullSablex](https://github.com/NullSablex) ![License](https://img.shields.io/badge/license-GPL--3.0-blue) ![SA-MP](https://img.shields.io/badge/SA--MP-0.3.7+-orange) @@ -42,8 +42,8 @@ The same binary loads on SA-MP and on Open Multiplayer — natively as a compone plugins mysql_samp.so ``` (or `mysql_samp.dll` on Windows) - - **Open Multiplayer (native, recommended)** — list the binary under `components` in `config.json`. It will be loaded via `ComponentEntryPoint`, with access to `ICore`, `ITimersComponent` and the other native APIs. - - **Open Multiplayer (legacy)** — still supported. Register the binary under `legacy_plugins` in `config.json` if you prefer the SA-MP compatibility path. Same binary, no extra build flags. + - **Open Multiplayer (native, recommended)** — drop the binary into the `components/` folder. open.mp auto-discovers it on start and loads it via `ComponentEntryPoint`, with access to `ICore`, `ITimersComponent` and the other native APIs. No `config.json` entry required. + - **Open Multiplayer (legacy)** — same binary works as a legacy plugin. Drop it into `plugins/` and add it to `legacy_plugins` in `config.json` (this one DOES need to be declared, otherwise open.mp skips legacy plugins). > [!IMPORTANT] > No `libmysqlclient` or other system library is required. The plugin is self-contained. @@ -86,6 +86,8 @@ public OnGameModeExit() { } ``` +Browse the [examples/](examples/) folder for self-contained `.pwn` scripts covering connection setup, threaded queries, ORM, TLS and error handling. The plugin natives (`mysql_*`, `cache_*`, `orm_*`) and the `OnQueryError` forward are identical across SA-MP and Open Multiplayer, so every example builds and runs on both — the only thing that differs between servers is the installation path documented above. + ## Documentation The full plugin documentation lives in [docs/](docs/): diff --git a/docs/api-reference.md b/docs/api-reference.md index ca68846..b4f9b6e 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -7,10 +7,10 @@ Source of truth: [`include/mysql_samp.inc.in`](https://github.com/NullSablex/mys ## Constants ```pawn -#define MYSQL_SAMP_VERSION "1.1.1" +#define MYSQL_SAMP_VERSION "X.Y.Z" ``` -The literal above is regenerated by `build.rs` from `CARGO_PKG_VERSION` on every build, so it always tracks the actual plugin version. +The literal is regenerated by `build.rs` from `CARGO_PKG_VERSION` on every build, so the value shipped in `mysql_samp.inc` always tracks the version declared in [`Cargo.toml`](https://github.com/NullSablex/mysql_samp/blob/master/Cargo.toml). This page does not hardcode the current number on purpose — check `Cargo.toml` or the include itself. ### Connection options diff --git a/docs/installation.md b/docs/installation.md index 0c8a9b3..c856a2a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -33,7 +33,7 @@ plugins mysql_samp.so ### Open Multiplayer — native mode (recommended) -Drop the binary into `components/` (open.mp folder) and register it under `components` in `config.json`. The plugin is loaded via `ComponentEntryPoint` and gets access to `ICore`, `ITimersComponent` and the other native APIs. +Drop the binary into the `components/` folder. open.mp auto-discovers and loads it via `ComponentEntryPoint` on start, with access to `ICore`, `ITimersComponent` and the other native APIs. No `config.json` entry is required — native components are discovered by scanning the folder, not by declaration. ### Open Multiplayer — legacy mode diff --git a/examples/01_basic_connection.pwn b/examples/01_basic_connection.pwn new file mode 100644 index 0000000..451da9a --- /dev/null +++ b/examples/01_basic_connection.pwn @@ -0,0 +1,57 @@ +// 01_basic_connection.pwn — open a connection at OnGameModeInit, close it on exit. +// +// Two variants are shown: +// 1. The minimal call (default port 3306, no SSL, auto_reconnect on). +// 2. The same call routed through mysql_options_new() so you can tweak port, +// connect timeout, auto-reconnect, etc. + +#include +#include + +#define MYSQL_HOST "127.0.0.1" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +public OnGameModeInit() +{ + // --- Variant 1: no options ---------------------------------------------- + // g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE); + + // --- Variant 2: with options -------------------------------------------- + new opts = mysql_options_new(); + mysql_options_set_int(opts, MYSQL_OPT_PORT, 3306); + mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 5); // seconds + mysql_options_set_int(opts, MYSQL_OPT_AUTO_RECONNECT, 1); + + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, opts); + + if (g_MysqlConn == 0) + { + new err[256]; + mysql_error(0, err); // 0 = global error slot (set when connect itself fails) + printf("[mysql] connect failed: %s", err); + return 1; + } + + // Optional: ask the server how it's doing. + new status[256]; + if (mysql_status(g_MysqlConn, status)) + { + printf("[mysql] connected. %s", status); + } + + return 1; +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) + { + mysql_close(g_MysqlConn); + g_MysqlConn = 0; + } + return 1; +} diff --git a/examples/02_threaded_query.pwn b/examples/02_threaded_query.pwn new file mode 100644 index 0000000..0cbc6c1 --- /dev/null +++ b/examples/02_threaded_query.pwn @@ -0,0 +1,69 @@ +// 02_threaded_query.pwn — non-blocking SELECT with a callback, FIFO-ordered. +// +// mysql_query() runs the query on a worker thread and dispatches the result +// to the named callback. Inside the callback the result is the *active cache* +// — read it with the cache_* natives. The cache is freed automatically when +// the callback returns (use cache_save() to keep it longer). +// +// Format spec for the variadic args: each letter maps to one extra param. +// "d" int, "f" float, "s" string. Order must match the callback signature. + +#include +#include + +#define MYSQL_HOST "127.0.0.1" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +public OnGameModeInit() +{ + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE); + return 1; +} + +public OnPlayerConnect(playerid) +{ + new name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + new query[256]; + mysql_format(g_MysqlConn, query, sizeof(query), + "SELECT id, money, score FROM players WHERE name = '%e' LIMIT 1", + name); + + // Pass playerid through to the callback so we know which session to apply + // the result to. FIFO ensures the result lands before any later query for + // the same player. + mysql_query(g_MysqlConn, query, "OnPlayerLoad", "d", playerid); + return 1; +} + +forward OnPlayerLoad(playerid); +public OnPlayerLoad(playerid) +{ + if (cache_get_row_count() == 0) + { + printf("[mysql] new player, no row yet (id=%d)", playerid); + return 1; + } + + new playerDbId = cache_get_value_name_int(0, "id"); + new money = cache_get_value_name_int(0, "money"); + new score = cache_get_value_name_int(0, "score"); + + GivePlayerMoney(playerid, money); + SetPlayerScore(playerid, score); + + printf("[mysql] loaded player %d (db_id=%d, money=%d, score=%d)", + playerid, playerDbId, money, score); + return 1; +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) mysql_close(g_MysqlConn); + return 1; +} diff --git a/examples/03_parallel_query.pwn b/examples/03_parallel_query.pwn new file mode 100644 index 0000000..04cc561 --- /dev/null +++ b/examples/03_parallel_query.pwn @@ -0,0 +1,57 @@ +// 03_parallel_query.pwn — mysql_pquery: parallel, no ordering guarantee. +// +// Use mysql_pquery when the result does NOT depend on a previously queued +// query for the same row. Typical fit: independent UPDATEs, write-only logging, +// or read-only lookups whose order doesn't matter. +// +// If you fire mysql_pquery() for the same key (e.g. same playerid) multiple +// times in a row and need ordering, switch to mysql_query() instead. + +#include +#include + +#define MYSQL_HOST "127.0.0.1" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +public OnGameModeInit() +{ + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE); + return 1; +} + +// Fire-and-forget log entry. We don't care when it lands, only that it lands. +LogPlayerAction(playerid, const action[]) +{ + new name[MAX_PLAYER_NAME]; + GetPlayerName(playerid, name, sizeof(name)); + + new query[256]; + mysql_format(g_MysqlConn, query, sizeof(query), + "INSERT INTO action_log (player, action, ts) VALUES ('%e', '%e', NOW())", + name, action); + + // Empty callback string = run and discard the result. + mysql_pquery(g_MysqlConn, query); +} + +public OnPlayerDeath(playerid, killerid, reason) +{ + LogPlayerAction(playerid, "death"); + return 1; +} + +public OnPlayerSpawn(playerid) +{ + LogPlayerAction(playerid, "spawn"); + return 1; +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) mysql_close(g_MysqlConn); + return 1; +} diff --git a/examples/04_escape_and_format.pwn b/examples/04_escape_and_format.pwn new file mode 100644 index 0000000..476c297 --- /dev/null +++ b/examples/04_escape_and_format.pwn @@ -0,0 +1,86 @@ +// 04_escape_and_format.pwn — building queries safely. +// +// mysql_format specifiers: +// %d int +// %f float +// %s string, auto-escaped (preferred for user input) +// %e alias of %s, explicit "escape" +// %r string, raw (NO escape) — use only with values YOU control +// %% literal percent sign +// +// If you absolutely need to escape a string outside mysql_format, use +// mysql_escape_string. It is a pure function: no connection required. + +#include +#include + +#define MYSQL_HOST "127.0.0.1" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +public OnGameModeInit() +{ + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE); + return 1; +} + +// Example 1: a SELECT with an escaped user-supplied name. +SearchPlayerByName(const input[]) +{ + new query[256]; + mysql_format(g_MysqlConn, query, sizeof(query), + "SELECT id, score FROM players WHERE name = '%e' LIMIT 1", + input); + mysql_query(g_MysqlConn, query, "OnSearchResult"); +} + +forward OnSearchResult(); +public OnSearchResult() +{ + if (cache_get_row_count() == 0) return printf("[mysql] not found"); + new id = cache_get_value_name_int(0, "id"); + new score = cache_get_value_name_int(0, "score"); + printf("[mysql] id=%d score=%d", id, score); + return 1; +} + +// Example 2: an UPDATE mixing ints, floats and strings. +PersistPlayer(playerid, const note[]) +{ + new Float:x, Float:y, Float:z; + GetPlayerPos(playerid, x, y, z); + + new query[512]; + mysql_format(g_MysqlConn, query, sizeof(query), + "UPDATE players SET pos_x=%f, pos_y=%f, pos_z=%f, score=%d, note='%e' WHERE id=%d", + x, y, z, GetPlayerScore(playerid), note, playerid); + mysql_pquery(g_MysqlConn, query); +} + +// Example 3: %r — raw, no escape. Only for trusted, hard-coded values. +TruncateActionLog() +{ + new query[128]; + mysql_format(g_MysqlConn, query, sizeof(query), + "DELETE FROM %r WHERE ts < NOW() - INTERVAL %d DAY", + "action_log", 30); + mysql_pquery(g_MysqlConn, query); +} + +// Example 4: escape outside mysql_format (rare, but available). +ManualEscape() +{ + new src[] = "O'Brien"; + new dest[64]; + mysql_escape_string(src, dest); + printf("[mysql] escaped: %s", dest); // O\'Brien +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) mysql_close(g_MysqlConn); + return 1; +} diff --git a/examples/05_orm.pwn b/examples/05_orm.pwn new file mode 100644 index 0000000..b0d31bd --- /dev/null +++ b/examples/05_orm.pwn @@ -0,0 +1,122 @@ +// 05_orm.pwn — minimal ORM: bind Pawn vars to columns, save / load by key. +// +// Workflow: +// 1. orm_create(table, conn) — returns an orm_id tied to that table. +// 2. orm_addvar_* — bind a Pawn variable to a column. Reads write into the +// var; writes pull the var's current value. +// 3. orm_setkey(orm_id, "id") — declare the primary key. orm_save() decides +// INSERT vs UPDATE based on whether the key var is zero/empty. +// 4. orm_select / orm_save / orm_delete / orm_insert / orm_update — async, +// each with an optional callback. +// +// Schema assumed for the example: +// CREATE TABLE players ( +// id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, +// name VARCHAR(24) NOT NULL UNIQUE, +// money INT NOT NULL DEFAULT 0, +// score INT NOT NULL DEFAULT 0 +// ); + +#include +#include + +#define MYSQL_HOST "127.0.0.1" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +// One ORM instance per player slot. +new gPlayerOrm[MAX_PLAYERS]; +new gPlayerDbId[MAX_PLAYERS]; +new gPlayerName[MAX_PLAYERS][MAX_PLAYER_NAME]; +new gPlayerMoney[MAX_PLAYERS]; +new gPlayerScore[MAX_PLAYERS]; + +public OnGameModeInit() +{ + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE); + return 1; +} + +public OnPlayerConnect(playerid) +{ + GetPlayerName(playerid, gPlayerName[playerid], MAX_PLAYER_NAME); + gPlayerDbId[playerid] = 0; + gPlayerMoney[playerid] = 0; + gPlayerScore[playerid] = 0; + + new orm = orm_create("players", g_MysqlConn); + orm_addvar_int (orm, gPlayerDbId[playerid], "id"); + orm_addvar_string(orm, gPlayerName[playerid], MAX_PLAYER_NAME, "name"); + orm_addvar_int (orm, gPlayerMoney[playerid], "money"); + orm_addvar_int (orm, gPlayerScore[playerid], "score"); + orm_setkey(orm, "id"); + + gPlayerOrm[playerid] = orm; + + // First load attempt is by name (key is zero, so SELECT uses bound vars). + // We use a raw query for the WHERE-by-name lookup since orm_select keys on + // the primary key. + new query[256]; + mysql_format(g_MysqlConn, query, sizeof(query), + "SELECT id FROM players WHERE name = '%e' LIMIT 1", + gPlayerName[playerid]); + mysql_query(g_MysqlConn, query, "OnPlayerLookup", "d", playerid); + return 1; +} + +forward OnPlayerLookup(playerid); +public OnPlayerLookup(playerid) +{ + if (cache_get_row_count() == 0) + { + // New player: orm_save with id=0 → INSERT. + orm_save(gPlayerOrm[playerid], "OnPlayerInserted", "d", playerid); + return 1; + } + + // Existing player: store the id and pull the rest via orm_select. + gPlayerDbId[playerid] = cache_get_value_name_int(0, "id"); + orm_select(gPlayerOrm[playerid], "OnPlayerLoaded", "d", playerid); + return 1; +} + +forward OnPlayerLoaded(playerid); +public OnPlayerLoaded(playerid) +{ + if (orm_errno(gPlayerOrm[playerid]) != ORM_OK) return 1; + GivePlayerMoney(playerid, gPlayerMoney[playerid]); + SetPlayerScore (playerid, gPlayerScore[playerid]); + return 1; +} + +forward OnPlayerInserted(playerid); +public OnPlayerInserted(playerid) +{ + gPlayerDbId[playerid] = cache_insert_id(); + printf("[orm] created player %d (db_id=%d)", playerid, gPlayerDbId[playerid]); + return 1; +} + +public OnPlayerDisconnect(playerid, reason) +{ + gPlayerMoney[playerid] = GetPlayerMoney(playerid); + gPlayerScore[playerid] = GetPlayerScore(playerid); + + if (gPlayerOrm[playerid] != 0 && gPlayerDbId[playerid] != 0) + { + // Key is set → orm_save performs an UPDATE. + orm_save(gPlayerOrm[playerid]); + orm_destroy(gPlayerOrm[playerid]); + gPlayerOrm[playerid] = 0; + } + return 1; +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) mysql_close(g_MysqlConn); + return 1; +} diff --git a/examples/06_ssl.pwn b/examples/06_ssl.pwn new file mode 100644 index 0000000..2b5f7b9 --- /dev/null +++ b/examples/06_ssl.pwn @@ -0,0 +1,51 @@ +// 06_ssl.pwn — TLS connection. Requires mysql_samp v1.1.1 or later. +// +// Up to v1.1.0 the SSL options were silently ignored and every connection was +// plaintext. From v1.1.1 onwards: +// - MYSQL_OPT_SSL = 1 turns on TLS via rustls (SslOpts::default()). +// - MYSQL_OPT_SSL_CA is an optional path to a root certificate (.pem or .der). +// Without it, the OS trust store is used. + +#include +#include + +#define MYSQL_HOST "db.example.com" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +public OnGameModeInit() +{ + new opts = mysql_options_new(); + mysql_options_set_int(opts, MYSQL_OPT_PORT, 3306); + mysql_options_set_int(opts, MYSQL_OPT_CONNECT_TIMEOUT, 5); + + // --- Enable TLS --------------------------------------------------------- + mysql_options_set_int(opts, MYSQL_OPT_SSL, 1); + + // --- Optional: pin a CA certificate ------------------------------------- + // Path is resolved relative to the server's working directory. + // Comment this line out to fall back to the platform's trust store. + mysql_options_set_str(opts, MYSQL_OPT_SSL_CA, "certs/ca.pem"); + + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, opts); + + if (g_MysqlConn == 0) + { + new err[256]; + mysql_error(0, err); + printf("[mysql] TLS connect failed: %s", err); + return 1; + } + + printf("[mysql] TLS connection established to %s", MYSQL_HOST); + return 1; +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) mysql_close(g_MysqlConn); + return 1; +} diff --git a/examples/07_error_handling.pwn b/examples/07_error_handling.pwn new file mode 100644 index 0000000..220529b --- /dev/null +++ b/examples/07_error_handling.pwn @@ -0,0 +1,73 @@ +// 07_error_handling.pwn — OnQueryError, mysql_errno, mysql_error. +// +// Every threaded query that fails (syntax error, constraint violation, lost +// connection, ...) triggers OnQueryError BEFORE the regular callback. +// +// errorid — native MySQL error code (e.g. 1062 = duplicate key) or +// one of the MYSQL_ERROR_* codes (1..=8) for plugin-internal +// failures. +// error[] — human-readable message. +// callback[] — the callback that would have been invoked on success. +// query[] — the SQL text (may be truncated for very long queries). +// connId — the connection that produced the error. + +#include +#include + +#define MYSQL_HOST "127.0.0.1" +#define MYSQL_USER "samp" +#define MYSQL_PASSWORD "secret" +#define MYSQL_DATABASE "samp_server" + +new g_MysqlConn = 0; + +public OnGameModeInit() +{ + // Crank logging up while debugging. + mysql_log(MYSQL_LOG_ALL); + + g_MysqlConn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE); + + if (g_MysqlConn == 0) + { + new err[256]; + mysql_error(0, err); + printf("[mysql] connect failed (code=%d): %s", mysql_errno(0), err); + return 1; + } + + // Trigger a deliberate failure to exercise OnQueryError. + mysql_query(g_MysqlConn, "SELECT * FROM table_that_does_not_exist", "OnNeverCalled"); + return 1; +} + +forward OnNeverCalled(); +public OnNeverCalled() +{ + // Won't run — OnQueryError fires first and the regular callback is skipped. + return 1; +} + +public OnQueryError(errorid, const error[], const callback[], const query[], connId) +{ + printf("[mysql] error %d on connId %d: %s", errorid, connId, error); + printf("[mysql] callback: %s", callback); + printf("[mysql] query: %s", query); + + // errorid is the native MySQL code (1146 = base table or view not found, + // 1062 = duplicate key, 1045 = access denied, ...) — handle the ones you + // care about explicitly. + switch (errorid) + { + case 1062: printf("[mysql] duplicate key — ignoring"); + case 1146: printf("[mysql] missing table — schema migration pending?"); + default: printf("[mysql] unhandled MySQL error %d", errorid); + } + return 1; +} + +public OnGameModeExit() +{ + if (g_MysqlConn != 0) mysql_close(g_MysqlConn); + return 1; +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c532e1b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,56 @@ +# mysql_samp examples + +Runnable Pawn snippets showing how to use the plugin on **SA-MP** and **Open Multiplayer**. The plugin natives (`mysql_*`, `cache_*`, `orm_*`) and the `OnQueryError` forward are identical across both servers — every snippet here exercises only the plugin API. + +| File | Topic | +|---|---| +| [`01_basic_connection.pwn`](01_basic_connection.pwn) | Connect / close, with and without `mysql_options_new()` | +| [`02_threaded_query.pwn`](02_threaded_query.pwn) | `mysql_query` + callback, read cache by column name / index | +| [`03_parallel_query.pwn`](03_parallel_query.pwn) | `mysql_pquery` for unordered, parallel work | +| [`04_escape_and_format.pwn`](04_escape_and_format.pwn) | `mysql_format` (`%s` auto-escape, `%e`, `%r`, `%d`, `%f`) | +| [`05_orm.pwn`](05_orm.pwn) | ORM: bind Pawn vars to columns, `orm_save` / `orm_select` | +| [`06_ssl.pwn`](06_ssl.pwn) | Enable TLS via `MYSQL_OPT_SSL` / `MYSQL_OPT_SSL_CA` (v1.1.1+) | +| [`07_error_handling.pwn`](07_error_handling.pwn) | `OnQueryError`, `mysql_errno`, `mysql_error` | + +## Compiling + +The examples assume the include path is set up so that `` resolves to [`../include/mysql_samp.inc`](../include/mysql_samp.inc). + +```bash +pawncc -i../include 01_basic_connection.pwn +``` + +Or copy `mysql_samp.inc` into your `pawno/include/` (SA-MP) or `qawno/include/` (open.mp) folder and compile from inside the gamemode tree. + +## Installing the plugin + +### SA-MP + +Drop `mysql_samp.so` (Linux) or `mysql_samp.dll` (Windows) into `plugins/` and register it in `server.cfg`: + +``` +plugins mysql_samp.so +``` + +### Open Multiplayer — native component (recommended) + +Drop the binary into the `components/` folder. open.mp auto-discovers it on start and loads it via `ComponentEntryPoint`. **No `config.json` entry is required** — listing the file in `components` is not a thing; the folder itself IS the registration. + +### Open Multiplayer — legacy mode + +Drop the binary into `plugins/` and declare it under `legacy_plugins` in `config.json` (legacy plugins must be listed explicitly, unlike native components): + +```json +{ + "pawn": { + "legacy_plugins": ["mysql_samp"] + } +} +``` + +## Conventions used across the examples + +- Connection credentials are placed at the top as `#define`s — replace with values from a config file in real code. +- One global `g_MysqlConn` holds the connection id; `0` means "not connected". +- Threaded callbacks read from the implicit active cache; no need to call `cache_set_active` unless you persisted the cache with `cache_save`. +- All queries are non-blocking — the gamemode never waits on MySQL.