ESOMod:MNF File Format

The purpose of MNF files are as indexes to the sub-files stored within DAT files. There are 3 known MNF files:
:* \game\client\game.mnf
:* \depot\eso.mnf
:* \vo_en\esoaudioen.mnf

Each of these are matched with one or more DAT files with the similar name (game0000.dat, eso0000.dat, esoaudioen0000.dat).


File Format
The MNF files have a short header followed by one or more blocks of compressed data.

     [File Header (0x0F bytes)]
     [Block 0..N]
          [Block Data...]

Byte data is little endian unless noted.

File Header

Version 2
This format of the file header occurs in all MNF files before update 25. The MNF file header appears to be a fixed size of 15 (0x0F) bytes and contains a few fields:

     byte  MAGIC_DWORD[4] = "MES2"        /* MES MAGIC WORD */
     word  MES_VERSION = 0x0002           /* MES FORMAT VERSION */
     byte  FileCount                      /* NUMBER OF DAT FILES THAT CORRESPOND TO THE MNF FILE */
     dword MNF_TYPE = 0x0000001 (eso.mnf) || 0x0000006 (game.mnf)
     dword DataSize                       /* DataSize is the total amount of data that follows the file header */

Version 3
This format of the file header occurs in all MNF files on and after update 25. The MNF file header is now a variable size:

     byte  MAGIC_DWORD[4] = "MES2"
     word  MES_VERSION = 0x0003
     dword FileCount
     word  FileTypes[FileCount]           /* Eso.mnf has increasing numbers here 01..FD, 01, Game.mnf has 06 */
     word  Unknown1 = 0x0000
     dword DataSize                       /* DataSize is the total amount of data that follows the file header */

Data Blocks
One or more data blocks follow the MNF file header in a contiguous manner. The block format is identified by a 2 byte block id at the start of the block data. At the moment there are two known block types.

Block Type 3
This is the most common block type found in all three MNF files. Note that the block and data header information is all stored in big endian bit order. This block is composed of a short fixed size header followed by three data blocks with the same overall format:

     [Block Header (0x12 bytes)]
     [Data Block 3.1]
          [Data Header (8 bytes)]
          [Data (...)]
     [Data Block 3.2]
          [Data Header (8 bytes)]
          [Data (...)]
     [Data Block 3.3]
          [Data Header (8 bytes)]
          [Data (...)]

The block header is a fixed size of 18 (0x12) bytes:

     word  BlockID = 0x0003
     dword FieldSize = 0x00000004 /* Size of each field of a record - Not used by the game */
     dword RecordCountB1
     dword RecordCountB2
     dword RecordCountB3

The record counts are the number of records found in each of the three data blocks that follow (see below). If all three record counts are zero the block contains no data and should not be processed (started happening in the ZOSFT file in the Thieves Guild DLC update).

Data Header
Each of the three data blocks are preceded by an 8 byte header:

     dword UncompressedSize
     dword CompressedSize

The CompressedSize is the number of data in bytes that follows the data header. The data is assumed to be in the zLib format.

Data Block 3.1
This is the first of three data blocks in a type 3 block in a MNF file. The data is compressed in a zLib format and once uncompressed it is composed of multiple fixed sized records of 4 bytes with the format:

     dword Unknown1
     
     Uncompressed_Size = RecordCountB1 * FieldSize

Data Block 3.2
This is the second of three data blocks in a type 3 block in a MNF file. The data is compressed in a zLib format and once uncompressed it is composed of multiple fixed sized records of 8 bytes with the format:

    dword Unknown1
    dword Unknown2

The number of records is equal to RecordCountB2 found in the block header. 

    Uncompressed_Size = RecordCountB2 * 2 * FieldSize

Data Block 3.3
This is the third of three data blocks in a type 3 block in a MNF file. The data is compressed in a zLib format and once uncompressed it is composed of multiple fixed sized records of 20 (0x14) bytes with the format (in MNF version 2 and lower):

    dword UncompressedSize
    dword CompressedSize
    dword FileHash
    dword FileOffset
    byte  CompressType
    byte  ArchiveIndex
    word  Unknown

In version 3 (and higher) MNF files the CompressType and ArchiveIndex fields are swapped:

    dword UncompressedSize
    dword CompressedSize
    dword FileHash
    dword FileOffset
    byte  ArchiveIndex
    byte  CompressType
    word  Unknown

The number of records is equal to RecordCountB3 found in the block header.

    Uncompressed_Size = RecordCountB3 * 5 * FieldSize

The size fields are simply the size of the file when compressed and uncompressed. The compressed size are the number of bytes that need to be read from the appropriate DAT file. The FileOffset is the offset from the beginning of a DAT file where the file data begins. The CompressType is one of:
:* 0 = Uncompressed
:* 1 = zLib
:* 2 = Snappy
The ArchiveIndex specifies which data file to find it in (i.e., ArchiveIndex=23 in eso.mnf means look in eso0023.dat). The last two bytes are unknown but take on a variety of purposes.

Block Type 0
This block type is only found as the first block in eso.mnf. Like the other block the header information here is all stored in big endian bit order. This block is composed of a short fixed size header followed by two data blocks with the same format:

     [Block Header (0x04 bytes)]
     [Data Header (0x04 bytes)]
     [Data]
     [Data Header (0x04 bytes)]
     [Data]

The block header format is a small 4 bytes:

     word  BlockID = 0x0000
     word  Unknown1 = 0x0000

In newer version of the client, the uncompressed size present in BlockId 3 is not present in BlockId 0, so the data header looks like:

     dword CompressedSize


The CompressedSize is the number of data in bytes that follows the data header. The data for this block is unknown (appears to be compressed but unknown format).

In eso.mnf, there is a version 3 block following the version 0 block.

Extra Data
The file eso.mnf has an unknown block of 00s at the end of the file 2497716 (0x261CB8) bytes in size.

Notes
:* eso.mnf
::* RecordCount1 = 527361
::* RecordCount2 = 258797
::* RecordCount3 = 313810
::* Block 1 Size = 2109444 (/4 = 527361)
::* Block 2 Size = 2510480 (/8 = 313810)
::* Block 3 Size = 6276200 (/20 = 313810)
::* Non-Zero entries in Block 3 = 313364
:* game.mnf
::* RecordCount1 = 8241
::* RecordCount2 = 3094
::* RecordCount3 = 3094
::* Block 1 Size = 32964 (/4 = 8241)
::* Block 2 Size = 24752 (/8 = 3094)
::* Block 3 Size = 61880 (/20 = 3094)
::* Non-Zero entries in Block 3 = 3094
:* esoaudioen.mnf
::* RecordCount1 = 263681
::* RecordCount2 = 120345
::* RecordCount3 = 120345
::* Block 1 Size = 1054724 (/4 = 263681)
::* Block 2 Size = 962760 (/8 = 120345)
::* Block 3 Size = 2406900 (/20 = 120345)
::* Non-Zero entries in Block 3 = 120345
:* The patch for the 8/02/2014 beta does not appear to have changed the MNF format.

Misc Stuff
Most of the below is slightly incorrect or out of date but kept here until the required information is merged to the main article above.

     [MNF] - have 3 compressed blocks

     0x4 - szID (always MES2)
     0x15 - unknown  (unique id?)
     0x4 - szFilesCount (endian BIG)
     0x4 - szFilesCount (endian BIG)
     
     0x4 - szBlockSize (endian BIG)
     0x4 - szBlockZSize (endian BIG)
          [........Block........]
     
     0x4 - szBlockSize (endian BIG)
     0x4 - szBlockZSize (endian BIG)
          [........Block........]
     
     0x4 - szBlockSize (endian BIG)
     0x4 - szBlockZSize (endian BIG)

     Blocks 1 unknown tables

     Block 2: 8 bytes per file?
          
     [Block 3] - Endian Little     
          0x4 - szFileSize
          0x4 - szFileZSize
          0x4 - szUnknown01 (Hash?)
          0x4 - szOffset
          0x1 - szComType(0 - Not Compressed, 1 - Zlib, 2 - Snappy)
          0x1 - szArchiveNum
          0x2 - szUnknown02
          [........Block........]


     Hash Init: 0xA8396u
     Use Hash.cpp from RDF-3X project (

     
              unknown1  num_dat_archives       EOF_offset           dummy         unknown2
     FILENAME         [02 00] [XX] 00 00 00 00 [XX XX XX XX] 00 03 [00 00 00] 04 [XX XX XX XX]
     eso.mnf:         [02 00] [D0] 00 00 00 00 [7B FB 4F 00] 00 03 [00 00 00] 04 [00 08 0C 01]
     esoaudioen.mnf:  [02 00] [04] 00 00 00 00 [2B 09 1E 00] 00 03 [00 00 00] 04 [00 04 06 01]
     game.mnf:        [02 00] [01] 00 00 00 00 [3A A2 00 00] 00 03 [00 00 00] 04 [00 00 10 19]
     
     unknown1: This always has to be 2 for some reason. The files don't open otherwise.
     num_dat_archives: number of DAT files
     EOF_offset (little endian): After reading these 4 bytes, add EOF_offset to current offset to reach the end of the third block. This is EOF except for eso.mnf which has extra data.
     unknown2 (big endian): This is 4120*(2^n)+1, with n ranging from 0 to 2. It seems n is also the number of the DAT archive containing the ZOSFT, but maybe this is just a coincidence.


More detailed stuff for reference:
 
FILE STRUCTURE ANALYSIS
------ DAT: Decompiled ------
0x4 - ID string ('2SEP'/'PES2')
0x2 - always 1 (little endian)
0x4 - always 0
0x4 - always 0E (14 in decimal). Seems to be the absolute offset of the data start
 
 
------ MNF: Decompiled ------
0x4 - ID string ('2SEM'/'MES2')
0x2 - always 2 (little endian)
0x1 - number of DAT files
0x4 - always 0
0x4 - EOF_offset: offset after block 3
 
------ ZOSFT ----------------
0x5 - ID string ('ZOSFT')
0x4 - always 06 00 10 00
0x6 - unknown
0x4 - number of records, little endian
for(i=0; i