Database
SQLite or MySQL, moving between them, every table, and saving from addons.
Everything RPG X saves goes to the server's own sv.db (SQLite) by default. Nothing to set up; it's wiped along with the server files (like a reinstall).
It can all move to an external MySQL / MariaDB (InnoDB) instead. That needs the
mysqloo module on the server:
garrysmod/lua/bin/gmsv_mysqloo_linux64.dll for this (64-bit Linux) server. It can't be shipped
in the Workshop addon.
All database commands are server console only (or RCON), since they handle credentials:
| Command | Does |
|---|---|
rpgx_db_mysql <host> <user> <password> <database> [port] |
Tests the connection, then saves it. Doesn't switch anything yet. Port defaults to 3306. |
rpgx_db_migrate |
Copies every table from sv.db into MySQL, then switches to MySQL. Refuses if the MySQL tables already have data. |
rpgx_db_migrate overwrite |
Copies anyway; players in both get the sv.db version, others in MySQL are kept. |
rpgx_db_use sqlite / rpgx_db_use mysql |
Switches without copying. Online players are saved to the old one, then reloaded from the new one. |
rpgx_db_status |
Where everything is stored, whether MySQL is connected, row counts in sv.db. |
Typical move to MySQL:
rpgx_db_mysql db.example.com rpgx secretpassword rpgx
rpgx_db_migrate
- The settings are kept in
garrysmod/data/rpgx/database.json(server-side only, never sent to players). The password is stored there in plain text, so keep that folder private. - sv.db is left untouched by a migration, as a backup.
- If MySQL is unreachable at startup, nothing loads or saves (so nothing gets overwritten) and it retries every 30 seconds.
- Several servers can share one MySQL database (point each at it with
rpgx_db_mysqlandrpgx_db_use mysql): players, inventories and the server's quests are the same on all of them. Every change to the quests or givers writes a new stamp inrpgx_sync; each server checks it everyrpgx_quest_syncseconds and reloads the quests when another server changed them. Spawn points aren't synced that way: two servers on the same map share them, but each only sees the other's point changes after a map change or restart. - Addons that change shared tables can do the same:
DB:Stamp( tx, name )in their transaction,DB:ReadStamp( name, callback )to check. Changes to a MySQL table that adding columns can't cover (moving or dropping columns) go inDB:MySQLMigration( function( run, done ) ... end ), run each time MySQL connects.
Tables #
All created automatically, in both sv.db and MySQL. Foreign keys are ON DELETE CASCADE: deleting
a player deletes their inventory and its items.
| Table | Columns | |
|---|---|---|
rpgx_player |
steamid (SteamID64), name (last known), first_seen, last_seen (unix time) |
One row per player. Everything saved per player references it. |
rpgx_player_rank |
steamid → rpgx_player, rank_id (admin / owner), migrated (copied into ULX) |
Only players above user have a row. |
rpgx_inventory |
id, steamid → rpgx_player, slots (NULL = rpgx_inventory_slots), selected (hotbar slot) |
One per player for now. |
rpgx_inventory_item |
id, inventory_id → rpgx_inventory, slot, class, clip1, clip2, equipped (in hand), amount (consumables: how many in the stack, NULL = not one) |
1:n. class is the name the weapon is spawned by (weapon_pistol), since items live in addons, not the database. |
rpgx_enemy_spawn |
id, map, x, y, z, ceiling, hostile, invincible, static, silent, hive, level_min, level_max, respawn, number, created |
One per spawn point. map is a plain map name (gm_construct), not a key. |
rpgx_enemy_spawn_npc |
spawn_id → rpgx_enemy_spawn, npc |
1:n, which enemies can spawn at a point. npc is the spawn-menu name the NPC is spawned by (npc_zombie). |
rpgx_npc_role |
spawn_id → rpgx_enemy_spawn, role (shop / questgiver) |
Points whose NPC has a role. |
rpgx_shop_item |
spawn_id → rpgx_enemy_spawn, item (weapon class) |
1:n, what a shop hands out. |
rpgx_quest |
id, number (place in the list), name, repeatable, cooldown (seconds), min_level, reward (weapon class or NULL), xp, skill (a skill's id or NULL), skill_levels |
The server's quests. |
rpgx_quest_giver |
quest_id → rpgx_quest, spawn_id → rpgx_enemy_spawn |
n:m, which givers hand out which quests. |
rpgx_quest_requirement |
quest_id → rpgx_quest, required_id → rpgx_quest |
Quests that have to be finished first. |
rpgx_quest_task |
id, quest_id → rpgx_quest, number, type, data (the type's options, JSON) |
1:n, a quest's tasks in order. |
rpgx_quest_task_requirement |
task_id → rpgx_quest_task, required_id → rpgx_quest_task |
Tasks that have to be done first. |
rpgx_player_quest |
steamid → rpgx_player, quest_id → rpgx_quest, status (active / done), completions, tracked (the one on the HUD), done_at (last turned in, Unix time; for the cooldown) |
A player's quests. |
rpgx_player_task |
steamid → rpgx_player, task_id → rpgx_quest_task, progress (JSON) |
A player's progress in the tasks of active quests. |
rpgx_player_resource |
steamid → rpgx_player, resource, progress (spent towards the next training point) |
Per player and resource. (Tables made before skills also have an unused spec.) |
rpgx_player_training_point |
id, steamid → rpgx_player, resource, remaining (online seconds left) |
1:n, training points. |
rpgx_weapon |
class, resource, cost, damage, bullets, spread (NULL = unchanged), buffs (1 = its rpgx_weapon_buff rows replace SWEP.HitBuffs, none too) |
One per changed gun. |
rpgx_weapon_buff |
class → rpgx_weapon, buff (buff id), chance (percent) |
1:n, what a hit with the gun may apply. |
rpgx_weapon_field |
class → rpgx_weapon, field (Primary.Damage, RPM...), kind (number / bool / string), value |
1:n, a gun's changed SWEP values. |
rpgx_player_level |
steamid → rpgx_player, level, xp (towards the next level) |
Only players who have played since levels exist. |
rpgx_player_skill |
steamid → rpgx_player, skill (its id), level, granted (levels given by quests), spent (skill points put in) |
1:n, a player's skills. |
rpgx_setting |
name (level_formula, kill_xp_formula), value |
Set by commands (see Levels and skills). |
rpgx_sync |
name (quests), stamp |
Servers sharing a database: a new stamp with every change, so the others notice it (rpgx_quest_sync). |
Ids are 32-character strings made by the server, so a save never has to wait for the database to hand one back, and an item keeps the same id for its whole life. Inventories saved in the first version (one JSON row per player) are converted automatically on startup.
Saving things from code (addons too) #
Everything that's saved goes through RPGX.Database (server side, available once the
gamemode has loaded). Nobody writes SQL: you register tables and build queries, and it turns them
into whatever the server uses, sv.db or MySQL, so rpgx_db_migrate covers your data too.
Tables. Register them once, parents before the tables that reference them:
local DB = RPGX.Database
DB:RegisterTable( "myaddon_bank", {
columns = {
{ "id", "id" },
{ "steamid", "string" },
{ "balance", "int" },
{ "note", "text", null = true },
},
primary = { "id" },
foreign = { steamid = "rpgx_player(steamid)" }, -- ON DELETE CASCADE
index = { "steamid" },
} )
Types: id (use DB:NewId() for values), string, text, int, number, bool. Columns are
NOT NULL unless marked null = true; default = value gives a number or bool default.
Columns added to a spec later are added to the existing table on the next start (sv.db and
MySQL). They need null = true or a default, since the rows already there need a value.
Queries are built by chaining, then :Run( callback ):
-- Select: callback( rows, err ). Rows come back typed: numbers, true/false, strings.
DB:Select( "myaddon_bank" ):Where( "steamid", steamid ):Run( function( rows, err ) end )
DB:Select( "rpgx_inventory_item" )
:Columns( "class", "slot", "rpgx_player.name", { "rpgx_inventory.id", "inventory" } )
:Join( "rpgx_inventory", "rpgx_inventory.id", "rpgx_inventory_item.inventory_id" )
:LeftJoin( "rpgx_player", "rpgx_player.steamid", "rpgx_inventory.steamid" )
:Where( "slot", "<=", 10 ):Where( "rpgx_player.name", "LIKE", "Maff%" )
:OrderBy( "slot", "DESC" ):Limit( 20 ):Offset( 0 )
:Run( function( rows, err ) end )
DB:Select( "myaddon_bank" ):Where( "balance", ">", 0 ):Count():Run( function( count, err ) end )
DB:Select( "myaddon_bank" ):Where( "id", id ):First( function( row, err ) end ) -- row or nil
-- Writes: callback( ok, err )
DB:Insert( "myaddon_bank" ):Values( { id = DB:NewId(), steamid = steamid, balance = 0 } ):Run( callback )
DB:Update( "myaddon_bank" ):Set( { balance = 50, note = DB.NULL } ):Where( "id", id ):Run( callback )
DB:Delete( "myaddon_bank" ):WhereIn( "id", { a, b } ):Run( callback )
DB:Upsert( "myaddon_bank" ):Values( row ):Keep( "created" ):Run( callback ) -- insert, or update if the key exists
-- Several writes, all or nothing: callback( ok, err )
DB:Transaction( function( tx )
GAMEMODE:PlayerRecord( tx, steamid, name ) -- make sure rpgx_player has them
tx:Insert( "myaddon_bank" ):Values( { id = DB:NewId(), steamid = steamid, balance = 100 } )
tx:Update( "myaddon_bank" ):Set( { balance = 0 } ):Where( "id", oldId )
end, callback )
| Method | Does |
|---|---|
:Where( column, value ) / :Where( column, op, value ) |
op: = != < <= > >= LIKE IN. Several are ANDed. |
:WhereIn( column, list ), :WhereNull( column ), :WhereNotNull( column ) |
DB.NULL works as a value too. |
:Columns( ... ) |
"column", "table.column" or { "table.column", "alias" }. Default: every column of the starting table. |
:Join( table, a, b ) / :LeftJoin( ... ) |
Joins on a = b. A bare column name must belong to exactly one table in the query. |
:OrderBy( column, "ASC"/"DESC" ), :Limit( n ), :Offset( n ), :Count(), :First( cb ) |
Select only. |
:Values( row ) / :Set( values ) |
Only the columns present are written; DB.NULL writes a NULL. |
:Keys( ... ), :Keep( ... ) |
Upsert: what counts as "exists" (default: primary key), and columns only written on insert. |
- Every table and column name is checked against the registered tables; a typo is a Lua error right away, and values are always sent as parameters, so nothing can inject SQL.
UpdateandDeleteneed aWhere, so a whole table can't be wiped by accident.DB:IsReady()is false while MySQL is unreachable or a migration runs; queries then fail with an error instead of running. Save later rather than lose data.- Hook
RPGXDatabaseFlush(save everything now,wait= block) andRPGXDatabaseSwitched(reload afterrpgx_db_use) if you keep data in memory.