Ouch my eye!

Do not look at LASER with remaining eye!


MAXed out

Here we go again, on yet another side quest, I didn’t mean to, but couldn’t help myself. After finishing off with the CAT file variant we saw with M1 Tank Platoon from MicroProse in my last post, I took a quick poke around the other files included with the game. A number of files with “MAX” in the name along with “EGA”, “VGA”, and “MCGA” caught my eye. These are obviously related to the various graphics modes the game supports, and likely contain more graphic assets, just like we found with the 3 CAT files in my last post. I’ll try to keep this adventure a quick one, so with that let’s dig into it.

-rw-rw-r--  33253 19 Jun  2023 CGA_MAX1
-rw-rw-r--  27274 19 Jun  2023 CGA_MAX2
-rw-rw-r--  29021 19 Jun  2023 CGA_MAX3
-rw-rw-r--  36846 19 Jun  2023 CGA_MAX4
-rw-rw-r--  40232 19 Jun  2023 EGA_MAX1
-rw-rw-r--  27333 19 Jun  2023 EGA_MAX2
-rw-rw-r--  35610 19 Jun  2023 EGA_MAX3
-rw-rw-r--  48196 19 Jun  2023 EGA_MAX4
-rw-rw-r--  70951 19 Jun  2023 MCGAMAX1
-rw-rw-r--  30309 19 Jun  2023 MCGAMAX2
-rw-rw-r--  76110 19 Jun  2023 MCGAMAX3
-rw-rw-r--  53070 19 Jun  2023 MCGAMAX4
-rw-rw-r--  43000 19 Jun  2023 MCGAMAX5

Interesting that there is one more MCGA file than there is EGA or CGA. I wonder if the file size is limited to 64K, and they had to break the MCGA content up a bit more due to the larger size. I guess we’ll only know if we take a look at what’s inside. (Ed. I clearly missed the two files larger than 64K when I said that, so the mystery continues)


First look

As is tradition around these parts, let’s dive right into looking at the files with a hex viewer. As our first CAT file in the last post was the EGA content, let’s start with EGA_MAX1.

File: MAX/EGA_MAX1  [40232 bytes]
Offset    x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF  Decoded Text
0000000x: 30 30 30 30 00 00 00 00 40 01 00 00 08 00 00 00  0 0 0 0 · · · · @ · · · · · · ·
0000001x: 41 44 53 00 00 00 00 00 48 01 00 00 10 21 00 00  A D S · · · · · H · · · · ! · ·
0000002x: 41 54 54 4E 4C 00 00 00 58 22 00 00 16 00 00 00  A T T N L · · · X " · · · · · ·
0000003x: 41 54 54 4E 52 00 00 00 6E 22 00 00 41 00 00 00  A T T N R · · · n " · · A · · ·
        ⋮                                                 ⋮
0000012x: 54 49 54 4C 45 31 31 00 33 76 00 00 9C 21 00 00  T I T L E 1 1 · 3 v · · · ! · ·
0000013x: 4C 4F 47 4F 00 00 00 00 CF 97 00 00 59 05 00 00  L O G O · · · · · · · · Y · · ·
0000014x: 02 00 01 00 F5 55 AA 00 A0 00 C8 00 F5 00 02 0A  · · · · · U · · · · · · · · · ·
0000015x: 1C 48 B0 A0 C1 07 01 11 1A 5C C8 B0 A0 42 82 0F  · H · · · · · · · \ · · · B · ·

30  ...  00: Asset name (8 bytes)
40 01 00 00: 0x00000140 (320) Offset of 1st asset data 
08 00 00 00: 0x00000008 (8) Size of 1st asset 
48 01 00 00: 0x00000148 (328) Offset of 2nd asset data 
02 00 01 00: Beginning of 1st asset data
A0 00 C8 00: Beginning of 2nd asset data

Well that appears to be very CAT like. I find it odd that a 2nd type of aggregate file is being used here. It’s almost as if two different people were managing the project, and they couldn’t agree on how to store the assets. Regardless, we’re here now so let’s figure this one out. Given the alignment of the names, it’s obvious that each record is 16 bytes in size. The fields pretty much stand out on their own, and are pretty obvious. We start off with an 8 byte name field, followed by a 32bit offset, and then a 32bit length. There does not appear to be a length, or count, prefix like we see with CAT files, so we will have to calculate the number of entries by using the offset of the first data payload, and dividing it by the record size.

0x0140 (320) / 16 = 20

A quick count of the names and we do indeed have 20 records, so it all works out. We can also validate that the size field is what it is by subtracting the 2nd data offset from the 1st, and again that works out to match. So we have the structure pretty cleanly figured out, let’s turn that into a proper structure.

typedef struct {
    char name[8];    // Name of asset (filename without extension)
    uint32_t offset; // absolute file offset to start of data for this entry
    uint32_t size;   // data length of this entry
} mps_max_entry_t;

With that structure it should be easy enough to modify our CAT code just like we did the last time, to parse and extract from the MAX files. As we’ve already covered the code, and the only real change is the structure above, I’m not going to detail the code here. It will be posted in a github repo when we conclude the format.

MicroProse MAX Asset Extractor
Opening: 'EGA_MAX1'	File Size: 40232 bytes
Catalogue contains 20 items
 1:     0000      8  [@00000140]
 2:      ADS   8464  [@00000148]
 3:    ATTNL     22  [@00002258]
 4:    ATTNR     65  [@0000226e]
 5:   BLASTA    228  [@000022af]
 6:   BLASTB    189  [@00002393]
 7: CREDITS2   8540  [@00002450]
 8:   LTRACK    818  [@000045ac]
 9:      MAG    107  [@000048de]
10:      MPS   2521  [@00004949]
11:   RTRACK    817  [@00005322]
12:  TCOMARM     52  [@00005653]
13:  TITCOMA    401  [@00005687]
14:  TITCOMB    396  [@00005818]
15:  TITCOMC    400  [@000059a4]
16:  TITCOMD    397  [@00005b34]
17:  TITCOME    396  [@00005cc1]
18:    TITLE   6118  [@00005e4d]
19:  TITLE11   8604  [@00007633]
20:     LOGO   1369  [@000097cf]

Well that was easy. Now for extraction, since we don’t know the format of the data, I’ll give it a .MAX extension for the time being. Once we determine the type we can change it. But before we do that, let’s look at the listings of all the MAX files to see if we have all the same files, or does MCGA have additional assets.

CGA Assets:

MicroProse MAX Asset Extractor
Opening: 'CGA_MAX1'
File Size: 33253 bytes
Catalogue contains 20 items
 
MicroProse MAX Asset Extractor
Opening: 'CGA_MAX2'
File Size: 27274 bytes
Catalogue contains 19 items

MicroProse MAX Asset Extractor
Opening: 'CGA_MAX3'
File Size: 29021 bytes
Catalogue contains 94 items

MicroProse MAX Asset Extractor
Opening: 'CGA_MAX4'
File Size: 36846 bytes
Catalogue contains 5 items






Total Items: 138
EGA Assets:

MicroProse MAX Asset Extractor
Opening: 'EGA_MAX1'
File Size: 40232 bytes
Catalogue contains 20 items

MicroProse MAX Asset Extractor
Opening: 'EGA_MAX2'
File Size: 27333 bytes
Catalogue contains 19 items

MicroProse MAX Asset Extractor
Opening: 'EGA_MAX3'
File Size: 35610 bytes
Catalogue contains 93 items

MicroProse MAX Asset Extractor
Opening: 'EGA_MAX4'
File Size: 48196 bytes
Catalogue contains 5 items






Total Items: 137
MCGA Assets:

MicroProse MAX Asset Extractor
Opening: 'MCGAMAX1'
File Size: 70951 bytes
Catalogue contains 20 items

MicroProse MAX Asset Extractor
Opening: 'MCGAMAX2'
File Size: 30309 bytes
Catalogue contains 19 items

MicroProse MAX Asset Extractor
Opening: 'MCGAMAX3'
File Size: 76110 bytes
Catalogue contains 93 items

MicroProse MAX Asset Extractor
Opening: 'MCGAMAX4'
File Size: 53070 bytes
Catalogue contains 3 items

MicroProse MAX Asset Extractor
Opening: 'MCGAMAX5'
File Size: 43000 bytes
Catalogue contains 2 items

Total Items: 137

Now that is interesting, and I think it does confirm that the file size has been capped to 64K. So from the looks of it both MAX1 and MAX2 having the same fixed set of assets, with the remaining assets being spread over however many files as it takes. I will have to see if I can identify the one extra file that CGA seems to have.

Full asset lists here, collapsed as they are a bit long. (click to expand)
CGA Asset List
     0000      8
      ADS   6351
    ATTNL     22
    ATTNR     65
   BLASTA    217
   BLASTB    194
 CREDITS2   6744
   LTRACK    868
      MAG    106
      MPS   2209
   RTRACK    873
  TCOMARM     50
  TITCOMA    327
  TITCOMB    320
  TITCOMC    330
  TITCOMD    313
  TITCOME    317
    TITLE   5267
  TITLE11   6983
     LOGO   1369
      2S1   1313
      2S3   1690
     BMP1   1445
     BMP2   1389
     LEO1   1629
     LEO2   1662
       M1   1501
     M109   1504
     M113    951
     M163    995
       M2   1416
      M60   1600
     MRLS   1428
     MTLB   1181
      T55   1445
      T62   1526
      T72   1541
      T80   1595
    VEHID   1159
     0000      8
    BACK2    605
    BACK3    341
    BACK4    370
    BACK5     35
    BACKH    264
    BACKT    927
   BLACKA    357
   BLACKB    345
   BLACKC    342
   BLACKD    308
   BLACKE    295
   BLACKF    317
   BLACKG    327
   BLACKH    326
   BLACKI    345
 COMMANDT   4185
   EYES2A     25
   EYES2B     23
   EYES2C     25
   EYES2D     25
    EYESA     23
    EYESC     19
    EYESD     22
    EYESE     25
    EYESF     22
    EYESG     20
    EYESH     21
    EYESI     22
    EYESJ     22
     GUNA    128
     GUNB    129
     GUNC    129
     GUND    130
    HELIA     41
    HELIB     43
    HELIC     27
    HELID     21
   HOUSEA    631
   HOUSEB    652
   HOUSEC    851
   HOUSED    752
   HOUSEE    693
   HOUSEF    648
   M5778T   7596
 M58CLOSE   1329
     MANA     52
     MANB     65
     MANC     68
     MAND     68
  MAPBACK   1169
  MOUTH2A     17
  MOUTH2B     17
  MOUTH2C     18
  MOUTH2D     18
  MOUTH2E     18
  MOUTH2F     16
  MOUTH2G     17
  MOUTH2H     18
  MOUTH2I     18
   MOUTHK     17
   MOUTHL     17
   MOUTHM     17
   MOUTHN     18
   MOUTHO     17
   MOUTHP     17
   MOUTHQ     16
   MOUTHR     17
   MOUTHS     17
   MOUTHT     17
    MSPRA     11
    MSPRB     11
    MSPRC     19
    MSPRD     10
    MSPRE     12
    MSPRF     12
    MSPRG     12
    MSPRH     16
    MSPRI     11
    MSPRJ     26
    MSPRK     26
    MSPRL      9
    MSPRM     80
    MSPRN     76
   TANKSA    153
   TANKSB    153
   TANKSC    155
   TANKSD    174
   TANKSE    170
   TANKSF    148
   TANKSG    172
   TANKSH    170
   TANKSI    175
   TANKSJ    176
   DEATHT   9113
    LOSTT   6969
     RETT   7778
     WONT   8695
     CERT   4211
EGA Asset List
     0000      8
      ADS   8464
    ATTNL     22
    ATTNR     65
   BLASTA    228
   BLASTB    189
 CREDITS2   8540
   LTRACK    818
      MAG    107
      MPS   2521
   RTRACK    817
  TCOMARM     52
  TITCOMA    401
  TITCOMB    396
  TITCOMC    400
  TITCOMD    397
  TITCOME    396
    TITLE   6118
  TITLE11   8604
     LOGO   1369
      2S1   1313
      2S3   1690
     BMP1   1445
     BMP2   1389
     LEO1   1629
     LEO2   1662
       M1   1501
     M109   1504
     M113    951
     M163    995
       M2   1416
      M60   1600
     MRLS   1428
     MTLB   1181
      T55   1445
      T62   1526
      T72   1541
      T80   1595
    VEHID   1218
     0000      8
    BACK2    605

    BACK4    496
    BACK5     35
    BACKH    345
    BACKT   1177
   BLACKA    433
   BLACKB    420
   BLACKC    408
   BLACKD    373
   BLACKE    366
   BLACKF    392
   BLACKG    408
   BLACKH    406
   BLACKI    426
 COMMANDT   5272
   EYES2A     21
   EYES2B     20
   EYES2C     21
   EYES2D     21
    EYESA     27
    EYESC     23
    EYESD     27
    EYESE     28
    EYESF     25
    EYESG     21
    EYESH     25
    EYESI     27
    EYESJ     25
     GUNA    134
     GUNB    139
     GUNC    138
     GUND    144
    HELIA     44
    HELIB     44
    HELIC     27
    HELID     21
   HOUSEA    840
   HOUSEB    881
   HOUSEC   1023
   HOUSED   1078
   HOUSEE    940
   HOUSEF    938
   M5778T  10675
 M58CLOSE   1376
     MANA     52
     MANB     65
     MANC     68
     MAND     68
  MAPBACK   1202
  MOUTH2A     14
  MOUTH2B     14
  MOUTH2C     16
  MOUTH2D     16
  MOUTH2E     16
  MOUTH2F     14
  MOUTH2G     14
  MOUTH2H     16
  MOUTH2I     16
   MOUTHK     18
   MOUTHL     18
   MOUTHM     19
   MOUTHN     20
   MOUTHO     18
   MOUTHP     19
   MOUTHQ     18
   MOUTHR     18
   MOUTHS     18
   MOUTHT     19
    MSPRA     11
    MSPRB     11
    MSPRC     19
    MSPRD     12
    MSPRE     12
    MSPRF     12
    MSPRG     10
    MSPRH     19
    MSPRI     11
    MSPRJ     30
    MSPRK     29
    MSPRL     40
    MSPRM     80
    MSPRN     76
   TANKSA    153
   TANKSB    153
   TANKSC    160
   TANKSD    176
   TANKSE    170
   TANKSF    151
   TANKSG    172
   TANKSH    169
   TANKSI    173
   TANKSJ    174
   DEATHT  11061
    LOSTT  11434
     RETT  10237
     WONT  11173
     CERT   4211
MCGA Asset List
     0000      9
      ADS   8879
    ATTNL     53
    ATTNR    133
   BLASTA    361
   BLASTB    365
 CREDITS2  16815
   LTRACK   1621
      MAG    169
      MPS   2776
   RTRACK   1628
  TCOMARM     91
  TITCOMA    713
  TITCOMB    715
  TITCOMC    715
  TITCOMD    701
  TITCOME    707
    TITLE  14571
  TITLE11  17830
     LOGO   1779
      2S1   1422
      2S3   1804
     BMP1   1576
     BMP2   1507
     LEO1   1763
     LEO2   1783
       M1   1604
     M109   1625
     M113   1074
     M163   1125
       M2   1507
      M60   1706
     MRLS   1558
     MTLB   1277
      T55   1569
      T62   1650
      T72   1648
      T80   1723
    VEHID   2084
     0000      9
    BACK2    647

    BACK4    880
    BACK5     50
    BACKH    705
    BACKT   2684
   BLACKA   1129
   BLACKB   1087
   BLACKC   1068
   BLACKD    983
   BLACKE   1012
   BLACKF   1010
   BLACKG   1145
   BLACKH   1178
   BLACKI   1203
 COMMANDT  14386
   EYES2A     28
   EYES2B     28
   EYES2C     29
   EYES2D     29
    EYESA     43
    EYESC     43
    EYESD     43
    EYESE     48
    EYESF     41
    EYESG     40
    EYESH     41
    EYESI     43
    EYESJ     40
     GUNA    262
     GUNB    269
     GUNC    262
     GUND    255
    HELIA     50
    HELIB     50
    HELIC     37
    HELID     26
   HOUSEA   1607
   HOUSEB   1702
   HOUSEC   2036
   HOUSED   2042
   HOUSEE   2000
   HOUSEF   1875
   M5778T  23269
 M58CLOSE   2807
     MANA     61
     MANB     67
     MANC     70
     MAND     77
  MAPBACK   2536
  MOUTH2A     22
  MOUTH2B     23
  MOUTH2C     25
  MOUTH2D     22
  MOUTH2E     23
  MOUTH2F     26
  MOUTH2G     26
  MOUTH2H     27
  MOUTH2I     22
   MOUTHK     22
   MOUTHL     23
   MOUTHM     25
   MOUTHN     25
   MOUTHO     27
   MOUTHP     23
   MOUTHQ     26
   MOUTHR     23
   MOUTHS     27
   MOUTHT     22
    MSPRA     16
    MSPRB     14
    MSPRC     26
    MSPRD     17
    MSPRE     14
    MSPRF     17
    MSPRG     12
    MSPRH     26
    MSPRI     14
    MSPRJ     40
    MSPRK     40
    MSPRL     48
    MSPRM     91
    MSPRN     90
   TANKSA    255
   TANKSB    256
   TANKSC    259
   TANKSD    291
   TANKSE    293
   TANKSF    248
   TANKSG    262
   TANKSH    260
   TANKSI    270
   TANKSJ    272
   DEATHT  21720
    LOSTT  23144
     RETT  21248
     WONT  24925
     CERT   4953

As we can see from the listings above, the CGA assets contains a file called “BACK3” which the others do not. Not sure if this is an unused asset, or an error and it is missing. But otherwise all 3 have the exact same set of assets. Sadly I doubt we’ll find any spicy Easter eggs here.


First look at the assets

So looking through the lists, I think it’s pretty clear that these are image assets (or at least most of them are). Now as we have no extensions for them we have no idea what the format is. I wonder if it might be the same .PK format we saw in the CAT files. I think we’ll start with “MPS.MAX”, this is likely the MicroProse Labs logo, that we used previously when figuring out the PIC format, and like before we’ll start with the EGA version of it.

File: EGA_MPS.MAX  [2521 bytes]
Offset    x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF  Decoded Text
0000000x: A0 00 C8 00 F5 00 02 0A 1C 48 B0 A0 C1 83 01 11  · · · · · · · · · H · · · · · ·
0000001x: 20 5A C8 B0 A1 C3 87 10 23 4A 9C 48 B1 A2 43 84    Z · · · · · · # J · H · · C ·
0000002x: 18 33 6A DC C8 D1 20 82 8E 20 43 8A 04 F9 71 A4  · 3 j · · ·   · ·   C · · · q ·
0000003x: C9 93 1D 4B A2 5C C9 32 A3 CA 96 30 51 BE 8C 49  · · · K · \ · 2 · · · 0 Q · · I

A0 00: 0x00A0 (160) Width
C8 00: 0x00C8 (200) Height
F5: Typical marker for PIC encoded data stream. (11 bit LZW, Linear pixel arrangement)

Well that certainly looks consistent with what we learned with the .PK files last time. We only didn’t have the image dimensions encoded with the data, but we did find that the image was effectively encoded as linear, but was actually 4bit packed. Let’s try rendering it as-is, to see if it holds. I’ll strip off the dimensions and rename it as a .PIC then pass it to my PIC88 code to render it.

% pic2png 160x200 EGA_MPS.PIC 
MicroProse PIC(88) to PNG image converter
Resolution: 160 x 200
Opening PIC Image: 'EGA_MPS.PIC'	File Size: 2517
Creating PNG Image: 'EGA_MPS.png'
Reading PIC image	Mem Size: 31960 bytes
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 2578 bytes
MPS (EGA 160)

Yikes! something is wrong here. Oddly the encoder didn’t complain, but that image is clearly not right either. Could this be a corrupted image? Also odd the Mem Size value after decoding should be 32000, which means we didn’t get enough pixel data either. The good news is it does appear to have been encoded like a PIC, otherwise the decoder would have crapped out with an error, or the image would have been absolute random garbage. We can at least see that there is a correct image buried in there somewhere. Let’s try the other versions before coming back to this.


Let’s try the CGA one, it also specifies 160×200.

% pic2png 160x200 CGA_MPS.PIC
MicroProse PIC(88) to PNG image converter
Resolution: 160 x 200
Opening PIC Image: 'CGA_MPS.PIC'	File Size: 2205
Creating PNG Image: 'CGA_MPS.png'
Reading PIC image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 2247 bytes
MPS (CGA 160)

Now that’s more like what we would expect. Let’s try forcing the encoding to be packed, by changing the F5 to 0B in the first byte of the file, and then rendering at 320×200, like we did with the .PK files.

% pic2png 320x200 CGA_MPS.PIC
MicroProse PIC(88) to PNG image converter
Resolution: 320 x 200
Opening PIC Image: 'CGA_MPS.PIC'	File Size: 2205
Creating PNG Image: 'CGA_MPS.png'
Reading PIC image	Mem Size: 64000 bytes
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 2711 bytes
MPS (CGA 320)

Well that worked… sort of. It looks like the column data is swapped. We’ve seen this before when we were first decoding the PIC format trying to determine the pixel order within a byte. I wonder if that is why these images are encoded as linear and not packed, to bypass the nibble expansion code in the PIC decoder, and then they do the nibble expansion in a separate pass as it is different here. I have no idea how they’re actually doing it, but that is how I’m going to do it. Time to customize my PIC code a bit and give it a go. Might as well have it use the dimensions too so I don’t have to keep editing the binary of the files.

% max2png CGA_MPS.MAX                                                     
MicroProse MAX to PNG image converter
Opening MAX Image: 'CGA_MPS.MAX'	File Size: 2209
Resolution: 160 x 200
Creating PNG Image: 'CGA_MPS.png'
Reading MAX image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 2539 bytes
MPS (CGA 320 – fixed)

Now that is a heck of a lot better, and I think provides a decent explanation of why the F5 encoding value was used. (maybe) I decided to try this new decode path with the .PK files from before, but they render incorrectly then, so they definitely use the normal PIC nibble ordering there and no idea why those ones would have been encoded as F5 and not 0B. Now we still have the downside in that we don’t know if we should be doing this expansion and nibble swapping or not, other than from the archive we pulled it from. As a result this may need to be managed through an external switch. Now that we have the 4 bit encoding solved (probably), let’s look at the 8 bit images in the MCGA files.


Since we have a the ability to read in the image dimensions now, all I need to do is disable the “fixup” code I made for the 4 bit packed CGA & EGA images (though EGA still remains untested) to be able to get back to “default” and be able to decode the MCGA files.

% max2png VGA_MPS.MAX                                                     
MicroProse MAX to PNG image converter
Opening MAX Image: 'VGA_MPS.MAX'	File Size: 2776
Resolution: 320 x 200
Creating PNG Image: 'VGA_MPS.png'
Reading MAX image	Mem Size: 64000 bytes
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 2880 bytes
MPS (VGA)

Perfect! that seems to be as expected. So really the only outlier here (so far) has been the EGA version. I guess we should try some other assets to see if they present the same problem.


It’s not a bug it’s a feature!

Let’s grab “LOGO” as our next guinea pig for testing.

LOGO (CGA)
LOGO (EGA)

Well it looks like the CGA and EGA versions both work fine, though since we don’t know the correct CGA palette, we’re likely showing the wrong colour. This is great news, we are not completely broken for the EGA files, it’s just that file that has something going on. While those two appear fine, there is something very odd with the VGA/MCGA version. The image height is insanely high. Maybe I do have a corruption problem here, or I’ve completely misread the file structure. The -7 error code also signifies a LZW code error, so something is going on deeper in the file too.

% max2png MCGA/LOGO.MAX 
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/LOGO.MAX'	File Size: 1779
Resolution: 320 x 32968
Creating PNG Image: 'MCGA/LOGO.png'
Reading MAX image	Mem Size: 35705 bytes
Warning: Decompression returned with code -7
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 11687 bytes

Let’s pull it up in the hex viewer to see what’s going on with this file.

File: MCGA/LOGO.MAX  [1779 bytes]
Offset    x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF  Decoded Text
0000000x: 40 01 C8 80 F6 00 02 0A 1C 48 B0 A0 C1 83 08 13  @ · · · · · · · · H · · · · · ·
0000001x: 2A 5C C8 B0 A1 C3 87 10 23 4A 9C 48 B1 A2 C5 8B  * \ · · · · · · # J · H · · · ·
0000002x: 18 33 6A DC C8 B1 A3 C7 8F 20 43 8A 1C 49 B2 A4  · 3 j · · · · · ·   C · · I · ·
0000003x: C9 93 28 53 AA 5C C9 B2 A5 CB 97 30 63 CA 9C 49  · · ( S · \ · · · · · 0 c · · I

40 01: 0x0140 (320) Image Width
C8 80: 0x80C8 (32968) Image Height??
F6: PIC Encoding marker (10 bit LZW, Linear Pixel Encoding)

Okay now that is interesting. We start off with a normal width, but then have this insane height value. However on closer inspection if we zero out the upper byte, we end up with 200 as we were expecting. I don’t think this 80 value is a mistake either as it is just the MSBit being set, perhaps they stole a bit from the height value to use as a flag bit to indicate something. Back in ’88 image heights would never have come close to needing that bit… even today with our 4K+ displays we’re not close to needing it. Then we’re followed by F6 which is unusual, but we did account for variable width when we determined the PIC format, so this should be fine. Perhaps the error we saw was the result of trying to decode such a large image. I think this may be the fist time we’ve seen a value other than 0B of F5 in an asset from MicroProse in the wild. Let’s try masking off that one bit, and decoding the image to see what we get. We’ll come back later to see if we can figure out what that bit might be signifying.

% max2png MCGA/LOGO.MAX                                                   
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/LOGO.
MAX'	File Size: 1779
Resolution: 320 x 200
Creating PNG Image: 'MCGA/LOGO.png'
Reading MAX image	Mem Size: 35705 bytes
Warning: Decompression returned with code -7
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 1456 bytes
LOGO (VGA)

Well that’s a whole heck of a lot better, but we’re still getting that error. Maybe I have a bug in my code around the maximum code size, let’s take a look.

        if(ctx->next_code == ctx->resize_code) {
            // check if we are at max bits, if so reset instead of resize
            if(LZW_MAX_CODE_WIDTH > ctx->code_bits) { // we can expand
                lzw_resize(ctx);
            } else { // we must reset
                lzw_reset(ctx);
                new_code = symbol; // root the new chain tree from the last symbol we wrote
            }
        }

Well crap! I don’t have bit width selection enabled at all LZW_MAX_CODE_WIDTH is a constant macro expression. Looks like the code I grabbed is an older generation of code before I implemented the file selected maximum bit width. Luckily it’s a super easy fix, we have the value, just weren’t using it.

        if(ctx->next_code == ctx->resize_code) {
            // check if we are at max bits, if so reset instead of resize
            if(pic->max_width > ctx->code_bits) { // we can expand
                lzw_resize(ctx);
            } else { // we must reset
                lzw_reset(ctx);
                new_code = symbol; // root the new chain tree from the last symbol we wrote
            }
        }

Okay, with that fixed (I hope) let’s try again. One. More. Time. (fingers crossed)

% max2png MCGA/LOGO.MAX                                                   
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/LOGO.MAX'	File Size: 1779
Resolution: 320 x 200
Creating PNG Image: 'MCGA/LOGO.png'
Reading MAX image	Mem Size: 64000 bytes
Warning: Decompression returned with code -4
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 1873 bytes
LOGO (VGA – Fixed)

Success! Nailed it first try 😉 Not a complete success though, we now have a -4 return error code. This code indicates a buffer overflow, meaning there is more image data, than the buffer we allocated for. Yet we can see the whole image, and it looks fine. I wonder if that “flag bit” earlier has something to do with this? Let’s stick a pin in that for now though. We’ve made a bunch of changes and fixes, maybe we’ve fixed that original EGA image so let’s give that a go again.


The primordial soup

% max2png EGA/MPS.MAX
MicroProse MAX to PNG image converter
Opening MAX Image: 'EGA/MPS.MAX'	File Size: 2521
Resolution: 160 x 200
Creating PNG Image: 'EGA/MPS.png'
Reading MAX image	Mem Size: 31960 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 2910 bytes
MPS (EGA)

Well crap! Still not working, but also still not erroring. Strange. It’s not bugging out the LZW decoding, so that’s good, and the more I think about it, this must be something going on in the RLE data. Let’s resurrect our old testing code from when we were figuring out the PIC format, and get just the output from the LZW decompressor. If you recall to decode a PIC image we had a flow something like the following

[.PIC] -> LZW decompressor -> RLE decoder -> Pixel Unpacker -> [RAW Image]

By all appearances the LZW portion appears to be working, and certainly incorrect RLE decoding could explain what we are seeing. So let’s look at what is being fed into the RLE decoder.

File: EGA_MPS.RLE  [32001 bytes]
Offset    x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF  Decoded Text
0000000x: F5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000001x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000002x: 00 00 00 00 00 00 00 08 88 88 88 88 88 88 88 88  · · · · · · · · · · · · · · · ·
0000003x: 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88  · · · · · · · · · · · · · · · ·

F5: Format Byte Pass Through

We can ignore the first byte of F5 as that is just our format byte being passed forward by the test program. What follows is very interesting. This is not what I expected to see at all. This looks to be RAW uncompressed image data, not RLE compressed data. If it was RLE data, as PIC files normally encode, we would not see long strings of 00, instead we would see a sequence like 00 90 26 to encode all the 00‘s we see above. Also note the size, it is 32001 bytes, which is exactly what it should be for an uncompressed image of 160×200 (Remember pixels are packed 2 per byte for the EGA image). I wonder, are they all like that, or just the select ones, let’s look at the VGA one to see, as that one was working just fine.

File: VGA_MPS.RLE  [64001 bytes]
Offset    x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF  Decoded Text
0000000x: F5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000001x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000002x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000003x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·

F5: Format Byte Pass Through

Well that’s interesting, even the files that work are also not RLE compressed. Again the long string of 00‘s as well as the size support that conclusion. I guess that is possible since bytes are copied through the decoder unless a 90 token is encountered, at which point a run is processed. I guess 90 is less common in the data than you would think, allowing for several files to pass unaffected. Perhaps these assets are in some sort of Proto-PIC format, where MicroProse hadn’t fully settled on the format and all its features just yet, and this version did not include RLE. Regardless of the reason, we have a direction to go with here. Let’s bypass the RLE stage in our decoder and see what we get.

% max2png EGA/MPS.MAX                                                     
MicroProse MAX to PNG image converter
Opening MAX Image: 'EGA/MPS.MAX'	File Size: 2521
Resolution: 160 x 200
Creating PNG Image: 'EGA/MPS.png'
Reading MAX image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 2880 bytes
MPS (EGA – Fixed)

That seems to have done the trick. Going back and trying all the assets we’ve done before shows that this change has not broken anything. But for good measure let’s try one more image.


The trailing bits

At this point I think we’re in a pretty good place. Let’s try a few more, and then see if we can knock off the last of the unknowns. What better place to start than the title screen of the game.

TITLE (CGA)
TITLE (EGA)
TITLE (VGA)
MicroProse MAX to PNG image converter
Opening MAX Image: 'CGA/TITLE.MAX'	File Size: 5267
Resolution: 160 x 200
Creating PNG Image: 'CGA/TITLE.png'
Reading MAX image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 4935 bytes
MicroProse MAX to PNG image converter
Opening MAX Image: 'EGA/TITLE.MAX'	File Size: 6118
Resolution: 160 x 200
Creating PNG Image: 'EGA/TITLE.png'
Reading MAX image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 5229 bytes
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/TITLE.MAX'	File Size: 14571
Resolution: 320 x 200
Creating PNG Image: 'MCGA/TITLE.png'
Reading MAX image	Mem Size: 64000 bytes
Warning: Decompression returned with code -4
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 11408 bytes

They all look to have decoded fine, though the VGA one still displays the warning about the buffer overflow. Would be nice to find the palette for the 256 colour images too, it appears to be different than the one I found for the .PK images as they still didn’t render right with it. I did look and that VGA image does have that extra bit set again, so far it seems we only get that warning when that apparent flag bit is set. Let’s try one more candidate, we’ll use ADS, It’s always fun to see what other games they were promoting at the time.

ADS (CGA)
ADS (EGA)
ADS (VGA)
MicroProse MAX to PNG image converter
Opening MAX Image: 'CGA/ADS.MAX'	File Size: 6351
Resolution: 160 x 200
Creating PNG Image: 'CGA/ADS.png'
Reading MAX image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 6176 bytes
MicroProse MAX to PNG image converter
Opening MAX Image: 'EGA/ADS.MAX'	File Size: 8464
Resolution: 160 x 200
Creating PNG Image: 'EGA/ADS.png'
Reading MAX image	Mem Size: 32000 bytes
Correcting palette................ Palette converted to 24 bit
Max Image Fixup
Expanding: 160x200 => 320x200
Writing PNG image	File Size: 7982 bytes
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/ADS.MAX'	File Size: 8879
Resolution: 320 x 200
Creating PNG Image: 'MCGA/ADS.png'
Reading MAX image	Mem Size: 64000 bytes
Warning: Decompression returned with code -4
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 7982 bytes

Once again all 3 seem to decode fine, but again the VGA one is giving a warning. It also has the flag bit set, there probably is a correlation between the two. Would be nice to know the CGA palette settings so we render with the correct colours, but I’d be more interested in finding the palettes for the 256 colour images, than writing the code to add support for rendering the various CGA palettes. But first, we need to figure out that pesky buffer overflow problem. Since we have our trusty LZW-only decoder that we used for solving the RLE problem, let’s run that on ADS to see what we get. (Again I need to edit the file to make it a PIC88 format image by stripping out the image dimensions, the dimensions aren’t needed here anyway)

% pic2rle VGA_ADS.PIC 
PIC LZW decompressor
Opening PIC Image: 'VGA_ADS.PIC'	File Size: 8875
Creating RLE Image: 'VGA_ADS.RLE'
Decompression Complete

Okay that is interesting, the LZW code had no problem with it, so the overrun must be happening at a later stage. Since we’ve taken RLE out of the loop, it must be the pixel writer. So it looks like we have a good LZW stream, juts not enough buffer to write all the data in it. Let’s see what the LZW decoder gave us.

File: VGA_ADS.RLE  [64769 bytes]
Offset    x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF  Decoded Text
0000000x: F5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000001x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000002x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000003x: 00 00 00 00 00 00 00 00 00 00 0F 00 00 00 00 00  · · · · · · · · · · · · · · · ·
        ⋮                                                 ⋮
0000F9Fx: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·
0000FA0x: 00 00 00 00 00 00 2A 00 2A 00 00 2A 2A 2A 00 00  · · · · · · * · * · · * * * · ·
0000FA1x: 2A 00 2A 2A 15 00 2A 2A 2A 15 15 15 15 15 3F 15  * · * * · · * * * · · · · · ? ·
0000FA2x: 3F 15 15 3F 3F 3F 15 15 3F 15 3F 3F 3F 15 3F 3F  ? · · ? ? ? · · ? · ? ? ? · ? ?
0000FA3x: 3F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ? · · · · · · · · · · · · · · ·
0000FA4x: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  · · · · · · · · · · · · · · · ·

00 00 00: First Entry of Palette Data

I almost missed this, that is a fantasic number! We subtract one, since the format byte is not actually part of the image, so we really have an output of 64,768 bytes, which is still not right. A 320×200 image should only take 64,000 bytes. Which means we at 768 byts oversized. You know what is 768 bytes in size? A damn 256 colour RGB palette, that’s what! I couldn’t be happier to see that number, we are killing two birds with one stone here. Looking down at what comes at 00FA01 which is where the file should end if it was 64,001 bytes of just image data, we can see what looks to be valid palette data. In this case it’s mostly 00‘s, with only the frst 16 entries valid, but that makes sense for this image as it appears to just be the EGA ADS image converted for 8 bit colour.


I’m guessing I was right in that that 80 we saw in the upper byte of the image height was a flag for something, in this case it looks to indicate the image includes palette data. Let’s quickly modify our code to take advantage of this fact and see what we get. I’ll increase the calculated buffer by 768 bytes if the flag is set.

TITLE (VGA – Palette Applied)
ADS (VGA – Palette Applied)
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/TITLE.MAX'	File Size: 14571
Resolution: 320 x 200
Creating PNG Image: 'MCGA/TITLE.png'
Reading MAX image	Mem Size: 64768 bytes
Applying embedded palette
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 11408 bytes
MicroProse MAX to PNG image converter
Opening MAX Image: 'MCGA/ADS.MAX'	File Size: 8879
Resolution: 320 x 200
Creating PNG Image: 'MCGA/ADS.png'
Reading MAX image	Mem Size: 64768 bytes
Applying embedded palette
Correcting palette................ Palette converted to 24 bit
Writing PNG image	File Size: 7982 bytes

By George I think we’ve got it! Not only do we have a visually corrected image, but that pesky overflow error is gone. It’s interesting to note that the mechanism for tacking on the palette after the image data like this is exactly how ZSoft added 256 colour support to PCX images of the time. Also the 4 bit per pixel encoding order also matches that of ZSoft/PCX, so I wouldn’t be surprised if these image assets were created in ZSoft’s PC Paintbrush, or PCX was used as an intermediary form of the images, and then the internal data was just compied over in hteir transcoder.

This image seems particularly appropriate at this moment.

WONT (CGA)
WONT (EGA)
WONT (VGA)

A victory lap

Let’s take a bit of a victory lap through the MCGA (VGA) assets and then wrap things up for this post as it has gotten REALLY long, so much for being a quick one. This little side quest ended up being more involved than I had expected, I thought at most we would just find more .PK assets, was not expecting a totally different encoding.

The last title image in the gallery above does not contain an embedded palette, so I applied the one extracted from TITLE earlier.

Final thoughts

Well that was quite the adventure, and far more involved than I would have guessed. In the end we figured it all out, and with that unlocked what is likely all the assets that M1 Tank Platoon has. There is more data within MCGAGRPH.TNK that we poked into in the last post to get the palette for the .PK files. At this point after taking another quick look I believe that to be more palettes and not images. Still worth an investigation down the road, but for now I’m done with M1 Tank Platoon.

I’ll be honest I’m still somewhat baffled by some of the choices made with M1 Tank Platoon. It really does feel like there were two drivers of this tank. We have two different container formats for no apparent good reason, so you have twice the reader code. Then you have two different image variants, nearly identical, but different enough to require different code. Top that off with the fact that each is partially packaged inside of the PIC framework that the game also contains. Adding even more unnecessary functionally duplicate code.

Speaking of code. I will create a mps-max repo and post my code there for managing the MAX container format. The repo should go live in a day or two, I’ll update this post with a link once it’s live. (or possibly make a new post with a quick announcement). As for the images within the MAX container, along with the .PK images from the last post, I will post the tools for reading and writing those along with the PIC related tools in the near future as they will rely at least in part on the PIC library.

That brings this little side-quest to a conclusion, hopefully I’ll be able to get my head back on track to finish up the other loose ends shortly. I’ve already blown my planned schedule of releases to oblivion. Till next time, happy hacking.

By Thread



Leave a comment