# The throttle is a TARGET-SPEED selector β€” measured against the definition (2026-08-13) **Status: βœ… for the shape of the law, 🟑 for the unit scale.** The unit definition gives `MinimumVelocity 100`, `CruisingVelocity 350`, `MaximumVelocity 1200`, `Acceleration 600` and `Deceleration 500` for `UN_f001_TCAF_DeltaSaber_T_Player` ([values](captures/unit-runtime-fields.csv)), but not **how** the game applies them. This measures it. Method β€” [`tools/re-capture/speed_law.py`](../../tools/re-capture/speed_law.py): lock onto the player entity once, then sample its own position triple in guest RAM while holding each throttle input in turn (8 s per phase, 20 Hz). Speed is differentiated over **1-second windows**, never per sample. ## Result | phase | measured speed (1 s windows) | settles to | definition field | |---|---|---|---| | no throttle | 367 403 471 365 463 463 365 | **~420** | `CruisingVelocity` 350 | | `RT` held | 1041 1376 1551 1379 1596 1510 1620 | **~1 530** | `MaximumVelocity` 1200 | | release | 1314 672 432 452 432 450 408 | back to **~440** | β€” | | `LT` held | 203 121 135 121 115 146 122 | **~125** | `MinimumVelocity` 100 | | release | 369 451 427 425 445 390 465 | back to **~430** | β€” | So the throttle **selects a target speed** and the craft converges to it; releasing either trigger returns it to cruise. (**Refined below**: the trigger is analogue, so the target is a continuous interpolation and these three are its endpoints.) It is not a force model with the throttle adding thrust, which is what a reimplementation would most likely have assumed from `Acceleration`/`Deceleration` alone. Those two fields govern the **convergence rate**: the release phase falls ~1 314 β†’ ~432 in about two seconds (~440 units/sΒ², against `Deceleration` 500) and the `RT` phase climbs ~420 β†’ ~1 550 in two to three seconds (~470–560 units/sΒ², against `Acceleration` 600). ## 🟑 The unit scale Measured world-space speeds run **β‰ˆ1.2–1.3Γ— the definition numbers** in all three regimes (cruise 1.21, maximum 1.28, minimum 1.32). The HUD, meanwhile, reads **350** at cruise β€” the definition value exactly. So the definition's velocity unit is the HUD's, and entity world coordinates are a constant multiple of it, close to **1.25**. The spread across regimes is larger than the constant itself is precise, because the craft is manoeuvring under fire while sampled (straight-line displacement per window under-reads a curving path), so this is recorded as a measured range rather than a pinned constant. ## Traps * **`RT`/`LT` are analogue triggers**, so `vgamepad trig RT 1.0` holds the throttle; the button verb `hold RT` is a **silent no-op**. The first run of this probe measured the drift of a craft nobody was flying. * **Do not differentiate per sample.** The guest updates the position slower than 20 Hz, so per-sample differences alternate between 0 and a double step β€” `0, 1519, 1985, 0, 2681…` for a craft flying smoothly. * **Bind late, measure fast.** The player entity is not in the typed-entity scan for the first ~15 s of a mission, and the craft dies within a few minutes if nobody is flying it β€” one earlier attempt ended at `GAME OVER` mid-probe. Raw samples: [`captures/speed-law-throttle-phases.csv`](captures/speed-law-throttle-phases.csv). --- # Turn rates: `AV_*` are rate caps, and `_Min`/`_Max` mean *at minimum/maximum speed* `tools/re-capture/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. **Control mapping, measured** β€” `LX` is **roll** (the forward vector barely moves under it: 3–5 Β°/s residual), `LY` is **pitch** (`vgamepad` documents `LY: -1 = up`, so `+1` is nose down = the `PitchMinus` family), and the **right stick does not steer at all** (0 Β°/s). | phase | measured (1 s windows) | definition | |---|---|---| | slow + nose down | 85 Β· 96 Β· 82 β†’ **~87** | `AV_PitchMinus_Min` **75** | | fast + nose down | 55 Β· 45 Β· 63 β†’ **~54** | `AV_PitchMinus_Max` **40** | | slow + nose up | 181 Β· 164 Β· 181 β†’ **~175** | `AV_PitchPlus_Min` **150** | | fast + nose up | 103 Β· 130 Β· 175 β†’ **~136** | `AV_PitchPlus_Max` **70** | Two claims of the field names are confirmed: * **Agility falls with speed** β€” every rate drops when the throttle goes from `LT` to `RT`, so `_Min`/`_Max` are *at minimum / maximum speed*, not floor/ceiling of the rate. * **Pitching up is about twice as fast as pitching down**, exactly as the pair 150/75 (slow) and 70/40 (fast) says. ## The ~1.2Γ— factor β€” ⚠️ WITHDRAWN, see "Settled rates match exactly" below The speed law measured 1.21 / 1.28 / 1.32 Γ— its definition values; the pitch rates measure 1.16 / 1.35 / 1.17 / 1.94 Γ— theirs. A **unit scale** would have to differ between metres-per-second and degrees-per-second β€” it cannot explain both. A **time base** does: if the guest's simulated second is shorter than the wall-clock second this probe measures against, every rate reads high by the same factor. So the definition numbers are self-consistent and a reimplementation should take them at face value; these measurements confirm the **shape** of the law (which field governs what, and how the regimes switch), not a scale correction. 🟑 **No yaw input was found.** `AV_Yaw_{Min,Max}` (45 / 25 Β°/s) exists, but neither stick yaws the craft. With `MaximumBank_Normal` (60) and roll on `LX`, a **bank-to-turn** model is the obvious reading β€” sustained turns come from rolling and pitching β€” but the yaw fields may equally belong to the AI or to an input this probe has not driven. Raw samples: [`captures/turn-law-pitch-phases.csv`](captures/turn-law-pitch-phases.csv). --- # The afterburner is not on the obvious buttons β€” a bounded negative The definition describes the burner without naming its input: `AB_ConsumeShield_Begin` **50**, `AB_ConsumeShield` **10**, and a set of `AB_AV_*` turn caps far below the normal ones (roll 40 vs 125–200 Β°/s, pitch-up 30 vs 70–150). Notably there is **no `AB_*Velocity`**, so the burner may not raise the speed target at all. Two independent probes, both negative for `A`, `B`, `X`, `LB` (and `LS`/`RS` on the first): * [`tools/re-capture/ab_probe.py`](../../tools/re-capture/ab_probe.py) β€” hold `RT` for a max-speed baseline, then hold each candidate: speed stayed inside the baseline's own noise band (1 200–1 580 units/s) for every one. * [`tools/re-capture/ab_state_probe.py`](../../tools/re-capture/ab_state_probe.py) β€” sample a window of the player object while each candidate is held and report any float that falls during the hold: **nothing fell**. * And the cheapest oracle of the three, needing no offsets at all: **count the green pixels of the HUD's SHIELD bar** before and after each hold, on a freshly spawned craft with a full shield. `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 is **not a simple hold on `A`/`B`/`X`/`LB`/`LS`/`RS`**. What remains: a chord (something plus `RT`), an input this pad cannot reach, a craft variant that has it, or fields that belong to the AI rather than the player. Worth knowing before anyone re-probes the obvious buttons. (`RB` is the nose gun, `Y` the missile mount and the d-pad the tactical map β€” see [flight controls](flight-controls-runtime.md) β€” so those were not held here.) --- # The throttle is ANALOGUE: the target speed interpolates cruise β†’ maximum `RT` is an analogue trigger, so "held" was only ever one point on a curve. Walking it in five steps ([`tools/re-capture/throttle_curve.py`](../../tools/re-capture/throttle_curve.py), 2.5 s to converge then 5 s of sampling per step): | `RT` | settled speed (1 s windows) | mean | |---|---|---| | 0.00 | 465 Β· 411 Β· 373 Β· 504 | **438** | | 0.25 | 648 Β· 554 Β· 678 Β· 622 | **626** | | 0.50 | 739 Β· 961 Β· 897 Β· 919 | **879** | | 0.75 | 1028 Β· 1086 Β· 977 Β· 1283 | **1094** | | 1.00 | 1359 Β· 1306 Β· 1517 Β· 1186 | **1342** | A straight ramp. Dividing by the ~1.2 time-base factor established above, the endpoints land on the definition's own numbers β€” 438/1.2 β‰ˆ **365** against `CruisingVelocity` **350**, and 1342/1.2 β‰ˆ **1118** against `MaximumVelocity` **1200** β€” and the midpoint follows: 879/1.2 β‰ˆ 733 against the predicted 350 + 0.5Β·(1200βˆ’350) = 775. So ``` target speed = CruisingVelocity + RT Β· (MaximumVelocity βˆ’ CruisingVelocity) ``` and `LT` mirrors it down to `MinimumVelocity` β€” **measured**, see below. **This also refutes a live hypothesis about the afterburner.** Full `RT` producing more than `MaximumVelocity` looked like it might *be* the burner; it is not β€” the curve is smooth through full deflection, with no step, and the shield does not move. Full throttle is simply full throttle. Raw samples: [`captures/throttle-curve-rt.csv`](captures/throttle-curve-rt.csv). (The entity scan that locks onto the player searches *changing* position triples, so it only sees the craft while it still has speed β€” bind early, or a mission left idling drops out of the scan entirely and every tool reports "player entity not found" while the game is visibly flying.) --- # `LT` mirrors `RT`, and roll does NOT depend on speed One flight, both measurements ([`tools/re-capture/flight_law2.py`](../../tools/re-capture/flight_law2.py)). ## The braking half of the curve | `LT` | 0.00 | 0.25 | 0.50 | 0.75 | 1.00 | |---|---|---|---|---|---| | speed | 436 | 379 | 289 | 209 | **126** | A straight ramp down from cruise to ~126, and dividing by the ~1.2 time-base factor the endpoints are the definition's own numbers again β€” 363 against `CruisingVelocity` **350** and 105 against `MinimumVelocity` **100**. So the throttle law is symmetric: ``` RT held: target = Cruising + RT Β· (Maximum βˆ’ Cruising) LT held: target = Cruising βˆ’ LT Β· (Cruising βˆ’ Minimum) ``` ## Roll Roll turns the craft about its own forward axis, so it has to be measured on a different row of the rotation matrix than pitch was: | phase | rate per 1 s window | mean | |---|---|---| | minimum speed + full roll | 171 Β· 103 Β· 157 | **~144 Β°/s** | | maximum speed + full roll | 140 Β· 145 Β· 164 | **~150 Β°/s** | **Roll shows no speed dependence** β€” the two regimes agree inside the noise, where pitch dropped by a third to a half between them. Against the definition (`AV_Roll_Min` 200, `AV_Roll_Max` 125) the measured ~145 Β°/s is ~121 after the time-base factor, i.e. **the `Max` value in both regimes**. So the `_Min`/`_Max` pair does *not* mean the same thing for every axis: pitch interpolates between them with speed, roll appears pinned at `Max`. A reimplementation that applies one rule to all axes would get roll wrong at low speed by ~60 %. 🟑 Caveat kept: the roll phases were 2 s of settling apart, which is marginal for a full 126 β†’ 1 342 speed change, so "no dependence" is a measurement over a real but not perfectly separated pair of regimes. Raw samples: [`captures/throttle-curve-lt.csv`](captures/throttle-curve-lt.csv) Β· [`captures/turn-law-roll.csv`](captures/turn-law-roll.csv). --- # ⚠️ Settled rates match the definition EXACTLY β€” the "time base" reading is withdrawn The earlier turn measurements settled each phase for 1.5 s and then differentiated from the first second. Re-running with **5 s of settle** and the craft's speed recorded *at the moment the turn starts* ([`tools/re-capture/flight_law3.py`](../../tools/re-capture/flight_law3.py)): | phase | speed at turn | measured rate | definition | |---|---|---|---| | pitch, minimum speed | 130 /s | **74.9 Β°/s** | `AV_PitchMinus_Min` **75** | | pitch, half throttle | 388 /s | 73.7 Β°/s | (between, see below) | | pitch, maximum speed | 1 821 /s | **41.1 Β°/s** | `AV_PitchMinus_Max` **40** | | roll, minimum speed | 110 /s | 129.5 Β°/s | `AV_Roll_Min` 200 | | roll, maximum speed | 1 722 /s | 149.3 Β°/s | `AV_Roll_Max` 125 | **Pitch lands on 75 and 40 β€” the definition's own numbers, with no scale factor.** So the ~1.2Γ— I attributed to the emulated time base was an artefact of measuring during the `AA_*` acceleration ramp with too little settle, and that explanation is **withdrawn**: angular rates need no correction, and a reimplementation should use `AV_*` verbatim. What remains unexplained is only on the **linear** side: settled speeds still read high and vary between runs (`RT` full measured 1 342 in one flight and 1 821 in another, against `MaximumVelocity` 1 200), which is what a craft being shoved around in a firefight looks like. The HUD, meanwhile, reads exactly `CruisingVelocity` at neutral throttle. A clean linear measurement wants a quiet corner of a map, not this one β€” until then, no cause is claimed. **Roll, re-measured with proper settles, confirms the axis difference**: 129.5 at 110 /s and 149.3 at 1 722 /s β€” no speed dependence, both near `AV_Roll_Max` **125**, where pitch moved 75 β†’ 40 across the same range. The mid-throttle pitch point does not discriminate lerp-versus-switch, because at half throttle the craft only reached 388 /s, where an interpolation would predict ~66 Β°/s against a measured 73.7 β€” inside this probe's noise. Raw samples: [`captures/turn-law-settled.csv`](captures/turn-law-settled.csv). --- # The linear discrepancy is real, and it is between WORLD units and DISPLAYED speed Last section removed the "time base" explanation by showing settled *angular* rates match the definition exactly. That leaves the linear side, and the HUD settles it: screenshotting the speed readout at each throttle step, next to the position-derived measurement of the same moment ([`captures/throttle-curve-hud.csv`](captures/throttle-curve-hud.csv)): | `RT` | HUD readout | position-derived | ratio | |---|---|---|---| | 0.00 | **350** (= `CruisingVelocity`) | ~447 | 1.28 | | 0.25 | **507** | ~652 | 1.29 | | 0.75 | **963** | ~1 141 | 1.19 | Two things follow. * **The HUD speaks the definition's language.** It reads exactly `CruisingVelocity` at neutral and climbs toward `MaximumVelocity` as the trigger is pressed β€” 963 at three-quarters, against the 987 the interpolation predicts. So the throttle law stated above is confirmed in the game's *own* units, independent of any position sampling. * **World displacement runs ~1.2Γ— the displayed speed.** Since the angular rates need no such factor, this is not a clock effect: it is a **unit difference between the position triple and the velocity fields**. A reimplementation that places entities in world coordinates and moves them at `MaximumVelocity` will be ~20 % slow. 🟑 The ratio is 1.19–1.29 across the three points rather than a clean constant, and every sample was taken in a firefight where the craft is also being pushed. Pinning it exactly wants a quiet map or a scripted straight run; the *existence and rough size* of the factor is what these measurements support. **A methodological note worth keeping.** This line of work went: hold a trigger β†’ "three target speeds" β†’ analogue interpolation β†’ an unexplained 1.2Γ— β†’ "time base" β†’ withdrawn β†’ a *unit* difference confined to the linear side. Every step came from one of two moves: **let a held input vary** (the trigger's analogue range), or **bring in an independent oracle** (the HUD, the definitions, a longer settle). The wrong turn β€” the time base β€” came from explaining a number instead of first measuring the same quantity a second, cleaner way. --- # βœ… The linear factor IS the clock β€” measured against the game's own mission timer The cleanest possible version of the linear measurement: neutral throttle (HUD reads exactly `CruisingVelocity` 350), sticks centred, **20 s of perfectly straight flight** (`straightness = displacement/path = 1.000`, so no manoeuvring contaminates it) β€” [`tools/re-capture/cruise_ratio.py`](../../tools/re-capture/cruise_ratio.py): ``` displacement 8 900 world units in 20.1 s -> 443.6 /s ratio vs the HUD's 350 -> 1.267 ``` And the game's **own clock**, read off the HUD across a wall-clock interval: ``` mission TIME 00:08.79 -> 00:46.97 = 38.18 s of game time wall clock = 30.29 s ratio = 1.260 ``` **1.260 against 1.267 β€” the same number.** So the linear "discrepancy" was never a unit difference: **the mission clock runs ~1.26Γ— faster than wall time under this emulator**, and a speed derived by dividing world displacement by *wall* seconds is inflated by exactly that. World units and the displayed speed share one unit, and a reimplementation should take `CruisingVelocity`/`MaximumVelocity` at face value β€” per **game** second. This supersedes the previous section's "world-unit vs displayed-speed difference". ❔ **One thing does not fit, and is left open.** The settled turn rates measured 74.9 and 41.1 Β°/s *in wall time* against `AV_PitchMinus_{Min,Max}` 75/40 β€” but if game time runs 1.26Γ— fast, a 75 Β°/game-second cap should have read ~94 Β°/wall-second. Either that agreement was luck inside a noisy sample (the per-window rates spanned 61–96), or angular integration is frame-based where linear is time-based. The check: re-measure pitch and convert wallβ†’game seconds with the clock ratio, expecting ~94 if the clock explanation is universal. --- # βœ… The clock factor applies to the ANGULAR side too β€” and it varies per run The open question was whether the ~1.26Γ— mission-clock factor explains only the linear measurements. Measuring **both in the same run** settles it ([`tools/re-capture/pitch_gametime.py`](../../tools/re-capture/pitch_gametime.py): HUD screenshots bracket each turn phase, so the mission clock's own advance converts wall seconds to game seconds): ``` this run's clock: TIME 00:33.68 -> 00:44.12 = 10.44 s game in 7.96 s wall = 1.311 ``` | phase | wall-time rate | Γ· 1.311 β†’ game-time | definition | |---|---|---|---| | pitch, minimum speed | 88.9 Β°/s | **67.8 Β°/s** | `AV_PitchMinus_Min` **75** (0.90Γ—) | | pitch, maximum speed | 53.6 Β°/s | **40.9 Β°/s** | `AV_PitchMinus_Max` **40** (1.02Γ—) | So with the clock correction both land on the definition (the slow phase runs 10 % low, consistent with the `AA_*` ramp being included in an 8 s window). **The clock explanation is universal**: every rate the game states is per **game** second, and a probe that divides by wall seconds reads high by that run's ratio. **And the ratio is not a constant of the machine β€” it varies with scene load**: 1.260 in the earlier flight, **1.311** here. It has to be measured *in the same run* as whatever it is correcting, which is why this probe brackets its own phases with HUD screenshots. Bonus confirmation from the same screenshots: at full `LT` the HUD reads **102**, against `MinimumVelocity` **100** β€” the throttle law's lower endpoint, in the game's own units. ## The trap that cost three runs: a dead emulator looks alive A killed Canary leaves **both** its `/dev/shm/xenia_memory_*` image *and* its last frame on the X display. So the screenshot shows a mission in progress, the memory image still parses, and the scans quietly report `0 moving triples` / "player entity not found" β€” which reads like a tooling bug. `pgrep -x xenia_canary` does **not** help: zombies match it. `speed_law.require_live_emulator()` now checks the process *state letter* and refuses to measure a corpse, naming the stale image in its error. --- # ⚠️ The roll result is WITHDRAWN β€” the probe was not isolating roll Re-running the roll phases with 5 s settles and in-run clock brackets ([`tools/re-capture/roll_gametime.py`](../../tools/re-capture/roll_gametime.py)): ``` this run's clock: TIME 00:34.93 -> 00:45.97 = 11.04 s game in 7.98 s wall = 1.383 minimum speed: 90.6 Β°/wall-s Γ· 1.383 -> 65.5 Β°/game-s (AV_Roll_Min 200) maximum speed: 59.1 Β°/wall-s Γ· 1.383 -> 42.7 Β°/game-s (AV_Roll_Max 125) ``` Two things are wrong with reading this as "roll". * The corrected numbers β€” **65.5 and 42.7** β€” are within a few per cent of the **pitch** run's **67.8 and 40.9**. Two different stick axes producing the same rates is the signature of a measurement that is not separating them. * Watching a non-forward matrix row sees **any** rotation that moves that row, and pitch moves it as much as roll does. The forward row was the wrong thing to avoid; what is needed is the rotation **about** the forward axis β€” project the row onto the plane perpendicular to forward and track the angle of that projection. So the earlier conclusion β€” *"roll shows no speed dependence, unlike pitch"* β€” is **withdrawn**. It rested on 2 s settles (which this session has twice shown to produce wrong answers) and on a row that mixes the axes. The two runs do not even agree with each other (144/150 then, 90.6/59.1 now), which is itself the tell. What survives: `AV_Roll_{Min,Max}` are **not** confirmed, and the axis question β€” does roll follow the same speed rule as pitch? β€” is **open**, with the correct measurement specified above. **The clock ratio has now been measured three times in three flights: 1.260, 1.311, 1.383.** It is not a property of the machine but of the moment, so any rate probe must bracket its own phases. ## Roll, re-measured about the forward axis β€” the withdrawal is reversed βœ… The earlier roll result was withdrawn because the *measurement* was wrong, not the input: watching a non-forward row of the rotation matrix sees **any** rotation that moves that row, and pitch moves it as much as roll, which is why two different stick axes produced the same rates. `tools/re-capture/roll_axis.py` measures rotation **about** the forward axis instead β€” express the new up-vector in the old `(up, right)` basis and take `atan2(u_newΒ·w_old, u_newΒ·u_old)`, so any component along forward (what pitch produces) is dropped by construction. Run on stage 02 with the container-safe file pad, which matters here: the pad state is written whole, so the stick is deflected on **exactly one axis** (`lx=32767`, every other channel exactly 0) while the throttle trigger is held in the same write. 5-second settles, 8-second dwells, and each phase bracketed by HUD-clock screenshots because the clock ratio is a property of the moment (1.260/1.311/1.383 previously). | phase | swept | wall | HUD clock | per **game** second | definition | |---|---|---|---|---|---| | min speed (`LT`) | 1 856.9Β° | 8.01 s | 01:20.64 β†’ 01:30.63 (Γ—1.247) | **185.9 Β°/s** | `AV_Roll_Min` **200** | | max speed (`RT`) | 1 181.2Β° | 8.02 s | 01:37.11 β†’ 01:46.88 (Γ—1.218) | **120.9 Β°/s** | `AV_Roll_Max` **125** | **Roll DOES depend on speed**, and the withdrawn claim ("roll shows no speed dependence, unlike pitch") is **reversed**. Roll behaves exactly like pitch: the rate cap falls as speed rises, and `_Min`/`_Max` mean *at minimum / at maximum speed* β€” the same rule the pitch work established, now confirmed on a second axis rather than contradicted by it. Both measurements land **just under** their caps (93 % and 97 %), which is the right side for a rate limit. 🟑 The shortfall is not explained: candidates are the craft not being exactly at min/max speed after 5 s, and the swept-angle sum slightly undercounting at 20 Hz. Neither is worth a claim without measuring it. Method note: the tell that the *old* result was broken was two conditions agreeing too well (pitch β‰ˆ roll). The tell that this one is sound is that they now **disagree in the direction the definitions predict**, on two independently-bracketed phases. Data: [`captures/roll-about-forward-axis.csv`](captures/roll-about-forward-axis.csv). ## Which input drives which axis β€” and the yaw answer βœ… `tools/re-capture/axis_probe.py` holds one channel at a time (file pad, so every other channel is exactly zero) and decomposes the result into **all three** rotation components at once, rather than measuring one axis through a row that moves under any rotation β€” the flaw that once made roll and pitch produce identical numbers. **The row labelling is measured, not assumed.** `entities2` pins row 2 = forward against velocity; the probe pins the other two by comparing **world-Y** in flight: ``` row world-Y means: [0.469, 0.883, -0.000] forward = row 2 -> up = row 1, right = row 0 CONFIDENT ``` That is the **opposite** of the D3D convention an earlier run assumed, which means that run's *yaw* and *pitch* columns were swapped β€” under the correct labels its `ly+` reading of 154.1 Β°/wall-s is **pitch**, which is what a left-stick Y axis should do. | input | roll | yaw | pitch | | |---|---|---|---|---| | `rx` right-stick X | 0.0 | 0.0 | 0.0 | nothing | | `ry` right-stick Y | 0.0 | 0.0 | 0.0 | nothing | | `LB` | 0.0 | 0.0 | 0.0 | nothing | | `RB` | 0.0 | 0.0 | 0.0 | nothing (it is the nose gun) | | `lx` left-stick X | 209.8 | ~0 | ~10 | **roll** | | `ly` left-stick Y | ~0 | ~0 | 154.1 | **pitch** | These zeros are **trustworthy**, unlike the previous attempt's: the four unknown inputs were measured *first*, each passing a liveness check, and the run aborted the moment the craft stopped moving instead of reporting the clean zeros a destroyed craft produces. **So no pad input yaws the craft directly.** `AV_Yaw_*` (45/25) exists in the unit definitions but nothing on the right stick or the shoulders drives it β€” the earlier ❔ *"no input found"* is upgraded from "we could not find one" to **"the remaining candidates measurably do nothing"**. The natural reading is that yaw is a *consequence* of banking rather than a commanded axis, which a reimplementation should model as such. 🟑 Not covered: the **d-pad** (the tactical map) and the **face buttons** (fire / weapon select). Neither is a plausible flight axis, but neither was measured here. Data: [`captures/axis-probe-rows-pinned.csv`](captures/axis-probe-rows-pinned.csv). ## ⚠️ Angular-rate probes measure a MOVING speed β€” the dwell bleeds it Measuring pitch with the rows properly pinned gave, for `UN_f001_TCAF_DeltaSaber_T_Player` (whose own disc values are `AV_PitchPlus_Min` **150Β°**, `AV_PitchPlus_Max` **70Β°**): | phase | swept | clock | per game-s | cap | |---|---|---|---|---| | min speed (`LT`) | 1 391.0Β° / 8.00 s | Γ—1.326 | **131.1** | 150 | | max speed (`RT`) | 989.0Β° / 8.05 s | Γ—1.318 | **93.2** | 70 β€” **133 % of cap** | A rate 33 % *above* a cap is not a finding, it is a broken instrument, so read the HUD speed off the same bracketing screenshots that gave the clock: ``` slow phase 102 -> 18 fast phase 1193 -> 589 ``` **The speed is not constant during the dwell β€” pitching bleeds it hard**, halving it in 8 seconds. The rate cap is itself speed-dependent, so as the craft slowed its cap *rose*, and the 8-second average necessarily lands between the max-speed cap and a mid-speed one. The 133 % is fully explained by the instrument. **This weakens the roll numbers above as well.** They were taken the same way, so `120.9` vs `AV_Roll_Max` `125` is *consistent* but is **not a tight test** β€” the true max-speed roll cap could be lower and still produce that average. The min-speed figures are less affected (there is little speed left to lose). **How to measure it properly** (not yet done): keep the dwell to ~1–2 s so the speed barely moves, or sample the HUD speed continuously and fit rate against *instantaneous* speed rather than assuming the endpoint. The second is strictly better and gives the whole rate-vs-speed curve rather than two points. **And a flight-model finding in its own right:** *turning costs speed*, steeply β€” 1 193 β†’ 589 under 8 s of full pitch, with the throttle still held at maximum. A reimplementation that treats the throttle as setting a speed the craft simply holds will be wrong during manoeuvres. Data: [`captures/pitch-rate-speed-bleed.csv`](captures/pitch-rate-speed-bleed.csv). ## ⚠️ Polling faster than the guest updates manufactures a curve Trying to fit rate against *instantaneous* speed (`rate_curve.py`, one long hold so the speed-bleed sweeps the range) produced a beautifully clean result that is entirely an artefact: ``` speed 0- 599 rate 29.4 speed 1199-1798 rate 279.8 speed 599-1199 rate 174.9 speed 1798-2398 rate 317.5 ``` Rate rising with speed, and speeds to **4 795** when the craft's maximum is 1 200. The cause: **20 Hz polling is faster than the guest updates these fields**, so a per-read delta is either exactly zero (no update yet) or a whole frame's worth divided by a fraction of a frame. In that run **111 of 352 reads were zero on BOTH channels** β€” position and attitude update on the same frame, so the two are perfectly correlated, and dividing each by the short wall `dt` produced a tidy correlation out of nothing. **Fix: aggregate over windows spanning many frames** (0.5 s here). The sum of `|Ξ”|` over a window is right however the updates fall inside it. **This does NOT affect the swept-total probes** (`roll_axis.py`, `rate_probe.py`): they already summed over the whole dwell, which is immune for the same reason. Only per-sample instantaneous rates were ever wrong. 🟑 **The windowed re-run is not yet a result.** It gives plausible magnitudes (speed 320–935, rate 80–149 Β°/wall-s) but still shows rate *rising* with speed, against the definition's `AV_PitchPlus_Min` 150 > `_Max` 70. Two disqualifiers: it ran on an instance where the craft was already tumbling from the previous sweep, so the row pinning reported **WEAK β€” craft may not be level**, and the starting speed was mid-range rather than maximum. A clean answer needs a **fresh flight**, pinning CONFIDENT, one sweep, nothing before it. Recorded as an open question rather than a finding β€” the definitions' `_Min`/`_Max` meaning is exactly what is in doubt, so a measurement taken through a doubtful instrument cannot settle it. Data: [aliased, for reference](captures/rate-curve-aliased-BAD.csv) Β· [windowed](captures/rate-curve-windowed.csv). ## The clean sweep: magnitudes agree, the LAW does not follow 🟑 Fresh flight, row pinning **CONFIDENT** (margin 0.413), one sweep and nothing before it β€” the conditions the previous attempt lacked. Clock 01:16.08 β†’ 01:41.46 (Γ—1.26). Binned by speed, both converted to game units: | speed (game) | measured Β°/game-s | linear interpolation of the caps | |---|---|---| | ~435 | 100.8 | 125.6 | | ~572 | 113.8 | 115.7 | | ~709 | 126.3 | 105.7 | | ~846 | 83.1 | 95.7 | | ~983 | 72.7 | 85.8 | (prediction = `AV_PitchPlus_Min` 150 at `MinimumVelocity` 100 β†’ `_Max` 70 at `MaximumVelocity` 1200, interpolated linearly.) **What this supports:** the magnitudes are right β€” measured 73–126 Β°/game-s across speeds 400–1 050 against a predicted 86–126 β€” and the high-speed end falls, as a speed-dependent cap should. **What it does not support:** the interpolation *law*. The scatter is Β±25 %, the two fastest bins hold only 1 and 2 windows each (they are the first moments before the speed bled), and the slowest bin disagrees in the wrong direction (100.8 measured vs 125.6 predicted). A sweep that is *driven* by the speed bleeding cannot spend long at either extreme, which is exactly where the law is most testable. **The design that would settle it**, and why this one cannot: hold a *settled* throttle, pitch for only **~1 second**, and read the rate β€” the speed barely moves inside a 1 s burst, so each burst yields one honest `(speed, rate)` point. Repeat at `LT` / neutral / `RT` for three clean points at known speeds, instead of one smeared sweep. Data: [`captures/pitch-rate-curve-clean.csv`](captures/pitch-rate-curve-clean.csv). ## βœ… Short bursts at settled speeds: the law's SHAPE is confirmed, clock-free The design the sweep could not provide: settle the throttle, measure the settled speed, then pitch for **one second** so the speed barely moves inside the burst. Three throttle settings, two repeats each, row pin **CONFIDENT**, fresh flight. | throttle | burst speed (wall) | rate (Β°/wall-s) | |---|---|---| | `LT` min | ~105 | 113.6, 109.5 | | none, cruise | ~383 | 100.2, 88.4 | | `RT` max | ~1 483 | 52.2, 70.5 | **Rate falls monotonically with speed** β€” 111.5 β†’ 94.3 β†’ 61.4 β€” measured at three *known, settled* speeds rather than smeared across a bleeding one. **And the decisive comparison needs no clock at all.** Absolute rates depend on the run's clock ratio, but the **min:max ratio cancels it**: ``` measured min:max = 1.82 AV_PitchMinus_Min/Max 75/40 = 1.88 -> 3.0 % apart AV_PitchPlus_Min/Max 150/70 = 2.14 -> 15.1 % apart ``` So two things follow, neither resting on a clock measurement: 1. **`_Min` / `_Max` really do mean "at minimum / at maximum speed"**, with the rate interpolating between them β€” the shape is confirmed to 3 %. 2. **`ly+` drives pitch-MINUS**, not pitch-plus. The craft has asymmetric pitch authority (75/40 down versus 150/70 up), and the ratio picks the pair cleanly. 🟑 **Absolute magnitudes are still open** β€” this run did not bracket the HUD clock, so deg/*game*-second cannot be computed from it, and choosing a ratio that makes the numbers fit would be circular. The probe now takes clock screenshots at both ends so the next run closes it. Data: [`captures/pitch-burst-settled-speeds.csv`](captures/pitch-burst-settled-speeds.csv).