Skip to content

BethesdaSoftworksArchive (BSA)

Saqib edited this page Sep 22, 2022 · 37 revisions

v104/v105

Game(s): Fallout 3 (v104), Fallout New Vegas (v104), Skyrim Legendary Edition (v104), Skyrim Special Edition (v105)

Structure

Flags

Archive Flags

Archive Content Flags

Ordering

Hashing Algorithm

Notes

v103

Game(s): Oblivion

Structure

Name Type Required Info
Magic Int32 ✔️ 0x00415342 ("BSA\0")
Version Int32 ✔️ 103
Folder Record Table Offset UInt32 ✔️ Offset pointing to folder record table, usually equal to 36 (default header size).
Archive Flags UInt32 ✔️ How archive is structures or Oblivion should read data, see the Flags section for more details.
Folder Count UInt32 ✔️ Number of folders inside archive (that contain files).
File Count UInt32 ✔️ Number of files inside archive.
Total Folder Name Length UInt32 ✔️ Total length of all folder names plus null terminators.
Total File Name Length UInt32 ✔️ Total length of all file names plus null terminators.
Archive Content Flags UInt16 ✔️ Types of files in archive, see the Flags section for more details.
Unknown UInt16 ✔️ PC: 0x0000
PC (Shivering Isles Meshes): 0x1150
PC (Voices 2): 0x3A8D
XBox 360 (Sounds): 0x00F6
XBox 360 (Textures): 0x96DE
PS3: 0xCDCD
Folder Record Table Folder Record[Folder Count] ✔️ Folder Record:
> UInt64 - Folder Name Hash
> UInt32 - Folder File Count
> UInt32 - File Record Offset (+ Total File Name Length)
File Record Table File Record[Folder Count][Folder File Count] ✔️ Byte - Folder Name Length (Plus Null Terminator)
String - Folder Name
Byte - Null Terminator
Above only included if Bit 1 of Archive Flags is set.

File Record:
> UInt64 - File Name Hash (see Hashing Algorithm section)
> UInt32 - File Size (See Flags Section)
> UInt32 - File Offset (Relative to Raw Data Start)
Name Table String[File Count] Only included if Bit 2 of Archive Flags is set.

Array of strings representing file names (null-terminated), just file names no folder paths.
Data Record Table Data Record[File Count] ✔️ Data Record:
> UInt32 - Uncompressed File Data Size (Only Exists if File is Compressed)
> Byte[] - File Data

Flags

Archive Flags

Bit Position Description
0x1 Folder names are present, before Folder File Record Table. (Game may not load without this set.)
0x2 File names are present, as a Name Table. (Game may not load without this set.)
3 0x4 Files are compressed by default.
0x8 Unknown, is checked for by game executable. Possibly instructs game to retain Folder names in memory.
0x10 Unknown, set in official BSAs containing sounds (but not voices). Possibly instructs game to retain File names in memory.
6 0x20 Unknown, unobserved in any official PC BSAs.
7 0x40 Unkown (see Notes)
0x80 Unknown, is checked for by game executable. Related to bInvalidateOlderFiles and bCheckRuntimeCollisions settings in 'Oblivion.ini' file, and somehow related to Bit 3.
0x100 Unknown
10¹ 0x200 Unknown
11¹ 0x400 Unknown

¹ Set in all official PC BSA archives.
² No effect on file structure, most likely instructions for game.

Archive Content Flags

Bit Position Description
1 0x1 Meshes (.nif)
2 0x2 Textures (.dds)
3 0x4 Menus (.xml)
4 0x8 Sounds (.wav)
5 0x10 Voices (.mp3)
6 0x20 Text/Shaders (.bat, .html, .scc, .txt)
7 0x40 Trees (.spt, .stg)
8 0x80 Fonts (.fnt, .tex)
9 0x100 Miscellaneous (.ctl, etc.)

Ordering

File Records and Names (from Name Table, if included) must be in the same order. The first File Record is associated with the first Name... and this continues for all other files.

Section Required Ordering Scheme
Folder Record Table ✔️ Hash of folder name; least to greatest.
File Record Table ✔️ Hash of file name, only ordered within folder; least to greatest.

Hashing Algorithm

PC

Courtesy of UESP Wiki, based on Oblivion Mod Manager by Timeslip.

using System;
using System.IO;

...

public static ulong GetHash(string name, bool isFolder)
{
    name = name.Replace('/', '\\').ToLowerInvariant();

    string extension = string.Empty;
    if (!isFolder)
    {
        extension = Path.GetExtension(name);
        name = Path.ChangeExtension(name, null);
    }

    var hashBytes = new byte[]
    {
            (byte)(name.Length == 0 ? 0x00 : name[name.Length - 1]),
            (byte)(name.Length < 3 ? 0x00 : name[name.Length - 2]),
            (byte)name.Length,
            (byte)(name.Length == 0 ? 0x00 : name[0])
    };

    uint hash1 = BitConverter.ToUInt32(hashBytes, 0);

    switch (extension)
    {
        case ".kf":
            hash1 |= 0x80;
            break;

        case ".nif":
            hash1 |= 0x8000;
            break;

        case ".dds":
            hash1 |= 0x8080;
            break;

        case ".wav":
            hash1 |= 0x80000000;
            break;
    }

    uint hash2 = 0;
    for (int i = 1; i < name.Length - 2; i++)
    {
        hash2 = hash2 * 0x1003F + (byte)name[i];
    }

    uint hash3 = 0;
    for (int i = 0; i < extension.Length; i++)
    {
        hash3 = hash3 * 0x1003F + (byte)extension[i];
    }

    return (((ulong)(hash2 + hash3)) << 32) + hash1;
}

XBox 360

See Notes (Bit 7 note) regarding usage of this variation of the hashing algorithm.

Courtesy of SockNastre, heavily based on UESP Wiki.

using System;
using System.IO;

...

public static ulong GetHash(string name, bool isFolder)
{
    name = name.Replace('/', '\\').ToLowerInvariant();

    string extension = string.Empty;
    if (!isFolder)
    {
        extension = Path.GetExtension(name);
        name = Path.ChangeExtension(name, null);
    }

    var hashBytes = new byte[]
    {
            (byte)(name.Length == 0 ? 0x00 : name[name.Length - 1]),
            (byte)(name.Length < 3 ? 0x00 : name[name.Length - 2]),
            (byte)name.Length,
            (byte)(name.Length == 0 ? 0x00 : name[0])
    };

    uint hash1 = BitConverter.ToUInt32(hashBytes, 0);

    switch (extension)
    {
        case ".kf":
            hash1 |= 0x80;
            break;

        case ".nif":
            hash1 |= 0x8000;
            break;

        case ".dds":
            hash1 |= 0x8080;
            break;

        case ".wav":
            hash1 |= 0x80000000;
            break;
    }

    uint hash2 = 0;
    for (int i = 1; i < name.Length - 2; i++)
    {
        hash2 = hash2 * 0x1003F + (byte)name[i];
    }

    uint hash3 = 0;
    for (int i = 0; i < extension.Length; i++)
    {
        hash3 = hash3 * 0x1003F + (byte)extension[i];
    }

    // Reversing byte order of hash2 and hash3 combined
    // https://stackoverflow.com/a/18145923
    uint hashSecondPart = hash2 + hash3;
    hashSecondPart = (hashSecondPart & 0x000000FFU) << 24 |
                     (hashSecondPart & 0x0000FF00U) << 8  |
                     (hashSecondPart & 0x00FF0000U) >> 8  |
                     (hashSecondPart & 0xFF000000U) >> 24;

    return (((ulong)(hashSecondPart)) << 32) + hash1;
}

PS3

Only used for PS3 DirectDraw Surface (DDS) textures, all other assets use PC hashing algorithm.

Courtesy of SockNastre from BethesdaSoftworksArchive OblivionPS3, based on UESP Wiki.

using System;
using System.IO;

...

public static ulong GetHash(string name)
{
    name = Path.ChangeExtension(name, null).ToLowerInvariant();
    string extension = Path.GetExtension(name).ToLowerInvariant();
    var lastChar = (byte)(name.Length == 0 ? 0x00 : name[name.Length - 1]); // Last character of "name" string

    var hashBytes = new byte[]
    {
        lastChar,
        lastChar,
        (byte)name.Length,
        (byte)(name.Length == 0 ? 0x00 : name[0])
    };

    // Extra handling if second-to-last character is underscore
    if (name.Length > 2 && name[name.Length - 2].Equals('_'))
    {
        name = name.Remove(name.Length - 2); // "_X" must be removed from the filename
        hashBytes[2] -= 2; // Reduces name length byte by 2

        hashBytes[1] = (byte)(name.Length == 0 ? 0x00 : name[name.Length - 1]);
        hashBytes[0] ^= 0x80;
    }
    else
    {
        hashBytes[0] = 0x80;
    }

    hashBytes[1] ^= 0x80;
    uint hash1 = BitConverter.ToUInt32(hashBytes, 0);

    uint hash2 = 0;
    for (int i = 1; i < name.Length - 1; i++)
    {
        hash2 = hash2 * 0x1003F + (byte)name[i];
    }

    uint hash3 = 0;
    for (int i = 0; i < extension.Length; i++)
    {
        hash3 = hash3 * 0x1003F + (byte)extension[i];
    }

    return (((ulong)(hash2 + hash3)) << 32) + hash1;
}

Notes

  • For Bit 7 of Archive Flags, although the UESP Wiki stated this controls endianness of the BSA (specifically for XBox 360) this appears to not really be the case. Hashing algorithm slightly differs for XBox 360 involving endianness, but this specific bit seems to play no role in endianess for XBox 360 BSAs although they are set (and little-endian is also used for XBox 360 and PC). It is possible that this is something only the PC executable handles, but not on the XBox 360. This could simply control whether the hashing algorithm uses XBox 360 variation or not.
  • Although the 'Unkown' UInt16 in the BSA can be set to platform-specific values, that does not mean that those platforms require that. For example, on PS3 the meshes BSA has 0x0000 for its 'Unknown' which matches with PC.
  • Adding on to the last note, just because a BSA is on XBox 360 or PS3 does not necessarily mean it requires XBox 360 or PS3-specific hashing algorithms or structure values; sometimes PC hashing algorithms or structures are used as well, but there is generally a pattern for this (which is documented to the best of our capabilities). You will never see an XBox 360 hashing algorithm or structure value being used on PS3, and vice-versa. Safest bet is to always use platform-specific hashing algorithms or structure values unless indicated not to by some pattern.
  • XBox 360 (maybe) and PS3 require their hashing algorithms to be used when required for file/folder names, otherwise folders/files may just not load since the game cannot find them by the hash it is expecting. On XBox 360 it seems one just uses the provided hashing algorithm on all file/folder names. On PS3 it seems only DDS (texture files) use the PS3-specific hashing algorithm, while all other files use the PC hashing algorithm on PS3.
  • First unknown structure could be another type of flag structure.

v100

Game(s): Morrowind

Structure

Name Type Required Info
Magic/Version Int32 ✔️ 0x00000100 (Version 100)
Hash Table Offset UInt32 ✔️ Offset to hash table start relative to end of header.
File Count UInt32 ✔️ Number of files inside archive.
File Record Table File Record[File Count] ✔️ File Record:
> UInt32 - File Size
> UInt32 - File Offset (Relative to Raw Data Start)
Name Offset Table UInt32[File Count] Array of offsets pointing to file names, relative to start of name table.
Name Table String[File Count] Array of strings representing file names.
Hash Table UInt64[File Count] ✔️ Array of unsigned 64-bit integers representing hashed file names (see Hashing Algorithm section).
Raw Data Byte[File Count][] ✔️ All file data stored.

Ordering

File Records, Name Offsets, and Hashes must be in the same order. The first File Record is associated with the first Name Offset which is associated with the first Hash... and this continues for all other assets.

Section Required Ordering Scheme
File Record Table
Name Offset Table
Hash Table
✔️ First four bytes of the hash, then last four bytes of the hash; all least to greatest.
Raw Data Alphanumerically (a-z) based on file name (including path).

Hashing Algorithm

Courtesy of RobinHood from the UESP Wiki.

using System;

...

private static uint RotateRight(uint value, int numBits) => value << (32 - numBits) | value >> numBits;

public static ulong GetHash(string name)
{
    name = name.ToLowerInvariant();
    int midPoint = name.Length >> 1;

    var low = new byte[4];
    for (int i = 0; i < midPoint; i++)
    {
        low[i & 3] ^= (byte)name[i];
    }

    uint high = 0;
    for (int 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;
}

Notes

  • If Bloodmoon BSA is left with Name Offset Table and Name Table but any of the other BSAs lack them Morrowind PC returns the error "Failed to load snowflake: Meshes\BM_Snow_01.nif"; therefore Name Offset Table and Name Table must be removed from Bloodmoon BSA if it is removed from any others on PC.
  • PC Morrowind uses Name Offset Table and Name Table by default.
  • XBox Morrowind does not use Name Offset Table or Name Table by default.
  • XBox Morrowind uses alphanumeric ordering for Raw Data, but it does not include ordering the parent folder.