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:
- ChunkIds —
EntryCount × 12bytes - OffsetAndLengths —
EntryCount × 10bytes (5B offset + 5B length, both big-endian) - PerfectHashSeeds —
HashSeedsCount × 4bytes (skipped if 0 or 0xFFFFFFFF) - ChunkIndicesWithoutPerfectHash —
NoHashCount × 4bytes - CompressedBlocks —
CompressedBlockCount × 12bytes - CompressionMethods —
CompressionMethodCount × CompressionMethodNameLengthbytes (null-terminated ASCII, e.g. "Oodle") - Signatures (if Signed flag) —
4B hash_size + hash_size×2 + CompressedBlockCount×20 - DirectoryIndex —
DirectoryIndexSizebytes (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
- Look up entry index from directory index
user_data - Get offset + length from OffsetAndLengths (5B big-endian each)
- Compute first/last compression blocks:
offset / CompressionBlockSize - For each block: seek in UCAS → read aligned to 16B → AES-ECB decrypt → Oodle decompress (if method > 0)
- 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 |