Skip to content

Retro Radial

This file is for developers integrating other resources with rs_radial or extending the system.

rs_radial is centered around:

  • a static config page system (Config.Pages)
  • a runtime API for other resources to add / remove / submenu items
  • server-side validation that re-runs visibility rules on dispatch
  • a predicate registry for sharing reusable canSee callbacks
  • a customer hook surface (custom_functions.lua) for inline action handlers

When integrating with it, think in terms of:

  • registering items from your resource
  • registering predicates your items can reference by name
  • listening to hook events (rs_radial:event:*)
  • respecting the server-authority model for any item with side effects

Client-side:

exports['rs_radial']:AddMenuItem('main', {
id = 'my_action',
label = 'MY ACTION',
icon = 'fa-bolt', iconType = 'class',
type = 'client', event = 'my_resource:client:doThing'
})
exports['rs_radial']:RemoveMenuItem('main', 'my_action')
exports['rs_radial']:UpdateMenuItem('main', 'my_action', { label = 'NEW LABEL' })
exports['rs_radial']:AddSubMenu('my_page', {
label = 'MY PAGE',
items = { ... }
})

Server-side, per-player:

exports['rs_radial']:AddMenuItemForPlayer(src, 'main', { ... })
exports['rs_radial']:RemoveMenuItemForPlayer(src, 'main', 'my_action')

When the resource that called AddMenuItem stops, rs_radial sweeps all items it owns. Reloads stay clean.

Reusable canSee callbacks shared across items.

Client-side:

exports['rs_radial']:RegisterPredicate('isOnDuty', function(ctx)
return ctx.onDuty == true
end)

Then in config.lua or any item:

{ id='x', label='X', visibleIf = 'isOnDuty', ... }

Server-side (for items with Config.Security.serverSideValidation = true):

exports['rs_radial']:RegisterServerPredicate('isCaptain', function(src)
return Bridge.getJob(src).grade >= 3
end)

rs_radial fires events for every significant action. Listen with AddEventHandler('rs_radial:event:<name>', fn).

Client hooks:

  • menuOpened, menuClosed
  • pageChanged(fromPage, toPage)
  • itemClicked(itemId, pageId, payload)
  • itemConfirmed(itemId, pageId) / itemCancelled(itemId, pageId)
  • itemFailed(itemId, reason) reason is one of 'cooldown', 'blocked', 'cuffed', 'dead', etc.
  • settingsSaved('save' | 'reset')
  • vehicleControlUsed(controlId, active)
  • blipToggled(catId, enabled)

Server hooks:

  • server:dispatched(src, pageId, itemId) fired AFTER a server-validated item runs
  • server:blocked(src, pageId, itemId, reason) fired when server validation rejects an item

For ultra-simple inline actions without registering a new event:

-- custom_functions.lua
CustomFunctions = CustomFunctions or {}
CustomFunctions.toggleEngine = function(args)
local veh = GetVehiclePedIsIn(PlayerPedId(), false)
SetVehicleEngineOn(veh, not GetIsVehicleEngineRunning(veh), false, true)
end

Then in config.lua:

{ id='engine', label='ENGINE', iconType='image', icon='engine.png',
type='customFunction', event='toggleEngine' }

custom_functions.lua is loaded BEFORE client.lua so the table exists when items are registered.

For items that take time server-side:

-- config.lua
{ id='heist', label='HEIST', async = true, progressText = 'Crackingโ€ฆ',
type='server', event='heist:start' }
-- server-side handler
RegisterNetEvent('heist:start', function()
local src = source
local ok, err = doHeist(src)
exports['rs_radial']:CompleteAsyncAction('heist', ok, err)
end)

The NUI shows a spinner from click until CompleteAsyncAction resolves it.

If your item visibility depends on a state bag (e.g. Player(src).state.isCop), tell rs_radial to refresh when the value changes:

exports['rs_radial']:WatchStateBagKey('isCop')

Auto-detected:

  • QBCore
  • Qbox / qbx_core
  • ESX

Standalone mode is supported push job/gang data manually via SetPlayerJob / SetPlayerGang.

Auto-detected:

  • ox_inventory
  • qb-inventory
  • qbx_core inventory
  • ESX
  • Custom (via Config.Inventory.customHasItem)
  • bridge/ox_lib.lua forward lib.addRadialItem / lib.removeRadialItem / lib.registerRadial calls into rs_radial. Opt-in.
  • No qb-radialmenu bridge ships by default the API surface is different. If you need one, the pattern is the same as bridge/ox_lib.lua.
  • Treat Config.Pages as the source of truth for static items; use the runtime API for items that only make sense for some players or some sessions.
  • Always set Config.Security.serverSideValidation = true for items with real side effects.
  • Use predicates instead of duplicating gating logic across items.
  • Register your hooks early so you donโ€™t miss menuOpened on the first NUI bootstrap.
  • Donโ€™t poll for state use WatchStateBagKey or call Refresh() from your job/state change event.
  • fxmanifest.lua load order and escrow_ignore
  • config.lua Config.Pages and feature toggles
  • custom_functions.lua customer hook surface
  • bridge/ox_lib.lua example of how a bridge layer is built
  • docs/configuration.md full config reference
  • docs/features.md item schema and action types

This is not a full internal architecture spec. It is an integration-oriented guide so another engineer can find the right entrypoints quickly.