Controls, keys and timing
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 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 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 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 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, sanitizers.