Radar, TADS and cockpit instruments
Open the recorded cockpit sequences.
Apache separates contact discovery, radar ranking, the selected target and the optical sight. update_avionics_systems_ (0x00453d50) first updates instrument values, rebuilds the nearby contact lists, scans the active radar and tactical display, then updates the selected target and waypoint information. TADS picking runs during view rendering, so rendering belongs in an integrated targeting replay.
Nearby contacts
scan_target_lists_ (0x004426d0) searches a seven-by-seven area of four-cell sectors around the helicopter. It considers static scenery, mission ground objects and mobile actors. A model must have a targetable value in its header at +0xb4. Destroyed ordinary objects and the player's own 3D object are excluded. Ground objects also require a nonzero side field.
Terrain visibility uses the weighted line-of-sight routine with precision 3. The lists hold up to 512 ground contacts, 512 mobile contacts and 256 missile contacts. Mobile actors are classified for ground or air radar from their drone class and height; an aircraft whose world Z is below 30 feet is classified with ground contacts by this routine. This test does not subtract terrain elevation.
Ground radar rotates candidates into heading-relative coordinates and clips them to the forward fan and selected range. Its three labels are 2.5, 5 and 10 nautical miles; the stored half-range values are 0x1dab00, 0x3b5600 and 0x76ac00. Tactical radar's labels are 2.5 and 8 nautical miles and describe its centre-to-edge range. Its grid step is 6,076 feet, the game's nautical mile.
Target ranking
get_target_priority_ (0x00442c8c) begins with the model's priority at header +0x9c. Special mission targets use 65,535 instead. The filter classes match exact priorities 10,000, 30,000 and 50,000; other priorities use class 0.
Ground-radar contacts receive a heading alignment factor. The calculation uses absolute lateral and longitudinal offsets, Q31 division and the original rounded trigonometry lookup. Contacts outside the ±45-degree alignment region receive zero base priority. Within it, the penalty combines lateral/longitudinal ratio, distance and a right shift by four. A separate distance penalty applies to both ground and air contacts.
The ranking calculation's distance constants use statute miles: its alignment term clamps at 50 miles and substitutes 49, while its final distance term clamps at 20 miles and substitutes 19. These are ranking limits, not radar display ranges or missile envelopes. The independent comparison covers 1,800 priority calculations, including these clamps and fixed-point rounding.
insert_radar_target_ maintains a doubly linked list in descending priority. New contacts precede existing contacts with equal priority. Automatic targeting selects the head. Manual cycling wraps in both directions. If a manually selected contact disappears from the radar list, the system returns to automatic selection.
The optical sight
get_tads_target_ (0x00460980) transforms the visible contacts into the current sight coordinate system. It rejects contacts behind the sight, then chooses the smallest abs(horizontal) + abs(vertical) offset, initially limited to 0x100000 world units. This is a distance from the sight line in world space, not an angular or screen-pixel distance. Consequently, a nearby object offset slightly from the line can win over a more distant object which appears closer to the crosshair.
Ground contacts are scanned first, followed by mobile contacts. Equal scores keep the earlier candidate. Only after choosing a candidate does the game project it to the screen. A failed projection does not trigger a search for the next-best candidate. The symbol check uses fixed bounds of 800 by 600, even at the lower display resolutions.
Locking keeps a target only while it remains in the visible-contact list and inside a 270-degree horizontal tracking arc centred on the helicopter's heading. Leaving that arc or losing visibility releases the lock. A retained lock can temporarily have no visible screen symbol. TADS damage clears active optical targeting and its lock.
The TADS comparison contains 600 selection/projection cases and 100 lock-retention cases. Terrain visibility has its own sampling checks.
MFD pages
| ID | Page |
|---|---|
| 0 | Off |
| 1 | Damaged |
| 2 | Moving map |
| 3 | Radar |
| 4 | Tactical display |
| 5 | FLIR |
| 6 | Flight plan |
| 7 | Weapons |
| 8 | Engines |
| 9 | Status |
The bracket keys cycle pages 2 through 9; Shift reverses direction and Ctrl switches the panel off or on. Cycling skips the page currently shown on the other main MFD. Off or damaged panels do not cycle. The pilot and gunner keep separate page selections, and the gunner has a third, central FLIR display. Individual damage bits affect each panel.
In the normal single-player setup, the pilot begins with radar and flight plan, and the gunner with weapons and tactical display. Switching a panel back on chooses a page through the current cycle direction rather than restoring the page it showed before being switched off.
The comparison covers 800 combinations of seat, panel, page, opposite page and cycle direction. Recorded journeys also exercise switching pages, powering panels off/on, changing seats, radar modes, radar ranges, target cycling and locking, optical targeting, helmet sight and night vision.
Player weapon controls
Enter arms the selected weapon; later Enter presses select the next available weapon. Ctrl+Enter disarms it. Space while unarmed arms without firing. Attempting to fire while landed instead disarms the weapons and leaves the helicopter safe. The selected pod, ammunition and firing deadline govern availability. Target lock is not a universal launch requirement: unguided fire and unlocked guided launches have their own behaviour. Seeker and impact rules are covered in the weapons analysis.
Reproducible references
tools/capture_avionics_journeys.py sends real key events through the original event queue. It runs the normal simulation and renders after each update, preserving the feedback from optical sight picking. The two missions use 320×240 and 640×480. All 66 indexed frames, palettes, instrument states, targets and observed cockpit-sound calls repeat identically from fresh instances. These are original-engine references for the reconstruction, not a comparison with a completed C++ game.
Reconstructed panels and MFD controls
The C++ warning lamps, communication text and time-acceleration badge match 552 original frames at both resolutions and in both seats. The three lamps copy small lit or unlit rectangles from the gunner's front-panel artwork, including when displayed in the pilot's cockpit. Radio text is uppercased, clipped to its aperture and drawn over a freshly restored panel background.
The off, damaged, engine and system-status pages match another 1,234 frames. The damaged display creates bands in palette colours 208 and 215, using two calls to its own linear-congruential generator for each 32-row group. Engine bars restore their backdrop from the pilot's front panel and draw eight values: left/right torque, left/right temperature, left engine RPM, rotor RPM, right engine RPM and collective. At 640×480 each bar is two pixels wide. The system-status page lists radar mode and active failures; text below the aperture is clipped. Bezel sprites are a separate final pass at 640×480. The 320×240 version shades only the four corner pixels of each main display.
All 4,068 MFD control comparisons match the original, including initialization, page changes, power switching, radar requests and display damage. The cycling direction is shared between seats and initially runs backwards. Pressing either bracket key sets the direction, even when its panel is off or broken. Powering a working panel back on chooses the next page after map in that direction, skipping the other panel's page. It does not restore the last page shown. The resulting startup power cycle therefore selects status unless a forward-cycle key has changed the direction.
Display damage is applied when drawing the affected seat. A powered-off damaged panel remains off; switching it on selects the damaged page. Clearing the fault alone does not restore a working page. Cycling ignores both off and damaged pages.
The C++ reports are under work/checks/cpp/panel-status/, mfd-basic/, mfd-corners/ and mfd-state/, with corresponding sanitizer runs. The following C++ pages now have separate framebuffer comparisons. The combined avionics pass is checked separately below.
Target close-ups
The selectable FLIR page at 0x0045ded0 shows the selected object without the surrounding terrain. It normalizes the vector from the helicopter to the target, builds a camera matrix around that direction and places the camera twice the larger of the model radius and height behind the target. Camera height is raised above terrain when necessary. The target retains its authored animation, attachment tree and distance-based model selection. The caption is the model name at header +0x48, rather than its longer description.
This isolated-object path disables textures, Gouraud shading and glass. It leaves the colour map from the preceding scene render in place. A normal scene therefore supplies normal colours, while a preceding thermal scene supplies the MFD infrared map. The central gunner display calls this same page. The library also contains an unreferenced whole-scene MFD infrared renderer at 0x0049c400; the game does not call it.
The C++ comparison covers 5,266 combinations of target model, viewpoint, resolution, panel and inherited colour map. All 5,017 cases without invalid original normal references match pixel for pixel, including the No Target page. The remaining 249 use the same bounded missing-normal handling as the main renderer. The complete sweep also passes under AddressSanitizer and UndefinedBehaviorSanitizer.
Weapon and radar artwork
The weapons page copies its background and small missile/pod images from the gunner's forward cockpit artwork. Both resolutions have their own source rectangles and placements. Wingtip stores show one or two missiles; Hellfire launchers show up to four. The display also shows gun ammunition, chaff, flares, arming and automatic countermeasure states. An original display quirk gates both HYDRA ammunition labels on the HYDRA261 count, so a loadout containing only HYDRA255 rockets omits those labels. The reconstruction retains that defined display behavior.
Radar symbols come from the model's radar classification at header +0xb4: cross, square, plus, diamond or either orientation of a tee. Selected contacts draw last and brighter than the other contacts. Air radar rotates the contact symbols with the heading; the tactical page rotates their positions while keeping their symbols upright. The tactical path omits classification 4. Radar failure leaves the range grid visible with FAILED; freezing adds FROZEN. The optical sight switches the ground-radar filter caption to TADS or BORE.
The weapons comparison covers 2,240 complete frames. Radar and tactical comparisons cover another 2,000, with populated contact lists, all symbol classes and control states at both resolutions. Circle outlines have 4,452 independent clipping comparisons.
Flight-plan display
The route page chooses an extent large enough to include the active waypoint and the following waypoint, or the preceding waypoint at the end of the route. Its grid spacing changes at six distance thresholds. The route rotates with heading over a fixed grid. The active waypoint and the helicopter's plus sign are drawn last. The corner fields show heading, waypoint bearing, distance in nautical miles and an arrival estimate based on current airspeed; zero airspeed shows --:--.
The distance uses an integer square root of the horizontal squared displacement. The arrival estimate divides by airspeed times 432 in the engine's 1/256-foot units. C++ comparisons cover 1,150 complete route frames and 10,487 integer-square-root inputs, including exact squares and adjacent values.
The new local reports are under work/checks/cpp/mfd-flir/, mfd-weapons/, mfd-radar/, mfd-flight-plan/, direction-math/, integer-sqrt/ and circle-outlines/.
Moving maps and contours
The moving map uses the engine's two-dimensional fixed-point projection. It draws super-features first, then terrain triangles, ordinary features and ground-object symbols in sector order. Contours form a final pass. The terrain triangles use seven pairs of palette colours; their diagonal and hidden-half flags come from the same records used by the 3D renderer. Feature colours distinguish water and selected road surfaces.
The normal moving-map configuration enables terrain and 1,000-foot contours. Optional map flags replace ground-object dots with the model's shadow outline and include dynamically placed ground objects. The six MFD ranges are labelled 10, 5, 2.5, 1.3, 0.7 and 0.3.
Contour construction walks the edges of each height-sorted triangle using integer increments. The longer edge continues across the middle vertex, while the shorter edge changes direction there. Its accumulated rounding is retained rather than snapping back to the vertex. Flat-topped triangles reverse the endpoint order, which affects line rasterization. Sea-level contours are omitted. These details are visible in pixel comparisons, particularly when the map rotates.
Ground-object initialization at 0x00490140 sets status bit zero for model IDs 125, 127, 387, 441, 442, 482, 483, 505, 890, 906 and 908. This excludes those objects from map symbols and target selection. It does not hide their geometry in the 3D world. Across the four warzones, 1,205 records change; all 42,317 initialized placement records have been compared with the original.
The C++ comparisons cover 744 complete maps and 640 moving-map MFD frames at both resolutions, all matching pixel for pixel. Separate checks cover 3,080 contour frames and 5,000 inverse-projection cases. All map, contour, inverse-projection and scenery-initialization checks also pass under AddressSanitizer and UndefinedBehaviorSanitizer. Reports are under work/checks/cpp/maps/, mfd-map/, map-contours/, inverse2/ and scenery-state/.
Helmet display
The helmet sight is a separate 2D drawing pass. Its logical window runs from -0x100000 to +0x100000 horizontally and -0xc0000 to +0xc0000 vertically. Each cockpit view chooses a pixel viewport within the screen. The 640×480 version uses the larger instrument font and adds a bank-angle arc that the 320×240 version omits. Heading, attitude, altitude and waypoint values otherwise use the same fixed-point geometry.
The pitch ladder rotates around the aircraft's roll angle, with solid positive-pitch marks and dashed negative-pitch marks. It reflects the numbered steps when the aircraft passes the vertical. The climb marker clamps to 100 feet per second in either direction; the adjacent radar-altitude bar saturates at 200 feet while its numeric readout continues above that height. Barometric altitude subtracts the helicopter's nine-foot reference offset. Radar altitude subtracts terrain elevation and bottoms out at zero.
There are four helmet layouts. Two emphasize flight information; the others show the selected weapon's sight and, optionally, the additional flight scales. A helmet-display failure suppresses the complete overlay. Optical views also show the selected device, magnification and sight movement within its allowed field.
Gun mode draws an impact cross and connecting line, Hellfires a roll-adjusted acquisition box, Hydra rockets an I-shaped elevation sight and intercept cross, and Stingers a lock diamond. Targets outside the visible area receive a direction line, dashed when behind the view. The target projection is independent of model clipping: it projects into screen pixels, accepts the original 800×600 outer bounds, then converts back to helmet coordinates.
Two details come directly from the original instructions. The gun impact marker is gated by target-projection validity rather than impact-projection validity. The Hellfire box selects a dashed line mask when it lacks a firing solution but then calls the solid-line drawing routine, so that mask has no effect on the box. These are defined drawing choices retained by the reconstruction.
Weapon text includes ammunition, a pending missile's estimated impact time, armed/safe/failed state, selection or laser designation, range and the reason for a missing firing solution. An airborne target also gets bearing, altitude, speed and heading readouts.
The reconstruction matches 2,640 flight-instrument frames and 2,200 weapon-cue/complete-layout frames. Another 5,420 cases compare world-to-helmet projection, and 4,112 compare the conversion from flight and waypoint state to displayed values. The complete helmet and instrument-state comparisons also pass under AddressSanitizer and UndefinedBehaviorSanitizer. Local reports are under work/checks/cpp/helmet-components/, helmet-weapons/, helmet-projection/ and instrument-state/.
Combined cockpit pass
tools/check_cpp_avionics_frames.py captures actual mission state and the scene/panel image immediately before draw_avionics_displays_. The C++ compositor and original engine independently draw all instruments over that common input. The 174 cases cover both seats and resolutions, all 20 views, all ten MFD pages, optical targeting, night vision, display failures and real key-driven page changes.
All 147 frames with valid original model references match exactly: 25,113,600 pixels, display selections and damaged-display random state. Another 27 target-camera frames reach the previously documented missing normals; the reconstruction uses bounded rendering for those. The comparison does not substitute the original drawing functions into the C++ renderer. Simulation and the scene/panel image are reference inputs, so these are avionics acceptance tests, not complete-game acceptance.
Startup replaces model names and gameplay properties using 336 executable records at 0x0044ec00. The archive name M113, for example, becomes M113(APC) on the target screen. Unlisted models receive numbered unnamed labels. The initializer also sets classification, protection, damage, score, radar symbols and a target-property flag. All 1,314 resulting records match the original initializer in tools/check_cpp_model_metadata.py.
The complete combined-frame and runtime-property checks also pass under AddressSanitizer and UndefinedBehaviorSanitizer. Reports: work/checks/cpp/avionics-frames/ and model-metadata/, with matching -sanitize directories.
Live targeting and camera reconstruction
The C++ radar and optical systems now run on the mission's live scenery, actors and projectiles. All 110 missions match the original across 5,500 scans and 22,000 optical selections, including 671,389 scenery contacts, 451,538 mobile contacts and 2,187 lost locks. Eight complete combat traces also execute the avionics update between combat and visual effects, matching 3,282,731 fields, 2,079 launches and 69 kills. The same checks pass under address and undefined-behavior sanitizers.
Target classification is a cache in the target record. The original name/status queries do not fill it, while classification queries do. An eager classification lookup can leave an aircraft classification attached to a later optical ground contact, incorrectly allowing a Stinger to track a building. The reconstruction keeps these query effects separate. Optical picking also preserves the original classification field when replacing the picked target; it does not initialize that cache itself.
Waypoint passage uses horizontal distance. Once the helicopter is within 0x14a000 units and starts moving away, the route advances. Waypoint type 6 plays cockpit cue 69. At the last waypoint, the landing message plays once unless the surface is water. Manual cycling wraps in either direction and retains the previous distance. There are 12,000 original-versus-C++ cases covering these transitions and cooperative key forwarding.
Cameras and optical devices
The twenty ordinary views include both cockpit seats and their quarter/side views, player and actor tracking, helmet sight, optics, missile tracking, missile target, spectator, remote, target lock and crash cameras. The first-person and target-lock cameras update optical picking after drawing; the external cameras do not. Every view except the spectator saves its final camera pose as the next spectator position.
The tracking camera changes heading, pitch and distance from the external controls at half-rate, scaled by frame time. Pitch is limited to a quarter-turn in either direction. Model IDs 103 and 607 use a smaller fixed orbit radius, while other objects use their model radius. The remote camera remembers the helicopter's position when selected and keeps looking at it. The missile-target camera sits on the far side of the target, looking back towards the missile; its offset is three times the target's explosion-size metadata field.
The optics key cycles DVO, FLIR and TV, skipping failed devices. Each device remembers its own magnification level. These are the original captions and camera offsets:
| Device | Caption | Forward offset, internal units | Movement rate |
|---|---|---|---|
| DVO | x3.5 | 199,680 | 835,132,530 |
| DVO | x16 | 819,200 | 238,609,294 |
| FLIR | x2 | 135,680 | 954,437,177 |
| FLIR | x11 | 563,200 | 357,913,941 |
| FLIR | x36 | 1,177,600 | 119,304,647 |
| TV | x28 | 716,800 | 178,956,971 |
| TV | x126 | 4,096,000 | 59,652,324 |
Magnification moves the viewpoint forwards along its viewing direction. FLIR also selects the infrared palette/rendering configuration. The camera is raised above terrain if this forward movement would put it underground.
Entering helmet or optical view saves the current targeting settings and selects manual TADS unless TADS has failed. Both exit paths restore the saved helmet pair when no target is locked. The optical exit uses the helmet pair too, even though the optical entry saved a separate pair. This is a defined inconsistency in leave_view_mode_ at 0x00461efc.
Camera geometry matches 449,970 fields across 30,000 cases. View selection and optical controls match another 477,416 fields across 28,000 cases. Continuous camera traces cover all twenty views, saved targeting settings, optical faults, weapon removal, crash transitions and fading. The geometry check encountered two exactly vertical targets which overflow the original signed division; C++ bounds the ratio instead of reproducing the CPU fault. Reports are under work/checks/cpp/cameras, view-modes, view-sequences, player-targeting, player-sensors, player-route and player-sensor-battles.
Live rendering and cockpit connection
The reconstruction now binds landscape rendering to the mission's live actor,
projectile, smoke, explosion, crater and scenery records. Four battle traces
match 1,100,627 state fields and render 60 reference views. Five views have no
invalid original model reads and match every indexed pixel and RGB byte. The
other 55 reach the known missing model normals and use bounded handling in C++.
Normal and sanitizer results agree. Reports: work/checks/cpp/mission-frames/ and
work/checks/cpp/mission-frames-sanitize/.
Nine continuous camera sequences cover all twenty views in 4,680 updates, matching 322,920 fields. Daylight and gun-impact calculations add 24,000 cases and 84,000 matching fields. The untargeted chain-gun impact estimate uses the absolute vertical velocity for upward and downward fire, then returns horizontal displacements without adding the barrel's world position. This is defined original behavior, distinct from the missing-normal reads.
The live cockpit adapter now matches 467,620 instrument fields and 1,170,510
simulation fields across 1,200 continuous mission updates. This includes flight
data, target and weapon captions, gun impact and rocket intercept cues, radar
and tactical contacts, stores and route labels. Normal and sanitizer runs agree.
Reports: work/checks/cpp/mission-instruments/ and
work/checks/cpp/mission-instruments-sanitize/.
The frontend still opens menus; mission input, campaign, audio and persistence connections remain active work.