mission.yaml Reference¶
mission.yaml is the optional build-time configuration file for veaf-tools. Place it at the root of your mission folder, next to veaf-tools-updater.exe. If absent, veaf-tools mission build works with default settings.
This page covers the top-level sections of mission.yaml. Configuration for individual Lua modules is documented in each module's own page (see the index by module below).
Understanding the YAML file landscape¶
A VEAF mission folder uses two distinct categories of YAML files. Understanding the difference helps you know which file to edit for a given task.
Category A — Build pipeline files¶
These files drive the design-time injection steps that veaf-tools mission build performs before writing the final .miz. Each step reads its own YAML file and injects data into the mission. They are listed under the pipeline: section of mission.yaml.
File (in src/) |
Pipeline step | What it does |
|---|---|---|
waypoints.yaml |
waypoints |
Injects named waypoints into the mission |
presets.yaml |
presets |
Configures radio presets for each aircraft group |
spawnables.yaml |
spawnable_aircrafts |
Spawnable aircraft groups (veafSpawn- prefix) |
dynamic-slot-templates.yaml |
dynamic_slot_templates |
Dynamic-slot templates (dynSpawnTemplate=true) |
warehouses.yaml |
warehouses |
Dynamic-Slot warehouses: dynamicSpawn, stock, fuel, template links |
spawn-groups.yaml |
spawn_data |
Spawn database for _spawn unit / _spawn group — optional: the step always runs, the framework data being embedded, and this file only adds to it |
versions.yaml |
weather |
Generates one .miz variant per weather preset |
These files are not loaded at DCS runtime — they are consumed by veaf-tools mission build and then compiled into the .miz.
Category B — Runtime module configuration (this file)¶
mission.yaml itself configures how VEAF Lua modules behave at DCS runtime. It is translated at build time into veaf-config.lua, which is injected into the mission and executed when DCS loads the mission.
The modules: section describes runtime module behaviour (QRA, assets or shortcuts configuration lives under the relevant module, e.g. modules.QRA).
mission folder/
├── mission.yaml ← runtime module config (THIS FILE)
│ └── pipeline:
│ ├── waypoints: true ──► src/waypoints.yaml (build-time injection)
│ ├── presets: true ──► src/presets.yaml (build-time injection)
│ ├── spawnable_aircrafts: true ──► src/spawnables.yaml
│ ├── dynamic_slot_templates: true ──► src/dynamic-slot-templates.yaml
│ ├── warehouses: true ──► src/warehouses.yaml
│ ├── spawn_data: true ──► src/spawn-groups.yaml (optional)
│ └── weather: true ──► src/versions.yaml
└── src/
├── waypoints.yaml
├── presets.yaml
├── spawnables.yaml
├── dynamic-slot-templates.yaml
├── warehouses.yaml
├── spawn-groups.yaml
└── versions.yaml
Minimal working example¶
# mission.yaml — minimum viable configuration
global_log_level: debug # remove before deploying to players
mission:
name: "My-Mission"
security:
disabled: true
modules:
RADIO:
enabled: true
ASSETS:
enabled: true
assets:
- sort: 1
name: "Texaco"
description: "Texaco (KC-135)"
information: 'Tacan 51Y\nU251.00 (21)'
Syntax errors¶
If mission.yaml contains a YAML syntax error (wrong indentation, a missing colon, a tab character…), veaf-tools mission build stops immediately and displays a clear message indicating the file name, the line and column of the problem, and a plain-language hint on how to fix it:
Syntax error in mission.yaml, line 81, column 4.
The error starts near line 34, column 3.
→ Check the indentation around these lines. YAML uses spaces only (never tabs).
All items in the same block must be aligned at the same column.
Common causes:
| Symptom | Cause | Fix |
|---|---|---|
indentation hint |
A block is indented differently from the rest | Align the key with its siblings |
tab hint |
A tab character was used instead of spaces | Replace all tabs with spaces in your editor |
colon hint |
A key is missing its : separator |
Write key: value, not key value |
Tip
Most text editors can visualise whitespace characters — enable this option to quickly spot tab/space mix-ups.
Top-level sections¶
global_log_level¶
Forces a log level on all VEAF modules. Remove before deploying to players.
global_log_level: debug # error | warning | info | debug | trace
| Value | Description |
|---|---|
error |
Errors only |
warning |
Errors and warnings |
info |
Standard operational messages (production default) |
debug |
Detailed module activity — use during development |
trace |
Extremely verbose — use when tracking specific issues |
mission:¶
Mission identity (name, export path, era) and mission-wide options (e.g. silence_atc_on_all_airbases) — used in radio menus, log messages, and export paths. This block holds mission-level settings, not just identity.
mission:
name: "My-Mission" # shown in radio menus and log messages
export_path: null # null = default DCS Saved Games path
era: MODERN # MODERN | COLD_WAR | WW2
language: fr # in-game VEAF message language: fr | en (default: the tools' language)
silence_atc_on_all_airbases: false # mission-wide option: mute DCS ATC at every airbase
third_party_mods: [] # third-party DCS mods to make non-blocking (see below)
ground_control: # game master, JTAC… slots and vehicle control (see below)
game_master: 5
forced_options: # options enforced on every player (see below)
labels: none
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
name |
string | — | No | Mission name shown in menus and logs, and the name of the built .miz — see the naming note below |
export_path |
string | null | null |
No | Override DCS Saved Games export path |
era |
string | inferred | No | MODERN | COLD_WAR | WW2 — affects available spawn groups. When absent it is inferred at every build from the base mission's content: a WW2 unit type or a year ≤ 1945 gives WW2, a year ≤ 1991 COLD_WAR, otherwise MODERN (era_detector.py). The inferred value is not written back into your mission.yaml — it is recomputed; set the key to pin it. |
silence_atc_on_all_airbases |
boolean | false |
No | Mission-wide option: mute DCS ATC at every airbase (emits veaf.silenceAtcOnAllAirbases()). convert-v5 migrates it from an active call and annotates its provenance |
language |
string | tools' language | No | Language of in-game VEAF messages (fr | en); emitted into veaf-config.lua as veaf.config.language and read by veaf.t(). When omitted, the build uses the tools' language (--lang > VEAF_LANG > user config > OS locale > en) |
third_party_mods |
list of strings | [] |
No | Third-party DCS mods (paid/community aircraft) to make non-blocking: their ids are removed from the .miz's requiredModules table at build, so a pilot who does not own the mod can still load the mission (that slot is simply unavailable). The list is unioned with a VEAF default list covering the common mods (Hercules, UH-60L, A-4E-C, T-45, AM2, SU-30/FlankerEx, Bronco-OV-10A) — only declare mods not already handled. Not to be confused with VEAF Modules (the modules: block, which are capabilities, not DCS add-ons) |
ground_control |
mapping | absent | No | Slots of the roles that fly no aircraft, and vehicle control — see below |
forced_options |
mapping | absent | No | Options enforced on every player (labels, F10 view…) — see below |
The .miz file name is an interface — _ICAO_<code> and real weather¶
mission.name becomes the built file name: <name>_<YYYYMMDD>.miz, plus a _<VARIANT> suffix
when build_variants: is used. Give a name ending in .miz instead and the
name is taken verbatim, with no date.
That matters because server-side tooling reads the file name. On the VEAF servers, the
RealWeather extension of DCSServerBot
looks for _ICAO_<code> in the mission's file name and fetches that airfield's live METAR at
mission start — so the weather follows reality without rebuilding. Name the mission accordingly:
mission:
name: VEAF_Foothold_Caucasus_ICAO_URSS # -> VEAF_Foothold_Caucasus_ICAO_URSS_20260728.miz
Two rules when picking the code:
- it must be an airfield on that theatre (any one; a large one is better);
- it must have a live METAR station. Check before trusting it — a station can exist and be stale, which is worse than no real weather at all, since the mission then advertises a "real" weather that is days old. A one-line check:
curl -s https://tgftp.nws.noaa.gov/data/observations/metar/stations/URSS.TXT
The first two digits of the DDHHMMZ group are the day of observation — compare them with today.
When a whole theatre is degraded, there are two defensible answers, and the choice belongs to
whoever runs the server. Measured on Afghanistan, every station lags (Kabul a month, Herat
sixteen days, Bagram a day; six airfields have no station at all): either omit the marker, so
RealWeather leaves the mission alone and the authored weather stands, or take the least bad
and know what you are getting. The VEAF Foothold Afghanistan uses OAIX (Bagram, about a day
behind) — a deliberate choice, not an oversight.
ground_control: game master, JTAC, observer slots, and vehicle control¶
DCS keeps in the mission how many players may take each role that does not fly, on each side, and whether a pilot may drive ground vehicles. A v6 mission is born with none of these slots: no game master until they are opened. The block sets them at every build:
mission:
ground_control:
game_master: 5 # "Game Master" (instructor in the mission)
tactical_commander: 5 # "Tactical Commander" (artillery_commander)
jtac: {blue: 5, red: 0} # "JTAC/Operator" (forward_observer), per side
observer: 5 # "Observer"
pilot_control_vehicles: true # a pilot may take control of ground vehicles
- A number applies to both sides;
{blue: n, red: m}sets them apart. - A role, a side or
pilot_control_vehiclesthe block does not name keeps the mission's value. - Block absent: the build changes nothing, a mission set up in the editor keeps its slots.
- An unknown role or side, a negative number:
validatereports it and the build stops. - Role passwords stay out of reach: written in
mission.yaml, they would be committed in clear.
The VEAF Open Training missions open 5 slots per role and per side, with vehicle control, as in v5.
forced_options: the options enforced on every player¶
The editor's Mission options panel enforces some options on every player, whatever their own settings: labels, F10 map view, external views, wake turbulence…
The block sets them at every build, in the mission's forcedOptions table:
mission:
forced_options:
labels: none # full | abbreviated | symbol_only | dot_neutral | none
f10_view: fog_of_war # all | fog_of_war | allies_only | map_only | my_aircraft
external_views: false
f10_marks: true # players' F10 marks (the VEAF marker commands need them)
wake_turbulence: true
civil_traffic: off # off | low | medium | high
| Name | Editor label | Values |
|---|---|---|
labels |
Labels | full, abbreviated, symbol_only, dot_neutral, none |
spotting_dot |
Spotting dot | off, auto, one_pixel, two_pixels |
f10_view |
F10 View Options | all, fog_of_war, allies_only, map_only, my_aircraft |
f10_view_by_role |
F10 View Options → Extended | see below |
f10_marks |
F10 Map User Marks | true / false |
external_views |
External views | true / false |
padlock |
Padlock | true / false |
permit_crash |
Permit crash recovery | true / false |
easy_flight |
Easy flight | true / false |
easy_communication |
Easy communication | true / false |
radio_assist |
Radio assist | true / false |
mini_hud |
Mini HUD | true / false |
cockpit_visual_recon_mode |
Cockpit visual recon mode | true / false |
cockpit_status_bar |
Cockpit Status Bar | true / false |
battle_damage_assessment |
Battle Damage Assessment | true / false |
unlimited_fuel |
Unlimited fuel | true / false |
unlimited_weapons |
Unlimited weapons | true / false |
immortal |
Immortal | true / false |
g_effects |
G-effect | none, reduced, realistic |
g_warm_up |
G warm up in air | true / false |
wake_turbulence |
Wake turbulence | true / false |
unrestricted_satnav |
Unrestricted SATNAV | true / false |
birds |
Birds | a number from 0 to 1000 |
civil_traffic |
Civ traffic | off, low, medium, high |
random_failures |
Random System Failures | true / false |
- An option the block does not name keeps the mission's value; the block never removes one. To stop enforcing an option, untick it in the editor.
- Block absent: the build changes nothing, a mission set up in the editor keeps its options.
- An unknown option or value:
validatereports it and the build stops. - DCS does not apply enforced options to a player who ticked the global preset of the GAMEPLAY tab of their options (the editor panel's note).
- In a campaign, put the block in the
mission.yamlof thetemplate/folder:campaign nextcopies it into every mission.
The F10 view per role. f10_view_by_role replaces f10_view (both together are refused) and sets each role's map:
mission:
forced_options:
f10_view_by_role:
pilot: {view: fog_of_war, hide: [enemies_weapons]}
jtac: all
spectator: all
- Roles:
pilot,jtac,tactical_commander,observer,spectator,airboss; a role not named sees everything. - A view alone, or
{view: …, hide: […]}with the layers to hide:<side>_<category>, sideallies,enemiesorneutral, categoryplanes,helicopters,ground,navalorweapons. - What DCS does with it in game is not seen yet: the shape comes from the editor's code, no saved mission used it.
security:¶
Controls the VEAF security system. Security is active by default (veaf.SecurityDisabled = false in veaf.lua): with no security: block nothing is emitted and the sensitive commands require a pilot level or a password. disabled: true turns it off for the whole mission.
security:
disabled: false # false = a password is required
password_hashes: # SHA-1 hashes granting player/JTF access
- "2a4efd2397e081bcacb82b3e447c584c65cc83ee"
password_mm_hashes: # SHA-1 hashes granting Mission Master access
- "99685b3c7cb1fb08a829fc97d4a8564fc5f9435a"
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
disabled |
boolean | false |
No | true = no password required |
password_hashes |
string[] | [] |
No | SHA-1 hashes granting player access. Emitted at levels L1 and L9, so the password opens marker authentication and the sensitive spawns, not only the L9 gates |
password_mm_hashes |
string[] | [] |
No | SHA-1 hashes granting Mission Master access (its own table, no level cascade) |
SHA-1, not SHA-256.
veafSecurity._checkPasswordhashes what the player types withsha1.hex(password)and looks it up in the table, so a SHA-256 hash never matches and the password silently never works.To generate one:
echo -n "yourpassword" | sha1sum(Linux/macOS), orpython -c "import hashlib,sys; print(hashlib.sha1(sys.argv[1].encode()).hexdigest())" yourpassword.
settings:¶
Arbitrary key-value pairs injected into the mission as veaf.config.KEY = value. Use this to pass mission-specific constants to Lua scripts.
settings:
MY_MISSION_FLAG: 42
ENABLE_CONVOY: true
MISSION_PHASE: "alpha"
Each key becomes veaf.config.MY_MISSION_FLAG = 42 in the generated veaf-config.lua.
module_settings:¶
Scalar settings written straight onto a VEAF module's table in the generated veaf-config.lua. Where settings: can only write veaf.config.KEY, this section targets any VEAF table.
module_settings:
veafSkynet.DelayForStartup: 150 # IADS start-up delay
veafSkynet.DynamicSpawn: true
veafRadio.RadioMenuName: "BFR" # root radio menu name, seen by players
veaf.DEFAULT_GROUND_SPEED_KPH: 25
Each entry becomes the matching Lua assignment, here veafSkynet.DelayForStartup = 150.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| (key) | string | — | — | The full Lua target, veafXxx.Field. A key that does not start with veaf is refused at generation time: this section is a migration path for VEAF settings, not a hatch for writing anywhere in the runtime |
| (value) | boolean | number | string | — | — | A scalar. Tables and functions cannot be expressed here |
A documented field beats a migration leftover. When a setting now has its own field in
mission.yamland amodule_settings:entry targets the same Lua variable, the documented field is what applies: themodule_settings:entry is dropped, and the build says so, naming both. This section is a migration path, not a permanent override — delete the line that is no longer needed.One setting is in that position today:
veaf.HideNamesFromSpawnedGroups, superseded bymission.hide_names_from_spawned_groups.Where it comes from.
convert-v5fills this section automatically: under v5, half of these settings reached neithermission.yamlnor the generated Lua, and nothing reported it. Whatever the conversion still cannot carry — a table, a function — is now listed as comments in the generatedmission-script.lua, under "Settings NOT migrated".
Third-party modules: SKYNET / CTLD / CSAR (under modules:)¶
v6 change (hard break): the
external_modules:andqra:sections no longer exist. All of their configuration now lives under themodules:block, the single source of truth. See ADR 0001.
Skynet IADS, CTLD and CSAR are configured like any other module, directly under modules::
modules:
SKYNET:
enabled: true
include_red_in_radio: false # add RED IADS status to F10 menu
debug_red: false # verbose Skynet debug for RED
include_blue_in_radio: false # add BLUE IADS status to F10 menu
debug_blue: false # verbose Skynet debug for BLUE
CTLD: true # configured in ctld-config.yaml, not here
CSAR:
enabled: true
settings: # csar.xxx = value pairs
enableAllslots: true
modules.SKYNET fields¶
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable Skynet IADS integration |
include_red_in_radio |
boolean | false |
Add RED IADS status to F10 radio menu |
debug_red |
boolean | false |
Enable verbose Skynet debug for RED coalition |
include_blue_in_radio |
boolean | false |
Add BLUE IADS status to F10 radio menu |
debug_blue |
boolean | false |
Enable verbose Skynet debug for BLUE coalition |
spotter_network |
boolean | false |
Spotter network: ground units see aircraft and relay the contact |
spotter_radio_range_km |
number | 20 |
How far one unit can relay, in kilometres |
spotter_propagation_speed_kmh |
number | 3600 |
How fast an alert crosses the network, in km/h (hop period = range / speed) |
spotter_view |
"off" | "on" | "radio" |
"off" |
F10 map view of the spotter network; "radio" puts the switch in the F10 menu. Quote it: YAML reads a bare on/off as a boolean |
modules.CSAR fields¶
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable CSAR integration |
settings |
mapping | — | csar.xxx pairs (e.g. enableAllslots: true) |
VEAF generates the csar.xxx = value assignments and the csar.initialize() call in veaf-config.lua. On convert-v5, these settings are extracted automatically from missionConfig.lua. For complex settings such as aircraftType (a per-aircraft table), continue using the Lua callback pattern in mission-script.lua.
modules.CTLD: a switch, a few VEAF keys, no settings:¶
CTLD 2 is configured outside mission.yaml, in a ctld-config.yaml file sitting next to it and edited with ctld-tools.exe. A settings: block under CTLD is rejected by validate: it was no longer read, and letting it pass in silence is exactly the defect this change removes. Besides the switch, mission.yaml only carries keys read by VEAF at runtime, never by the CTLD engine:
manage_logistics(boolean, defaulttrue) — with it on, the build adds the carriers and FARP ammo dumps VEAF has always recognised to thelogisticUnitTypes/troopZoneShipTypeslists in yourctld-config.yaml; it adds, it does not replace.manage_airbase_logistics(boolean, defaulttrue) — VEAF registers every airfield on the map as a CTLD logistic zone and keeps it up to date (two classes, two minutes of ground occupation for a neutral or captured field). Atfalse, nothing is registered and the log says the feature was explicitly switched off.airbase_logistics_radius(number greater than 0, default250) — radius, in metres, of the logistic zone and of the green circle drawn around each airfield. One value for every field: size it for the largest airfield the transports start from (1,100 m for a major air base such as Ramstein, measured 2026-10-03).airbase_occupation_radius(number greater than 0, default2000) — radius, in metres, probed around the airfield for ground occupation.airbase_logistics_tick(number greater than 0, default30) — re-evaluation period, in seconds.
modules:
CTLD:
enabled: true
manage_logistics: true
manage_airbase_logistics: true # default
# airbase_logistics_radius: 250
# airbase_occupation_radius: 2000
# airbase_logistics_tick: 30
See CTLD and CSAR Integration — including where to get ctld-tools, which does not ship with VEAF MCT, FARPs placed in the editor, and airfields become logistic zones.
Sounds. CTLD and CSAR play sounds by filename at runtime (
beacon.ogg,beaconsilent.ogg,CSAR.ogg). When CTLD or CSAR is enabled, the build automatically injects the required sounds it ships (src/scripts/community/sounds/) into the mission'sl10n/DEFAULT/, without overwriting any sound your mission already provides. A required sound shipped by neither the tools nor your mission is reported with a build warning — add it tosrc/mission/l10n/DEFAULT/(e.g.radiobeep.ogg, the JTAC fallback beep, is not redistributed).The build then declares every sound the mission carries in
mapResource, through a "Declare mission sounds" trigger that plays it to a country no coalition uses — so nothing is audible. Without that declaration the DCS Mission Editor treats these files as orphans (it cannot know a script plays them by name) and deletes them the moment you save the mission in the editor, with no message at all.
veaf_tools:¶
Version compatibility constraint for veaf-tools-updater.exe. The updater skips releases that don't match.
veaf_tools:
version: "6" # accept any 6.x.x
| Format | Example | Meaning |
|---|---|---|
| Major only | "6" |
Any 6.x.x |
| Major.Minor | "6.1" |
Any 6.1.x |
| Exact | "6.1.3" |
Exactly 6.1.3 |
| Compatible | "^6.1.3" |
>=6.1.3, <7.0.0 |
| Approximate | "~6.1.3" |
>=6.1.3, <6.2.0 |
modules:¶
The unified modules: block enables, disables, or configures every VEAF Lua module and every community script in a single place. Modules not listed are enabled with their default settings.
Migration note.
modules:replaces the olderlua_modules:+community_scripts:keys. The legacy keys still work but emit a deprecation warning at build time.enabled:replaces the oldenable:key.
Each entry can take three forms:
modules:
# 1. Shorthand — just enable an optional module with default settings:
RADIO: true
# 2. Block — enable and configure:
SPAWN:
enabled: true
logLevel: debug # optional per-module log level override
init:
help_menus: true
# 3. Bare null — mandatory infrastructure module, configure only:
UNITS:
logLevel: debug
Infrastructure modules are always active.
UNITS,TIME,CACHE,EVENTS,MARKERS, andCOMMANDSare mandatory and always loaded. Do not setenabled:on them — it is a build error. They may still appear inmodules:to configure other fields such aslogLevel.
Common fields (all modules):
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable or disable this module (optional modules only — not allowed on Infrastructure modules) |
logLevel |
string | (global) | Override log level for this module only |
initialized_by_mission |
boolean | false |
true: the module's script is loaded and its configuration applied, but the build does not call its initialize() — your own scripts do. For a mission that starts a module itself (otherwise it is initialized twice); also accepted under SKYNET, CTLD and CSAR, for which false would drop the script. See Porting a mission with its own script layer |
Additional init: or data fields are module-specific — see each module's documentation page.
RADIO init: fields:
| Field | Type | Default | Description |
|---|---|---|---|
help_menus |
boolean | false |
true shows the built-in "Help" entries of the radio menus; negated into veafRadio.initialize's skipHelpMenus |
create_menus |
boolean | true |
false builds no VEAF F10 menu at all (dontCreateMenus). Combined with security:, that is how a public mission keeps the VEAF commands reachable only through password-protected map markers. Omit the key to keep today's behaviour |
modules:
RADIO:
enabled: true
init:
create_menus: false # no VEAF radio menu; commands via markers only
CARRIER init: fields:
| Field | Type | Default | Description |
|---|---|---|---|
include_carrier_operations_radio |
boolean | true |
false: no carrier menu in F10 |
operations_scheduler |
boolean | true |
false: no VEAF operations loop (every 60 s), for a mission that drives its carriers — see veafCarrierOperations |
RADIO diagnostic option (outside init:): menu_stats: true writes to dcs.log, on every refresh, the F10 menu's size and what changed. Turn it on temporarily in a real mission to watch the menu in multiplayer — see Watching the F10 menu.
Community scripts are listed in the same block, using their IDs (case does not matter: CTLD: and ctld: are equivalent). When a script is absent from modules:, it keeps its default state — included for the opt-out scripts, excluded for the two opt-in ones (MIST and TUM, see below). Set a script to false to exclude it:
Because that default is easy to misread, generated files (prepare --template, convert-v5) and the shipped mission.yaml always write the five opt-out scripts (STTS, CTLD, AIEN, CSAR, SKYNET) out, at true or false: the state of a community script is never inferred from silence.
modules:
CTLD: true
CSAR: true
SKYNET: false # excluded from this mission
| Community ID | Script |
|---|---|
MIST |
MIST (Mission Scripting Tools) — opt-in, and auto-enabled when one of your own scripts calls mist. |
STTS |
DCS-SimpleTextToSpeech |
CTLD |
CTLD (Combat Transport & Logistics Dispatcher) |
AIEN |
AIEN (AI Enhancement) |
CSAR |
CSAR (Combat Search and Rescue) |
SKYNET |
Skynet IADS |
TUM |
The Universal Mission (TUM) |
An unknown identifier in
modules:is a blocking error: the build stops with a message naming the offending key.
MIST— opt-in, and injected only when something needs it. MiST used to be injected into every mission, because the VEAF scripts called it everywhere. They no longer call it at all, so it is now off by default and the 336 KB it weighs stay out of your.miz. Before packaging, the build scans your ownsrc/scripts/*.lua; if one of them callsmist., MiST is injected and the build log names the file that asked for it. AMIST: falsedoes not win against that scan — a script that calls MiST gets MiST, otherwise it would break in flight. SetMIST: trueonly when something reaches MiST indirectly, which the scan cannot see (through_G["mist"], or from a script loaded by another script). A bareMIST:line inherited from an oldermission.yamlnow reads as "disabled", which is the right answer for almost every mission. See MiST: injected only when you need it.
TUM(The Universal Mission) — mission prerequisite. TUM is a self-contained PvE mission generator (third-party community script) that takes over the whole map at start-up: it makes every airbase neutral, then assigns zones and airfields to the coalitions based on the trigger zones defined in the mission editor. If you enableTUM: trueon a mission that was not authored for TUM, the script aborts at start-up with an error such as:
Coalition red has no territory zones and/or controls no airfields. Please add zone with a name starting with REDFOR…This is not a VEAF bug: it is a TUM design prerequisite. To use it, create in the mission editor a trigger zone whose name starts with
BLUFORand another starting withREDFOR, each containing at least one airbase, plus at least one other mission zone. Only enableTUMfor a TUM-style mission.Opt-in (like
MIST, unlike the other community scripts). TUM is off by default: a vanilla mission, a freshly v5-converted mission, or amodules:block that does not mentionTUMall leave it disabled. Only an explicitTUM: trueturns it on.MISTis opt-in too, for a different reason (see the note above); the remaining community scripts are opt-out (active unless you set them tofalse). WhenTUM: true, the build automatically callsTUM.initialize()at start-up — you do not need to add anything tomission-script.lua.
VEAF module IDs:
| ID | Module | Doc page |
|---|---|---|
RADIO |
veafRadio | veafRadio |
SHORTCUTS |
veafShortcuts | veafShortcuts |
NAMEDPOINTS |
veafNamedPoints | veafNamedPoints |
ASSETS |
veafAssets | veafAssets |
CARRIER |
veafCarrierOperations | veafCarrierOperations |
ASSIST |
veafAssist | veafAssist |
SANCTUARY |
veafSanctuary | veafSanctuary |
COMBATZONE |
veafCombatZone | veafCombatZone |
AIRWAVES |
veafAirWaves | veafAirWaves |
QRA |
veafQraManager | veafQraManager |
CASMISSION |
veafCasMission | veafCasMission |
CAMPAIGN |
veafCampaign | veafCampaign |
COMBATMISSION |
veafCombatMission | — |
SPAWN |
veafSpawn | veafSpawn |
MOVE |
veafMove | veafMove |
SECURITY |
veafSecurity | veafSecurity |
GRASS |
veafGrass | veafGrass |
WEATHER |
veafWeather | veafWeather |
INTERPRETER |
veafInterpreter | veafInterpreter |
MISSILEGUARDIAN |
veafMissileGuardian | veafMissileGuardian |
TRANSPORTMISSION |
veafTransportMission | veafTransportMission |
AIRBASES |
veafAirbases | veafAirbases |
GROUNDAI |
veafGroundAI | — |
REMOTE |
veafRemote | — |
SKYNET_MONITOR |
veafSkynetMonitor | — |
I18N |
veafI18n | — |
modules.RADIO.user_menus — F10 radio menus in YAML¶
Since ADR 0011, a mission maker can declare a custom F10 radio menu (in particular for the Mission Master) entirely in YAML, with no Lua, under modules.RADIO.user_menus.
modules:
RADIO:
user_menus:
restrict_to_group: "MM Ctrl" # optional: name of a DCS group; menu restricted to that group. Omitted = global.
tree:
- menu: "QRA Control"
items:
- { command: "Start QRA North", action: qra.start, qra: "QRA-North" }
- { command: "Stop QRA North", action: qra.stop, qra: "QRA-North" }
- { command: "Global message", action: message, text: "The mission is starting!" }
- { command: "Custom function", action: lua, function: "myMission.startEverything", args: ["alpha", 3] }
Each node of tree is either a submenu ({ menu: "...", items: [...] }, recursive) or a command ({ command: "...", action: <verb>, <keys> }). The action vocabulary is closed (qra.start/qra.stop, airwave.start/airwave.stop/airwave.reset, flag.on/flag.off/flag.set/flag.increment/flag.decrement, message, lua). A lua action references a function defined in mission-script.lua: if the function is missing, the build fails.
See the full schema, the action table and a detailed example in veafRadio → Radio menus in YAML.
modules.CAMPAIGN — a campaign mission¶
Turned on and filled by veaf-tools campaign next, never by hand: it makes the mission one episode of a multi-mission campaign.
modules:
CAMPAIGN:
enable: true
data_file: src/campaign-data.yaml
| Key | Role |
|---|---|
data_file |
the file, relative to the mission folder, the build turns into the veafCampaign.data table (zones, garrisons, reserves, destroyed scenery); read at build, never handed to the runtime as is |
modules.QRA, cap_missions:, combat_missions:¶
QRA definitions live under modules.QRA (silence_all + definitions:). The cap_missions: / combat_missions: sections remain top-level. All require the corresponding modules enabled under modules:.
See the respective module pages for full schema:
- modules.QRA — Quick Reaction Alert definitions
- cap_missions: and combat_missions: — CAP and combat mission definitions
Radio menu shortcut (QRA / AirWaves). A QRA definition (
modules.QRA.definitions[]) or an AirWave zone (modules.AIRWAVES.airwave_zones[]) acceptsradio_menu: true(and the optionalradio_menu_restrict_to_group: "<DCS group>") to automatically generate an F10 control submenu (start/stop, plus reset for AirWaves). See veafQraManager and veafAirWaves.
community_scripts: (legacy)¶
Deprecated. Community scripts are now configured in the unified
modules:block using their uppercase IDs (e.g.CTLD: true). The separatecommunity_scripts:section still works but emits a deprecation warning. Seemodules:for the current syntax and the list of community IDs.
custom_scripts:¶
Declares custom Lua scripts present in src/scripts/ that are not part of the standard VEAF v6 set.
A declared script is included in the .miz without triggering a warning. By default a DCS load trigger is generated for it automatically; setting generate_load_trigger: false disables that trigger (useful when the script is loaded manually from mission-script.lua).
| Field | Type | Default | Description |
|---|---|---|---|
generate_load_trigger |
bool |
true |
Global default: generate a DCS trigger for all scripts in the list |
scripts[].path |
string |
(required) | Path to the file, relative to the mission folder (e.g. src/scripts/FgMission.lua) |
scripts[].generate_load_trigger |
bool |
(global default) | Per-script override; if absent, the global default applies |
scripts[].delay_seconds |
number |
(none) | Load this script after this delay (seconds) instead of with the rest. See below |
Loading behaviour
generate_load_trigger: true(default) → the script is loaded at mission start, in both loading modes: in static builds it is embedded in the.mizand loaded by the VEAF mission-loading trigger; in dynamic builds it is loaded from disk by the generatedveafDynamicConfig.lua. The same flag governs both modes — there is no per-mode flag.generate_load_trigger: false→ the script is still injected into the.mizbut no load is generated in either mode;mission-script.lua(or another script) must load it viadofile.
Load order: mission scripts always load as veaf-config.lua → mission-script.lua → your custom_scripts (in declaration order). So a custom script can rely on the VEAF config and on mission-script.lua already being loaded.
custom_scripts:
generate_load_trigger: true # global default for all scripts below
scripts:
- path: src/scripts/FgMission.lua
- path: src/scripts/FgTools.lua
generate_load_trigger: false # loaded manually from mission-script.lua
Any
.luafile present insrc/scripts/but absent from this section (and not one of the standard files) triggers a build warning with a reminder to declare it here.
Loading a script after a delay: delay_seconds
By default every mission script is loaded in one go at start-up. Some scripts need time to pass before they start — typically because they inventory the world once and must let the scripts before them create their units first. AIEN in Foothold is exactly that: loaded 12 seconds after the rest.
custom_scripts:
scripts:
- path: src/scripts/Moose.lua
- path: src/scripts/zoneCommander.lua
- path: src/scripts/AIEN.lua
delay_seconds: 12 # its own trigger, 12 s in
- Absent (the default) → loaded in the shared trigger, exactly as before.
- Present → the script leaves the shared trigger for a
triggerOnceof its own, gated onc_time_after. Scripts sharing the same delay share one trigger, in declaration order. - The delay must be greater than zero. A zero, negative or non-numeric value is refused with a build warning and the script loads in the shared trigger instead — it is never lost.
The delay decides the order, not the position in the list. A script at
delay_seconds: 12loads after every undelayed one, wherever it is written. If a delayed script is declared before an undelayed one, the build warns you — the list then reads in a different order from the one it runs in.
Dynamic builds behave the same way: veafDynamicConfig.lua schedules the load instead of doing it
inline. Since generate_load_trigger governs both modes, a delay could not exist in only one of them.
convert-other detects these delays in the source mission and writes delay_seconds: for you, so an
adopted mission reproduces the upstream staging without you having to notice it.
A script only in one variant (e.g. a dynamic-only debug script)
generate_load_trigger is a single flag — it does not distinguish static from dynamic. To load a script in only one variant (a debug helper you want only in your local dynamic dev build, never in the static distribution), use a build profile instead of a per-script flag:
custom_scripts:
scripts:
- path: src/scripts/FgMission.lua # always loaded (both variants)
profiles:
DEV:
custom_scripts:
scripts:
- path: src/scripts/FgMission.lua # ⚠️ must repeat the base scripts
- path: src/scripts/FgDebug.lua # extra, dev-only
Build the dev variant with veaf-tools mission build --profile DEV (it loads FgMission.lua + FgDebug.lua); the default build loads only FgMission.lua.
⚠️ Pitfall: profile deep-merge replaces lists, it does not concatenate them (see
profiles:). The profile'scustom_scripts.scriptsmust therefore repeat the base scripts and add the variant-specific one — otherwise the base scripts are lost in that profile.
pipeline:¶
Controls the optional build pipeline steps. See the Pipeline Reference for the full schema of each step's config file.
Each step accepts either a scalar value (true/false to enable or skip the step) or a mapping of detailed options.
pipeline:
presets: false
waypoints: true
spawnable_aircrafts:
file: src/my-spawnables.yaml
mode: replace
dynamic_slot_templates: false
weather: false
On top of the scalar form, the presets step accepts a mapping that keeps the radio injection while suppressing the PNG kneeboard plates globally:
pipeline:
presets:
enabled: true # default true — inject radio presets; false = disable the whole step
kneeboards: false # default true — when false, no kneeboard PNG (KNEEBOARD/<type>/IMAGES/presets[-<coalition>].png) is generated
The dynamic_slot_templates step's shipped catalogue injects only the templates of the sides holding at least one base that offers dynamic slots (a base whose warehouse says dynamicSpawn, or a side warehouses.yaml declares): the others would serve nobody.
Your own file is injected as written; when the source warehouses cannot be read, nothing is left out. A side that captures a base in game needs its templates from the start:
pipeline:
dynamic_slot_templates:
all_sides: true # default false — keeps both sides' templates
The waypoints step takes the same kind of mapping, for the automatic injection of the mission's own
bullseye (see The bullseye, injected for you):
pipeline:
waypoints:
enabled: true # default true — inject flight plans; false = disable the whole step
bullseye: false # default true — when false, no BULLSEYE waypoint is added automatically
build:¶
Controls how veaf-tools mission build resolves the VEAF scripts bundle.
These settings are normally set via the CLI (--dev-mode, --scripts-path) and then persisted here automatically.
| Field | Type | Default | Description |
|---|---|---|---|
dev_mode |
bool |
false |
When true, scripts are loaded from <scripts_path>/build/veaf-scripts.lua instead of the published copy |
scripts_path |
string |
(user config) | Path to a local VEAF-Mission-Creation-Tools clone; required when dev_mode: true |
dynamic_loading |
bool |
false |
When true, VEAF and mission scripts are loaded from disk at runtime (dynamic mode, dev/test) instead of being embedded in the .miz. Profile-overridable. CLI: --dynamic-mode / --no-dynamic-mode (takes precedence) |
scripts_path resolution order (first match wins):
1. --scripts-path <path> CLI option
2. mission.yaml build.scripts_path
3. ~/veafmct.yaml scripts_path
dynamic_loading resolves as: --dynamic-mode/--no-dynamic-mode (CLI) > build.dynamic_loading (profile-overridable) > false.
Dynamic loading — DEV vs PROD. When dynamic_loading: true, scripts load from disk at runtime (so they are not exposed inside the .miz/.trk):
- DEV (
dev_mode: true) —scripts_pathmust point at a VEAF-Mission-Creation-Tools checkout; the framework loads the individualveaf/*.luaviaVeafDynamicLoader.lua(edit and re-test without rebuilding). - PROD (
dev_mode: false) — the framework loads the concatenated bundleveaf/veaf-scripts.luafromscripts_path(default<mission>/published, installed by the updater).
In both modes the mission scripts — including your custom_scripts: — are loaded from disk via a generated src/scripts/veafDynamicConfig.lua (do not edit it by hand; declare scripts in custom_scripts:). The build errors out if the framework loader is missing under scripts_path.
build:
dev_mode: true
scripts_path: C:/dev/VEAF-Mission-Creation-Tools
dynamic_loading: false
profiles:
test:
build:
dynamic_loading: true # dynamic loading for the test profile
See the Developer Mode section of the Developer Guide for the full workflow.
profiles:¶
Named build profiles. Each profile is a set of config overrides that deep-merge onto the base mission.yaml when you pass --profile <name> to veaf-tools mission build. Keys absent from the profile retain their base values. Lists are replaced, not concatenated. The profiles: key itself is never written to the built mission.
| Field | Type | Description |
|---|---|---|
profiles.<name> |
dict |
Any top-level mission.yaml key (e.g. global_log_level, security, pipeline) |
Merge rules
- Nested dicts are merged recursively (only the keys you specify are overridden).
- Scalar values and lists are replaced wholesale; for combat zones, see zone_edits.
- profiles: is stripped from the effective config — it is never passed to the Lua generator or the pipeline.
profiles:
TEST:
global_log_level: debug
security:
disabled: true
pipeline:
weather: false
SERVER:
global_log_level: info
pipeline:
weather: true
Usage:
.\veaf-tools.exe mission build --profile TEST
.\veaf-tools.exe mission build --profile SERVER
If the named profile does not exist in
mission.yaml, a warning is emitted and the base config is used unchanged.
Editing a combat zone without copying the list¶
combat_zones is a list: a profile that gives it replaces it whole. To change two zones, a profile gives modules.COMBATZONE.zone_edits instead, mapping a zone_name (case-insensitive) to the keys to set on that entry.
The "*" key sets its keys on every entry; named edits are applied after it.
The rest of each entry is kept, and zone_edits never reaches the Lua generator.
A name no zone carries is reported by the build and does nothing.
Example: a training version of the mission, with no QRA, two SAM batteries off at start, every zone in training.
profiles:
TRAINING:
modules:
QRA:
enabled: false
COMBATZONE:
zone_edits:
"*": { training: true }
SA6Sasa: { active_at_start: false }
SA11Kiswah: { active_at_start: false }
Removing Mission Editor groups¶
A profile changes configuration only, except remove_groups:: a list of group names, exact or as patterns ("SA-10 *"), that the build takes out of that profile's .miz — whether placed by hand, carrying a #veafInterpreter, members of a combat zone or statics.
A pattern that matches no group is reported by the build.
profiles:
NO_SA10:
remove_groups: ["SA-10 *", "EWR Gudauta"]
build_variants:¶
A list of build profiles to emit together: a single veaf-tools mission build then produces one .miz per variant (the "moulinette" goal — typically Modern and Cold-War from one mission folder, the variant being only a config difference). Each variant builds the full pipeline with its merged profile (see profiles:) and its .miz is suffixed with the variant name (<base>_<VARIANT>.miz).
| Field | Type | Description |
|---|---|---|
build_variants |
list[str] |
Profile names (declared under profiles:) to each emit as a distinct .miz. |
Rules
- Without build_variants: (or an empty list) → a single .miz, behaviour unchanged.
- --profile <name> is the escape hatch: it forces building a single variant (that profile), suffixed with its name like a variant — build_variants: is ignored.
- Variants build in declaration order; each name must match a profiles: profile (otherwise a warning + base config, like --profile).
profiles:
MODERN:
mission:
era: MODERN
COLD_WAR:
mission:
era: COLD_WAR
build_variants:
- MODERN
- COLD_WAR
.\veaf-tools.exe mission build # produces <base>_MODERN.miz AND <base>_COLD_WAR.miz
.\veaf-tools.exe mission build --profile MODERN # produces only the MODERN variant (<base>_MODERN.miz)
training:¶
The mission's training versions, one per role: with this block, the build also produces <base>_TRAINING_A2G.miz, _TRAINING_CAP, _TRAINING_SEAD and _TRAINING_ALL, on the same setting and with the same briefing.
elements classifies what the mission holds (enemy_cap, qra, sam_lr, a2g_target, sam_threat); randomize declares the briefing's vague elements, drawn again at every launch.
The rules, examples and what is refused: Training versions, one per role.
- Cannot be combined with
build_variants:. --profile TRAINING_SEADbuilds that version only.
Keys documented elsewhere¶
Four top-level keys are read by the build but explained on the page that introduced them. They are listed here so that reading this reference does not miss them.
| Key | What it does | Page that documents it |
|---|---|---|
conversion_profile |
Names the adoption profile applied to a third-party mission (imposed modules, incompatibilities refused at validation) | convert-other |
config_override |
Injects raw configuration values over the ones the profile decided | convert-other |
strip_native_triggers |
Lists the original mission's load triggers to remove, so the VEAF scripts are not loaded twice | convert-other |
dcs_bridge |
Injects the dcs-bridge.lua bridge into the .miz (enabled, lua_path) — this is what makes data capture from a running DCS possible |
Mission maker's guide |
Index by category¶
The six domains match the French version, and every top-level section of this page appears in exactly one of them.
Core¶
| Section / Field | Description |
|---|---|
global_log_level |
Force a log level on all modules |
mission: |
Mission name, era, export path |
settings: |
Arbitrary veaf.config.KEY = value pairs |
veaf_tools: |
Version compatibility constraint |
modules: |
Enable, disable and configure each Lua module |
| Keys documented elsewhere | conversion_profile, config_override, strip_native_triggers, dcs_bridge |
Security¶
| Section / Field | Description |
|---|---|
security: |
Enable/disable security, password hashes |
modules.SECURITY |
veafSecurity |
Combat¶
| Section / Field | Description |
|---|---|
modules.QRA |
QRA definitions |
cap_missions: |
CAP mission definitions |
combat_missions: |
Combat mission definitions |
modules.COMBATZONE |
veafCombatZone |
modules.AIRWAVES |
veafAirWaves |
modules.CASMISSION |
veafCasMission |
Air Defense¶
| Section / Field | Description |
|---|---|
modules.SKYNET |
Skynet IADS integration |
modules.SANCTUARY |
veafSanctuary |
modules.MISSILEGUARDIAN |
veafMissileGuardian |
Assets & Support¶
| Section / Field | Description |
|---|---|
modules.ASSETS |
veafAssets |
modules.CARRIER |
veafCarrierOperations |
modules.NAMEDPOINTS |
veafNamedPoints |
modules.RADIO |
veafRadio |
modules.RADIO.user_menus |
F10 radio menus declared in YAML |
modules.SHORTCUTS |
veafShortcuts |
modules.ASSIST |
veafAssist |
modules.CTLD / modules.CSAR |
Cargo transport and pilot recovery (third-party sidecars) |
Build Pipeline¶
| Section / Field | Description |
|---|---|
pipeline: |
Pipeline step control |
pipeline.presets |
presets.yaml schema |
pipeline.waypoints |
waypoints.yaml schema |
pipeline.spawnable_aircrafts / pipeline.dynamic_slot_templates |
aircraft groups schema |
pipeline.weather |
versions.yaml schema |
custom_scripts: |
Custom Lua scripts to include in the mission |
community_scripts: |
Bundled community scripts (legacy form) |
build: |
Developer mode and scripts path override |
build.dev_mode |
Use local Lua bundle instead of published scripts |
build.scripts_path |
Path to local VEAF-Mission-Creation-Tools clone |
profiles: |
Named build profiles (deep-merge overrides for --profile) |
build_variants: |
Produce one .miz variant per named profile |
training: |
Training versions, one per role, built with the mission |
Index by module¶
| Module | mission.yaml key | Doc page |
|---|---|---|
| veafRadio | modules.RADIO |
veafRadio |
| veafShortcuts | modules.SHORTCUTS |
veafShortcuts |
| veafNamedPoints | modules.NAMEDPOINTS |
veafNamedPoints |
| veafCarrierOperations | modules.CARRIER |
veafCarrierOperations |
| veafAssets | modules.ASSETS |
veafAssets |
| veafAssist | modules.ASSIST |
veafAssist |
| veafSanctuary | modules.SANCTUARY |
veafSanctuary |
| veafCombatZone | modules.COMBATZONE |
veafCombatZone |
| veafAirWaves | modules.AIRWAVES |
veafAirWaves |
| veafQraManager | modules.QRA |
veafQraManager |
| veafCasMission | cap_missions: + combat_missions: |
veafCasMission |
See Also¶
- Pipeline Reference — YAML schemas for presets, waypoints, spawnables, dynamic-slot-templates, warehouses, spawn-groups, versions
- Mission Maker Guide — complete workflow
- Lua API Reference — Lua builder chain API for advanced use