Skip to content

veafRadio — F10 Radio Menu Builder

Module ID: RADIO | File: veafRadio.lua


Purpose

The F10 "Other" menu in DCS is the primary interface between players and VEAF scripts. veafRadio manages that entire menu tree: building it, refreshing it as human groups join and leave, and providing helpers that let other VEAF modules and mission makers add their own entries without worrying about DCS radio menu limits.


Enable

veafRadio.initialize()

Optional parameters:

veafRadio.initialize(
  skipHelpMenus,   -- bool: omit "Help" entries from built-in menus (default false)
  dontCreateMenus  -- bool: suppress all DCS radio menu creation (default false)
)

After all modules are initialised, call:

veafRadio.refreshRadioMenu()

The F10 tree is rendered again, but only the entries that changed are touched in DCS: an unchanged entry is never removed nor recreated. It is safe to call multiple times — it debounces internally (1-second delay).


Configuration (mission.yaml)

modules:
  RADIO:
    enabled: true          # default: true
    logLevel: info        # optional log level override
    init:
      help_menus: false   # true shows the built-in "Help" entries in radio menus (default: false)
    menu_stats: false     # log the F10 menu's size and changes (default: false)
Field Type Default Required Description
enabled boolean true No Enable or disable the module
logLevel string (global) No Per-module log level override
init.help_menus boolean false No true shows the built-in "Help" entries of the radio menus (combat zones, CAS, assets, move, missile guardian); they are hidden by default, as in every VEAF mission since v5
menu_stats boolean false No Write to dcs.log, on every refresh, the F10 menu's size and what changed — see Watching the F10 menu

Minimal example

modules:
  RADIO:
    enabled: true

menu_stats: true is a diagnostic option, to turn on temporarily in a real mission (an operation, an evening on the server) to see how the F10 menu behaves in multiplayer. On every menu refresh, an info line goes to the server's dcs.log:

radio menu stats: 3 added, 1 removed since the last report; 412 live entries (everyone 38, coalitions 6, groups 368 across 23 groups); 57 ids parked in all
  • added / removed: the entries created and removed in DCS since the previous line. A 0 added, 0 removed line is a refresh that changed nothing.
  • live entries: the VEAF menu's size at that moment, user_menus menus excluded since they are built apart, split between the entries everyone sees, those one coalition sees, and those a single group sees.
  • ids parked: the total number of invisible commands created since the mission started. DCS gives the id of a removed entry to the next entry created, which sent a click on a menu left open to another command; each freed id is therefore "parked" on an invisible command that does nothing. They pile up for the whole mission.

The lines are at info level. A mission that lowers its log level (global_log_level: warning or error) hides them: then add logLevel: info under RADIO, which outranks the global level. Once the measurement is done, remove the option.


Creating a custom menu

Use veafRadio.createUserMenu() to build a structured menu tree from a simple Lua table, using three helpers:

veafRadio.createUserMenu(
  veafRadio.mainmenu(
    veafRadio.menu("QRA Management",
      veafRadio.command("Start QRA North", myMission.startQra, { name = "QRA-NORTH" }),
      veafRadio.command("Stop QRA North",  myMission.stopQra,  { name = "QRA-NORTH" })
    ),
    veafRadio.menu("Flags",
      veafRadio.command("Set Flag 10",   trigger.action.setUserFlag, { "FLAG-10", true }),
      veafRadio.command("Clear Flag 10", trigger.action.setUserFlag, { "FLAG-10", false })
    )
  )
)

Helpers

Helper Signature Returns
mainmenu(...) Variable args of menu() or command() items Flat array for createUserMenu()
menu(name, ...) Name + variable args of nested items A submenu node
command(name, fn, params) Name + function + parameter table A command node

Group-specific menus

Pass a groupId as the second argument to createUserMenu() — the menu will only appear for that group:

local playerGroupId = 101
veafRadio.createUserMenu(
  veafRadio.mainmenu(
    veafRadio.command("Request tanker", myMission.requestTanker, { groupId = playerGroupId })
  ),
  playerGroupId
)

Lua vs YAML. veafRadio.createUserMenu(configuration, groupId) is Lua: it goes in mission-script.lua. Since ADR 0011, the same thing can be declared directly in YAML under modules.RADIO.user_menus (see Radio menus in YAML), with no Lua at all. The YAML lua action remains the bridge to attach a mission-maker-written Lua function to a menu declared in YAML.


Radio menus in YAML

Since ADR 0011, a mission maker can declare a custom F10 radio menu entirely in YAML, with no Lua, under modules.RADIO.user_menus. This is the declarative counterpart of veafRadio.createUserMenu() (see the callout above), intended in particular for Mission Master (MM) control menus.

modules:
  RADIO:
    user_menus:
      restrict_to_group: "MM Ctrl"   # optional: name of a DCS group; the menu only appears for that group. Omitted = global menu.
      tree:
        - menu: "QRA Control"
          items:
            - { command: "Start QRA North", action: qra.start, qra: "QRA-North" }
            - { command: "Stop QRA North",  action: qra.stop,  qra: "QRA-North" }
        - menu: "Phases"
          items:
            - { command: "Begin Phase 2",  action: flag.on,  flag: "PHASE2" }
            - { command: "Set counter",    action: flag.set, flag: "SCORE", value: 100 }
        - { command: "Global message", action: message, text: "The mission is starting!" }
        - { command: "Custom function", action: lua, function: "myMission.startEverything", args: ["alpha", 3] }

tree structure

Each node of tree is either a submenu or a command:

  • Submenu — { menu: "Title", items: [ ... ] }. The items field in turn holds submenus or commands; nesting is recursive.
  • Command — { command: "Label", action: <verb>, <target keys> }. Placed directly in tree (top level) or in a submenu's items.

restrict_to_group

restrict_to_group is optional. When present, the menu only appears for the named DCS group (for example a "MM Ctrl" control group). When omitted, the menu is global and visible to all players.

secured on a command

A command may carry secured: true: it then only runs when the group's pilot holds the required security level (see veafSecurity); its label is prefixed with a +, like the VEAF menu's own secured commands. The level checked is that of the group the menu is posted for, so secured requires restrict_to_group, and the build refuses a global menu carrying one. Without secured, a command is open to every player who sees the menu — on a public server, think about what a "Stop the QRA" menu posted for everyone allows.

      restrict_to_group: "MM Ctrl"
      tree:
        - { command: "Stop QRA North", action: qra.stop, qra: "QRA-North", secured: true }

Action vocabulary

The action vocabulary is closed (v1). Each action requires the listed keys:

action Required keys Effect
qra.start qra: "<QRA name>" Brings the named QRA online
qra.stop qra: "<QRA name>" Takes the named QRA offline
airwave.start airwave: "<AirWave zone name>" Starts the named AirWave zone
airwave.stop airwave: "<AirWave zone name>" Stops the named AirWave zone
airwave.reset airwave: "<AirWave zone name>" Resets the named AirWave zone
flag.on flag: "<flag name or number>" Sets the flag to 1
flag.off flag: "<flag name or number>" Sets the flag to 0
flag.set flag, value (integer) Sets the flag to the given integer value
flag.increment flag Increments the flag by 1
flag.decrement flag Decrements the flag by 1
message text: "<displayed text>" Displays the text on screen
lua function: "<name.of.function>", args: [ ... ] (optional) Calls a mission-maker Lua function

The lua action is the bridge to your Lua. The function referenced by function: must be defined by the mission maker in mission-script.lua. If it is referenced in YAML but missing from the mission's Lua, the build fails (and veaf-tools mission validate flags it). This is how you attach a custom Lua function to a menu declared in YAML.

The function is looked up on click, not when the configuration loads: mission-script.lua loads after the generated configuration, and that is where it belongs. args reaches it as a single parameter, a table: function myMission.startEverything(args) … args[1] … end.


Usage constants

When adding commands via the low-level API, the usage parameter controls who sees the entry:

Constant Value Behaviour
veafRadio.USAGE_ForAll 0 Single entry visible to all players
veafRadio.USAGE_ForGroup 1 One entry per connected human group; the unit name is appended to params automatically
veafRadio.USAGE_ForUnit 2 One entry per connected human pilot; entry title is prefixed with the pilot's callsign

USAGE_ForGroup is ideal for commands that should respond differently per flight (e.g. "Request support for my flight"). USAGE_ForUnit is for individual pilot interactions.


Restricting a menu to one coalition

usage decides who gets a command. To hide a whole submenu from the other side, pass a coalition as addSubMenu's third argument:

-- this submenu, and everything under it, only exists for red
local redMenu = veafRadio.addSubMenu("RED zones", nil, coalition.side.RED)
veafRadio.addCommandToSubmenu("Status", redMenu, myFunction, nil, veafRadio.USAGE_ForGroup)

Three consequences, all automatic:

  • the side is inherited — by child submenus, commands, and the pagination pages created when a menu exceeds MENU_PAGE_SIZE; there is no "visible to all" child under a restricted menu;
  • a USAGE_ForGroup or USAGE_ForUnit command is only attached for groups of that side (a group whose coalition DCS does not report is kept);
  • the menu is rebuilt on every player join, and restricted menus are removed explicitly at that point — otherwise they would stack up one duplicate per join.

The parent menu is untouched: hang a restricted submenu under a global one and the other side still sees the parent, simply without that entry.

Used by combat zones, which offer their menu to the side playing them — see veafCombatZone.


Menus reserved to some groups, menus per pilot

A submenu reserved to some groups

addSubMenuForGroups shows a submenu, and everything under it, only to the groups with a pilot who passes the filter filter(unitName, groupId):

local function carrierAircraft(unitName)
  local unit = Unit.getByName(unitName)
  return unit ~= nil and unit:getTypeName() == "FA-18C_hornet"
end
local carrierMenu = veafRadio.addSubMenuForGroups("Carrier", nil, carrierAircraft)
veafRadio.addCommandToSubmenu("Deck status", carrierMenu, deckStatus, "CVN-74")

The USAGE_ForAll commands under it are posted for each of those groups, with their parameters unchanged. A filter that raises hides the menu and leaves a line in the log; it does not stop the rebuild. A fourth argument restricts the menu to one coalition as well.

A submenu per pilot

In a multi-slot group, a USAGE_ForGroup command is seen by every pilot of the group, and knows only the first one. addSubMenuPerPilot gives each pilot present his own submenu, named after him, under which USAGE_ForGroup and USAGE_ForUnit commands receive his unit name:

local menu = veafRadio.addSubMenuPerPilot("Recovery", nil)
veafRadio.addCommandToSubmenu("Request my pass", menu, requestPass, nil, veafRadio.USAGE_ForGroup)

function requestPass(unitName)
  veaf.outTextForUnit(unitName, "Pass noted", 10) -- to the pilot who clicked only
end

A pilot alone in his group gets the commands directly, without the level named after him. The menu takes the same group filter as addSubMenuForGroups.

Commands follow the pilots present

A per-group command is bound to a pilot still in his unit. When a pilot leaves his slot, dies, ejects or crashes, he is forgotten, the menu is rebuilt and his wingmen get the group's commands back; when he returns, he is registered again. A message addressed to a unit that no longer exists is shown to nobody, instead of going to every player.


Low-level API

For finer control, build the menu tree directly:

-- Add a top-level submenu under the VEAF root
local missionMenu = veafRadio.addMenu("Mission Control")

-- Add a submenu inside it
local qraMenu = veafRadio.addSubMenu("QRA", missionMenu)

-- Add a command (ForAll)
veafRadio.addCommandToSubmenu(
  "Start QRA North",
  qraMenu,
  myMission.startQra,
  { name = "QRA-NORTH" },
  veafRadio.USAGE_ForAll
)

-- Add a secured command (runs at the group's effective level — its lowest-graded occupant)
veafRadio.addSecuredCommandToSubmenu(
  "Emergency stop",
  missionMenu,
  myMission.emergencyStop,
  {},
  veafRadio.USAGE_ForAll
)

-- Trigger a rebuild
veafRadio.refreshRadioMenu()

Automatic pagination

DCS truncates a submenu past 10 entries. You don't have to do anything: any radio menu that exceeds the limit is paginated automatically at render time. The overflow is spread across "Next page" submenus created on demand — no pagination helper call needed. A menu that fits in 10 entries gets no "Next page" (no wasted slot).

To opt a specific menu out of pagination:

veafRadio.doNotPaginate(myMenu)

Special case: a menu holding a USAGE_ForUnit command (one entry per aircraft in the group) disables its own pagination automatically, with a log warning — a global split cannot guarantee the per-group limit.

Page size is fixed at the DCS limit (veafRadio.MENU_PAGE_SIZE = 10). The addPaginatedRadioElements / addPaginatedRadioMenu helpers still exist (they sort and insert elements) but no longer paginate themselves: the render does.


Examples

QRA start/stop from a custom menu

local function _changeQra(parameters)
    local name, action = veaf.safeUnpack(parameters)
    local qra = veafQraManager.get(name)
    if qra then
        if action:upper() == "START" then
            qra:start(false)
        else
            qra:stop(false)
        end
    end
end

veafRadio.createUserMenu(
    veafRadio.mainmenu(
        veafRadio.menu("QRA Management",
            veafRadio.menu("QRA Maykop",
                veafRadio.command("START", _changeQra, {"QRA-Maykop", "start"}),
                veafRadio.command("STOP",  _changeQra, {"QRA-Maykop", "stop"})
            )
        )
    )
)

Destroy a group by name

local function _destroyGroup(name)
    local names = type(name) == "string" and {name} or name
    for _, n in pairs(names) do
        local g = Group.getByName(n)
        if g then
            g:destroy()
            trigger.action.outText(string.format("Group %s destroyed", n), 10)
        end
    end
end

veafRadio.createUserMenu(
    veafRadio.mainmenu(
        veafRadio.menu("Adversaries",
            veafRadio.command("CAP Maykop",   _destroyGroup, "CAP-Maykop"),
            veafRadio.command("SA-6 Minvody", _destroyGroup, "SA6-Minvody")
        )
    )
)

Flag-based mission triggers

veafRadio.createUserMenu(
  veafRadio.mainmenu(
    veafRadio.menu("Phase control",
      veafRadio.command("Begin Phase 2", trigger.action.setUserFlag, { "PHASE2", true }),
      veafRadio.command("Begin Phase 3", trigger.action.setUserFlag, { "PHASE3", true }),
      veafRadio.command("End mission",   trigger.action.setUserFlag, { "MISSIONEND", true })
    )
  )
)

Per-group command (ForGroup)

A "Request close air support" entry that appears once per connected flight, automatically passing that group's unit name:

veafRadio.addCommandToSubmenu(
  "Request CAS",
  supportMenu,
  myCasDispatch,      -- receives { originalParams, unitName } at runtime
  {},
  veafRadio.USAGE_ForGroup
)

See Also