Exports
Every Lua export tLib registers, which side it lives on, and what it returns.
Call every export as exports['tLib']:FunctionName(...). Each section says which side it is
registered on.
Menu System
Client and server. One NUI menu shared by every resource. tLib owns the screen, the keyboard and the permission check; you declare pages and handle rows.
Pages register on the server, and every open is answered with that player's
ACE-filtered tree. Handlers, presenters and hooks register on the client. Menu IDs are
namespaced owner:name. Every export takes an optional trailing declaredOwner, used only
where the platform cannot identify the caller.
Menu Consumers builds one end to end.
Server — declaring the tree
| Export | Description |
|---|---|
DefineMenuNamespace(spec) | Required first. { ace, catalog?, order?, capabilities?, theme? }; ace gates the namespace. |
RegisterMenuDomains(domains) | Rate/cooldown domains for actions. |
RegisterMenu(menu, definition) | One page plus its authority. |
UnregisterMenu(menuId) | Remove a page. |
RegisterMenuAuthority(spec) / UnregisterMenuAuthority() | Answer for rows you do not own. |
OnMenuSnapshot(hook) | Adjust a player's tree as it is built. |
RefreshMenus() | Re-push every open menu. |
definition is { ace, actions = { [itemId] = { ace, run?, clientAction?, cooldown?, fixedValue?, domain? } }, theme?, rootVisible? }.
rootVisible = false omits the shared-root link only; it changes no permission.
Server — changing what is registered
| Export | Description |
|---|---|
PatchMenuItem(menuId, itemId, changes) | Patch one row. |
PatchMenuAction(itemId, changes) | Patch one action; finds the page itself. |
SetMenuLinks(menuId, links) | Replace dynamic submenu rows on your page. {} removes them. |
SetMenuItemVisible(itemId, visible) | Show or hide a row. |
SetPlayerMenuOptions(source, itemId, options) | One player's choices for a selector. |
SetMenuNamespaceTheme(theme) / SetMenuTheme(menuId, theme) | nil clears. |
Client — behaviour
| Export | Description |
|---|---|
RegisterMenuActions(handlers) | { [name] = function(value, context) end }. Installed whole — all or none. |
RegisterMenuCatalogProvider(provider) | Build the catalog once, off a function that may yield. |
RegisterMenuHooks(spec) | { snapshot, enter, leave, page, selection, close }. enter may refuse a navigation. |
RegisterMenuPresenter(spec) / UnregisterMenuPresenter() | Fill rows in as a page is presented. |
RegisterMenuStrings(dictionaries) | Translations for your rows. |
ConfigureMenuInterface(spec) | The root page. One screen, so the last caller wins. |
SetMenuEffect(name, enabled, definition) | A handler that keeps running after activation, re-checked against the player's grants. |
PublishMenuItem(name, changes) | Patch a row from the client. |
SetMenuItemState(name, value) / GetMenuItemState(name) | Read and write a row's value. |
ExecuteMenuAction(name, value, context) | Run an action as if the row was activated. |
OnMenuActionsCleanup(fn) / OnMenuActionsPersist(fn) | Resource-stop hooks. |
Client — the screen
Unscoped: the screen belongs to the player.
| Export | Description |
|---|---|
OpenMenu(menuId?) | Re-checks against a fresh snapshot. Refuses while a menu is up. |
CloseMenu() | The close key's teardown. No toggle: closing nothing answers false, 'Menu is not open'. |
IsMenuVisible(), GetMenuStackIds() | Current state. |
NavigateMenu(menuId), InvalidateMenuContext() | Move within the open tree. |
BindMenuKey(menuId) | Claim a key-binding slot; returns the command it runs under. |
SetMenuLocale(tag) / GetMenuLocale() | Row translation locale. |
SetMenuAnchor(anchor), SetMenuPalettes(value) | Placement and colour. |
PromptMenuText(title, initial, maximum, context) | The game's onscreen keyboard. Only inside an action handler, whose context it needs. |
SubmitFeedback(message, callback) | Send one player message to TIMMYG Studios. |
SubmitFeedback
For a resource whose settings live in a tLib dialog. A menu already grows a "Send feedback" row on its owner's root page.
local sent = exports['tLib']:SubmitFeedback(message, function(result)
if result.ok then return end
-- result.error == 'rate-limited' | 'invalid' | 'unreachable'
end)| Attribution | The calling resource, resolved by the runtime. It cannot be sent in another's name. |
callback | { ok, error? }; error is invalid, rate-limited or unreachable. |
| Return | false, reason on a malformed call — empty message, or no callback. |
| Limits | One per player per minute, six per server per minute, shared with the menu row. The endpoint counts a game server as one IP. |
A page
{ id = 'myres:tools', title = 'Tools', subtitle = ..., description = ..., parent = ..., items = { ... } }A row
kind is action, toggle, select, slider, submenu or status.
| Key | Type | Description |
|---|---|---|
id | string | owner:name, permanent: what the server authorizes and what state is stored under. |
label / description | string | The row text, and the line under the panel. |
value | boolean|string|number | A toggle's state, a slider's position. |
options / index | table / number | A selector's choices, and the current one. |
min / max / step | number | A slider's bounds. |
menu | string | A submenu row's destination. |
confirm | string | Ask this before the row runs. |
icon / rightIcon / rightLabel | string | Row decoration. |
Every kind on one page, in that order:
A namespace or page theme reskins the same definition:
Dialog System
Client. Modal form dialogs. Results arrive as local events and as optional inline callbacks — additive, not alternatives.
| Export | Description |
|---|---|
ShowDialog(id, opts) | Open one. Returns the resolved id, or nil, reason. |
CloseDialog(id) | Close it; fires the cancelled event. |
UpdateDialogField(id, fieldId, changes) | Patch one field of a live dialog. |
UpdateDialogFields(id, changes) | Patch several at once. |
A default md dialog, with a required text, a two-option select as a segmented control
and a checkbox:
ShowDialog options
| Key | Type | Default | Description |
|---|---|---|---|
title | string | — | Dialog heading. |
description | string | — | Subheading. |
submitLabel | string | "Submit" | Submit button label. |
cancelLabel | string | "Cancel" | Cancel button label. |
size | string | "md" | "sm", "md" or "lg". |
theme | string | — | Id of a registered tLib theme. |
blur | boolean | false | Blur the scene behind the dialog. |
resetState | boolean | false | Open at the defaults, not where the player left it. |
fields | table[] | — | Field definitions. |
A dialog reopens where the player left it — selected tab, scroll position, dragged panel
position — per dialog id. Values and validation state are never kept, and a tLib restart or
a reconnect forgets the rest. resetState = true opts out.
Field types
| Type | Extra keys |
|---|---|
text, number, password | placeholder, defaultValue, maxLength, min/max (number only) |
textarea | placeholder, defaultValue, maxLength, rows (default 3) |
select, dropdown, radio | options ({ value, label }[]), placeholder, defaultValue |
slider | min, max, step, defaultValue |
checkbox | defaultValue (boolean) |
button | variant (default / primary / destructive), confirm |
display | none |
All share id (required), type, label, description, required, disabled. An
unknown type logs a warning and falls back to text.
maxLength is enforced by the page, so a server-side cap is visible while typing rather
than on submit.
display
Text, not a control: the label renders at full opacity with no input beside it, carries no
value and is never submitted. Use it for a readout, a section heading or help text.
{ id = 'note', type = 'display', label = 'Patterns are saved per vehicle model.' }Confirming a dialog button
A button fires onButtonClick on press. Give it a confirm string and the press asks
that question first; only an explicit Confirm fires the callback. Escape and Cancel
answer it without firing, and the dialog stays open either way. variant = 'destructive'
is colour only, so a button that cannot be undone wants both:
{
id = 'wipe',
type = 'button',
label = 'Clear all patterns',
variant = 'destructive',
confirm = 'Every pattern on this vehicle is deleted. This cannot be undone.',
}One of each type in an lg dialog. A select with two options renders as a segmented
control:
Events
tLib:dialog:opened (dialogId) · tLib:dialog:submitted (dialogId, values), keyed by
field id · tLib:dialog:cancelled (dialogId)
local dialogId = exports['tLib']:ShowDialog('my_dialog', {
title = 'Player Info',
fields = {
{ id = 'name', type = 'text', label = 'Name', required = true },
{ id = 'age', type = 'number', label = 'Age', min = 1, max = 120 },
{ id = 'role', type = 'select', label = 'Role', options = {
{ value = 'civ', label = 'Civilian' },
{ value = 'leo', label = 'Law Enforcement' },
}},
},
})
AddEventHandler('tLib:dialog:submitted', function(id, values)
if id == dialogId then print(values.name, values.age, values.role) end
end)Toast / Notification System
Client. Non-interactable notifications stacked in a screen corner.
| Export | Description |
|---|---|
ShowToast(id, toastType, title, description, duration, theme) | Returns the resolved id, or nil, reason at capacity. |
UpdateToast(id, changes) | Merge changes into a live toast in place. |
DismissToast(id) | Dismiss one before it expires. |
DismissAll() | Dismiss this resource's visible toasts. |
IDs are local to the producing resource, and stopping a producer removes its persistent
cards. A type change or duration = 0 restarts the type default; text- or theme-only
updates keep the deadline. Limits: 32 active per producer, 256 total.
| Type | Duration |
|---|---|
success | 4000 ms |
error | 6000 ms |
warning | 5000 ms |
info | 4000 ms |
loading | Until dismissed or updated |
Each type carries its own icon and colour. Newest sits at the bottom:
Events: tLib:toast:shown (id, toastType, title, owner) · tLib:toast:updated
(id, patch, owner) · tLib:toast:dismissed (id, owner) · tLib:toast:dismissedAll
(owner)
exports['tLib']:ShowToast(nil, 'success', 'Saved', 'Your settings have been saved.')
local toastId = exports['tLib']:ShowToast(nil, 'loading', 'Saving...', 'Please wait.')
exports['tLib']:UpdateToast(toastId, { type = 'success', title = 'Done', description = 'Save complete.' })Theme System
Client. CSS-variable themes, global or scoped to one dialog, toast or menu. One id themes all three surfaces.
| Export | Description |
|---|---|
AddTheme(id, opts) | Register, or replace one with the same id. true on success. |
RemoveTheme(id) | Unregister. true if it existed. |
SetTheme(id) | Apply globally. "" clears. |
GetTheme(id) | A copy of one definition, or nil. |
GetThemes() | A copy of every theme, keyed by id. |
opts key | Type | Description |
|---|---|---|
name | string | Display name. |
description | string | Optional. |
cssVars | table | Variable name → value. Keys must not carry the leading --. |
extends | string | Theme id to inherit from; these vars overlay it. |
font | string | Font-family string; sets --font-sans. |
fontSize | string | Base font-size percentage, e.g. "87.5%". |
Events: tLib:theme:added, tLib:theme:removed, tLib:theme:set — each (id).
HUD Layout
Client. Puts your on-screen element in the shared HUD editor, so a player moves, scales and hides it alongside every other resource's HUD.
| Export | Description |
|---|---|
RegisterHudComponent(id, options, fn) | Register a draggable component. fn(layout) runs on every layout change. |
UnregisterHudComponent(id) | Remove it. |
GetHudLayout(id) | Its current layout, or nil. |
SetHudLayout(id, layout) | Move your own component from code. true, or false, reason. |
OpenHudEditor(options) | Open the editor. { demo = true } adds a sample component. |
CloseHudEditor() | Close it, if this resource opened it. |
ResetHudLayout(id?) | Restore factory placements and clear the saved record. No id means all; returns the count. |
options is { label, width, height, canHide?, canScale?, minScale?, maxScale?, persist?, minVisiblePixels?, minVisibleFraction?, defaults = { x, y, scale, visible, anchor }, layout? }. Layouts persist per server endpoint; without one they are session-only and tLib
says so at startup. For a persist component the record is written before fn runs, so
true means the position survived.
One editor, not one per resource:
defaults and layout are different things
defaults is the factory placement, where "Reset this" goes. layout is where the
widget is now. Pass both, always:
exports['tLib']:RegisterHudComponent('radio', {
label = 'Radio', width = 300, height = 440, persist = false,
defaults = factoryPlacement, -- where Reset puts it
layout = savedPlacement, -- where it is
}, onLayout)With persist = false you own the storage, and passing the saved position as defaults
makes Reset restore the placement the player is escaping. Pin tlib_min_version '1.0', the
first release carrying layout.
Every layout tLib is handed — saved record, layout, defaults — is validated
independently and falls through to the next when unusable, then clamped against the live
resolution so at least minVisiblePixels (96) or minVisibleFraction (0.4) stays on
screen. That runs on registration.
Units
x and y are a ratio of a 1080px-tall reference screen on both axes, the same unit
the widget's size scales by. Centre-anchored x is the exception and stays proportional to
width.
If a player loses their HUD, /tlibhud reset restores every component to its factory
placement and clears the saved layout — editor open or shut, whichever resource opened it.
Keybind registry
Client. The only place two different resources' bindings are visible at once — a
resource's own tlib.platform.keybindConflicts() sees only its own VM.
| Export | Description |
|---|---|
ReportKeybind(entry) | Record one bind: { token, resource?, label?, default?, group? }. tlib.platform.bindKey calls it for you. |
GetKeybinds() | Every bind so far, as { resource, token, label, default, group }. |
GetKeybindConflicts() | { { group, key, claims = { … } }, … }. |
Only token is required; resource defaults to ?, label to the token, group to
keyboard. A malformed entry is dropped silently rather than raised, so a typo shows up as
a bind missing from GetKeybinds(). An entry with no default never counts as a collision,
and a conflict needs more than one resource.
tLib prints its report ten seconds after start, once; a bind reported later is recorded but
misses it, so call GetKeybindConflicts() yourself after that. Nothing here reassigns a
key — act on a conflict by changing a default and shipping it.
Marketplace
Client and server. The in-game panel for browsing and installing published content.
| Export | Side | Description |
|---|---|---|
RegisterMarketplaceType(kind, opts) | Server | Declare a content type this resource provides. |
OpenMarketplace(opts) | Client | The standalone panel. { resource = 'tRadio' } scopes it; { initialTab = 'sounds' } picks the tab. |
ProvideMarketplaceData(opts) | Client | Same payload into a tab of a dialog you already opened. Takes no NUI focus. |
CloseMarketplace() | Client | Close the standalone panel. |
For ProvideMarketplaceData, give your dialog a tab with view = "marketplace" and call
it when the server answers with what is installed. Use OpenMarketplace where there is no
host dialog to embed in.
Logger
Client and server. Levels are 1 DEBUG, 2 INFO, 3 WARN, 4 ERROR. A trailing
number in the arguments is the level; everything else is tostring'd and space-joined.
| Export | Description |
|---|---|
CreateLogger(loggerId, printFn, opts) | A log function pinned to a source name. opts.minLevel overrides the global level for it alone. |
Log(loggerId, args) | Format and filter one line. Returns the string, or nil when filtered or empty. |
SetLogLevel(level) | Set the global minimum. |
local log = exports['tLib']:CreateLogger('myresource', print)
log('Server started') -- INFO
log('Something went wrong', 4) -- ERROR
-- Log() returns the line and leaves printing to you:
local line = exports['tLib']:Log('myresource', { 'Something went wrong', 4 })
if line then print(line) endHTTP Helper
Server. Non-blocking requests through FiveM's PerformHttpRequest. Not related to
tLib's public HTTP routing hub.
| Export | Description |
|---|---|
Fetch(opts, callback) | Make an HTTP or HTTPS request. |
opts key | Type | Description |
|---|---|---|
url | string | Required. Full URL including scheme. |
method | string | Default "GET". |
headers | table | Key/value table. |
body | string | Request body. |
Redirects are followed. There are no timeout or TLS-verification options. callback is
function(statusCode, body, headers, errorData). An HTTP error status is still a delivered
response, so check the status.
exports['tLib']:Fetch({ url = 'https://example.com/api', method = 'GET' },
function(status, body, headers, err)
if status ~= 200 then return print('Request failed:', status, err) end
print('Body:', body)
end)Permissions
Client and server. Levels: 0 none, 1 admin (tlib.admin), 2 superadmin
(tlib.superadmin).
| Export | Side | Description |
|---|---|---|
IsAdmin() | Client | true if the local player is at least admin. |
GetPermissionLevel() | Client | The local player's cached level. |
RefreshAdminPermission() | Client | Re-ask the server. |
IsPlayerAdmin(playerId) | Server | true if that player is at least admin. |
GetPlayerLevel(playerId) | Server | That player's numeric level. |
HasPermission(source, key) | Server | Check one permission key. |
HasPermissionBatch(source, keys) | Server | A key list in one call. Returns { [key] = boolean }. |
RegisterAdminEvent(eventName, handler) | Server | A net event that only fires for admins. Denials are logged. |
RegisterPermissionResolver(fn) | Server | Replace ACE checks with a framework's own system. |
Diagnostics consent
Server. Both exports cross as a JSON string. See tlib_telemetry in
Convars for what is collected.
| Export | Description |
|---|---|
GetTelemetryConsent() | { enabled, source, locked, convar, convarValue, queued, transport }. |
SetTelemetryConsent(enabled) | { ok = true, consent }, or { ok = false, reason, detail }. |
local c = json.decode(exports['tLib']:GetTelemetryConsent())- There is no
provisionedfield; never render a "cannot transmit" state. - It is not permission-gated. Gate it at your own call site.
- Never put it on an unauthenticated surface. tLib's hub binds
0.0.0.0with no auth on its index, so it calls neither export.
tlib_telemetry_status in the server console prints the same answer plus the queue depth
and whether the transport has given up.
Local inference
Server. For resources shipping their own model runtime. Everything crosses as a JSON string; an empty string is the universal "no".
| Export | Description |
|---|---|
InferenceAdvertise(json) | Register an engine you started: { capability, workload, url, model }. Returns a handle; the URL must be 127.0.0.1. |
InferenceLookup(capability, workload) | Another resource's engine for the same weights, or ''. Never your own. |
InferenceWithdraw(handle) | Drop one of yours. Stopping the resource does this anyway. |
InferenceList() | Everything registered, for diagnostics. |
Starting those engines is a separate, gated surface — see Child processes.
Child processes
Server. One line covers a whole box, however many resources on it ship a local engine:
add_unsafe_child_process_permission tLibtLib makes the call, so no other resource needs its own grant.
| Export | Description |
|---|---|
SpawnServe(json) | Start a long-lived process: { key, program, args, cwd, env, mode }. { pid } or { error }; a live key served twice returns the first pid. |
SpawnOutput(key, since) | { lines, next, alive, code } since your cursor. In bytes mode, { chunks, next, stderr, alive, code }. |
SpawnUnserve(key) | SIGTERM, then SIGKILL after a five-second grace. |
SpawnWrite(key, base64) | Write bytes to a bytes-mode process's stdin. |
SpawnRun(json) | A one-shot: { program, args, cwd, env, stdin }. Returns { job }. |
SpawnJob(id) | { state, code, stdout, stderr, error }. Reading a finished job forgets it. |
SpawnCancel(id) | Kill a running one-shot. |
SpawnList() | What is running and for whom. |
cwd is resource-relative and must be one the catalog declares for that program; omitted,
it is the binary's own directory. env names must be on the program's allowlist, so
LD_PRELOAD cannot be set. mode is lines (default) or bytes, which returns stdout
base64 with nothing dropped and keeps stdin open.
The caller names a program, never a path. tLib's catalog declares which resources may spawn, which programs each may run, where those live and what their arguments may look like. It resolves the executable inside the calling resource, rebuilds the argv from a declared shape, and execs with no shell. The catalog declares a binary per platform, so the caller never says which it wants and nothing shells out to PowerShell.
Lifecycle
A child belongs to tLib's runtime, not its owner's. tLib reaps it when the owning
resource stops (including the stop half of a restart), when the owner calls SpawnUnserve,
and when tLib stops. A caller restarting is kill-and-respawn, not reuse.
Binary integrity
These binaries are verified where they are fetched, not where they are run. This gate adds containment and argument shape, not integrity.
Utilities
| Export | Side | Description |
|---|---|---|
GenerateId() | Both | A unique id string; the generator behind dialogs and toasts. |
Platform() | Client | tLib's platform adapter as a table of callable references. |
CreateDiscovery(opts) | Server | Scan resources for tagged config files, load and save them, and pick up late starters. |
CreateServerSettings(opts) | Server | Admin-configurable settings for one resource, persisted to a JSON file inside it. |
CommunityConfigsImport(...) / CommunityConfigsUpload(...) | Server | Community configs API proxy; the consumer repeats no HTTP boilerplate and never sees the server hash. |
CommunityConfigsPeek(id, cb) | Server | Import's payload without incrementing the import counter. Use it to render a listing. |
CommunityConfigsList(opts, cb) | Server | Browse published configs. { script, configType, key?, limit? }; first two required, limit default 30. cb(ok, items, err), items decoded. |
CommunityConfigsMatch(opts, cb) | Server | Candidates by model fingerprint. { fp, script?, configType?, key?, limit? }; fp required, configType default led_layout. Each item carries the publisher's own fp — comparing it is yours to do. |
IsDemoMode() | Server | true while tlib_demo is on. The export behind tlib.demo.active(). |
CreateDiscovery needs resourceName = GetCurrentResourceName() when called at module init
time: GetInvokingResource() is nil during top-level execution, and without the name the
file I/O targets tLib instead of you.
Exporting a config into another resource
Discovery can move a config out of your data.json and into the resource that owns the
model — a vehicle pack ships tels.json beside its .yft, declares it in its own
fxmanifest.lua, and the config travels with the vehicle.
So the destination performs the write: tLib calls the target's own WriteResourceFile
export. Nothing is reported as exported unless the file landed.
The target decides which path you get:
- It runs its own code. Any resource whose server bundle calls
registerWriteResourceFile()fromtLib/shared/ts/fs/server.tsaccepts the export directly. tLib writes the JSON in and adds the metadata line to itsfxmanifest.lua. - It is a plain asset pack. No scripts, no bundle, no runtime to write with. tLib writes the finished file into its own directory and prints these steps to the player in chat and to the server console:
1. Copy tLib/export-staging/<target-resource>/<file>.json -> <target-resource>/<file>.json
2. Add this line to <target-resource>/fxmanifest.lua: tels_config 'tels.json'
3. Restart <target-resource> (or the server).
The staged file is the target's existing config with your model merged in. Until you copy it the config is still local and still served to clients.
The metadata key is whatever the consuming resource declares — tels_config for tELS,
tradio_config for tRadio — and the value is the file name relative to the resource root.
Without that line the file is never read.
tLib
Still stuck? Support can answer it directly.
Was this page helpful?
TIMMYG Studios