pilot: RB fires, Y is the main mount, and guided missiles produce the first kills

fire_probe.sh holds each pad input in flight and photographs the HUD ammo
counters. RB moves NOSE BM 06000 -> 05956 in 4 s (~11 rounds/s, HEAT rises);
Y moves MAIN MPM 00300 -> 00299; nothing else moves either counter. So the
control mapping is measured rather than assumed, and 'we never shoot' is dead:
we shoot and miss.

The disc data says to stop shooting: Shell_TCAF_DeltaSaber_Missile_P is Power
200, GuidanceType 5 (guided), MaximumRange 5000, versus the nose gun's Power 15
unguided — one missile is worth ~14 gun hits on a 500 HP fighter and it steers
itself. Launching them (press Y, release a tick later, >=2 s apart) produced
YOU KILLED: WARPLANES 0002 — the first non-zero kill counter of the series,
against 0000 in all five gun-only runs, with hostiles down 134 -> 104.

Still only 2 kills per 98 missiles (~2%). Likely cause: the game expects a lock
before launch and an unlocked missile is wasted. Reading the lock state out of
RAM is the next step.
This commit is contained in:
claude-re
2026-07-30 18:40:39 +00:00
parent 3f6efadf9e
commit 76f463b611
5 changed files with 107 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@@ -147,10 +147,35 @@ angular half-size **alone** (2.7° at 2584 units for a fighter) is far tighter t
steering loop can hold the nose, and firing collapsed to 1 frame in 2639. Angular size steering loop can hold the nose, and firing collapsed to 1 frame in 2639. Angular size
belongs in the gate as a **floor** that opens it up close, never as a cap. belongs in the gate as a **floor** that opens it up close, never as a cap.
**Open (next):** find out why nothing dies. The cheap decisive test is whether the ### Why nothing died — settled by probe, then fixed
`RB` command discharges the gun at all — the HUD carries a live ammo count
(`MAIN MPM 00300`, which the weapon RE matched to `LoadingCount`), so holding fire and `fire_probe.sh` holds each pad input in turn in flight and photographs the HUD ammo
watching that number settles "we never shoot" vs "we shoot and miss" in one run. counters. Result:
| input | `NOSE BM` | `MAIN MPM` |
|---|---|---|
| idle | 06000 | 00300 |
| **RB** | **05956** (44 in 4 s, HEAT rises) | 00300 |
| **Y** | 05951 | **00299** (1) |
| LB / X / B / A / RT / LT | no change | no change |
So **`RB` is the nose gun (~11 rounds/s) and `Y` is the main mount** — measured, not
assumed — and the "we never shoot" hypothesis is dead: **we shoot and miss.**
Which is what the disc data says to stop doing. `Shell_TCAF_DeltaSaber_Missile_P` is
**Power 200, `GuidanceType` 5 (guided), `MaximumRange` 5000**, against the nose gun's
**Power 15, unguided**. One missile is worth ~14 gun hits on a 500 HP fighter *and it
steers itself* — the accuracy problem solved rather than tuned. (`ASMissile_P` is
Power **5000**, the anti-ship option.)
Adding missile launches to the pilot (press `Y`, release a tick later, ≥2 s apart)
produced **the first kills of the whole series: `YOU KILLED: WARPLANES 0002`**, versus
`0000` in all five gun-only runs, with hostiles down 134 → 104 (the largest fall yet).
**Still poor, and stated as such: 98 missiles for 2 kills (~2 %).** The likely cause is
that the game expects a *lock* — holding the target in the reticle before launch — and
an unlocked launch is wasted. Reading the lock state (or the lock timer) out of RAM is
the next step, and it is the same anchoring trick as everything else here.
## Reimplementation notes ## Reimplementation notes

42
tools/re-capture/fire_probe.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Does the pad actually DISCHARGE a weapon? Hold each candidate input in turn
# and photograph the HUD's ammo counters.
#
# Why this exists: the autopilot's HUD kill counters read WARSHIPS 0000 /
# WARPLANES 0000 at the end of every run, in every targeting configuration, so
# "fraction of frames with the guns commanded on" was never measuring anything.
# Before tuning aim any further, settle the prior question — whether the fire
# command reaches the gun at all. The HUD carries a live ammo count (`MAIN MPM
# 00300`, matched to `LoadingCount` by the weapon RE), so the counter falling
# during a hold is direct evidence of a discharge, and the counter sitting still
# through every button is direct evidence that we have never fired a shot.
#
# `RB fires` came from an earlier session; this re-tests it rather than assuming
# it, and sweeps the other buttons so a wrong mapping cannot hide.
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
SD="$(cd "$(dirname "$0")" && pwd)"
SHOTS=/sylph-home/re/shots
HOLD="${1:-4}"
shot(){ screenshot "$SHOTS/fire-$1.png" >/dev/null 2>&1; echo " shot $1"; }
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
sleep 3
echo "=== probing (hold ${HOLD}s each) ==="
shot "00-idle"
# RB first: it is the incumbent claim. Then the rest, so a wrong mapping cannot
# hide behind it. A/B/X/Y may also switch weapon or open something — that is
# fine for a probe, and the final frame records wherever it ended up.
for b in RB LB Y X B A LS RS; do
vgamepad press "$b"; sleep "$HOLD"; shot "hold-$b"; vgamepad release "$b"; sleep 1.5
done
# triggers are analogue, not buttons
vgamepad trig RT 1.0; sleep "$HOLD"; shot "hold-RT"; vgamepad trig RT 0.0; sleep 1.5
vgamepad trig LT 1.0; sleep "$HOLD"; shot "hold-LT"; vgamepad trig LT 0.0; sleep 1.5
vgamepad reset
shot "99-final"
echo "PROBE DONE"

View File

@@ -77,6 +77,22 @@ SHELL_VELOCITY = 8000.0
SHELL_MAX_RANGE = 4000.0 SHELL_MAX_RANGE = 4000.0
SHELL_RADIUS = 20.0 SHELL_RADIUS = 20.0
# The MAIN weapon, fired with Y — measured, not assumed: a hold-each-input probe
# (fire_probe.sh) moved NOSE BM 06000 -> 05956 under RB and MAIN MPM 00300 ->
# 00299 under Y, so RB is the nose gun (~11 rounds/s) and Y is the main mount.
# That probe also settled the lethality question the other way round: we DO
# shoot, so the kill counters reading 0000 mean we shoot and MISS.
#
# Which is exactly what the disc data says to stop doing. Shell_TCAF_DeltaSaber_
# Missile_P is Power 200 with GuidanceType 5 (guided) and MaximumRange 5000,
# against the nose gun's Power 15 unguided — one missile is worth ~14 gun hits
# on a 500 HP fighter, and it steers itself, which is the accuracy problem
# solved rather than tuned. Range is held under the confirmed 5000 because which
# main weapon is actually loaded is not read from RAM yet.
MISSILE_RANGE = 4000.0
MISSILE_CONE = math.radians(20.0)
MISSILE_PERIOD = 2.0 # s between launches; 300 rounds is not unlimited
# Stage 02's protected asset. Named rather than derived: "the biggest friendly" # Stage 02's protected asset. Named rather than derived: "the biggest friendly"
# picks the f105 cruiser (30000 HP > the Acropolis's 25000), and "the friendly # picks the f105 cruiser (30000 HP > the Acropolis's 25000), and "the friendly
# with the most HP" picks it too, so neither rule finds the right ship. The # with the most HP" picks it too, so neither rule finds the right ship. The
@@ -126,6 +142,9 @@ class Pilot:
self.asset_hist = deque(maxlen=64) self.asset_hist = deque(maxlen=64)
self.asset_hp0 = None self.asset_hp0 = None
self.asset_last_hit = -1e9 self.asset_last_hit = -1e9
self.missile_down = False
self.missile_t = -1e9
self.missiles = 0
def f32(self, off): def f32(self, off):
b = os.pread(self.W.fd, 4, off) b = os.pread(self.W.fd, 4, off)
@@ -431,17 +450,33 @@ class Pilot:
aim_ok = abs(aim[0]) < cone and abs(aim[1]) < cone aim_ok = abs(aim[0]) < cone and abs(aim[1]) < cone
fire = bool(fire and tgt and aim_ok and tgt[4] < self.FIRE_RANGE and pn < 1.2) fire = bool(fire and tgt and aim_ok and tgt[4] < self.FIRE_RANGE and pn < 1.2)
# The main mount is a discrete launch, not a continuous stream: press Y
# and let go a tick later, then wait out MISSILE_PERIOD. Holding it
# would empty 300 rounds in half a minute.
msl = bool(tgt and self.mode in ("ENGAGE", "DEFEND")
and tgt[4] < MISSILE_RANGE
and abs(aim[0]) < MISSILE_CONE and abs(aim[1]) < MISSILE_CONE
and pn < 1.2)
if not self.dry: if not self.dry:
self.pad.axis("LX", sx) self.pad.axis("LX", sx)
self.pad.axis("LY", sy) self.pad.axis("LY", sy)
if fire != self.firing: if fire != self.firing:
(self.pad.press if fire else self.pad.release)("RB") (self.pad.press if fire else self.pad.release)("RB")
self.firing = fire self.firing = fire
if self.missile_down and t - self.missile_t > 0.15:
self.pad.release("Y")
self.missile_down = False
elif (not self.missile_down and msl
and t - self.missile_t > MISSILE_PERIOD):
self.pad.press("Y")
self.missile_down = True
self.missile_t = t
self.missiles += 1
msg = (f"{self.mode:<7} hull={hull:6.0f} shd={shield:6.0f} spd={speed:6.0f} " msg = (f"{self.mode:<7} hull={hull:6.0f} shd={shield:6.0f} spd={speed:6.0f} "
f"thr={self.throttle:+d} yaw={math.degrees(yaw):+6.1f} " f"thr={self.throttle:+d} yaw={math.degrees(yaw):+6.1f} "
f"pit={math.degrees(pitch):+6.1f} aim={math.degrees(aim[0]):+6.1f}" f"pit={math.degrees(pitch):+6.1f} aim={math.degrees(aim[0]):+6.1f}"
f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)}") f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)} msl={self.missiles}")
if ast is not None: if ast is not None:
msg += f" ast={a_frac*100:5.1f}%" msg += f" ast={a_frac*100:5.1f}%"
if a_dmg > self.ASSET_ALERT: if a_dmg > self.ASSET_ALERT: