-
Notifications
You must be signed in to change notification settings - Fork 19
BethesdaSoftworksArchive (BSA)
Game(s): Fallout 3 (v104), Fallout New Vegas (v104), Skyrim Legendary Edition (v104), Skyrim Special Edition (v105)
Game(s): Oblivion
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 |
Bit | Position | Description |
---|---|---|
1¹ | 0x1 | Folder names are present, before Folder File Record Table. (Game may not load without this set.) |
2¹ | 0x2 | File names are present, as a Name Table. (Game may not load without this set.) |
3 | 0x4 | Files are compressed by default. |
4² | 0x8 | Unknown, is checked for by game executable. Possibly instructs game to retain Folder names in memory. |
5² | 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) |
8² | 0x80 | Unknown, is checked for by game executable. Related to bInvalidateOlderFiles and bCheckRuntimeCollisions settings in 'Oblivion.ini' file, and somehow related to Bit 3. |
9¹ | 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.
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. ) |
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. |
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;
}
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;
}
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;
}
- 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.
Game(s): Morrowind
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. |
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). |
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;
}
- 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.