Apache Longbow Research archive

Campaigns, pilot logs and debriefing

Open the campaign browser.

The three campaigns have twenty missions each, spread across eight stages. A new campaign begins at stage 1 with four aircraft. Success advances one stage; failure moves back one, with stage 0 ending the campaign in defeat. Stage 9 is victory. Losing all four aircraft also ends the campaign. The chooser tests stage 9 before the aircraft count.

Choosing the next mission

get_next_campaign_mission_ (0x00422e90) filters the loaded mission list by warzone, campaign flow and stage. It prefers untried missions. If all have been attempted, it prefers aborted or failed ones; otherwise it uses the whole set. There are at most three candidates per stage in the supplied campaigns.

Selection uses a local copy of the pilot's campaign seed:

seed = (seed * 0x41c64e6d + 0x3039) modulo 2^32
choice = ((seed >> 16) & 0x7fff) modulo candidate_count

Choosing or discarding an attempt does not save that new seed. log_mission_state_ (0x00413c2c) advances the stored seed when a result is logged, updates both campaign and single-mission history, and changes the stage. Retrying restores the campaign snapshot and reloads the selected mission.

The original chooser handles result codes 0 through 3 but omits 4, Outstanding. That path uses an uninitialized temporary. The reconstruction should classify Outstanding with Success and use a bounded candidate list. No memory-state imitation is needed.

What the debrief evaluates

do_debrief_ (0x00412dec) combines the event history, mission actors, ground targets, final position and landing results. Its order is significant:

  1. Sum flight time and collect destroyed objects.
  2. Count mission-specific prohibited, required and extra mobile objects.
  3. Check required ground targets at target waypoints.
  4. Apply extra ground-target credit and prohibited-target penalties.
  5. Check the final territory and distance from the front line.
  6. Apply crash/death state and the worst landing recorded during the sortie.
  7. Add the pilot's final status and the explanatory text.

A failure-object count greater than the mission's allowed count causes failure. Otherwise the required count must be met for Success. Required plus extra objects must meet the Outstanding count for Outstanding. Required ground targets which still exist and are not destroyed reduce the result to Failure. A destroyed extra ground target can promote Success to Outstanding; a destroyed prohibited target reduces a passing result to Failure.

The landing check searches every landing record and takes the highest evaluation code, rather than the last one. No recorded landing fails a passing mission and marks a live exit as aborted. Codes 1 and 2 retain the result, except for an enemy-territory landing. Codes 3 and 4 cap it at Success. Codes 5 and above fail it and invoke the pilot survival rules. Finishing in enemy territory also fails a passing result. The territory and landing checks are separate.

Pilot survival uses the campaign's game-wide random stream, rather than the mission-selection seed. Injury/crash status first chooses between injured and missing/killed outcomes; enemy territory and distance from the front line can instead produce capture or escape. set_pilot_crash_status_ (0x00414a70) leaves an already-established status alone. The native decision check covers 720 survival cases and 1,626 landing, territory and target cases.

Promotions and medals

update_log_ (0x00416944) recomputes completed mission counts from the stored results. Training missions 61 through 70 do not contribute. Aborted and untried entries do not count as completed. Failed, successful and outstanding results do.

The promotion score is:

successful missions + outstanding missions + 8 * campaigns won

Promotion thresholds are 5 for Lieutenant, 15 for Captain, 30 for Major and 50 for Lieutenant Colonel. Only one promotion branch runs per update, so a low-ranked profile with a high score advances through successive updates. Protected profiles retain their rank, although the original still sets the promotion-presentation flag.

The medal tests use both campaign victories and this score. Medal of Honor requires two victories, or one victory and a score above 9. Distinguished Service Cross requires three victories, or two and a score above 19. Bronze Star requires three victories and a score above 29. These names and their ordering come from the game's own log tables. Each medal is awarded once.

Pilot availability

Status is saved with the time it changed, in whole Unix hours. Loading a pilot returns Inactive or Injured to Active after 24 hours, Missing In Action after 48 hours and Prisoner Of War after 96 hours. The check uses elapsed wall-clock time, not simulated flight time. Killed pilots do not recover. A protected profile blocks the status change but the original setter still updates its status-change timestamp when asked to change it.

Value Status
0 Unknown
1 Immortal
2 Active
3 Retired
4 Inactive
5 Missing In Action
6 Prisoner Of War
7 Injured
8 Killed In Action
9 Killed In Training

Pilot file format

save_log_ (0x00416038) writes log\\log<user>.<slot>. It is a variable-length little-endian record, without a magic number or checksum:

Field Representation
Pilot name NUL-terminated bytes, at most 63 characters
Rank, status Two 32-bit words
Total time, combat time Two 32-bit words, 200 Hz game ticks
Status-change time, protected flag Two 32-bit words; timestamp in Unix hours
Medal of Honor, Cross, Bronze Star Three 32-bit words
Mission history Pairs of 32-bit mission ID and result; ends with ID 0xffffffff
Current campaign Four words: warzone, stage, aircraft remaining, random seed
Campaign mission history ID/result pairs; ends with ID 0xffffffff
Campaign victories Warzone IDs 1, 2, 3; ends with 0xffffffff

Older records may omit the final victory list. Result codes are 0 untried, 1 aborted, 2 failed, 3 success and 4 outstanding. The runtime object is 0x938 bytes, with 500 pointers to single-mission records and 64 pointers to campaign records. Each pointed-to record is eight bytes. Pointers are never saved to disk.

Two original campaign-history insertion routines use a 500-entry loop limit against the 64-entry array. The supplied campaigns fit within 20 entries; the new codec uses the real bounds and rejects malformed input. Tests do not reproduce writes beyond those arrays.

tools/logbook.py provides an independent bounded decoder and encoder. The save comparison includes 80 original save/load round trips and 80 snapshot restores, with byte-for-byte comparison. All pilot records are synthetic; original file I/O is intercepted in memory. The progression comparison adds 1,200 awards/count cases, 220 recovery cases and twelve campaign journeys through victory, setbacks and defeat.

Debrief records and kill attribution

A debrief block stores 1,000 records of six 32-bit words, followed by previous and next block pointers. Adjacent elapsed-time records coalesce within a block. If the current block is full, the allocator starts another block before checking for a coalescing opportunity.

Mobile destruction is record type 8. Kill attribution uses the first matching record for the model instance: a later record does not replace an earlier kill by another unit. Required and protected mobile totals count every destroyed unit regardless of who killed it. Outstanding totals require a player-attributed kill. Formation children use their leader's objective flags, with their own destruction and attribution states.

The record checks exercise 2,400 insertions across two blocks, 14,400 kill-attribution cases and 400 complete formation tallies. They run the original allocation, search and debrief routines.

Complete debrief callbacks

tools/capture_debrief_journeys.py executes the complete do_debrief_ routine and the log or discard button callback, including pilot serialization and subsequent campaign selection. Forty cases run twice, from independent initialization. The trace index covers missions 61, 1, 21 and 41 and five controlled outcomes. Saves are captured in memory; the reference installation is never changed.

Training mission 61 accepts a good landing without an attack. The three combat missions fail in that situation, succeed with the required targets destroyed, and award Outstanding when all marked targets are destroyed. Aborting and fatal crashes take their respective result paths. Discarding creates no save; logging captures the original pilot bytes. These tests begin with explicit landing/destruction fixtures, rather than simulating a complete flight to each objective.

Reconstruction checks

The C++ pilot codec and campaign progression now have 2,476 original-code comparison cases. Debrief decision and recording logic has 5,565 cases. Twenty mission-derived debrief fixtures additionally compare exact explanation text and serialized pilot results. All pass in normal and AddressSanitizer/UndefinedBehaviorSanitizer builds. Reports are under work/checks/cpp/logbook, work/checks/cpp/debrief and work/checks/cpp/mission-debrief, with matching sanitizer directories.

The frontend now connects campaign introduction, stage selection, briefing, takeoff, debrief and retry. Pilot and campaign persistence and the remaining pilot-menu controls are still being integrated.

Persistent frontend integration

The C++ logbook now uses the original file formats under a separate user-data directory. Eight populated logbook frames match all 2,457,600 pixels, including user selection, time totals, campaign summaries and medals. The original updates medal visibility before recomputing awards in update_log_screen_; the C++ screen follows that order.

Complete menu journeys now cover level-zero defeat, exhausted campaign lives, campaign victory, replacing a killed pilot while retaining the campaign, promotions and all three medal screens. These pass in normal and sanitizer builds. A defeated campaign stays defeated instead of being silently reset on return from debrief. Reports are in work/checks/cpp/pilot-frames, pilot-menu, profile-campaign and their sanitizer counterparts.