Skip to content

Import Modules

The 40 Lua modules behind the tlib global — access, config, permission, platform, kvp, settings and the rest.

lua
dependencies { 'tLib' }

shared_scripts {
    '@tLib/imports.lua',
}
lua
local kvp = tlib.kvp

Modules load on first access, so you declare nothing.

Declaring modules

tlib_modules { 'coords', 'kvp' } still works and makes a load eager, which fails at boot on a module that does not compile. Both spellings are accepted (tlib_module, tlib_modules).

DeclareWhy
realtimeRequired. The server bundle registers the rt* exports only if a manifest names it.
marketplaceRequired to register content types. Otherwise registerType returns false and warns; the resource starts, its types never register.
menuNeeded when loading the module is the wiring — it defines the globals MenuSchema and MenuCatalog, which shared files may read with no tlib.menu access to trigger the load.

occupancy and telemetry are in AUTOLOAD and load themselves in every consumer. telemetry defaults to studio-authored resources; declare it to opt in.


access

Decides who may use your resource, on the server, the way the owner configures — ACE, job, inventory item or their own function, all one shape.

lua
Config.access = {
    mode = "item",          -- everyone | ace | job | item | custom; a list passes if EITHER does
    ace  = "tdetector.use",
    job  = { police = 0, sheriff = 2 },   -- job = MINIMUM grade
    item = "radar_detector",
    count = 1,                            -- how many of `item`; default 1
    custom = function(src) return false end,
}
lua
local gate = tlib.access.gate(Config.access, { key = 'detector', label = 'the detector' })

RegisterNetEvent('tDetector:deploy', function()
    if not gate:allowed(source) then return end
end)
Server functionDescription
gate:allowed(src)The decision. Call it before every privileged action.
gate:push(src)Recompute and send the answer to that player's client mirror.
gate:invalidate(src)Drop the cached answer after changing what the gate reads. nil = all.
gate:describe() / gate:report()One line of prose for <resource>_diag; the same, structured. Assert on report.
access.jobs(src){ [job] = grade } from the detected framework.
access.count(src, item)How many of an item, from the detected inventory.
access.framework() / access.inventory()The name detected, or nil — not the adapter.
access.use(fw, inv)Pin detection. 'none', or 'auto' to resume detecting.
access.registerFramework(a) / registerInventory(a)Teach it about a fork.
access.problems(block, label, env)Check a block without building a gate.

The client half draws UI only:

lua
local gate = tlib.access.gate({ key = 'detector' })   -- same key as the server

if gate:allowed() then showButton() end
gate:onChange(function(allowed) toggleButton(allowed) end)
gate:refresh()   -- after going on duty, picking the item up
  • It fails closed, always. An absent, unstarted or throwing framework denies; a custom that errors or returns a truthy non-boolean denies; an unset ACE denies; a gate with config errors denies everyone and says why.
  • problems(block, label, env) with env = false catches file mistakes, which disable the gate. env = true catches mode = 'job' with no framework — never check that at startup, because an unstarted framework is indistinguishable from a missing one and the gate starts passing by itself when it appears.
  • registerFramework takes { name, detect, jobs } and must be called from a server_script, not the shared config.lua. jobs returns a map of job → grade, so a player who is both police and sheriff is expressible. Under escrow, keep one server file in escrow_ignore for it.
AxisSupported, in detection order
Framework (job)qbx_core, qb-core, ND_Core, es_extended, ox_core, nat2k15framework
Inventory (item)ox_inventory, qs-inventory, codem-inventory, origen_inventory, core_inventory, tgiann-inventory, qb-inventory, ps-inventory, jpr-inventory
  • The axes are independent, so nat2k15 for jobs beside ox_inventory for items works, as does an item gate with no framework.
  • Detection needs the resource started, not present, and reruns whenever any resource starts.
  • Grades are normalised across qb, ESX and ND. A framework with no grade concept reports 0, so job = { LSPD = 3 } denies rather than ignoring the 3.
  • vRP has no adapter. Load a script with vRP.loadScript, call registerFramework, and report grade 0.

config

Validates an owner-edited config.lua once at startup and refuses to run on a broken one.

FunctionDescription
config.check(tbl, spec)Validates, prints every problem, returns false if any were fatal.
config.validate(tbl, spec)Same checks, returns { ok, errors, warnings }, prints nothing.
lua
local ok = tlib.config.check(Config, {
    file = 'shared/config.lua',
    keys = {
        detectionRangeMeters = { type = 'number', default = 500.0, min = 1, max = 2000 },
        speedUnit            = { type = 'string', default = 'kmh', oneOf = { 'kmh', 'mph' } },
    },
})
if not ok then return end
Spec fieldDescription
filePath shown in messages. Use the path the owner edits.
keysMap of key name → definition.
prefixTable name in messages. Default Config.
allowUnknownSkip unknown-key warnings. Only for a table that legitimately holds extras.

A key definition takes type (number, string, boolean, list, table), default, required, min, max, oneOf, and fix to override the suggested remedy.

  • Every problem is reported at once, each naming file, key, expected, found and a fix.
  • A default fills a key the owner omitted; a value the owner set is reported and left alone, never silently replaced.
  • An unknown key warns with the key probably meant, including unit-less near-misses.
  • A missing required key is fatal.

coords

Vehicle coordinate maths, local ↔ world.

FunctionDescription
Coords.localToWorld(right, fwd, up, pos, lx, ly, lz)Vehicle-local → world.
Coords.worldToLocal(right, fwd, up, pos, wx, wy, wz)World → vehicle-local.
Coords.buildBoneRotMatrix(rx, ry, rz)World-space rotation matrix from bone angles (ZXY order).
Coords.boneLocalToWorld(...) / Coords.worldToBoneLocal(...)Bone-relative ↔ world.
lua
local right, fwd, up, pos = GetEntityMatrix(GetVehiclePedIsIn(PlayerPedId(), false))
local wx, wy, wz = tlib.coords.localToWorld(right, fwd, up, pos, 0.5, 1.0, 0.0)

demo

Reports whether the server runs in demo mode — the tlib_demo convar, see Convars.

FunctionSideDescription
demo.active()bothtrue while demo mode is on. Cheap enough for a hot path.
demo.dropWrite(what)servertrue when the caller must skip a write. Prints one notice per target.

A resource on the normal primitives needs nothing. Your own writer that bypasses tlib.fs needs one line:

lua
local function saveData(json)
    if tlib.demo.dropWrite('data.json') then return true end
    return tlib.fs.writeResourceFile('data.json', json)
end

Never let demo.active() decide something a player benefits from being wrong about on the client. The client value is pushed by tLib, so it is correct whether the owner wrote set or setr.


devgate

A gate for a feature that exists on your own servers and is absent from the shipped build. tlib.devgate(name) returns a predicate for the convar name, read on every call, on both sides.

lua
local isInternal = tlib.devgate('my_resource_convar')

if isInternal() then
    RegisterCommand('my_internal_tool', runTool, false)
end
  • Put the call in a script not in escrow_ignore, or there is a convar name a customer can read.
  • Gate registration, not behaviour. No command, keybind, menu row, panel or export should exist at all — one that exists and refuses advertises the feature in cmdlist.
  • Read per call, so a replicated convar flipped mid-session applies at once. Where registration happens at load, off → on needs a restart.
  • A client VM only sees setr. A client gate on a set convar is permanently off.
  • 1, true, yes, on enable it; anything else, including unset, is off.

diag

Console blocks for the things an operator must go and fix. You write the prose; diag does the wrapping, dashed rules, ^colour codes and the [resource] prefix.

FunctionDescription
Diag.fatal(title, opts)Red, prefixed FATAL:. Nothing works until this is fixed.
Diag.error(title, opts)Red. Something is denied or broken; the resource still runs.
Diag.warn(title, opts)Yellow. Works, but not as intended.
Diag.info(title, opts)White.
Diag.render(level, title, o)The lines, without printing — for a support dump or a panel.
Diag.wrap(text, width, ind)The word wrap on its own.
Diag.said(key) / Diag.reset()Has this once key printed? Forget every once key.
lua
tlib.diag.fatal('Config does not exist. tRadio cannot start.', {
    cause = 'config/config.lua failed to load. Almost always a Lua syntax error.',
    fix   = 'Scroll UP: FiveM printed the exact file and line just before this message.',
    once  = 'no-config',
})

opts is { cause, fix, detail, scope, once }, all optional. A fix list renders as "Fix it one of these ways". It prints and never logs.


kvp

Typed KVP wrapper, prefixed per community server via tlib_community_id (needs setr) or sv_projectName — see Convars.

FunctionDescription
kvp.getString/getFloat/getInt/getBool/getJson(key, default)Typed reads.
kvp.setString/setFloat/setInt/setBool/setJson(key, value)Typed writes.
kvp.get(key, default) / kvp.set(key, value)Auto-detect the type.
kvp.has(key) / kvp.delete(key)Existence, removal.
kvp.key(key)The full prefixed key string.

settings

Typed settings store backed by KVP, with defaults, user-set tracking and server-pushed defaults.

FunctionDescription
settings.create(defs)A store from { key, type, default }[].
store:get(key) / store:set(key, value)Read; write, persisted to KVP.
store:isUserSet(key)true if the player set this explicitly.
store:applyDefaults(table)Apply server defaults only to keys the player has not set.
store:reset(key)Back to the default, and remove from KVP.
lua
local store = tlib.settings.create({
    { key = 'volume', type = 'float', default = 0.8 },
    { key = 'darkMode', type = 'bool', default = false },
})

modules

Server-side content discovery. A module is a folder plus a module.json declaring what it is, as in layouts/QRX-720/module.json.

FunctionDescription
modules.scan(basePath, kind, resourceName?)The ids of every usable module of kind, sorted.
modules.list(basePath, kind, resourceName?)The same as { id, manifest }.
modules.read(basePath, id, kind?, resourceName?)One validated manifest, or nil, err.
modules.load(basePath, id, fileName, resourceName?)Read a file out of a module folder.
modules.hasFile(basePath, id, fileName, resourceName?)Whether a module ships an optional asset.
modules.validate(raw, id, kind?)Validate a decoded manifest directly.
modules.clearCache()Clear the scan cache, for modules added at runtime.
modules.onRefusal(basePath, id, err)Replace with your logger; called for every folder refused.

schema, kind, id, name, version are required; author, license, description, slug optional. A marketplace sound module adds sound.file and may add sound.durationSeconds, sound.toneKeys, sound.peaks.

  • id must equal the folder name, byte for byte, which catches a rename on copy.
  • kind is declared, never inferred from location. A mismatch is refused at start rather than 404ing on ui.html on a player's machine.
  • sounds and themes are reserved and cannot be module ids.
  • Content comes from a live scan, so a folder dropped in at runtime is found without a restart — clearCache() makes it visible to a scan already run.

logger

Levelled console logging with one format everywhere: [scope][LEVEL] message.

Function / fieldDescription
logger.create(scope)A log(...) function that prefixes every line.
log(...) / log(..., level)Space-joined arguments, INFO by default. A trailing number is the level and is not printed.
logger.DEBUG / INFO / WARN / ERROR1 / 2 / 3 / 4.
logger.setLevel(level)Minimum level printed. Default INFO.

permission

Admin and permission checks matching tLib's own rules.

Server functionDescription
permission.isAdmin(src)true for tlib.admin or tlib.superadmin.
permission.level(src)0 none, 1 admin, 2 superadmin.
permission.has(src, key)Any key, resolved by tLib — ACE, or a registered framework resolver.
permission.hasBatch(src, keys)A key list in one round trip. { [key] = boolean }.
permission.hasAll(src, keys)true only if every key is held. An empty list is true.
Client functionDescription
permission.isAdmin() / .level()This player, as last answered by the server.
permission.onChange(fn)fn(isAdmin, level) on every change, and once immediately.
permission.refresh()Ask the server for a fresh answer.
  • isAdmin is answered inside your resource, so it works while tLib is still starting. has must ask tLib and returns false if tLib cannot answer, since a resolver may deny what ACE would grant.
  • Use onChange, not a single read of exports['tLib']:IsAdmin() at start — that export is cached when tLib starts, so a revoked ace leaves a panel drawing its admin section until the player reconnects.

net

A net event with its guard built into the registration.

net.fromServer (client). Runs the handler only for an event that really came from the server (source == 65535), not a local TriggerEvent from another resource or a cheat menu. A rejection is reported once per event name, never per call.

lua
tlib.net.fromServer('myres:serverDefaults', function(defaults)
    applyServerDefaults(defaults)
end)

net.fromClient (server). The source guard, a rate limit and an optional permission gate, applied once. The handler is called as fn(src, ...).

lua
tlib.net.fromClient('myres:saveLayout', {
    allow    = function(src) return gate:allowed(src) end,
    onDenied = function(src) TriggerClientEvent('myres:denied', src) end,
}, function(src, layout)
    saveLayout(src, layout)
end)
OptionDefaultDescription
rateLimit1000Minimum ms between one player's calls. 0 disables it.
allownonefn(src) -> boolean. Only true is a grant; throwing denies.
onDeniednonefn(src), so you can tell the player.

A rate-limited call is dropped silently, so a feature that must reopen a panel it just closed uses panel:reopen() from opengate, not the request path.

opengate

The "may I open my panel?" handshake: the client asks, the server decides fresh on every open, and the answer is one of three things.

lua
tlib.opengate.serve({
    key      = 'settings',
    allow    = function(src) return gate:allowed(src) end,
    configOk = function() return myresConfigOk() end,
    payload  = function(src) return { isAdmin = tlib.permission.isAdmin(src) } end,
})
Server optionDefaultDescription
keysettingsNamespaces the wire events. Must match the client's.
allowrequiredfn(src) -> boolean. A missing one denies everyone and says so at boot.
configOknonefn() -> boolean. Omit it with no config spec to check.
payloadnonefn(src) -> table, travelling with the reply. Draw hints only.
rateLimit1000Passed through to net.fromClient.

Checks run in order: rate limit, configOk, then allow. A panel open to everyone writes allow = function() return true end.

lua
local panel = tlib.opengate.panel({
    key      = 'settings',
    onOpen   = function(data) openSettingsDialog(data) end,
    onDenied = function() toast("You don't have permission to use this.") end,
    onBroken = function() toast("This server's config has errors — ask an admin.") end,
})
Client memberDescription
panel:open()Ask the server. The answer arrives as one of the three callbacks.
panel:reopen()Re-run onOpen with the last answer, no round trip. false if there was never one.
onOpenRequired. fn(payload).
onDeniedRequired. A denial the player is never told about reads as a bug.
onBrokenOptional, but omitting it means a broken-config reply says nothing.

All three replies register through net.fromServer, so a local TriggerEvent can neither open the panel nor fake a denial. reopen() replays a grant and cannot create one.


platform

The FiveM native adapter, inside your VM.

Server functionDescription
platform.getPlayers() / getPlayerName(src)The roster. getPlayerName is nil for src <= 0 or an empty name.
platform.getPlayerPed(src) / getPlayerCoords(src)nil rather than a throw for a disconnected source.
platform.getPlayerFrontSeat(src)seat, netId — seats -1 and 0 only. A rear seat reads as nil, like on foot.
platform.getPlayerSeat(src)seat, netId across every index.
platform.getPlayerVehicleNetId(src)Requires OneSync.
platform.getNearbyPlayers(coords, radius)Server IDs by squared distance. {} if either argument is falsy.
platform.resolveNetworkId(netId) / isNetworkIdValid(netId) / doesEntityExist(e) / getEntityCoords(e)Entity lookups.
platform.triggerClientEvent(event, target, ...) / triggerEvent(event, ...)Event dispatch.
platform.now() / isServer()GetGameTimer(), and the duplicity check.
Client groupFunctions
This playergetMyId, getMyName, getMyPosition, getMySpeed, getMyEntityHandle, getClothingItem, isPlayerShooting, getActiveWeaponId, cancelAnimations, getIgnoredWeaponIds
This vehiclegetMyVehicle, getMyFrontSeat, getMySeat, getFrontSeatServerIds
Other entitiesgetRemotePlayerPosition, getNetworkEntityPosition, isNetworkIdValid, isEntityValid, getNetworkId, resolveNetworkId, getEntityPosition, getCameraState
BlipscreateMapMarker, removeMapMarker, isMapMarkerValid, moveMapMarker, setMarkerRotation/Icon/Color/Scale/ShortRange/Display/Opacity/Label/HeadingIndicator, getPlayerMapMarker
NUI focusshowCursor, releaseCursor, setInputMode, setKeepInputActive, releaseCursorSuppressPause, releaseCursorDeferred
KeybindsbindKey, keybinds, keybindConflicts
Voicevoice.isAvailable, voice.backend, voice.onAvailabilityChanged, voice.overrideProximityRange, voice.clearProximityOverride, voice.setSubmixForServerId, voice.setAudioInputIntent
Miscnow, getGameTime, loadData/saveData/removeData, sendToEngine, sendToServer, showChatMessage, emitEvent, getResourceId, wrapExport

getMyVehicle() answers nil or { handle, type, displayType, modelHash, isDriver, isEmergency, isEngineOn }, where type is heli | plane | boat | car and displayType is Air | Boat | Vehicle.

lua
tlib.platform.bindKey({
    id      = 'toggle',
    product = 'tRadar',
    action  = 'Toggle the radar',
    default = 'F5',
    press   = function() toggle() end,
})

Defaults: group keyboard, pair true, default '', which appends "(unbound)" to the label. The positional form bindKey(key, cb, eventType, bindingId, label) also works. Each bind is reported to the keybind registry.

MAX_SEAT_INDEX is 16.


occupancy

One shared answer to who is aboard a vehicle, in which tier, and which of them may act on it. The controller is the driver if one is seated, otherwise the front passenger. In AUTOLOAD.

Client functionDescription
occupancy.poll() / .state()A copy of { seat, vehicle, netId, driver, passenger, partner, isController, tier, cabinSeat, cabinVehicle, cabinNetId }.
occupancy.isController()May this client act on the vehicle?
occupancy.isFrontSeat() / .isDriver() / .isPassenger() / .isRearPassenger() / .isAboard()Predicates about this player; no arguments.
occupancy.tier()driver | passenger | rear | nil.
occupancy.myVehicle()handle, netId — nil unless in a front seat.
occupancy.cabin()handle, netId, seat, tier — any seat, so nil only on foot.
occupancy.partner()The other front seat's server id.
occupancy.onChange(fn)fn(state). Returns an unsubscribe function.
occupancy.startReporting() / .startSweep(ms) / .stopSweep()Opt in to the move ping and poll loop. Sweep default 250.
Server functionDescription
occupancy.isControllerOf(src, netId) / .isFrontSeat(src, netId) / .isAboard(src, netId)Gates. Each re-reads the host; none can be stale.
occupancy.refresh(src) / .refreshOccupant(src)Re-read and repair the cache. seat, netId[, tier].
occupancy.seatOf(src) / .occupantSeatOf(src) / .tierOf(src)The cache.
occupancy.frontSeats(netId) / .occupantsOf(netId) / .controllerOf(netId)By vehicle. Copies.
occupancy.partnerOf(src) / .peersOf(src)The other front seat / everyone aboard.
occupancy.onChange(fn) / .onCabinChange(fn)Front-seat edges / every-seat edges. Both return an unsubscribe.
occupancy.seatTier(seat) / .clear(src) / .startSweep(ms) / .stopSweep()Helpers. Sweep default 1000.
  • seat, vehicle, netId, partner and isController are front-seat-only and nil for a rear passenger, so state.seat ~= nil reads as "may I operate this". A rear passenger appears only in tier and the cabin* fields, and only onCabinChange fires for them.
  • tlib:occupancy:moved is one event name for every resource; its payload is a hint, since the seat is re-read from the host.
  • Neither sweep runs until you start it.

jsonstore

The single memoised writer for one resource's JSON data file, with per-owner schema seeding, versioning and forward migration.

FunctionDescription
jsonstore.open(opts)The store. opts.log is required; file defaults to data.json, resource to GetCurrentResourceName(). Memoised per resource and file.
store:read() / store:section(name)The whole document, or one section. {} when absent.
store:write(sections)Merge and persist. false when nothing reached the disk.
store:ensure(schema)Seed or migrate this owner's slice. Returns what it did.
store:schemaState(owner)That result again, or nil.
store:locked()Why writes are refused, or nil.
lua
local store = tlib.jsonstore.open({ log = tlib.logger.create('tDetector') })
store:write({ props = placements })
  • Migrations must be idempotent. A write can report success without the bytes landing, so the next start migrates again.
  • A migration dropping a top-level key not listed in allowRemove is refused and nothing is written. Write a rename as a copy plus an allowRemove.
  • A malformed schema raises rather than locking.
  • __schema is keyed by owner. Never stamp a version when applying defaults, or your migrations will skip it.

fs

One function. fs.writeResourceFile(relPath, contents, resource, log) returns true, or false, err with err never empty. resource defaults to GetCurrentResourceName(); log only controls the one-time warning.

lua
local ok, err = tlib.fs.writeResourceFile('data.json', encoded)
if not ok then log('could not write data.json — ' .. err, tlib.logger.ERROR) end

SaveResourceFile is tried first, falling back to the calling resource's own Node bridge, which exists only if that resource calls registerWriteResourceFile() from tLib/shared/ts/fs/server.ts and rebuilds its bundle; otherwise err says so by name. No server.cfg line changes this.


assets

Reconciles a stored list of operator-dropped files against what is in the folder.

FunctionDescription
assets.scan(subdir, opts)Sorted filenames, or nil, reason.
assets.reconcile(tracked, diskFiles, opts)list, changed. Pure.
assets.sync(tracked, subdir, opts)Both at once: list, changed, reason.
assets.slug(filename)The name with its final extension stripped.
assets.AUDIO{ "wav", "ogg", "mp3" }.
lua
local list, changed, reason = tlib.assets.sync(installedSounds, 'layouts/sounds',
                                               { extensions = tlib.assets.AUDIO })

changed reports membership, not length, so a one-in-one-out swap reports true. Retained records come back byte-identical. scan needs tLib's Node bundle (ScanResourceFiles); a missing export reads back as "unavailable" rather than throwing.


audio3d

Positional device audio: the server decides who is near enough to hear a vehicle-mounted device, and the client pushes one batched frame per tick into the consumer's own NUI.

lua
local bus = tlib.audio3d.createBus({
    event  = 'tradar:worldSound',
    range  = Config.audioRange,
    fields = { kind = { type = 'string', max = 16 } },
})
RegisterNetEvent('tradar:emitWorldSound', function(payload) bus:emit(source, payload) end)
Server memberDescription
bus:emit(src, payload)Receivers reached. 0 for every rejection — on foot, throttled, malformed, nobody near.
bus:sanitize(payload)The whitelisted fields, or nil if any present field is malformed.
bus:receivers(netId, sender)serverIds, coords.
bus:setRange(r) / bus:getRange()Metres, clamped to MAX_RANGE (250).
bus:stats(){ sent, delivered, throttled, rejected, notAboard, capped }.
bus:forget(netId)Drop a vehicle's throttle state.

createBus asserts event and range. Defaults: maxReceivers 24 (1–128), minIntervalMs 100 (0–10000), maxPerSecond 20 (1–120). The emitter is the vehicle, so a sender on foot emits nothing.

Client memberDescription
audio3d.createReceiver(opts)Asserts event, send, range. prefix defaults to audio3d, keepAliveMs to 6000.
rx:setLocal(netId)Mark the unit this player's own UI already sounds. nil on leaving.
rx:accept(payload)true only when the frame was forwarded to the NUI.
rx:pushFrame()How long to wait next: 250 idle, else 16/33/50 by listener speed.
rx:activeEmitters() / rx:getLocal() / rx:stats()State.
rx:setRange(r) / rx:stopAll()Range, and a full teardown.

Mark your own unit with setLocal, or the player hears two copies of the tone and they comb-filter.


framerelay

Publishes the device picture computed by the occupant who owns it onto the vehicle's statebag, for every other client in scope to read.

MemberDescription
framerelay.create(descriptor)Asserts key, frame, sanitise, differs.
relay.publishTick()Safe every frame; writes rarely.
relay.read(vehicle)The sanitised frame, or nil — covering none published, malformed and stale.
relay.withdraw()Clear the key. Idempotent.
relay.publishedVehicle() / relay.publishedSeq()State.
lua
local relay = tlib.framerelay.create({
    key       = 'tdetScreenFrame',
    frame     = function() return localFrame end,
    hasScreen = function(vehicle) return tdetGetAttachedProp(vehicle) ~= nil end,
    sanitise  = tdetScreenSanitiseFrame,
    differs   = tdetScreenFrameDiffers,
})

Defaults: absent hasScreen means yes, heartbeatMs 1000, staleMs 3000.

seq and netId are written by the relay and powered is read by it, so those names are reserved; powered = false publishes nothing. staleMs clears a crashed driver's last frame. publishTick reads occupancy.isController() live, so the occupancy sweep must be running.


drivenprop

Drives an unattached, frozen prop's world matrix from its parent vehicle's, each frame. Use it when a prop needs a non-unit scale: scale lives in the basis-vector lengths of SetEntityMatrix, which AttachEntityToEntity does not carry.

FunctionDescription
drivenprop.create(opts)A driver. opts.onError defaults to a print.
driver.add(prop, parent, placement) / .update(prop, placement) / .remove(prop)Membership.
driver.tick()written, skipped.
driver.write(prop) / .isDriven(prop) / .count() / .clear()The rest.
drivenprop.normaliseScale(v) / .isUnitScale(v)Clamped to [0.1, 10.0]; anything unusable lands on 1.0.
drivenprop.transform(basis, placement) / .localRotation(rx, ry, rz) / .toWorld(basis, L)The pure maths.
drivenprop.handedness(f, r, u) / .assertHandedness(...) / .dot / .crossBasis checks.
drivenprop.hasMoved(last, basis)Whether a rewrite is needed.
  • add detaches, freezes and disables collision, then writes the matrix. Undo any of the three and it breaks: attached loses the scale, unfrozen falls through the world, collision-enabled bounces off the vehicle.
  • remove and clear neither delete the prop nor re-attach it; re-attaching discards the scale.
  • transform raises on a left-handed basis, and returns the native's (forward, right, up, at) ordering, not the (right, forward, up) the rest of the module takes.
  • create starts a per-frame thread and an onResourceStop handler; importing does not. Share one driver per resource.

lifecycle

Keeps a registration against a dependency alive across that dependency's restarts. The worked example is in Menu Consumers.

MemberDescription
lifecycle.version1. Assert it.
lifecycle.watch({ resource, install, cleanup?, readyEvent? })The handle below.
handle.ready() / .retry() / .invalidate() / .dispose()Lifecycle. dispose is idempotent.
ctx.once(name, fn) / ctx.isActive()Inside install.
  • Watch a dependency, never yourself — your own resource name asserts.
  • ctx.once requires fn to return exactly true; a truthy non-true fails the step. Completed steps are remembered across retries within one generation.
  • Retries stop after 300 attempts 100 ms apart and print the last real error. Only another start event or your own retry() re-arms it.

Lets one resource expose navigation slots that other resources register links into. See Menu Consumers.

MemberDescription
menuhost.version1. Assert it.
menuhost.install(slots)Once only — a second call asserts.
RegisterPluginMenu(spec)Host-scoped export it registers. { id, label, menu, slot, description? }.
UnregisterPluginMenu(id)The other one.

Per slot, enabled defaults to on, an absent resources allowlist admits any resource, and an unranked entry sorts at 1000. A slot's menu must be one of the host's own pages and a plugin's menu one of the plugin's. The caller comes from GetInvokingResource(), so the host cannot register on its own behalf. install fires tlib:menu:hostReady before returning — wait on that, not on the host resource starting.


Loads tLib's menu validators into your VM: tlib.menu.schema is MenuSchema, tlib.menu.catalog is MenuCatalog.

Loading it also defines the bare globals MenuSchema and MenuCatalog. If your shared files read those globals, declare tlib_module { 'menu' }, or the lazy load has not run and they are nil.


healthcheck

Ordered HTTP GET probes against a base URL, with retries, delivering one aggregate result. tlib.healthcheck(opts) and .check(opts) are the same call and return nothing. Everything arrives at opts.callback as { success, steps, attempts, failedStep, message, url }, each step being { name, passed, status, error }.

  • Defaults: one step (reachable, path /), retries 2 (three attempts), retryDelay 1000, timeout 8000, delay 500. With no expect, a step passes on any status 200–599.
  • Give it opts.url or opts.waitEvent; waitEvent wins and takes the URL from the event payload. With neither it does nothing.
  • The sequence short-circuits, so result.steps is shorter than opts.steps on a failure and a retry restarts at step 1. A timeout is status = 0, not an error.
  • %port% in a step's errorMsg becomes the port parsed from the URL — 443, 80, or a literal ?.

marketplace

The consumer's half of the in-game marketplace: the payload that opens the browser, and the server-side installed-sound registry.

Client functionDescription
marketplace.openPanel(resource, layoutNames, soundEntries, opts)Open the standalone panel.
marketplace.provide(resource, layoutNames, soundEntries, opts)The same payload into a tab of a dialog you already opened. Takes no focus.
marketplace.baseUrl()The studio API host. Re-read per call, so tlib_api_base_url needs no rejoin.

opts carries initialTab (uis or sounds), previewFn, previewState and panelPreviewUrl, which lets a theme be shown on a real dispatch panel instead of as a swatch strip. Only tRadio publishes one.

Server functionDescription
marketplace.standardTypes(resourceName, opts)Register the standard ui and sound types and wire their lifecycle events.
marketplace.registerType(kind, installPath, warn)One type. true when tLib accepted it.
marketplace.upsertSound(installed, slug)The slug already owning that filename, or nil.
marketplace.removeSound(installed, toneWavMap, slug, scopedMaps)The filename removed, or nil.
marketplace.forgetSoundFile(filename, flatMap, scopedMaps)changed.
marketplace.soundConflict(conflict, slug, meta, warnEvent, warn)true only when a client was really messaged.
lua
tlib.marketplace.standardTypes(GetCurrentResourceName(), {
    note = note, warn = warn,
    layouts = { installed = onLayoutInstalled, uninstalled = onLayoutRemoved },
    sounds  = { warnEvent = 'tdet:soundOverwriteWarning',
                installed = onSoundInstalled, uninstalled = onSoundRemoved },
})

Paths default to layouts and layouts/sounds; layouts = false or sounds = false skips a type. note and warn take one argument, a message — not a logger plus a level. meta.src is nil when the installing admin disconnected mid-install.


hud

A transport bridge for a consumer with its own NUI that wants its widget in tLib's shared HUD editor.

FunctionDescription
tLibHud.register(id, definition)ok, err.
tLibHud.unregister(id)Also purges the cache, so the widget cannot come back.
tLibHud.set(id, layout)ok, err.
tLibHud.edit()Open the shared editor.
  • Updates arrive as SendNUIMessage({ tlibHud = data }) — one wrapper key, not an action string — queued until your NUI calls __tlib_hud_ready.
  • Definitions survive a tLib restart. Register once; never re-register on onClientResourceStart. A registration made while tLib is stopped, or refused because the HUD editor is open, is cached and retried for a minute.
  • When tLib stops you receive the last committed geometry with phase = 'rollback' and editing = false. Handle that phase, or a stop mid-edit leaves a preview the player cannot cancel.

ratelimit

A per-player minimum interval on a net event, as a closure you call at the top of the handler.

FunctionDescription
ratelimit.bucket(name, intervalMs)Returns fn(src) -> boolean. true means refused.
ratelimit.forget(src)Drop one player's timestamps. nil drops every player.
ratelimit.reset()Drop the buckets themselves. Tests only.
lua
local gate = tlib.ratelimit.bucket('requestVehicleProps', 1000)

RegisterNetEvent('tdet:requestVehicleProps', function()
    local src = source
    if not src or src <= 0 then return end
    if gate(src) then return end
end)

gate(nil) answers false. Names are scoped to your own VM; the same name twice hands back two closures over one timestamp table and the last intervalMs wins. Disconnected players are swept for you. net.fromClient already applies a rate limit, so use this directly only for something that is not a net event, or to put several events on one budget.


realtime

Rooms, typed events and the upstream control socket, over tLib's WebSocket router. tlib_module { 'realtime' } is required, or the rt* exports are never registered.

FunctionDescription
realtime:issueToken(playerId, opts){ token, expiresAt, wsUrl }. opts.meta travels as JSON.
realtime:connectionInfo(){ wsUrl, port, host }.
realtime:kick(playerId, reason)Sessions closed. reason defaults to kicked.
realtime:setRooms(playerId, roomIds) / :getRooms(playerId)Membership. getRooms never returns nil.
realtime:sendEvent(playerId, type, payload)Sessions reached.
realtime:broadcastEvent(roomId, type, payload, opts)Sessions reached. opts.exceptPlayerId skips one.
realtime:publish(roomId, bytes)A fanout frame, stamped senderId 0.
realtime:onConnect(fn) / :onDisconnect(fn)fn(playerId, sessionId, meta) / (…, code, reason).
realtime:onEvent(type, fn) / :onEvent(fn)One type, or all as fn(playerId, sessionId, type, payload).
realtime:stats()Transport counters.
realtime:startControl(cfg) / :stopControl()The upstream socket. cfg is { license, tokenEndpoint }.
realtime:sendControl(type, data, correlation)One control frame.
realtime:onControlRecv(fn) / :onControlState(fn)fn(type, data, correlation) / fn(state, wsUrl).
realtime:controlState()The state string, or nil.
lua
tlib.realtime:setRooms(playerId, { 'radio:freq:154.125' })
tlib.realtime:onEvent('ptt', function(playerId, sessionId, payload) end)

Handler lists are per consumer VM, so two resources can subscribe to one event. A handler that throws is caught and printed, not propagated. Audio frames do not come through here.


inference

Announce a model runtime this resource started, and find one another resource already has up for the same weights. Colon-called, like realtime. The exports underneath are in Exports; to start the engine, see Child processes.

FunctionDescription
inference:advertise(descriptor)A handle, or nil if the advertisement was rejected.
inference:lookup(capability, workload)Another resource's advertisement, or nil. Never your own.
inference:withdraw(handle)Drop one of yours. Stopping the resource does this anyway.
inference:list()Everything registered, for diagnostics.
lua
local shared = tlib.inference:lookup('text', 'qwen3-1.7b-q4_k_m')
if not shared then
    local handle = tlib.inference:advertise({
        capability = 'text',
        workload   = 'qwen3-1.7b-q4_k_m',
        url        = 'http://127.0.0.1:18291/v1',
        model      = 'local',
    })
end

Give url as the full URL including the route; nothing is appended. Only 127.0.0.1, [::1] and localhost are accepted. Re-advertising replaces every entry for the same resource and capability, so moving an engine to a new port needs no withdraw.


soundstore

The tone-key to .wav registry behind a resource's sound settings, with a boot reconciliation against what is on disk.

FunctionDescription
soundstore.open(opts)The store. opts.log is required.
store:config(){ soundWavMap, installedSounds, defaultSoundKeys }.
store:setMapping(key, filename)Map a key to a file; an empty filename clears it. Saves.
store:removeSound(filename)Forget a file everywhere it is mapped. Saves.
store:install(slug) / store:uninstall(slug)The slug already claiming that filename / the filename removed. Saves.
store:save()Write now. Every mutator above already does.

Defaults: resource to GetCurrentResourceName(), dir to layouts/sounds, file to data.json, keys to {}.

An empty directory scan counts as a failed scan. The store shares your resource's jsonstore object.


secrets

Overlays a gitignored config.local.lua and then matching convars onto Config in place, so the tracked config file ships blank. Server-only.

FunctionDescription
secrets.overlay(config, opts)Apply both layers. { applied, errors, sidecar }.
secrets.read(resource)The sidecar table, or nil, err. nil, nil when there is no file.
secrets.credentialPaths(config)Dotted paths of every credential-shaped key, sorted.

Precedence is convar, then config.local.lua, then config.lua. A convar is named <prefix>_<dotted_path_with_underscores>. opts.convarPrefix defaults to the resource name lowercased, opts.resource to GetCurrentResourceName(), and opts.quiet is falsy, so it prints what it applied. The sidecar loads into an empty environment: it must return a plain table and can call nothing, not even print. A list in the sidecar replaces the one in config.lua rather than appending.


spatial

A throttled index of player positions: the cached counterpart to tlib.platform.getNearbyPlayers.

FunctionSideDescription
spatial.getNearbyPlayers(coords, radius)bothServer IDs within radius.
spatial.setReady()serverStart building the index.
spatial.setRefreshRate(ms)bothRebuild interval. Default 100.
spatial.clear()bothEmpty the index.

The two halves are separate indexes with separate clocks. setRefreshRate applies to the whole VM.


telemetry

The diagnostics pipeline: scrubbing, fingerprinting, operation timing and the batched send.

FunctionDescription
telemetry.error(err, site, kind)Report a fault. Returns the stack hash — even when deduped.
telemetry.warn(code, kind, message)Report a warning. A code that slugs to empty is dropped.
telemetry.timed(operation, thresholdMs, fn)Run fn, recording how long it took. Re-raises a throw.
telemetry.record(operation, thresholdMs, ms)The same, for something you timed yourself.
telemetry.consent(){ enabled, source, locked }. A cache, not the authority.
telemetry.flush()Send the pending batch now.
telemetry.scrub(text) / .scrubIdentifiers(text)Redact registered values / anything identifier-shaped.
telemetry.registerScrubValue(value)Add a value to redact. Refuses anything under 3 characters.
telemetry.createTimers(opts)A private { record, timed, reset }. opts.report is required.

While consent is off, events are dropped rather than queued, so turning diagnostics on sends no backlog. With it on, the queue caps at 32 and discards oldest first. A fault is deduped for 120 seconds per fingerprint.


screentarget

Picks the one vehicle whose device screen this client renders — AddReplaceTexture is keyed on the texture name, so there can only be one. Your own vehicle always wins, otherwise the nearest in range, with hysteresis.

MemberDescription
screentarget.create(opts)An arbiter. opts.hasScreen is required.
self.update()vehicle, netId, source — or a single nil when nothing qualifies.
self.current()The same, without re-deciding.
self.currentDistance()Distance at the last update; 0 for your own vehicle.
self.onChange(fn)fn(vehicle, netId, source). Returns an unsubscribe function.
self.reset()Forget the incumbent.
self.settings(){ range, switchMargin, dwellMs }.

Defaults: range 8.0 m, switchMargin 2.0 m (absolute metres, not a fraction), dwellMs 1500. source is own or nearest.

onChange fires only on a vehicle or source change, never on distance, so it is safe to rebuild a browser inside it. Treat update() returning nil as the signal to tear that browser down, or it paints stale pixels onto a vehicle the player has left. dwellMs delays displacement only. Create one arbiter per device kind.


runtimetxd

Calls CREATE_RUNTIME_TXD at most once per resource load, and mints names a restart cannot collide with.

FunctionDescription
runtimetxd.create(prefix)A per-resource txd owner.
txds.ensure()The txd handle, created on first call and cached.
txds.name() / txds.tag()The dictionary name, and the per-load tag inside it.
txds.textureName(stem, serial)<stem>_<tag>, or <stem>_<tag>_<serial>.
txds.count()How many times the native was called, not how many names.
lua
local txds = tlib.runtimetxd.create('tdet_scr_txd')
local name = txds.textureName('tdet_screen', serial)

Budget one permanently pinned streaming slot per resource load: the engine frees neither a runtime txd nor a runtime texture, so repeated restarts accumulate them. Reusing a name across a restart instead makes AddReplaceTexture resolve against the previous load's dictionary and silently do nothing.


soundfields

Builds the Sounds tab of a settings dialog and keeps it live, remembering which keys the open dialog drew.

FunctionDescription
soundfields.append(fields, opts)Append the dropdowns. Mutates fields; returns the keys drawn.
soundfields.refresh(dialogId, data, patch)Re-push options after a server change. Returns how many it patched.
soundfields.keyOf(fieldId) / .fieldId(key)Convert between a tone key and its field id.
soundfields.drawn() / soundfields.clear()The remembered key list; forget it.

The remembered list is module-level, so keep one dialog open at a time per resource. refresh patches what was drawn, not the current config, so a sound key added server-side after the dialog opened appears only at the next open.


propeditor and vehicleprop

The in-vehicle prop stack. vehicleprop owns the model registry, the placement store and the attach/detach lifecycle; propeditor owns the orbit camera and the editing UI. Create vehicleprop and reach the editor through instance.editor.

lua
local prop = tlib.vehicleprop.create({
    prefix = 'tdet', model = 'prop_cs_police_torch', decor = 'tdet_prop',
    saveEvent = 'tdet:savePlacement', removeEvent = 'tdet:removePlacement',
})
MemberDescription
instance.setRegistry(cfg) / .isRegisteredModel(n) / .resolveModel(n) / .defaultModel()The model registry.
instance.getPlacement(veh) / .setPlacements(src) / .getAttached(veh) / .detach(veh)Placements and props.
instance.reconcile() / .tick() / .reclaimOrphans() / .cleanupAll()Lifecycle, exposed so a suite can drive it without a thread.
instance.editorThe propeditor instance, including editor.driver.
propeditor.create(descriptor)The editor on its own, without vehicleprop.
propeditor.orbit.*The camera maths — new, eye, lookFrom, applyDrag, zoom, command.
vehicleprop.normaliseModelName(m) / .buildRegistry(builtin, cfg)Pure helpers.

create asserts prefix, model, decor, saveEvent and removeEvent.

  • Scale decides the attach mechanism. A unit scale uses AttachEntityToEntity; anything else is driven per frame through drivenprop. Never test "is this attached" to decide whether a prop is live — ask the tracking table.
  • CreateObject signals failure with 0, which Lua treats as true. if not prop then never catches it, and the prop simply never appears.
  • The placement, attached and model tables are emptied in place, never replaced.
  • A record the editor did not change round-trips byte-identical. receiveEvent accepts server-triggered events only (source == 65535).

versioncheck

tlib.versioncheck(opts) — the startup banner, version comparison and changelog. It is both callable and a table, so tlib.versioncheck(opts) and .check(opts) are the same call, alongside .github(repo), .studios(product, current) and .lean(...). It has its own page: Version Check.

Still stuck? Support can answer it directly.

Was this page helpful?