> ## Documentation Index
> Fetch the complete documentation index at: https://docs.awokenlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# QBCore Radial Menu

> Make qb-radialmenu render through Awoken UI without removing it.

QBCore servers ship with `qb-radialmenu`, which uses its own NUI and never touches ox\_lib. Awoken UI hooks the **ox\_lib radial**, so by default it doesn't render qb-radialmenu's items. This guide adds a small bridge to qb-radialmenu that routes everything through ox\_lib (and therefore Awoken UI) without removing the resource.

<Info>
  **QBX users:** `qbx_radialmenu` already runs on ox\_lib. No bridge needed, Awoken UI renders it out of the box.
</Info>

## Setup

<Steps>
  <Step title="Add ox_lib to qb-radialmenu's manifest">
    Open `qb-radialmenu/fxmanifest.lua` and add `@ox_lib/init.lua` as the **first** entry in `shared_scripts`:

    ```lua qb-radialmenu/fxmanifest.lua theme={"dark"}
    shared_scripts {
        '@ox_lib/init.lua',
        'config.lua',
        '@qb-core/shared/locale.lua',
        'locales/en.lua',
        'locales/*.lua',
    }
    ```

    Without this, the bridge can't reach `lib.addRadialItem` and bails on load with a console warning.
  </Step>

  <Step title="Paste the bridge at the bottom of qb-radialmenu/client/main.lua">
    Open `qb-radialmenu/client/main.lua` and paste the block below at the very end of the file, after the existing `exports('AddOption', AddOption)` and `exports('RemoveOption', RemoveOption)` lines. The new exports replace the originals because FiveM uses the last registered handler.

    ```lua qb-radialmenu/client/main.lua theme={"dark"}
    --[[
        Awoken UI bridge for qb-radialmenu.
        Converts qb-radialmenu items into ox_lib radial items and routes them
        through Awoken UI's radial overlay.

        Requires '@ox_lib/init.lua' in qb-radialmenu's shared_scripts.
    ]]

    if not lib then
        print('^1[awoken-bridge]^7 ox_lib not loaded, bridge aborted.')
        return
    end

    -- Helpers

    local function getPlayer()
        if QBCore and QBCore.Functions then return QBCore.Functions.GetPlayerData() end
        return PlayerData
    end

    local function isPoliceOrEMS(pd)
        return pd and pd.job and (pd.job.name == 'police' or pd.job.type == 'leo' or pd.job.name == 'ambulance')
    end

    local function isDowned(pd)
        return pd and pd.metadata and (pd.metadata.isdead or pd.metadata.inlaststand)
    end

    local function isHandcuffed(pd)
        return pd and pd.metadata and pd.metadata.ishandcuffed
    end

    -- Item conversion

    local function convert(tbl)
        if type(tbl.canOpen) == 'function' and not tbl.canOpen() then
            return nil
        end

        if tbl.items then
            local items = {}
            for _, v in pairs(tbl.items) do
                local c = convert(v)
                if c then items[#items + 1] = c end
            end

            local menuId = (tbl.id or 'qbrm') .. 'Menu'
            lib.registerRadial({ id = menuId, items = items })

            return {
                id = tbl.id,
                label = tbl.label or tbl.title,
                icon = tbl.icon,
                menu = menuId,
            }
        end

        local action
        if tbl.type and tbl.event then
            if tbl.type == 'client' then
                action = function() TriggerEvent(tbl.event, tbl) end
            elseif tbl.type == 'server' then
                action = function() TriggerServerEvent(tbl.event, tbl) end
            elseif tbl.type == 'command' then
                action = function() ExecuteCommand((tbl.event:gsub('^/', ''))) end
            end
        elseif tbl.event then
            action = function() TriggerEvent(tbl.event, tbl.args) end
        elseif tbl.serverEvent then
            action = function() TriggerServerEvent(tbl.serverEvent, tbl.args) end
        elseif tbl.command then
            action = function() ExecuteCommand(tbl.command) end
        end

        return {
            id = tbl.id,
            label = tbl.label or tbl.title,
            icon = tbl.icon,
            onSelect = tbl.onSelect or (action and function() action() end),
            keepOpen = tbl.keepOpen or (tbl.shouldClose == false) or nil,
        }
    end

    -- Push functions

    local function pushDefaultItems()
        if not Config or not Config.MenuItems then return end

        for _, item in pairs(Config.MenuItems) do
            if type(item) == 'table' and item.id then
                local c = convert(item)
                if c then lib.addRadialItem(c) end
            end
        end
    end

    local function pushJobItems()
        if not Config or not Config.JobInteractions then return end

        local pd = getPlayer()
        if not pd or not pd.job then return end

        local jobKey = pd.job.type == 'leo' and 'police' or pd.job.name
        local jobConfig = Config.JobInteractions[jobKey]

        lib.removeRadialItem('jobinteractions')

        if jobConfig and next(jobConfig) and pd.job.onduty then
            local c = convert({
                id = 'jobinteractions',
                title = 'Work',
                icon = 'briefcase',
                items = jobConfig,
            })
            if c then lib.addRadialItem(c) end
        end
    end

    local function nearestVehicle()
        local ped = PlayerPedId()
        local pos = GetEntityCoords(ped)
        local forward = GetOffsetFromEntityInWorldCoords(ped, 0.0, 20.0, 0.0)
        local ray = CastRayPointToPoint(pos.x, pos.y, pos.z, forward.x, forward.y, forward.z, 10, ped, 0)
        local _, _, _, _, vehicle = GetRaycastResult(ray)
        return vehicle
    end

    local function pushVehicleItems()
        lib.removeRadialItem('vehicle')

        local ped = PlayerPedId()
        local vehicle = GetVehiclePedIsIn(ped, false)
        if vehicle == 0 then vehicle = nearestVehicle() end
        if vehicle == 0 then return end

        local items = {}

        if Config.VehicleDoors then
            items[#items + 1] = deepcopy(Config.VehicleDoors)
        end

        if Config.EnableExtraMenu and Config.VehicleExtras then
            items[#items + 1] = deepcopy(Config.VehicleExtras)
        end

        if not IsVehicleOnAllWheels(vehicle) then
            items[#items + 1] = {
                id = 'vehicle-flip',
                title = 'Flip Vehicle',
                icon = 'car-burst',
                type = 'client',
                event = 'qb-radialmenu:flipVehicle',
                shouldClose = true,
            }
        end

        if IsPedInAnyVehicle(ped) and Config.VehicleSeats then
            local seatsMenu = deepcopy(Config.VehicleSeats)
            seatsMenu.items = seatsMenu.items or {}

            local seatTable = {
                [1] = Lang:t('options.driver_seat'),
                [2] = Lang:t('options.passenger_seat'),
                [3] = Lang:t('options.rear_left_seat'),
                [4] = Lang:t('options.rear_right_seat'),
            }

            local seats = GetVehicleModelNumberOfSeats(GetEntityModel(vehicle))
            for i = 1, seats do
                seatsMenu.items[#seatsMenu.items + 1] = {
                    id = i - 2,
                    title = seatTable[i] or Lang:t('options.other_seats'),
                    icon = 'caret-up',
                    type = 'client',
                    event = 'qb-radialmenu:client:ChangeSeat',
                    shouldClose = false,
                }
            end

            items[#items + 1] = seatsMenu
        end

        if #items > 0 then
            local c = convert({
                id = 'vehicle',
                title = 'Vehicle',
                icon = 'car',
                items = items,
            })
            if c then lib.addRadialItem(c) end
        end
    end

    local function refreshAll()
        lib.clearRadialItems()
        pushDefaultItems()
        pushJobItems()
    end

    -- External script API

    local function addOption(data, id)
        if type(data) ~= 'table' then return end

        if data.id or data.items or data.title or data.label then
            data.id = data.id or id
            if data.id then
                local c = convert(data)
                if c then lib.addRadialItem(c) end
                return data.id
            end
        end

        for _, item in pairs(data) do
            if type(item) == 'table' and item.id then
                local c = convert(item)
                if c then lib.addRadialItem(c) end
            end
        end

        return id
    end

    local function removeOption(id)
        lib.removeRadialItem(id)
    end

    exports('AddOption', addOption)
    exports('RemoveOption', removeOption)

    -- Keybind redirect + state guards

    RegisterCommand('radialmenu', function()
        local pd = getPlayer()

        if isHandcuffed(pd) or IsPauseMenuActive() then return end

        if isDowned(pd) then
            if not isPoliceOrEMS(pd) then return end

            lib.clearRadialItems()
            local c = convert({
                id = 'emergencybutton2',
                title = Lang:t('options.emergency_button'),
                icon = 'circle-exclamation',
                type = 'client',
                event = 'police:client:SendPoliceEmergencyAlert',
                shouldClose = true,
            })
            if c then lib.addRadialItem(c) end
        else
            refreshAll()
            pushVehicleItems()
        end

        TriggerEvent('qb-radialmenu:client:onRadialmenuOpen')
        ExecuteCommand('+awoken_radial')
    end, false)

    AddEventHandler('awoken_ui:radialClosed', function()
        TriggerEvent('qb-radialmenu:client:onRadialmenuClose')
    end)

    -- Lifecycle

    CreateThread(function()
        Wait(2000)
        refreshAll()
    end)

    RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
        Wait(500)
        refreshAll()
    end)

    RegisterNetEvent('QBCore:Client:OnJobUpdate', function()
        pushJobItems()
    end)

    RegisterNetEvent('QBCore:Client:SetDuty', function()
        pushJobItems()
    end)
    ```
  </Step>

  <Step title="Restart qb-radialmenu">
    Run `restart qb-radialmenu` (or restart your server). The bridge initialises on next player load and pushes `Config.MenuItems` into the Awoken UI radial automatically.
  </Step>
</Steps>

## Keybinds

After the bridge is installed, the qb-radialmenu keybind opens Awoken UI's radial instead of qb-radialmenu's own UI. Awoken UI's own radial keybind (default `Z`, set in `awoken_ui/config.lua` via `radialOpenKey`) opens the same overlay. Either key works.

<Tip>
  Both keys open the same radial. If you want only one in play, remove `RegisterKeyMapping('radialmenu', ...)` from qb-radialmenu's `client/main.lua` so the qb-radialmenu key is no longer bound.
</Tip>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Console prints '[awoken-bridge] ox_lib not loaded, bridge aborted'">
    `@ox_lib/init.lua` is missing from qb-radialmenu's `shared_scripts` block. Add it (see step 1 above) and restart the resource.
  </Accordion>

  <Accordion title="Pressing the key opens an empty radial">
    `Config.MenuItems` is empty, or the bridge ran before the player was fully loaded. Wait a couple of seconds after spawn and try again. If items added by external scripts via `exports['qb-radialmenu']:AddOption(...)` still don't show, check that those scripts call AddOption after `QBCore:Client:OnPlayerLoaded` has fired.
  </Accordion>

  <Accordion title="qb-radialmenu's old UI still shows up">
    The bridge wasn't pasted at the bottom of the file, or there are other `exports('AddOption', ...)` registrations further down. Make sure the bridge block is the **last** code in `qb-radialmenu/client/main.lua`.
  </Accordion>

  <Accordion title="Items appear but clicking them does nothing">
    The item is using a field the bridge doesn't recognise. The converter handles `type+event`, `event`, `serverEvent`, `command`, and `onSelect`. If a script uses something custom, wrap it in an `onSelect` callback inside the item definition.
  </Accordion>
</AccordionGroup>
