# xp-flight — put a planned flight into X-Plane

Reference implementation for atcwatch's `flight_apply_cmd`, the command behind
the tray entry **To the Sim**. You plan a flight in ATCMatch, press the button,
and this script sets it up in a running X-Plane 12.

It is a *reference exemplar*, not a product: small enough to read in one
sitting and to change into whatever your own setup needs.

```
xp_flight.py --job                 # apply the planned flight
xp_flight.py --probe               # is my X-Plane reachable? (reads only)
xp_flight.py --probe-flight EDDF   # does a stand-less start work? (LOADS a world)
```

Python standard library only. No plugin, no `pip install`.

## Setting it up

1. **Turn the network API on.** In X-Plane: *Settings → Network → Accept
   incoming connections*. It is **off by default**, and that is the single most
   common reason this script reports "X-Plane is not running" while it plainly
   is.
2. Check the connection:
   ```
   ./xp_flight.py --probe
   ```
   Reads only, changes nothing: it reports whether the simulator answers and
   whether a world is loaded and running.
3. Copy `config.example.json` to `config.json` **in this directory** and set
   `xplane_root` to your X-Plane folder. This is required — see *Why it needs
   your X-Plane folder* below.

   Not next to atcwatch's `watchlist.json`: atcwatch owns the `config.json`
   there and rejects any field it does not know, so a setting of ours in that
   file would stop the notifier from starting. Two tools, two files.
4. In atcwatch's `config.json`, point `flight_apply_cmd` at this script:
   ```json
   "flight_apply_cmd": ["/home/you/xp-flight/xp_flight.py", "--job"]
   ```

That is enough to be put at the right airport, at a real parking stand.
Aircraft and livery need one more step — see below.

## Why it needs your X-Plane folder

X-Plane will not start a flight from an airport alone. Asked for one, it
answers:

```
400  It needs both an 'airport_id' and a 'ramp' key
```

There is no endpoint that lists an airport's stands, and stand names belong to
whichever scenery you have installed — the same EDDF is `A11` with one add-on
and something else entirely with another. So the script reads the name out of
the scenery, in the order X-Plane itself uses: `scenery_packs.ini` from the
top, first pack that knows the airport wins, and the Gateway scenery that ships
with X-Plane catches everything nobody installed by hand.

It picks a *valid* stand, not a good one: gates before tie-downs, then the
first by name, always the same one for the same airport. No occupancy check, no
jetway, no runway direction — choosing a *good* gate is a far bigger job than
this whole script. Moving is five seconds in the sim, and `--ramp NAME`
overrides the choice.

Missing `scenery_packs.ini` is a stop, not a fallback: it is X-Plane's priority
order, and without it the far likelier reading is that `xplane_root` points
somewhere that is not an X-Plane installation.

## Aircraft and livery: `aircraft-map.json`

ATCMatch names an aircraft by an `aircraft_code` such as `A319`, `B738` or
`LU737`. That is ATCMatch's own vocabulary, and the folder it lives in differs
on every installation — so this script cannot turn a code into a path on your
disk without guessing, and a guess would put the wrong aeroplane in the sim
while still reporting success.

So you state it once. Copy `aircraft-map.example.json` to `aircraft-map.json`
next to `config.json` (this directory) and add the aircraft you actually own:

```json
{
  "A319": "Aircraft/Toliss/ToLissA319/a319.acf",
  "B738": "Aircraft/Other/B737-800X/b738.acf"
}
```

Paths are relative to the X-Plane folder, which you name once in `config.json`
beside the map.

**Family packages.** One folder can hold several models — a 737 pack with the
736 through the 739, an E-Jets pack with the E170 through the Lineage. There
the code alone names no model, because which one a paint belongs to is a
property of the paint. Use a `code/livery name` key for those; it wins over the
plain code:

```json
{ "LU737/Lufthansa D-ABIA": "Aircraft/Other/LevelUp/737_80NG.acf" }
```

### What happens when the map says nothing

Nothing bad, and nothing silent:

| Situation | What the script does |
|---|---|
| No entry for the code (the default — an empty map) | Sets the flight, and says in its one output line that model and livery were left alone. You pick the aeroplane in the sim. |
| Entry points at a file that is not there | Refuses, names the path it expected. The sim is left untouched. |
| Aircraft installed, livery folder missing | Sets the model, not the paint, and names the folder you are missing. |

It never falls back to "then just fly something else". A flight it cannot set
honestly is one it refuses.

### Generating the map from a livery database

If you already have the `liveries.db` that ATCMatch is built on, the helper
writes the map for you:

```
./make-aircraft-map.py --db ~/path/to/liveries.db --root ~/X-Plane-12
```

It maps what it can prove and reports what it cannot — a folder with several
models and no variant to tell them apart, or a paint whose name and recorded
variant point at different models. Those it leaves to you rather than picking
one. `xp_flight.py` itself never reads the database: the database is one
person's hangar, the map is the small portable statement of where things live.

## While X-Plane loads

Loading dominates everything this script does. Setting a flight at the airport
you are already parked at is a matter of seconds; sending X-Plane somewhere
else makes it build a new world, which takes **two to five minutes** — and for
all of that time its web API does not answer at all, because the same main loop
serves the API and builds the scenery.

That has two consequences, and neither is a fault:

* **`X-Plane is loading, so not read back`** after a flight was set. The script
  asks the simulator what it actually loaded, and while a world is being built
  there is nothing to ask. The flight is set; the read-back simply could not
  run. This is the normal case when you change airports.
* **`X-Plane was busy loading and did not confirm`**. The request was delivered
  — X-Plane read it and then went quiet mid-load instead of replying. Look at
  the sim in a minute; the flight is almost certainly there.

If you press it *while* a world is loading, the script says so and asks you to
try again once the load has finished. It will not shout a request into a
simulator that cannot hear it.

## Exit codes

atcwatch shows the script's output either way and treats non-zero as a failure.

| Code | Meaning |
|---|---|
| 0 | Flight set |
| 1 | Refused — the order, the configuration or the map is wrong |
| 2 | The simulator is not ready (not running, hung, or paused) |
| 3 | X-Plane rejected the flight, or the read-back disagreed |

After setting a flight the script reads the aircraft **and the livery** back out
of the simulator and compares them with what it asked for — a POST that
returned success can still have produced a different aeroplane, and a silently
ignored livery is the kind of thing nobody notices until they are in the
cockpit.

The one case it cannot check is a flight posted from the start screen: X-Plane
is building the world for the next few minutes, so there is nothing to read
yet. Waiting is not an option either — atcwatch gives the command 120 seconds.
So the output line says `not verified` instead of pretending. That is the only
time exit 0 comes without a read-back.

## How it decides the sim is ready

`/api/capabilities` answers in a fraction of a millisecond even while X-Plane
is wedged, so HTTP alone cannot tell "ready" from "hung". UDP goes quiet in
that state. The script therefore subscribes briefly to the simulator clock over
UDP: no answer means not running, an answer that does not grow means paused or
hung. The start screen is a deliberate exception — there no world is loaded
yet, and setting the flight is exactly what loads one.

## Scope, honestly

This script sets an airport and a parking stand, plus a model and livery if you
have mapped them. It does **not** rate the stand (no occupancy, no ATIS runway,
no jetway check), does not read SimBrief or weather, does not start X-Plane,
and does not detect your installation — the X-Plane folder comes from the
config or the script declines to guess.

Host and port are hard-wired to loopback and are never taken from the order.
The order arrives as JSON from a web server, and a server-supplied host would
turn it into a target selector for writes into a local process.

Measured against X-Plane 12.4.3 on 2026-08-25; the numbers behind the design
decisions are in the script's own header. `/api/v3` is Laminar's version
number, not ours — a future update may change it, and then this script needs a
small edit. That is the nature of a reference exemplar.

Finding a stand took under 0.3 s when an installed scenery had the airport and
about 2 s when the 380 MB Gateway file had to be read. A wrong stand name is
refused in 27 ms with a clear error, so nothing is ever quietly parked in the
wrong place.
