Tuesday, 23 August 2016

I76 - Nitro Riders and Compression

The Cover, via Wikipedia

Nitro Riders (or Nitro Pack in the US) uses the same ZFS file format as the original I76, however it also supports compression of the files in the ZFS, which leads to a minor complication in unpacking.

The compression scheme used is, as per Battlezone, LZO and the standard lzo library can handle the decompression process.

The standard library is available here:
http://www.oberhumer.com/opensource/lzo/

So we can add a simple link (on my stock Debian box) to -llzo2, or in Qt/qmake speak we can update the .pro file with:
 LIBS += -llzo2

And our code needs to include an initialisation call to:
 lzo_init()

There are actually several different compression algorithms inside the LZO family, and the Nitro ZFS uses two different variants, so there are three possible options for each file entry in the ZFS.
  • No compression
  • lzo1x algorithm
  • lzo1y algorithm
In the original I76 ZFS then we noted that file header contains a 4 byte "NULL" value. This is used to flag compression in the Nitro version.

If this value is zero then there is no compression involved, and the file can simply be unpacked, as in the original I76.

Otherwise the lowest byte details the exact compression algorithm used
  • If the value is 2 then use lzo1x
  • If the value is 4 then use lzo1y

The remaining (upper three) bytes tell us the unpacked size of the target file.

So we can implement a file decompressor with the signature:
static bool do_lzo(QString name, QByteArray& ba, int comp, int size)

Where name and ba are the input data from the ZFS, and comp and size are supplied by:
  • comp:  fh.null&0xff
  • size:  fh.null >> 8
And we can implement a simple decompress (given a correct comp field) with:

dst =  (unsigned char*)malloc(size);
dst_len = size;
src = (unsigned char*)ba.data();

  if (comp & 2)
  {
    r = lzo1x_decompress_safe(src, ba.length(), dst, &dst_len, NULL);

  }
  else if (comp & 4)
  { 
    r = lzo1y_decompress_safe(src, ba.length(), dst, &dst_len, NULL);
  }

  
  if (r != LZO_E_OK)
  {// Error

...
  }
  else
  {// Good - write it out
...
  }
  free(dst);


And that's it. We can just write out the decompressed data from the 'dst' buffer to the target file name.

We could be neater and keep the decompression buffer rather than reallocate each time and use the non-safe version of the LZO decompression algorithms, but this is fast enough for our purposes.

We'd also be caught out by any other comp values/LZO variants, but this seems to cope with every piece of Nitro content I have here (from the original game CD).