veaf-tools mission export — JSON contract for the BFR dcs-mission-tools plugin¶
Audience: developers of the BFR Claude plugin
dcs-mission-tools(bfr-claude-plugins) who consumeveaf-tools mission export --format jsoninstead of running mission files throughlua54. This document is the frozen contract between the two tools.schemaVersion: 2.
Why this exists¶
veaf-tools parses a .miz's mission / dictionary / mapResource files with a pure-Python
luadata state machine — it never executes Lua (luadata/serializer/unserialize.py). Exporting
that parse as JSON lets the plugin read any mission (a .miz from a forum, a DM, an extracted
folder) without running untrusted Lua. The plugin keeps lua54 only to run its own .lua checks.
The single hard problem is faithfully mapping Lua tables to JSON and back. JSON object keys are always strings, so a plain JSON object cannot tell a Lua integer key from a Lua string key — and DCS missions carry both, sometimes in the same table:
- sparse integer keys —
payload.pylons = {[1]=,[2]=,[8]=,[11]=}(pylon numbers skip gaps); - mixed keys —
callsign = {[1]=,[2]=,[3]=,["name"]="Colt11"}; - string-numeric keys —
failures = {["10"]=,["11"]=}(DCS failure ids are strings).
A heuristic that "coerces numeric-string keys to integers" is therefore impossible to get right: it
breaks failures (real strings) while a "keys are always strings" rule breaks pylons and the integer
part of callsign. The contract below removes all guessing by preserving the key type in the JSON
itself, using JSON's native number-vs-string distinction.
1. Top-level shape¶
{
"schemaVersion": 2,
"theatre": "Caucasus",
"mission": { "...": "the parsed `mission` table" },
"dictionary": { "DictKey_...": "..." },
"mapResource": { "ResKey_...": "filename.lua" }
}
schemaVersion(integer) — always present, first, currently 2. See §5.theatre— string (ornullif absent).mission,dictionary,mapResource— the parsed tables, mapped per §2.dictionaryandmapResourceare string→string maps and always serialize as JSON objects.
2. Table mapping rule (deterministic, key-type-lossless)¶
For each Lua table, exactly one of these forms is emitted:
| Lua table | JSON form | When |
|---|---|---|
| Sequence | JSON array | keys are exactly the contiguous integers 1..n (n ≥ 1) |
| String record | JSON object | every key is a string |
| Integer / mixed table | __luaTable__ envelope |
at least one integer key, and not a pure sequence |
| Empty | JSON {} |
no keys (parity-neutral, see §3) |
The envelope is a single-key object whose value is an array of [key, value] pairs:
{ "__luaTable__": [ [1, "..."], [2, "..."], [8, "..."], ["name", "..."] ] }
Each pair key is a JSON number for a Lua integer key, a JSON string for a Lua string key. JSON's own number/string distinction carries the type, so the decoder never guesses. Guarantees:
- Integer pair keys are JSON integers (
1, never1.0) — a Lua integer key, not a float. - Each pair is a JSON array of exactly two elements;
pair[0]is a number or a string only.
Worked examples¶
"trigrules": [ {"comment": "init"}, {"comment": "win"} ],
"trig": { "actions": [ "a_do_script(...)" ], "flag": [ true ] },
"pylons": { "__luaTable__": [[1,"AIM-9"],[2,"AIM-120"],[8,"fuel"],[11,"AIM-9"]] },
"callsign": { "__luaTable__": [[1,169],[2,1],[3,1],["name","Colt11"]] },
"failures": { "10": {"enable": false}, "11": {"enable": true} }
Numerically-indexed sequences (trigrules, trig.actions/conditions/flag, groups/countries/zones)
stay arrays, so #t / ipairs work after decoding. failures stays a string-keyed object — never
coerced.
3. Empty tables¶
An empty Lua table {} exports as JSON {}. This is parity-neutral: a JSON {} and a JSON []
both decode to an empty Lua table where #t == 0 and next(t) == nil. The decoder must yield an empty
Lua table for both.
4. Scalars, strings, encoding¶
- Lua numbers → JSON numbers; Lua booleans → JSON
true/false. - Lua strings → JSON strings, UTF-8, not ASCII-escaped (
ensure_ascii=false). - Absent /
nilvalues are not present (theatremay benull). - Key/pair order is not semantically significant; decoders must not depend on it.
5. schemaVersion and compatibility¶
schemaVersionis an integer, bumped on any breaking change to this contract. The plugin must read it and refuse / warn on an unknown version rather than mis-reading silently.- Current version: 2 (v1 used a numeric-string-key coercion heuristic and is withdrawn).
6. Decoder requirements (summary, plugin side)¶
A conforming JSON→Lua decoder:
- JSON array → Lua sequence with integer keys
1..n. - JSON object without a sole
__luaTable__key → Lua table with the keys verbatim as strings (no numeric coercion — this is what keepsfailurescorrect). - JSON object whose only key is
__luaTable__and whose value is an array of 2-element pairs → Lua table built from the pairs:pair[0]of JSON type number → integer key, string → string key;pair[1]decoded recursively. (Harden against the sentinel collision by requiring exactly this shape; otherwise treat the object as a verbatim record.) - Empty array and empty object → empty Lua table.
With these rules the decoded tables reproduce load()'s output table-for-table (array-ness and
key types), so the plugin's checks return identical findings — and stay correct for future checks
that read tables today's checks ignore.
7. A note on the JSON shape¶
With the envelope, --format json is a Lua-faithful representation: frequent tables like pylons
and callsign become __luaTable__ wrappers, which is less ergonomic for a generic JSON consumer
(jq …). That is by design — this JSON is the plugin's parser contract. The human-friendly views are
--format yaml (native integer keys via PyYAML) and --format markdown.
8. Resources (for .miz input)¶
When the input is a .miz, veaf-tools mission export --extract-dir <dir> also extracts the archive's
embedded resources — .lua scripts and l10n/DEFAULT/* (sounds/images) — to a sidecar directory
mirroring the archive layout, so the plugin can run its .lua checks and resolve mapResource
filenames without unzipping. Data files already carried by the JSON are skipped. For an already-extracted
mission folder, nothing is extracted.
Out of scope (plugin-owned)¶
The JSON→Lua decoder, rerouting the plugin's missionLoader.lua away from load(), and bundling
veaf-tools live in the BFR plugin repo. This document only specifies what veaf-tools guarantees
and what the decoder must do.