RPG Xdocumentation About RPG X

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_mysql and rpgx_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 in rpgx_sync; each server checks it every rpgx_quest_sync seconds 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 in DB: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.
  • Update and Delete need a Where, 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) and RPGXDatabaseSwitched (reload after rpgx_db_use) if you keep data in memory.