/**
 * SPCAMO_ScenData.js
 * ─────────────────────────────────────────────────────────────────────────────
 * Parser / writer for SPCTS_SAVE_V100 scenario (.DAT) files.
 * Covers both SP WW2 and SP MBT — the engine format is identical between the
 * two games.  The only game-specific data are the nation and unit-class lookup
 * tables, which are kept as separate arrays (SPWW2_NATIONS / SPMBT_NATIONS and
 * SPWW2_UNIT_CLASSES / SPMBT_UNIT_CLASSES).
 *
 * Naming convention
 * ─────────────────
 *   SPCAMO_*   format/engine constants and functions (game-agnostic)
 *   SPWW2_*    SP WW2 lookup tables only
 *   SPMBT_*    SP MBT lookup tables only
 *   S37_*      Section 37 byte offsets
 *   S1_*       Section 1 byte offsets (unit records)
 *   S17_*      Section 17 byte offsets (unit positions)
 *   S34_*      Section 34 byte offsets (leader records)
 *
 * File format: SPCTS_SAVE_V100
 * ┌──────────────────────────────────────────────────────┐
 * │ 0x00  16 bytes  Magic: 'SPCTS_SAVE_V100\0'           │
 * │ 0x10   4 bytes  Reserved (0x00000000)                │
 * │ 0x14   …        Section stream (one per section)     │
 * └──────────────────────────────────────────────────────┘
 *
 * Each section header (9 bytes):
 * ┌──────────────────────────────────────────────────────┐
 * │ +0  uint32 LE  section_number                        │
 * │ +4  uint32 LE  bytes_to_read (may be stored negative)│
 * │ +8  uint8      compressed  (0=raw, 1=RLE)            │
 * │ +9  N bytes    rawData                               │
 * └──────────────────────────────────────────────────────┘
 *
 * RLE codec (compressed == 1):
 *   lead > 0x80  →  run:     emit (lead − 0x80) copies of next byte
 *   lead ≤ 0x80  →  literal: emit next (lead) bytes verbatim
 *
 * KNOWN SECTIONS
 * ──────────────
 *  1  Unit records         (RLE; 1000 × 261 bytes — 500 P1 + 500 P2)
 *  8  Per-hex terrain      (RLE; 15 bytes/hex × MAPSIZE × mapW)
 * 11  Unit extra names?    (RLE; large; layout TBD)
 * 17  Unit positions       (RLE; 999 × 19 bytes)
 * 34  Leader data          (RLE; 999 × 40 bytes)
 * 35  Formation data       (RLE; 400 × 60 bytes)
 *      Off  Sz  Field
 *        0  16  name            (0x0A-terminated ASCII)
 *       16   2  vpCostCode      VP cost indicator (1=HQ, 50+=company, 12=captured, 13=allied)
 *       18   1  parentUnitSlot  first unit slot of the parent formation (0 = top-level)
 *       20   1  firstUnitSlot   first unit slot belonging to this formation
 *       22   1  nationCode      nation index (matches obatNationID)
 *       26   1  firstUnitSlot2  mirror of offset 20
 *       30   1  formationIndex  sequential index within its side (0, 1, 2 …)
 *       35   1  active          1 = formation in use
 * 36  Unknown 780 bytes    (RLE)
 * 37  Scenario metadata    (RLE; 594 bytes decompressed — primary edit target)
 * 38  Map width            (raw; 4 bytes; byte[0] = column count)
 * 39  Map height           (raw; 4 bytes; byte[0] = row count)
 * 40  Unknown              (raw; 120 bytes)
 * 41  Unknown 200 bytes    (RLE)
 * 52  Per-hex data?        (RLE; 32000 bytes = MAPSIZE × mapW)
 * 54  Map label text       (RLE; 6400 bytes = 100 × 64-byte records)
 * 56  Unknown 32000        (RLE)
 * 57  Unknown 32000        (RLE)
 * 59  Unit data block      (RLE; large)
 *
 * SECTION 37 LAYOUT (594 bytes decompressed)
 * ─────────────────────────────────────────
 * Offset   Size  Field
 *  +0       1    unknown (0)
 *  +1       1    unknown (2)
 *  +2       1    unknown (62)
 *  +3       1    unknown (0)
 *  +4       1    month            (1=Jan … 12=Dec)
 *  +5       1    year             (e.g. 44 → 1944; add 1900)
 *  +6       1    weather          (0=Desert, 1=Summer, 2=Winter)
 *  +7       1    unknown
 *  +8       1    unknown
 *  +9       1    p1_nation        (nation index; game-specific table)
 * +10       1    p2_nation
 * +11       1    p3_nation
 * +12       1    p4_nation
 * +13       1    battle_type_bf1  ┐ together encode battle type:
 * +14       1    battle_type_bf2  ┘  (0,3)=P1 Advances  (1,2)=P1 Assaults
 *                                    (1,1)=Meeting Eng.  (2,1)=P2 Advances
 *                                    (3,0)=P2 Assaults
 * +15       1    max_turns
 * +16       1    unknown
 * +17..+37  21   vloc_xloc[]      (0xDE = empty slot)
 * +38..+58  21   vloc_yloc[]      (0xDE = empty slot)
 * +59..+79  21   vloc_value[]     (VP point value)
 * +80..+100 21   vloc_ctrl[]      (0=P1, 1=P2, 2=Neutral)
 * +101      1    unknown
 * +102      1    unknown
 * +103      1    visibility       (in hexes)
 * +104      1    wind_direction   (blows-to, raw byte — decode TBD)
 * +105..+136 32  unknown (zeroes)
 * +137      29   battle_location  (null-terminated ASCII, max 29 chars incl. NUL)
 * +166..+593     remainder (zeroes/unknown)
 */

'use strict';

// ─── Engine constants ─────────────────────────────────────────────────────────

const SPCAMO_MAGIC       = 'SPCTS_SAVE_V100\x00';
const SPCAMO_HEADER_SIZE = 0x14;   // 20 bytes (16 magic + 4 reserved)
const SPCAMO_MAPSIZE     = 200;    // stride constant — always 200

// Section 37 byte offsets
const S37_MONTH         =   4;
const S37_YEAR          =   5;
const S37_WEATHER       =   6;   // 0=Desert, 1=Summer, 2=Winter
const S37_P1_NATION     =   9;
const S37_P2_NATION     =  10;
const S37_P3_NATION     =  11;
const S37_P4_NATION     =  12;
const S37_BF1           =  13;
const S37_BF2           =  14;
const S37_TURNS         =  15;
const S37_VLOC_XLOC     =  17;   // base of 21-entry xloc array
const S37_VLOC_YLOC     =  38;   // base of 21-entry yloc array
const S37_VLOC_VALUE    =  59;   // base of 21-entry VP-value array
const S37_VLOC_CTRL     =  80;   // base of 21-entry control array
const S37_VLOC_COUNT    =  21;
const S37_VLOC_EMPTY    = 0xDE;  // sentinel: empty slot
const S37_VISIBILITY    = 103;
const S37_WIND          = 104;   // wind direction (blows-to), raw byte — decode TBD
const S37_LOCATION      = 137;
const S37_LOCATION_LEN  =  29;   // max chars including null terminator

// Backward-compat alias — old code used S37_SEASON for the weather byte
const S37_SEASON = S37_WEATHER;

// ─── Shared scenario lookup tables ───────────────────────────────────────────
// Weather and month names are identical between both games.

const SPCAMO_WEATHER = ['Desert', 'Summer', 'Winter'];  // index 0–2

const SPCAMO_MONTHS = [
  '', 'January', 'February', 'March', 'April', 'May', 'June',
  'July', 'August', 'September', 'October', 'November', 'December',
];

// Battle type decode table — entries: [bf1, bf2, label]
const SPCAMO_BATTLE_TYPES = [
  [0, 3, 'Player 1 Advances'],
  [1, 2, 'Player 1 Assaults'],
  [1, 1, 'Meeting Engagement'],
  [2, 1, 'Player 2 Advances'],
  [3, 0, 'Player 2 Assaults'],
];

// ─── Game-specific nation tables ──────────────────────────────────────────────

const SPWW2_NATIONS = [
  'SPCamo Renationalized',  //  0
  'Slovak Republic',        //  1
  'Poland',                 //  2
  'Manchukuo',              //  3
  'Italian Social Republic',//  4
  'Japan',                  //  5
  'France',                 //  6
  'Great Britain',          //  7
  'Vichy France',           //  8
  'Polish LWP',             //  9
  'Unused',                 // 10
  'Soviet Union',           // 11
  'USA',                    // 12
  'USMC',                   // 13
  'Chinese Communists',     // 14
  'Australia/New Zealand',  // 15
  'Germany',                // 16
  'Unused',                 // 17
  'India',                  // 18
  'Bulgaria',               // 19
  'Yugoslavia',             // 20
  'Nationalist China',      // 21
  'Unused',                 // 22
  'Unused',                 // 23
  'Unused',                 // 24
  'Unused',                 // 25
  'Unused',                 // 26
  'Belgium',                // 27
  'Netherlands',            // 28
  'Norway',                 // 29
  'Canada',                 // 30
  'Greece',                 // 31
  'Spanish Republic',       // 32
  'Spain',                  // 33
  'Italy',                  // 34
  'Finland',                // 35
  'Sweden',                 // 36
  'Czechoslovakia',         // 37
  'Hungary',                // 38
  'Romania',                // 39
  'Green',                  // 40
  'Thailand',               // 41
  'Blue',                   // 42
  'Red',                    // 43
  'Unused',                 // 44
  'Unused',                 // 45
  'Unused',                 // 46
  'Unused',                 // 47
  'Unused',                 // 48
  'Unused',                 // 49
  'For R&D only',           // 50
];

const SPMBT_NATIONS = [
  'Re-Nationalized',        //  0
  'Egypt',                  //  1
  'Poland',                 //  2
  'Syria',                  //  3
  'Israel',                 //  4
  'Japan',                  //  5
  'France',                 //  6
  'Great Britain',          //  7
  'PLO',                    //  8
  'Jordan',                 //  9
  'Iran',                   // 10
  'Russia',                 // 11
  'USA',                    // 12
  'USMC',                   // 13
  'China',                  // 14
  'Australia',              // 15
  'Gulf States',            // 16
  'Iraq',                   // 17
  'India',                  // 18
  'North Korea',            // 19
  'South Korea',            // 20
  'Taiwan',                 // 21
  'Mujahideen',             // 22
  'Vietnam',                // 23
  'South Vietnam',          // 24
  'Pakistan',               // 25
  'Afghanistan',            // 26
  'Belgium',                // 27
  'Netherlands',            // 28
  'Norway',                 // 29
  'Canada',                 // 30
  'Greece',                 // 31
  'Turkey',                 // 32
  'Spain',                  // 33
  'Italy',                  // 34
  'Denmark',                // 35
  'East Germany',           // 36
  'Czechoslovakia',         // 37
  'Hungary',                // 38
  'Romania',                // 39
  'Libya',                  // 40
  'Yugoslavia / Serbia',    // 41
  'Green',                  // 42
  'Red',                    // 43
  'Germany',                // 44
  'Argentina',              // 45
  'Austria',                // 46
  'Cyprus',                 // 47
  'Brazil',                 // 48
  'Cuba',                   // 49
  'Algeria',                // 50
  'Slovakia',               // 51
  'South Africa',           // 52
  'Switzerland',            // 53
  'Thailand',               // 54
  'Albania',                // 55
  'Angola',                 // 56
  'Bosnia-Herzegovina',     // 57
  'Cambodia',               // 58
  'Chile',                  // 59
  'Croatia',                // 60
  'Nigeria',                // 61
  'Chad',                   // 62
  'Ethiopia',               // 63
  'Saudi Arabia',           // 64
  'Finland',                // 65
  'Sweden',                 // 66
  'Sudan',                  // 67
  'Lebanon',                // 68
  'Kenya',                  // 69
  'Chechnya',               // 70
  'Indonesia',              // 71
  'Viet Minh / Viet Cong',  // 72
  'Portugal',               // 73
  'Malaysia',               // 74
  'Zimbabwe',               // 75
  'South Yemen',            // 76
  'Yemen',                  // 77
  'Eritrea',                // 78
  'Ukraine',                // 79
  'UN',                     // 80
  'Mozambique',             // 81
  'Ecuador',                // 82
  'El Salvador',            // 83
  'Nicaragua',              // 84
  'Peru',                   // 85
  'Rhodesia',               // 86
  'Bulgaria',               // 87
  'Somalia',                // 88
  'Tanzania',               // 89
  'Uganda',                 // 90
  'Uruguay',                // 91
  'Paraguay',               // 92
  'Georgia',                // 93
  'For R&D only',           // 94
  'For R&D only',           // 95
  'For R&D only',           // 96
  'For R&D only',           // 97
  'For R&D only',           // 98
  'For R&D only',           // 99
];

// ─── Game-specific unit-class tables ──────────────────────────────────────────
// classID is stored at S1 offset S1_CLASS_ID.
// Use SPMBT_UNIT_CLASSES for SP MBT, SPWW2_UNIT_CLASSES for SP WW2.

const SPMBT_UNIT_CLASSES = [
  'Fortifications',          //   0
  'Infantry',                //   1
  'Inf-AT',                  //   2
  'Cavalry',                 //   3
  'Machine Gun Section',     //   4
  'Mortar',                  //   5
  'AT Gun',                  //   6
  'AGL',                     //   7
  'Flak',                    //   8
  'Field Gun',               //   9
  'O/M Field Artillery',     //  10
  'Armoured Car',            //  11
  'Light Tank',              //  12
  'Medium Tank',             //  13
  'Heavy Tank',              //  14
  'Close Support Tank',      //  15
  'Area SAM',                //  16
  'Flame Tank',              //  17
  'Assault Gun',             //  18
  'SP ATGM',                 //  19
  'Engineer',                //  20
  'SP Artillery',            //  21
  'SP Flak',                 //  22
  'APC (Wheel)',             //  23
  'APC (Halftrack)',         //  24
  'APC (Track)',             //  25
  'Utility Vehicle',         //  26
  'Medium Truck',            //  27
  'Heavy Truck',             //  28
  'Infantry SAM',            //  29
  'SP SAM',                  //  30
  'O/M Rocket Arty',         //  31
  'Scout Vehicle',           //  32
  'Light Amphibian',         //  33
  'Heavy Amphibian',         //  34
  'Amphibian Tank',          //  35
  'Mine Clearing Vehicle',   //  36
  'Engineer Vehicle',        //  37
  'SP Mortar',               //  38
  'SP Gun',                  //  39
  'Heavy Infantry',          //  40
  'Landing Barge',           //  41
  'Assault Boat',            //  42
  'LCS',                     //  43
  'Fighter-Bomber',          //  44
  'Sniper',                  //  45
  'LCG',                     //  46
  'Glider',                  //  47
  'Barge Carrier',           //  48
  'Motorcycle',              //  49
  'Air OP Aircraft',         //  50
  'Light Infantry',          //  51
  'Wheeled SP-ATGM',         //  52
  'Helicopter',              //  53
  'Crew',                    //  54
  'HQ',                      //  55
  'Ammo Carrier',            //  56
  'FO Vehicle',              //  57
  'Scout',                   //  58
  'MBT',                     //  59
  'Paratroop Transport',     //  60
  'Transport Aircraft',      //  61
  'Level Bomber',            //  62
  'Art Observer',            //  63
  'Medium Infantry',         //  64
  'LMG Section',             //  65
  'SMG Section',             //  66
  'Rifle Section',           //  67
  'Rifle Squad',             //  68
  'Mechanised Infantry',     //  69
  'Heavy Mech Infantry',     //  70
  'Medium Mech Infantry',    //  71
  'Light Mech Infantry',     //  72
  'Mech Support Squad',      //  73
  'Mech Section',            //  74
  'Irregulars',              //  75
  'Partisans',               //  76
  'Partisan Support',        //  77
  'Partisan Band',           //  78
  'Native Troops',           //  79
  'Partisan Scouts',         //  80
  'Mech Scouts',             //  81
  'Patrol',                  //  82
  'Commandos',               //  83
  'Commando Support',        //  84
  'Light Commandos',         //  85
  'Medium Commandos',        //  86
  'Heavy Commandos',         //  87
  'Commando Scouts',         //  88
  'Guards Infantry',         //  89
  'Guards Squad',            //  90
  'Light Guards Squad',      //  91
  'Medium Guards Squad',     //  92
  'Heavy Guards Squad',      //  93
  'Guards Scouts',           //  94
  'Guards Support Squad',    //  95
  'Paratroops',              //  96
  'Paratroop Support',       //  97
  'Para light Infantry',     //  98
  'Para Medium Infantry',    //  99
  'Para SMG Squad',          // 100
  'Paratroop Scouts',        // 101
  'Reserve Tank',            // 102
  'Medium Tank',             // 103
  'CS MBT',                  // 104
  'Infantry Tank',           // 105
  'CS Infantry Tank',        // 106
  'Obsolete Heavy Tank',     // 107
  'Heavy Armoured Car',      // 108
  'Wagon',                   // 109
  'Marines',                 // 110
  'Rangers',                 // 111
  'Marine Light Infantry',   // 112
  'Marine Support Squad',    // 113
  'Ranger Support Squad',    // 114
  'Airmobile Section',       // 115
  'Airmobile Spt Section',   // 116
  'Legionnaires',            // 117
  'Legion SMG Squad',        // 118
  'Legion Support Wpns',     // 119
  'Heavy APC',               // 120
  'Light APC',               // 121
  'Light Support APC',       // 122
  'Airborne APC',            // 123
  'Airborne Support APC',    // 124
  'Drone',                   // 125
  'Gun APC (Wheeled)',       // 126
  'Gun APC (Tracked)',       // 127
  'Light SPAA Vehicle',      // 128
  'Light SP ATGM',           // 129
  'SP Howitzer',             // 130
  'Cavalry Tank',            // 131
  'Light Cavalry Tank',      // 132
  'Light SP Mortar',         // 133
  'Demolition',              // 134
  'Main Battle Tank',        // 135
  'Improvised APC',          // 136
  'SP Rocket Launcher',      // 137
  'Heavy Transporter',       // 138
  'Self Propelled Gun',      // 139
  'Pioneers',                // 140
  'Para Engineers',          // 141
  'Para Inf-AT',             // 142
  'Para Sniper',             // 143
  'Para MG',                 // 144
  'Commando Engineers',      // 145
  'Commando Inf-AT',         // 146
  'Commando Sniper',         // 147
  'Motorised Inf Section',   // 148
  'Para MG Section',         // 149
  'Cavalry Scouts',          // 150
  'Rocket Launcher',         // 151
  'Heavy Mortar',            // 152
  'Light Mortar',            // 153
  'Howitzer',                // 154
  'O/M Medium Artillery',    // 155
  'O/M Heavy Artillery',     // 156
  'Light AT Gun',            // 157
  'Heavy AT Gun',            // 158
  'Captured Tank',           // 159
  'Reserve Infantry',        // 160
  'Reserve Support Squad',    // 161
  'Ski Troops',              // 162
  'Heavy Ski Troops',        // 163
  'Ski Sniper',              // 164
  'Ski Inf-AT',              // 165
  'Ski Pioneers',            // 166
  'Snow Vehicle',            // 167
  'Ski Scouts',              // 168
  'Ski SMG Squad',           // 169
  'Guards Engineers',        // 170
  'Marine Engineers',        // 171
  'Bicycle Squad',           // 172
  'Para Mortar Team',        // 173
  'Airmobile Engineers',     // 174
  'Airborne Lt Tank',        // 175
  'Heavy MG',                // 176
  'Heavy MG Section',        // 177
  'Police',                  // 178
  'Hvy Bicycle Squad',       // 179
  'Artillery Prime Mover',   // 180
  'AA Truck',                // 181
  'Pack Animals',            // 182
  'Light Truck',             // 183
  'Airborne Field Howitzer', // 184
  'Airmobile Hvy Infantry',  // 185
  'Airmobile Rifle Squad',   // 186
  'Motorcycle(Light Inf)',   // 187
  'Motorcycle(Heavy Inf)',   // 188
  'Militia',                 // 189
  'Light Militia',           // 190
  'Heavy Militia',           // 191
  'Militia Support',         // 192
  'MG Unit',                 // 193
  'MG Section',              // 194
  'Engineer Support Squad',  // 195
  'Pack Howitzer',           // 196
  'Rivercraft',              // 197
  'Torpedo Boat',            // 198
  'Patrol Boat',             // 199
  'Mortar(Sub Type A)',      // 200
  'Mortar(Sub Type B)',      // 201
  'Mortar(Sub Type C)',      // 202
  'Attack Helicopter',       // 203
  'Light Helicopter',        // 204
  'Heavy Helicopter',        // 205
  'Inf-ATGM Team',           // 206
  'Para-ATGM Team',          // 207
  'Inf-RR Team',             // 208
  'Para-RR Team',            // 209
  'O/M Minelet Rocket Bty',  // 210
  'O/M Minelet Arty Bty',    // 211
  'Heavy Inf-ATGM',          // 212
  'Para Hy Inf-ATGM',        // 213
  'SEAD Aircraft',           // 214
  'Gunship Aircraft',        // 215
  'NO UNIT',                 // 216
  'MRV APC',                 // 217
  'Mercenaries',             // 218
  'Light MRV APC',           // 219
  'Heavy MRV APC',           // 220
  'Light Attack Helicopter', // 221
  'COIN Armed Helo',         // 222
  'COIN Fighter Plane',      // 223
  'COIN Infantry Squad',     // 224
  'COIN Lt Infantry Squad',  // 225
  'COIN Hvy Infantry Squad', // 226
  'COIN Inf Support Squad',  // 227
  'COIN Scout',              // 228
  'Para Forward Observer',   // 229
  'Guerrilla Inf Squad',     // 230
  'Guerrilla Lt Inf Squad',  // 231
  'Guerrilla Hvy Inf Squad', // 232
  'Guerrilla Inf Support',   // 233
  'Guerrilla Scout',         // 234
  'Guerrilla Sniper',        // 235
  'Guerrilla Rocket',        // 236
  'MRV IFV',                 // 237
  'MRV Light IFV',           // 238
  'Hovercraft',              // 239
  'Marine Heavy Infantry',   // 240
  'Marine Scout',            // 241
  'Marine Sniper',           // 242
  'Marine Forward Observer', // 243
  'Marine Infantry AT',      // 244
  'Rocket APC',              // 245
  'Ranger Lt Infantry Squad',// 246
  'Ranger Hvy Infantry Squad',//247
  'Ranger Scout',            // 248
  'Ranger Sniper',           // 249
  'Ranger Pioneer',          // 250
  'APC',                     // 251
  'Field Gun',               // 252
  'COIN Bomber',             // 253
  'Machinegun Team',         // 254
  'Mortar(Sub Type D)',      // 255
];

const SPWW2_UNIT_CLASSES = [
  'Fortifications',           //   0
  'Infantry',                 //   1
  'Inf-AT',                   //   2
  'Cavalry',                  //   3
  'Machine Gun Section',      //   4
  'Mortar',                   //   5
  'AT Gun',                   //   6
  'AGL',                      //   7
  'Flak',                     //   8
  'Field Gun',                //   9
  'O/M Field Artillery',      //  10
  'Armoured Car',             //  11
  'Light Tank',               //  12
  'Medium Tank',              //  13
  'Heavy Tank',               //  14
  'Close Support Tank',       //  15
  'Close Support Tank (2)',   //  16
  'Flame Tank',               //  17
  'Assault Gun',              //  18
  'TD',                       //  19
  'Engineer',                 //  20
  'SP Artillery',             //  21
  'SP Flak',                  //  22
  'APC (Wheel)',              //  23
  'APC (Halftrack)',          //  24
  'APC (Track)',              //  25
  'Utility Vehicle',          //  26
  'Medium Truck',             //  27
  'Heavy Truck',              //  28
  'Wagon',                    //  29
  'Armoured Train',           //  30
  'O/M Rocket Arty',          //  31
  'Scout Vehicle',            //  32
  'Light Amphibian',          //  33
  'Heavy Amphibian',          //  34
  'Amphibian Tank',           //  35
  'Mine Clearing Vehicle',    //  36
  'Engineer Vehicle',         //  37
  'SP Mortar',                //  38
  'SP Gun',                   //  39
  'Heavy Infantry',           //  40
  'Landing Barge',            //  41
  'Assault Boat',             //  42
  'LCS',                      //  43
  'Fighter-Bomber',           //  44
  'Sniper',                   //  45
  'LCG',                      //  46
  'Glider',                   //  47
  'Barge Carrier',            //  48
  'Motorcycle',               //  49
  'Air OP Aircraft',          //  50
  'Light Infantry',           //  51
  'SP-AT Vehicle',            //  52
  'Helicopter',               //  53
  'Crew',                     //  54
  'HQ',                       //  55
  'Ammo Carrier',             //  56
  'FO Vehicle',               //  57
  'Scout',                    //  58
  'GunTank',                  //  59
  'Paratroop Transport',      //  60
  'Transport Aircraft',       //  61
  'Level Bomber',             //  62
  'Art Observer',             //  63
  'Medium Infantry',          //  64
  'LMG Section',              //  65
  'SMG Section',              //  66
  'Rifle Section',            //  67
  'Rifle Squad',              //  68
  'Mechanised Infantry',      //  69
  'Heavy Mech Infantry',      //  70
  'Medium Mech Infantry',     //  71
  'Light Mech Infantry',      //  72
  'Mech Support Squad',       //  73
  'Mech SMG Squad',           //  74
  'Irregulars',               //  75
  'Partisans',                //  76
  'Partisan Support',         //  77
  'Partisan Band',            //  78
  'Native Troops',            //  79
  'Partisan Scouts',          //  80
  'Mech Scouts',              //  81
  'Patrol',                   //  82
  'Commandos',                //  83
  'Commando Support',         //  84
  'Light Commandos',          //  85
  'Medium Commandos',         //  86
  'Heavy Commandos',          //  87
  'Commando Scouts',          //  88
  'Guards Infantry',          //  89
  'Guards SMG Squad',         //  90
  'Light Guards Squad',       //  91
  'Medium Guards Squad',      //  92
  'Heavy Guards Squad',       //  93
  'Guards Scouts',            //  94
  'Guards Support Squad',     //  95
  'Paratroops',               //  96
  'Paratroop Support',        //  97
  'Para light Infantry',      //  98
  'Para Medium Infantry',     //  99
  'Para SMG Squad',           // 100
  'Paratroop Scouts',         // 101
  'Cruiser Tank',             // 102
  'Heavy Cruiser',            // 103
  'CS Cruiser Tank',          // 104
  'Infantry Tank',            // 105
  'CS Infantry Tank',         // 106
  'Super Heavy Tank',         // 107
  'Heavy Armoured Car',       // 108
  'Wagon',                    // 109
  'Marines',                  // 110
  'Rangers',                  // 111
  'Marine Light Infantry',    // 112
  'Marine Support Squad',     // 113
  'Ranger Support Squad',     // 114
  'Waffen SS Squad',          // 115
  'Waffen SS SMG Squad',      // 116
  'Legionnaires',             // 117
  'Legion SMG Squad',         // 118
  'Legion Support Wpns',      // 119
  'Lend-Lease Tank',          // 120
  'Tankette',                 // 121
  'Support Tankette',         // 122
  'Cruiser Gun Tank',         // 123
  'Light Infantry Tank',      // 124
  'Gun APC (HalfTrack)',      // 125
  'Gun APC (Wheeled)',        // 126
  'Gun APC (Tracked)',        // 127
  'Light SPAA Vehicle',       // 128
  'Close Support Tank',       // 129
  'SP Infantry Gun',          // 130
  'Cavalry Tank',             // 131
  'Light Cavalry Tank',       // 132
  'Light SP Mortar',          // 133
  'DD Tank',                  // 134
  'Snorkel Tank',             // 135
  'Improvised APC',           // 136
  'SP Rocket Launcher',       // 137
  'Bicycles',                 // 138
  'Self Propelled Gun',       // 139
  'Pioneers',                 // 140
  'Para Engineers',           // 141
  'Para Inf-AT',              // 142
  'Para Sniper',              // 143
  'Para MG',                  // 144
  'Commando Engineers',       // 145
  'Commando Inf-AT',          // 146
  'Commando Sniper',          // 147
  'Lancers',                  // 148
  'Cavalry Mounts',           // 149
  'Cavalry Scouts',           // 150
  'Rocket Launcher',          // 151
  'Heavy Mortar',             // 152
  'Light Mortar',             // 153
  'Infantry Howitzer',        // 154
  'Off Map Heavy Artillery',  // 155
  'Off Map Light Artillery',  // 156
  'Light AT Gun',             // 157
  'Heavy AT Gun',             // 158
  'Captured Tank',            // 159
  'Reserve Infantry',         // 160
  "Reserve Sup't Squad",      // 161
  'Ski Troops',               // 162
  'Heavy Ski Troops',         // 163
  'Ski Sniper',               // 164
  'Ski Inf-AT',               // 165
  'Ski Pioneers',             // 166
  'Snow Vehicle',             // 167
  'Ski Scouts',               // 168
  'Ski SMG Squad',            // 169
  'Guards Engineers',         // 170
  'Marine Engineers',         // 171
  'Infantry Section',         // 172
  'Para Mortar Team',         // 173
  'Waffen SS Engineers',      // 174
  'Airborne Lt Tank',         // 175
  'Gurkhas',                  // 176
  'Gurkha Hvy Section',       // 177
  'Police',                   // 178
  'Hvy Infantry Squad',       // 179
  'Artillery Prime Mover',    // 180
  'AA Truck',                 // 181
  'Pack Animals',             // 182
  'Light Truck',              // 183
  'Airborne Field Howitzer',  // 184
  'Waffen SS Hvy Infantry',   // 185
  'Waffen SS Rifle Squad',    // 186
  'Motorcycle (Light Inf)',   // 187
  'Motorcycle (Heavy Inf)',   // 188
  'Militia',                  // 189
  'Light Militia',            // 190
  'Heavy Militia',            // 191
  'Militia Support',          // 192
  'MG Unit',                  // 193
  'MG Section',               // 194
  'Engineer Support Squad',   // 195
  'Pack Howitzer',            // 196
  'Rivercraft',               // 197
  'Torpedo Boat',             // 198
  'Patrol Boat',              // 199
  'Mortar (Sub Type A)',      // 200
  'Mortar (Sub Type B)',      // 201
  'Mortar (Sub Type C)',      // 202
  'Mtn Infantry Sqd',         // 203
  'Mtn Lt Infantry Sqd',      // 204
  'Mtn Hvy Infantry Sqd',     // 205
  'Mtn Infantry-AT',          // 206
  'Mtn Pioneer Squad',        // 207
  'Mtn Scout',                // 208
  'Mtn Sniper',               // 209
  'Mtn Forward Observer',     // 210
  'Medium Cavalry',           // 211
  'Para Forward Observer',    // 212
  'Marine Heavy Infantry',    // 213
  'Marine Scout',             // 214
  'Marine Sniper',            // 215
  'Marine Forward Observer',  // 216
  'Marine Anti-Tank',         // 217
  'Mounted Infantry',         // 218
  'Ranger Pioneer',           // 219
  'Colonial Infantry',        // 220
  'Colonial Light Infantry',  // 221
  'Colonial Heavy Infantry',  // 222
  'Colonial Support Infantry',// 223
  'Colonial Field Gun',       // 224
  'Colonial Scout',           // 225
  'Marksman',                 // 226
  'Colonial Cavalry',         // 227
  'Demolition',               // 228
  'Colonial Lt Cavalry',      // 229
  'Colonial Armoured Car',    // 230
  'Colonial Tank',            // 231
  'Armoured Truck',           // 232
  'Machine Gun Section',      // 233
  'Partisan Squad',           // 234
  'Partisan Pioneer',         // 235
  'Militia Scout',            // 236
  'Field Gun',                // 237
  'Anti-Tank Gun',            // 238
  'Light Ski Infantry',       // 239
  'Light Armoured Car',       // 240
  'Machine Gun Team',         // 241
  'Mortar (Sub Type D)',      // 242
  'Ground Attack',            // 243
  'Reserved',                 // 244
  'Ranger Lt Inf Squad',      // 245
  'Ranger Hvy Inf Squad',     // 246
  'Ranger Scout',             // 247
  'Ranger Sniper',            // 248
  'Ranger Forward Observer',  // 249
  'Mountain Pioneer Support', // 250
  'Native Scouts',            // 251
  'APC',                      // 252
  'Tank',                     // 253
  'Bunker',                   // 254
  'Heavy Glider',             // 255
];

// ─── Low-level binary helpers ─────────────────────────────────────────────────

function _u8(b, off)    { return b[off]; }
function _u16le(b, off) { return b[off] | (b[off + 1] << 8); }
function _u32le(b, off) { return ((b[off] | b[off+1]<<8 | b[off+2]<<16 | b[off+3]<<24) >>> 0); }
function _i32le(b, off) { return   b[off] | b[off+1]<<8 | b[off+2]<<16 | b[off+3]<<24; }
function _str(b, off, maxLen) {
  let s = '';
  for (let i = 0; i < maxLen; i++) {
    const c = b[off + i];
    if (c === 0) break;
    s += String.fromCharCode(c);
  }
  return s;
}

// S34 names are terminated by 0x0A (linefeed) rather than 0x00.
function _str34(b, off, maxLen) {
  let s = '';
  for (let i = 0; i < maxLen; i++) {
    const c = b[off + i];
    if (c === 0 || c === 0x0A) break;
    s += String.fromCharCode(c);
  }
  return s;
}

// ─── RLE codec ────────────────────────────────────────────────────────────────

/**
 * Decompress SPCTS RLE data.
 *   lead > 0x80  →  run of (lead − 0x80) copies of next byte
 *   lead ≤ 0x80  →  (lead) literal bytes follow
 */
function rleDecompress(data) {
  const out = [];
  let i = 0;
  while (i < data.length) {
    const lead = data[i++];
    if (lead > 0x80) {
      const runLen = lead - 0x80;
      const val    = data[i++];
      for (let j = 0; j < runLen; j++) out.push(val);
    } else {
      const count = lead;
      for (let j = 0; j < count; j++) out.push(data[i++]);
    }
  }
  return new Uint8Array(out);
}

/**
 * Compress data using SPCTS RLE.
 * Produces run packets (≤127 repeat) or literal packets (≤128 bytes).
 */
function rleCompress(data) {
  const out = [];
  const n   = data.length;
  let i     = 0;
  while (i < n) {
    if (i + 1 < n && data[i] === data[i + 1]) {
      let runLen = 2;
      while (i + runLen < n && data[i + runLen] === data[i] && runLen < 127) runLen++;
      out.push(0x80 + runLen);
      out.push(data[i]);
      i += runLen;
    } else {
      const litStart = i;
      let litLen     = 0;
      while (i < n && litLen < 128) {
        if (i + 1 < n && data[i] === data[i + 1]) break;
        i++;
        litLen++;
      }
      if (litLen === 0) {
        out.push(1);
        out.push(data[i++]);
      } else {
        out.push(litLen);
        for (let j = 0; j < litLen; j++) out.push(data[litStart + j]);
      }
    }
  }
  return new Uint8Array(out);
}

// ─── Section parser ───────────────────────────────────────────────────────────

/**
 * Parse all sections from a SPCTS_SAVE_V100 ArrayBuffer.
 * Returns a plain object keyed by section number:
 *   sections[N] = { fileOffset, compressed, bytesToRead, rawData: Uint8Array }
 * Throws if the magic header is absent.
 */
function parseSections(arrayBuffer) {
  const u8 = new Uint8Array(arrayBuffer);

  const magic = String.fromCharCode(...u8.slice(0, 16));
  if (!magic.startsWith('SPCTS_SAVE_V100')) {
    throw new Error('Not a valid SPCTS_SAVE_V100 file (bad magic)');
  }

  const sections = {};
  let pos = SPCAMO_HEADER_SIZE;

  while (pos + 9 <= u8.length) {
    const secNum      = _u32le(u8, pos);
    let   bytesToRead = _i32le(u8, pos + 4);
    if (bytesToRead < 0) bytesToRead = -bytesToRead;
    const compressed  = u8[pos + 8];
    const rawData     = u8.slice(pos + 9, pos + 9 + bytesToRead);

    sections[secNum] = { fileOffset: pos, compressed, bytesToRead, rawData };
    pos += 9 + bytesToRead;
  }

  return sections;
}

/**
 * Decompress (or pass through) a section's data as a Uint8Array.
 */
function getSectionData(section) {
  if (!section) return null;
  return section.compressed ? rleDecompress(section.rawData) : section.rawData;
}

// ─── Map dimensions ───────────────────────────────────────────────────────────

/**
 * Read map width (columns) and height (rows) from sections 38 and 39.
 * Returns { w, h }.
 */
function getMapDims(sections) {
  const d38 = getSectionData(sections[38]);
  const d39 = getSectionData(sections[39]);
  return { w: d38 ? d38[0] : 0, h: d39 ? d39[0] : 0 };
}

// ─── Reader: Section 37 (scenario metadata) ───────────────────────────────────

/**
 * Read scenario metadata from section 37.
 *
 * opts.nationTable — SPWW2_NATIONS or SPMBT_NATIONS (default: SPWW2_NATIONS)
 *
 * Returns:
 * { month, monthName, year, fullYear,
 *   weather, weatherName,
 *   p1NationIdx, p1Nation, p2NationIdx, p2Nation,
 *   p3NationIdx, p3Nation, p4NationIdx, p4Nation,
 *   bf1, bf2, battleType,
 *   maxTurns, visibility, wind,
 *   location, mapW, mapH,
 *   _s37  ← raw Uint8Array for annotated-dump views }
 */
function readSPCAMOScenarioData(arrayBuffer, opts) {
  opts = opts || {};
  const nationTable = opts.nationTable || SPWW2_NATIONS;

  const sections = parseSections(arrayBuffer);
  const s37 = getSectionData(sections[37]);
  if (!s37) throw new Error('Section 37 (scenario metadata) not found');

  const { w: mapW, h: mapH } = getMapDims(sections);

  const month   = _u8(s37, S37_MONTH);
  const year    = _u8(s37, S37_YEAR);
  const weather = _u8(s37, S37_WEATHER);
  const bf1     = _u8(s37, S37_BF1);
  const bf2     = _u8(s37, S37_BF2);

  const btEntry    = SPCAMO_BATTLE_TYPES.find(([b1, b2]) => b1 === bf1 && b2 === bf2);
  const battleType = btEntry ? btEntry[2] : 'Unknown';

  const p1NationIdx = _u8(s37, S37_P1_NATION);
  const p2NationIdx = _u8(s37, S37_P2_NATION);
  const p3NationIdx = _u8(s37, S37_P3_NATION);
  const p4NationIdx = _u8(s37, S37_P4_NATION);

  return {
    month,
    monthName:   SPCAMO_MONTHS[month]    || ('Month#'   + month),
    year,
    fullYear:    1900 + year,
    weather,
    weatherName: SPCAMO_WEATHER[weather] || ('Weather#' + weather),
    p1NationIdx,
    p1Nation:    nationTable[p1NationIdx] || ('Nation#' + p1NationIdx),
    p2NationIdx,
    p2Nation:    nationTable[p2NationIdx] || ('Nation#' + p2NationIdx),
    p3NationIdx,
    p3Nation:    nationTable[p3NationIdx] || ('Nation#' + p3NationIdx),
    p4NationIdx,
    p4Nation:    nationTable[p4NationIdx] || ('Nation#' + p4NationIdx),
    bf1,
    bf2,
    battleType,
    maxTurns:    _u8(s37, S37_TURNS),
    visibility:  _u8(s37, S37_VISIBILITY),
    wind:        _u8(s37, S37_WIND),
    location:    _str(s37, S37_LOCATION, S37_LOCATION_LEN),
    mapW,
    mapH,
    _s37: s37,
  };
}

// ─── Reader: Section 37 (victory locations) ───────────────────────────────────

/**
 * Read all 21 victory location slots from section 37.
 * Returns array of 21 entries:
 *   { index, xloc, yloc, value, control, controlLabel, isEmpty }
 * isEmpty = true when xloc === 0xDE (empty sentinel).
 * control: 0=P1, 1=P2, 2=Neutral
 */
function readSPCAMOVLocs(arrayBuffer) {
  const sections = parseSections(arrayBuffer);
  const s37 = getSectionData(sections[37]);
  if (!s37) return [];

  const CTRL_LABEL = ['P1', 'P2', 'Neutral'];
  const vlocs = [];
  for (let i = 0; i < S37_VLOC_COUNT; i++) {
    const xloc    = _u8(s37, S37_VLOC_XLOC  + i);
    const yloc    = _u8(s37, S37_VLOC_YLOC  + i);
    const value   = _u8(s37, S37_VLOC_VALUE + i);
    const control = _u8(s37, S37_VLOC_CTRL  + i);
    const isEmpty = (xloc === S37_VLOC_EMPTY);
    vlocs.push({
      index: i, xloc, yloc, value, control,
      controlLabel: isEmpty ? '' : (CTRL_LABEL[control] || 'Unknown'),
      isEmpty,
    });
  }
  return vlocs;
}

// ─── Section 1: Unit sub-record constants ─────────────────────────────────────
//
// Section 1 decompresses to 261,000 bytes = 1,000 sub-records × 261 bytes each.
// Sub-records 0–499 = P1 units; 500–999 = P2 units. Empty slots: name[0] === 0.
//
// Off   Sz   Field
//   0   15   name            null-terminated ASCII
//  16    1   classID         unit type class (game-specific table, e.g. 13=Medium Tank,
//                              55=HQ); distinct from obatID (offset 163) which is the
//                              OOB index shown in the in-game editor
//  18    2   normalIcon      u16 LE
//  20    2   formID          u16 LE
//  22    1   formSubID  — sub-index within the formation (0=first unit, 1=second, …)
//  25    1   size
//  26    1   damageUnit
//  28    2   leaderRangeFirst  u16 LE — first leader slot assigned to this unit's formation
//  30    2   leaderRangeLast   u16 LE — last  leader slot assigned to this unit's formation
//  32    1   smokeDischarger
//  46    1   wpn1He / +1 wpn1Ap / +2 wpn1Sabot / +3 wpn1Heat
//  50    1   wpn2He / +1 wpn2Ap
//  54    1   wpn3He / +1 wpn3Ap
//  58    1   wpn4He / +1 wpn4Ap
//  62–97     Armor block.  Each AP face is stored as 3 identical bytes (×3).
//            SH (side hull) and ST (side turret) have an extended 8-byte block where
//            the AP value is echoed after the HEAT byte, and HEAT is echoed again at the end:
//              AP[0..2]  HEAT  AP[0..2]  HEAT   (8 bytes total for SH and ST)
//            All other faces: AP[0..2]  HEAT    (4 bytes total)
//
//            Full byte map:
//              62–64  fhAp   × 3
//                 65  fhHeat × 1
//              66–68  shAp   × 3
//                 69  shHeat × 1
//              70–72  shAp   × 3  (AP echo — write same value)
//                 73  shHeat × 1  (HEAT echo — write same value)
//              74–76  rhAp   × 3
//                 77  rhHeat × 1
//              78–80  ftAp   × 3
//                 81  ftHeat × 1
//              82–84  stAp   × 3
//                 85  stHeat × 1
//              86–88  stAp   × 3  (AP echo — write same value)
//                 89  stHeat × 1  (HEAT echo — write same value)
//              90–92  rtAp   × 3
//                 93  rtHeat × 1
//              94–96  topAp  × 3
//                 97  topHeat × 1
//  98–106  9   ERA per face — SH and ST ERA are also echoed:
//                 98  fhEra
//                 99  shEra
//                100  shEra  (echo — write same value)
//                101  rhEra
//                102  ftEra
//                103  stEra
//                104  stEra  (echo — write same value)
//                105  rtEra
//                106  topEra
// 116    1   men
// 117    1   speed
// 118    1   suppression
// 119    1   experience
// 120    1   morale
// 121    1   status
// 124    2   bombardType  u16 LE — bombardment mission type
//                        65535=None, 1001=HE, 1002=Smoke, 1003=DPICM
// 128    1   vision
// 129    2   passengerLink u16 LE — transport: chain head; passenger: prev in load order
// 131    1   loadedMen     transport: total passenger men; passenger: carry-cost flag (3)
// 132    1   radio
// 133    1   fireControl
// 134    2   cost  u16 LE (values can exceed 255)
// 136    2   leader      u16 LE; 65535=abandoned
// 138    1   bombardXloc  target hex X
// 139    1   bombardYloc  target hex Y
// 141    2   bombardSpotter u16 LE — unit ID of spotting unit
// 144    1   wpn1Smoke
// 146    2   picture     u16 LE
// 148    2   rotation1   u16 LE — unit rotation #1 (hull facing, degrees)
// 150    2   rotation2   u16 LE — unit rotation #2 (turret facing, degrees)
// 161    1   statModified  set to 1 by editor when any unit stat is manually changed;
//                          used for tech-support diagnostics and PBEM anti-cheat detection
// 162    1   obatNationID  nation index for the unit's Order of Battle (indexes into
//                        SPWW2_NATIONS / SPMBT_NATIONS — same tables used for scenario
//                        nation fields). Displayed as "[idx] NationName".
// 163    2   obatID       OOB index — which Order of Battle table supplies this unit's
//                        weapons. Differs by unit nationality/type; HQ units typically
//                        have a different value (e.g. 244) from line units (e.g. 16).
// 165    1   unitMode     bitmask — bit2=side(0=P1,1=P2), bits0-1=type
//                          0=P1 HQ/Cmd  1=P1 Normal  2=P1 Captured  3=P1 Allied
//                          4=P2 HQ/Cmd  5=P2 Normal  6=P2 Off-map   7=P2 Allied
// 168    2   slotID       u16 LE — individual slot number within the OBAT table.
// 175    2   carrier     u16 LE; 65535=not carried
// 177    1   valid
// 178    1   entr
// 180    1   isOffMap    1 = off-map artillery/support unit; 0 = on-map
// 182    1   side        0 = P1, 1 = P2  (explicit side flag — do NOT use slot position)
// 183    1   rateOfFire
// 184    1   stabilizer
// 185    1   rangeFinder
// 186    1   ew
// 187    1   carryCapacity
// 189    1   costToCarry
// 192    1   waterSpeed
// 195    1   survivability
// 208    1   wpn1Id / 209 wpn2Id / 210 wpn3Id / 211 wpn4Id
// 215    1   altitude
// 220    1   wpn1Sound / 221 wpn2Sound / 222 wpn3Sound / 223 wpn4Sound
// 248    2   winterIcon  u16 LE
// 250    2   desertIcon  u16 LE

// Unit purchase mode — S1 offset 165.
// Bitmask: bit2 = side (0=P1, 1=P2), bits0-1 = purchase type (0=HQ, 1=Normal, 2=Captured/Off-map, 3=Allied)
const S1_UNIT_MODE_NAMES = [
  'P1 HQ/Command',  // 0  (000)
  'P1 Normal',      // 1  (001)
  'P1 Captured',    // 2  (010)
  'P1 Allied',      // 3  (011)
  'P2 HQ/Command',  // 4  (100)
  'P2 Normal',      // 5  (101)
  'P2 Off-map',     // 6  (110)
  'P2 Allied',      // 7  (111)
];

// Formation letter lookup — formID (offset 20) indexes into this table.
// 0="A", 1="B", … 25="Z", 26="AA", … 199="GR"
const SPCAMO_FORMATION_LETTERS = [
  'A','B','C','D','E','F','G','H','I','J','K','L','M',
  'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
  'AA','AB','AC','AD','AE','AF','AG','AH','AI','AJ','AK','AL','AM',
  'AN','AO','AP','AQ','AR','AS','AT','AU','AV','AW','AX','AY','AZ',
  'BA','BB','BC','BD','BE','BF','BG','BH','BI','BJ','BK','BL','BM',
  'BN','BO','BP','BQ','BR','BS','BT','BU','BV','BW','BX','BY','BZ',
  'CA','CB','CC','CD','CE','CF','CG','CH','CI','CJ','CK','CL','CM',
  'CN','CO','CP','CQ','CR','CS','CT','CU','CV','CW','CX','CY','CZ',
  'DA','DB','DC','DD','DE','DF','DG','DH','DI','DJ','DK','DL','DM',
  'DN','DO','DP','DQ','DR','DS','DT','DU','DV','DW','DX','DY','DZ',
  'EA','EB','EC','ED','EE','EF','EG','EH','EI','EJ','EK','EL','EM',
  'EN','EO','EP','EQ','ER','ES','ET','EU','EV','EW','EX','EY','EZ',
  'FA','FB','FC','FD','FE','FF','FG','FH','FI','FJ','FK','FL','FM',
  'FN','FO','FP','FQ','FR','FS','FT','FU','FV','FW','FX','FY','FZ',
  'GA','GB','GC','GD','GE','GF','GG','GH','GI','GJ','GK','GL','GM',
  'GN','GO','GP','GQ','GR',
];

const S1_SUB_SIZE        = 261;
const S1_TOTAL_UNITS     = 1000;
const S1_P2_OFFSET       = 500;
const S1_NAME            =   0;
const S1_NAME_LEN        =  15;
const S1_CLASS_ID        =  16;
const S1_NORMAL_ICON     =  18;
const S1_FORM_ID         =  20;
const S1_FORM_SUB_ID     =  22;   // was: S1_MIN_FORM
const S1_MIN_FORM        = S1_FORM_SUB_ID;  // backward-compat alias
const S1_SIZE            =  25;
const S1_WPN1_TYPE       =  26;  // u16 LE — weapon slot 1 type/category code (0=occupied-unknown, 0xFFFF=empty)
const S1_WPN2_TYPE       =  28;  // u16 LE — weapon slot 2 type/category code
const S1_WPN3_TYPE       =  30;  // u16 LE — weapon slot 3 type/category code
const S1_WPN4_TYPE       =  32;  // u16 LE — weapon slot 4 type/category code
// NOTE: these were previously (wrongly) labelled S1_DAMAGE_UNIT, S1_LEADER_RANGE_FIRST,
// S1_LEADER_RANGE_LAST, S1_SMOKE_SHOTS. There are no leaderRangeFirst/Last fields
// at these offsets — they are all wp_type entries per the WarCab struct.
const S1_WPN1_HE         =  46;
const S1_WPN1_AP         =  47;
const S1_WPN1_SABOT      =  48;
const S1_WPN1_HEAT       =  49;
const S1_WPN2_HE         =  50;
const S1_WPN2_AP         =  51;
const S1_WPN3_HE         =  54;
const S1_WPN3_AP         =  55;
const S1_WPN4_HE         =  58;
const S1_WPN4_AP         =  59;
// ── Armor face offsets (62–106) ──────────────────────────────────────────────
// AP: each face stored as 3 consecutive identical bytes — all must be written.
// SH/ST HEAT: interleaved with AP echoes (HEAT, AP, AP, AP, HEAT — 5 bytes).
// ERA: each face has its own byte; SH and ST also have a second echo byte.
//
// Front Hull
const S1_FH_AP_1         =  62;  // front hull AP echo 1  ─┐
const S1_FH_AP_2         =  63;  // front hull AP echo 2   ├ all three must be identical
const S1_FH_AP_3         =  64;  // front hull AP echo 3  ─┘
const S1_FH_HEAT         =  65;  // front hull HEAT
const S1_FH_ERA          =  98;  // front hull ERA
// Side Hull
const S1_SH_AP_1         =  66;  // side hull AP echo 1  ─┐
const S1_SH_AP_2         =  67;  // side hull AP echo 2   │ all six must be identical
const S1_SH_AP_3         =  68;  // side hull AP echo 3   │
const S1_SH_HEAT_1       =  69;  // side hull HEAT echo 1 ─┐ both must be identical
const S1_SH_AP_4         =  70;  // side hull AP echo 4   │
const S1_SH_AP_5         =  71;  // side hull AP echo 5   │
const S1_SH_AP_6         =  72;  // side hull AP echo 6  ─┘
const S1_SH_HEAT_2       =  73;  // side hull HEAT echo 2 ─┘
const S1_SH_ERA          =  99;  // side hull ERA  ─┐ both must be identical
const S1_SH_ERA_2        = 100;  // side hull ERA echo 2 ─┘
// Rear Hull
const S1_RH_AP_1         =  74;  // rear hull AP echo 1  ─┐
const S1_RH_AP_2         =  75;  // rear hull AP echo 2   ├ all three must be identical
const S1_RH_AP_3         =  76;  // rear hull AP echo 3  ─┘
const S1_RH_HEAT         =  77;  // rear hull HEAT
const S1_RH_ERA          = 101;  // rear hull ERA
// Front Turret
const S1_FT_AP_1         =  78;  // front turret AP echo 1  ─┐
const S1_FT_AP_2         =  79;  // front turret AP echo 2   ├ all three must be identical
const S1_FT_AP_3         =  80;  // front turret AP echo 3  ─┘
const S1_FT_HEAT         =  81;  // front turret HEAT
const S1_FT_ERA          = 102;  // front turret ERA
// Side Turret
const S1_ST_AP_1         =  82;  // side turret AP echo 1  ─┐
const S1_ST_AP_2         =  83;  // side turret AP echo 2   │ all six must be identical
const S1_ST_AP_3         =  84;  // side turret AP echo 3   │
const S1_ST_HEAT_1       =  85;  // side turret HEAT echo 1 ─┐ both must be identical
const S1_ST_AP_4         =  86;  // side turret AP echo 4   │
const S1_ST_AP_5         =  87;  // side turret AP echo 5   │
const S1_ST_AP_6         =  88;  // side turret AP echo 6  ─┘
const S1_ST_HEAT_2       =  89;  // side turret HEAT echo 2 ─┘
const S1_ST_ERA          = 103;  // side turret ERA  ─┐ both must be identical
const S1_ST_ERA_2        = 104;  // side turret ERA echo 2 ─┘
// Rear Turret
const S1_RT_AP_1         =  90;  // rear turret AP echo 1  ─┐
const S1_RT_AP_2         =  91;  // rear turret AP echo 2   ├ all three must be identical
const S1_RT_AP_3         =  92;  // rear turret AP echo 3  ─┘
const S1_RT_HEAT         =  93;  // rear turret HEAT
const S1_RT_ERA          = 105;  // rear turret ERA
// Top
const S1_TOP_AP_1        =  94;  // top AP echo 1  ─┐
const S1_TOP_AP_2        =  95;  // top AP echo 2   ├ all three must be identical
const S1_TOP_AP_3        =  96;  // top AP echo 3  ─┘
const S1_TOP_HEAT        =  97;  // top HEAT
const S1_TOP_ERA         = 106;  // top ERA
const S1_MEN             = 116;
const S1_SPEED           = 117;
const S1_SUPPRESSION     = 118;
const S1_EXPERIENCE      = 119;
const S1_MORALE          = 120;
const S1_STATUS          = 121;
const S1_MAX_FIRE_RANGE  = 127;  // u8 — maximum fire range cap (0 = no cap / default)
const S1_BOMBARD_TYPE    = 124;  // u16 LE — bombardment mission type (65535=None, 1001=HE, 1002=Smoke, 1003=DPICM)
const S1_VISION          = 128;
const S1_PASSENGER_LINK  = 129;  // u16 LE — on transport: first passenger (chain head);
                                  //          on passenger: previous passenger in load order
                                  //          0xFFFF = no link (empty transport or first-loaded passenger)
const S1_LOADED_MEN      = 131;  // u8 — on transport: total men count of all loaded passengers
                                  //      on passenger: set to 3 (carry-cost flag)
const S1_RADIO           = 132;
const S1_FIRE_CONTROL    = 133;
const S1_COST            = 134;  // u16 LE — values can exceed 255
const S1_LEADER          = 136;
const S1_BOMBARD_XLOC    = 138;  // u8 — bombardment target hex X
const S1_BOMBARD_YLOC    = 139;  // u8 — bombardment target hex Y
const S1_BOMBARD_SPOTTER = 141;  // u16 LE — unit ID of spotting unit
const S1_WPN1_SMOKE      = 144;  // u8 — main gun smoke rounds for wpn1.
                                   // NOTE: SPWW2 randomly assigns this per unit on scenario load
                                   // for stock (stat_modified=0) units — do not rely on a fixed value.
const S1_PICTURE         = 146;
const S1_ROTATION1       = 148;  // u16 LE — unit rotation #1 (hull facing, degrees)
const S1_ROTATION2       = 150;  // u16 LE — unit rotation #2 (turret facing, degrees)
const S1_STAT_MODIFIED   = 161;  // set to 1 by editor on any manual stat change; used for
                                  // tech-support diagnostics and PBEM anti-cheat detection
const S1_OBAT_NATION_ID  = 162;  // nation index for the unit's OOB (indexes SPWW2/SPMBT_NATIONS)
const S1_NATION_ID       = S1_OBAT_NATION_ID;  // backward-compat alias
const S1_OBAT_ID         = 163;  // OOB table index
const S1_UNIT_MODE       = 165;  // 0=HQ/command, 1=Normal, 2=Captured, 3=Allied
const S1_SLOT_ID         = 168;  // u16 LE — slot within the OOB table
const S1_CARRIER         = 175;
const S1_VALID           = 177;
const S1_ENTR            = 178;
const S1_IS_OFF_MAP      = 180;  // 1 = off-map unit
const S1_SIDE            = 182;  // 0=P1, 1=P2 — explicit side flag
const S1_RATE_OF_FIRE    = 183;
const S1_STABILIZER      = 184;
const S1_RANGE_FINDER    = 185;
const S1_EW              = 186;
const S1_CARRY_CAPACITY  = 187;
const S1_COST_TO_CARRY   = 189;
const S1_WATER_SPEED     = 192;
const S1_SURVIVABILITY   = 195;
const S1_WPN1_ID         = 208;
const S1_WPN2_ID         = 209;
const S1_WPN3_ID         = 210;
const S1_WPN4_ID         = 211;
const S1_NR_SD           = 212;  // u8 — number of smoke discharger units (OBAT nrSD field)
const S1_ALTITUDE        = 215;
const S1_WPN1_SOUND      = 220;
const S1_WPN2_SOUND      = 221;
const S1_WPN3_SOUND      = 222;
const S1_WPN4_SOUND      = 223;
const S1_WINTER_ICON     = 248;
const S1_DESERT_ICON     = 250;
const S1_OOB_NUMBER      = S1_CLASS_ID;  // legacy alias

const S1_STATUS_NAMES = [
  'Ready', 'Pinned', 'Retreating', 'Routed', 'Abandoned',
  'Destroyed', 'Burning', 'Buttoned', 'Immobilized', 'Passenger',
];

const S1_ALTITUDE_NAMES = ['Ground/Landed', 'Nap of Earth', 'High Altitude'];

const S1_BOMBARD_TYPE_NAMES = {
  0xFFFF: 'No Mission',
  1001:   'HE Bombardment',
  1002:   'Smoke Bombardment',
  1003:   'DPICM Bombardment',
};
const S1_BOMBARD_TYPES = [
  { value: 0xFFFF, label: 'No Mission' },
  { value: 1001,   label: 'HE Bombardment' },
  { value: 1002,   label: 'Smoke Bombardment' },
  { value: 1003,   label: 'DPICM Bombardment' },
];

// ─── Section 17: Unit position constants ──────────────────────────────────────
// RLE. 999 records × 19 bytes. Index matches Section 1 slot.
//   +0  u16 LE  xloc
//   +2  u16 LE  yloc

const S17_RECORD_SIZE = 19;
const S17_XLOC_OFF    =  0;
const S17_YLOC_OFF    =  2;

// ─── S17 Obstacle Record Layout (19 bytes, same as unit position records) ────
// Obstacles live in S17 beyond the first 1000 unit-position records.
// Block 2 (recs 2000-2999): Dragon's Teeth (typ=0, strength = count×10)
// Block 6 (recs 6000-6999): Mines (typ=1) + Barbed Wire (typ=0, tile=1491, str=1004)
// Block 4 (recs 4000-4999): Position shadow markers (str=0xFFFF)
// Per-hex flag sections (1 byte/hex, column-major, stride=MAPSIZE):
//   S9:  Mine presence    S52: DT presence    S56: BW presence
const S17_OBS_RECSIZE  = 19;
const S17_OBS_DT_START = 2000;   // block 2 start
const S17_OBS_DT_END   = 3000;
const S17_OBS_MN_START = 6000;   // block 6 start (mines + BW)
const S17_OBS_MN_END   = 7000;
const S17_OBS_TILE_BW  = 1491;   // barbed wire tile ID
const S17_OBS_STR_BW   = 1004;   // barbed wire strength value

// ─── Section 34: Leader record constants ──────────────────────────────────────
// RLE. 999 records × 40 bytes.
//   +0   16  name  ASCIIZ
//   +16   1  unitID
//   +19   1  rank
//   +20   1  rally
//   +21   1  infantry
//   +22   1  artillery
//   +23   1  armor
//   +27   2  kills  u16 LE
//   +29   1  status  (0=Dead, 1=Alive)

const S34_RECORD_SIZE  = 40;
const S34_SLOT_COUNT   = 999;
const S34_NAME         =  0;
const S34_NAME_LEN     = 16;
const S34_UNIT_ID      = 16;
const S34_RANK         = 19;
const S34_RALLY        = 20;
const S34_INFANTRY     = 21;
const S34_ARTILLERY    = 22;
const S34_ARMOR        = 23;
const S34_KILLS        = 27;
const S34_STATUS       = 29;

const S34_RANK_NAMES = [
  'KIA', 'Private', 'Corporal', 'Sergeant',
  '2nd Lieutenant', '1st Lieutenant', 'Captain',
  'Major', 'Lt Colonel', 'Colonel', 'General',
];

const S34_STATUS_NAMES = ['Dead', 'Alive'];

// ─── Reader: Section 17 (unit positions) ──────────────────────────────────────

/**
 * Read unit X/Y positions from section 17.
 * Returns array of up to 999 entries: { xloc, yloc }
 * Index matches section 1 slot.
 */
function readSPCAMOPositions(arrayBuffer) {
  const sections = parseSections(arrayBuffer);
  const s17 = getSectionData(sections[17]);
  if (!s17) return [];

  const count = Math.min(999, Math.floor(s17.length / S17_RECORD_SIZE));
  const out   = [];
  for (let i = 0; i < count; i++) {
    const base = i * S17_RECORD_SIZE;
    out.push({
      xloc: _u16le(s17, base + S17_XLOC_OFF),
      yloc: _u16le(s17, base + S17_YLOC_OFF),
    });
  }
  return out;
}

// ─── Reader: Section 34 (leaders) ────────────────────────────────────────────

/**
 * Read all leader records from section 34.
 * includeEmpty=false (default) skips unnamed slots.
 *
 * Returns array of:
 * { index, name, unitID, rank, rankName,
 *   rally, infantry, artillery, armor, kills,
 *   status, statusName }
 */
function readSPCAMOLeaders(arrayBuffer, includeEmpty) {
  includeEmpty = includeEmpty || false;
  const sections = parseSections(arrayBuffer);
  const s34 = getSectionData(sections[34]);
  if (!s34) return [];

  const count   = Math.min(S34_SLOT_COUNT, Math.floor(s34.length / S34_RECORD_SIZE));
  const leaders = [];
  for (let i = 0; i < count; i++) {
    const base = i * S34_RECORD_SIZE;
    const name = _str34(s34, base + S34_NAME, S34_NAME_LEN);
    if (!name && !includeEmpty) continue;

    const rank   = _u8(s34, base + S34_RANK);
    const status = _u8(s34, base + S34_STATUS);
    leaders.push({
      index:      i,
      name,
      unitID:     _u16le(s34, base + S34_UNIT_ID),
      rank,
      rankName:   S34_RANK_NAMES[rank]    || ('Rank#'   + rank),
      rally:      _u8(s34, base + S34_RALLY),
      infantry:   _u8(s34, base + S34_INFANTRY),
      artillery:  _u8(s34, base + S34_ARTILLERY),
      armor:      _u8(s34, base + S34_ARMOR),
      kills:      _u16le(s34, base + S34_KILLS),
      status,
      statusName: S34_STATUS_NAMES[status] || ('Status#' + status),
    });
  }
  return leaders;
}

// ─── Reader: Section 1 (units) ───────────────────────────────────────────────

/**
 * Read all unit records from section 1.
 *
 * opts.classTable — SPWW2_UNIT_CLASSES or SPMBT_UNIT_CLASSES
 *                   (default: SPWW2_UNIT_CLASSES)
 *
 * Returns { p1: [...], p2: [...] }
 * Each unit entry: { index, side, name, classID, className,
 *   formID, minForm, obatNationID, nationID, obatID, slotID, valid, entr,
 *   normalIcon, winterIcon, desertIcon, picture,
 *   radio, size, damageUnit, smokeDischarger,
 *   men, speed, suppression, waterSpeed, experience, morale,
 *   status, statusName, leader, carrier, altitude, altitudeName,
 *   xloc, yloc,
 *   vision, fireControl, cost, rateOfFire, stabilizer,
 *   rangeFinder, ew, carryCapacity, costToCarry, survivability,
 *   armor: { fhAp, fhHeat, fhEra, shAp, shHeat, shEra,
 *            rhAp, rhHeat, rhEra, ftAp, ftHeat, ftEra,
 *            stAp, stHeat, stEra, rtAp, rtHeat, rtEra,
 *            topAp, topHeat, topEra },
 *   weapons: [ { id, sound, he, ap, sabot, heat, smoke } × 4 ] }
 * Empty slots (name === '') are omitted.
 */
function readSPCAMOUnits(arrayBuffer, opts) {
  opts = opts || {};
  const classTable = opts.classTable || SPWW2_UNIT_CLASSES;

  const sections = parseSections(arrayBuffer);
  const s1 = getSectionData(sections[1]);
  if (!s1) return { p1: [], p2: [] };

  const s17      = getSectionData(sections[17]);
  const posCount = s17 ? Math.min(S1_TOTAL_UNITS, Math.floor(s17.length / S17_RECORD_SIZE)) : 0;

  function readSub(idx) {
    const base = idx * S1_SUB_SIZE;
    if (base + S1_SUB_SIZE > s1.length) return null;
    const b    = s1;
    const name = _str(b, base + S1_NAME, S1_NAME_LEN);
    if (!name) return null;

    const classID = _u8(b, base + S1_CLASS_ID);
    const status  = _u8(b, base + S1_STATUS);
    const alt     = _u8(b, base + S1_ALTITUDE);

    return {
      index:          idx,
      side:           _u8(b, base + S1_SIDE),
      name,
      classID,
      className:      classTable[classID]  || ('Class#'  + classID),
      formID:         _u16le(b, base + S1_FORM_ID),
      formSubID:      _u8(b, base + S1_FORM_SUB_ID),
      get formLetter()    { return SPCAMO_FORMATION_LETTERS[this.formID] || ('Form#' + this.formID); },
      get formationCode() { return (SPCAMO_FORMATION_LETTERS[this.formID] || ('Form#' + this.formID)) + this.formSubID; },
      obatNationID:   _u8(b, base + S1_OBAT_NATION_ID),
      nationID:       _u8(b, base + S1_OBAT_NATION_ID),  // backward-compat alias
      obatID:         _u16le(b, base + S1_OBAT_ID),
      unitMode:       _u8(b, base + S1_UNIT_MODE),
      unitModeName:   S1_UNIT_MODE_NAMES[_u8(b, base + S1_UNIT_MODE)] || ('Mode#' + _u8(b, base + S1_UNIT_MODE)),
      slotID:         _u16le(b, base + S1_SLOT_ID),
      valid:          _u8(b, base + S1_VALID),
      entr:           _u8(b, base + S1_ENTR),
      isOffMap:       !!_u8(b, base + S1_IS_OFF_MAP),
      normalIcon:     _u16le(b, base + S1_NORMAL_ICON),
      winterIcon:     _u16le(b, base + S1_WINTER_ICON),
      desertIcon:     _u16le(b, base + S1_DESERT_ICON),
      picture:        _u16le(b, base + S1_PICTURE),
      rotation1:      _u16le(b, base + S1_ROTATION1),
      rotation2:      _u16le(b, base + S1_ROTATION2),
      radio:          _u8(b, base + S1_RADIO),
      size:           _u8(b, base + S1_SIZE),
      wpnType: [
        _u16le(b, base + S1_WPN1_TYPE),
        _u16le(b, base + S1_WPN2_TYPE),
        _u16le(b, base + S1_WPN3_TYPE),
        _u16le(b, base + S1_WPN4_TYPE),
      ],
      smokeDischarger:_u8(b, base + S1_NR_SD),
      men:            _u8(b, base + S1_MEN),
      speed:          _u8(b, base + S1_SPEED),
      suppression:    _u8(b, base + S1_SUPPRESSION),
      waterSpeed:     _u8(b, base + S1_WATER_SPEED),
      experience:     _u8(b, base + S1_EXPERIENCE),
      morale:         _u8(b, base + S1_MORALE),
      status,
      statusName:     S1_STATUS_NAMES[status]  || ('Status#' + status),
      bombardType:    _u16le(b, base + S1_BOMBARD_TYPE),
      bombardTypeName:S1_BOMBARD_TYPE_NAMES[_u16le(b, base + S1_BOMBARD_TYPE)] || ('Type#' + _u16le(b, base + S1_BOMBARD_TYPE)),
      bombardXloc:    _u8(b, base + S1_BOMBARD_XLOC),
      bombardYloc:    _u8(b, base + S1_BOMBARD_YLOC),
      bombardSpotter: _u16le(b, base + S1_BOMBARD_SPOTTER),
      leader:         _u16le(b, base + S1_LEADER),
      leaderStatus:   _u16le(b, base + S1_LEADER) === 0xFFFF ? 'Abandoned' : 'Manned',
      carrier:        _u16le(b, base + S1_CARRIER),
      isOnMap:        _u16le(b, base + S1_CARRIER) === 0xFFFF,
      passengerLink:  _u16le(b, base + S1_PASSENGER_LINK),
      loadedMen:      _u8(b, base + S1_LOADED_MEN),
      altitude:       alt,
      altitudeName:   S1_ALTITUDE_NAMES[alt]   || ('Alt#'    + alt),
      xloc: (s17 && idx < posCount) ? _u16le(s17, idx * S17_RECORD_SIZE + S17_XLOC_OFF) : null,
      yloc: (s17 && idx < posCount) ? _u16le(s17, idx * S17_RECORD_SIZE + S17_YLOC_OFF) : null,
      vision:         _u8(b, base + S1_VISION),
      fireControl:    _u8(b, base + S1_FIRE_CONTROL),
      cost:           _u16le(b, base + S1_COST),
      rateOfFire:     _u8(b, base + S1_RATE_OF_FIRE),
      stabilizer:     _u8(b, base + S1_STABILIZER),
      rangeFinder:    _u8(b, base + S1_RANGE_FINDER),
      maxFireRange:   _u8(b, base + S1_MAX_FIRE_RANGE),
      ew:             _u8(b, base + S1_EW),
      carryCapacity:  _u8(b, base + S1_CARRY_CAPACITY),
      costToCarry:    _u8(b, base + S1_COST_TO_CARRY),
      survivability:  _u8(b, base + S1_SURVIVABILITY),
      armor: {
        fhAp:  _u8(b, base + S1_FH_AP_1),  fhHeat:  _u8(b, base + S1_FH_HEAT),   fhEra:  _u8(b, base + S1_FH_ERA),
        shAp:  _u8(b, base + S1_SH_AP_1),  shHeat:  _u8(b, base + S1_SH_HEAT_1), shEra:  _u8(b, base + S1_SH_ERA),
        rhAp:  _u8(b, base + S1_RH_AP_1),  rhHeat:  _u8(b, base + S1_RH_HEAT),   rhEra:  _u8(b, base + S1_RH_ERA),
        ftAp:  _u8(b, base + S1_FT_AP_1),  ftHeat:  _u8(b, base + S1_FT_HEAT),   ftEra:  _u8(b, base + S1_FT_ERA),
        stAp:  _u8(b, base + S1_ST_AP_1),  stHeat:  _u8(b, base + S1_ST_HEAT_1), stEra:  _u8(b, base + S1_ST_ERA),
        rtAp:  _u8(b, base + S1_RT_AP_1),  rtHeat:  _u8(b, base + S1_RT_HEAT),   rtEra:  _u8(b, base + S1_RT_ERA),
        topAp: _u8(b, base + S1_TOP_AP_1), topHeat: _u8(b, base + S1_TOP_HEAT),  topEra: _u8(b, base + S1_TOP_ERA),
      },
      weapons: [
        { id:_u8(b,base+S1_WPN1_ID), sound:_u8(b,base+S1_WPN1_SOUND),
          he:_u8(b,base+S1_WPN1_HE), ap:_u8(b,base+S1_WPN1_AP),
          sabot:_u8(b,base+S1_WPN1_SABOT), heat:_u8(b,base+S1_WPN1_HEAT),
          smoke:_u8(b,base+S1_WPN1_SMOKE) },
        { id:_u8(b,base+S1_WPN2_ID), sound:_u8(b,base+S1_WPN2_SOUND),
          he:_u8(b,base+S1_WPN2_HE), ap:_u8(b,base+S1_WPN2_AP),
          sabot:0, heat:0, smoke:0 },
        { id:_u8(b,base+S1_WPN3_ID), sound:_u8(b,base+S1_WPN3_SOUND),
          he:_u8(b,base+S1_WPN3_HE), ap:_u8(b,base+S1_WPN3_AP),
          sabot:0, heat:0, smoke:0 },
        { id:_u8(b,base+S1_WPN4_ID), sound:_u8(b,base+S1_WPN4_SOUND),
          he:_u8(b,base+S1_WPN4_HE), ap:_u8(b,base+S1_WPN4_AP),
          sabot:0, heat:0, smoke:0 },
      ],
    };
  }

  const p1 = [], p2 = [];
  for (let i = 0; i < S1_TOTAL_UNITS; i++) {
    const u = readSub(i);
    if (!u) continue;
    if (u.side === 0) p1.push(u); else p2.push(u);
  }

  // Normalise formIDs per side.
  // The engine stores formation IDs as a single global sequence across both sides,
  // so P2's raw formIDs are offset into higher numbers.  Each side's display
  // labels must restart at "A", so we build a dense rank map per side:
  // sorted unique raw formIDs → 0, 1, 2 …
  // Normalise formIDs per side for display label generation ("A","B","C"…).
  // WARNING: this modifies u.formID in place.  S1_FORM_ID is a GLOBAL S35 record
  // index, so P2 raw formIDs are in a higher range (e.g. 27–51).  After normalisation
  // they become 0-based per side.  writeSub writes the normalised value, which means
  // a load+save round-trip of a scenario with existing P2 units will corrupt their
  // formIDs.  This is a known limitation; for the import workflow P2 units are
  // freshly allocated with globally-offset formIDs and are unaffected.
  function normaliseFormIDs(units) {
    const sorted = [...new Set(units.map(u => u.formID))].sort((a, b) => a - b);
    const rank   = new Map(sorted.map((id, i) => [id, i]));
    units.forEach(u => { u.formID = rank.get(u.formID) ?? u.formID; });
  }
  normaliseFormIDs(p1);
  normaliseFormIDs(p2);

  return { p1, p2 };
}

// ─── Writer: Section 37 (scenario metadata) ──────────────────────────────────

/**
 * Write scenario metadata into a copy of the file buffer.
 * Any undefined opts key is left unchanged in the file.
 *
 * opts = {
 *   month,          // 1–12
 *   year,           // 2-digit, e.g. 44
 *   weather,        // 0=Desert, 1=Summer, 2=Winter
 *   p1NationIdx, p2NationIdx, p3NationIdx, p4NationIdx,
 *   bf1, bf2,
 *   maxTurns,
 *   visibility,
 *   wind,           // raw byte 0–255
 *   location,       // string ≤28 chars
 * }
 *
 * Returns a new ArrayBuffer.
 */
function writeSPCAMOScenarioData(arrayBuffer, opts) {
  const sections = parseSections(arrayBuffer);
  const s37 = getSectionData(sections[37]);
  if (!s37) throw new Error('Section 37 not found');

  if (opts.month       !== undefined) s37[S37_MONTH]      = opts.month       & 0xFF;
  if (opts.year        !== undefined) s37[S37_YEAR]       = opts.year        & 0xFF;
  if (opts.weather     !== undefined) s37[S37_WEATHER]    = opts.weather     & 0xFF;
  if (opts.p1NationIdx !== undefined) s37[S37_P1_NATION]  = opts.p1NationIdx & 0xFF;
  if (opts.p2NationIdx !== undefined) s37[S37_P2_NATION]  = opts.p2NationIdx & 0xFF;
  if (opts.p3NationIdx !== undefined) s37[S37_P3_NATION]  = opts.p3NationIdx & 0xFF;
  if (opts.p4NationIdx !== undefined) s37[S37_P4_NATION]  = opts.p4NationIdx & 0xFF;
  if (opts.bf1         !== undefined) s37[S37_BF1]        = opts.bf1         & 0xFF;
  if (opts.bf2         !== undefined) s37[S37_BF2]        = opts.bf2         & 0xFF;
  if (opts.maxTurns    !== undefined) s37[S37_TURNS]      = opts.maxTurns    & 0xFF;
  if (opts.visibility  !== undefined) s37[S37_VISIBILITY] = opts.visibility  & 0xFF;
  if (opts.wind        !== undefined) s37[S37_WIND]       = opts.wind        & 0xFF;
  if (opts.location    !== undefined) {
    for (let i = 0; i < S37_LOCATION_LEN; i++) s37[S37_LOCATION + i] = 0;
    const loc = opts.location.slice(0, S37_LOCATION_LEN - 1);
    for (let i = 0; i < loc.length; i++) s37[S37_LOCATION + i] = loc.charCodeAt(i);
  }

  return _rebuildFile(arrayBuffer, sections, { 37: s37 });
}

// ─── Writer: Section 37 (victory locations) ──────────────────────────────────

/**
 * Write all 21 victory location slots into section 37.
 *
 * vlocs — array of up to 21 entries:
 *   { xloc, yloc, value, control, isEmpty }
 *   isEmpty=true → slot written as 0xDE sentinel
 *   control: 0=P1, 1=P2, 2=Neutral (default: 2)
 *
 * Returns a new ArrayBuffer.
 */
function writeSPCAMOVLocs(arrayBuffer, vlocs) {
  const sections = parseSections(arrayBuffer);
  const s37 = getSectionData(sections[37]).slice();  // mutable copy

  for (let i = 0; i < S37_VLOC_COUNT; i++) {
    const v = vlocs[i];
    if (v && !v.isEmpty) {
      s37[S37_VLOC_XLOC  + i] = v.xloc                                    & 0xFF;
      s37[S37_VLOC_YLOC  + i] = v.yloc                                    & 0xFF;
      s37[S37_VLOC_VALUE + i] = v.value                                   & 0xFF;
      s37[S37_VLOC_CTRL  + i] = (v.control !== undefined ? v.control : 2) & 0xFF;
    } else {
      s37[S37_VLOC_XLOC  + i] = S37_VLOC_EMPTY;
      s37[S37_VLOC_YLOC  + i] = S37_VLOC_EMPTY;
      s37[S37_VLOC_VALUE + i] = 0;
      s37[S37_VLOC_CTRL  + i] = 2;
    }
  }

  return _rebuildFile(arrayBuffer, sections, { 37: s37 });
}

// ─── File rebuilder ───────────────────────────────────────────────────────────

/**
 * Rebuild the full file buffer, replacing the named sections.
 * replacements = { secNum: Uint8Array (decompressed), … }
 * Each replaced section retains its original compressed/raw flag.
 * Returns a new ArrayBuffer.
 */
function _rebuildFile(origBuffer, sections, replacements) {
  const origU8 = new Uint8Array(origBuffer);
  const header = origU8.slice(0, SPCAMO_HEADER_SIZE);

  const secNums = Object.keys(sections).map(Number).sort((a, b) => a - b);
  const chunks  = [header];

  for (const secNum of secNums) {
    const orig = sections[secNum];
    let newData;

    if (Object.prototype.hasOwnProperty.call(replacements, secNum)) {
      const dec = replacements[secNum];
      newData = orig.compressed ? rleCompress(dec) : dec;
    } else {
      newData = orig.rawData;
    }

    const hdr = new Uint8Array(9);
    hdr[0] = secNum          & 0xFF;
    hdr[1] = (secNum >>  8)  & 0xFF;
    hdr[2] = (secNum >> 16)  & 0xFF;
    hdr[3] = (secNum >> 24)  & 0xFF;
    hdr[4] = newData.length          & 0xFF;
    hdr[5] = (newData.length >>  8)  & 0xFF;
    hdr[6] = (newData.length >> 16)  & 0xFF;
    hdr[7] = (newData.length >> 24)  & 0xFF;
    hdr[8] = orig.compressed;

    chunks.push(hdr);
    chunks.push(newData);
  }

  const totalLen = chunks.reduce((s, c) => s + c.length, 0);
  const out      = new Uint8Array(totalLen);
  let   pos      = 0;
  for (const chunk of chunks) { out.set(chunk, pos); pos += chunk.length; }
  return out.buffer;
}

// ─── High-level read-all ──────────────────────────────────────────────────────

/**
 * Read all known scenario data from a SPCTS_SAVE_V100 ArrayBuffer.
 *
 * opts.nationTable — SPWW2_NATIONS or SPMBT_NATIONS
 * opts.classTable  — SPWW2_UNIT_CLASSES or SPMBT_UNIT_CLASSES
 *
 * Returns { scenario, vlocs, allVlocs, units, leaders, mapDims, sections }
 */
function readSPCAMOAll(arrayBuffer, opts) {
  opts = opts || {};
  const sections = parseSections(arrayBuffer);
  const mapDims  = getMapDims(sections);

  let scenario = null;
  try { scenario = readSPCAMOScenarioData(arrayBuffer, opts); } catch (e) {}

  const allVlocs = readSPCAMOVLocs(arrayBuffer);
  const vlocs    = allVlocs.filter(v => !v.isEmpty);
  const units    = readSPCAMOUnits(arrayBuffer, opts);
  const leaders  = readSPCAMOLeaders(arrayBuffer);

  return { scenario, vlocs, allVlocs, units, leaders, mapDims, sections };
}

// ─── Backward-compat aliases ──────────────────────────────────────────────────
// These keep any existing code that used the old SPWW2-prefixed names working.

const SPWW2_MAGIC       = SPCAMO_MAGIC;
const SPWW2_HEADER_SIZE = SPCAMO_HEADER_SIZE;
const SPWW2_MAPSIZE     = SPCAMO_MAPSIZE;
const SPWW2_SEASONS     = SPCAMO_WEATHER;   // old name for the weather array
const SPWW2_MONTHS      = SPCAMO_MONTHS;

function readSPWW2ScenarioData(ab)        { return readSPCAMOScenarioData(ab, { nationTable: SPWW2_NATIONS }); }
function readSPMBTScenarioData(ab)        { return readSPCAMOScenarioData(ab, { nationTable: SPMBT_NATIONS }); }
function readSPWW2VLocs(ab)               { return readSPCAMOVLocs(ab); }
function readSPMBTVLocs(ab)               { return readSPCAMOVLocs(ab); }
function readSPWW2Positions(ab)           { return readSPCAMOPositions(ab); }
function readSPWW2Leaders(ab, inc)        { return readSPCAMOLeaders(ab, inc); }
function readSPWW2Units(ab)               { return readSPCAMOUnits(ab, { classTable: SPWW2_UNIT_CLASSES }); }
function readSPMBTUnits(ab)               { return readSPCAMOUnits(ab, { classTable: SPMBT_UNIT_CLASSES }); }
function writeSPWW2ScenarioData(ab, opts) { return writeSPCAMOScenarioData(ab, opts); }
function writeSPMBTScenarioData(ab, opts) { return writeSPCAMOScenarioData(ab, opts); }
function writeSPWW2VLocs(ab, vlocs)       { return writeSPCAMOVLocs(ab, vlocs); }
function writeSPMBTVLocs(ab, vlocs)       { return writeSPCAMOVLocs(ab, vlocs); }
function readSPWW2All(ab)                 { return readSPCAMOAll(ab, { nationTable: SPWW2_NATIONS, classTable: SPWW2_UNIT_CLASSES }); }
function readSPMBTAll(ab)                 { return readSPCAMOAll(ab, { nationTable: SPMBT_NATIONS, classTable: SPMBT_UNIT_CLASSES }); }

// ─── Node.js / CommonJS exports ──────────────────────────────────────────────

// ─── Obstacle reader ─────────────────────────────────────────────────────────
/**
 * Read all obstacle records (mines, dragon's teeth, barbed wire) from Section 17.
 * Returns array of { xloc, yloc, tileID, strength, count, type, typeName, side, sideLabel, block, recIndex }
 */
function readSPCAMOObstacles(arrayBuffer) {
  const sections = parseSections(arrayBuffer);
  if (!sections[17]) return [];
  const s17 = getSectionData(sections[17]);
  if (!s17) return [];

  const SIDE_LABELS = ['P1', 'P2'];
  const obstacles = [];

  function scanBlock(startRec, endRec) {
    for (let r = startRec; r < endRec; r++) {
      const off = r * S17_OBS_RECSIZE;
      if (off + S17_OBS_RECSIZE > s17.length) break;

      const xloc     = s17[off] | (s17[off+1] << 8);
      const yloc     = s17[off+2] | (s17[off+3] << 8);
      const strength = s17[off+12] | (s17[off+13] << 8);

      if (xloc === 0xFFFF && yloc === 0xFFFF) continue;
      if (strength === 0xFFFF || strength === 0) continue;

      const tileID = s17[off+6] | (s17[off+7] << 8);
      const typ    = s17[off+14];
      const side   = s17[off+15];

      let typeName;
      if (tileID === S17_OBS_TILE_BW && strength === S17_OBS_STR_BW) {
        typeName = 'Barbed Wire';
      } else if (typ === 1) {
        typeName = 'Mines';
      } else {
        typeName = "Dragon's Teeth";
      }

      obstacles.push({
        xloc, yloc, tileID, strength,
        count:     typeName === 'Barbed Wire' ? 1 : Math.floor(strength / 10),
        type:      typ,
        typeName,
        side,
        sideLabel: SIDE_LABELS[side] || ('Side#' + side),
        block:     Math.floor(r / 1000),
        recIndex:  r,
      });
    }
  }

  scanBlock(S17_OBS_DT_START, S17_OBS_DT_END);  // Dragon's Teeth
  scanBlock(S17_OBS_MN_START, S17_OBS_MN_END);  // Mines + Barbed Wire

  return obstacles;
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    // Codec
    rleDecompress, rleCompress,
    // Parser
    parseSections, getSectionData, getMapDims,
    // Primary SPCAMO API
    readSPCAMOScenarioData, readSPCAMOVLocs, readSPCAMOPositions,
    readSPCAMOLeaders, readSPCAMOUnits, readSPCAMOAll,
    writeSPCAMOScenarioData, writeSPCAMOVLocs,
    // Backward-compat wrappers
    readSPWW2ScenarioData, readSPMBTScenarioData,
    readSPWW2VLocs,        readSPMBTVLocs,
    readSPWW2Positions,
    readSPWW2Leaders,
    readSPWW2Units,        readSPMBTUnits,
    readSPWW2All,          readSPMBTAll,
    writeSPWW2ScenarioData,writeSPMBTScenarioData,
    writeSPWW2VLocs,       writeSPMBTVLocs,
    // Engine constants
    SPCAMO_MAGIC, SPCAMO_HEADER_SIZE, SPCAMO_MAPSIZE,
    SPCAMO_WEATHER, SPCAMO_MONTHS, SPCAMO_BATTLE_TYPES,
    // Compat constant aliases
    SPWW2_MAGIC, SPWW2_HEADER_SIZE, SPWW2_MAPSIZE, SPWW2_SEASONS, SPWW2_MONTHS,
    // Game-specific lookup tables
    SPWW2_NATIONS, SPMBT_NATIONS,
    SPWW2_UNIT_CLASSES, SPMBT_UNIT_CLASSES,
    // S37 offsets
    S37_MONTH, S37_YEAR, S37_WEATHER, S37_SEASON,
    S37_P1_NATION, S37_P2_NATION, S37_P3_NATION, S37_P4_NATION,
    S37_BF1, S37_BF2, S37_TURNS,
    S37_VLOC_COUNT, S37_VLOC_XLOC, S37_VLOC_YLOC, S37_VLOC_VALUE, S37_VLOC_CTRL, S37_VLOC_EMPTY,
    S37_VISIBILITY, S37_WIND, S37_LOCATION, S37_LOCATION_LEN,
    // S1 unit offsets
    S1_SUB_SIZE, S1_TOTAL_UNITS, S1_P2_OFFSET,
    S1_NAME, S1_NAME_LEN, S1_CLASS_ID, S1_OOB_NUMBER,
    S1_NORMAL_ICON, S1_FORM_ID, S1_FORM_SUB_ID, S1_MIN_FORM,
    SPCAMO_FORMATION_LETTERS,
    S1_WPN1_TYPE, S1_WPN2_TYPE, S1_WPN3_TYPE, S1_WPN4_TYPE,
    S1_UNIT_MODE, S1_UNIT_MODE_NAMES,
    S1_OBAT_NATION_ID, S1_NATION_ID, S1_OBAT_ID, S1_SLOT_ID, S1_VALID, S1_ENTR, S1_IS_OFF_MAP, S1_SIDE,
    S1_RADIO, S1_SIZE,
    S1_MEN, S1_SPEED, S1_SUPPRESSION, S1_WATER_SPEED, S1_EXPERIENCE, S1_MORALE,
    S1_STATUS, S1_STATUS_NAMES, S1_LEADER, S1_CARRIER,
    S1_PASSENGER_LINK, S1_LOADED_MEN,
    S1_BOMBARD_TYPE, S1_BOMBARD_TYPE_NAMES, S1_BOMBARD_TYPES,
    S1_BOMBARD_XLOC, S1_BOMBARD_YLOC, S1_BOMBARD_SPOTTER,
    S1_ALTITUDE, S1_ALTITUDE_NAMES,
    S1_VISION, S1_FIRE_CONTROL, S1_COST,
    S1_RATE_OF_FIRE, S1_STABILIZER, S1_RANGE_FINDER, S1_MAX_FIRE_RANGE,
    S1_EW, S1_CARRY_CAPACITY, S1_COST_TO_CARRY, S1_SURVIVABILITY,
    S1_WPN1_ID, S1_WPN2_ID, S1_WPN3_ID, S1_WPN4_ID, S1_NR_SD,
    // Armor AP echoes (individual byte constants)
    S1_FH_AP_1, S1_FH_AP_2, S1_FH_AP_3, S1_FH_HEAT, S1_FH_ERA,
    S1_SH_AP_1, S1_SH_AP_2, S1_SH_AP_3, S1_SH_AP_4, S1_SH_AP_5, S1_SH_AP_6,
    S1_SH_HEAT_1, S1_SH_HEAT_2, S1_SH_ERA, S1_SH_ERA_2,
    S1_RH_AP_1, S1_RH_AP_2, S1_RH_AP_3, S1_RH_HEAT, S1_RH_ERA,
    S1_FT_AP_1, S1_FT_AP_2, S1_FT_AP_3, S1_FT_HEAT, S1_FT_ERA,
    S1_ST_AP_1, S1_ST_AP_2, S1_ST_AP_3, S1_ST_AP_4, S1_ST_AP_5, S1_ST_AP_6,
    S1_ST_HEAT_1, S1_ST_HEAT_2, S1_ST_ERA, S1_ST_ERA_2,
    S1_RT_AP_1, S1_RT_AP_2, S1_RT_AP_3, S1_RT_HEAT, S1_RT_ERA,
    S1_TOP_AP_1, S1_TOP_AP_2, S1_TOP_AP_3, S1_TOP_HEAT, S1_TOP_ERA,
    S1_PICTURE, S1_ROTATION1, S1_ROTATION2, S1_STAT_MODIFIED,
    // S17 offsets
    S17_RECORD_SIZE, S17_XLOC_OFF, S17_YLOC_OFF,
    S17_OBS_RECSIZE, S17_OBS_DT_START, S17_OBS_DT_END,
    S17_OBS_MN_START, S17_OBS_MN_END, S17_OBS_TILE_BW, S17_OBS_STR_BW,
    readSPCAMOObstacles,
    // S34 offsets
    S34_RECORD_SIZE, S34_SLOT_COUNT,
    S34_NAME, S34_NAME_LEN, S34_UNIT_ID,
    S34_RANK, S34_RALLY, S34_INFANTRY, S34_ARTILLERY, S34_ARMOR,
    S34_KILLS, S34_STATUS, S34_RANK_NAMES, S34_STATUS_NAMES,
  };
}
