Skip to content

Repository files navigation

cortex-lib

Version 2.2.1 License MIT FiveM cerulean Lua 5.4 React 18

Lightweight, modular UI and utility library for the Cortex ecosystem on FiveM.
Shared client / server helpers, NUI components, and game utilities through a single lib global.

Not affiliated with Cfx, FiveM, Rockstar Games, Take-Two Interactive, or any related entity.


Overview

cortex-lib is the shared foundation for Cortex resources. It lazy-loads only what you use, exposes a consistent lib API on both client and server, and ships a vendored React 18 NUI bundle so your UI never depends on a CDN.

Use it for notifications, progress bars, menus, radial menus, zones, callbacks, interactions, settings, and common game utilities without reimplementing the same helpers in every resource.

The cortex-lib resource is the sole owner of shared NUI rendering and focus. The external loader proxies UI-backed calls into that running resource; consumer-local helpers such as zones and points still execute in the consumer so their callbacks and lifecycle remain correctly owned.

-- any resource that includes @cortex-lib/init.lua
lib.notify({ type = 'success', description = 'Hello from cortex-lib!' })

lib.zones.box({
  coords = GetEntityCoords(PlayerPedId()),
  size = vector3(10, 10, 5),
  onEnter = function() print('entered') end,
  onExit  = function() print('exited') end,
})

Features

Module Context What it does
notify client HUD toasts, progress bars, alert dialogs, text UI and helpers (success, error, info)
callback shared Promise-style client ↔ server RPC with await support
menu client Keyboard and mouse NUI menus with nested options
radial client Pie / radial menu with nested submenus
zones client Poly, box and sphere zones — onEnter, onExit, inside
points client Distance-based point triggers — onEnter, onExit, nearby
raycast client Camera and coordinate raycasts
getters client Closest / nearby player, vehicle, ped and object queries
disablecontrols client Instance-based control locks (movement, combat, vehicle, mouse)
help client Persistent key-hint bar at the bottom of the screen
interaction client Owner-scoped screen and world prompt registry + 3D NUI renderer
settings client KVP-backed settings registry with built-in NUI settings menu
timer shared Pausable / resumable timer class
waitFor shared waitFor(condition, timeout) helper
utils shared JSON, KVP, distance, table and number utilities
cache client Live ped, playerId, serverId, vehicle, seat cache updated every 100ms

Client utilities from client/utils.lua and client/debug_panel.lua are also available (pool clearing, ped/vehicle helpers, camera direction, clipboard, debug panel).


Requirements

  • FiveM cerulean (fx_version cerulean, game gta5)
  • Lua 5.4 — every dependent resource must have lua54 'yes' in its fxmanifest.lua
  • cortex-lib must start first — before any resource that uses lib

Installation

1. Install the resource

Copy or clone into your server's resources, e.g.:

resources/[eco]/cortex-lib

2. Ensure start order

In server.cfg — before any dependent resource:

ensure cortex-lib

3. Load it in each dependent resource

In your resource's fxmanifest.lua:

fx_version 'cerulean'
game 'gta5'
lua54 'yes'

shared_script '@cortex-lib/init.lua'

That's it. lib and cache are now available globally in that resource.

Caution

If lua54 'yes' is missing, cortex-lib will throw on load.


Quick Start

Notifications

lib.notify({ type = 'info', title = 'Hello', description = 'World!' })
lib.notify({ type = 'success', description = 'Saved!' })
lib.notify({ type = 'error', title = 'Failed', description = 'Try again.' })

-- server -> client
TriggerClientEvent('cortex-lib:notify', source, {
  type = 'info', description = 'Sent from server!'
})

Callbacks

-- server
lib.callback.register('myResource:getData', function(source, key)
  -- Treat source/key as hostile input. Revalidate permissions, session state,
  -- ownership and gameplay prerequisites on the server before any mutation.
  return { source = source, key = key }
end)

-- client
CreateThread(function()
  local data = lib.callback.await('myResource:getData', false, 'test')
  print(json.encode(data))
end)

Zones & Points

-- box zone
lib.zones.box({
  coords = vector3(0, 0, 0),
  size = vector3(10, 10, 5),
  debug = false,
  onEnter = function() print('entered') end,
  onExit  = function() print('exited') end,
})

-- distance point
lib.points.new({
  coords = vector3(100.0, 200.0, 30.0),
  distance = 3.0,
  onEnter = function() lib.notify({ description = 'Near point' }) end,
})

Menus & Radial

lib.registerMenu({
  id = 'actions',
  title = 'Actions',
  options = {
    { label = 'Repair', icon = 'wrench', args = { action = 'repair' } },
    { label = 'Clean', args = { action = 'clean' } },
  }
}, function(selected, _, args)
  print(('selected %d: %s'):format(selected, args.action))
end)
lib.showMenu('actions')

lib.registerRadial({
  id = 'doors',
  items = {
    { id = 'door_fl', label = 'Front Left', onSelect = function() print('front left') end },
  }
})

lib.registerRadial({
  id = 'vehicle',
  items = {
    { id = 'engine', label = 'Engine', icon = 'engine', onSelect = function() print('engine') end },
    { id = 'doors', label = 'Doors', menu = 'doors' },
  }
})
lib.showRadial('vehicle')

Interaction Prompts

Display-only prompts. Your resource owns the input — cortex-lib only renders.

RegisterKeyMapping('+exampleAction', 'Example action', 'keyboard', 'E')
RegisterCommand('+exampleAction', function()
  -- your own distance / state / permission checks here
end, false)

lib.showInteraction({
  id = 'example-action',
  label = 'INTERACT',
  key = 'E',
  priority = 10,
})

-- replace all prompts owned by this resource
lib.setInteractions({
  { id = 'throw', label = 'THROW', key = 'G', priority = 20 },
  { id = 'aim',   label = 'AIM',   key = 'RMB', priority = 10 },
})

-- GTA-style target context: actions sharing this validated panel render as
-- labeled key discs, a divider, and one filled-center context marker.
local targetPanel = {
  id = 'social-target',
  label = 'STRANGER',
  variant = 'target',
}

lib.setInteractions({
  { id = 'greet', label = 'GREET', key = 'G', priority = 20, panel = targetPanel },
  { id = 'taunt', label = 'TAUNT', key = 'H', priority = 19, panel = targetPanel },
})

lib.hideInteraction('example-action')
lib.clearInteractions()

Screen interactions are press-only: perform the action once from the mapped +command after checking lib.isInteractionActive(id). Supplying holdDuration without a world anchor is rejected so the input behavior cannot contradict the one-press screen UI.

panel is optional and screen-only. The supported target variant shows each caller-supplied key label inside its white action disc and uses an outer ring with a filled center for the context marker. Optional panel.marker accepts one letter, digit, or ? and defaults to ?; for example, equipment can supply marker = 'A'. Numpad action discs stack a small NUM caption over the digit or Enter arrow, while the original key string remains the arbitration and accessible identity. id and label are bounded and sanitized at the registry boundary; panel data is copied in public snapshots so callers cannot mutate live renderer state.

World anchors (position / entity / bone)

Follow a bone-less or moving object's root transform:

lib.showInteraction({
  id = 'wallet-pickup',
  label = 'PICK UP WALLET',
  key = 'E',
  holdDuration = 350,
  anchor = {
    type = 'entity',
    entity = wallet,
    model = GetEntityModel(wallet), -- optional handle-reuse guard
    offset = { z = 0.08 },
    maxDistance = 2.0,
  },
})

Use a named entity bone when the exact moving part matters:

lib.showInteraction({
  id = 'vehicle-door',
  label = 'OPEN',
  key = 'E',
  priority = 100,
  holdDuration = 1200,
  anchor = {
    type = 'entity-bone',
    entity = vehicle,
    bone = 'door_dside_f',
    offset = { z = 0.08 },
    maxDistance = 2.0,
  },
})

-- static world position
lib.showInteraction({
  id = 'stash',
  label = 'OPEN',
  key = 'E',
  anchor = { type = 'world', x = 0, y = 0, z = 0 },
})
  • For world anchors, anchor.offset follows world axes. For entity and entity-bone anchors, it follows the entity's local axes.
  • The renderer revalidates entity existence, the optional expected model, range, and screen projection every frame. Bone indices are cached per entity/model pair and rebuilt automatically if the handle resolves to a different model.
  • World anchors use the same key disc and may opt into the outer hold-progress ring with holdDuration (100-600000 ms).
  • Start and cancel that ring from the owning +command / -command pair with lib.startInteractionHold(id) and lib.cancelInteractionHold(id). The ring is presentation only; the resource still measures elapsed time, revalidates the target, and owns the action.
  • lib.getInteractionState(id) returns an owner-scoped copy with active, visible, and (for a visible world prompt) distance. lib.isInteractionVisible(id) is the cheap boolean form. Check it before starting an anchored action, then revalidate entity identity and gameplay rules again before mutating anything.

Rules

  • IDs are scoped to the invoking resource.
  • Max 8 prompts per resource, 16 total in the client registry.
  • Prompts are removed automatically when their owner resource stops.
  • When keys collide, only the highest priority prompt is active. Gate gameplay mutations with lib.isInteractionActive(id).
  • A world prompt is visible only while its winning entry is in range and successfully projected on screen; callers cannot set renderer-owned visibility.
  • lib.startInteractionHold(id) and lib.cancelInteractionHold(id) are owner-scoped and require a world prompt that defines holdDuration.

Available via lib, lib.interaction, and exports['cortex-lib'].

Performance pattern

Register prompts on state transitions, not in a permanent Wait(0) loop. This keeps the export boundary and input normalization off the frame path. Repeating an identical showInteraction or setInteractions call is an optimized no-op, but event-driven ownership is still cheaper and easier to reason about.

Hot path Previous work Current work
isInteractionActive Deep-copy and sort the full registry per query Direct owner/id lookup against mutation-time arbitration
Capacity checks Rebuild the snapshot and scan owners Constant-time total and per-owner counters
Stable visible entity-bone prompt Resolve the bone and send NUI every frame Revalidate the model, reuse the bone index, and skip an unchanged NUI frame
Moving world prompt in React Update the root app state Coalesce to one animation-frame update in an isolated interaction surface
Stable vehicle seat cache Scan fixed seats every 100 ms Check the cached seat once; scan the vehicle's real seat range only after a change
Points with nothing nearby Resume an empty coroutine every frame No frame coroutine until the detector finds a nearby point
local benchPoint = lib.points.new({
  coords = vector3(-347.14, -133.42, 39.01),
  distance = 2.0,

  onEnter = function()
    lib.showInteraction({
      id = 'mechanic-bench',
      label = 'USE BENCH',
      key = 'E',
      priority = 50,
    })
  end,

  onExit = function()
    lib.hideInteraction('mechanic-bench')
  end,

  nearby = function(self)
    if self.currentDistance <= 1.5
      and IsControlJustReleased(0, 38)
      and lib.isInteractionActive('mechanic-bench')
    then
      -- The server must revalidate the job, inventory and bench proximity.
      TriggerServerEvent('mechanic:server:openBench')
    end
  end,
})

Key arbitration is recomputed only when the registry changes, so lib.isInteractionActive(id) is a direct owner-scoped lookup. World projection remains frame-bound only while a prompt is visible; distant anchors use an adaptive wait, stable entity bones reuse their lookup, unchanged frames do not cross the NUI bridge, and the React interaction surface is isolated from unrelated UI.

The scheduling follows the Cfx Citizen.Wait guidance: reserve Wait(0) for genuinely frame-bound work, adapt idle waits, and cache infrequently changing native results. The cache and points lifecycles are adapted from proven ox_lib cache and points patterns while retaining cortex-lib's existing public values and callback timing.


Settings

Settings are stored per-client with SetResourceKvp / GetResourceKvpString. Built-ins:

Key Type Description
notifySound boolean Master toggle for notification audio
notifySoundPreset string Default sound preset
notifyPosition string Default toast position (top-right, top, bottom, …)

Register your own tab:

-- lazy-load to avoid paying for what you don't use
local settings = lib('settings')
local soundOn = settings.getSetting('notifySound')

-- from another resource via exports
exports['cortex-lib']:registerSettings(
  'myResource',
  'My Resource',
  'myResource:',
  {
    { key = 'enabled', type = 'toggle', label = 'Enabled', default = true },
  },
  { enabled = true }
)

The third argument is either the legacy icon value or a validated KVP namespace ending in :. Built-in Cortex settings keep their existing cortex: keys; consumer tabs should use a stable resource-specific namespace such as myResource:. Duplicate tab IDs, field keys, and cross-resource registrations are rejected instead of silently overwriting another owner. Registration also rejects any concrete KVP key that would overlap a built-in field or a field owned by another registered tab; a prefix may be shared only when the resulting field keys remain distinct.

Shared UI Applications

registerUiApp, openUiApp, updateUiApp, and closeUiApp let a consumer drive a renderer hosted by cortex-lib without adding another React root. Registrations are owner-scoped and are removed when the consumer stops; NUI events are accepted only for the active owner/session.

The bundled weatherzonesEditor renderer is optional and exists for the external es_weatherzones integration. It is inert until that resource explicitly registers and opens the app; cortex-lib does not treat es_weatherzones as a dependency.


How It Works

Lazy loadinglib is a metatable with __index / __call. Consumer-local modules are loaded from imports/<module>/<context>.lua once and cached; shared files (shared.lua) are prepended automatically. UI-backed modules and direct client utilities resolve to cortex-lib exports so callbacks, NUI messages, focus and owner cleanup stay in the resource that owns the shared UI.

fxmanifest.lua          →  cerulean, gta5, lua54
resource/init.lua       →  internal lib / cache bootstrap
init.lua                →  external loader for other resources (@cortex-lib/init.lua)
imports/                →  modular lazy-loaded modules (client / server / shared)
client/                 →  directly loaded helpers + interaction renderer + debug panel
ui/                     →  React 18 NUI bundle (index.html, app.js, style.css) + vendored React
tests/                  →  in-game /cortex test menu and specs

Public functions are available through the global lib. Cortex-owned functions are also exported for explicit cross-resource use:

-- inside a dependent resource
lib.notify({ type = 'success', description = 'Hello!' })
local notify = lib('notify')
notify({ type = 'success', description = 'Hello!' })

-- cross-resource
exports['cortex-lib']:notify({ type = 'success', description = 'Hello!' })

Development & Testing

Enable the in-game test menu:

-- in fxmanifest.lua (development only)
client_scripts {
  'client/utils.lua',
  'client/interaction_renderer.lua',
  'client/debug_panel.lua',
  'tests/client/debug_commands.lua',
}

Restart the resource and run /cortex in-game. Covers notifications, progress bars, menus, radial, dialogs, text UI, debug panel and utilities.

Quick console checks (F8):

lib.notify({ type = 'success', title = 'Success', description = 'Works!' })
lib.progress({ label = 'Test', duration = 3000 })
lib.clearNotifications()

Node contract tests:

node --test tests/*.test.mjs

Full static gates (with a Lua 5.4 executable installed):

node --check ui/app.js
node --test tests/*.test.mjs
Get-ChildItem tests\*_spec.lua | ForEach-Object { lua $_.FullName }
.\scripts\validate-resource.ps1 -Path .
git diff --check

The release workflow runs the same checks before packaging, builds the ZIP from tracked files only, writes a SHA-256 checksum, extracts the exact archive, and repeats the manifest, JavaScript, Node, and Lua gates before publishing.

For an interaction performance comparison, restart cortex-lib, let each state settle for 10-15 seconds, and record the same route and camera movement before and after the change:

resmon 1
profiler record 500
profiler saveJSON cortex-lib-interactions.json

Capture at least: no prompts, one screen prompt, four static world prompts, four moving entity-bone prompts, and the 16-prompt collision limit. The Cfx profiler identifies resource threads and source lines; resmon/profiler results from a live FiveM client are the release measurement, while the repository tests only prove static contracts and deterministic operation counts.

See the official Cfx profiler workflow for profiler status, profiler view, and loading saved captures.

Note

tests/client/debug_commands.lua is for development — don't ship it enabled in production.


Configuration Reference

cortex-lib itself has no config.lua. Behaviour is driven by KVP settings and module options passed at call time. To add resource-specific settings, use lib.registerSettings as shown above.


Troubleshooting

Symptom Cause / Fix
Lua 5.4 is required error Add lua54 'yes' to the calling resource, not just cortex-lib
cortex-lib must be started before this resource Move ensure cortex-lib above dependents in server.cfg
NUI not showing Check ui_page loads and ui/app.js + ui/vendor/ are in files
Prompts not visible Max 16 total — check lib.clearInteractions() and owner scoping
Settings not persisting KVP is per-client and per-machine — not synced between players

License

MIT — see LICENSE.

Copyright (c) 2026 Ever3st

The embedded Barlow Condensed interaction font is distributed under the SIL Open Font License — see ui/barlow-condensed-OFL.txt.

About

Library inspired by ox_lib but not a clone. provides UI and reused functions across my script ecosystem. Always required for my scripts.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages