Skip to content

P — Pilot Save (.P)

PLTnnn.P files (e.g. PLT441.P, PLT628.P) are binary pilot save files written by the FA engine. They store the persistent state for each pilot slot. Unlike all other FA data, pilot files are stored directly in the FA install directory, not inside any .LIB archive. The filename number does not imply a slot sequence — it appears to be a randomly assigned identifier. All observed files are exactly 9,696 bytes.

Tools

fx

fx plt info  PLT441.P        # identity block: name, rank, campaign, ordnance
fx plt dump  PLT441.P        # full stats block: missions, kills, weapon accuracy

The identity block (0x010xAF) is fully mapped and editable via the fxs PLT editor. The stats counters (0x1F800x21F7) are confirmed from RE and displayed by fx plt dump and the fxs stats pane. The four gap regions remain unmapped (see Open Questions), except the medal-flag band (0x5720x57C) inside gap 2, mapped statically (§ Medal-flag band).

The fx_lib write API (plt_read → edit → plt_write, api.md § plt.h) serializes a pilot file back to bytes, overlaying only the mapped fields and passing the unmapped regions through verbatim — a plt_readplt_write round-trip is byte-identical (see § Round-Trip Notes). Editing is done in fx_lib, not from the CLI: there is no fx plt write verb, so the tool never takes an output path for a save file.

Other Tools

  • FATK — free (abandonware, 1998); original GUI tool with full pilot editing support; requires a compatibility layer on 64-bit Windows
  • HxD — free, Windows; use with the field table below for manual patching
  • 010 Editor $ — paid; binary templates will allow a fully labelled struct view once all gaps are mapped

File Layout

All multi-byte integers are little-endian.

_campaignPilot global base VA = 0x004f8bb8 (from FA.SMS). File size 0x25E0 = 9,696 bytes. All offsets below are confirmed from decompile (DumpAllFunctions.txt) or computed from base VA.

Identity block (0x00–0xAF) — confirmed

Offset Size Type Field
0x00 1 u8 Type / version tag — observed: 0x0F
0x01 63 char[] Pilot name, null-padded
0x40 32 char[] Callsign, null-padded
0x61 13 char[] Callsign voice file (e.g. ^ACID.5K), null-padded
0x6E 13 char[] Nose art ID (e.g. NOSE01), null-padded
0x7B 13 char[] Left wing decal ID (e.g. LEFT03), null-padded
0x88 13 char[] Right wing decal ID (e.g. RIGHT03), null-padded
0x95 13 char[] Pilot portrait ID (e.g. PILOT02), null-padded
0xA2 14 char[] Rank string (e.g. 2nd Lieutenant), null-padded

Text and display region (0xB0–0xDAD) — partially mapped

Four anchor fields confirmed from FUN_004674f0 (pilot card display, VA 0x4674f0); gaps between them remain unmapped.

Offset Size Type Field
0xB0 18 ? Unknown (see Open Questions)
0xC2 ~13 char[] Secondary identity string — printed on pilot card after rank (squadron, unit, or location); exact length unknown
0xCF 1187 ? Unknown — mission log region (see Open Questions)
0x572 11 u8[11] Medal-awarded flag band — one byte per decoration slot, written by the per-campaign *Medals passes (see § Medal-flag band)
0x57D 50 ? Unknown — mission log region remainder (see Open Questions)
0x5AF var char[][] Mission log — up to 10 null-terminated entries read sequentially; each up to 3 lines; likely mission history
0xD7F 13 char[] Campaign .CAM filename (e.g. EGYPT.CAM) — confirmed: DAT_004f9937 = _campaignPilot + 0xD7F, written by campaign init
0xD8C 32 char[] Campaign display name (e.g. Egypt 1998) — confirmed: DAT_004f9944 = _campaignPilot + 0xD8C, written by campaign init
0xDAC 2 u16 Pilot status enum — confirmed: DAT_004f9964 = _campaignPilot + 0xDAC; 0=Available, 1=On mission, 2=MIA, 3=KIA, 4=Retired From Active Duty

Medal-flag band (0x5720x57C) — mapped

Recovered statically (2026-08-01) from the complete post-#482 decompile corpus: the six per-campaign medal passes (_UkraineMedals 0x483E00, _KurileMedals 0x484050, _VietnamMedals 0x484410 — a return 0 stub, _ATFEgyptMedals 0x484430, _ATFVladMedals 0x484690, _ATFBalticMedals 0x484B70) write fixed VAs 0x4F912A0x4F9134 inside _campaignPilot. Each byte is an awarded-flag for one decoration slot; a slot means the same decoration class in every campaign, with the Navy or Air Force variant of the title by theater. Pattern per medal: if (flag == 0 && <criteria>) { flag = 1; _AwardMedal(title); } — the criteria read the end-of-mission scratch stats (0x54DDxx), the pilot stats counters (e.g. +0x1FC8), and the promotion/rank word (+0xDAE). _AwardMedal (0x467110) then appends the title to the service-record block at +0x5AF, skipping the append once the block would exceed 1,000 bytes.

Offset Global Decoration slot
0x572 pilotMedalHonor Medal of Honor (Navy / Air Force)
0x573 pilotMedalCross Navy Cross / Air Force Cross
0x574 pilotMedalDSM Distinguished Service Medal
0x575 pilotMedalAir Air Medal
0x576 pilotMedalDFC Distinguished Flying Cross
0x577 pilotMedalAchievement Achievement Medal
0x578 pilotMedalCommendation Commendation Medal
0x579 pilotMedalExpeditionary Expeditionary Medal
0x57A pilotPurpleHearts Purple Heart — a count 0–3, not a flag
0x57B pilotMedalSilverStar Silver Star
0x57C pilotMedalYellowFever Conquest of Yellow Fever Medal (Kurile easter egg)

Campaign data strings (0xDAE–0x1C5F) — partially mapped

Variable-length null-terminated strings packed sequentially from 0xDAE: - Assigned aircraft .PT reference (e.g. F22.PT) - Available aircraft pool (.PT references) - Sensor/ECM loadout (.SEE, .ECM references) - Other campaign-specific strings

Aircraft pool (0x0DB0–0x1C5F) — confirmed

20 slots × 0xBC (188) bytes, immediately before the store table and ending exactly where it begins — the two tables tile the campaign block with nothing between them. _AddCampaignPlane (0x480C90) walks 0x4F9968 in 0xBC steps while the s16 at +0x0E is non-zero, and _CampaignPlanesLeft counts the same field; _campaignPilot is 0x4F8BB8, so 0x4F9968 is file offset 0x0DB0.

Field Offset within slot Size Type
Aircraft type filename (.PT) +0x00 14 char[] (null-padded)
In use +0x0E 2 s16 — zero means the slot is empty; this, not the name, is what the engine tests
(unmapped) +0x10 30 per-airframe campaign state; not yet mapped
Repair state +0x2E 2 s16 — percent under repair: _CallCampaignProc cmd 3 writes dam×100/damMax + PT+0x1B4, clamped to 100, after a home landing (#492)
Damage array copy +0x30 139 the plane's live _dam per-system damage block, banked on a home landing and restored on the next sortie; a bail (!AlmostHome && !AtFriendlyAP) zeroes the whole slot instead (#492)

A pilot owns one slot per airframe, so the same type appears more than once (PLT937.P holds three F22.PT, three F117.PT, one B2.PT, four F31E.PT).

Store inventory (0x1C60–0x1F7F) — confirmed

50 entries × 16 bytes = 800 bytes. DAT_004fa818 = _campaignPilot + 0x1C60. Not only weapons: drop tanks (.GAS), sensor pods (.SEE) and ECM pods (.ECM) are drawn from the same table.

Field Offset within entry Size Type
Store type filename (.JT, .GAS, .SEE, .ECM) +0x00 14 char[] (null-padded)
Quantity +0x0E 2 s16 — 0x7FFF = unlimited (_AddCampaignStore returns early on it)

A free slot is one with no name, not one with a quantity of -1: this spec used to say "-1 if slot unused", but every campaign save carries its guns (GSH301.JT, M61.JT, GAU12.JT, …) as named entries with a quantity of -1, while the genuinely unused slots at the end of the table are blank. Whatever -1 means to the engine, it does not mean "empty" (#491).

lib/src/plt.cpp used to recover both tables by scanning the campaign block for printable strings, capped at 0x0F00 — 3,840 bytes short of the store table — and read the quantity as a single byte after the name. A fully-equipped campaign pilot therefore decoded to no aircraft, no ordnance and no sensors at all. The round-trip suite could not see it: fx plt never writes these fields, so an unedited repack stayed byte-identical. PLT937.P holds 11 airframes and 35 stores; fx plt info reported none.

Managed by _AddCampaignStore (0x480E10): searches by name, increments/decrements quantity, or allocates a free slot.

Stats counters (0x1F80–0x21F7) — confirmed

All fields confirmed via FUN_00485380 (0x485380, end-of-mission stats flush) and related functions. _campaignPilot base 0x4f8bb8 + listed offset = VA of each field.

Mission and loss counters (0x1F80–0x1FAF):

Offset VA Size Field
0x1F80 DAT_004fab38 u32 Missions flown (total)
0x1F84 DAT_004fab3c u32 Wingman missions
0x1F88 DAT_004fab40 u32 Missions failed — copied to _campaignFailures (0x54e418) before campaign proc
0x1F8C DAT_004fab44 u32 Total shots fired — accumulated from per-mission DAT_0054ddc4
0x1F90 DAT_004fab48 u32 Ejections / bail-outs
0x1F94 DAT_004fab4c u32 Wingman KIA
0x1F98 DAT_004fab50 u32 Player aircraft damage % accumulated
0x1F9C DAT_004fab54 u32 Wingman aircraft damage % accumulated
0x1FA0 DAT_004fab58 u32 Player landing count
0x1FA4 DAT_004fab5c u32 Wingman landing count
0x1FA8 DAT_004fab60 u32 Player landing quality score
0x1FAC DAT_004fab64 u32 Wingman landing quality score

Kill tallies by target class (0x1FB0–0x2017):

13 kill categories; each has player u32 then wingman u32 (8 bytes per category). Category dispatch from _KillStats_12 (0x485820) based on obj_class word bits:

Offset VA (player) Category obj_class bits
0x1FB0 DAT_004fab68 Air — aircraft / fighters 0x8000 set
0x1FB8 DAT_004fab70 Air — type B (fighters subtype) 0x4000 set
0x1FC0 DAT_004fab78 Aircraft destroyed by crash or BA weapon obj byte 0 = 0x04 with OBJ_TYPE+0xba bit 3
0x1FC8 DAT_004fab80 Naval vessels 0x2000 set
0x1FD0 DAT_004fab88 SAM launchers 0x1000 set
0x1FD8 DAT_004fab90 AAA guns 0x800 set
0x1FE0 DAT_004fab98 Armor / tanks 0x400 set
0x1FE8 DAT_004faba0 APCs 0x200 set
0x1FF0 DAT_004faba8 Vehicles / trucks 0x100 set
0x1FF8 DAT_004fabb0 Infantry 0x40 set
0x2000 DAT_004fabb8 Friendly fire same faction, other conditions
0x2008 DAT_004fabc0 Air — non-0x8000 (non-fighter aerial) 0x8000 absent, aerial
0x2010 DAT_004fabc8 Capital ships naval + hitpoints > 999

Wingman slot for each = player VA + 4.

Weapon accuracy stats (0x20B8–0x21F7) — confirmed:

8 weapon-type groups; each group = player slot (0x14 bytes) + wingman slot (0x14 bytes) = 0x28 bytes per group.

Each slot = 5 × u32: [damage_total, shots_fired, hits, type3, kills]. Dispatched by FUN_004856f0 (0x4856f0) based on OBJ_TYPE flags; accumulated by FUN_004854a0 (0x4854a0).

Offset VA Group
0x20B8 DAT_004fac70 Air-to-air gun (OBJ_TYPE bit 0x10000)
0x20E0 DAT_004fac98 Air-to-air missile (OBJ_TYPE bit 0x20000)
0x2108 DAT_004facc0 Ground attack (OBJ_TYPE bits 0x20080)
0x2130 DAT_004face8 Naval attack (OBJ_TYPE bit 0x10)
0x2158 DAT_004fad10 Kill by aircraft (shooter = obj byte 0 0x04)
0x2180 DAT_004fad38 Kill type B
0x21A8 DAT_004fad60 Kill type C
0x21D0 DAT_004fad88 Kill type D

Wingman slot for each = player VA + 0x14.

The regions at 0x2018–0x20B7 and 0x21F8–0x25DF are unmapped — see Open Questions.

Engine Notes

Confirmed engine functions (FA.SMS + DumpAllFunctions.txt):

VA Symbol/Name Description
0x467180 PilotSave(PILOT*, short) Write pilot save — takes a PILOT* and a short slot index; serialises the full struct to PLTnnn.P
0x4674f0 PilotBuildPaper Pilot dossier ("paper") builder — interleaves SHWPILOT.TXT template lines with record fields (+0xC2, +0x5AF, +0xD8C, +0xDAC, +0x1F88) and renders through the shell format engine (#492)
0x467860 PilotPaperAddLine Template line copy — copies until a control byte (styled-text terminator)
0x480E10 _AddCampaignStore Adds or increments an ordnance entry in the ordnance inventory table at +0x1C60
0x481320 _CampaignSave Saves _campaignPilot to disk (copies to RM, then _SaveFile with full 0x25E0 bytes)
0x484D90 _EndOfMissionStats@0 Computes per-mission damage %, landing, protection, and player/wm state into temp globals
0x485040 _EndOfFortMissionStats@0 Computes fort-related kill/suppression stats into named temp globals
0x485380 FUN_00485380 Flushes all per-mission temp stats into permanent PILOT struct fields at +0x1F80 onwards
0x4854E0 _WpnStats@28 Per-shot weapon stats accumulator; updates shots_fired, hits, damage_total, kills in temp buffer
0x485820 _KillStats@12 Records kill into the correct kill-category slot (13 categories at +0x1FB0) based on target's obj_class
0x485A40 _LandingStats@12 Accumulates landing count and quality score into temp globals

PilotSave saves the full struct as a single 9,696-byte block via _SaveFile. The stats counters are accumulated by the functions above into _campaignPilot directly.

Round-Trip Notes

plt_write is a byte-exact passthrough serializer: it starts from a copy of the original file bytes (PltFile::raw) and overlays only the fixed-offset mapped fields — the identity block (0x000xAF), the medal-flag band (0x5720x57C, decoded into PltMedals and editable), and, when present, the stats counters (0x1F800x21F7). Everything else — the remaining gap regions and the variable-length campaign/ordnance region (0xB00x1F7F) — is copied through unchanged. The service-record citations at 0x5AF are decoded as a read-only view (plt_service_record): the surrounding mission-log region is still unmapped, so the codec never re-encodes it. A plt_readplt_write round-trip is therefore byte-identical, and Phase 6 (#29) can map the remaining gaps without touching the codec.

Three details keep the round-trip exact even before the gaps are understood:

  • Unedited string fields are left verbatim. An identity field is only rewritten when its value differs from what the original bytes decode to. Untouched fields — including any non-zero bytes left past a field's null terminator (a shorter callsign written over a longer one in-engine) — pass through unmodified rather than being re-zero-padded.
  • The variable-length campaign region is never re-encoded. PltInfo's cam_file / aircraft / ordnance views come from a heuristic forward scan; they are read-only display state. The bytes themselves pass through, so the scan's imprecision cannot perturb the file.
  • The medal band is overlaid raw. All eleven slots are written back as the u8 values held in PltMedals — an unedited read → write reproduces whatever bytes were there (even out-of-range values), so the overlay cannot normalize a file it merely passed through.

Validation: plt_repack (read → write) is byte-identical across all 7 real pilot files in the reference install (PLT441/628/937/991/992/993/994.P, FX_FA_ROOT-gated) and the synthetic fixtures in tests/test_plt.cpp, which also exercise full-width fields, stale terminator bytes, and single-field edits.

Open Questions

Static Ghidra analysis (AnalyzePLT.java, 46,985-line output) and binary diff of three fresh pilot saves (PLT441.P, PLT628.P, PLT937.P) were exhausted for all four gaps against the May corpus. A 2026-08-01 re-scan of the complete post-#482 corpus (the May analysis predates #496, which surfaced 490 never-disassembled functions) recovered the gap-2 medal-flag band (§ Medal-flag band) and found zero fixed-VA references into gaps 1, 3, and 4 — the bench verdict stands, on stronger evidence. Every remaining region needs pilot saves taken after actual gameplay: complete 5+ standard missions (gaps 2 and 3), a fort-assault mission (gap 4), and a rank advance (gap 1), then diff with HxD (side-by-side compare → Differ) or 010 Editor using the field tables above. A binary probe test (2026-05-21, four PROBE_GAP* pilots) confirmed the pilot records screen reads none of the gap regions. Bench bonus: a medal award should flip exactly the predicted byte in the 0x572 band — a live validation of the static read.

1. Gap 0xB0–0xC1 (18 bytes)

No named DAT_ label or MOV/CMP instruction targeting VA 0x004f8c680x004f8c79 found in any function in the game executable. All zeros in all three fresh saves. Struct context suggests these bytes are written only after campaign assignment — possibly a score tier index, medal count, or secondary rank fields.

Status: open — re-gameplay (#29)

2. Gap 0xCF–0x5AE (1,248 bytes; medal-flag band mapped)

The medal-flag band (0x5720x57C, § Medal-flag band) was recovered statically from the post-#482 corpus: eleven one-byte decoration slots written by the per-campaign *Medals passes. (The size previously stated here, 1,344, was an arithmetic error — 0x5AF − 0xCF = 1,248.) The rest of the region holds variable-length null-terminated mission log text; decompile of FUN_004674f0 shows the pilot card reader scanning from 0x5AF backwards, implying the entries grow downward from 0x5AE. No other fixed-offset accesses within the region. All zeros in fresh saves (no missions flown). Structure known, content unsampled: each log entry is one or more null-terminated lines terminated by a 0x01 styled-text byte.

Status: open — re-gameplay (#29)

3. Gap 0x2018–0x20B7 (160 bytes)

findFunctionsReadingOffsets returned only false positives — video-decoder functions accessing param_1 + 0x2044 where param_1 is a frame buffer, not _campaignPilot. No genuine PILOT struct access in this range was found anywhere in the analyzed code. All zeros in fresh saves. Sits between the confirmed kill-tally block (ends 0x2017) and the confirmed weapon-accuracy block (starts 0x20B8). Could be additional kill subcategories (objective kills, suppression counts), a mission-result history array, or reserved padding.

The pilot-file migration routine _ConvertPilotFiles (0x485AE0) corroborates this positively rather than by absence: it zero-fills the whole 9,696-byte record, then copies legacy 0x15B4 fields into the new layout only through +0x2010 — one dword below this gap. So an upgraded pilot receives nothing here and the region is left zero, consistent with a field written only at runtime.

Status: open — re-gameplay (#29)

4. Gap 0x21F8–0x25DF (~1,000 bytes)

_EndOfFortMissionStats@0 (0x485040) and all callers write exclusively to scratch globals at 0x005xxxxx — no flush path into this region was found in the decompile. All zeros in fresh saves. Fort/campaign-phase stats scratch globals (_statsFortKills__3PAJA, _statsFortAircraftUsed__3PAJA, etc.) are confirmed present but their flush path into the PILOT struct was not identified. This region is populated only after completing campaign fort-assault missions.

_ConvertPilotFiles (0x485AE0) confirms the region is runtime-only from the migration side: its old→new field copies stop at +0x2010, so everything from here up is left zero on a format upgrade. The pilot record itself is shared across the engine family — U.S. Navy Fighters '97 carries the same trailing region 0xA0 bytes earlier, inside its smaller 0x2540-byte pilot record (see Game compatibility) — so this is family-wide career state rather than an FA-only addition, which argues against it being reserved padding.

Status: open — re-gameplay (#29)

Formats: BRF.PT (plane type) and .JT (ordnance) records referenced by name; M — mission files whose outcomes feed the stats counters; CAM — the campaign whose filename is stored at +0xD7F.