veaf-mission-mcp — serveur MCP d'édition de mission assistée par LLM¶
Public visé : les développeurs qui font évoluer le serveur MCP d'édition de mission, ou qui branchent un client MCP (Claude Code, un agent) dessus.
🎯 Côté Mission Maker (catalogue en langage courant) :
mission-maker/AI_ASSISTANT_CATALOG.md.
Pourquoi ce serveur¶
Première phase de NL-MISSION-GEN (voir ROADMAP.md §4) : permettre à un LLM d'éditer une
mission DCS pour le compte d'un Mission Maker — et à terme, d'en générer une entière depuis un
prompt détaillé. Voir ADR 0014 pour la
décision d'architecture, et CONTEXT.md (section « LLM-assisted mission editing ») pour le
vocabulaire.
Deux familles d'actions, volontairement séparées :
- Action editor-parity — mute directement les tables Lua brutes du
.mizsource, exactement comme un Mission Maker le ferait à la main dans l'éditeur DCS (ajouter un groupe, un trigger, une zone). Ne passe jamais parmission.yaml. C'est tout le périmètre de ce serveur en v1. - Action VMCT — passe par le pipeline déclaratif
mission.yamlexistant (inject_presets,aircraft_groups...). Depuis la vague 4, le serveur en expose une première brique : éditer lemission.yamlsource (voir plus bas), en plus du pipeline CLI/config habituel.
Lancer le serveur localement¶
poetry install
poetry run veaf-mission-mcp # en dev
# ou, depuis le binaire livré (ce que `veaf-tools ai-setup` déclare dans les clients IA) :
veaf-tools mcp
Démarre un serveur MCP sur stdio (transport par défaut du SDK mcp). Aucune configuration :
chaque action reçoit le chemin du .miz à éditer en paramètre. La sous-commande veaf-tools mcp
embarque le serveur dans le binaire veaf-tools déjà livré (pas de binaire séparé à builder) — c'est
elle que veaf-tools ai-setup déclare dans Claude Desktop, Claude Code, Codex, Antigravity, Gemini CLI et OpenCode.
Lancé depuis le binaire livré, le serveur se met aussi à jour tout seul : dans un thread d'arrière-plan, au plus
une fois toutes les 4 heures, seulement si check_updates est actif et qu'aucune pré-release n'est épinglée (VEAF_MCP_UPDATER_TAG), il compare sa version à la release
published-latest et, quand elle est plus récente, lance veaf-tools-updater dans son propre dossier, dans une
console cachée (veaf_mission_mcp/self_update.py). L'updater met de côté le veaf-tools.exe en cours
d'exécution au lieu de l'écraser, ce que Windows refuse ; la nouvelle version sert au démarrage suivant du client.
Catalogue d'actions (v1)¶
La mission se nomme mission_path, partout (FIX-OPEN-TRAINING-PROMPT-FINDINGS, ticket 05)
42 des 47 actions prenaient la mission sous cinq noms : miz_path (17), target (9),
folder_path (6), mission_path (4) et mission_yaml_path (6). Le catalogue publie désormais
mission_path (un dossier de mission ou un .miz) pour toutes, et le traduit vers la clé que
le gestionnaire lit : les trois anciens noms restent acceptés comme alias, les exemples ci-dessous
qui les emploient restent valides. mission_yaml_path est gardé, puisqu'il désigne un autre fichier ;
un mission_path donné à sa place (un dossier) est lu comme <dossier>/mission.yaml. Un test du
catalogue échoue si une nouvelle action introduit un sixième nom.
miz_path accepte aussi un dossier de mission (lot FIX-MCP-AUTHORING-GAPS, ticket 03)
Toutes les actions d'édition — edit_route, set_group_properties, set_unit_properties,
edit_zone, add_trigger_zone, add_map_drawing, edit_map_drawing — prennent soit un .miz,
soit un dossier de mission, exactement comme le target de add_group. Le compromis est le
même : une édition dans un dossier est durable (elle va dans src/mission/, donc elle survit
au veaf-tools build suivant), une édition dans un .miz est transitoire (le build suivant
l'écrase). Chaque action renvoie désormais durable pour le dire. Sauvegarde horodatée dans les
deux cas.
Avant, elles n'acceptaient qu'un .miz : un groupe pouvait être créé durablement mais pas
modifié durablement, et pointer edit_route sur le dossier échouait sur un
[Errno 13] Permission denied (lire un répertoire comme une archive zip). Un dossier qui n'est pas
un dossier de mission est maintenant refusé avec un message qui le dit.
Le serveur n'expose pas un outil MCP par action métier. Il expose une surface de découverte
fixe, à l'image du serveur MCP dcs-bridge (pont vers une mission qui tourne) :
| Outil MCP | Rôle |
|---|---|
capabilities() |
Identité du serveur (nom, version). |
list_catalog(full=False) |
Liste les actions enregistrées en {name, summary} — la première phrase de la description, une dizaine de ko pour toutes ; full=true rend chaque description et parameters_schema (quelque 70 ko, qu'un client range dans un fichier au lieu de les montrer). describe_action en donne une en entier. |
describe_action(name) |
Détaille le schéma JSON des paramètres d'une action. |
run_action(name, params) |
Exécute une action enregistrée. |
Les actions elles-mêmes sont enregistrées par
veaf_mission_mcp.actions.register_default_actions (src/python/veaf-tools/veaf_mission_mcp/actions.py).
describe_mission¶
Lecture seule. Liste les groupes (nom, coalition, pays, catégorie) et zones de déclenchement
(nom, position, rayon) déjà présents dans le .miz — pour que l'appelant vérifie l'état courant
avant d'écrire, comme un humain consulterait l'arborescence de l'éditeur avant d'ajouter quelque
chose. Réutilise le parseur pur-Python existant (mission_tools.miz_tools.read_miz) — aucun
nouveau parsing.
{"miz_path": "chemin/vers/mission.miz"}
describe_units (lot FEAT-MCP-MUTATION-ACTIONS)¶
Lecture seule. Le niveau de détail que describe_mission ne donne pas : les unités de chaque
groupe (type, skill, livrée, indicatif, numéro de flanc, position, cap, altitude, carburant,
leurres/canon), leur emport et la route du groupe avec les tâches de chaque point.
Trois choix de forme, chacun pour une raison mesurée sur une mission réelle (Foothold Caucasus 4.4.1, 357 unités armées) :
pylonsest indexé par numéro de pylône, jamais positionnel. DCS numérote les stations et les numéros ne sont pas contigus : un FA-18C réel porte les pylônes 1, 4, 5, 6 et 9. Dans cette mission, 170 unités sur 357 ont une disposition à trous, et le parseur Lua rend celles-ci endictalors qu'il aplatit les contiguës enlist. Un lecteur qui traiterait les pylônes comme une liste ordonnée aurait donc raison une fois sur deux et tort en silence le reste du temps — c'est ainsi qu'un futur setter accrocherait une arme sur la mauvaise station.- Les tâches automatiques de l'éditeur sont signalées et allégées. Une tâche de point de
passage est un
ComboTaskqui mélange la tâche voulue par l'auteur et les options que l'éditeur écrit tout seul (ROE, usage du radar, formation), toutes marquéesauto = true: 1093 entrées automatiques contre 189 voulues sur cette mission. Les deux sont rapportées — les masquer fausserait la description — mais seules les tâches voulues portent leursparams. - Un plafond dont l'appelant est informé. La mission entière fait 1,9 Mo de JSON, et un seul
groupe de 62 points de passage en fait 18 ko. D'où les filtres (
group_namepar fragment,coalition,category), la limite par défaut de 50 groupes avectruncated/matcheddans la réponse, etinclude_route: falsequi omet la clé au lieu de renvoyer une liste vide (« pas demandé » n'est pas « ce groupe n'a pas de route »).
Les booléens sont rendus comme des booléens : DCS omet une clé qui vaut faux, et un appelant
qui lit null ne peut pas distinguer « désactivé » de « le lecteur n'a pas regardé ».
{"miz_path": "chemin/vers/mission.miz", "group_name": "Colt", "include_route": false}
set_unit_properties (lot FEAT-MCP-MUTATION-ACTIONS)¶
Depuis
FIX-SCRATCH-MISSION-FINDINGSticket 19, elle renomme (new_name, refusé si une autre unité porte déjà ce nom : DCS les veut uniques dans toute la mission) et déplace (position) une unité seule ; l'ancre et la route du groupe ne bougent pas. Pour un avion, dont la place tient à la route, un avertissement le signale.
Écriture. La première action qui modifie un objet déjà présent dans la mission : toutes les
set_* livrées avant elle agissent sur la configuration (modules, sécurité, logs, coalition d'une
base). Sauvegarde horodatée avant écriture, comme ses sœurs.
Adresse l'unité par nom exact de groupe et nom exact d'unité — pas par fragment, contrairement
à describe_units : un fragment fait porter la modification au premier groupe qui correspond, ce qui
n'est pas rattrapable. Un nom introuvable liste ce qui existe, pour qu'un appelant réessaie sans
relire toute la mission.
Trois formes ont été mesurées sur de vraies missions plutôt que déduites, et deux contredisent le ticket qui les demandait :
skilla sept valeurs, pas quatre.Average,Good,High,ExcellentetRandomsont des niveaux d'IA ;ClientetPlayersont des slots humains. Franchir cette limite dans un sens ajoute une place à la liste multijoueur, dans l'autre la supprime — le bug pour lequelFIX-TEMPLATE-SLOTS-VISIBLEa été ouvert. Les deux sens sont donc refusés en nommant la raison, au lieu d'être honorés comme un réglage de compétence.- L'indicatif d'un appareil n'est pas un champ simple. C'est une table
{1: famille, 2: vol, 3: numéro, name: "Colt11"}oùnameest le mot de la famille suivi des deux indices ({1:1, 2:1, 3:2}se litEnfield12). Écrirenameseul désynchronise ce que DCS annonce à la radio de ce que montre l'éditeur : l'action modifie donc les indices et reconstruitnameà partir du préfixe déjà présent. Changer la famille exige la table famille→mot de DCS, que ce dépôt n'embarque pas : c'est refusé sauf si l'appelant fournit lui-même lenamerésultant. headingest en radians alors qu'un créateur de mission parle en degrés — le piège queresolve_coordinatesmasque ailleurs. Le paramètre s'appelleheading_degpour que l'unité soit impossible à confondre, et la valeur est normalisée sur un tour (−90 vaut 270). Sur un appareil en vol (catégorie avion/hélico, route de 2+ points, premier waypoint en l'air), l'action avertit : DCS recalcule le cap depuis le premier segment de la route à la sauvegarde, donc le cap posé a une durée de vie d'une sauvegarde (FIX-MCP-EDITOR-ROUNDTRIP, mesuré 2026-08-15). Pour orienter un appareil en vol, on règle la route, pas le cap. Le cap est quand même écrit — l'avertissement informe, il ne refuse pas ; un appareil parké ou une unité au sol ne déclenchent pas l'avertissement.
Ce que l'action ne valide pas, faute des données pour le faire : le CLSID d'une arme face à
l'appareil qui la porte, et une livrée face aux peintures installées. DCS retire silencieusement une
arme impossible et affiche silencieusement la peinture par défaut, donc les deux limites sont
renvoyées comme warnings plutôt que sous-entendues par leur absence.
pylons est indexé par numéro de station, jamais positionnel, pour la raison mesurée dans
describe_units. pylons absent = « ne touche pas à l'emport » ; {} en mode replace = « ne porte
rien » ; en mode merge, un CLSID vide vide cette station.
{
"miz_path": "chemin/vers/mission.miz",
"group_name": "Colt 1-1",
"unit_name": "Colt 1-1-1",
"skill": "Excellent",
"heading_deg": 270,
"pylons": {"4": ""},
"pylons_mode": "merge"
}
La réponse porte changed, qui donne pour chaque champ touché sa valeur précédente et la
nouvelle : un appelant qui ne peut pas dire ce qu'il a remplacé ne peut pas le défaire.
set_group_properties (lot FEAT-MCP-MUTATION-ACTIONS)¶
Écriture. Agit sur le groupe entier : déplacement, renommage, fréquence, modulation, et les trois
booléens (lateActivation, hidden, uncontrolled). Sauvegarde horodatée avant écriture.
Le déplacement porte toute la conception de ce module, et ce n'est pas « écrire x et y ». Un
groupe, ce sont des unités en formation plus éventuellement une route. La translation
s'applique donc à toutes les unités, tous les points de passage et l'ancre x/y du groupe,
d'un seul vecteur : autrement la formation se déforme, ou la route se détache des unités auxquelles
elle appartient — et aucun des deux ne se voit avant qu'on vole la mission. Le test du cisaillement
(déplacer les unités en laissant les points de passage) est écrit pour tomber sur toute
implémentation qui l'oublierait, et ça a été vérifié en cassant volontairement la translation.
Le vecteur vient de l'offset géodésique de FEAT-GEO-PLACEMENT
(ADR 0015), pas d'une addition de mètres sur x : un théâtre
DCS est le monde réel projeté, donc « 5 km à l'est » est une question de latitude/longitude. Un
théâtre sans projection fait refuser la forme cap + distance, en invitant à passer move_to.
frequency_mhz est contrôlée face au HumanRadio de l'appareil, en réutilisant le validateur de
l'injecteur de presets plutôt qu'en le redérivant : FIX-PRIMARY-FREQ-HUMANRADIO a établi que
l'éditeur DCS refuse d'enregistrer une mission dont la fréquence primaire sort de cette plage.
Tous les types d'unités du groupe sont vérifiés, pas seulement le premier — un groupe hétérogène
passerait sinon sur son premier membre pour être refusé par l'éditeur à cause d'un autre.
Le renommage lance la vérification des conventions VEAF réservées (validate_group_name) et
refuse par défaut : un groupe renommé avec le nom de la zone de déclenchement d'une combat zone
est despawné au démarrage, en silence. Renommer vers une convention est une intention légitime,
d'où acknowledge_conventions — l'important est que ce soit délibéré. Les noms d'unités ne
suivent jamais : ils portent leurs propres marqueurs (#command=, #veafInterpreter[...]), qu'une
cascade réécrirait à l'aveugle.
Ce que l'action ne peut pas faire, mesuré et non oublié : vérifier la nature du sol à l'arrivée.
Il n'y a aucune donnée de terrain côté Python — land.getSurfaceType est une API d'exécution, seul
son schéma est livré ici — et c'est exactement pour cette raison que FEAT-SCENERY-AWARE-SPAWN a
résolu le problème à l'exécution. Le déplacement avertit donc qu'il n'a pas pu regarder, au lieu
de valider et de mentir.
{
"miz_path": "chemin/vers/mission.miz",
"group_name": "Red SAM Battery",
"move_bearing": 90,
"move_distance_m": 5000,
"late_activation": true
}
edit_route (lot FEAT-MCP-MUTATION-ACTIONS)¶
Écriture. Deux couches : la route (add, insert, remove, reorder, set) est pour l'essentiel
une opération de liste sur route.points ; les tâches d'un point de passage (add_task,
clear_tasks) sont ce qui fait qu'un vol fait quelque chose.
Route ou hors route. Sur un groupe terrestre, road: true écrit l'action On Road d'un Turning Point (les véhicules suivent les routes), road: false écrit Off Road ; pour add, insert et set.
Refusé sur un autre groupe et sur un autre type de point.
Tous les points étaient écrits Turning Point, et le convoi de la mission de démo coupait à travers champs (FIX-DEMO-MISSION-FINDINGS 07).
Route d'un convoi à la création. Un groupe terrestre créé par add_group ou create_combat_zone avec au moins deux points part sur la route (On Road, comme ses étapes) et roule à 16,4 m/s (59 km/h) ; un point peut donner sa propre vitesse (speed, en m/s). Mesuré le 2026-10-13 sur les 126 groupes de véhicules de 14 missions faites à la main dont la route a une étape On Road : premier point On Road dans 85, Off Road dans 37 ; vitesse des étapes de médiane 16,4 m/s, 5,6 m/s dans 11 % seulement (FIX-OPEN-TRAINING-FALKLANDS-FINDINGS ticket 04). Un point isolé reste Off Road, un navire garde ses Turning Point et sa vitesse. Que le départ soit sur une route n'est vérifié par rien hors du jeu (ticket 06).
L'invariant qui en fait de la chirurgie et pas de l'édition de liste.
FIX-WAYPOINTS-ETA-LOCKED a établi que DCS refuse d'enregistrer une mission dont une route n'a
aucun point de passage à heure verrouillée (« Route has no waypoints with locked time! »), et que sa
propre réparation consiste à verrouiller le premier. Supprimer ou réordonner peut donc produire une
mission que l'éditeur rejette, loin de l'édition qui l'a causée. Chaque opération rétablit l'invariant
et le signale quand elle a dû le faire.
Unités. La table de mission contient des mètres et des mètres par seconde ; un créateur de mission
parle en pieds et en nœuds. Comme pour le heading_deg de set_unit_properties, les paramètres portent
leur unité dans leur nom (altitude_ft, speed_kt) et la réponse donne les deux, pour que l'appelant
n'ait jamais à reconvertir.
Les tâches sont un jeu nommé à signatures vérifiées, pas une table libre — choix explicite du ticket : une action générique « écris cette table de tâche » est un piège, parce qu'un agent produit une table plausible, DCS l'ignore en silence, et le créateur de mission le découvre une heure plus tard. L'échappatoire démarre fermée.
Chaque signature a été lue dans une vraie mission, et trois sont des pièges :
SetFrequencyprend des hertz (31000000pour 31 MHz) alors que la fréquence d'un groupe —set_group_properties— est en MHz. Deux unités pour la même notion, dans le même fichier. L'action prend des MHz et convertit.EngageTargetsInZoneduplique sa liste de cibles dans une chaîne sérialiséevalue("Air;Cruise missiles;") à côté du tableautargetTypes; n'écrire que le tableau laisse la mission porter deux versions de la même décision.SetFrequencyetSwitchWaypointne sont pas des tâches mais des actions, portées dans une enveloppeWrappedAction. Écrite comme une tâche nue, DCS l'ignore.
Deux détails mesurés qui n'étaient pas dans le ticket : type et action d'un point de passage sont
une paire (« Land » va avec « Landing »), et un point ajouté hérite de l'altitude et de la
vitesse de son voisin — sinon il s'écrit à l'altitude 0 et le vol plonge au sol pour l'atteindre —
sauf si altitude_ft/speed_kt sont fournis à add/insert, auquel cas ils sont écrits
(FIX-MCP-EDITOR-ROUNDTRIP : ils étaient acceptés puis silencieusement ignorés, l'héritage écrasant la
valeur demandée).
Les tâches d'attaque doivent porter le jeu de champs complet que l'éditeur conserve.
FIX-MCP-EDITOR-ROUNDTRIP a mesuré (2026-08-15) qu'un Bombing écrit sans weaponType est jeté par
l'éditeur à la sauvegarde — un dispositif d'attaque qui ne largue rien. Bombing et AttackGroup
portent donc désormais weaponType (défaut « Auto » mesuré : 2032 pour Bombing, 9659482112 pour
AttackGroup, surchargeable via weapon_type), les paires altitude/altitudeEnabled et
direction/directionEnabled présentes mais désactivées par défaut (activées si l'appelant passe
altitude_ft/direction_deg), et l'ensemble expend/attackQty/groupAttack. EngageTargetsInZone
porte aussi noTargetTypes (liste d'exclusion, vide par défaut).
L'ordre des tâches compte. DCS les exécute par number, et une tâche placée après une orbite sans
fin n'est jamais atteinte. add_task ajoute à la fin par défaut ; task_position (1-based) l'insère à
une place donnée et renumérote les autres — pour mettre un engagement avant l'orbite
(FIX-SCRATCH-MISSION-FINDINGS ticket 17).
{
"miz_path": "chemin/vers/mission.miz",
"group_name": "Colt 1-1",
"operation": "add_task",
"index": 2,
"task": "orbit",
"task_params": {"pattern": "Race-Track", "altitude_ft": 20000, "speed_kt": 300}
}
edit_zone (lot FEAT-MCP-MUTATION-ACTIONS)¶
Écriture. add_trigger_zone ne crée que des zones circulaires et rien n'en modifiait une ensuite,
donc ajuster une combat zone VEAF — qui est une zone de déclenchement — imposait de la supprimer et
de la refaire.
Deux mesures avant toute ligne de code, comme le ticket l'exigeait :
- La forme réelle d'une zone polygonale, lue dans
veaf-demo-mission.miz(czBatumi) :type: 2plus une listeverticies— l'orthographe de DCS, conservée telle quelle parce que corriger la coquille écrirait un champ que DCS ignore — tandis quex,yetradiusrestent présents. Un polygone n'est donc pas un cercle avec des champs en plus. - Ce que le runtime VEAF gère. À l'époque,
veafCombatZone.luane testait que deux types :0→mist.getUnitsInZones,2→mist.getUnitsInPolygon(triggerZone.verticies). Il n'y avait pas deelse, donc une zone d'un autre type ne contenait aucune unité, en silence — pire que de ne pas proposer la forme. L'action n'écrit donc que 0 et 2. DepuisDROP-MIST, le test vit dansveaf.getUnitsInTriggerZoneet appelle les fonctions VEAFveaf.getUnitsInCircularZone/veaf.getUnitsInPolygon; il n'accepte toujours que 0 et 2, mais un type inattendu journalise désormais une erreur et renvoienilau lieu d'une liste vide : le silence a disparu. La règle de l'action est inchangée.
Décision de David sur le nombre de sommets (2026-08-12) : accepter trois ou plus, puisque « suivre
la ligne de crête » est le cas d'usage réel et que mist gère un polygone quelconque — mais avertir
dès que le compte n'est pas quatre, l'éditeur DCS n'ayant aucun outil pour dessiner ou remodeler une
zone non quadrilatère. La question ouverte de savoir s'il préserve une telle zone a été tranchée en
jeu le 2026-08-15 (FIX-MCP-EDITOR-ROUNDTRIP) : une zone à 6 sommets est revenue identique après
sauvegarde. L'action ne refuse donc pas au-delà de quatre ; l'avertissement énonce une limite
connue (on ne peut pas éditer la forme à la main dans l'éditeur), plus un risque inconnu.
Deux refus que le ticket laissait ouverts, décidés ici : un lien vers une unité inexistante est
refusé plutôt qu'averti (une zone liée à rien ne suit simplement jamais rien, sans bruit), et une
collision de nom est refusée (les zones sont référencées par nom depuis mission.yaml).
{
"miz_path": "chemin/vers/mission.miz",
"zone_name": "czBatumi",
"vertices": [
{"x": -359753.0, "y": 614918.0},
{"x": -355602.0, "y": 622688.0},
{"x": -352849.0, "y": 617192.0},
{"x": -358731.0, "y": 614282.0}
]
}
add_map_drawing / edit_map_drawing (lot FEAT-MCP-MUTATION-ACTIONS)¶
Écriture. Rien dans VMCT ne touchait aux dessins de la carte F10, donc une ligne de briefing, un couloir d'entrée ou une boîte interdite se dessinait à la main dans l'éditeur — et disparaissait dès que la mission était reconstruite depuis son dossier. C'est tout l'argument : un dessin posé par un agent fait partie de la recette, un dessin fait à la main non.
La mesure qui gouverne la conception, lue dans les fixtures du dépôt :
Les
pointssont relatifs à l'ancremapX/mapYdu dessin, le premier valant{0, 0}.
Un dessin écrit en coordonnées absolues atterrit à des centaines de kilomètres et rien ne lève
d'erreur — la même classe de panne silencieuse que confondre le {x=nord, y=est} de la table de
mission avec un vec3 d'exécution (voir docs/agents/dcs-coordinates.md). Les actions prennent donc les
coordonnées absolues dont l'appelant dispose et font l'ancrage elles-mêmes. Le bénéfice apparaît
dans edit_map_drawing : déplacer un dessin, c'est déplacer son ancre, et la forme suit gratuitement.
Six formes sont livrées parce que six formes ont été mesurées : Line (avec lineMode
segment ou segments, et closed pour une forme qui se referme), Polygon en mode rect
(width/height/angle, aucun point), TextBox (text/font/fontSize ; la police est reprise
d'un vrai dessin, une police absente de DCS ne s'affichant pas du tout), et — ajoutés le 2026-08-15
depuis bridge-Syria-editeur.miz (ticket 10) — Polygon en mode circle (radius, aucun point ni
angle), oval (r1/r2/angle) et free (des points relatifs à l'ancre comme une Line, une
zone libre remplie, au moins trois points).
arrow et icon ont été mesurés mais restent refusés, avec une raison plutôt qu'une supposition :
un arrow stocke un contour calculé de 8 points en plus de ses length/angle, donc écrire les
seuls paramètres demande un aller-retour en jeu pour savoir si DCS recalcule le contour (son propre
ticket) ; un icon réclame un file du jeu d'icônes de l'éditeur (p. ex. P91000007.png), qu'aucune
donnée du dépôt n'énumère, et un nom non validé ne s'affiche pas. chevron a été retiré : il n'existe
pas dans l'éditeur DCS.
Style de trait (style, FIX-OPEN-TRAINING-CAUCASUS-FINDINGS) : l'un des seize styles de DCS — solid (défaut), solid2, dot, dot2, dotdash, dash, cross, square, strongpoint, triangle, wirefence, boundry1 à boundry5, orthographe de DCS comprise.
Il vaut pour toutes les formes sauf textbox, qui le refuse ; un nom hors liste est refusé avec la liste.
Mesuré dans me_draw_panel.lua de l'éditeur : la mission ne stocke que le nom du style, l'éditeur en déduit la texture au chargement — un nom inconnu l'y ferait échouer.
La couche est un paramètre de première classe, jamais une valeur par défaut : un dessin sur la mauvaise couche est invisible pour les pilotes qui en ont besoin et visible pour ceux qui ne devraient pas le voir.
{
"miz_path": "chemin/vers/mission.miz",
"layer": "Blue",
"shape": "line",
"name": "FSCL",
"points": [{"x": -300000.0, "y": 600000.0}, {"x": -290000.0, "y": 610000.0}]
}
add_group¶
Écriture. Insère un groupe terrestre/véhicule dans le .miz source, en place, avec une
sauvegarde horodatée systématique avant l'écriture
(mission_tools.miz_backup.backup_before_write, ex. mission.20260712-143012.miz). Une
collision sur la même seconde est désambiguïsée (-2, -3, ...), jamais silencieusement
écrasée.
{
"miz_path": "chemin/vers/mission.miz",
"coalition": "red",
"country_id": 0,
"country_name": "Russia",
"category": "vehicle",
"name": "Red Armor Section",
"position": {"x": 1000.0, "y": 2000.0},
"units": [{"type": "T-72B", "count": 2}],
"route": [{"x": 1000.0, "y": 2000.0}, {"x": 1200.0, "y": 2000.0}],
"patrol": true
}
units— le serveur ne fait aucune curation de catalogue d'unités : les types DCS concrets (T-72B,BTR-80...) sont la décision de l'appelant (LLM), pas de cette action. Chaque unité peut porter unnameexplicite (sinon auto-nommée). C'est là qu'on pose un marqueur de combat zone (le runtime les lit sur le nom d'unité) :#command,#spawngroup,#spawnradius,#spawncount,#spawnchance,#spawndelay,#alarm. L'idiome classique = un groupe « fausse unité » dont le nom est#command="-armor ..."(aliaslist_shortcuts) : à l'activation de la zone, il spawne le groupe décrit. Ex.units: [{"type": "Soldier M4", "name": "#command=\"-armor, spawnRadius 300\""}].route— optionnelle ; par défaut un unique point stationnaire àposition. Avecpatrol: true(et au moins 2 points), le dernier point boucle sur le premier via une tâcheGoToWaypoint— une patrouille terrestre DCS classique.- Un nom déjà pris est refusé, nom de groupe comme nom d'unité, en citant le groupe qui le porte.
DCS ne refuse pas deux objets du même nom :
Group.getByNameen renvoie un des deux sans rien dire, et tout ce qui désigne un groupe par son nom (QRA, combat zone,remove_group) se trompe de groupe. Vaut pour toutes les actions qui créent un groupe (add_air_group,add_player_slot,create_qra…), qui passent par la même insertion. - Les
groupId/unitIdsont toujours frais (mission_tools.group_insertion.max_ids), y compris sur une mission aux plages d'ids déjà trouées.
Intentions de nommage (vague 6). L'appelant exprime l'intention et add_group produit un
nom conforme aux conventions VEAF lui-même (veaf_mission_mcp.group_naming.resolve_group_name) :
for_combat_zone: <zone>— préfixe le nom par le nom de la trigger-zone (règle d'appartenance combat zone), idempotent et insensible à la casse ;late_activation: true— pose le drapeau DCSlateActivation(intercepteurs QRA, templates CAP) ;as_spawn_template: true— préfixeveafSpawn-(template d'avion spawnable).
add_group renvoie aussi un champ warnings (voir validate_group_name ci-dessous) : il écrit
quand même, mais signale toute collision de convention pour que l'appelant la relaie.
Terrain dégagé (lot FEAT-CLEAR-GROUND-AT-AUTHORING). Quand c'est l'outil qui choisit la position,
un groupe de véhicules immobile (sans route) est posé sur un terrain mesuré dégagé des arbres et
des bâtiments, à 1 km au plus de la position demandée, en translatant le groupe d'un bloc. La mesure
vient du catalogue de terrain dégagé du théâtre, balayé une fois dans DCS avec
veaf-tools dcs clear-ground-sweep (le Caucase est livré ; sinon, celui balayé sur le poste). La place
nécessaire est l'étendue des unités écrites ; pour un marqueur #command, c'est le pire cas du
groupe que le runtime dessinera (calculé depuis veaf-units.yaml, rayon d'apparition compris : environ
214 m pour -sa10). Un warnings dit toujours ce qui s'est passé : déplacé de N m, rien d'assez grand
dans le rayon cherché, zone non couverte par le catalogue (avec la commande pour la balayer), ou groupe
dont la taille n'est connue qu'à l'apparition (-armor, -infantry…), laissé au runtime. Le groupe
n'est jamais refusé : faute de mieux, il reste où il était demandé.
keep_position: true— la position est celle que l'utilisateur a donnée : le groupe n'est jamais déplacé. Même paramètre sur les groupes decreate_combat_zone.
add_player_slot (lot FIX-SCRATCH-MISSION-PLAYABLE)¶
Écriture. Crée une place joueur — un groupe avion jouable — que add_group (terrestre) ne sait
pas produire et sans laquelle une mission bâtie de zéro n'est pas jouable. Sauvegarde horodatée avant
écriture. Cible un dossier (durable) ou un .miz (transitoire).
{
"target": "chemin/vers/dossier-mission",
"coalition": "blue",
"country_id": 2,
"country_name": "USA",
"name": "Player Viper",
"unit_type": "F-16C_50",
"position": {"x": 1000.0, "y": 2000.0},
"start": "ground-cold",
"parking": "43",
"parking_id": "16",
"airdrome_id": 24
}
skill: Client— la compétence de slot multijoueur, jouable aussi en solo. Cette action ne modifie pas la compétence d'une unité existante :set_unit_propertiesrefuseClient/Playeret ceci n'en est pas une porte dérobée.dynSpawnTemplateest mis àfalse. Ce drapeau marque un template de spawn dynamique, qui exige une base configurée pour ça ; laissé actif (comme sur une copie d'un template) le slot existe dans le fichier mais n'apparaît pas dans la liste des places — le défaut trouvé en jeu le 2026-08-14.start—"air"(position +altitude_ft+speed_kt+heading_deg, aucune donnée runtime ; DCS oriente un départ en vol vers son deuxième point de route, donc le slot en reçoit un à 10 km dans l'axe deheading_deg— avec un seul point, il partait plein nord quel que soit le cap, FIX-AIR-SLOT-HEADING),"ground-cold"ou"ground-hot". Un départ au sol exigeparking,parking_idetairdrome_id; sans eux il est refusé (message nommant la donnée capturée parFEAT-MCP-MUTATION-ACTIONSticket 09), jamais deviné. La pairetype/actiondu premier waypoint est écrite selon le mode.frequency_mhzest écrite (radio de groupe active) plutôt qu'héritée d'uncommunication: false.- Le plein interne par défaut.
fuel(kg) etfuel_fraction(]0, 1]) laissent choisir la charge ; sans eux, l'appareil reçoit la capacité interne de son type, lue dans la base d'unités livrée (dcsUnits.yaml, champfuel_capacityissu duM_fuel_maxdu datamine). Ces deux actions écrivaientfuel = 0jusqu'à la 6.15.1, ce qui veut dire aucun carburant : mesuré en jeu le 2026-08-18, un KC-135 et ses deux F-15C d'escorte créés à 20 000 ft piquaient au sol dès leur apparition, réacteurs éteints. Un départ au parking masquait le défaut, DCS remplissant les réservoirs d'un appareil garé depuis le stock de la base. Un type inconnu de la base — un mod tiers — est créé sans cléfuel(DCS applique alors son propre défaut) et l'appelant est averti, plutôt que de se voir inventer un chiffre. - Assigne le pays à son camp dans
coalitions(voiradd_group), donc la mission reste chargeable.
add_air_group (lot FEAT-MCP-MUTATION-ACTIONS, ticket 09)¶
Écriture. Pose un vol (un ou plusieurs appareils) au parking en résolvant lui-même les places
depuis un nom d'aérodrome — le cas « un deux-ship de F-16 à Incirlik » que add_player_slot (un
appareil, place fournie) ne couvre pas. Sauvegarde horodatée avant écriture. Cible un dossier (durable)
ou un .miz (transitoire).
{
"target": "chemin/vers/dossier-mission",
"coalition": "blue", "country_id": 2, "country_name": "USA",
"name": "Viper", "unit_type": "F-16C_50", "count": 2,
"start": "parking-cold", "airfield": "Kobuleti"
}
- Résolution des places. Le nom d'aérodrome est résolu en id (
veaf_libs.dcs_airdromes), puis en places libres via la capture allégée bundlée (veaf_libs.dcs_parking, générée parveaf-build update-dcs-data --parking). L'action prendcountplaces libres, les plus proches de la piste d'abord, et pose chaque appareil à la position exacte du stand. parking_id=parking. Établi en jeu le 2026-08-15 :parkingest leTerm_Indexde la capture, l'appareil se cale sur la position exacte, et leparking_idpropre à l'éditeur — absent de la capture — n'est pas porteur. Il est donc écrit égal àparking.- Seuls les types de terminal 68, 72 et 104 sont proposés comme parking — le masque
FighterAircraft(244) de DCS lui-même. Le100(SmallSizeFighter) est exclu volontairement : DCS le documente comme une place étroite réservée aux petits appareils, et il ne débloque aucun aérodrome que les trois autres ne couvrent pas déjà. Un aérodrome sans place d'aucun de ces trois types est refusé plutôt que de poser un appareil sur un seuil de piste ou une hélisurface. La mesure qui fonde cet ensemble, et les sept aérodromes qu'elle a débloqués, sont consignées à côté deAIRCRAFT_STAND_TYPESdansveaf_libs/dcs_parking.py. - Collision refusée. Une place déjà occupée dans la mission (un groupe avion dont le premier waypoint vise cet aérodrome et dont une unité déclare cette place) est refusée en nommant le groupe qui la tient ; la sélection automatique saute les places occupées.
- Départs.
parking-cold/parking-hot(exigentairfield),runway(exigeairfield, ancré sur le terrain, pas de place consommée),air(exigeposition). La pairetype/actionet le verrouETA_lockeddu premier waypoint sont écrits pour l'appelant. skillvaut un niveau d'IA par défaut (High) — un vol au parking est IA sauf demande deClient/Player.parkingaccepte une liste explicite de places qui court-circuite la sélection.- Le plein interne par défaut.
fuel(kg) etfuel_fraction(]0, 1]) laissent choisir la charge ; sans eux, l'appareil reçoit la capacité interne de son type, lue dans la base d'unités livrée (dcsUnits.yaml, champfuel_capacityissu duM_fuel_maxdu datamine). Ces deux actions écrivaientfuel = 0jusqu'à la 6.15.1, ce qui veut dire aucun carburant : mesuré en jeu le 2026-08-18, un KC-135 et ses deux F-15C d'escorte créés à 20 000 ft piquaient au sol dès leur apparition, réacteurs éteints. Un départ au parking masquait le défaut, DCS remplissant les réservoirs d'un appareil garé depuis le stock de la base. Un type inconnu de la base — un mod tiers — est créé sans cléfuel(DCS applique alors son propre défaut) et l'appelant est averti, plutôt que de se voir inventer un chiffre. - Un théâtre sans capture, un aérodrome inconnu, ou trop peu de places libres sont refusés en nommant
la cause. Assigne le pays à son camp dans
coalitions.
remove_group (lot FIX-MCP-AUTHORING-GAPS, ticket 02)¶
Écriture. Retire un groupe de la mission — la seule édition au niveau groupe qui manquait au
catalogue, alors qu'edit_zone et edit_map_drawing ont tous deux un remove: true. Cible un
dossier (durable) ou un .miz (transitoire), sauvegarde horodatée avant écriture.
{"target": "chemin/vers/dossier-mission", "group_name": "Texaco"}
- Renumérote ce qu'il laisse derrière. C'est la raison d'être de l'action : supprimer un bloc Lua
à la main laisse la liste englobante numérotée
1,3,4, ce que Lua charge sans broncher et sur quoi le build meurt (AttributeError: 'int' object has no attribute 'get'), à une ligne qui ne désigne pas l'édition. Trois builds corrompus le 2026-08-18 venaient de là — et la rustine de l'époque, une regex de renumérotation calée sur la seule indentation, a aussi renumérotéunitsetroute.points. Les survivants gardent leur ordre et sont ré-indexés à partir de 1. - Le dernier groupe d'une catégorie fait disparaître la clé
group, au lieu de laisser un conteneur vide — la forme exacte qu'un lecteur en aval prend pour une liste (FIX-GROUP-CONTAINER-SHAPE). - Nom exact exigé. Un fragment est refusé, comme pour
set_group_properties: une suppression qui atterrit sur le premier groupe correspondant n'est pas rattrapable. Un nom introuvable liste ce qui existe, et rien n'est écrit. - Un nom porté par plusieurs groupes est refusé, en listant leurs
groupId:group_iddésigne celui à retirer, l'autre reste. Avant, l'action les retirait tous et n'en annonçait qu'un. - Nomme ce qu'il casse, sans refuser — le créateur de mission veut peut-être précisément ça :
une combat zone qui capture le groupe par préfixe de nom, une tâche
Escortqui pointe songroupId(y compris imbriquée dans unComboTask, la forme réelle de DCS), et une entréemodules.ASSETS.assetsdemission.yamlqui le nomme. Cette dernière n'est vérifiable que sur une cible dossier : un.mizne porte pas demission.yaml.
validate_group_name (vague 6)¶
Lecture seule. Contrôle un nom proposé contre les motifs réservés (préfixes
veafSpawn-/OnDemand-/VEAF-placeholder-, marqueurs #veafInterpreter[...]/#command=,
syntaxe de déploiement QRA, noms CAS fixes) et, avec miz_path, le piège de capture combat
zone (nom commençant par une trigger-zone existante). expected_combat_zone supprime
l'avertissement pour la zone intentionnellement visée. Partage le module
veaf_mission_mcp.group_naming avec add_group.
{"name": "combatZone_North-tanks", "miz_path": "chemin/vers/mission.miz"}
add_trigger_zone (vague 2)¶
Écriture. Insère une zone de déclenchement circulaire nommée dans mission.triggers.zones,
avec un zoneId frais, en place et sauvegardée d'abord. C'est la zone qu'une combat zone VEAF
référence : combinée à add_group, elle permet de poser une combat zone complète (la trigger
zone que group_validation exige + les groupes à l'intérieur). Pas de déduplication.
{
"miz_path": "chemin/vers/mission.miz",
"name": "combatZone_North",
"position": {"x": 1000.0, "y": 2000.0},
"radius": 3000,
"hidden": false
}
add_startup_script_trigger (vague 2)¶
Écriture. Ajoute un trigger « au démarrage de la mission » qui exécute un script — pour
outiller une mission vanilla ou CTLD avec du scripting sans passer par l'onglet Triggers de
l'éditeur DCS. Généralise inject_dcs_bridge_trigger et le chargement static/dynamic VEAF
(ADR 0004). Contrairement à ce helper (qui insère en
position 1 et renumérote tout), cette action ajoute à la fin (index libre suivant) — aucun
trigger existant n'est renuméroté. Trois modes :
inline— exécute du Lua fourni (inline_lua) viaa_do_script.file_static— embarque un fichier.lua(source_path) dans le.miz(l10n/DEFAULT/<nom>.lua+ entréemapResource) et le charge viaa_do_script_file.file_dynamic— charge un.luadepuis un chemin disque à l'exécution (runtime_path) vialoadfile, sans rien embarquer.
{
"miz_path": "chemin/vers/mission.miz",
"mode": "file_static",
"comment": "load my script",
"source_path": "C:/scripts/myscript.lua"
}
Sauvegarde horodatée avant écriture ; pas de déduplication.
Édition des fichiers Lua embarqués (vague 3)¶
Troisième famille d'actions : éditer le texte des fichiers .lua embarqués dans le
.miz (l10n/DEFAULT/**/*.lua), sans rebuild — ni les tables brutes mission.lua
(editor-parity), ni le pipeline mission.yaml (action VMCT). Brique commune :
mission_tools.rewrite_miz_members recopie l'archive verbatim et ne remplace que les membres
ciblés (aucune re-sérialisation des tables Lua). Sauvegarde horodatée avant chaque écriture.
replace_in_mission_files — search/replace générique¶
Remplacement texte ou regexp, restreint à l10n/DEFAULT/**/*.lua (jamais mission/
options ni les binaires). files est un glob appliqué au chemin relatif sous
l10n/DEFAULT/.
{
"miz_path": "chemin/vers/mission.miz",
"search": "debug",
"replace": "info",
"files": "veaf-*.lua",
"regex": false
}
Retourne {files_changed, total_replacements}.
Réglages VMCT (veaf-config.lua)¶
Actions sémantiques qui éditent l10n/DEFAULT/veaf-config.lua (config VEAF générée au build).
Chacune remplace la ligne si elle existe, sinon l'insère en tête (avant l'init des
modules) :
set_log_level(level)→veaf.ForcedLogLevel = "<level>"(parmi error/warning/info/debug/trace).set_module_enabled(module_id, enabled)→veaf.setConfig("<MOD>", "enable", <bool>).set_security_disabled(disabled)→veaf.SecurityDisabled = <bool>.set_veaf_config(key, value)→veaf.config.<key> = <scalaire Lua>.
Coalition d'un aérodrome¶
set_airbase_coalition(folder_path, name, coalition, dynamic_spawn=True)— assigne durablement un aérodrome DCS à une coalition, dans un dossier de mission ;dynamic_spawn=Falselaisse ses slots dynamiques fermés (une base ennemie).
⚠️ La coalition d'un aérodrome vit dans
warehouses.airports[<id>].coalition, pas dansmission.coalition. Poser une unité à côté d'une base ne la fait donc jamais changer de camp : c'est cette action qu'il faut. Elle résout le nom de l'aérodrome en identifiant via le théâtre de la mission, pose la coalition, et active les slots Dynamic Spawn de la base (le build les approvisionne ensuite), sauf sidynamic_spawnest faux. Sauvegarde préalable, comme les autres actions d'édition.
dynamic_spawn: falseinscrit aussi la base sous<camp>.exclude_airportsdanssrc/warehouses.yaml, ettruel'en retire : le build ouvrait toute base d'un camp déclaré sans listeairports:,dynamicSpawn = falseou pas (FIX-SCRATCH-MISSION-FINDINGSticket 15). Rien n'est écrit si le fichier manque (l'étape ne tourne pas) ou si le camp n'y est pas déclaré (le déclarer ouvrirait toutes ses bases).
Campagne multi-missions¶
campaign_status(campaign_folder)— où en est la campagne : missions appliquées, propriétaire, force de garnison, type et voisins de chaque zone, réserves, objectifs atteints ou non, changements de la dernière mission. Lecture seule.campaign_apply(campaign_folder, state_file)— fusionne le fichier d'état d'une mission jouée, joue le tour entre les missions, juge les objectifs, écrit et rend le débriefing FR/EN ; refuse un fichier déjà appliqué, d'une autre campagne ou qui saute une mission, sans rien écrire.campaign_next(campaign_folder, players?)— crée (copie detemplate/) ou rafraîchitmissions/mission-NN/mission: aérodromes à leur propriétaire parset_airbase_coalition,src/campaign-data.yaml, moduleCAMPAIGNdansmission.yaml, et, sur un dossier qu'il crée, la situation stratégique comme briefing et la date, l'heure et la météo fixées dans la mission (une seule mission, sans variante ; date du lendemain de la mission précédente, heure solaire calculée sur le terrain de la campagne, météo tirée sol visible) ; écrit à côté le document de briefing stratégique, et le briefing de mission si la mission est construite ; rend le dossier,conditions, les documents et la partie factuelle du briefing stratégique en FR et EN. Sa description demande à Claude de fixer date et heure selon l'avancée de la campagne et de garder le sol visible, et de demander combien de joueurs sont attendus ce soir :players(un nombre, ou une fourchette"5-7", qui prime sur leplayersdecampaign.yaml) écrit le blocopposition:de la mission.campaign_briefing(campaign_folder)— écritmissions/mission-NN/briefing-campagne.pptxet sa carte stratégique à partir de la campagne (les faits) et debriefing.yaml(la prose) ; rend les chemins, le nombre de pages, si la prose était là, si la carte a dû se passer de tuiles, les sections quebriefing.yamlpeut contenir, etmission_deck: le briefing de mission (briefing-mission.pptx, cartes tactiques), lu dans la mission construite —Noneet un avertissement qui dit comment construire tant qu'il n'y a pas de.miz. Sa description porte les règles d'écriture : un briefing de situation militaire, aucune mécanique de jeu hors de l'annexe, l'ennemi réduit à un renseignement inégal.
Chacune est la commande veaf-tools campaign correspondante (campaign_manager.CampaignWorker), qui rend des données au lieu de les afficher. Voir Campagne multi-missions.
FARP¶
add_farp(target, name, position, coalition, country_id, country_name, farp_type="FARP", frequency_mhz=127.5, modulation="AM", callsign_id=1, ammo_dump=True, keep_position=False)— un FARP complet : le statique d'héliport (category = "Heliports",shape_namedu type), sa radio et son indicatif, et l'entrée d'entrepôtwarehouses.warehouses[<unitId>]qui permet de s'y ravitailler.add_groupenstaticne posait que l'objet (ticket 19). Forme mesurée sur les 372 héliports des missions deD:\dev\_VEAF. Lefarps:dewarehouses.yamll'approvisionne ensuite au build, comme une base. Par défaut, son dépôt de munitions (FARP Ammo Dump Coating,<nom> - Ammo, 120 m à l'est,Fortifications/SetkaKPcomme les 68 des missions OT) que CTLD prend comme point de chargement ;ammo_dump=Falses'en passe (FIX-OPEN-TRAINING-SYRIA-FINDINGSticket 05). Un héliport en mer (sol à 0 m sur la grille d'altitude) est signalé (ticket 04). Le pad est vérifié (FIX-FARP-PLACEMENTticket 01) : là où le catalogue de sol dégagé couvre l'endroit, un pad dans les arbres ou les bâtiments est déplacé avec son dépôt sur le sol dégagé le plus proche, à 1 km au plus (keep_position=Truerefuse le déplacement et se contente d'avertir) ; là où la grille d'altitude le couvre, plus de 3 m de dénivelé entre le centre et un anneau à 40 m est signalé comme une pente. Là où ni l'un ni l'autre ne couvre l'endroit, l'avertissement dit ce qui n'a pas été vérifié et donne la commandeveaf-tools dcs farp-spot, qui cherche dans DCS une place plate et dégagée ;positiondans le résultat dit où le pad est allé. Ni la grille ni le catalogue ne voient un lac ou une rivière (la grille ne connaît que la mer) : le résultat porte toujourssurface_check, qui le dit et redonne cette commande (FIX-OPEN-TRAINING-FALKLANDS-FINDINGSticket 03). Une fréquence qui est celle d'une tour d'un aérodrome du théâtre, dans la même bande (AM:vhfetuhf;FM:fm), est signalée avec le nom de l'aérodrome. Retirer les arbres autour du pad par un trigger (ticket 02) a été essayé puis retiré : l'action DCS « SCENERY REMOVE OBJECTS ZONE » n'a enlevé aucun arbre, ni sur MarianaIslands ni sur Caucasus (limitation connuescenery-remove-objects-zone-leaves-the-forest).
Groupe aéronaval, slots sur le pont, sons (FIX-OPEN-TRAINING-PROMPT-FINDINGS)¶
add_carrier_group(mission_path, coalition, country_id, country_name, name, position, heading_deg, speed_kt, carrier_type="Stennis", carrier_name, escorts, tower_mhz, tacan_channel, tacan_callsign, icls_channel, link4_mhz, recovery_tanker, tanker_*, rescue_helicopter)— le porte-avions (et ses escorteurs) en route, sa radio sur l'unité (hertz),ActivateBeacon(TACAN,system3),ActivateICLS, et sur un pont à brins d'arrêtActivateLink4+ActivateACLS; les groupes<unité> S3B-Tanker(tâcheTanker+ TACAN Y) et<unité> PedroqueveafCarrierOperationscherche par leur nom ; l'entréewarehouses.warehouses[<unitId>]. Les tâches ATC sont écrites au premier point de route et dans letasksdu groupe : sur 218 groupes mesurés, 120 les rangent au groupe, 55 sur la route, 43 aux deux, etveafCarrierOperationsne lit que le groupe ; lequel des deux DCS exécute n'a pas été mesuré.add_air_group(..., start="deck-cold"|"deck-hot", carrier=<unité navire>)— premier point lié au navire (linkUnit=helipadId= sonunitId), places de pont numérotées à la suite ; refusé si l'appareil ne peut pas à la fois décoller de ce pont et y apponter (TakeOffRWCategories/LandRWCategories, capturés dansdcsUnits.yaml).add_sound(mission_path, sound_path, resource_name)— copie un.ogg/.wavdansl10n/DEFAULTet le déclare dansmapResourcesousMCP_Sound_<nom>;edit_routetransmit_messagele diffuse (TransmitMessageenveloppé,file= la clé,loop,duration,subtitleécrit au dictionnaire).set_briefing_picture(mission_path, source_path, side, resource_name)— copie un.png/.jpgdansl10n/DEFAULT, le déclare dansmapResourcesousMCP_Picture_<nom>et ajoute la clé aupictureFileNameB/R/Ndu camp, en gardant les images qu'il avait (FIX-DEMO-MISSION-FINDINGS 07).build_mission_briefing(mission_path, title, strategic_situation, context, mission_bullets, objectives, threats, air_opposition, air_defences, other, deployment, waypoint_notes, flights, code_words, recon_folder, diversions, kneeboard, pilots, loadouts, basemap, side)— le briefing VEAF d'une mission à objectifs, sans script (FEAT-MCP-MISSION-BRIEFING) :docs/briefing.pptx, son PDFdocs/briefing.pdf(dessiné parfpdf2avec la police DejaVu embarquée : mêmes pages et mêmes mots, mise en page propre, aucun Office requis ;pdf_pagesdans le résultat),docs/maps/et les images de briefing DCS, dans le dossier et le.mizconstruit, en tête depictureFileNameB/Nsous des clésMCP_Briefing_*remplacées à chaque appel. L'assistant passe les mots ; le reste est lu dans le.mizconstruit (mission_briefing/), dont les presets injectés (Radio[n].channelsNames) pour le plan de fréquences.basemap: topoprend les tuiles OpenTopoMap, avec leur crédit et leur dossier de cache. FEAT-OBJECTIVE-BRIEFING-QUALITY :strategic_situationetdeploymentfont chacun leur page, omise vide ; une chaîne est de la prose, ses paragraphes séparés par une ligne vide (mission_bulletsen chaîne perd ses tirets) ;flightsnomme les vols d'un package en slots dynamiques, listés sous leur base au lieu des types qu'elle offre ;waypoint_notes(par nom de waypoint) va sur les fiches « étape par étape » du plan de vol. La carte générale est cadrée parMapCanvas(width=…, rotation=…): au moins 2000 px, symboles à l'échelle, tournée parbest_rotationquand le cadre 2:1 y gagne 15 %, flèche nord, clé dessinée dans la carte ; les zooms portent un encart (Inset). Les cibles et les étapes sont desCardSection(mission_briefing/briefing_cards.py), dessinées dans le deck et le PDF ; le deck et le PDF embarquent les cartes en JPEG. FEAT-OBJECTIVE-MISSION-V2 : une carte « zone des combats » (render_overview_map(combat_area=True),docs/maps/combat-area.png, aussi image de briefing DCS) suit la carte générale ; les cartes du kneeboard sont tournées axe d'attaque vers le haut (attack_axis_rotation, cap vrai de la base aux objectifs), flèche nord sur chacune ;code_words(word,meaning,who) fait une page « Mots-codes » après le déploiement, et une page de kneeboard. FIX-BRIEFING-PARITY-MARTEAU : la carte générale cadre le théâtre (bases, ravitailleurs, AWACS, centres des QRA, lignes dessinées sur la carte par le camp des joueurs), les QRA en tirets orange, la portée d'un SAM en contour sur une teinte légère ; un objectif porte le nom du point du plan de vol qui le couvre (objective_waypoint), la route hélicos est verte en tirets ; la zone des combats ajoute les unités au sol alliées, le JTAC (groupe nomméJTAC…), les axes des groupes routés et la portée sur l'étiquette de chaque menace (ground_picture). Une cible est une unité quand ses unités portent un nom donné par l'auteur, sinon son type ou la famille de sa commande de spawn VEAF, avec son rôle (attributs DCS) et son bullseye ;objectives[].flightsdonne le « Pour : » d'un objectif. Avec une opération dansmission.yaml, ses tâches sont les objectifs principaux, les autres secondaires, une zone enchaînée nulle part. Avecflights, le plan de fréquences et la carte de vol des planchettes suivent les types que volent les vols, un bloc par plan radio. PDF aligné à gauche ; ATO, déploiement (<quand> — <qui> : <quoi>) et mots-codes en tableaux.waypoint_notesaccepte{"plane": {…}, "helicopter": {…}};kneeboard_replaceretire les pages de planchette qui ne sont pas de l'outil (dossier →.veaf-backups/kneeboard-<date>/, et chaque.miz). FIX-BRIEFING-PARITY-MARTEAU-2 : la carte générale dessine et cadre les bases de départ (en slots dynamiques, celles des vols deflights, positions deairdrome-positions.yaml) et les terrains ennemis, le niveau de vol des ravitailleurs, et laisse de côté une ligne dessinée qui ne fait que retracer une route ; chaque route part de la base de ses vols. La zone des combats nomme les objectifs comme le plan de vol et n'a plus les arcs QRA ; une clé plus haute que 40 % de la carte passe sur deux colonnes, dans le coin que les étiquettes laissent libre, au-dessus de l'échelle et du crédit. Un bloc de fréquences dit quels vols le portent sur quelle radio.objectives[].status(main,secondary,prerequisite) l'emporte sur l'opération. La carte de vol des planchettes est un tableau avec l'altitude. L'ATO donne le niveau des ravitailleurs ;support(callsign,role,frequency,details) y ajoute un JTAC, le fat cow, un GCI. FIX-BRIEFING-MAGNETIC-HEADINGS : tous les caps et gisements bullseye que le briefing imprime sont magnétiques,°M, comme les lisent les cockpits (le manuel du F-16C donne le bullseye « in degrees magnetic » ; le F/A-18C affiche du magnétique sauf TRUE choisi sur le HSI) : le cap vrai sur l'ellipsoïde WGS84 moins la déclinaison du World Magnetic Model au mois et à l'année de la mission, le modèle qu'utilise DCS (veaf_libs/magnetic.py). Ils étaient pris sur la grille DCS, qui n'est ni l'un ni l'autre : 6,9 degrés d'écart sur le Golan. Le cap d'un porte-avions et celui d'une photo de reco sont sur la grille dans la mission et dans la fiche, comme les tiennent l'éditeur de mission et la caméra DCS : ils sont tournés en magnétique là où ils se trouvent, dans les légendes et sur le bandeau de la photo (dont la rose pointe le nord vrai, comme les cartes). Les distances sont mesurées sur l'ellipsoïde WGS84, plus sur la grille, qui les gonflait de son échelle (0,1 % sur le Golan, 1 % à Tbilissi). Photos de reconnaissance (mission_briefing/briefing_recon.py) — l'interface que le lot en jeu remplit : un dossier (recon_folder, relatif au dossier de mission ou absolu ;docs/recon/par défaut, ignoré s'il n'existe pas) de paires<stem>.jpg(ou.jpeg) +<stem>.json, UTF-8 :build_mission_promotion(mission_path, title, event, tagline, pitch, program, practical, aircraft, closing, calendar_url, briefing_url, recon_folder, side)— l'annonce d'une mission construite (FEAT-OBJECTIVE-MISSION-V2) :docs/poster.jpg(1920 × 1080),docs/calendar-banner.jpg(2400 × 432, le 5,55:1 du calendrier VEAF) etdocs/discord-post.md, refusé au-delà des 2 000 caractères de Discord (mission_briefing/mission_promotion.py). Fond : la première photo de reco (parorder) étalonnée, ou la carte ; la carte « zone des combats » debuild_mission_briefinget la photo épinglées. Les lignes pratiques du message viennent du.mizconstruit (théâtre, moment de la journée, ciel, bases à slots dynamiques ou vols).event(date et heure locales) et les liens sont des paramètres, jamais inventés : absents, c'est dit danswarnings. Le résultat rend le texte (discord_post,discord_chars).
| clé | requise | sens |
|---|---|---|
zone |
oui | la zone de combat de l'objectif, comme objectives[].zone (casse ignorée) |
title |
oui | la légende, dans la langue de la mission |
heading |
non | cap de la caméra, en degrés sur la grille de la mission comme la caméra DCS le tient (ce qu'écrit veaf-tools dcs recon-shoot) : l'axe de la photo ; imprimé magnétique, °M |
pitch |
non | inclinaison, degrés, négative vers le bas |
x, y |
non | position de la caméra, coordonnées carte DCS (x nord, y est) ; imprimée en DMS |
altitude_m |
non | altitude de la caméra, mètres ; imprimée en pieds |
order |
non | tri dans la zone (puis nom de fichier), 0 par défaut |
Après le zoom de chaque objectif, ses photos deux par diapositive (deck et PDF) et deux par page de kneeboard, légendées « titre — cap 095°, piqué -12°, depuis recon.json que veaf-tools dcs recon-render écrit à côté est ignoré. Une photo sans fiche, une fiche sans photo, sans zone ou title, ou d'une zone qui n'est pas un objectif : ignorée, dans warnings.
Deux photos par objectif au plus (une par défaut) : au-delà, les suivantes dans l'ordre (order, puis le nom) sont ignorées, dans warnings — Marteau du Golan en avait six pour un seul objectif. Le résultat liste les photos prises dans recon.
Open Training Syrie (FIX-OPEN-TRAINING-SYRIA-FINDINGS)¶
- Indicatif tiré du nom : un vol occidental nommé comme son indicatif (
Texaco 2,Magic 1) reçoit cet indicatif (Texaco21) si la famille convient à la tâche et que le vol est libre ; sinon la règle d'avant (première famille libre) et un avertissement qui dit pourquoi. Un vol d'IA à tâche de combat (Escort,CAP,CAS,SEAD…) sanspylonsest signalé (ticket 01). set_unit_properties:callsign.namesans chiffres est complété (Texaco→Texaco21) ;pylonsprend{station: CLSID}comme{station: {CLSID: …}}et refuse tout autre valeur en nommant la station (ticket 02).create_qran'écrit plussimple_groupsquandgroups_by_enemy_countest donné : à côté de paliers, ils ne décollent jamais (ticket 03, mesuré danstest_veafQraManager.lua).- Contrôle de surface :
add_group,create_combat_zone,add_farpet le déplacement deset_group_propertiessignalent un véhicule, un statique ou un FARP à 0 m (en mer) et un navire au-dessus de 0 m (à terre), là où le théâtre a une grille d'altitude ; sinon « surface not checked ». 3 m est de la terre : DCS ne descend jamais sous 0 (ticket 04). add_air_group(task="AFAC")— un drone laser comme ceux de GermanyCW-v6 : premier point avecSetUnlimitedFuelpuis une orbiteCircleà l'altitude et la vitesse du groupe. Le marquage est celui de CTLD, par l'entréemodules.ASSETS(jtac,freq,mod) ; CTLD le remonte àJTAC_droneAltitude, il ne désigne que des véhicules, à 10 km (ticket 06).geocode: une requête par seconde au plus (Nominatim) ; un 429 attend ce que demandeRetry-After(30 s au plus) une fois, puis le refus revient enfound: falsedit en clair. Cinq candidats demandés, un lieu nommé préféré à une route ou une région ;osm_class/osm_typedans la réponse, et un avertissement quand c'est une route ou une région (« Al-Kiswah » → une rue d'Amman) (ticket 07).list_unit_typesdonnethreat_range_m/detection_range_m(mètres,ThreatRange/DetectionRangedu datamine, les cercles de l'éditeur) pour chaque unité qui en a ;dcsUnits.yamlles porte, régénéré parveaf-build update-dcs-data --units(ticket 08).- Emports DCS par nom :
add_air_group,create_qra(par groupe) etcreate_cap_missionprennentpayload, le nom d'un emport que l'éditeur propose (list_payloads), à côté depylonsetloadout_from— un seul des trois. Source :MissionEditor/data/scripts/UnitPayloadsd'une installation (613 emports, 45 types), le datamine n'ayant pas ces fichiers (ticket 09). repair_static_shapes(target)— complète leshape_namede chaque statique posé sans (avant 6.26) depuis la base d'unités, dit ce qu'il a complété et les statiques dont le type n'a pas de forme connue ; n'écrit rien s'il n'y a rien à compléter. Le message devalidatele nomme (ticket 17).- Les sauvegardes d'un fichier de
src/mission/l10n/DEFAULT/vont dans.veaf-backups/(elles restaient à côté et partaient dans le.miz), et une sauvegarde de dossier réécritmapResource(tickets 15, 16).
Open Training Mariannes (FIX-OPEN-TRAINING-MARIANAS-FINDINGS)¶
create_qra: un groupe ne porte aucune tâche sur son point, quelle que soit satask(CAPcommeIntercept). Avectask: "CAP", l'EngageTargetsautomatique de l'éditeur faisait voler la route telle quelle : un point, pas d'orbite, et l'intercepteur se posait au lieu de défendre sa zone (ticket 01).add_carrier_group: le ravitailleur embarqué prend la familleShellen partant du vol 9 (Shell91, puisShell81pour le porte-avions suivant), ettanker_callsign("Arco 5") en impose un ; le résultat le dit danstanker_callsign. Les ravitailleurs nommésTexaco 1,Arco 1posés ensuite gardent le leur (ticket 02).add_air_groupprendloadout_from, comme les composites : un groupe de la mission, puis des catalogues du dossier, puis des catalogues livrés (un templateveafSpawn-*). C'est le seul emport sourcé d'un avion de module sans emport DCS nommé (J-11A, F-16C) ; un seul depylons,payloadetloadout_from(ticket 03).- Catalogue livré des slots dynamiques : les templates J-11A portent l'emport air-air de l'Open Training Mariannes v5 (
veafSpawn-J-11A - FOX3 - ECM ON - Radar ON - HARD X1), doncloadout_from: "J-11A Template Red"arme un vol. Les templatesA-4E-C,OV-10AetT-45, trois mods, en sortent, et le build avertit quand un template nomme un type que la base d'unités DCS ne connaît pas (ticket 04). describe_missionlit un dossier de mission ;describe_unitsdonne legroup_idde chaque groupe et leunit_idde chaque unité (edit_route add_task attack_groupprend le premier) ;terrain_elevationprendmission_pathet y lit le théâtre (theatrereste possible) (ticket 05).geocodeavertit quand il rend une île ou une zone pour un lieu qui s'y trouve : « Umatac, Guam » rendait le centre de Guam sans un mot (ticket 05).add_groupet chaque groupe decreate_combat_zoneprennentroe(weapons_free,return_fire,weapons_hold) etdisperse, écrits sur le premier point comme l'éditeur les écrit (Option0 ;Option8 sans valeur pour « ne se disperse pas »).roe: "weapons_hold"etdisperse: falsefont une cible inerte (ticket 06).- Sans grille d'altitude ou sans catalogue de terrain dégagé, l'avertissement de placement dit ce qui n'a pas été vérifié (la mer, les arbres, les bâtiments) et comment le vérifier :
veaf-tools dcs clear-ground-checksur la mission construite. Sur MarianaIslands, rien n'est vérifié tant que les balayages n'ont pas été faits (limitation connuemarianas-placement-checks-nothing, ticket 07).
Réglages de la mission (FIX-SCRATCH-MISSION-FINDINGS ticket 07)¶
Ce que l'éditeur règle hors de tout groupe, et que GermanyCW-v6 a dû patcher par un sérialiseur Lua.
Chaque action vise un dossier de mission (durable) ou un .miz, sauvegardé avant écriture.
set_mission_date(target, date?, start_time?)—mission.date(AAAA-MM-JJ) etmission.start_time(HH:MM[:SS], l'horloge du théâtre). Les variantes météo gardent la main sur les deux.set_bullseye(target, coalition, position)—mission.coalition.<camp>.bullseye, d'où vient le waypoint BULLSEYE de chaque plan de vol.set_route(mission_path, plan, points, coalition?, category?, aircraft_type?, country?)— un plan de vol des joueurs dans lesrc/waypoints.yamldu dossier : les points dans l'ordre (x/y ou lat/lon), chacun posé au sol depuis la grille du théâtre, un nom répété refusé, le plan remplacé avec ses anciens points ; les commentaires du fichier sont gardés. Rend le cap (magnétique, à la date de la mission) et la distance (sur l'ellipsoïde) de chaque branche, et nomme un plan qui l'emporterait encore (WaypointsManager.get_flight_plan_for: le plus spécifique, puis le premier déclaré).set_briefing(target, sortie?, situation?, blue_task?, red_task?, neutrals_task?)— les textes du briefing. Quand la table de mission porte une référenceDictKey_…, le texte va dansl10n/DEFAULT/dictionaryderrière elle, et la référence reste valide ;write_mission_folderréécrit désormais ce dictionnaire, seulement s'il change.set_weather(target, metar?, temperature?, wind_speed?, wind_direction?, visibility?, cloud_type?, cloud_height?, precipitation?, fog_enabled?, clearsky?, reference_airfield?, wind_gust?, turbulence?, wind_veer?, wind_speed_2000?, wind_direction_2000?, wind_speed_8000?, wind_direction_8000?)— la météo de la mission de base, dans les champs que DCS lit (ticket 19 : la mission vierge a ses nuages au sol,Preset1à 0 m). Même vocabulaire et même convertisseur queversions[].weather, donc un METAR marche aussi ; les variantes gardent la main au build.wind_speedest le vent voulu à 10 m au-dessus du terrain de référence, pas un réglage de DCS (FEAT-MISSION-WIND-CHECK) : la réponse donnereference_airfield(nom, altitude, comment il a été choisi),simulated_wind(le vent que DCS simulera au-dessus, par couche : sol, 1 000 et 3 000 ft sol, FL100, FL250) et deswarnings— niveau de la mer pris faute de terrain, jet dans la couche basse, et avec un slot hélico, chaque endroit où le vent simulé dépasse 20 kt entre 0 et 600 m sol (le vent).
Les hashes de mot de passe (
veafSecurity.password_L9[...]/password_MM[...]) — un cas multi-lignes — ne sont pas couverts pour l'instant : seul le drapeauSecurityDisabledl'est.
Actions VMCT sur mission.yaml (vague 4)¶
Quatrième famille — la première vraiment VMCT : éditer le source déclaratif
mission.yaml (ce que le build consomme pour générer le .miz), au lieu de patcher un
artefact déjà construit. Brique commune : mission_tools.mission_yaml_editor (mode round-trip
ruamel.yaml) qui préserve commentaires, ordre des clés et mise en forme — indispensable
pour un fichier source très commenté, édité à la main et tenu en lockstep avec le défaut livré.
Sauvegarde horodatée avant chaque écriture.
describe_mission_config¶
Lecture seule. Liste le bloc modules: et, par module, son état : mandatory (clé nue),
scalar (booléen MODULE: true/false) ou extended (bloc de config imbriqué type
COMBATZONE/CTLD). Le pendant VMCT de describe_mission.
{"mission_yaml_path": "chemin/vers/mission.yaml"}
set_mission_module¶
Écriture. Active/désactive un module ou pose son bloc de config étendu, en préservant les
commentaires. value est soit un booléen (forme scalaire), soit un objet (bloc étendu). La clé
est remplacée si présente, insérée sinon. Pas de déduplication.
{
"mission_yaml_path": "chemin/vers/mission.yaml",
"module_id": "COMBATZONE",
"value": {"enabled": true, "combat_zones": [{"type": "zone", "zone_name": "CZ-Alpha"}]}
}
Périmètre volontairement générique (toggle + pose de mapping) — pas de validateur de schéma par module : la forme du bloc de config passé reste la responsabilité de l'appelant (LLM), comme les types d'unités pour
add_group.
Parité recette / construit (vague 7)¶
Chaque réglage éditable sur le veaf-config.lua construit (vague 3) a son pendant source
mission.yaml, pour que les deux cibles soient joignables. Actions séparées (cohérent avec
set_mission_module), sur la brique mission_yaml_editor :
| Réglage | Recette (mission.yaml) |
Construit (veaf-config.lua) |
|---|---|---|
| Niveau de log | set_mission_log_level → global_log_level |
set_log_level |
| Sécurité | set_mission_security → bloc security: (+ hash de mots de passe) |
set_security_disabled |
| Paramètre arbitraire | set_mission_setting → settings.<clé> |
set_veaf_config → veaf.config.<clé> |
| Activation de module | set_mission_module (vague 4) |
set_module_enabled |
| Options imposées aux joueurs | set_forced_options → mission.forced_options |
— (le build les écrit dans forcedOptions de la mission) |
Oracle de connaissance métier (vague 5)¶
Les actions ci-dessus sont les mains (écriture) et les yeux (describe_*) du LLM. La
vague 5 lui donne un cerveau : des actions de lecture seule exposant la connaissance
DCS + VEAF nécessaire pour éditer correctement. Toutes lisent depuis les sources canoniques
que le build utilise déjà, donc sans dérive possible :
- Données DCS générées (
update-dcs-data→veaf_libs/data/dcsUnits.yaml, publiées sur le GitHub VEAF) ; - alias VEAF (
veaf_libs/data/veaf-units.yaml) ; - artefacts vendorisés (
vendored.yaml,check-vendored) ; - repos de datamining en amont (provenance).
Implémentation : veaf_mission_mcp/oracle.py. Le pendant « prose / comment raisonner » est le guide de création,
veaf_mission_mcp/data/authoring-guide.md, servi par describe_authoring_guide — le serveur = les mains, le guide
= le cerveau.
list_unit_types¶
Lecture seule. Types d'unités DCS depuis la base générée, filtrables par category et/ou
name_contains. Pour que le LLM choisisse des types concrets.
{"category": "Plane", "name_contains": "su-27"}
list_countries (lot FIX-OPENCODE-TEST-FINDINGS)¶
Lecture seule, sans mission. Les pays DCS en {id, name, short?}, par id, depuis veaf_libs/data/dcs-countries.yaml : le country_id et le country_name que prennent add_group, add_air_group, add_farp et les composites.
name_contains cherche dans le nom, le nom de l'éditeur de mission et le code court.
Faute d'action, une IA lisait le fichier dans le dossier dépaqueté de l'exécutable.
{"name_contains": "turk"}
list_payloads¶
Lecture seule. Les emports par défaut que l'éditeur de mission propose pour un type d'avion d'IA, par
nom (« R-40T2,R-334 » pour un MiG-31), avec leurs pylônes — ce que prend payload dans
add_air_group, create_qra et create_cap_mission. Sans unit_type, la liste des types qui en ont.
Lus dans veaf_libs/data/payloads.yaml, généré depuis une installation DCS
(veaf-build update-dcs-data --payloads --dcs-path <DCS>) : le datamine n'a pas ces fichiers.
Le générateur lit le dossier UnitPayloads de l'éditeur et celui de chaque module
(CoreMods/**/UnitPayloads) : les avions de module (P-51D, P-47D, Fw 190, F-16C, FA-18C…) y sont. Il
liste les rares fichiers qui sont du vrai Lua et non des données (un CLSID calculé depuis un local) et
laisse leurs types de côté — le Spitfire n'a aucun fichier dans l'installation. Les deux bombardiers IA
d'une mission WW2 n'en ont pas non plus : leurs emports par défaut (Ju-88A4 « SC5002,SC5010 »,
B-17G « M64*12 ») sont écrits à la main dans veaf_libs/data/payloads-ai-bombers.yaml, lus dans le
db.Units.lua de l'installation.
{"unit_type": "MiG-31"}
list_shortcuts¶
Lecture seule. Le vocabulaire d'alias VEAF (shilka, sa8…) — alias d'unités
(_spawn unit <alias>) et de groupes composites (_spawn group <alias> : sites SAM, convois),
plus les raccourcis #command (-samLR, -armor…) avec randomParameters : la plage
{min, max} de chaque paramètre tiré à chaque usage — -samLR et -samSR lancent la même
commande et ne diffèrent que par leur plage defense. Filtrable par name_contains.
Chaque entrée porte une era, lue dans ce qu'elle fait apparaître et jamais saisie à côté
(veaf_libs/shortcut_eras.py) : follows_era (-aaa, -samSR, -samLR, -samVLR, -armor,
-infantry, -transport, -combat, -convoy, -cas : les unités viennent de tables indexées par
mission.era), ww2 (-freya, -wurzburg, -flak88, -lefh, -pak40, -wespe), post_ww2 (du
matériel fixe d'une époque postérieure : -ewr, -arty, -sa6…), era_free (n'apparaît rien) ou
unknown (-jtac, -afac, -cargo : non mesuré, donc non affirmé). era (WW2, COLD_WAR, MODERN)
garde ce qui vaut d'être proposé dans une mission de cette époque. validate avertit d'un #command ou
d'un #veafInterpreter à raccourci post_ww2 dans une mission WW2.
describe_known_limitations¶
Lecture seule. Pour la version de veaf-tools en cours, les entrées de
veaf_libs/data/known-limitations.yaml : les limites des outils pas encore corrigées
(kind: tool, retirées à partir de la version de leur fixed_in) et les comportements de DCS qui
ne lèvent aucune erreur (kind: dcs, toujours renvoyés, avec la date de leur mesure). Chaque
entrée : id, kind, area, title, symptom, workaround, éventuellement cost et theatres. Le
fichier est la seule source : docs/agents/dcs-runtime-traps.md en est généré.
Le texte entier fait environ 74 000 caractères, plus qu'un résultat d'outil n'en porte. Sans argument,
l'action renvoie une liste courte — id, kind, area et theatres s'il y en a, sans titre, environ 7 000 caractères — et le nombre
d'entrées de chaque area. area (spawning, air-defence, players, radio, ground-ai, scripting, mcp, pipeline, runtime),
kind et theatre resserrent cette liste et la gardent courte ; theatre écarte les entrées qui ne concernent que d'autres théâtres
(une entrée sans theatres vaut partout). Lisez en entier celles dont vous avez besoin avec id ou ids (une liste) ; full: true renvoie
tout ce qui est retenu. Une area revenait en entier : trois d'entre elles faisaient 46 000 caractères en un appel, portés jusqu'à la fin de la session (FIX-OPENCODE-TEST-FINDINGS).
{"ids": ["ai-ground-start-needs-a-parking-capture"]}
describe_authoring_guide (lot FEAT-AI-ASSISTANT-ANY-CLIENT)¶
Lecture seule, sans paramètre. Renvoie {"guide": …} : le guide de création, veaf_mission_mcp/data/authoring-guide.md, tel quel.
Le build embarque tout le dossier veaf_mission_mcp/data/ dans l'exe ; en développement l'action lit le fichier du paquet.
Les consignes du serveur (instructions du protocole MCP, envoyées au client à la connexion) demandent d'appeler cette action en premier, puis describe_known_limitations.
Elles tiennent en quelques lignes exprès : un client les met dans le prompt de chaque session, elles désignent donc le guide au lieu de le contenir.
Tous les clients n'en tiennent pas compte ; la page d'installation fait donc commencer la conversation par la demande explicite.
{}
read_mission_file / edit_mission_file (lot FEAT-AI-ZERO-SETUP)¶
Les sources YAML d'un dossier de mission — mission.yaml et src/*.yaml, rien d'autre, ni .. ni chemin absolu — pour une IA sans accès direct aux fichiers.
En mode chat de Claude Desktop, une IA à qui l'on demandait de renommer une mission a répondu qu'aucune action ne le pouvait ; mission.name, mission.era et les fichiers src/*.yaml que la liste du guide lui demande d'adapter n'avaient aucune action.
read_mission_file renvoie {file, first_line, last_line, total_lines, content}, 400 lignes par défaut (start_line, line_count) : le catalogue de slots en compte 11 000.
Ses lignes finissent par \n, quel que soit le fichier.
Sans mission_path, elle lit le gabarit livré avec les outils — ce que scaffold_mission pose : les templates de slots dynamiques, le plan de presets, mission.yaml avec l'exemple commenté de chaque module — et le dit par shipped: true, pour vérifier un plan avant qu'un dossier existe.
edit_mission_file remplace un passage exact (search / replace), refusé s'il apparaît zéro ou plusieurs fois, ou si le résultat ne se relit plus en YAML ; sauvegarde avant écriture.
Les fins de ligne sont comparées normalisées, et le fichier garde les siennes : un passage recopié avec \n depuis un fichier CRLF était « introuvable » (FIX-OPENCODE-TEST-FINDINGS).
Implémentation : veaf_mission_mcp/mission_files.py.
{"mission_path": "C:/VEAF/missions/convoi", "file": "mission.yaml", "search": "name: My-Mission", "replace": "name: helico_convoi_Montagnes"}
describe_mission_prompt (lot FEAT-AI-ZERO-SETUP)¶
Lecture seule. Renvoie {"kind", "lang", "prompt"} : les consignes qui construisent une sorte de mission de bout en bout — les questions à poser, les règles de conception, l'ordre de construction.
kind vaut objective (une mission jouée une fois, en une séance) ou open-training (une mission d'entraînement VEAF sur une carte entière) ; lang vaut en (par défaut) ou fr.
Les prompts vivent dans veaf_mission_mcp/data/mission-prompts/, embarqués avec le guide ; ils étaient dans le .prompts/ du dépôt, à coller par l'utilisateur.
Le guide de création envoie ici un utilisateur qui demande une nouvelle mission : « crée une mission » suffit.
Le prompt objectif vient en deux parties, coupées à une ligne <!-- part: build --> : part: scenario (par défaut, environ 13 000 caractères, qui finit en disant comment charger la suite) et part: build, une fois un scénario approuvé ; part: all renvoie les deux.
Les sections 4 à 9, 21 000 de ses 34 000 caractères, ne servent qu'après l'approbation, et étaient portées à chaque tour de la discussion du scénario (FIX-OPENCODE-TEST-FINDINGS).
{"kind": "objective", "lang": "fr"}
offer_clear_ground_check (lot FEAT-CLEAR-GROUND-AT-AUTHORING)¶
Lecture seule, et ne lance rien. Sur un .miz construit, renvoie de quoi proposer à
l'utilisateur une vérification en jeu : la commande veaf-tools dcs clear-ground-check à lancer (elle
demande DCS), ce qu'elle fera, le nombre de véhicules et de marqueurs, et le critère de comptage. La
vérification sonde la position de chaque véhicule sur la mission d'arpentage vide : aucun véhicule
n'y existe, donc aucun n'est compté comme bloqué par ses voisins — le piège qui a faussé trois chiffres
publiés. Elle compare ensuite chaque réponse à ce que le catalogue avait prédit.
{"miz_path": "chemin/vers/mission.miz"}
offer_scenery_lookup (lot FEAT-OBJECTIVE-MISSION-PROMPT)¶
Lecture seule, et ne lance rien. Pour prendre un objet de la carte (un pont, un bâtiment de la
carte elle-même) comme objectif d'une zone de combat, il faut son identifiant DCS — le
scenery_targets de la zone n'accepte que ça, et ces identifiants n'existent que dans DCS. L'action
renvoie de quoi proposer à l'utilisateur la commande veaf-tools dcs scenery-objects (elle demande
DCS), qui liste les objets autour de chaque point avec leur identifiant, leur type et leur distance.
Rayon par défaut : 150 m.
{"theatre": "Syria", "points": [{"x": -64230.0, "y": 352140.0, "radius": 100}]}
terrain_elevation (lot FEAT-TERRAIN-ELEVATION)¶
Lecture seule, sans DCS : répond depuis la grille d'altitudes du théâtre, relevée une fois par
veaf-tools dcs terrain-sweep ou téléchargée parmi les grilles publiées au premier usage (données de terrain). Quatre questions, combinables
dans un même appel :
points— l'altitude du sol en chaque point, en mètres et en pieds (l'altitude d'une cible) ;route— le sol le plus haut de chaque branche (le plancher d'une route basse altitude) ;route+observers— combien de mètres de chaque branche chaque radar ou SAM voit au-dessus du relief, dans sa portée, horizon radar de la Terre aux 4/3. Chaque point de la route doit alors porter sonalt: une branche entre deux pointsRADIO(au-dessus du sol) suit le relief, toute autre va en ligne droite entre les deux altitudes ;area— le sol le plus haut par carré MGRS de 10 km (la grille F10) ou par quadrilatère de 30′.
Relief seul — ni bâtiments, ni pylônes, ni arbres — et interpolé entre deux échantillons : chaque
réponse porte le pas de la grille et cette mise en garde. Un point hors de la grille vaut null,
jamais 0. Sans grille pour le théâtre, la réponse est available: false avec la commande qui la
relève.
{"theatre": "Caucasus",
"route": [{"x": -281000, "y": 647000, "alt": 60, "alt_type": "RADIO"},
{"x": -262000, "y": 690000, "alt": 60, "alt_type": "RADIO"}],
"observers": [{"name": "SA-6 Kutaisi", "x": -284500, "y": 683500, "range": 25000}]}
describe_naming_conventions¶
Lecture seule. Les 8 motifs de nommage réservés (appartenance combat zone, préfixes
veafSpawn-/OnDemand-, marqueurs #veafInterpreter[…]/#command=, entrées de déploiement
QRA, noms CAS fixes…) avec, pour chacun, la règle et le module qui la consomme. À vérifier avant
un add_group.
describe_module¶
Lecture seule. Localisateur (pas un validateur de schéma) : vérifie qu'un module VEAF existe
(via la liste canonique lua_module_scanner), renvoie sa page de doc, et — si mission_yaml_path
est fourni — son état activé. Les clés de config de chaque module vivent dans sa page de doc.
Il connaît aussi les scripts communautaires que le build embarque (CTLD, CSAR, AIEN, STTS, TUM, MIST) : kind vaut veaf ou community.
{"module_id": "QRA", "mission_yaml_path": "chemin/vers/mission.yaml"}
Composites — une passe, deux mondes (vague 8)¶
Actions haut niveau qui posent une fonctionnalité complète en un appel, sur un dossier de
mission : elles éditent la source durable (le src/mission/ exploité — zones/groupes — via
mission_folder, et mission.yaml), sans déclencher de build (un veaf-tools mission build ultérieur
produit le .miz). Elles orchestrent les primitives des vagues 1-7 (insert_trigger_zone,
insert_group_into_content, l'éditeur mission.yaml). Implémentation : veaf_mission_mcp/composites.py.
create_combat_zone¶
Zone de déclenchement + groupes placés dedans (noms auto-préfixés par la zone → capturés au
runtime, coalition indifférente) + bloc modules.COMBATZONE.combat_zones[] ajouté au yaml.
Chaque groupe accepte route et patrol, de la même forme qu'add_group : un convoi qui traverse la
zone est un groupe de la zone, pas un appel séparé. En catégorie ship, les navires sont espacés de
600 m (ceux d'un véhicule, 20 m, les faisaient se percuter à l'apparition).
add_combat_operation¶
Écrit dans mission.yaml seulement une entrée combat_zones[] de type operation : tasking_orders (chaque tâche, ses dependencies), friendly_name, briefing, active_at_start.
Le nom d'une opération est une étiquette, pas une zone de déclenchement : rien n'est écrit dans src/mission/.
Refuse une tâche ou une dépendance qui ne nomme pas une zone de combat déclarée (ou qui nomme une autre opération) : le GetZone() généré vaudrait nil au runtime (FIX-DEMO-MISSION-FINDINGS 07).
Activer l'opération fait apparaître toutes ses zones d'un coup ; les dependencies ne décident que du moment où une tâche devient l'objectif en cours.
create_qra¶
Zone + intercepteurs Late Activation (coalition significative) + entrée
modules.QRA.definitions[] référençant les groupes par nom exact (simple_groups). La
coalition est passée en minuscule pour le placement, majuscule dans la définition YAML. Chaque intercepteur a un seul point, sans tâche, à dessein : au décollage, le module QRA donne à un groupe
CAP/Intercept dont la route n'engage aucun aéronef une patrouille sur la zone
(ce que fait un groupe décollé).
Sa description demande des paliers par nombre d'intrus jusqu'à la taille attendue de l'escadrille — jamais une seule paire fixe face à cinq joueurs ou plus — et nomme scale_with_opposition et rearm_while_occupied (niveau d'opposition).
scale_with_opposition: true est écrit par défaut (le qra de l'appelant peut le mettre à false), et le bloc racine opposition: {level: 0} est ajouté quand la mission n'en a pas : le menu Opposition existe, et la mission démarre sans changement.
Un intercepteur décolle de la piste par défaut : celle de airfield, sinon l'aérodrome de sa coalition le plus proche de la zone ; start: air le met en l'air à position, sur demande seulement. Sans aérodrome de sa coalition, ou sans données de parking pour le théâtre, le défaut retombe en l'air avec un avertissement ; un start ou un airfield demandés et impossibles sont refusés.
create_cap_mission¶
Groupe template Late Activation nommé OnDemand-<nom> + entrée cap_missions[]
(group_name: <nom>, sans préfixe — le build résout vers le groupe OnDemand-). Le premier point
porte la tâche EngageTargets (cibles Air) que l'éditeur ajoute de lui-même à une tâche CAP, numérotée
avant l'orbite : sans elle, le vol patrouille et n'engage jamais.
menu_name est le nom que montre le menu F10 MISSIONS (le nom de la mission s'il est omis : un nom vide donnait une entrée blanche dont les « Infos » plantaient), briefing ce qu'en disent les Infos.
Une clé de cap que le build ne lit pas (friendly_name, le mot des zones de combat) est signalée dans warnings, et validate_mission la signale aussi.
Scaffolding d'un dossier de mission (vague 9)¶
Toutes les actions ci-dessus supposent qu'un dossier de mission existe déjà. La vague 9 fournit l'amont : créer ce dossier depuis un dossier vide, en pilotant les vrais binaires VEAF comme le ferait un Mission Maker à sa première installation.
scaffold_mission¶
Écriture. Sur un dossier cible vide :
- Résout l'asset updater de l'OS courant (
veaf-tools-updater.exesous Windows,veaf-tools-updater-<os>-<arch>sous Unix) et le télécharge depuis l'URL de release stable (…/releases/download/<tag>/<asset>— pas d'API GitHub, donc pas de rate-limit). - Lance l'updater dans le dossier (il télécharge et installe les outils VEAF +
published/). - Lance
veaf-tools mission prepare --template <tier> --forcedans le dossier.
{
"target_folder": "chemin/vers/dossier-vide",
"template": "standard",
"github_token": "…",
"tag": "published-latest"
}
- Refuse un dossier non vide — le scaffolding n'initialise qu'un dossier vide.
template—minimal/standard/full. Le tier interactifcustomn'est pas supporté ici (son sélecteur TUI n'a pas de TTY sous un sous-processus) ; c'est au LLM appelant de poser la question du template au Mission Maker et de le passer en paramètre.theatre— optionnel ; relayé àprepare --theatrepour déposer une mission vierge synthétique de cette carte DCS danssrc/mission/(sans passer par DCS). Omis →src/mission/reste vide (le maker fournit son propre.miz).github_token— optionnel, relayé à l'updater (--token) pour contourner la limite de débit de l'API.- Un code retour non nul de l'updater ou de
prepare, ou l'absence deveaf-tools/published/après l'updater, remonte comme une erreur explicite.
C'est l'étape 0 d'une mission créée de zéro, avant les composites de la vague 8.
Carte & coordonnées (vague 10)¶
Les actions de placement prennent des coordonnées locales DCS (x/y, en mètres dans la
projection propre au théâtre) ; un Mission Maker raisonne plutôt en lat/long sur une carte. La vague
10 donne au LLM de quoi se repérer et convertir, en design-time (sans DCS lancé).
Socle : veaf_libs.coordinates — Transverse Mercator WGS84 pur-Python (pas de pyproj), dont les
constantes par théâtre viennent de la donnée vendorisée data/dcs-maps.yaml (export MIT de
VEAF/dcs-maps, voir ADR 0015)
— tous les théâtres DCS (Caucasus, Syria, PersianGulf, Marianas, Normandy, Nevada, SinaiMap,
GermanyCW, Kola, TheChannel, Falklands, Afghanistan, Iraq). Comme les cartes DCS sont le monde
réel projeté, ce socle relie x/y DCS ↔ lat/lon réel.
describe_map¶
Lecture seule. Depuis un .miz ou un dossier de mission : renvoie le théâtre, les
bullseyes par coalition, et les zones/groupes existants comme points de repère — pour que le
LLM s'oriente sans DCS.
Chaque zone porte son x/y/radius, chaque groupe son x/y et son nombre d'unités (units).
{"mission_path": "chemin/vers/mission.miz-ou-dossier"}
list_airfields¶
Lecture seule. Liste les bases d'un théâtre — nom, id d'aérodrome DCS, et x/y DCS en mètres entiers quand
la projection du théâtre est connue (lat/lon sinon), plus la coalition de chaque base avec une mission — depuis la donnée livrée avec les outils
(veaf_libs/data/airdrome-positions.yaml, générée avec airdromes.yaml depuis la base de référence dcs-world-schema par
veaf-build update-dcs-data --airdromes). La position est le point de référence de l'aérodrome, au centre des pistes. Avec mission_path, le théâtre de la mission ; sans
mission, theatre.
name_contains garde les bases dont le nom contient un texte ; details: true ajoute lat/lon, parking (les places utilisables par des planes et par des helicopters) et atc (uhf, vhf, fm en MHz et tacan, tels que DCS les donne).
Court par défaut : tous les champs pour Syria faisaient 37 000 caractères, 21 000 maintenant (FIX-OPENCODE-TEST-FINDINGS).
{"theatre": "Syria", "name_contains": "incirlik", "details": true}
describe_airfield_channels¶
Lecture seule. Liste, pour un dossier de mission, les aérodromes qu'elle utilise — camp (depuis
warehouses), slots dynamiques une fois src/warehouses.yaml appliqué, slots posés au parking — avec
les fréquences ATC et le TACAN que DCS leur donne (veaf_libs/data/airfield-frequencies.yaml), et
l'alias que chacun a déjà dans la collection bases. Classés : tenus avec slots, tenus sans, puis
(include_neutral) neutres. C'est la proposition à soumettre à l'auteur de la mission avant
set_airfield_channels : une radio DCS tient une vingtaine de canaux. Même code que
veaf-tools content airfield-channels.
{"folder_path": "…", "include_neutral": false}
set_airfield_channels¶
Écrit les aérodromes choisis dans la collection de canaux bases du src/presets.yaml du dossier,
avec les fréquences de DCS ; refuse un aérodrome que DCS ne déclare pas. Un aérodrome déjà dans
bases garde son alias et l'orthographe de son titre (Büchel pour le Buchel de DCS), un nouveau prend l'alias Base-<nom DCS>, et une entrée qui ne correspond à aucun
aérodrome choisi (un FARP, un navire) est laissée telle quelle et signalée dans untouched. Seule
bases change ; not_on_a_radio liste les canaux écrits qu'aucune entrée de channel_lists
n'utilise encore. Idempotent.
{"folder_path": "…", "airfields": ["Batumi", "Kutaisi", 31]}
resolve_coordinates¶
Utilitaire. Convertit une position entre {x, y} (local DCS) et {lat, lon} (degrés décimaux) pour
le théâtre de la mission (lu depuis la mission — l'appelant ne fournit jamais de paramètres de
projection), ou pour un theatre nommé avant qu'une mission existe.
{"mission_path": "…", "position": {"lat": 42.18, "lon": 41.68}}
Pour convertir plusieurs points en un appel, positions (une liste) à la place de position : la
réponse est {theatre, points}, dans l'ordre donné, et une position incomplète est nommée par son
index.
geocode¶
Lecture seule (lot FEAT-GEO-PLACEMENT). Résout un nom de lieu réel en coordonnées DCS pour le
théâtre de la mission — les cartes DCS étant le monde réel projeté. Géocodeur enfichable :
OpenStreetMap Nominatim par défaut (gratuit, sans clé ; attribution © OpenStreetMap requise), Google
Maps si GOOGLE_MAPS_API_KEY est défini. bearing+distance_km optionnels (« à 10 km au nord de
X »). Renvoie {found, display_name, latlon, xy, in_theatre_bounds, warnings} — approximatif, à
confirmer visuellement ; les lieux nommés marchent, le terrain vague non. Sans mission, theatre nomme la carte.
{"mission_path": "…", "query": "Kobuleti", "bearing": 0, "distance_km": 10}
Build & validation (vague 11)¶
Les actions précédentes créent, orientent et éditent un dossier de mission, mais rien ne
produisait le .miz jouable — le maker lançait veaf-tools mission build à la main. La vague 11 rend le
serveur autonome de bout en bout : dossier vide → scaffold → blank théâtre → composites/placement
→ validation → build → .miz jouable, sans quitter l'assistant.
validate_mission¶
Lecture seule. Lint d'un dossier avant build : réutilise veaf_libs.mission_validator en
process. Renvoie {ok, errors[], warnings[]} (ok = false dès qu'une erreur). À lancer avant
build_mission.
{"folder_path": "chemin/vers/dossier-mission"}
inspect_built_mission (lot FIX-OPENCODE-TEST-FINDINGS)¶
Lecture seule. Ce que contient une mission construite, pour le contrôle d'après build, de façon compacte — une ligne par élément, jamais une table brute :
dynamic_slot_templates (les types de templates de chaque camp, une fois), airbases et farps_and_ships (camp, slots dynamiques, démarrage à chaud, les templates liés — « every blue template linked (61) » plutôt que la liste à nouveau — et combien de types sont en stock sans template), presets (par camp et disposition des radios, les types qui la portent, ou no presets), bullseye, weather (heure, ciel, vent au sol et visibilité de la mission et de chaque variante de missions/), support (ravitailleurs et AWACS, fréquence, TACAN, activation différée) et counters (presets_injected, routes_on_player_groups, warehouse_links, dead_links, variants), pour qu'un zéro se voie.
Une section que la mission n'a pas vaut absent. mission_path est le dossier (son dernier build) ou un .miz.
Le guide demandait d'« ouvrir le .miz construit et vérifier » : avec ses seules actions, une IA le dézippait et parsait ses tables Lua en PowerShell.
La mission par défaut livrée, construite, se lit en environ 5 000 caractères.
Implémentation : veaf_mission_mcp/inspect_built.py.
{"mission_path": "chemin/vers/dossier-mission"}
build_mission¶
Écriture. Construit le dossier en .miz jouable en pilotant veaf-tools mission build dans le dossier
(le binaire installé par scaffold_mission, ou veaf-tools du PATH). L'orchestration du build vit
dans la commande CLI, on la réexécute telle quelle. Un échec de build est remonté (RuntimeError).
profile (optionnel) est passé en --profile — LOCAL_TEST pour un build de test local.
{"folder_path": "chemin/vers/dossier-mission", "profile": "LOCAL_TEST"}
Le résultat liste dans missions les .miz écrits par le build : celui de la soirée, et ses versions d'entraînement quand mission.yaml porte un bloc training:.
propose_training_plan¶
Lecture. Brouillon du bloc training: d'un dossier de mission (versions d'entraînement par rôle) : vols IA ennemis à tâche air-air → enemy_cap, cap_missions → enemy_cap, définitions QRA du camp ennemi → qra, groupes portant un type que DCS classe LR SAM → sam_lr, combat zones → a2g_target, autres défenses aériennes dans une combat zone ou à moins de 15 km → sam_threat.
Tout est proposé tel que placé, chaque élément avec sa raison : le tri entre précis et vague se fait en lisant le briefing, pas dans l'action.
Le camp ennemi est l'opposé de celui qui porte le plus de slots joueurs (bleu par défaut).
{"mission_path": "chemin/vers/dossier-mission"}
set_training_plan¶
Écriture. Écrit le bloc training: de mission.yaml (commentaires préservés, sauvegarde avant), après les contrôles du build (mission_builder.training_variants.check_training) : refusé avec la liste de tous les problèmes, sinon le build suivant produit aussi _TRAINING_A2G, _CAP, _SEAD et _ALL.
{"mission_path": "chemin/vers/dossier-mission", "training": {"elements": [{"groups": ["SA-10 *"], "kind": "sam_lr"}], "randomize": []}}
Prochaines vagues (hors périmètre)¶
- Un éditeur de triggers SI/ALORS générique (conditions/actions DCS arbitraires) — la vague 2 se limite aux triggers de démarrage chargement-de-script / exécution-Lua.
- Un validateur de schéma par module pour
set_mission_module(la vague 4 reste générique). - Composites CAS (pur runtime, pas d'écriture) et génération end-to-end depuis un prompt (l'objectif NL-MISSION-GEN au-delà de ce lot).
Voir .backlog/archive/FEAT-MCP-MISSION-EDITOR.md pour le détail.