veafRadio — Gestionnaire du menu radio F10¶
Module ID : RADIO | Fichier : veafRadio.lua
Objectif¶
Le menu "Autre" F10 de DCS est l'interface principale entre les joueurs et les scripts VEAF. veafRadio gère l'intégralité de cet arbre de menus : sa construction, son rafraîchissement à mesure que les groupes humains rejoignent et quittent la mission, et fournit des helpers qui permettent aux autres modules VEAF et aux créateurs de missions d'ajouter leurs propres entrées sans se soucier des limites du menu radio DCS.
Activation¶
veafRadio.initialize()
Paramètres optionnels :
veafRadio.initialize(
skipHelpMenus, -- bool : omettre les entrées "Aide" des menus intégrés (défaut false)
dontCreateMenus -- bool : supprimer toute création de menu radio DCS (défaut false)
)
Après l'initialisation de tous les modules, appeler :
veafRadio.refreshRadioMenu()
L'arbre F10 est rendu à nouveau, mais seules les entrées qui ont changé sont touchées dans DCS : une entrée inchangée n'est jamais supprimée ni recréée. L'appel est idempotent — il gère en interne un délai anti-rebond de 1 seconde.
Configuration (mission.yaml)¶
modules:
RADIO:
enabled: true # défaut : true
logLevel: info # surcharge optionnelle du niveau de log
init:
help_menus: false # true affiche les entrées "Aide" intégrées dans les menus radio (défaut : false)
menu_stats: false # journaliser la taille et les changements du menu F10 (défaut : false)
| Champ | Type | Défaut | Requis | Description |
|---|---|---|---|---|
enabled |
booléen | true |
Non | Activer ou désactiver le module |
logLevel |
string | (global) | Non | Surcharge du niveau de log par module |
init.help_menus |
booléen | false |
Non | true affiche les entrées "Aide" intégrées dans les menus radio (zones de combat, CAS, assets, move, missile guardian) ; elles sont masquées par défaut, comme dans toutes les missions VEAF depuis la v5 |
menu_stats |
booléen | false |
Non | Écrire dans dcs.log, à chaque rafraîchissement, la taille du menu F10 et ce qui a changé — voir Surveiller le menu F10 |
Exemple minimal¶
modules:
RADIO:
enabled: true
Surveiller le menu F10¶
menu_stats: true est une option de diagnostic, à activer temporairement sur une mission réelle (une opération, une soirée sur le serveur) pour voir comment le menu F10 se comporte en multijoueur.
À chaque rafraîchissement du menu, une ligne de niveau info s'écrit dans le dcs.log du serveur :
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 : les entrées créées et supprimées dans DCS depuis la ligne précédente. Une ligne à
0 added, 0 removedest un rafraîchissement qui n'a rien changé. - live entries : la taille du menu VEAF à cet instant, hors menus
user_menusconstruits à part, répartie entre les entrées visibles par tout le monde, par une coalition, et par un seul groupe. - ids parked : le nombre total de commandes invisibles créées depuis le début de la mission. DCS redonne l'identifiant d'une entrée supprimée à la prochaine entrée créée, ce qui faisait partir un clic sur un menu resté ouvert vers une autre commande ; chaque identifiant libéré est donc « garé » sur une commande invisible qui ne fait rien. Elles s'accumulent pendant toute la mission.
Les lignes sont de niveau info. Une mission qui baisse son niveau de log (global_log_level: warning ou error) les masque : ajoutez alors logLevel: info sous RADIO, qui prime sur le niveau global.
Une fois la mesure faite, retirez l'option.
Créer un menu personnalisé¶
Utilisez veafRadio.createUserMenu() pour construire un arbre de menus structuré à partir d'une simple table Lua, en utilisant trois helpers :
veafRadio.createUserMenu(
veafRadio.mainmenu(
veafRadio.menu("Gestion QRA",
veafRadio.command("Démarrer QRA Nord", maMission.demarrerQra, { name = "QRA-NORD" }),
veafRadio.command("Arrêter QRA Nord", maMission.arreterQra, { name = "QRA-NORD" })
),
veafRadio.menu("Flags",
veafRadio.command("Activer Flag 10", trigger.action.setUserFlag, { "FLAG-10", true }),
veafRadio.command("Désactiver Flag 10", trigger.action.setUserFlag, { "FLAG-10", false })
)
)
)
Helpers¶
| Helper | Signature | Retourne |
|---|---|---|
mainmenu(...) |
Arguments variables de menu() ou command() |
Tableau plat pour createUserMenu() |
menu(name, ...) |
Nom + arguments variables d'éléments imbriqués | Nœud de sous-menu |
command(name, fn, params) |
Nom + fonction + table de paramètres | Nœud de commande |
Menus spécifiques à un groupe¶
Passer un groupId comme second argument de createUserMenu() — le menu n'apparaîtra que pour ce groupe :
local groupIdJoueur = 101
veafRadio.createUserMenu(
veafRadio.mainmenu(
veafRadio.command("Demander ravitailleur", maMission.demanderRavitailleur, { groupId = groupIdJoueur })
),
groupIdJoueur
)
Lua vs YAML.
veafRadio.createUserMenu(configuration, groupId)est du Lua : il se place dansmission-script.lua. Depuis ADR 0011, la même chose se déclare directement en YAML sousmodules.RADIO.user_menus(voir Menus radio en YAML), sans écrire de Lua. L'action YAMLluareste le pont pour rattacher une fonction Lua écrite par le créateur de mission à un menu déclaré en YAML.
Menus radio en YAML¶
Depuis ADR 0011, un créateur de mission peut déclarer un menu radio F10 personnalisé entièrement en YAML, sans écrire de Lua, sous modules.RADIO.user_menus. C'est le pendant déclaratif de veafRadio.createUserMenu() (voir l'encart ci-dessus), destiné notamment aux menus de contrôle du Mission Master (MM).
modules:
RADIO:
user_menus:
restrict_to_group: "MM Ctrl" # optionnel : nom d'un groupe DCS ; le menu n'apparaît que pour ce groupe. Absent = menu global.
tree:
- menu: "Contrôle QRA"
items:
- { command: "Démarrer QRA Nord", action: qra.start, qra: "QRA-Nord" }
- { command: "Arrêter QRA Nord", action: qra.stop, qra: "QRA-Nord" }
- menu: "Phases"
items:
- { command: "Activer Phase 2", action: flag.on, flag: "PHASE2" }
- { command: "Régler compteur", action: flag.set, flag: "SCORE", value: 100 }
- { command: "Message global", action: message, text: "La mission commence !" }
- { command: "Fonction custom", action: lua, function: "maMission.demarrerTout", args: ["alpha", 3] }
Structure de tree¶
Chaque nœud de tree est soit un sous-menu, soit une commande :
- Sous-menu —
{ menu: "Titre", items: [ ... ] }. Le champitemscontient à son tour des sous-menus ou des commandes ; l'imbrication est récursive. - Commande —
{ command: "Libellé", action: <verbe>, <clés cibles> }. Placée directement danstree(premier niveau) ou dans lesitemsd'un sous-menu.
restrict_to_group¶
restrict_to_group est optionnel. S'il est présent, le menu n'apparaît que pour le groupe DCS nommé (par exemple un groupe de contrôle « MM Ctrl »). Absent, le menu est global et visible par tous les joueurs.
secured sur une commande¶
Une commande peut porter secured: true : elle n'est alors exécutée que si le pilote du groupe a le niveau de sécurité requis (voir veafSecurity) ; son libellé est préfixé d'un +, comme les commandes sécurisées du menu VEAF. Le niveau vérifié est celui du groupe pour lequel le menu est posé : secured exige donc restrict_to_group, et le build refuse un menu global qui en porterait une. Sans secured, une commande est ouverte à tout joueur qui voit le menu — sur un serveur public, réfléchissez à ce qu'un menu « Arrêter la QRA » posé pour tous permet.
restrict_to_group: "MM Ctrl"
tree:
- { command: "Arrêter QRA Nord", action: qra.stop, qra: "QRA-Nord", secured: true }
Vocabulaire d'actions¶
Le vocabulaire d'actions est fermé (v1). Chaque action requiert les clés indiquées :
action |
Clés requises | Effet |
|---|---|---|
qra.start |
qra: "<nom de la QRA>" |
Met en ligne la QRA nommée |
qra.stop |
qra: "<nom de la QRA>" |
Met hors ligne la QRA nommée |
airwave.start |
airwave: "<nom de la zone AirWave>" |
Démarre la zone AirWave nommée |
airwave.stop |
airwave: "<nom de la zone AirWave>" |
Arrête la zone AirWave nommée |
airwave.reset |
airwave: "<nom de la zone AirWave>" |
Réinitialise la zone AirWave nommée |
flag.on |
flag: "<nom ou numéro de flag>" |
Met le flag à 1 |
flag.off |
flag: "<nom ou numéro de flag>" |
Met le flag à 0 |
flag.set |
flag, value (entier) |
Met le flag à la valeur entière donnée |
flag.increment |
flag |
Incrémente le flag de 1 |
flag.decrement |
flag |
Décrémente le flag de 1 |
message |
text: "<texte affiché>" |
Affiche le texte à l'écran |
lua |
function: "<nom.de.fonction>", args: [ ... ] (optionnel) |
Appelle une fonction Lua du créateur de mission |
L'action
luaest le pont vers votre Lua. La fonction référencée parfunction:doit être définie par le créateur de mission dansmission-script.lua. Si elle est référencée en YAML mais absente du Lua de la mission, le build échoue (etveaf-tools mission validatela signale). C'est le moyen de rattacher une fonction Lua personnalisée à un menu déclaré en YAML.La fonction est cherchée au clic, pas au chargement de la configuration :
mission-script.luaest chargé après la configuration générée, et c'est bien là qu'elle doit vivre.argslui arrive en un seul paramètre, une table :function maMission.demarrerTout(args) … args[1] … end.
Constantes d'utilisation¶
Lors de l'ajout de commandes via l'API bas niveau, le paramètre usage contrôle qui voit l'entrée :
| Constante | Valeur | Comportement |
|---|---|---|
veafRadio.USAGE_ForAll |
0 |
Entrée unique visible pour tous les joueurs |
veafRadio.USAGE_ForGroup |
1 |
Une entrée par groupe humain connecté ; le nom de l'unité est ajouté automatiquement aux paramètres |
veafRadio.USAGE_ForUnit |
2 |
Une entrée par pilote humain connecté ; le titre est préfixé avec l'indicatif du pilote |
USAGE_ForGroup est idéal pour les commandes qui doivent répondre différemment par vol (ex : "Demander appui pour mon vol"). USAGE_ForUnit est pour les interactions individuelles par pilote.
Réserver un menu à une coalition¶
usage décide qui reçoit une commande. Pour masquer un sous-menu entier à l'autre camp,
passez une coalition en troisième argument de addSubMenu :
-- ce sous-menu, et tout ce qu'il contient, n'existe que pour les rouges
local menuRouge = veafRadio.addSubMenu("Zones ROUGE", nil, coalition.side.RED)
veafRadio.addCommandToSubmenu("Statut", menuRouge, maFonction, nil, veafRadio.USAGE_ForGroup)
Trois conséquences, automatiques :
- l'appartenance est héritée — sous-menus, commandes et pages de pagination créées quand le
menu dépasse
MENU_PAGE_SIZE; il n'y a pas d'enfant « visible par tous » sous un menu réservé ; - une commande
USAGE_ForGroupouUSAGE_ForUnitn'est posée que pour les groupes de ce camp (un groupe dont DCS ne nous donne pas la coalition est conservé) ; - le menu est reconstruit à chaque connexion de joueur, et les menus réservés sont retirés explicitement à ce moment-là — sans quoi ils s'empileraient en double à chaque arrivée.
Le menu parent n'est pas modifié : si vous accrochez un menu réservé sous un menu global, l'autre camp continue de voir le parent, simplement vide de cette entrée.
Utilisé par les zones de combat, qui proposent leur menu au camp qui les joue — voir veafCombatZone.
Menus réservés à des groupes, menus par pilote¶
Un sous-menu réservé à certains groupes¶
addSubMenuForGroups montre un sous-menu, et tout ce qu'il contient, aux seuls groupes dont un pilote passe le filtre filtre(nomUnité, idGroupe) :
local function avionEmbarque(unitName)
local unit = Unit.getByName(unitName)
return unit ~= nil and unit:getTypeName() == "FA-18C_hornet"
end
local menuPA = veafRadio.addSubMenuForGroups("Porte-avions", nil, avionEmbarque)
veafRadio.addCommandToSubmenu("État du pont", menuPA, etatDuPont, "CVN-74")
Les commandes USAGE_ForAll qu'il contient sont posées pour chacun de ces groupes, avec leurs paramètres inchangés.
Un filtre qui lève une erreur cache le menu et laisse une ligne dans le journal ; il n'interrompt pas la reconstruction.
Un quatrième argument restreint en plus le menu à une coalition.
Un sous-menu par pilote¶
Dans un groupe à plusieurs places, une commande USAGE_ForGroup est vue par tous les pilotes du groupe, et ne connaît que le premier.
addSubMenuPerPilot donne à chaque pilote présent son propre sous-menu, à son nom, sous lequel les commandes USAGE_ForGroup et USAGE_ForUnit reçoivent son nom d'unité :
local menu = veafRadio.addSubMenuPerPilot("Appontage", nil)
veafRadio.addCommandToSubmenu("Demander mon passage", menu, demanderPassage, nil, veafRadio.USAGE_ForGroup)
function demanderPassage(unitName)
veaf.outTextForUnit(unitName, "Passage noté", 10) -- au seul pilote qui a cliqué
end
Un pilote seul dans son groupe reçoit les commandes directement, sans le niveau à son nom.
Le menu accepte le même filtre de groupes que addSubMenuForGroups.
Les commandes suivent les pilotes présents¶
Une commande par groupe est liée à un pilote encore dans son unité. Quand un pilote quitte son slot, meurt, s'éjecte ou s'écrase, il est oublié, le menu est reconstruit et ses équipiers retrouvent les commandes du groupe ; à son retour, il est réinscrit. Un message adressé à une unité qui n'existe plus n'est montré à personne, au lieu de partir vers tous les joueurs.
API bas niveau¶
Pour un contrôle plus fin, construire l'arbre de menus directement :
-- Ajouter un sous-menu de premier niveau sous la racine VEAF
local menuMission = veafRadio.addMenu("Contrôle Mission")
-- Ajouter un sous-menu à l'intérieur
local menuQra = veafRadio.addSubMenu("QRA", menuMission)
-- Ajouter une commande (ForAll)
veafRadio.addCommandToSubmenu(
"Démarrer QRA Nord",
menuQra,
maMission.demarrerQra,
{ name = "QRA-NORD" },
veafRadio.USAGE_ForAll
)
-- Ajouter une commande sécurisée (exécutée au niveau effectif du groupe — son occupant le moins gradé)
veafRadio.addSecuredCommandToSubmenu(
"Arrêt d'urgence",
menuMission,
maMission.arretUrgence,
{},
veafRadio.USAGE_ForAll
)
-- Déclencher une reconstruction
veafRadio.refreshRadioMenu()
Pagination automatique¶
DCS tronque un sous-menu au-delà de 10 entrées. Vous n'avez rien à faire : tout menu radio dépassant cette limite est paginé automatiquement au rendu. Les entrées en trop sont réparties dans des sous-menus « Page suivante » créés à la demande — inutile d'appeler un helper de pagination. Un menu qui tient en 10 entrées n'a pas de « Page suivante » (aucun slot gaspillé).
Pour désactiver la pagination sur un menu précis :
veafRadio.doNotPaginate(monMenu)
Cas particulier : un menu contenant une commande USAGE_ForUnit (une entrée par
appareil du groupe) désactive sa pagination automatiquement, avec un avertissement
dans le log — le découpage global ne pourrait pas garantir la limite par groupe.
La taille de page est fixée à la limite DCS (
veafRadio.MENU_PAGE_SIZE = 10). Les helpersaddPaginatedRadioElements/addPaginatedRadioMenuexistent toujours (ils trient et insèrent les éléments) mais ne paginent plus eux-mêmes : le rendu s'en charge.
Exemples pratiques de fonctions de callback¶
Les commandes d'un menu utilisateur appellent une fonction Lua. Voici des exemples courants :
Démarrer / arrêter une QRA¶
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("Gestion QRA",
veafRadio.menu("QRA Maykop",
veafRadio.command("START", _changeQra, {"QRA-Maykop", "start"}),
veafRadio.command("STOP", _changeQra, {"QRA-Maykop", "stop"})
)
)
)
)
Détruire un groupe par son nom¶
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("Adversaires",
veafRadio.command("CAP Maykop", _destroyGroup, "CAP-Maykop"),
veafRadio.command("SA-6 Minvody", _destroyGroup, "SA6-Minvody")
)
)
)
Gérer des drapeaux DCS (pour déclencher des triggers)¶
veafRadio.createUserMenu(
veafRadio.mainmenu(
veafRadio.menu("Flags",
veafRadio.command("Flag ALPHA ON", veafSpawn.missionMasterSetFlagFromTable, {"alpha", 1}),
veafRadio.command("Flag ALPHA OFF", veafSpawn.missionMasterSetFlagFromTable, {"alpha", 0}),
veafRadio.command("Incrémenter 127", veafSpawn.missionMasterIncrementFlagValue, 127),
veafRadio.command("Décrémenter 127", veafSpawn.missionMasterDecrementFlagValue, 127)
)
)
)
Commande par groupe (ForGroup)¶
Une entrée « Demander un appui aérien » qui apparaît une fois par patrouille connectée, en passant automatiquement le nom d'unité de ce groupe :
veafRadio.addCommandToSubmenu(
"Request CAS",
supportMenu,
myCasDispatch, -- reçoit { originalParams, unitName } à l'exécution
{},
veafRadio.USAGE_ForGroup
)
Voir aussi¶
- veafSecurity — paliers de sécurité et niveau effectif du groupe
- Référence API Lua — API complète de
veafRadio