Morrowind Mod:BSA File Format
Overview[edit]
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 format[edit]
A Morrowind BSA is broken into 6 sections:
Type/Size | Info |
---|---|
struct (12 bytes) |
Header
|
struct[number of files] (8 bytes per entry) |
File size and offset
|
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. |
Sorting[edit]
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 calculation[edit]
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; }