Skip to content

MK1 IoStore and DataTable

thethiny edited this page Jun 21, 2026 · 1 revision

MK1 IoStore & DataTable

IoStore Container System

MK1 uses UE4.27's IoStore (Zen storage) instead of UE3's .xxx archives. Assets are distributed across PAK files (legacy) and IoStore containers (UTOC + UCAS pairs).

Container Files

pakchunk0-WindowsNoEditor.utoc    — Table of contents (10MB, indices + directory)
pakchunk0-WindowsNoEditor.ucas    — Content archive (5.7GB, compressed + encrypted data)
pakchunk0-WindowsNoEditor.pak     — Legacy PAK (4.2GB, not yet supported)
global.utoc / global.ucas         — Engine globals (small, special handling needed)

MK1 ships with 26 UTOC/UCAS pairs. pakchunk0 alone contains 18,765 files (16,401 .uasset).

Extraction Pipeline

1. Parse UTOC header (144 bytes)
2. Read section tables (ChunkIds, OffsetAndLengths, CompressedBlocks)
3. Skip signatures section (if Signed flag — hash_size×2 + block_count×20 bytes)
4. Decrypt directory index (AES-256-ECB)
5. Walk directory tree to build virtual path → entry index mapping
6. For each requested file:
   a. Look up entry index → offset + length in UCAS
   b. Compute compression block range
   c. Read blocks from UCAS, align to 16 bytes
   d. AES-ECB decrypt each block
   e. Oodle v9 decompress (if method > 0)
   f. Trim to exact offset within first block + requested length

Key Discovery: Signatures Section

The UTOC section layout was initially parsed incorrectly because the signatures section was not accounted for. When ContainerFlags & 0x04 (Signed), a variable-length signature section sits between CompressionMethods and DirectoryIndex:

[4 bytes: hash_size]
[hash_size bytes: TOC signature]
[hash_size bytes: block signature]  
[compressed_block_count × 20 bytes: per-block SHA-1 hashes]

For pakchunk0, hash_size = 512, adding ~5.5MB of signature data. Skipping this was the key to finding the correct directory index offset.

Key Discovery: Counter Order in DFP

While implementing the DFP cipher (unrelated to MK1), a critical bug was found in the stream cipher's counter logic. The shellcode does check → maybe_reset → increment, but the Python port initially did increment → check → maybe_reset. This caused the counter to trigger state doubling one iteration early, corrupting all output after the 17th byte. The fix was verified against 5 reference files.

DataTable System

MK1's inventory and game configuration data is stored in UE4 DataTable assets. These are .uasset files with export type 0x0B.

Export Detection

Export type is encoded in the low byte of ObjectFlags:

  • 0x0B = DataTable (inventory, config, game data)
  • 0x39 = Unknown secondary type (also parseable)

DataTable Structure

DataTable exports start with an ObjectProperty containing a RowStruct reference:

FName "RowStruct"
FName "ObjectProperty"
u64   size (= 4)
u8    padding
i32   object_reference
FName object_super (typically "None")
i32   class_reference (signed, into name table)
u32   children_count
For each child:
    FName row_key (e.g., "SubZero_Fatality001")
    [Tagged properties until "None"]

Each row is a struct with tagged UE4 properties. The row key is the primary identifier.

Property Types

MK1 uses UE4's self-describing tagged property format. Each property:

FName property_name → FName property_type → type-specific data
Type Header (non-array) Data
BoolProperty u64 size + u8 value + u8 pad
IntProperty u64 size + u8 pad int of size bytes
FloatProperty u64 size + u8 pad float/double
StrProperty u64 size + u8 pad i32 len + string
NameProperty u64 size + u8 pad FName (u32+u32)
EnumProperty u64 class_id + FName class + u8 value_id + FName value dict
StructProperty u32 size + u32 dup_id + FName type + u8 + u64 + u64 recursive
ArrayProperty u64 size + FName element_type + u8 + u32 count elements
MapProperty u64 size + FName key_type + FName val_type + u8 + u32 unk + u32 count entries
ObjectProperty u64 size + u8 pad + i32 ref special dispatch by element_name
TextProperty u64 size + u16 unk + u32 unk2 3 strings (or empty)
SoftObjectProperty u64 size + u8 pad FName path + u32 subpath

Key difference from UE3: Container types (Array, Map) serialize their element types inline. No whitelist lookup needed.

FName Format

u32 name_index (into name table)
u32 name_suffix (0 = no suffix, N = append "_{N-1}")

Inventory File Examples

File Contents Rows
SubZero_Fatalities Fatality moves 2
SubZero_Gear Equipment items 35
SubZero_Skins Costume variants 24
SubZero_Character Character config 1
Announcer Voice pack entries 1
Bundles DLC/store bundles 18
Consumables Consumable items 6

Sample Output

{
  "RowStruct": {
    "SubZero_Fatality001": {
      "Title": ["", "C5CD3A29...", "(Anywhere) Hairline Fracture"],
      "Rarity": {
        "class": "EMKInventoryItemRarity",
        "value": "EMKInventoryItemRarity::Rarity1"
      },
      "bIsDefaultItem": true,
      "MaxCount": 1,
      "Tags": ["Fatality", "SubZero"],
      "PreviewIcon": "/Game/Disk/Char/SubZero/UI/UIFatalities/SubZero_A.SubZero_A",
      "BuyPriceDefinition": {
        "CurrencyType": { "DataTable": "...", "RowName": "None" },
        "PricePerItem": 100
      }
    }
  }
}

Browser Integration

Mounting

  • Standalone .uasset: Parsed directly as midway
  • .utoc container: Parsed to extract directory index → all contained files listed as exports in the browse tree

IoStore Browse Flow

  1. User adds MK1 game path → auto-resolves to MK12/Content/Paks/
  2. File detector finds .utoc files (mountable) and .ucas files (companions, filtered)
  3. Mounting a .utoc creates a flat list of 18,765 virtual file entries
  4. Tree shows MK1 / Engine / ... and MK1 / MK12 / Content / ... (../../../ prefix stripped)
  5. Single-click on a .uasset node: extracts from container, parses midway, shows DataTable JSON in right pane
  6. Double-click on a .uasset node: expands tree node with internal exports as children

Clone this wiki locally