Three findings turned the previous dead end into a working loop, each checked
against something independent rather than assumed:
* a live entity's definition pointer sits at position + 0x130, so one heap
scan types every craft in the scene -- which is what separates ~20 real
combatants from ~30000 moving particles. The result is coherent: wingmen,
enemy turrets and attackers, friendly capital ships, one Player.
* orientation is a 3x3 at position - 0x70 stored with a 16-BYTE ROW STRIDE
(a 4x4 whose translation row is the position). The earlier search for nine
contiguous floats could not find this by construction, which is why the
first pass wrongly concluded there was no transform. Confirmed by its row 2
matching measured direction of travel at cos = +1.000.
* RB is the fire button, established by consequence: of RB/LB/A/B/X/Y/RT/LT
it is the only one that makes the nose-ammo counter in RAM fall.
Control is PD on the aiming error with the derivative from body angular
velocity, and target selection weighted by off-boresight angle -- pure
nearest-first kept picking targets 90 deg off the nose, whose bearing rate then
outran the turn rate and held the craft outside its firing cone at a steady 27
deg pitch error.
Observed: distance to target closing monotonically, yaw error driven from -8 deg
to ~0, fire=1 once inside the cone, ammo counter falling.
Not solved: survival. There is no evasion, no shield/armour awareness and no
throttle control, so it flies a straight pursuit into defended space and is
shot down; every long run has ended in GAME OVER. Mission completion needs
those, plus objective-aware target priority.
9.1 KiB
Memory-driven autopilot — build log and current state
Status: 🟢 IT FLIES AND SHOOTS — it does not yet survive. Updated 2026-07-29 (second pass). The autopilot reads the live world, picks hostile targets, pursues them and opens fire. What it cannot do is stay alive: there is no evasion or shield management, so it dies before a mission ends. This is the honest state, not a plan.
What the loop does now, observed
[ 82.5] tgt=e007_ADAN_Turret d=3384 yaw= -7.3 pit=+14.6 stick=(-0.13,-0.34) fire=0
[ 85.7] tgt=e007_ADAN_Turret d=2797 yaw= +1.9 pit=+27.2 stick=(+0.20,-0.59) fire=0
[117.3] tgt=e007_ADAN_Turret d=4211 yaw= +5.7 pit= +9.0 stick=(+0.25,-0.40) fire=1
Distance closes monotonically, yaw error is driven from −8° to ~0, and once both
errors are inside the firing cone it holds RB and the ammo counter falls. A
rescan reports the scene as e.g. 136 entities {'TCAF': 16, 'ADAN': 120}.
The chain that made it work — each link checked, not assumed:
- Entity typing. A live entity's definition pointer sits at
position + 0x130. One heap scan then yields every craft with its unit
type, which is what separates 20-odd real combatants from ~30 000 moving
particles. Verified by the result being coherent: wingmen, enemy turrets and
attackers, friendly capital ships, and exactly one
…_Player. - Orientation. A 3×3 rotation at position − 0x70, stored with a 16-byte row stride (a 4×4 transform whose translation row is the position). An earlier search for nine contiguous floats structurally could not find this, which is why the first pass concluded "no transform". The binding is confirmed independently: its row 2 matches the craft's measured direction of travel with cos = +1.000.
- The fire button is RB — established by consequence, not by guessing:
of RB/LB/A/B/X/Y/RT/LT, pressing RB is the only one that makes the nose-ammo
counter in RAM fall (5958 → 5940).
RTis not the throttle, and no button tested is. - Control. PD on the aiming error with the derivative taken from the
craft's own body angular velocity (from two consecutive rotation matrices),
and target selection weighted by off-boresight angle
(
score = d·(1 + 3·(θ/π)²)) rather than pure nearest — closing on a target 90° off the nose only raises the bearing rate, which is what held the first run outside its firing cone at a steady ~27° pitch error.
Goal
Fly and fight a mission by reading the game's own world state out of guest RAM and driving the pad from it — the RE payoff being an oracle for the reimplementation's flight model and AI, and a way to reach missions the save cannot otherwise reach (unit coverage for unit-struct-runtime is capped at 21/110 because definitions load per stage).
What works (verified)
| Piece | Tool | Evidence |
|---|---|---|
| Live guest-RAM reads at loop rate | gworld.py |
/dev/shm file opened once, pread per tick; a whole-RAM scan is ~6 s, a targeted read is microseconds |
| Whole-RAM float scanning | numpy over SEEK_DATA extents |
1 270 orthonormal 3×3 blocks located in 6.2 s |
| Entity enumeration by unit type | gworld.py entities |
116 live instances in Stage 02, typed by unit ID, incl. exactly one …_Player |
| Pad control at loop rate | flight_probe.Pad |
writes command lines straight into the vgamepad FIFO; the vgamepad CLI spawns a process per command and its tap/hold sleep inside the server, so neither is usable in a control loop |
| Unattended mission entry | launch_mission.sh |
title → LOAD GAME → slot 01 → READY ROOM → TAKE OFF → in flight, repeatable |
| Hangar loadout | launch_mission.sh --hangar |
the "Recommended" control is AUTO SELECT — "Mount most suitable weapons", already the default cursor position. At 5 % progress it is a no-op: only two weapons are developed, and they are already mounted |
| Finding moving objects | findplayer.py |
position triples recovered from motion alone — straight-line, constant-speed filter over K whole-RAM samples |
What is NOT solved
Survival, and therefore mission completion. The loop has no evasion, no shield/armour awareness and no throttle control, so it flies a straight pursuit into defended space and is eventually shot down — every long run so far has ended in GAME OVER. Completing a mission needs, at least: reading own shield/armour, breaking off when hit, and prioritising the mission's actual objective targets over the nearest turret.
Superseded (kept because the reasoning still matters)
The notes below were written before the chain above worked. They remain true as
statements about 0x820af030, which is not the live entity —
- The
0x820af030class is not the live entity. It has one object per spawned thing and carries the unit-ID string, so it looked like the entity list — but over a 29 s in-flight capture all 384 words of it are constant (whatchanges.py). It is a static spawn record. The earlier claim inunit-struct-runtime.mdthat this class is "the spawned entity instance — live state" is wrong in the second half: it is per-spawn, but it is not live state. The parts of that document that depend on the definition class0x820af844are unaffected. - No transform in or one hop from that object: no orthonormal 3×3, and no
unit quaternion, within
0x2000of it or behind any of its 121 pointers. - Input correlation finds a self-object, but not obviously the craft.
Holding hard-left then hard-right yaw and looking for a position whose turn
axis reverses (
findself.py,selfstate.py) gives clean hits (cos ≈ −0.99), but they cluster at0x40009xxxin what looks like an 8-corner box with ±45 000 coordinates — a camera/skybox volume that follows the player, not the craft. Its speed (≈359/s) is suspiciously close to the HUD's 350, which supports "follows the player" but is not proof of identity. - Entity typing is unavailable: no definition pointer within ±0x800 of the self-position, so the trick of learning one object's layout and applying it to all the others has nothing to anchor on. Without typing, the 33 418 moving triples in a firefight cannot be separated into enemies, friendlies and bullets, so there is nothing to aim at.
Dead ends, recorded so they are not re-run
- Speed-scan for the player object (
findspeed.py, the classic two-state value scan: coast → boost → coast). Sound method, butRTis not the throttle — 5 864 floats matched the cruise speed and none rose. The control actually bound to acceleration was never established, and the run that would have established it ended in GAME OVER. - Comparing orientation matrices 2 s apart. At a real turn rate that is far
outside the small-angle regime, so the skew part of
A·Bᵀis not the rotation vector and the "angular velocity" comes out as ~30 000. Sample incrementally (6 Hz) and re-check orthonormality on every read — blocks found by a scan get overwritten between the scan and the read.
The next step that unblocks the most
Find the game's own target pointer instead of typing entities ourselves.
The HUD has a lock-on system (a TARGET marker and a target-cycle button), so
a global almost certainly holds a pointer to the currently-targeted entity.
Reading that gives an enemy's live object address directly — which yields both
target selection and the entity layout (position offset within it), i.e. it
collapses problems 3 and 4 into one. It is also cheap to find: cycle the target
with the pad and watch which pointer-shaped global changes in step.
Operational notes
- An unattended craft dies — the ship flies straight into a firefight, and two long scans were invalidated by a GAME OVER mid-run. Any scan longer than ~30 s needs either a survivable holding pattern or a fresh mission.
Xvfband the emulator die on their own every few minutes here, cleanly (exit 0), cause unidentified. Everything that must not be interrupted is run as one background task that starts the display, the emulator, the navigation and the measurement together, so nothing has to survive between tool calls.pgrepcannot be used for liveness in this container: PID 1 issleep infinityand never reaps, so dead processes linger as<defunct>and still match by name. Useps -o stat=and skipZ, orxdpyinfofor X.- numpy is not installed system-wide;
pip install --break-system-packages numpyputs it in/sylph-home/.local, which is only onsys.pathwhenHOME=/sylph-home. Scripts run withHOME=/sylph-home/reneedPYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages.
Files
gworld.py (live reader + entity list) · flight_probe.py (scripted inputs +
sampling, and the Pad FIFO client) · flight_analyze.py · whatchanges.py
(encoding-agnostic "which words are live") · findplayer.py · findself.py ·
findrot_global.py · findspeed.py · liveents.py · selfstate.py (the
whole chain → JSON) · autopilot2.py (PD controller; untested — it has never
had a valid config to run against) · launch_mission.sh.