OPX//77
opx_infinityConcepts

Storage

Last updated on

How opx_infinity reads and writes the database through OPX.Storage, how modules add their tables and indexes with OPX.Schema, and what OPX.Carry keeps across a resource reload.

Characters, inventories, vehicles and the rest live in the server's MySQL database, reached through the platform's MySQL bridge (MySQL, an alias of Open77.database, which needs the database.access permission). All access goes through OPX.Storage in lib/server/storage.lua, server only.

Reading and writing

Every call yields — call it from a thread (a module's Start, a CreateThread, a command or event handler on a thread) — and answers a Result instead of raising.

local found = OPX.Storage.Single(
	'SELECT name FROM my_table WHERE citizen_id = @citizen',
	{ citizen = citizenId })
if not found.ok then
	return Open77.log.error('my_table read failed: ' .. tostring(found.detail))
end
local row = found.value   -- nil means "no row", not a failure
CodeMeaning
no-databaseThe bridge is not installed.
query-failedThe statement raised; detail holds the raw error (logs only).

Use named parameters (@citizen), not ?, and never put an SQL comment inside a statement.

Adding a table

A module adds its CREATE TABLE IF NOT EXISTS statements in Init:

function M.Init()
	OPX.Schema.Add({
		[[CREATE TABLE IF NOT EXISTS opx_thing (
			citizen_id VARCHAR(16) NOT NULL PRIMARY KEY,
			value INT NOT NULL DEFAULT 0
		)]],
	})
end
  • The server applies every statement once, after Api and before Start, stopping at the first failure.
  • A failed schema boots the server degraded, with OPX.BootError = 'schema failed: <table>': nobody can load a character.
  • There are no migrations: a table that changes shape needs a hand-written ALTER by the operator. Never DROP DATABASE — the server's grants live with it.

Adding an index

CREATE TABLE IF NOT EXISTS never touches an existing table, so a key added to it only reaches fresh installs. Declare indexes separately:

OPX.Schema.AddIndexes({
	{ TABLE = 'opx_thing', NAME = 'idx_opx_thing_value', COLUMNS = { 'value' } },
})
  • Applied by OPX.Schema.Apply after every table, through OPX.Storage.EnsureIndex: it asks information_schema.STATISTICS and runs ALTER TABLE … ADD KEY only when the index is missing, so it is safe on every boot.
  • An index that cannot be added is a logged warning, not a boot failure.
  • character uses it (M.Storage.INDEXES) for idx_opx77_characters_seen, so existing servers get that index automatically. API: OPX.Schema.AddIndexes.

Tables from the old resources

Several modules still read tables written by the old opx77_* resources at boot and adopt their rows (e.g. opx77_garages, opx77_dealership_previews), printing each adopted row as the config line that replaces it. Each module page names the tables it reads.

Keeping state across a reload: OPX.Carry

OPX.Carry.Save(namespace, value) and OPX.Carry.Load(namespace) keep a small value across a resource reload, using the platform's resource-private Open77.state.save/load. The weather module keeps the sky and clock this way.

Player state bags

A player's replicated state bag reaches every client in the same routing bucket. Since PR #93 the character module writes nothing there (it used to publish name, job, gang…), and clears the keys an older build left. Anything about another player is read on the server.

On this page