Apache Longbow Research archive

Apache multiplayer protocol

Reference: January 1998 software APACHE95.EXE, SHA-256 6ee5028c9ede0f3b8f9e6c757846d48f111dea1b5b96bebd7010a04674f7bb6e.

The game uses IPX socket 0x1235 + channel. Its APNET layer carries player discovery, lobby settings, synchronized mission start, flight state, weapons, casualties and chat. Two-player modes also share a 104-byte pilot/gunner record. The reconstruction implements message encoding, flight-state exchange, remote scene updates and firing. Complete shared pilot/gunner sessions also pass original-code comparisons. Transport and lobby controls remain in progress.

Layouts

All integers are little-endian. Most messages are 408 bytes: type at offset 0, an eight-character name at 4 with a terminator at 12, sender index at 16, and data at 24. Names and sender fields are used selectively. Message 30 has a four-byte type followed by sixteen compressed 32-byte player records, for a total of 516 bytes. Message 39 is only 24 bytes: type followed by five settings words.

Type Contents
2, 3, 5, 38 Discovery, identity, name, synchronization; model at 76
6 Five preference words at 24
7 Menu selection at 44 and five arguments at 48
8 Wind strength and direction at 68 and 72
10 Position at 24; war and weapon random seeds at 88 and 92
12 Pose at 24, speed at 72, target at 392; sender subtracts 0x900 from Z
13 Weapon at 96, projectile random seed at 112, target/direction at 392
16, 17, 21, 22 Kill, death, crash and collision information at 396
18, 19, 23, 24 Leave, disconnect, chaff and flare notifications
20 Restart pose at 24
25 Shared cockpit's 104-byte record at 24
26 Mobile casualties: byte count at 24, up to 63 four-byte records at 26
27 Scenery casualties: byte count at 24, up to 28 twelve-byte records at 28
28 Captured-flag notification, word 1 at 400
29 Client pose at 24, speed at 72, timing double at 80
30 Sixteen compressed player records
31-35 Server/client readiness, departure and nomination; nomination clock at 80
36 Sixteen score words at 24
39 Two-player mode, side, weather, start time and difficulty at 4
40 Chat text: 80-byte field at 24
41, 42 CD presence query and response

Receive cases 4 and 9 also exist. Several retained send functions are empty. The table describes the active reference routines rather than assigning behavior to those empty functions.

Each compressed player record has three unsigned 24-bit coordinates (original coordinates shifted right four), signed 24-bit speed, signed 16-bit model number, and nine signed 16-bit orientation components (the upper halves of Q31 values). The receiver restores coordinate precision, sign-extends speed/model, and normalizes each orientation basis row.

The original transmits unused stack bytes, reads an uninitialized orientation in the initial-state message, and accesses a timestamp beyond the received client-state payload. Those effects are not part of the reconstruction. Packet storage is zero-initialized, reads are bounded, and undefined fields are excluded from comparisons.

Verification

tools/native_network.py intercepts the original transport functions under Unicorn; no socket is opened. tools/check_cpp_network_packets.py compares the C++ encoders, compressed receiver and malformed-input handling. Both normal and sanitizer builds pass 2,240 packets across 35 message types (176,892 defined bytes), 64 compressed-state decodes (14,336 fields), and 1,620 malformed-input cases. Reports are in work/checks/cpp/network-packets and work/checks/cpp/network-packets-sanitize.

The original entry points include apnet_receive_ at 0x430d48, send_server_data_ at 0x4365f8, send_client_here_ at 0x436818, get_tx_pilot_gunner_packet_ at 0x4344a4, and set_rx_pilot_gunner_packet_ at 0x434818.

Shared pilot and gunner cockpit

The 104-byte record uses different fields according to the sending seat. Pilot data contains heading/pitch/roll at 0/4/8, integer flight endurance at 12, climb at 16, signed 16-bit fuel and weight at 24/26, combined torque at 34, eight one-byte engine/rotor/collective readings at 35, side/forward speed bytes at 45/46, frame ticks at 48, a landing report at 49 and position at 52. Flag 4 at byte 50 means landed. The compact speed fields are read as unsigned bytes, including side speed.

The gunner sends Hydra elevation at 20, signed 16-bit drone/task identities at 28/30 for mobile targets, target display coordinates at 52, target position at 64, target type at 76 and target source at 80. Receiving a missing target clears its type.

Both seats send death at 43, key count at 44, radio-message selection at 47, departure flag 2 at 50, and up to ten 16-bit key actions at 84. The queue suppresses duplicate actions and stops at ten. Packing clears queued actions, radio messages and the pilot's pending landing report. Received actions temporarily execute with both seats' control permissions.

tools/check_cpp_network_crew.py verifies 1,000 original sends and receives (104,000 payload bytes), 1,000 key-queue sequences, 48,006 decoded/queue fields and 147 malformed records. Both normal and sanitizer builds pass. That packet-level suite observes scene placement, key dispatch, radio output and target lookup at their service boundaries. The scene adapter is checked separately below.

Flight exchange

tools/check_cpp_network_exchange.py exercises active/inactive sessions, peer versus client/server mode, self and remote senders, dead crews, already synchronized peers, server readiness/nomination, score updates and extrapolation. The 900 cases cover 15 operations, with 404,100 checked fields in each normal/sanitizer run. Initial-state orientation is preserved and the invalid client timestamp is replaced by the arrival clock; those two fields are excluded from original equality.

The server extrapolator converts elapsed ticks with 0.005, then applies a second factor of 0.005 to each velocity component. Fixed-point multiplication rounds using bit 30 before converting the elapsed displacement to an integer. Both factors are present in the machine instructions at 0x4368d4.

The first three gunner target words come from offsets 8-16 of the radar target record: display coordinates, not a flight direction. Its final word is the target source field at offset 40.

The firing word at packet offset 112 is the projectile random seed (round + 0x10), assigned by initialise_weapon_round_. Each receiver constructs its remote projectile and overwrites the locally generated seed with that transmitted value. It is not a pylon index.

Remote scene and weapons

update_remote_drones_ (0x42ef60) visits task leaders. It excludes the local aircraft, unassigned connections, destroyed state 1 and aircraft still attached to a carrier. Speed is the received value multiplied by 432; rotation is copied directly. Placement goes through the ordinary mobile-object registry. A pending shot is consumed only when all three position coordinates are nonzero.

update_remote_weapons_ (0x42f0dc) fires guns 1 and 19 in four staggered positions. Each step is the firing direction multiplied by launch speed divided by the firing interval, using rounded Q31 multiplication. A step below terrain retains the preceding position. Guided and unguided launches receive the transmitted random seed after construction. Chaff and flares keep their original per-drone release timers and random sequence. The five simple effect types, 11-15, are constructed without calling launch_round_; type 11 creates 35 records.

The Cannon Pod (weapon 10) has movement class 1, but this dispatcher calls construct_guided_weapon_ for it. That explicitly sets flight flag 4. Selecting construction solely from its table changes its trajectory, even though no seeker is active.

Scenery targets use locate_ground_object_ (0x49157c). It searches the addressed sector's static list, then the dynamic list, for matching 256-unit coordinate cells. It ignores altitude and collision bounds. Some authored objects sit exactly at a sector boundary while remaining in the preceding sector's list; the lookup does not find those through the adjacent sector. Actor identities are task number plus drone serial, including formation members.

Verification in normal and sanitizer builds:

Mode rules

Combat and the larger multiplayer/flag games use the client/server exchange. Cooperative single missions, shared pilot/gunner and Apache-versus-Hind use peer synchronization. Shared cockpit chooses control permission 0 for the pilot and 1 for the gunner, force level 2, and no unlimited fuel/ammunition. Cooperative single missions use force level 9. Combat uses the chosen weather with no wind and starts at noon or 20:30; the larger multiplayer and flag modes force clear weather and noon. Their fuel, ammunition and crash options come from network preferences. general_initialise_connection_ is at 0x435884.

Aircraft allocation and restarting

allocate_remote_drone_ at 0x42ec14 selects a task leader by connection number. Flow 7 instead uses leader zero. Cooperative single missions and pilot/gunner mode retain friendly allegiance; Capture the Flag uses task zero for the friendly team and the other task for the enemy team. Carrier aircraft detach through the original ship routine before model replacement. Allegiance changes update combat contacts without reordering the mobile scene list.

Apache/Hind model replacement refreshes classification, attachment choices and ammunition. Unused ammunition slots retain their prior contents. Restarting an Apache uses model 64; restarting another advertised aircraft uses model 990 with attachment 1009, rather than attachment 859 used by fresh Hind allocation. Destruction and accumulated damage are cleared before placement. Unassigned leaders are removed in the combat and flag modes, except the flag models 473, 994 and 995, which remain as enemy objects.

check_cpp_network_allocation.py compares four ordinary mission player tasks and the dedicated MULTI.DTA/CAPTURE.DTA files. Normal and sanitizer runs match 675 assignment attempts, 313 successful assignments, 108 restarts, two unassigned-removal passes and 785 complete mobile/contact-list fingerprints: 24,857 reported fields. The trace SHA-256 is 81ab2857c4cc4a7d2532f2faf40e399e57e6776dfe61d2ecef35a5edfd13df92.

The tests follow actual aircraft lifecycle transitions. Reassigning arbitrary ground units as aircraft or repeatedly detaching an already-flying carrier aircraft corrupts the original linked lists. Those invalid call sequences are excluded; the reconstruction prevents duplicate detachment and refuses non-aircraft player tasks.

Casualties and participant lifecycle

Mobile records contain a signed 16-bit drone serial, an unsigned task byte and a source byte. Scenery records contain X/Y coordinates and a source byte, with three zero padding bytes. The per-frame buffers hold 63 mobile or 28 scenery records. Sending consumes and clears the buffer; an empty buffer sends nothing. The original asserts on overflow. The reconstruction rejects the append without corrupting the existing batch.

Death, crash, restart, departure, disconnection, countermeasure and captured-flag messages now reach the mission scene. Removal releases combat contacts and creates model debris; restart rebuilds the aircraft and places it again. Remote casualties use the existing damage, explosion and crater routines, and report their source byte to the mission record layer. Shared-cockpit and leader-following modes retain their original mission-exit rules.

A death receiver looks up the sender's connection number as the drone serial, rather than the separate transmitted serial word. The collision sender receives both the drone serial and task number from general_notify_users_that_player_killed_, and writes them at offsets 396 and 400. The receiver resolves that task/serial pair for a remote victim. An earlier isolated fixture supplied only the first argument; the caller trace corrected that fixture and the receiver now checks both local and remote victims.

check_cpp_network_lifecycle.py passes 1,800 cases across twelve message types and 109,180 state/event fields. check_cpp_network_casualties.py passes 400 batches, 9,963 attempted records, 196 rejected overflows and 159,528 complete packet bytes. Both pass normal and sanitizer builds.

check_cpp_network_scene_events.py passes 360 event sequences across four warzones and MULTI/CAPTURE, with 439,192 fields, 18,573 debris states and 76,213 explosion/crater states in both normal and sanitizer builds. It includes model maintenance between events, as a normal frame does; the original debris routine otherwise copies unset transforms from newly constructed attachments. The original and C++ trace SHA-256 is 07c6d32384ec62a4332d486144626265f6964630658e940a34016b929bf4ea20.

Shared cockpit scene connection

mission_crew applies the pilot record to the live aircraft transform, spatial registry and instrument readings. A dead or destroyed local aircraft retains its existing position and matrix, while its received scalar readings still update. The gunner record resolves mission actors or scenery by the same identities used elsewhere in APNET. Missing targets clear the target kind while retaining the received position, source and display coordinates. Radio messages use the receiving seat's ten-entry text and sample tables; replayed key actions temporarily have both seats' permissions.

check_cpp_network_crew_scene.py executes the original receiver in four complete mission scenes, covering both flight models. Normal and sanitizer runs match 640 receives, 37,092 state/event fields and 66,560 outgoing packet bytes. The trace SHA-256 is 54c6aae404892f8fd0505785e04fbcf2f6bc7d216e71b9756d33ba6c4bac7146. Physical death gating is isolated in this suite; key, radio and landing outputs are observed callbacks. Frontend controls, synchronized frame progression and transport are still being connected.

The target's display coordinates are copied with the selected radar record, rather than recalculated unconditionally. A stable manual radar selection retains them until another selection is made; optical selection copies the separate TADS record. These fields now have explicit comparisons in the running sensor/battle suite.

Live shared cockpit timing

mission_session connects crew records to the real input handlers, instruments and renderer. The pilot sends before physics, and the world renderer invokes the receive barrier after terrain, scenery and debris submission but before mobile-object scanning. Received actions temporarily have both seats' permissions. Aircraft mass in the packet is _model_weight (dry weight, initially 14,058), not the fuel-inclusive physical _weight.

update_avionics_systems_ calls get_display_values_ at 0x453d51, before the render barrier. Computing these readings after receiving the pilot position changes the gunner's HUD by one frame. The flight-plan MFD also uses signed arrival-time arithmetic, including negative airspeeds.

fragment_3d_object_ copies existing child transforms. Maintaining attachments again at fragmentation advances them to a different pose and changes projected debris. Crash-view handling at 0x462233 ends game mode 3 after the fade. Other network modes continue into their follow/restart paths. initialise_weapons_ initializes _ships_attacked_time to -60,000 at 0x446ab5, so ship-attack radio reactions are eligible from the beginning of a mission.

The isolated crew-session comparison matches 400 frames, 30,000 state fields, 41,600 crew bytes and 25,036,800 complete-frame pixels in both normal and sanitizer builds. See work/checks/cpp/shared-session and shared-session-sanitize. The broader matrix is in progress. Full APNET event delivery is a separate comparison; the isolated crew test does not deliver the independent firing or countermeasure packets.

Complete shared-session APNET exchange

native_shared_network.py runs both seats with APNET enabled and delivers all queued messages at the renderer's network callback. check_cpp_shared_network.py replays those same messages through the C++ session. Each flight model runs for 100 frames per seat. Normal and sanitizer builds match 400 seat frames, 30,000 state/camera fields, 416 packets and 46,392 meaningful packet bytes. The 310 complete frames without invalid original model reads match 23,808,000 pixels. Reports are in work/checks/cpp/shared-apnet-{realistic,arcade}[-sanitize]. These comparisons use deterministic delivery, without sockets.

This connects locally fired weapons, chaff, flares, collision notifications and crash notifications as well as the crew records. A non-carrier mobile collision in shared mode sends an extra type-21 notification in apache_collision_test_, after the crash view has already sent type 22 for the victim's serial/task identity. The local aircraft must be bound to its connection before incoming effects are applied.

The flight-plan and moving-map MFDs read current model heading and speed after the receive barrier. The HUD and mechanical instruments use the earlier sampled display values. Sharing one sample for both changed a gunner flight-plan arrival time by one second in the Korea trace.

Aircraft parked on a carrier share the carrier's maintained attachment transforms. A normal renderer visit updates those transforms, as can explicit maintenance during destruction. Advancing all parked-aircraft transforms at the end of simulation was too early: an incoming collision can destroy the carrier before it is drawn. The C++ attachment maintenance now updates the corresponding actor when the carrier tree is actually visited, preserving the original debris positions.

The reusable ground-radar record stores XYZ for scenery targets but leaves those words untouched for mobile targets. The latter are resolved through their object pointer locally and their serial/task pair over APNET. A cleared target also retains those unused words. Crew comparisons exclude the 12 unused XYZ bytes for mobile/empty targets, while checking their target kind, mobile identity, display coordinates and source. No stale memory simulation is needed.

Outgoing casualty attribution

The live damage callbacks now populate mobile/scenery casualty batches and player-killed notifications. The source allegiance occupies status bits 1 and 2; bit 0 marks destruction. A shot that lands after its launcher is destroyed therefore keeps the same allegiance. Apache-versus-Hind reverses the transmitted side and swaps task IDs 0 and 1. The other two-player participant, and either crew member in pilot/gunner mode, use source code 3.

A locally fired round killing an assigned remote aircraft also removes/fragments that aircraft and sends type 16. The original tests the identity of the shared actor target record before this notification. Locally received fatal damage sends the same message for the local serial. In client/server mode the server updates its score, broadcasts the score table and displays the score message.

check_cpp_network_kill_sources.py compares 729 source/notification cases across all four warzones: 406 ordinary source cases, 87 destroyed launchers, 232 local-player deaths and four remote-player deaths. Both builds match 414 packets, 11,614 defined bytes and 58 score notices. These cases cover all seven game-mode values. The trace SHA-256 is 81de75eb11bf2c7c101c9da1ae7a7260d15c05553769e269b1f13c8c12fd6a98.

The expanded isolated shared-seat matrix now passes all four warzones, both resolutions and both flight models: 3,200 seat frames, 240,000 state fields, 315,568 meaningful crew bytes and 665 complete frames containing 129,177,600 matching pixels. Its report is work/checks/cpp/shared-session-matrix/report.json; the sanitizer replay is recorded separately when complete.

LAN transport and lobby receiver

APNET now has a nonblocking UDP transport, with original packet contents inside a small versioned node-identity envelope. The connection table sorts peers while in the lobby, fixes connection slots during flight, and expires absent lobby peers after 20 seconds. Sockets are created only when requested. Loopback tests cover discovery, names, preferences, readiness, menu messages, flight delivery, return to lobby and disconnect, along with malformed datagrams and a full 16-player roster. Both normal and sanitizer CTest runs pass all five targets.

The menu branches of the original receiver match 1,200 cases and 82,414 state/event fields in both builds, including all twelve lobby message types. Trace SHA-256: 142cbd368bcbe9c6cc7bd80cd3951ca692ddfd668823ed038f0a5e44a541cc54. The full shared-seat matrix also passes the sanitizer run with the same 3,200 frames and trace digest as the normal build. Connecting the actual network screens and all multiplayer mission modes remains in progress. CI remains disabled.

The menu receiver at apnet_receive_ reports "Found one other player" for any positive peer count in the two-player screens. Its later "Found more than one other player" branch is unreachable. This affects the message, not the player-count checks before takeoff. The receiver comparison retains that wording.

tools/check_cpp_network_lobby.py runs the original receiver with transport and widget operations intercepted. Reports are work/checks/cpp/network-lobby/report.json and work/checks/cpp/network-lobby-sanitize/report.json. UDP tests use two independent sockets bound exclusively to loopback with broadcast disabled; they contact no external hosts.

Connected multiplayer screens and sessions

The original network and two-player screens now drive discovery, names, roles, preferences, readiness, shared mission selection and takeoff. Live sessions use the original peer or client/server exchange according to the selected mode. The obsolete serial/modem controls are replaced by LAN discovery and a direct host address. UDP carries the original APNET payloads in the reconstruction's transport envelope; it does not connect to the DOS game's IPX or modem drivers.

Five loopback journeys use actual menu clicks, two sockets and 80 seat frames per mode: two-player combat, single missions, pilot/gunner, multiplayer combat and capture the flag. They also cover departure, returning to the lobby and a two-player server handover. Broadcast is disabled in these tests. Normal and sanitizer builds pass all five journeys and all five CTest targets. The dedicated server/client departure comparison matches 800 original cases and 49,668 state/event fields.

The combat MFD now draws the original kill scoreboard. Mission initialization applies connection difficulty before constructing actors, and combat-only modes skip the ordinary AI war simulation. The six-mode original-session matrix is still running. Remaining multiplayer integration includes respawn, landing rewards, flag completion, following a surviving partner and chat input. CI remains disabled.

Capture-the-flag mission variants

The European September 1996, Sold Out 1998, European October 2000 and French November 2000 discs share an incomplete MISSDATA/CAPTURE.DTA: 23,078 bytes, SHA-256 4876800ef06c3c2c33cb6ddd855eeadead7233d9dca201613a31d8bd20b57c96. It contains two tasks with eight Apaches and eight Hinds. Both the spawn-point task and the flag task are missing.

The US September 1996 and Japanese October 1996 discs have the complete four-task mission: 18,460 bytes, SHA-256 f8a521a39b1d7cf30853bc2983030db76d726e388b936955f5433e0c53bc70e0. Task 2 supplies sixteen respawn positions through model 447. Task 3 contains models 995 and 994; 994 is the landing goal. Each team also has three model-473 markers. The structural comparison is in work/checks/capture-mission-data.json.

general_restart_user_drone_ (0x436018) reads the third task, and awards_for_landing_ (0x43b450) follows the fourth task to find model 994. With the incomplete data, the original landing routine dereferences a null task through get_next_task_ (0x427e80). The C++ menu rejects that incomplete mission before takeoff, and its respawn routine checks the route bounds. No null-memory behaviour is reproduced.

The complete mission is tested with the reference January 1998 executable by substituting only CAPTURE.DTA in a temporary data tree. Original media and extracted files remain unchanged. Earlier startup comparisons using the incomplete mission verified loading and ordinary flight, but did not exercise a landing or respawn.

Server succession

A server-left message resets server_in_game. The original steady-state server sender only sends position records, so those records cannot restore the remaining clients' upload gate. The transport adapter now exchanges the existing server-waiting/server-ready messages again when the server changes. Aircraft, scores and simulation state remain intact. This is a networking repair; the isolated original packet receiver remains unchanged.

Completed session and window comparisons

All six modes pass the original-code session matrix in normal and sanitizer builds: 48 cases, 3,840 frames and 288,000 state/camera fields. They also match 2,949,120 palette bytes, 172,815,553 defined cockpit pixels and 200,524,800 pixels across 1,051 complete safe frames. Mode 6 covers the Apache side with an advertised Hind partner. The state digest is fb5166a8fb0647fc10767784ef141eb4e39ec224f3e4d8da9610ed9b1442a4aa.

The selected flight model must reach AI movement as well as player flight. Arcade AI helicopters use the original increased acceleration and different pitch response. A separate comparison matches every pose of all 154 actors in Mission 141 for 160 frames across both models.

Combat respawn, landing rewards, the flag-distance boundary and cooperative survivor views pass separate original-code checks. Shift+M chat matches 1,123 editor/command steps, 46,043 fields and 18,432,000 pixels. The hidden /terrain editor and all eight slash commands are included. Text is clipped safely where the original 320-pixel display writes beyond its row width.

Three real-window journeys exercise two-player combat, pilot/gunner flight and network combat in both builds. The tests navigate the menus, take off, send chat, change views and leave flight. GDB confines the unmodified application's transport to 127.0.0.1 before opening any socket, with broadcast disabled; it observes chat and completion callbacks without changing game state. Input goes only to a private X server.