Crimsonland (Steam) Pak Format

June 18th, 2014

A remake of Crimsonland was recently released on Steam (and soon PS4/Vita) and I thought I’d take a look at the pak data file. Didn’t turn out to be anything really interesting, just art assets, sounds, music, and some frontend lua. But was good fun (even if it was a simple format).

clpak.py

import os
import struct

"""
Crimsonland (Steam) pak file format
Header format:
50 40 4B 00 56 31 31 00   ("PAK" NUL "V11" NUL)
int32 index offset     (eg: 28679361)
int32 end index offset (eg: 28885444)
Index Format:
int32 number of indexes?
Index_File format:
null terminated string
int32 absolute offset of file
int32 file length
unknown maybe always (?) equal to: FF 26 E2 50 20 00 00 00
"""

class ClPak(object):
    def __init__(self, filename):
        self.file = open(filename, 'rb')

    def read_header(self):
        self.file.seek(0)
        bytes = self.file.read(16)

        (magic_1, magic_2, index_start_offset, index_end_offset) = 
            struct.unpack('3sx3sxii', bytes)

        if magic_1 != 'PAK' or magic_2 != 'V11':
            raise Exception('Unknown file format')

        return index_start_offset, index_end_offset

    def read_index(self):
        start_offset, end_offset = self.read_header()

        self.file.seek(start_offset)
        bytes = self.file.read(end_offset - start_offset)

        # index_size = struct.unpack('i', bytes[:4])

        return self.read_index_file_details(bytes[4:])

    def read_index_file_details(self, bytes):

        details = {'name': '', 'offset': 0, 'length': 0, 'unknown': ''}

        sequence = iter(bytes)

        for char in sequence:
            if char != chr(0):
                details['name'] += char
            else:
                offset_bytes = sequence.next() + sequence.next() +
                               sequence.next() + sequence.next()
                length_bytes = sequence.next() + sequence.next() +
                               sequence.next() + sequence.next()

                (details['offset'],) = struct.unpack('i', offset_bytes)
                (details['length'],) = struct.unpack('i', length_bytes)

                for x in range(0, 8):
                    details['unknown'] += sequence.next()

                yield details

                details = {'name': '', 'offset': 0, 'length': 0, 'unknown': ''}

    def dump_file(self, details, base_directory):
        dest_filename = base_directory + '/' + details['name']

        if not os.path.exists(os.path.dirname(dest_filename)):
            os.makedirs(os.path.dirname(dest_filename))

        with open(dest_filename, 'wb') as dest:
            self.file.seek(details['offset'])
            dest.write(self.file.read(details['length']))

if __name__ == '__main__':
    cl_pak = ClPak(r'D:\Steam\steamapps\common\Crimsonland\data.pak')
    for file_details in cl_pak.read_index():
        print file_details
        cl_pak.dump_file(file_details, 'd:/clpak_files')