# Controls, keys and timing

[Open the controls viewer](index.html).

The original has separate input paths for continuous axes and discrete key commands. `tools/export_controls.py` executes each registration function and records the key code, callback, source unit and flags. `tools/check_controls_native.py` independently implements the keyboard ramps and joystick response and compares them with original execution.

## Keyboard axes

`initialise_windows_timer_` at `0x0047e834` requests a 10 ms multimedia timer. Held keys accumulate counts in a 128-element array at `0x0072f490`; held state is a separate byte array at `0x0072f410`.

For each flight axis, the control update adds or subtracts `count << 25`, clamped to `±0x7fffffff`. Reversing direction first clears the previous sign. Releasing the key clears the axis. Positive input wins if both opposing keys are held. Aliases use their maximum count, not their sum. Counts are consumed by the update. Full deflection takes 64 timer interrupts, or 0.64 seconds.

Q/A and keypad +/− control collective. The arrows control cyclic, and Z/X or Insert/Delete control tail rotor. W/S change Hydra elevation in gunner mode. Period/comma control magnification. Pilot mode clears the gunner elevation input.

External-view routing depends on `_view_moves`: value 2 routes these axes to the view; value 1 does so while Alt or the joystick's alternate button is held. Flight inputs retain their previous values while routed to the external view. The [routing report](native-control-check.json) covers both routes.

## Joysticks and calibration

`setup_apache_controls_` resolves the selected cyclic, collective and tail-rotor devices into input capabilities. Cyclic value 0 selects keyboard; 1 selects joystick; 2 selects an analogue coolie hat; 3 and 4 select the supported special joystick button layouts. Collective value 6 enables an absolute throttle, and tail-rotor value 5 enables rudder. Invalid or conflicting choices can be reset by this setup routine.

The curved joystick path uses a symmetric 256-entry table. With deadband `d`, the integer step is `floor(0xb504 / (127 − d))`. For `i = d ... 127`, entry `128+i` is `((i−d) × step)²` and entry `127−i` is its negative. Calibration first maps the raw value to a table index, using independent lower and upper half-ranges around the measured centre.

The native check covers five deadbands, all 256 lookup values and all four axes across their calibrated ranges: 6,400 comparisons. Keyboard ramps and routing have another 14,000 comparisons over 1,000 generated input frames.

## Key registrations

The [key table](keys.json) contains 102 registrations from the normal initial state and three conditional model-cheat registrations. Modifier bits are `0x1000` Alt, `0x2000` Ctrl and `0x4000` Shift. `0x10000` ignores modifiers and `0x20000` requests repeat behaviour.

`initialise_keys_` calls `register_debug_keys_` as part of normal setup. That group includes screenshot, visual-detail and drone-view controls. Three additional model-cheat commands are registered only when `_model_cheats_enabled` is set; the table marks those explicitly. The vector flight model is a separate development path, not the menu's realistic or arcade choice. Flight-model-specific groups and multiplayer groups should be read in their corresponding contexts.

Tab cycles time acceleration through 1×, 2×, 4× and 8×. Shift+Tab selects 8×; Ctrl+Tab and Escape restore 1×. Ctrl+Esc, Ctrl+Q and Alt+X call the game-loop quit handler. P calls the pause handler. This differs from the Escape behaviour in TFX.

## Simulation time

The game clock runs at 200 ticks per second. `_frame_time` is elapsed ticks × 0.005 seconds. The rendering and gameplay loop can run at a variable rate. Flight updates are subdivided when the fixed-point delta exceeds `0x3fffffff`, approximately half a second, while weapons and other systems retain the full frame delta. Time acceleration repeats the simulation loop.

The reference scenarios currently use ten clock ticks per frame, or 20 Hz, so no subdivision is needed. These fixed test steps make repeated state sequences comparable; they do not replace the original variable-rate game loop.

[Forty original game-loop traces](native-loop-check.json) verify the update order, half-second subdivision boundary and 1×/2×/4×/8× acceleration. The reference harness uses the original Windows keyboard interrupt, 100 Hz timer, event queue, command dispatch and held-key updates. Rendering no longer performs a second avionics simulation update.

## Saved options and first launch

`PREFS.CFG` is a plain `name=value` file. Names are case-insensitive, spaces are significant, and values extend to the next line ending. Numeric values use decimal `atoi` conversion. The loader prepends each entry, so a later duplicate wins; saving emits the current list with CRLF endings. It preserves unknown tags. Menu actions update the runtime choice and its corresponding saved entry.

The reference disc's 49 entries are mostly map toggles, radio phrases and modem definitions. Applying those entries to the January 1998 software executable gives 640-resolution mode, Arcade physics, Forces 0, keyboard controls for all three axes, disabled mission planner and speech, enabled animations/music/minimal effects, and volume 63. Visual detail defaults to level 0. The first launch opens Preferences before the main menu; saved choices override those defaults on subsequent launches.

The menu offers Keyboard or Throttle Stick for collective; Keyboard or Pedals for tail rotor; and Keyboard, Joystick, Thrustmaster FCS, Flightstick Pro or Virtual Pilot Pro for cyclic. Their saved values are respectively 0/6, 0/5 and 0/1/2/3/4. Arcade and Realistic write model IDs 2 and 0.

`tools/check_preferences_native.py` checks the source file, reload and save round trips, 317 numeric/string cases, the control menu callbacks and all ten visual-detail levels. [The report](native-preferences-check.json) includes the full defaults and visual configuration words. File access is entirely in memory.


## C++ input comparison

`tools/check_cpp_controls.py` executes the original control routines and compares the native reconstruction. The sweep covers 1,000 keyboard frames with external-view routing, 128 device-configuration combinations, 20,480 calibrated joystick samples in all four sampling modes, and every entry in five deadband curves. All 36,784 fields agree in normal and sanitizer builds, with output SHA-256 `a70f27fa674620170bb5bbad63c497f08a66e8c0b1068dd8cb98a21a0f73137a`.

The sampler bounds device values to their calibration range and returns neutral for invalid calibration. It does not reproduce the original division by zero or out-of-range table access. Platform device polling and joystick-button command dispatch are separate integration work. Reports: [normal](../verification/cpp/controls/report.json), [sanitizers](../verification/cpp/controls-sanitize/report.json).
