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| Code | Meaning |
|---|---|
no-database | The bridge is not installed. |
query-failed | The 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
Apiand beforeStart, 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
ALTERby the operator. NeverDROP 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.Applyafter every table, throughOPX.Storage.EnsureIndex: it asksinformation_schema.STATISTICSand runsALTER TABLE … ADD KEYonly 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.
characteruses it (M.Storage.INDEXES) foridx_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.
The WebUI page
Every screen in opx_infinity is drawn by one CEF page built from Vue sources; Lua talks to it on named channels through OPX.UI, and modules keep their rules apart from the view.
Text and locales
Player-facing text comes from English and French catalogues looked up by key with OPX.Locale or the locale() shorthand; the server language is set once in config/shared.lua.