Skip to content

Writing your own bridge ​

A bridge adapter is one Lua file that tells tm_lib how to talk to a system it has no built in support for: your own framework, an inventory, a target script, a notification pack, a radial menu. Resources keep calling tm.fw, tm.inventory, tm.target, tm.notify, tm.showTextUI and tm.radial, and your file does the translating. The full function lists live in bridge.md. This page is the how-to.

Pick the kind ​

Every kind accepts the name custom. That name loads your file from a custom folder.

You want to connectConvarYour file(s)Runs on
A frameworksetr tm:framework custombridge/framework/custom/server.lua and client.luaboth
An inventorysetr tm:inventory custombridge/inventory/custom/server.lua and client.luaboth
A target scriptsetr tm:target custombridge/target/custom/client.luaclient
Notificationssetr tm:notify custombridge/notify/custom/client.luaclient
Text promptssetr tm:textui custombridge/textui/custom/client.luaclient
A radial (wheel) menusetr tm:radial custombridge/radial/custom/client.luaclient

Paths are inside the tm_lib folder. Create the custom folder yourself. Restart tm_lib after every change, because adapters load once at start.

Notifications, text UI and the radial menu have ready made templates in tm_lib/templates/bridge/, so start there (below). For the other three, copy a bundled adapter that is close to your system (for example bridge/target/ox_target/client.lua) and change it.

Files you add to tm_lib are never encrypted, and the manifest also lists every bridge/<kind>/custom folder in escrow_ignore, so a file placed there stays plain, readable Lua even if one is ever shipped in the download. Client adapters must match bridge/**/client.lua, which the manifest already lists, so no manifest edit is needed. Loading a file you added to an escrowed tm_lib has not been tested on a real escrowed download yet, so check the console after the first start.

Notifications, step by step ​

  1. Copy templates/bridge/notify/client.lua to bridge/notify/custom/client.lua.
  2. Set RESOURCE to the name of your notification resource.
  3. In show(item), replace the example export call with the one your resource documents. item.message is the main text, item.caption an optional second line, item.tone is 'success', 'error', 'warning' or 'info', and item.duration is milliseconds (0 means "until dismissed", nil means "your default").
  4. Add setr tm:notify custom to your server.cfg and restart tm_lib.
  5. Test (see below).

Leave dismiss and clear empty if your resource cannot remove a toast early. Keep isAvailable(): while your resource is stopped, tm_lib shows its own toasts instead of dropping the message.

Text prompts, step by step ​

Same steps with templates/bridge/textui/client.lua and setr tm:textui custom. One decision matters:

  • Your system shows one prompt at a time (most do): keep SINGLE = true. tm_lib shows the newest prompt, and when it is hidden it brings the previous one back. Your hide(id) is only called when nothing is left.
  • Your system can show several at once: set SINGLE = false. show and hide are then called for every prompt id, and your resource has to keep them apart.

You never need to answer isTextUIOpen or skip repeated calls. tm_lib already does both.

Radial menu, step by step ​

Same steps with templates/bridge/radial/client.lua and setr tm:radial custom. tm_lib keeps the whole list of items and menus and runs the action of a picked item, so your file only draws and erases. Things to decide:

  • Can your wheel open from code? Keep open if it can. Delete it if it only opens from its own key, and callers get false, 'unsupported'.
  • Does it have menus of its own? If it does, registerMenu creates one and an item with menu set opens it. If it does not, keep the menu in a table and nest it into the item when you add it (the qb-radialmenu adapter does that). tm_lib registers every menu before the items that point at them, and registers a menu again whenever it changes.
  • Set adapter.resource to your resource name. When it stops, tm_lib moves every item into its own wheel. When it starts again, or restarts (which wipes its items), tm_lib puts them back. Without it a restart is not noticed.
  • Picking an item: call item.onSelect() with no arguments. It is nil for an item that opens a menu. Never run the item's event or command yourself, tm_lib does that.
  • canShow: call it when you build the wheel if you can. If you cannot, ignore it, tm_lib checks it again when the item is picked.

Systems that open from their own key should also leave tm_lib's key alone: it already does nothing while another system owns the wheel.

Rules every adapter follows ​

  1. Same arguments, same results on every system. Translate differences inside the adapter, never in the caller.
  2. Never throw on purpose. A call that fails is logged and the player simply misses that one message. For actions return false, 'reason' (or nil, 'reason') instead of raising. A thing your system cannot do returns nil, 'unsupported'.
  3. Soft dependencies check the state. Ask GetResourceState(name) == 'started' before calling another resource, and provide isAvailable() where the kind supports it (notify, text UI and the radial menu do).
  4. Do not poll or allocate per frame. Adapters for framework, inventory and target run on hot paths. Cache handles and read from the system's own cached data.
  5. No config files. Anything a server owner should tune is a tm_lib setting, not a table in your adapter.
  6. Cross-resource calls use the resource's exports inside the adapter, the way the bundled adapters do.
  7. Put a comment at the top of the file saying what you checked it against and what the system cannot do. The bundled adapters all do, and it is the first thing the next person reads.

Checking your adapter ​

  • Console at boot. tm_lib checks your file against the contract and logs each missing required function once (... adapter "custom" (client) does not implement: ...). A missing function is replaced by one that answers nil, 'unsupported', so fix the list, do not ignore it.

  • File not found. For notifications, text UI and the radial menu, a missing custom file is logged and tm_lib shows its own toasts, prompts and wheel instead. For framework, inventory and target a missing file is logged and every call answers nil, 'bridge unavailable'.

  • Try it. From any client script on the running server:

    lua
    tm.notify({ message = 'Test toast', caption = 'second line', type = 'success' })
    tm.showTextUI('[E] Test', { id = 'test' })
    Wait(3000)
    tm.hideTextUI('test')

    Then switch back with setr tm:notify tm to compare against the built in toasts.

  • Test every system you claim to support. A bridge change is verified when it starts clean and the smoke test passes, not when it works on one server.