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
Watching the F10 menu¶
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 removedline is a refresh that changed nothing. - live entries: the VEAF menu's size at that moment,
user_menusmenus 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 inmission-script.lua. Since ADR 0011, the same thing can be declared directly in YAML undermodules.RADIO.user_menus(see Radio menus in YAML), with no Lua at all. The YAMLluaaction 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: [ ... ] }. Theitemsfield in turn holds submenus or commands; nesting is recursive. - Command —
{ command: "Label", action: <verb>, <target keys> }. Placed directly intree(top level) or in a submenu'sitems.
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
luaaction is the bridge to your Lua. The function referenced byfunction:must be defined by the mission maker inmission-script.lua. If it is referenced in YAML but missing from the mission's Lua, the build fails (andveaf-tools mission validateflags 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.lualoads after the generated configuration, and that is where it belongs.argsreaches 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_ForGrouporUSAGE_ForUnitcommand 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). TheaddPaginatedRadioElements/addPaginatedRadioMenuhelpers 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¶
- veafSecurity — security tiers and the group's effective level
- Lua API Reference — full
veafRadioAPI