Menu Consumers
Build a standalone resource on tLib's shared menu: ownership, the lifecycle adapter, host slots, and where a page may sit.
A consumer declares pages on the server and handles rows on the client. The screen, the keyboard hints and the breadcrumb are tLib's; the page, its rows and what they do are yours.
| Owner | Owns |
|---|---|
| tLib | Schema, rendering, navigation, authority, dispatch, state/effects, the shared adapters. |
| Consumer | Feature behaviour, menu declarations, configuration, commands, its own state. |
| Optional host | Public slots, placement policy, allowlists, ordering. Hosting does not make it the engine. |
ConfigureMenuInterface is shared player-UI policy, not plugin branding; use namespace or
page themes for a consumer's visual identity.
A minimal consumer
fx_version 'cerulean'
game 'gta5'
dependency 'tLib'
tlib_min_version '1.0'
shared_script '@tLib/imports.lua'
server_script 'server.lua'
client_script 'client.lua'local owner, menu = GetCurrentResourceName(), exports['tLib']
local lifecycle = tlib.lifecycle
assert(type(lifecycle) == 'table' and lifecycle.version == 1, 'Update tLib: lifecycle API v1 required')
lifecycle.watch({ resource = 'tLib', install = function(ctx)
ctx.once('namespace', function()
return menu:DefineMenuNamespace({ ace = 'mytools.use' })
end)
ctx.once('page', function()
return menu:RegisterMenu({
id = owner .. ':tools', title = 'Tools', parent = 'main',
items = {{ id = owner .. ':hello', label = 'Hello', kind = 'action' }},
}, {
ace = 'mytools.use', rootVisible = true,
actions = { [owner .. ':hello'] = { ace = 'mytools.use', clientAction = 'hello' } },
})
end)
end })local owner, menu = GetCurrentResourceName(), exports['tLib']
local lifecycle = tlib.lifecycle
assert(type(lifecycle) == 'table' and lifecycle.version == 1, 'Update tLib: lifecycle API v1 required')
local registration = lifecycle.watch({ resource = 'tLib', install = function(ctx)
ctx.once('actions', function()
return menu:RegisterMenuActions({ hello = function()
menu:ShowToast('hello', 'info', 'Tools', 'Hello!')
end })
end)
end })
RegisterCommand('mytools', function()
if not registration.ready() then return end
local ok, reason = menu:OpenMenu(owner .. ':tools')
if not ok then print(reason) end
end, false)Grant mytools.use to the group that should have it. The server still checks the
namespace, page and action ACE. Decode data into locals before passing it as an export's
last argument: a multi-return Lua function can fill the optional declaredOwner parameter.
Shared lifecycle adapter (API v1)
tlib.lifecycle.watch({ resource, install, cleanup?, readyEvent? }) runs in the consumer
VM, so its subscriptions survive a dependency restart.
| Returns | Description |
|---|---|
ready() | Registration completed for the current dependency generation. |
retry() | Request a new bounded attempt, if not already busy or ready. |
invalidate() | Cancel pending work, clear step checkpoints, invoke cleanup. |
dispose() | Invalidate and release event subscriptions. Idempotent. |
It retries up to 300 times 100 ms apart and reports the real last error. A supplied
readyEvent must carry the dependency's resource name as its first argument; unrelated
events are ignored.
install(ctx) should throw on failure. ctx.once(name, fn) requires fn to return true
(or false, reason to fail) and skips successful steps on later attempts in the same
generation — one step per non-idempotent registration. All steps reset on dependency stop,
and duplicate start/readiness events do not reinstall. ctx.isActive() lets yielding setup
code check its lifetime.
Avoid native gameplay effects in registration; use authorized actions and SetMenuEffect
for ongoing behaviour and revocation cleanup. An exhausted registration can retry on a later
readiness event or an explicit retry().
Assert the import version — lifecycle.version, menuhost.version — not the
resource's fxmanifest.lua version.
Optional host (API v1)
Any server resource can expose navigation slots:
local host = tlib.menuhost
assert(type(host) == 'table' and host.version == 1, 'Update tLib: menu host API v1 required')
host.install({
['tools.extensions'] = {
menu = GetCurrentResourceName() .. ':tools',
enabled = true,
-- resources = { somePlugin = true },
-- order = { ['somePlugin:entry'] = 10 },
},
})Install once, after declaring the host's pages. It exposes the host-scoped server exports
RegisterPluginMenu({ id, slot, menu, label, description? }) and
UnregisterPluginMenu(id). Readiness is tlib:menu:hostReady(hostResource).
A plugin watches its configured host with the lifecycle adapter and calls
RegisterPluginMenu in a checkpointed step after registering its own target page. The same
owner and id replaces an existing link, so retrying is safe, and only the calling resource's
namespace may be targeted. Host policy controls allowed callers and ordering; player
permissions are checked independently by tLib.
The host adapter composes slots sharing a page, so do not also write that page's dynamic
link collection with SetMenuLinks. When the engine restarts, consumers recreate their own
pages and host links: invalidate the host-link watcher when engine registrations are lost,
then retry it after the page has registered.
Placement is not permission ancestry
RegisterMenu(page, { ace, actions, rootVisible = false }) suppresses only the automatic
shared-root link. It does not hide the page from authorized direct access, host links or
search, and it changes no permission. Omit it or use true for a root entry.
Keep an independently usable page parented to main. Parenting it to a host page also makes
that host's ancestor permissions mandatory, including for direct command access.
The shared "Send feedback" row
tLib adds one row of its own to every namespace's entry page — the page whose parent
is main or absent. You do not register it and cannot.
The dialog sends the player's message to TIMMYG Studios with the resource name, its manifest
version, and tLib's random per-install id. Nothing else — no player name, licence, Steam
or Discord id, no server name, no IP.
- It does not need telemetry consent and is unaffected by
tlib_telemetry. See Convars. - It is not permission-gated, and no ACE hides it.
It appears only on an entry page, is hidden while the player is filtering, and a page with
no owner: prefix — the shared main root — gets no row.
Verifying a consumer
Test in game against a tLib restart, a permission revocation, a direct OpenMenu on a
root-hidden page, and a host that starts after you. Refresh resource discovery before a
reload — source edits alone do not update a running resource.
tLib
Still stuck? Support can answer it directly.
Was this page helpful?
TIMMYG Studios