diff --git a/docs/re/captures/fire-probe-ammo-counters.png b/docs/re/captures/fire-probe-ammo-counters.png new file mode 100644 index 0000000..341ccea Binary files /dev/null and b/docs/re/captures/fire-probe-ammo-counters.png differ diff --git a/docs/re/captures/first-missile-kills.png b/docs/re/captures/first-missile-kills.png new file mode 100644 index 0000000..6b67515 Binary files /dev/null and b/docs/re/captures/first-missile-kills.png differ diff --git a/docs/re/mission-escort-state.md b/docs/re/mission-escort-state.md index f0c4184..7f48d57 100644 --- a/docs/re/mission-escort-state.md +++ b/docs/re/mission-escort-state.md @@ -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 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 -`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 -watching that number settles "we never shoot" vs "we shoot and miss" in one run. +### Why nothing died — settled by probe, then fixed + +`fire_probe.sh` holds each pad input in turn in flight and photographs the HUD ammo +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 diff --git a/tools/re-capture/fire_probe.sh b/tools/re-capture/fire_probe.sh new file mode 100755 index 0000000..9b66ec1 --- /dev/null +++ b/tools/re-capture/fire_probe.sh @@ -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" diff --git a/tools/re-capture/pilot.py b/tools/re-capture/pilot.py index d1863d5..d247452 100644 --- a/tools/re-capture/pilot.py +++ b/tools/re-capture/pilot.py @@ -77,6 +77,22 @@ SHELL_VELOCITY = 8000.0 SHELL_MAX_RANGE = 4000.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" # 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 @@ -126,6 +142,9 @@ class Pilot: self.asset_hist = deque(maxlen=64) self.asset_hp0 = None self.asset_last_hit = -1e9 + self.missile_down = False + self.missile_t = -1e9 + self.missiles = 0 def f32(self, 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 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: self.pad.axis("LX", sx) self.pad.axis("LY", sy) if fire != self.firing: (self.pad.press if fire else self.pad.release)("RB") 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} " 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"/{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: msg += f" ast={a_frac*100:5.1f}%" if a_dmg > self.ASSET_ALERT: