DirkScripts
Script Config
âšī¸ Client & Server Module
The scriptConfig module provides a full configuration system for resources â JSON Schema-driven defaults, database persistence, admin UI, live watchers, change history, and optimistic concurrency.
For the React/NUI admin panel side of this system, see dirk-cfx-react â Script Config.
#Full Setup Guide
Setting up scriptConfig in a new resource requires files on three layers: schema, Lua, and React. This section covers the schema and Lua layers. The React layer is covered in the dirk-cfx-react docs.
#1. Create Your Schema
Create a schema.json in your resource root. This JSON Schema defines every configurable setting, its type, and default value.
json{ "type": "object", "properties": { "basic": { "type": "object", "properties": { "debug": { "type": "boolean", "default": false }, "interactionType": { "type": "string", "default": "target" }, "maxPlayers": { "type": "number", "default": 32 }, "secretKey": { "type": "string", "default": "changeme", "x-serverOnly": true } } }, "rewards": { "type": "array", "x-arrayKey": "name", "items": { "type": "object", "properties": { "name": { "type": "string" }, "label": { "type": "string" }, "amount": { "type": "number", "default": 1 } } } } } }
You can optionally create a defaults.json with initial default values. If omitted, defaults are derived from the "default" fields in the schema.
#2. Set Up Your fxmanifest.lua
luafx_version 'cerulean' game 'gta5' shared_scripts { '@dirk_lib/init.lua', 'src/shared/*.lua', } client_scripts { 'src/client/*.lua', } server_scripts { '@oxmysql/lib/MySQL.lua', 'src/server/*.lua', } ui_page 'web/build/index.html' files { 'web/build/**', 'schema.json', 'defaults.json', 'locales/*.json', }
#3. Register on the Server
Create src/server/scriptConfig.lua:
luaCreateThread(function() lib.scriptConfig(schema, function(src) return IsPlayerAceAllowed(src, 'admin') end) end)
That single call handles everything:
- Waits for MySQL to be ready
- Creates the
dirk_scriptConfigDB table if needed - Loads stored settings from the database
- Smart-merges with schema defaults (schema is always source-of-truth)
- Computes a content-hash version for cache invalidation
- Fires all registered watchers with
source = "initial" - Registers all NUI callbacks for the admin panel
#4. Set Up the Client
Create src/client/scriptConfig.lua:
lualocal _ = lib.scriptConfig -- triggers lazy-load + KVP cache
The client:
- Checks the resource KVP cache for a fast-path load
- Calls the server with its cached version number
- If the server has a newer version, receives the delta and merges locally
- Caches updated settings to KVP for next startup
- Fires any registered client-side watchers
#5. Add Watchers (Server)
Register watchers in your server files to react to settings changes in real-time:
lua-- src/server/init.lua lib.scriptConfig.on('rewards', function(rewards) registerRewardItems(rewards) end) lib.scriptConfig.on('basic', function(basic) if basic.debug then RegisterCommand('debugMyResource', function(src) -- debug command logic end, true) end end) -- Wildcard: react to ALL changes lib.scriptConfig.on('*', function() regenerateConfigFiles() end)
#6. Add Watchers (Client â Optional)
lua-- src/client/controls.lua lib.scriptConfig.on('basic', function(basic) local controls = basic.defaultControls or {} RegisterKeyMapping('+myAction', 'Do Something', controls.main?._type or 'keyboard', controls.main?._key or 'E') end)
#7. Access Settings Anywhere
Read settings directly in any server or client file:
lua-- Direct access (available after init) local debug = scriptConfig.?.basic?.debug local rewards = scriptConfig.?.rewards or {} -- Via getter local value = lib.scriptConfig.get('basic.maxPlayers')
âšī¸ Optional helper pattern: Some resources define getLive* convenience functions in shared code for null-safe access:
lua-- src/shared/utils.lua function getLiveBasic() return scriptConfig.?.basic or {} end
This is entirely optional â you can access scriptConfig. directly.
#8. Build the Admin Panel (React)
See dirk-cfx-react â Script Config for the full React setup guide covering createScriptConfig, SettingsPanel, form sections, and the NUI callback flow.
#API Reference
#Client
#scriptConfig.get
Returns the current settings. Also callable via scriptConfig.().
lualocal settings = scriptConfig.get() -- or local settings = scriptConfig.()
#scriptConfig.on
Watch for settings changes. Returns an unsubscribe function.
lualocal unsubscribe = scriptConfig.on('basic', function(newValue, oldValue, meta) print('basic settings changed', meta.path) end, { immediate = true }) -- Stop watching unsubscribe()
| Parameter | Type | Required | Description |
|---|---|---|---|
| path | string | yes | Dot-separated path to watch, or '*' for all changes |
| cb | function | yes | function(newValue, oldValue, meta) |
| options | table | no | { once?: boolean, immediate?: boolean } |
immediate defaults to true â the callback fires immediately with the current value if settings are already loaded. once auto-unsubscribes after the first fire.
Path matching rules:
- Exact match: Watch
"basic", triggered by"basic"change - Parent match: Watch
"basic", triggered by"basic.debug"change - Child match: Watch
"basic.skillSettings", triggered by"basic"change - Wildcard:
"*"catches all changes
Watcher meta table:
| Field | Type | Description |
|---|---|---|
| path | string | Watched path |
| changedPaths | string[] | Which leaf paths actually changed |
| source | string | 'load', 'refresh', 'update', or 'initial' |
| current | table | Full current settings |
| previous | table | Full previous settings |
#scriptConfig.set
Update settings (admin only). Sends the change to the server.
luascriptConfig.set({ basic = { debug = true } }, expectedVersion)
| Parameter | Type | Required | Description |
|---|---|---|---|
| data | table | yes | Partial settings to merge |
| expectedVersion | number | no | Optimistic concurrency version |
#scriptConfig.getAll
Get the full unfiltered settings (admin only).
lualocal allSettings = scriptConfig.getAll(source)
#Server
#scriptConfig.(schema, canEditFn?, rules?)
Initialise the settings system. Call once during resource start. Creates the DB table, loads persisted data, and smart-merges with schema defaults.
lualib.scriptConfig(schema, function(src) return IsPlayerAceAllowed(src, 'admin') end, { migrations = { ['1.2.0'] = function(data) data.basic.newField = data.basic.oldField data.basic.oldField = nil return data end, }, ui = { command = 'myres:settings', help = 'Open settings panel', restricted = 'group.admin', }, })
| Parameter | Type | Required | Description |
|---|---|---|---|
| schema | table | yes | JSON Schema definition with defaults |
| canEditFn | function | no | function(src) â boolean â additional permission grant on top of the master ACE + overrides (see Access Control). Purely additive: cannot lock out the master. |
| rules | table | no | Migration and UI rules (see below) |
Save permission is gated by the master ACE list (
dirk_lib_master_groupconvar) and per-resource overrides first. YourcanEditFnonly fires as a fallback when the master/override check denies. If you want to let a non-master role edit just your resource, prefer adding a per-resource override in dirk_lib's Script Config tab â that survives in DB and doesn't require code changes.
#exports.dirk_lib:canEditScriptConfig(src, resourceName)
Authoritative server-side permission check. Returns true if the player can edit the named resource's scriptConfig (master ACE list + overrides + the consumer's own canEditFn). Useful when you want to gate a custom admin command or feature with the same access model dirk_lib uses internally.
luaif exports.dirk_lib:canEditScriptConfig(source, GetCurrentResourceName()) then -- show admin-only menu, run admin command, etc. end
#Rules
luarules = { migrations = { ['1.2.0'] = function(data) return data end, }, ui = { -- or false to disable UI entirely command = 'myres:settings', -- default: '{scriptName}:settings' help = 'Open settings', restricted = 'group.admin', openEvent = 'myres:openSettings', allowTargetArg = true, -- allow /command [playerId] }, }
#Schema Extensions
| Extension | Type | Description |
|---|---|---|
x-serverOnly | boolean | Marks a path as server-only â filtered from client responses and the admin panel |
x-renamedFrom | string | Declarative key migration â old path is automatically moved to new path on load |
x-arrayKey | string | Identity key for array-level smart merge (merge by unique field instead of by index) |
âšī¸ Smart merge behaviour: When settings load from the database, the schema is the source of truth. New keys from the schema get their defaults, removed keys are pruned, type mismatches revert to defaults, and arrays with x-arrayKey merge by identity field rather than position.
#scriptConfig.get
Get settings at a path.
lualocal val = scriptConfig.get('basic.maxPlayers') local all = scriptConfig.get() -- returns everything
#scriptConfig.set
Apply a partial settings update. Merges changes, persists to DB, broadcasts to clients, and fires watchers.
luascriptConfig.set({ basic = { maxPlayers = 64 } })
#scriptConfig.on
Same watcher API as the client side.
lualib.scriptConfig.on('basic', function(newVal, oldVal, meta) print('basic changed, source:', meta.source) end)
#scriptConfig.reset
Reset all settings to schema defaults.
luascriptConfig.reset()
#NUI Callbacks
These callbacks are registered automatically when you call lib.scriptConfig(). They power the admin panel:
| Callback | Purpose |
|---|---|
<resource>:getScriptConfig | Client fetch with version check â returns delta if changed |
<resource>:getFullScriptConfig | Admin panel full fetch (includes x-serverOnly paths) |
<resource>:updateScriptConfig | Save from admin panel with expectedVersion for conflict detection |
<resource>:resetScriptConfig | Reset to defaults |
<resource>:getScriptConfigHistory | Paginated audit log with search/filter |
<resource>:giveScriptConfigItem | Admin give-item action (for testing) |
#Database
Settings are stored in the dirk_scriptConfig table (created automatically):
| Column | Type | Description |
|---|---|---|
| script | varchar (PK) | Resource name |
| data | longtext | JSON settings data |
| client_version | int | Content-based hash (31-bit) for cache invalidation |
| resource_version | varchar | Resource version from manifest at last save |
| change_log | longtext | JSON array of audit entries (max 250) |
| last_editor | longtext | JSON { source, name, identifier } of last admin |
| lastupdated | timestamp | Automatic update timestamp |
Change log entry format:
lua{ at_unix = 1710454800, at_utc = "2024-03-14T15:20:00Z", script = "my_resource", admin = { source = 5, name = "Admin", identifier = "license:abc123" }, expected_version = 12345, applied_version = 67890, changes = { { path = "basic.debug", old = false, new = true }, { path = "basic.maxPlayers", old = 32, new = 64 }, }, }
#Version Conflict Detection
The system uses content-based hashing for optimistic concurrency:
- When settings are saved, a 31-bit hash is computed from the canonical JSON
- The admin panel sends
expectedVersionwith every update - If the server's current version doesn't match, the update is rejected
- The server returns the latest data so the admin can review and re-save
This prevents two admins from silently overwriting each other's changes.
#Watcher Patterns
#Common server-side patterns
lua-- Re-register useables when equipment config changes lib.scriptConfig.on('equipment', function() registerEquipmentUseables() end) -- Conditionally register debug commands lib.scriptConfig.on('basic', function(basic) if not basic.debug then return end RegisterCommand('debugInfo', function(src) -- debug logic end, true) end) -- React to ALL changes (e.g. regenerate config files) lib.scriptConfig.on('*', function() regenerateExportFiles() end)
#Common client-side patterns
lua-- Register keybinds from settings lib.scriptConfig.on('basic', function(basic) local controls = basic.defaultControls or {} RegisterKeyMapping('+myAction', 'My Action', controls.main?._type or 'keyboard', controls.main?._key or 'E') end)
#Shared-side patterns
lua-- Recalculate derived values used on both sides lib.scriptConfig.on('basic', function(basic) local skillSettings = basic.skillSettings or {} MySkill.baseLevel = skillSettings.baseLevel or 1 MySkill.maxLevel = skillSettings.maxLevel or 99 generateLevelMap() end)
#Real-World Example (dirk_fishing)
The fishing resource demonstrates the full system:
Server watchers (src/server/init.lua):
lualib.scriptConfig.on('equipment', function() registerRodUseables() end) lib.scriptConfig.on('basic', function() registerGuidebookUseable() end) lib.scriptConfig.on('stores', function() registerStoresAndPrices() end) lib.scriptConfig.on('fish', function() registerStoresAndPrices() end) lib.scriptConfig.on('baitDig', function() registerBaitDigUseables() end)
Wildcard watcher (src/server/install.lua):
lualib.scriptConfig.on('*', function() -- Regenerate ox.lua, qb.lua, esx.sql item definition files -- whenever ANY setting changes local basic = scriptConfig.?.basic or {} local fish = scriptConfig.?.fish or {} local equipment = scriptConfig.?.equipment or {} -- ... generate and save files end)
Client keybind registration (src/client/permit.lua):
lualib.scriptConfig.on('basic', function(basic) if permitControlsRegistered then return end permitControlsRegistered = true local controls = basic?.defaultControls or {} RegisterKeyMapping('+flipFishingPermit', 'Flip Fishing Permit', controls.flipCard?.main?._type or 'keyboard', controls.flipCard?.main?._key or 'r') end)
