Blizzard .GRP reader and .PNG spritesheet maker

I've been a Blizzard fan since I first picked up Warcraft 1 at Electronic Boutique. Warcraft 2 was even better and I've always wanted to try to recreate it in the browser. JCFields has created a Warcraft 2 Map Editor in the browser that is inspiring. The first step was converting the .GRP files to .PNG so I made a spritesheet maker that lets you extract and mirror images (because the .GRP files do not store data for units facing SW, W, NW as they are just mirrors of SE, E, NE so the mirroring is done by the game engine) into a .PNG spritesheet. I also made a .GRP viewer that lets you see what palette colors each .GRP frame is using and zoom in. Maybe someday I'll make it so you can edit the .GRP colors.

Tools

Experiments

I have not been able to decode the map tile format but I do have .PNGs of the map tiles. Warcraft 2 map tiles are 32x32 pixels.

forest

swamp

wasteland

winter

Blizzard developed the .GRP file format to encode their non-map tile art (units, buildings, projectiles, spells). It is a custom run length encoding format with 3 different instructions (shift, repeat, pixel).

.GRP files do not contain palette data, there are separate palettes for each map tileset (forest, iceland, swamp, xswamp (wasteland)).

.GRP files are all drawn using the 1st player (red) color. There are 4 red palette colors that are swapped to differentiate player units and buildings (see player colors below).

The maximum .GRP file canvas size is 128x128 pixels.

.GRP files have a max of 256 colors.

.GRP files do not contain frames for the southwest and northwest animations for walking, attacking, and dying. They are mirrored from northeast and southeast frames by the game engine. While frame height can differ from frame width, every .GRP in Warcraft 2 is square so they are the same values. The first animation of a building is a separate .GRP file as are the last 4 death rotting animations of many units and catapult and ballista death animations are a separate explosion .GRP file.

.GRP frames can vary in size based on unit or building size. A peasent for example is 72x72 despite the map tileset being 32x32 and there is a bit of transparency since it is a small unit. Destroyers are unique because they are 88x88.

There are some seemingly incorrect or missing pixels in various .GRPs. The grunt, oil refinery, and oil platforms come to mind.

The unit portraits and GUI buttons are in a .GRP file that has 198 Frames with each frame size being 46x38 pixels.

l_port

portrait

s_port

x_port

.GRP file Group Header (3 bytes)

.GRP Frame Headers (total number of frames (from Group Header) x 8 bytes each)

.GRP Data Header

I am not really sure the use of the data in the data header but each 2 little endian byte value is the offset to each pixel line in the frame. Adding this offset to the offset to data from the frame header gives you the offset from the base. Since the data always (but does not necessarily have to) starts after the data header, the offset to the data is usually 2 x (since each offset is 2 bytes) the total number of lines in the frame.

.GRP Data

Sprite Sheet

First read the palette data from the .pal files which are just a byte stream of (r,g,b) into a multidimensional array of palettes[] = [r,g,b]. Note that the color data seems like you just need to multiply each RGB value by 4 but if you do that it will be slightly off. It uses the VGA palette which is 6 bit. You need to use these formulas: (r*255)/63 or (r << 2) | (r >> 4). Then you need to draw each frame onto a canvas and then copy it to its appropriate place on the spritesheet (mirroring the image for southwest and northwest).

Draw a frame

Player colors

[164,  0,  0],[124,  0,  0],[ 92,  4,  0],[ 68,  4,  0] // player 1 (red)
[  0, 60,192],[  0, 36,148],[  0, 20,108],[  0,  4, 76] // player 2 (blue)
[ 44,180,148],[ 20,132, 92],[  4, 84, 44],[  0, 40, 12] // player 3 (teal)
[152, 72,176],[116, 48,132],[ 80, 16, 76],[ 44,  8, 44] // player 4 (violet)
[240,132, 20],[196, 88, 16],[152, 56, 16],[108, 32, 12] // player 5 (orange)
[ 40, 40, 60],[ 28, 28, 44],[ 20, 20, 32],[ 12, 12, 20] // player 6 (black)
[224,224,224],[152,152,180],[ 84, 84,128],[ 36, 40, 76] // player 7 (white)
[252,252, 72],[228,204, 40],[204,160, 16],[180,116,  0] // player 8 (yellow)
[  0,107,  0],[  0, 92,  0],[  0, 75,  0],[  0, 56,  0] // unused (green)
[255,148,253],[229,109,207],[195, 70,154],[159, 39,103] // unused (fuschia)