Storing data
A resource that declares [storage] gets its own database, dlmp.db, which the server keeps in its data/ folder across restarts. The accounts resource used on this page keeps one account per launcher installation; its code is covered by the server's tests.
Declare the storage
The manifest lists the migrations that create and update the schema:
toml
name = "accounts"
type = "library"
[server]
scripts = ["server.lua"]
exports = ["account_id"]
[storage.migrations]
sqlite = ["migrations/sqlite/001_accounts.sql"]Each migration runs once, in order, before the resource's scripts. Never edit a migration that has already run on a server; add a new one instead.
sql
-- One account per device credential. AUTOINCREMENT: the id of a deleted account is never handed out again, so data other
-- resources keep by account id cannot pass to someone else.
CREATE TABLE accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
credential TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
sign_ins INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
last_seen_at INTEGER NOT NULL DEFAULT (unixepoch())
);Query it
Every call queues the statement and returns immediately; the result is delivered to a callback in a later server step. The accounts sign-in waits for its query to finish before it admits the player:
lua
-- Device-only accounts: the credential the launcher proves for this server is the account, so a player gets the same
-- account back on every visit from the same launcher, with no password. Make it the server's auth resource with
-- `[auth] resource = "accounts"` in server.toml (and list it in `[resources] startup`). Other resources key their data by
-- `dlmp.import("accounts").account_id(player:id())`; a server with logins of its own replaces this resource.
local NO_CREDENTIAL = "Join through the DarqlineMP launcher to keep a profile here." -- at most 64 bytes
local SIGN_IN_FAILED = "Sign-in failed, try again later."
-- A returning device finds its account by the update; only a new one costs the second statement. Not an upsert: that
-- spends an AUTOINCREMENT id on every sign-in. NEW creates nothing when a parallel sign-in of the same device already
-- has, and that sign-in's account is then found by the update again.
local RETURNING = [[
UPDATE accounts SET name = ?, sign_ins = sign_ins + 1, last_seen_at = unixepoch() WHERE credential = ?
RETURNING id, sign_ins
]]
local NEW = [[
INSERT INTO accounts (credential, name) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM accounts WHERE credential = ?)
RETURNING id, sign_ins
]]
local account_of = {} -- player id -> account id, from the accept to the disconnect
-- Lets `pending` in as the account in `rows`, or turns it away when the storage failed or found none.
local function admit(pending, name, rows, err)
local account = rows and rows[1]
if not account then
print(("accounts: sign-in of %s failed: %s"):format(name, err or "no account"))
pending:reject(SIGN_IN_FAILED)
return
end
if not pending:accept() then
return -- left or timed out while the storage answered
end
account_of[pending:id()] = account.id
local kind = account.sign_ins == 1 and "new" or "returning"
print(("accounts: %s is account %d (%s)"):format(name, account.id, kind))
end
-- Queues `sql`; a full queue turns the connection away at once.
local function ask(pending, name, sql, params, callback)
local queued, err = dlmp.db.query(sql, params, callback)
if not queued then
print(("accounts: sign-in of %s not queued: %s"):format(name, err))
pending:reject(SIGN_IN_FAILED)
end
end
dlmp.add_event_handler("player_auth", dlmp.root, function(pending)
local credential = pending:credential()
if not credential then
pending:reject(NO_CREDENTIAL)
return
end
local name = pending:name()
local function load(callback)
ask(pending, name, RETURNING, { name, credential }, callback)
end
load(function(rows, err)
if not rows or #rows > 0 then
admit(pending, name, rows, err)
return
end
ask(pending, name, NEW, { credential, name, credential }, function(created, create_err)
if created and #created == 0 then
load(function(again, again_err)
admit(pending, name, again, again_err)
end)
else
admit(pending, name, created, create_err)
end
end)
end)
end)
dlmp.add_event_handler("player_disconnect", dlmp.root, function(player)
account_of[player:id()] = nil
end)
--- The account id of the connected player `player_id`, or nil before its sign-in finished.
---@param player_id integer
---@return integer?
function account_id(player_id)
return account_of[player_id]
enddlmp.db.querypasses the rows to its callback;execandtransactionmodify rows. Pass values as?parameters; never concatenate them into the SQL.- No Lua code runs between the statements of a single call, so put any condition that must hold at write time into the SQL (
WHERE money >= ?) rather than checking it in Lua first. account_idis an export: a resource that listsaccountsin its dependencies calls it asdlmp.import("accounts").account_id(player:id()).
The request queue, limits and storage lifecycle are described in Resources.
