Files
Syplheed-Reborn/docs/re/mission-escort-state.md
claude-re 76f463b611 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.
2026-07-30 18:40:39 +00:00

9.7 KiB
Raw Blame History

Escort / mission state from guest RAM — every entity's hull

Status: CONFIRMED (2026-07-30). Capture: tools/re-capture/mission_state.py, session tools/re-capture/escort_session.sh, Stage 02 from save slot 01, 240 s of flight, 240 samples at 1 Hz → captures/mission-state-stage02.jsonl. Screenshot evidence: captures/escort-stage02-hud.png.

The question

own_state.py found the player's hull by anchoring on a solved definition field — an undamaged craft carries its definition's HP (+0x054), so the live counter is the copy of that number that falls. Result: hull = position + 0x154 (autopilot).

Stage 02 is an escort, and it is lost when the ACROPOLIS sinks, not when the player dies: a 240 s run hit GAME OVER with our own hull at 1500/1500. Scoring that objective needs someone else's hull. So: is +0x154 a property of the entity class, or of the player object?

Finding — it is class-wide

At the first sample of the run, before this session's fighting had touched them, pos+0x154 equals the entity's own definition HP across seven classes and five distinct HP values:

Class radius definition HP pos+0x154 at t=0
UN_e007_ADAN_Turret 22 100 100.0 (all 60 instances)
UN_e010_ADAN_Attacker_S 100 500 500.0 (all 19)
UN_f106_TCAF_Destroyer 2000 10000 10000.0
UN_e106_ADAN_Destroyer 2100 10000 10000.0
UN_f105_TCAF_Cruiser 3800 30000 30000.0
UN_e105_ADAN_Cruiser 3800 30000 30000.0
UN_f101_TCAF_Acropolis 1400 25000 25000.0

Measured directly by mission_state.py scan at the start of three separate runs: 146/150, 147/150 and 147/150 entities hold exactly their definition's HP at pos+0x154. The handful that do not sit slightly below it (9800/10000, 29933.3/30000, 9725/10000, …) — the battle is already in progress when the player launches, so those ships have already been shot at. Nothing read above its HP, and nothing read an unrelated number, which is what a coincidental offset would produce.

The value behaves like a live counter, not a copy of the definition:

  • it falls under fire — 780 distinct damage events were logged across the run;
  • it goes negative at death and the entity then disappears from the heap (UN_f106_TCAF_Destroyer-30.0 of 10000, another → -0.0, a third GONE);
  • the drops match what the HUD draws — the screenshot shows the ACROPOLIS and the destroyer CHARON each with their own health bar, CHARON's already red.

So hull = position + 0x154 for every entity, and the escort objective is directly scoreable: read the protected ship's hull, normalise by its definition's HP, done. No new anchor, no value scan.

The escort asset, measured

UN_f101_TCAF_Acropolis, one instance, HP 25000, collision radius 1400.

Its hull over the 240 s run (pilot chasing the nearest hostile fighter, the current pilot.py behaviour):

t=  0..150s   25000.0     untouched
t= 180.1s     24779.5
t= 210.1s     24149.5
t= 239.1s     23038.2     -1961.8 total, ≈ -600 HP/min once it starts

⚠️ Onset is NOT a fixed schedule — corrected by a later run. From this run alone it looked like the asset is safe for the first ~170 s. A second run put the first damage at t = 70 s, and its hostile population grew (134 → 166 ADAN) where this one's shrank (147 → 118). So the stage is not replaying identically, and "the asset is untouched early" is a property of one run, not of Stage 02. What survives the second run is the weaker, still useful claim: the loss is slow — a few hundred to ~1400 HP/min against 25000, so tens of minutes to sink. The earlier GAME OVER therefore was not a fast loss; it was an undefended one.

Also captured

  • Hostile population fell 147 → 118 over the run (the pilot fired on 435 of 1913 engage frames; most of the remainder it was manoeuvring with the target outside the 9° firing cone).
  • Two friendly destroyers were lost while the pilot was elsewhere.
  • The HUD's REMAINING OB read 012 at t≈240 s while 118 ADAN entities were alive, so that counter is objectives, not hostiles — its RAM address is still unknown ( open).

Escort-weighted targeting — implemented, and what it did NOT fix

pilot.py gained a DEFEND mode (2026-07-30): while the asset is losing hull, target the hostiles pressing it — ranked by distance to the asset minus credit for closing on it — instead of the ones nearest to us. Trigger and ranking both read the live hull, so nothing is inferred.

It works mechanically: DEFEND engaged 1.9 s after the asset's first hit in one run (t=167.0), and held for 54 % of a 330 s run. But it did not measurably save the asset. Over the window the two policies share, they are the same to within noise:

t (s) nearest-fighter escort-weighted
120 25000.0 24910.0
180 24779.5 24460.0
239 23038.2 23218.0

Two honest reasons it cannot yet be scored better than "no worse":

  1. The runs are not comparable past that window — different spawn timing and, in the escort-weighted run, a hostile population that grew 134 → 166 while the baseline's fell 147 → 118.
  2. Lethality is the real bottleneck, not target choice. The guns are on for only 12 % of combat frames (320 of 2630); the rest of the time the target is outside the 9° firing cone while the loop manoeuvres. Choosing a better target does little when most passes do not shoot.

One bug found and fixed by the first escort run (worth keeping as a pattern): the new mode flies at the asset, which sits inside the friendly formation, and the run ended hull 1500 -> DEAD in a single tick at 2026 units/s, 0.6 s from a friendly destroyer the avoidance expected to clear by 365 units — against a hull of radius 2000. Keep-out had been applied only to hostile turrets. Every entity above BIG_RADIUS now gets a physical keep-out of its own radius + 800, with braking inside it, whatever its faction; the next run survived its full 330 s untouched.

Ballistics from the disc data — and the measurement that invalidates the metric

The solved Shell records give the player's guns exactly (Shell_TCAF_DeltaSaber_{NoseGun,Gun,Beam}_P, all CONFIRMED): Velocity 8000, LifeTime 0.5 s, MaximumRange 4000 — self-consistent, since 8000 × 0.5 = 4000 — plus shell Radius 2030 and Power 15/30/40.

Two things in pilot.py were plainly wrong against those numbers, and both are fixed:

  • Lead used our own speed as the shell speed. Flight time was d / max(our_speed, 300), i.e. 4002000 u/s instead of 8000 — every shot led 416× too far ahead.
  • FIRE_RANGE was 5000, past the range at which the shells expire.

But the outcome metric says none of this has been shown to help. The HUD's own counters — YOU KILLED: WARSHIPS / WARPLANES — read 0000 / 0000 at the end of every run, including the nearest-fighter baseline. The pilot is not killing anything in any configuration, so "fraction of frames with the guns on" (12 % → 5 % → 1 frame in 2639 as the firing gate was varied) was never measuring lethality. The corrections above are right on the physics and fix demonstrably wrong code; they are not evidence of improvement, and none is claimed.

The firing gate itself produced one clean result worth keeping: gating on the target's angular half-size alone (2.7° at 2584 units for a fighter) is far tighter than the 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.

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

  • Defeat conditions for an escort stage are readable as: protected-asset hull ≤ 0, or player hull ≤ 0.
  • Every unit's effective HP is the definition's HP, confirmed live for 7 classes — the same field the unit struct already solves statically, so disc data and runtime agree.
  • Entity removal on death is observable (the object leaves the heap), which gives a clean lifetime signal for anything modelling spawn/despawn.