Morrowind Mod:BSA File Format

Mod / Morrowind: Morrowind Mod: Modding

OverviewEdit

BSA files are the archive format used by Morrowind, its expansions, and modders. It is a relatively simple container format compared to the Oblivion BSA format, and as such, fonts, music, sound, and splash screens are not recognized inside a BSA. Several tools have been written to open and create them.

Much of the information presented here originally came from ghostwheel's site and Timeslip's BSA code for NifSkope.

The formatEdit

A Morrowind BSA is broken into 6 sections:

Type/Size Info
struct
(12 bytes)
Header
uint32 - Magic number (0x00000100)
uint32 - Offset of the hash table in the file, minus the header size (12).
uint32 - Number of files in the BSA.
struct[number of files]
(8 bytes per entry)
File size and offset
uint32 - Size of the file
uint32 - Offset of the file in the data section
uint32[number of files] Name offsets: offset of the filename relative to the start of the Names section.
zstring[number of files] Names: lowercase ASCII, null-terminated. Offset given by corresponding name offset.
uint64[number of files] Filename hashes: used as the sort key for all records except raw file data. Note that hash collisions are not allowed and will generate an error.
uint8[][number of files] Raw data: uncompressed and unseparated byte data. The offset of each file is given by the corresponding offset in the file size/offset structure.

SortingEdit

BSAs themselves are sorted by file modification date, with the latest file being the one that takes effect. If a loose file (one in a subfolder of the Data Files folder) has a later date than the BSA file, then that file will be loaded. However, if a BSA's modification date is later than that of the loose file, the BSA's version of the file will be the one used by the game.

All records within an archive, except for the file data, are sorted by hash; file data appears to be sorted by the alphabetical order of file names.

Hashes are sorted first by lower four bytes, then by higher four bytes.

Hash calculationEdit

C++Edit

From the source of bsapack:

unsigned l = (len>>1);
unsigned sum, off, temp, i, n;

for(sum = off = i = 0; i < l; i++) {
        sum ^= (((unsigned)(name[i]))<<(off&0x1F));
        off += 8;
}
hash.value1 = sum;

for(sum = off = 0; i < len; i++) {
        temp = (((unsigned)(name[i]))<<(off&0x1F));
        sum ^= temp;
        n = temp & 0x1F;
        sum = (sum << (32-n)) | (sum >> n);  // binary "rotate right"
        off += 8;
}
hash.value2 = sum;

C#Edit

Provided by RobinHood70. (Requires C# 7+ for the local function; for lower versions, just move RotateRight to a separate function.)

public ulong GetHash(string name)
{
    static uint RotateRight(uint value, int numBits) => value << (32 - numBits) | value >> numBits;

    name = name.ToLowerInvariant();
    var midPoint = name.Length >> 1;
    var low = new byte[4];
    for (var i = 0; i < midPoint; i++)
    {
        low[i & 3] ^= (byte)name[i];
    }

    var high = 0U;
    for (var i = midPoint; i < name.Length; i++)
    {
        var temp = (uint)name[i] << (((i - midPoint) & 3) << 3);
        high = RotateRight(high ^ temp, (int)(temp & 0x1F));
    }

    return BitConverter.ToUInt32(low, 0) | (ulong)high << 32;
}