Measured about the FORWARD axis (roll_axis.py), which is the fix the withdrawal
specified: express the new up-vector in the old (up, right) basis and take
atan2(u.w_old, u.u_old), so the component along forward -- what pitch produces --
is dropped by construction. The old probe watched a non-forward matrix row, which
sees any rotation that moves it, and that is why two different stick axes produced
the same numbers.
Stage 02, file pad, single-axis holds (lx=32767 with every other channel exactly
0, trigger held in the same write since the pad state is written whole), 5 s
settles, 8 s dwells, each phase bracketed by HUD-clock screenshots:
min speed (LT) 1 856.9 deg / 8.01 s wall, clock x1.247 -> 185.9 deg/game-s
vs AV_Roll_Min 200
max speed (RT) 1 181.2 deg / 8.02 s wall, clock x1.218 -> 120.9 deg/game-s
vs AV_Roll_Max 125
So roll behaves exactly like pitch: the rate cap falls as speed rises and
_Min/_Max mean "at minimum / at maximum speed". The withdrawn claim that roll
shows NO speed dependence is reversed, and the axis rule is now confirmed on a
second axis instead of contradicted by it.
Both land just under their caps (93% and 97%), the right side for a rate limit.
The shortfall is NOT explained and no claim is made about it: candidates are the
craft not being exactly at min/max speed after 5 s, and the 20 Hz swept-angle sum
undercounting.
The tell that the old result was broken was two conditions agreeing too well. The
tell that this one is sound is that they disagree in the direction the definitions
predict, on two independently bracketed phases.
Re-running roll with 5 s settles and in-run clock brackets:
this run's clock: TIME 00:34.93 -> 00:45.97 = 11.04 s game in 7.98 s wall = 1.383
min speed: 90.6 deg/wall-s /1.383 -> 65.5 deg/game-s (AV_Roll_Min 200)
max speed: 59.1 /1.383 -> 42.7 (AV_Roll_Max 125)
The corrected numbers are within a few per cent of the PITCH run's 67.8 and 40.9 —
two different stick axes cannot produce the same rates, so the probe is not
separating them. Cause: watching a non-forward matrix row sees any rotation that
moves that row, and pitch moves it as much as roll. The correct measure is rotation
ABOUT the forward axis (project the row onto the plane perpendicular to forward and
track that angle).
So "roll shows no speed dependence, unlike pitch" is withdrawn: it rested on 2 s
settles and a row that mixes axes, and the two runs disagree with each other
(144/150 then, 90.6/59.1 now). AV_Roll_{Min,Max} are not confirmed and the axis
question is open.
The clock ratio is now measured three times in three flights: 1.260, 1.311, 1.383 —
a property of the moment, not the machine, so every rate probe must bracket its own
phases.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
pitch_gametime.py brackets each turn phase with HUD screenshots, so the mission
clock's own advance converts wall seconds to game seconds within the same run:
this run's clock: TIME 00:33.68 -> 00:44.12 = 10.44 s game in 7.96 s wall = 1.311
pitch @ min speed 88.9 deg/wall-s /1.311 -> 67.8 deg/game-s vs AV_PitchMinus_Min 75
pitch @ max speed 53.6 /1.311 -> 40.9 vs AV_PitchMinus_Max 40
Both land on the definition (the slow phase 10% low, consistent with including the
AA_* ramp in an 8 s window), so the clock explanation covers angular motion as well:
every stated rate is per GAME second.
The ratio is not a machine constant — 1.260 in the earlier flight, 1.311 here — so it
must be measured in the same run as whatever it corrects. Bonus: the same shots show
the HUD reading 102 at full LT against MinimumVelocity 100.
Also documents the trap that cost three runs: a killed Canary leaves both its shm
image and its last frame on screen, so a dead emulator looks alive and the scans
report "0 moving triples" like a tooling bug. pgrep -x matches zombies, so
speed_law.require_live_emulator() checks the process state letter and refuses to
measure a corpse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
Cleanest linear measurement: neutral throttle (HUD = CruisingVelocity 350), sticks
centred, 20 s of perfectly straight flight (displacement/path = 1.000):
8 900 world units in 20.1 s -> 443.6 /s -> 1.267x the HUD's 350
And the game's own mission timer across a wall-clock interval:
TIME 00:08.79 -> 00:46.97 = 38.18 s of game time in 30.29 s wall = 1.260
Same number. So the linear discrepancy is not a unit difference: the mission clock
runs ~1.26x faster than wall time under this emulator, and dividing world
displacement by WALL seconds inflates speed by exactly that. World units and
displayed speed share one unit; the definition velocities are per GAME second.
This supersedes the previous "world-unit vs displayed-speed" reading.
Left open (❔): settled turn rates measured 74.9/41.1 deg/s in wall time against
AV_PitchMinus_Min/Max 75/40, but the clock argument predicts ~94 for the first.
Either that agreement was luck inside a noisy sample (per-window rates spanned
61-96) or angular integration is frame-based where linear is time-based. The check
is to re-measure pitch and convert wall->game seconds with the clock ratio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
Screenshotting the HUD speed readout at each throttle step, beside the
position-derived measurement of the same moment:
RT 0.00 HUD 350 (= CruisingVelocity) position ~447 ratio 1.28
RT 0.25 HUD 507 position ~652 ratio 1.29
RT 0.75 HUD 963 position ~1141 ratio 1.19
So (a) the HUD speaks the definition's units — exactly CruisingVelocity at neutral,
963 at three-quarters against the 987 the interpolation predicts — confirming the
throttle law in the game's own numbers without any position sampling; and (b) world
displacement runs ~1.2x the displayed speed. Since settled angular rates need no such
factor, this is a unit difference between the position triple and the velocity
fields, not a clock effect: a reimplementation moving entities at MaximumVelocity in
world coordinates will be ~20% slow.
Also fixes speed_law.find_player: a mission holds more than one *_Player object and
at least one never moves, so the finder now samples each candidate twice and keeps
the one that displaces. Locking onto the static one is what produced a run of exact
zeros while the game was visibly flying.
🟡 The ratio is 1.19-1.29 rather than a clean constant and every sample was taken in
a firefight; pinning it wants a quiet map.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
Re-measured with 5 s of settle per phase and the speed recorded at the moment the
turn starts (flight_law3.py):
pitch @ 130/s 74.9 deg/s vs AV_PitchMinus_Min 75
pitch @ 1821/s 41.1 vs AV_PitchMinus_Max 40
roll @ 110/s 129.5 vs AV_Roll_Min 200
roll @ 1722/s 149.3 vs AV_Roll_Max 125
Pitch lands on the definition's own numbers with NO scale factor, so the ~1.2x I
attributed to the emulated time base two iterations ago was an artefact of
differentiating during the AA_* acceleration ramp with too little settle. That
explanation is withdrawn: AV_* can be used verbatim.
What remains is only on the linear side — settled speeds still read high and vary
between runs (RT full: 1342 in one flight, 1821 in another, vs MaximumVelocity 1200),
consistent with a craft being shoved around in a firefight. The HUD reads exactly
CruisingVelocity at neutral. A clean linear measurement needs a quiet map; no cause
is claimed until then.
Roll re-measured with proper settles confirms the axis difference: no speed
dependence, both regimes near AV_Roll_Max, where pitch moved 75 -> 40.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
One flight, two measurements (flight_law2.py, binding early so the moving-craft scan
can see the player).
LT curve: 436, 379, 289, 209, 126 units/s across LT 0.00 -> 1.00 — a straight ramp,
whose endpoints after the ~1.2 time-base factor are CruisingVelocity 350 and
MinimumVelocity 100. So the law is symmetric:
RT: target = Cruising + RT * (Maximum - Cruising)
LT: target = Cruising - LT * (Cruising - Minimum)
Roll (measured on a non-forward matrix row, since roll turns about the forward axis):
~144 deg/s at minimum speed and ~150 at maximum — no speed dependence, where pitch
dropped by a third to a half between the same regimes. After the time-base factor
that is ~121, i.e. AV_Roll_Max 125 in BOTH regimes.
So _Min/_Max does not mean the same thing for every axis: pitch interpolates with
speed, roll appears pinned at Max. A reimplementation applying one rule to all axes
would get low-speed roll wrong by ~60%.
Caveat recorded: the two roll phases were 2 s of settling apart, marginal for a
126 -> 1342 speed change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
RT is an analogue trigger, so "held" was one point on a curve. Walking it 0.00 ->
1.00 (throttle_curve.py) gives a straight ramp: 438, 626, 879, 1094, 1342 units/s.
Dividing by the ~1.2 time-base factor, the endpoints land on the definition's own
numbers (365 vs CruisingVelocity 350; 1118 vs MaximumVelocity 1200) and the midpoint
follows, so
target speed = CruisingVelocity + RT * (MaximumVelocity - CruisingVelocity)
which refines the earlier "selects one of three targets" reading: those three are the
curve's endpoints.
It also refutes the standing afterburner hypothesis that full RT is the burner: the
curve is smooth through full deflection with no step, and the shield does not move.
The LT half is not measured yet — the entity scan needs the craft moving when it
runs, so a mission left idling drops out of it. Bind early.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
The definition describes the burner (AB_ConsumeShield_Begin 50, AB_ConsumeShield 10,
AB_AV_* turn caps well below normal) but names no input, and carries no AB velocity
field.
Three probes, all negative for A, B, X, LB (plus LS/RS on the first):
- ab_probe.py: hold RT for a max-speed baseline, then each candidate — speed stayed
inside the baseline's own noise band every time.
- ab_state_probe.py: sample a window of the player object during each hold and
report any float that falls — nothing fell.
- HUD oracle needing no offsets: count green pixels of the SHIELD bar on a freshly
spawned craft. AB_ConsumeShield_Begin 50 should take a visible bite; the bar read
156/156/156/157/157 across baseline and all four buttons.
So the burner needs a chord, an input this pad cannot reach, or belongs to another
craft/the AI. Recorded so the obvious buttons are not re-probed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
turn_law.py pins the speed regime with a throttle, holds a stick axis and
differentiates the craft's own forward vector over 1-second windows.
slow + nose down ~87 deg/s (AV_PitchMinus_Min 75)
fast + nose down ~54 (AV_PitchMinus_Max 40)
slow + nose up ~175 (AV_PitchPlus_Min 150)
fast + nose up ~136 (AV_PitchPlus_Max 70)
So agility falls with speed (_Min/_Max are at minimum/maximum speed, not rate
bounds) and pitching up is ~2x pitching down, exactly as the field pairs say.
Control mapping measured: LX is roll (forward vector barely moves, 3-5 deg/s), LY is
pitch (+1 = nose down per vgamepad's LY: -1 = up), and the right stick does not steer
at all.
The ~1.2x overshoot seen in the speed law appears again here (1.16-1.35x), and a
unit scale cannot explain both m/s and deg/s — a TIME BASE can: if the guest's
simulated second is shorter than the wall-clock second the probe measures against,
every rate reads high by the same factor. So the definition numbers are
self-consistent and these measurements confirm the shape of the law, not a scale.
Recorded 🟡: no yaw input found (AV_Yaw_* exists but neither stick yaws), which with
roll on LX and MaximumBank_Normal points at a bank-to-turn model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
speed_law.py locks onto the player entity once and samples its position while
holding each throttle input, differentiating over 1-second windows.
no throttle -> ~420 (CruisingVelocity 350)
RT held -> ~1 530 (MaximumVelocity 1200)
LT held -> ~125 (MinimumVelocity 100)
release -> back to cruise, from either direction
So the throttle SELECTS a target speed rather than adding thrust — which is what a
reimplementation would most likely have assumed from Acceleration/Deceleration
alone. Those govern the convergence rate instead: ~440 units/s^2 measured on
release (Deceleration 500) and ~470-560 under RT (Acceleration 600).
Recorded as 🟡: measured world speeds run ~1.2-1.3x the definition numbers in all
three regimes while the HUD shows the definition value exactly (350 at cruise), so
world coordinates are a constant multiple (~1.25) of the definition's velocity unit;
the spread is wider than the constant is precise because the craft manoeuvres while
sampled.
Three traps documented: RT/LT are analogue triggers (the button verb is a silent
no-op and the first run measured an unflown craft), per-sample differentiation
aliases against the guest's update rate (0, 1519, 1985, 0, 2681 for smooth flight),
and the player entity only enters the typed scan ~15 s in while the craft dies within
minutes if nobody flies it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9