MK1 Format - thethiny/NRS-Asset-Manager GitHub Wiki

MK1 (Mortal Kombat 1) File Format

MK1 uses UE4.27 with IoStore (Zen storage). Unlike UE3 NRS games which use .xxx compressed archives, MK1 stores assets in IoStore containers (.utoc + .ucas) or standalone .uasset files.

Pipeline

UTOC (table of contents) + UCAS (content archive)
  → AES-256-ECB decrypt
  → Oodle v9 decompress
  → .uasset (midway equivalent)
  → Exports (DataTable, textures, etc.)

The .uasset IS the midway — it contains name/import/export tables and serialized export data, equivalent to a decompressed UPK in UE3 games.

UAsset Header — 64 bytes (0x40)

No magic number. The header is a fixed struct at the start of every .uasset.

Offset Size Field Notes
0x00 8 FilePathFName u64, name hash
0x08 8 EngineFilesCount u64
0x10 4 UFlags u32, serialization flags
0x14 4 DataLocationInUCas u32, offset into UCAS (for container-hosted assets)
0x18 4 NameTableOffset u32, byte offset to name table
0x1C 4 NameTableSize u32, name table size in bytes
0x20 4 UnkData1Offset u32, auxiliary data offset
0x24 4 UnkData1Size u32, auxiliary data size
0x28 4 ImportTableLocation u32, import table offset
0x2C 4 ExportsLocation u32, export table offset
0x30 4 UnkTable2Location u32, unknown metadata table offset
0x34 4 UnkTableOffset u32, unknown table offset
0x38 8 UnkTableSize u64, unknown table total size

Name Table

Located at NameTableOffset, variable length (total = NameTableSize bytes).

Each entry:

  • u16 length (byte-swapped: stored LE, read as BE)
  • UTF-8 string of that length (no null terminator)

Export Table Entry — 72 bytes (0x48)

9 × u64 fields. Located at ExportsLocation, count derived from (UnkTable2Location - ExportsLocation) / 72.

Offset Size Field Notes
0x00 8 ObjectLocation u64, data offset (in UCAS for container assets)
0x08 8 ObjectSize u64, serialized data size
0x10 8 ObjectName u64, index into name table
0x18 8 OuterIndex u64, ImportObjectIndex encoded
0x20 8 ClassIndex u64, ImportObjectIndex encoded
0x28 8 SuperIndex u64, ImportObjectIndex encoded
0x30 8 TemplateIndex u64, ImportObjectIndex encoded
0x38 8 GlobalImportIndex u64, ImportObjectIndex encoded
0x40 8 ObjectFlags u64, low byte = export type (0x0B = DataTable)

ImportObjectIndex Encoding

The u64 index fields pack type and value:

  • Bits [63:62] = Type: 0=Export, 1=ScriptImport, 2=PackageImport, 3=Invalid
  • Bits [61:0] = Value
  • For PackageImport: upper 32 bits = package index, lower 32 bits = export hash index

Import Table Entry — 8 bytes

Single u64, decoded as ImportObjectIndex. Count derived from (ExportsLocation - ImportTableLocation) / 8.

Export Data

Export data is stored sequentially after the last table (after UnkTable). For standalone .uasset files, exports are read from the current file position — NOT from ObjectLocation (which stores the UCAS container offset).

Exports with ObjectFlags & 0xFF == 0x0B are DataTable exports containing UE4 tagged properties.

UE4 Tagged Properties

Export data uses UE4's self-describing tagged property format:

FName PropertyName → FName PropertyType → type-specific data

Supported types: BoolProperty, ByteProperty, IntProperty (u8/16/32/64, signed/unsigned), FloatProperty, StrProperty, NameProperty, TextProperty, EnumProperty, StructProperty, ArrayProperty, MapProperty, ObjectProperty, SoftObjectProperty, FieldPathProperty.

Unlike UE3, container types (Array, Map) serialize their element types inline — no whitelists needed.

Special ObjectProperty Handlers

element_name Behavior
RowStruct Read super FName + class ref + children count, then per-child struct elements
mLootStruct Read super FName + one property
ScriptStruct Read source FName + reference + 0-3 properties
mPreReqStruct Read inner struct element

IoStore Container Format (UTOC + UCAS)

UTOC Header — 144 bytes

Magic: -==--==--==--==- (16 bytes, ASCII literal)

Offset Size Field Notes
0x00 16 Magic -==--==--==--==-
0x10 4 Version u32, = 3 for UE4.27
0x14 4 HeaderSize u32, = 144
0x18 4 EntryCount u32, number of chunks
0x1C 4 CompressedBlockCount u32
0x20 4 CompressedBlockEntrySize u32, = 12
0x24 4 CompressionMethodCount u32
0x28 4 CompressionMethodNameLength u32, = 32
0x2C 4 CompressionBlockSize u32, = 65536
0x30 4 DirectoryIndexSize u32
0x34 4 PartitionCount u32
0x38 8 ContainerId u64
0x40 16 EncryptionKeyGuid GUID (all zeros for MK1)
0x50 4 ContainerFlags u32, bitmask
0x54 4 HashSeedsCount u32 (0 or 0xFFFFFFFF = none)
0x58 8 PartitionSize u64 (0xFFFFFFFFFFFFFFFF = single)
0x60 4 NoHashCount u32

Container Flags:

Bit Flag
0 Compressed
1 Encrypted
2 Signed
3 Indexed

UTOC Section Layout (after header)

Sections are sequential, order is critical:

  1. ChunkIdsEntryCount × 12 bytes
  2. OffsetAndLengthsEntryCount × 10 bytes (5B offset + 5B length, both big-endian)
  3. PerfectHashSeedsHashSeedsCount × 4 bytes (skipped if 0 or 0xFFFFFFFF)
  4. ChunkIndicesWithoutPerfectHashNoHashCount × 4 bytes
  5. CompressedBlocksCompressedBlockCount × 12 bytes
  6. CompressionMethodsCompressionMethodCount × CompressionMethodNameLength bytes (null-terminated ASCII, e.g. "Oodle")
  7. Signatures (if Signed flag) — 4B hash_size + hash_size×2 + CompressedBlockCount×20
  8. DirectoryIndexDirectoryIndexSize bytes (AES-ECB encrypted if Encrypted flag)

Compressed Block Entry — 12 bytes

Packed as u64 + u32:

  • u64[0:40] → block offset in UCAS (40 bits)
  • u64[40:64] → compressed size (24 bits)
  • u32[0:24] → uncompressed size (24 bits)
  • u32[24:32] → compression method index (8 bits, 0 = uncompressed)

Directory Index (decrypted)

AES-256-ECB decrypted with game key. Structure:

Field Type Notes
mount_len i32 Mount point string length
mount string UTF-8, typically ../../../
dir_count u32 Number of directory entries
dirs[] struct×dir_count 16B each: name_idx(u32) + first_child(u32) + next_sibling(u32) + first_file(u32)
file_count u32 Number of file entries
files[] struct×file_count 12B each: name_idx(u32) + next_file(u32) + user_data(u32)
str_count u32 Number of strings
strings[] variable FString: i32 len + data (UTF-8 if len>0, UTF-16LE if len<0)

Sentinel value 0xFFFFFFFF = end of linked list. user_data = entry index into ChunkIds/OffsetAndLengths.

UCAS Data Reading

  1. Look up entry index from directory index user_data
  2. Get offset + length from OffsetAndLengths (5B big-endian each)
  3. Compute first/last compression blocks: offset / CompressionBlockSize
  4. For each block: seek in UCAS → read aligned to 16B → AES-ECB decrypt → Oodle decompress (if method > 0)
  5. Trim to exact offset within first block + length

Encryption

  • Algorithm: AES-256-ECB, no padding
  • Key: 0x6FAABA4F4EF8A6AC188A517ACEF38F1422484E3B1F3F4CF3DACB27A6CBCCD076
  • Applied to: UCAS data blocks (aligned to 16B) and UTOC directory index
  • Compression: Oodle v9 (Kraken/Leviathan/Mermaid), requires oo2core_9_win64.dll

Cross-Game Comparison

Feature MKX IJ2 MK11 MK1
Engine UE3 UE3 UE3 UE4.27
Container .xxx (ZLIB/LZO) .xxx (Oodle v4) .xxx (Oodle v5) IoStore (UTOC/UCAS)
Midway Decompressed UPK Decompressed UPK Decompressed UPK .uasset
Magic 0x9E2A83C1 0x9E2A83C1 0x9E2A83C1 None (no magic)
Encryption None None None (DFP separate) AES-256-ECB
Properties UE3 tagged (whitelist) UE3 tagged (whitelist) UE3 tagged (whitelist) UE4 tagged (self-describing)
Localization Coalesced (AES) Coalesced (AES) Coalesced (AES) .locres (JSON)
Bulk data TFC TFC PSF UCAS