Settings and the admin panel
Resources ship no config files. Every tunable is a setting: its default lives in code, the saved value lives in the tm_settings database table, and staff edit it in game from the admin panel (/tmadmin, or a key bound to it in the player's key settings).
Register settings (server)
Put the schema in data/defaults.lua and register it once at start:
tm.settings.register('tm_garages', {
{ key = 'impoundFee', type = 'number', default = 250, min = 0, max = 10000, integer = true,
label = 'garage.settings.impoundFee.label', description = 'garage.settings.impoundFee.description',
category = 'garage.settings.category.fees' },
{ key = 'announce', type = 'boolean', default = true, label = 'garage.settings.announce.label' },
}, { label = 'garage.title', icon = 'lucide:car' })| Field | Meaning |
|---|---|
key | Letters, digits, _, . and -. Unique inside the namespace. |
type | boolean, number, string, text (multi line), select, color (#rrggbb or #rrggbbaa). |
default | Must be valid for the type. An invalid default makes the setting fail to register (and logs why). |
min, max, step, integer | Number limits. Enforced on the server, not only in the panel. |
maxLength | String limit (500 by default, 4000 for text). |
options | For select: { 'a', 'b' } or { { value = 'a', label = 'garage.optionA' } }. |
label, description, category | Locale keys of your resource (plain text also works). The panel translates them into the current language. |
scope | 'shared' (default): also sent to clients. 'server': never leaves the server. |
restart | Shows "needs a restart" in the panel for settings that cannot apply live. |
Read and react
local fee = tm.settings.get('tm_garages', 'impoundFee') -- server and client, from a local cache
tm.settings.onChange('tm_garages', 'impoundFee', function(value) end) -- or '*' for every key
tm.settings.set('tm_garages', 'impoundFee', 300, source) -- server only, validated like an admin edit
tm.settings.reset('tm_garages', 'impoundFee') -- back to the code defaultReads never cross a resource boundary: tm_lib keeps each resource's cache fresh by event. Always react with onChange, never read once at load. On the client, get returns the fallback until the first sync arrives. Use tm.settings.sync('tm_garages') to wait for it. Changes are announced locally (tm_lib:settings:changed) and, for shared settings, to every client.
Persistence
Saved values are written in one batch about half a second after the last change (and when tm_lib stops). A failed write is retried on the next flush. Resetting deletes the row. A saved value that no longer validates (the schema changed) is ignored and the default is used.
Who may edit
Only players for whom tm.fw.isAdmin(source) is true: the framework's admin permission or the group.admin ace. Every request is checked on the server, rate limited, validated, and logged with the character id of the editor.
Custom admin panels
Settings panels appear on their own. A resource that needs its own screen (a player manager, a business editor) adds one:
tm.admin.registerPanel({ id = 'garages', label = 'garage.panelTitle', icon = 'lucide:car',
onOpen = function(source) openGarageEditor(source) end })The hub closes, then onOpen runs in your resource for that staff member. Panels are removed when your resource stops.
Theme
tm_lib registers the tm_lib namespace with the theme (theme.accent, theme.surface, theme.surfaceRaised, theme.radius, theme.scale, theme.font), and debug. The language is not a setting: it follows ox_lib's ox:locale convar. Changing a theme setting restyles every open Twilight Moon page at once: the accent color replaces the kit's blue scale, the surfaces and radius are CSS variables, the scale zooms the page. A resource can give itself a theme of its own with tm.theme.settings() (see Theme).