The uinput pad was replaced on 2026-08-13 (canary cbfad1ab3), which is when flight_probe.Pad stopped reaching the game. flight-controls-runtime.md predates it (2026-07-30) and is safe; flight-speed-law.md was last touched on the same day and cannot be placed from dates alone; anything after it measured a craft that was not being flown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
166 lines
7.8 KiB
Markdown
166 lines
7.8 KiB
Markdown
# `pilot.py` never pulls the trigger — the proximal cause, measured
|
||
|
||
**Status: ✅ ROOT CAUSE FOUND AND FIXED (2026-08-23, later the same day) — the
|
||
flight tools' pad wrote to a FIFO nothing has read since the uinput pad was
|
||
removed, so every stick, trigger and button they sent went nowhere.** The gate
|
||
measured below is real; it was open because the craft was never being flown at
|
||
all. 🟡 The pilot still does not converge on a target in 150 s with the input
|
||
working — see the last section.
|
||
|
||
## The observation
|
||
|
||
Over a 900 s Stage 02 run, `fire=1` appears in **0 of 13 521** logged samples.
|
||
The loop flies, chases, evades and retires; the guns are never fired. Every
|
||
"kill something and watch X" experiment in this corpus has therefore been leaning
|
||
entirely on the wingmen.
|
||
|
||
## Which gate closes
|
||
|
||
`pilot.py`'s trigger needs all of: mode `ENGAGE`/`DEFEND`, a target, the aim error
|
||
inside the firing cone, range < `FIRE_RANGE` (`SHELL_MAX_RANGE` = 4 000), and a
|
||
small avoidance push. Over the 3 004 samples that had a target:
|
||
|
||
| quantity | median | min | inside its gate |
|
||
|---|---|---|---|
|
||
| `\|aim yaw\|` | **90.0°** | **90.0°** | 0 of 3 004 below 25° |
|
||
| `\|aim pitch\|` | 179.2° | 166.4° | 0 of 3 004 below 25° |
|
||
| range | 49 634 | 45 078 | 0 below 4 000, 0 below 10 000 |
|
||
|
||
`|aim yaw|` being **exactly** 90.0 in every sample is not a coincidence, it is a
|
||
branch: `sticks()` contains
|
||
|
||
```python
|
||
if ez < 0: # target behind: commit to a full turn
|
||
yaw = math.copysign(math.pi / 2, ex if ex else 1.0)
|
||
```
|
||
|
||
So the committed target was **behind the ship in 100 % of samples**, with a pitch
|
||
error near 180° — directly astern — and the range to it grew monotonically:
|
||
|
||
```
|
||
t= 0 s 22 265 t= 231 s 33 632
|
||
t=510 s 49 475 t= 884 s 48 200 (plateau)
|
||
```
|
||
|
||
The craft flies *away* from the thing it is chasing for fifteen minutes and the
|
||
turn never completes. Nothing else needs to be wrong for the trigger to stay
|
||
cold.
|
||
|
||
## What is NOT established
|
||
|
||
Three explanations fit that shape and this pass did not separate them:
|
||
|
||
1. the attitude matrix the pilot reads is **stale**, so the ship turns and the
|
||
controller cannot see it;
|
||
2. the matrix is live but `fwd_row`/`fwd_sign` name the wrong axis;
|
||
3. both are fine and the **yaw stick sign** is inverted, so the loop turns away
|
||
from the error it is nulling.
|
||
|
||
🔴 **An attempt to separate them was invalidated and is withdrawn.** `aim_probe.py`
|
||
watched the forward vector under neutral, full-left and full-right stick and
|
||
reported it pinned at `[-1, 0, 0]` with `|turn| = 0.00 °/s` in every phase — which
|
||
looks like explanation (1). It is not evidence: **the guest had frozen** partway
|
||
through, confirmed immediately afterwards by `frozen.py` (max pixel delta 0) and
|
||
by the player's position being byte-identical across 3 s. A dead world holds every
|
||
matrix still. The probe is committed because it is the right experiment; its
|
||
result is not.
|
||
|
||
## One confusion resolved on the way
|
||
|
||
Today's repeated `entities2.py self` failures — `# 0 moving triples`, "player
|
||
entity not found" — are **the freeze**, not a tool defect. `moving()` types
|
||
entities by their position *changing* between two samples, so a frozen world
|
||
yields nothing by construction. Runs where the bind failed were runs that had
|
||
already stopped.
|
||
|
||
## Next step
|
||
|
||
Re-run `aim_probe.py` on a run confirmed to be animating **at the end of the
|
||
probe as well as the start** — the tool should check `frozen.py` itself and
|
||
discard the phase otherwise. If the matrix does move under stick, the question
|
||
becomes the sign; if it does not, the binding is what to chase.
|
||
|
||
|
||
---
|
||
|
||
# ✅ ROOT CAUSE — the flight tools' pad was writing into a dead file
|
||
|
||
`flight_probe.Pad` opened **`/tmp/sylph-vgamepad.fifo`** and wrote
|
||
`axis LX 0.850`-style lines to it, described in its own docstring as "the vgamepad
|
||
server's FIFO". That server does not exist any anymore: the uinput pad was removed
|
||
because a uinput device is **not namespaced** and every scripted press leaked to
|
||
the *host's* desktop, and it was replaced by Canary's `--hid=file` driver. The
|
||
FIFO is now an ordinary 91-byte file that nothing reads.
|
||
|
||
Meanwhile `/tmp/xenia_pad.txt` — the file the emulator actually polls — was
|
||
**0 bytes** while the "autopilot" was supposedly flying.
|
||
|
||
So every axis, trigger and button from `pilot.py`, `autopilot3.py`,
|
||
`aim_probe.py` and `flight_probe.py` itself went into a dead file, silently, and
|
||
the craft flew on its own for the whole of every run. That is why the target sat
|
||
astern at 90.0° forever and the range grew to 49 km: **the ship was never being
|
||
turned.**
|
||
|
||
**The corpus already carried this trap — for the other half of the toolkit.**
|
||
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) records
|
||
"pad.py, NOT `vgamepad`: that command no longer exists … and every call here
|
||
failed silently, so the whole scripted route pressed nothing". The **shell**
|
||
scripts were fixed then. This class was not, and every flight tool imports it.
|
||
|
||
## Measured, before and after
|
||
|
||
Same probe, on runs confirmed animating at both ends of every phase
|
||
([`aim_probe.py`](../../tools/re-capture/aim_probe.py) brackets each phase with
|
||
`frozen.py` now, after a freeze voided the first attempt):
|
||
|
||
| | heading change under full stick | attitude matrix `pos-0x70` |
|
||
|---|---|---|
|
||
| **before** | `0.00°` in every phase, over 4 s, while travelling 350–735 units | `d 0.0000` — unchanged |
|
||
| **after** | `12.72°` on `LX=-1`; `LX=+1` swings the flight direction from `[1,0,0]` to `[0.13,-0.14,-0.98]` | `d 0.4438` / `d 0.3108` under stick, `0.0000` at neutral |
|
||
|
||
🔴 **So the "stale attitude matrix" reading is refuted**, and with it the earlier
|
||
suspicion that the binding was wrong: `pos-0x70` is live and tracks the ship. It
|
||
only looked dead because nothing was turning the ship.
|
||
|
||
## 🟡 Still open — the pilot does not converge
|
||
|
||
With the pad fixed, a 150 s `pilot.py` run still fired **0** times: over its 86
|
||
targeted samples `|aim yaw|` is *still* exactly 90.0° and the range is
|
||
36–43 km. Steering now works, so what remains is one of:
|
||
|
||
* the **sign** of the yaw stick against the pilot's own error convention — a
|
||
controller that turns away from the error would hold `ez < 0` forever, which is
|
||
exactly the observed signature;
|
||
* target selection that keeps committing to something 40 km away, which at
|
||
~300 units/s cannot be closed inside the window.
|
||
|
||
Both are now testable, because the input path finally works. The measurement to
|
||
make is the direct one: command the pilot's own `sx` for a known error and see
|
||
whether `|yaw|` falls or rises.
|
||
|
||
## Not fixed, and flagged in place
|
||
|
||
`findrot_global.py`, `findself.py`, `findspeed.py` and `selfstate.py` still write
|
||
to the dead FIFO. Each now carries a comment saying so and pointing here. They
|
||
are not repaired because none of them has been re-run since, and quietly changing
|
||
the input plumbing under a tool whose results are unverified would only
|
||
manufacture new unverified results.
|
||
|
||
|
||
## Which earlier results this puts at risk — dated, so it can be checked rather than worried about
|
||
|
||
The uinput pad was replaced on **2026-08-13** (canary `cbfad1ab3`), which is the
|
||
day `flight_probe.Pad` stopped reaching the game. So:
|
||
|
||
* ✅ **Safe:** anything measured before that. `flight-controls-runtime.md` was
|
||
last touched **2026-07-30**, comfortably on the working side.
|
||
* ⚠️ **Ambiguous, and worth re-checking:** `flight-speed-law.md` was last touched
|
||
**2026-08-13** — the same day — so whether its numbers were taken before or
|
||
after the FIFO went dead cannot be told from the dates alone.
|
||
* 🔴 **Anything that used `flight_probe.Pad` on or after 2026-08-13 measured a
|
||
craft that was not being flown.**
|
||
|
||
Stated as a rule for whoever picks this up: a flight result is only as good as a
|
||
demonstration that the stick moved the ship in the same session. `aim_probe.py`
|
||
is that demonstration, and it now brackets itself with a liveness check.
|