TO:R Data Map

Containers: how a game file is put together

Source
docs/formats/containers.md
SHA-256
1200789c…6b6acac7

How a game file is built, from the .dat on disk down to its entries, and how it is rebuilt. A .dat is obfuscated with RotXor; inside is a ZIP holding one member, usually a pakd archive of numbered entries. What's inside an entry (tables, text, …) has its own format doc: xlce for data tables, txte for text. Terms are as in the glossary.

Based on Gibbed's DataBogoCrypt.cs and BaseBogoCrypt.cs (reference_tools/Gibbed.LetUsClingTogether/projects/Gibbed.Reborn.FileFormats/Crypt/, the same in reference_tools/gibbed10/projects/Gibbed.Reborn.FileFormats/Crypt/), Gibbed.Reborn.DecryptData/Program.cs and Gibbed.Reborn.EncryptData/Program.cs; PackFile.cs (reference_tools/Gibbed.LetUsClingTogether/projects/Gibbed.TacticsOgre.FileFormats/, the same in reference_tools/gibbed10/); Gibbed.LetUsClingTogether.PackFileTable/Program.cs and Gibbed.LetUsClingTogether.UnpackFileTable/Program.cs; PKWARE's APPNOTE.TXT for the ZIP records; the 551 game files named under Method, all of them.
Checked 2026-10-11
Method Layout copied from these sources and checked against them. How the game reads it: static analysis of the unpacked executable (SHA-256 7b0230a11498f40396c9048345e7cfe0ef442cb7ca8faffc6f197920d2669599) with Ghidra 12.1.4, via rea and the decompiled code in local/exe/decomp/; the R31 review of names (2026-10-11). Checked against the game data, 2026-10-11, over the 551 files the docs cover (battle/, localize/LocalizeText_en-us.dat, menu/menu_data.dat, event/chapter/, event/event_data.dat): the round trip, tor round-trip <those paths> --docs docs, rebuilt 551/551 files byte for byte (1,182/1,182 decoded entries, 551/551 payloads); the scan, a C# program over the same files (session scratchpad r22/scan.cs), tallied every ZIP record value and checked every pakd's layout (582 packs, 31 of them nested; 1,841 entries). Every file in Data/ (34,227), 2026-10-11 (R28): the survey, tor survey local/game/Data --docs docs, unwrapped every file as Files that are not RotXor + ZIP says (0 that can't be) and listed each ZIP member's name and payload's magics; the full round trip, tor round-trip local/game/Data --docs docs --quiet, rebuilt 34,224 of the 34,227 files byte for byte (the 3 others are in Rebuilding); the ZIP tally, a C# program (session scratchpad r28/ziptally.cs) over the ZIP records of all 10,016 RotXor + ZIP files. No in-game tests yet.

Structure

A game file is a box inside a box: obfuscated, then zipped, around an archive of numbered entries. Each entry is one piece of data.

<name>.dat                      game file
└── RotXor obfuscation          (Layers, 1)
    └── ZIP, one member "pack"  (Layers, 2–3)
        └── pakd archive        (pakd archives)
            ├── #0   an entry
            ├── #1   an entry
            └── …

The tree is an illustration of the nesting, not of any one file. Not every file is built this way: some are wrapped otherwise, or not at all (Files that are not RotXor + ZIP), and some ZIP members are a single file of another format, not a pakd (Layers). An entry's first 4 bytes say what kind it is:

Layers

Layer What it is Confidence Source
1. Obfuscation RotXor, as in RotXor. No plaintext header. It doesn't change the length. confirmed The round trip (every file deobfuscated and obfuscated back byte for byte); the scan (each ZIP is as long as its file, 551/551). The game's own deobfuscation, 0x1401a2cf0 (How the game reads it). Gibbed DataBogoCrypt agrees (works in place, same length).
2. ZIP A ZIP archive holding a single member, compressed with DEFLATE (method 8), laid out as in ZIP layout. confirmed The scan: all 551 are exactly that, with nothing after the end record; the ZIP tally: every RotXor + ZIP file has one member, method 8. Gibbed UnpackFileTable agrees (reads a ZIP as exactly one member and fails on a second).
3. Payload The single member. When it is named pack or <name>.pack, it is a pakd; otherwise its name has a format extension and it is that format: .img and .btx a btx texture, .ash an ashg, .pac an ARC, .bin no known magic. confirmed The survey of every RotXor + ZIP file (R28; again 2026-10-13, tor survey local/game/Data --docs docs): every member named pack or *.pack is a pakd; every *.img and *.btx, a btx; every *.ash, an ashg; the *.pac (Shaders/Shader.dat), an ARC; every *.bin, no known magic.

RotXor

The obfuscation works on the whole file in place. All three steps, and the two keys, are confirmed (source: the round trip, through RotXor.FromDoc, which reads the keys from this doc; the game's 0x1401a2cf0, below; Gibbed DataBogoCrypt.Decrypt and Encrypt, BaseBogoCrypt, whose "BogoCrypt" this is, agree).

Deobfuscating, in order:

  1. Rotate left by 3 bits. The whole file is treated as one string of bits, most significant bit of byte 0 first, and rotated left by 3: byte i becomes (b[i] << 3) | (b[i+1] >> 5), and the last byte takes its low bits from the first byte (0x1401a2b70; Gibbed Deshift).
  2. XOR with key 1, start word 8. From byte 0, each whole word g (0, 1, 2, …; 4 bytes) is XORed with word (8 + g) mod 15 of key 1. Bytes after the last whole word are left alone.
  3. XOR with key 2 from byte 16. From byte 16 on, each whole word g is XORed with word (start + g) mod 17 of key 2, where the start word is byte[15] & 0x7F, read after step 2. Step 3 doesn't touch byte 15, so the start word comes from byte 15 of the plain file.

Obfuscating runs the steps in reverse (Gibbed Encrypt): step 3 with the start word from plain byte 15, then step 2, then rotate the whole file right by 3 bits (byte i becomes (b[i-1] << 5) | (b[i] >> 3), and byte 0 takes its high bits from the last byte; Gibbed Enshift).

Word g of the file is XORed with key word k = bytes 4k to 4k + 3 of the key, in order (Gibbed XorWithTable).

Key 1 (60 bytes, 15 words):

B0 BC 6D 19 E5 14 B6 EA F4 CB 16 B1 7D E5 7F B5
0B 7C 5E C4 2D 4F 31 99 67 98 19 E8 28 58 DE C9
C1 9B C9 83 34 C4 64 85 36 FA 0E DE FB E7 68 99
71 32 34 36 E5 01 7D 63 5C 81 77 7C

Key 2 (68 bytes, 17 words):

C6 53 98 ED D0 AA E0 1B 43 31 16 32 CE 0D 52 57
7B 72 0F FB B7 A9 C6 F1 F1 72 2C 89 5F B2 1B FA
A8 DB E3 E2 41 B5 BE C9 5D AE C6 21 E4 D2 88 74
97 26 4F 87 7A F0 69 7A B7 7E 15 09 BA 55 AD CA
B1 60 34 76

ZIP layout

The ZIP holds nothing beyond the one member: a local file header, the compressed data, one central directory record and the end of central directory record, in that order. The record layouts are the standard ones (APPNOTE.TXT, checked 2026-10-04). The values in Meaning are the ones in the game's files: the scan found each in all 551, and the round trip rebuilds every record byte for byte (it keeps the original's values, so the values come from the scan). confirmed: across all of Data/, every RotXor + ZIP file but one has exactly these values (the ZIP tally, R28). The one that doesn't, Shaders/Shader.dat (member sh_tacos.pac), was made by another tool: mod_time and mod_date set (0x644C, 0x54A9, 2022-05-09), version_made_by 0x003F (6.3), central flags 0x0000; its DEFLATE stream isn't zlib's (Rebuilding). n is the length of the member's name (4 for pack); csize is the compressed size.

The file in order:

Offset Size Type Name Meaning Confidence Source
0x0 30 + n — local_header Local file header. confirmed The round trip; the scan. APPNOTE 4.3.7.
30 + n csize raw[csize] compressed_data Raw DEFLATE stream of the member (Rebuilding). confirmed The round trip; the scan. APPNOTE 4.3.8; Gibbed UnpackFileTable and PackFileTable agree (DEFLATE).
30 + n + csize 46 + n — central_directory One central directory record, for the member. confirmed The round trip; the scan. APPNOTE 4.3.12.
next 22 — end_of_central_directory End of central directory record. No comment; ends the file. confirmed The round trip; the scan. APPNOTE 4.3.16.

Local file header

Every value here is the one in all 551 files checked (the scan), and every record rebuilds byte for byte (the round trip).

Offset Size Type Name Meaning Confidence Source
0x00 4 raw[4] signature 50 4B 03 04 (PK\3\4). confirmed The scan. APPNOTE 4.3.7.
0x04 2 u16 version_needed 20. confirmed The scan. APPNOTE 4.3.7; Gibbed PackFileTable agrees (insists on version 20 for the ZIPs it writes).
0x06 2 u16 flags 0x0000. Bit 3 clear, so no data descriptor follows the data. confirmed The scan. APPNOTE 4.3.7, 4.4.4.
0x08 2 u16 method 8 (DEFLATE). confirmed The scan. APPNOTE 4.4.5; Gibbed PackFileTable agrees (CompressionMethod.Deflated).
0x0A 2 u16 mod_time 0. confirmed The scan. APPNOTE 4.3.7.
0x0C 2 u16 mod_date 0. confirmed The scan. APPNOTE 4.3.7.
0x0E 4 u32 crc32 CRC-32 of the uncompressed member. confirmed The scan (checked against each inflated member). APPNOTE 4.3.7, 4.4.7.
0x12 4 u32 compressed_size csize. confirmed The scan. APPNOTE 4.3.7.
0x16 4 u32 uncompressed_size Size of the member. confirmed The scan. APPNOTE 4.3.7.
0x1A 2 u16 name_length n. confirmed The scan. APPNOTE 4.3.7.
0x1C 2 u16 extra_length 0: no extra field. confirmed The scan. APPNOTE 4.3.7.
0x1E n raw[n] name The member's name: pack in every file checked. confirmed The scan. APPNOTE 4.3.7.

Central directory record

Offset Size Type Name Meaning Confidence Source
0x00 4 raw[4] signature 50 4B 01 02 (PK\1\2). confirmed The scan. APPNOTE 4.3.12.
0x04 2 u16 version_made_by 20 (0x0014), made on MS-DOS (upper byte 0). confirmed The scan. APPNOTE 4.4.2.
0x06 2 u16 version_needed 20. confirmed The scan. APPNOTE 4.3.12; Gibbed PackFileTable agrees.
0x08 2 u16 flags 0x0800: bit 11, the name is UTF-8. Differs from the local header's flags. confirmed The scan. APPNOTE 4.4.4.
0x0A 2 u16 method 8 (DEFLATE). confirmed The scan. APPNOTE 4.4.5.
0x0C 2 u16 mod_time 0. confirmed The scan. APPNOTE 4.3.12.
0x0E 2 u16 mod_date 0. confirmed The scan. APPNOTE 4.3.12.
0x10 4 u32 crc32 As in the local header. confirmed The scan. APPNOTE 4.3.12.
0x14 4 u32 compressed_size csize. confirmed The scan. APPNOTE 4.3.12.
0x18 4 u32 uncompressed_size Size of the member. confirmed The scan. APPNOTE 4.3.12.
0x1C 2 u16 name_length n. confirmed The scan. APPNOTE 4.3.12.
0x1E 2 u16 extra_length 0. confirmed The scan. APPNOTE 4.3.12.
0x20 2 u16 comment_length 0. confirmed The scan. APPNOTE 4.3.12.
0x22 2 u16 disk_number_start 0. confirmed The scan. APPNOTE 4.3.12.
0x24 2 u16 internal_attributes 0. confirmed The scan. APPNOTE 4.3.12.
0x26 4 u32 external_attributes 0x20 (MS-DOS archive bit). confirmed The scan. APPNOTE 4.3.12.
0x2A 4 u32 local_header_offset 0. confirmed The scan. APPNOTE 4.3.12.
0x2E n raw[n] name As in the local header. confirmed The scan. APPNOTE 4.3.12.

End of central directory record

Offset Size Type Name Meaning Confidence Source
0x00 4 raw[4] signature 50 4B 05 06 (PK\5\6). confirmed The scan. APPNOTE 4.3.16.
0x04 2 u16 disk_number 0. confirmed The scan. APPNOTE 4.3.16.
0x06 2 u16 central_directory_disk 0. confirmed The scan. APPNOTE 4.3.16.
0x08 2 u16 members_on_disk 1: the one member. confirmed The scan. APPNOTE 4.3.16 ("total number of entries … on this disk"); Gibbed UnpackFileTable agrees (one member).
0x0A 2 u16 members_total 1. confirmed The scan. APPNOTE 4.3.16 ("total number of entries"); Gibbed UnpackFileTable agrees (one member).
0x0C 4 u32 central_directory_size 46 + n. confirmed The scan. APPNOTE 4.3.16.
0x10 4 u32 central_directory_offset 30 + n + csize. confirmed The scan. APPNOTE 4.3.16.
0x14 2 u16 comment_length 0: no comment. confirmed The scan. APPNOTE 4.3.16.

Files that are not RotXor + ZIP

Every other file in Data/ is RotXor + ZIP. The toolchain reads each file's wrapping from the Files and Wrapping columns of this table (Wrapping.FromDoc; globs matched case-insensitively against the path under Data/, * also matching /): MLZ0 → LZ4, XOR with key 1, or none, where the file is the payload.

confirmed: this table covers every file in Data/ that isn't RotXor + ZIP, and each wrapping is removed and put back byte for byte: every file below (the .sab and .efz files, the movies and the single files) rebuilds identically through the toolchain's Wrapping (MLZ0 packed again with LZ4's default compressor, our copy (../../toolchain/src/LZ4/UPSTREAM.md) of K4os.Compression.LZ4 1.3.8), and no file in Data/ fails to unwrap. Source: the full round trip and the survey (R28, 2026-10-11).

Files Raw magic Wrapping Notes Confidence Source
FileTable.bin none (F1 1E A6 03) none Not wrapped: the game's file table (filetable.md). confirmed Hex inspection; read as is by its loader 0x1402b1490 (filetable.md).
effect/**/*.efz MLZ0 MLZ0 → LZ4 LZ4: MLZ0, the unpacked size (u32), then one LZ4 block from byte 8 to the end of the file. The payload is an EFX0011 effect. Not RotXor. The game's effect loads read it this way (How the game reads it). confirmed The round trip of effect/, 2026-10-13: tor round-trip local/game/Data/effect --docs docs --quiet, every file byte-identical (2,087/2,087): the unwrap decodes each block from byte 8 to the end of the file and refuses one that doesn't unpack to exactly the size at byte 4, and LZ4's default compressor gives back every .efz byte for byte (efx.md). Gibbed UnpackFileTable (MLZ0 → LZ4Codec.Decode); the game's unwrap kind 2 (0x1401a2a00).
effect/global/common/indices.bin none (94 09 00 00) none Not wrapped: a table, effect_indices: vfx ID → set of c_all.efz (Vfx). confirmed Hex inspection; read as is by 0x140311b20.
effect/EffectMaterialData.bin xlce none A plain, unwrapped xlce table, effect_material_data, rows of 0x74 bytes (Vfx). confirmed Hex inspection; read as is by 0x140311b20.
sound/**/*.sab sabf none A sead sound bank (sabf.md). The same magic is in pakd entries of sound/bin/SECommon.dat, title/title.dat and event/scene/*.dat. confirmed The survey, 2026-10-13: tor survey local/game/Data/sound local/game/Data/title local/game/Data/event/scene --docs docs, every .sab file's payload is sabf, and sabf entries are in sound/bin/SECommon.dat, title/title.dat and event scene packs (sabf.md).
sound/sead.config scnf none The sound engine's settings (scnf.md). confirmed Hex; scnf.cs.
movie/*.pmf, movie/*.PMF FF DB 0A 4A ... XOR with key 1 Not RotXor + ZIP: an Ogg Theora file XORed word by word with RotXor's key 1, word w of the file with key word w mod 15 (pmf.md). confirmed Exploration program pmfscan.cs over every movie file (every Ogg page CRC right); the game's read callback 0x14026c260 → 0x1402b0970 → 0x1401a2bf0.

pakd archives

Offsets are from the start of the pakd (its magic). Checked in all 582 packs of the files checked (the scan), and rebuilt from these rules byte for byte in all 551 top-level packs (the round trip, through Pakd.Build); Gibbed's PackFile agrees throughout.

Offset Size Type Name Meaning Confidence Source
0x0 4 raw[4] magic pakd. Gibbed also accepts it byte-swapped, and then reads the pack as big-endian; no file checked is. confirmed The scan. Gibbed PackFile (Signature).
0x4 4 u32 count Number of entries. confirmed The scan; the round trip. Gibbed PackFile.
0x8 4 × count u32[count] offsets Start of each entry, from the start of the pakd. confirmed The scan; the round trip. 0x1402d4a30; Gibbed PackFile.
0x8 + 4 × count 4 u32 pack_size The pack's own size: the end of the last entry, from the start of the pakd. For a top-level pack it is the payload's size; for a nested pack it is 16 less than its entry's size (below). The game skips it. confirmed The scan (551/551 top-level, 31/31 nested); the round trip writes it as the end of the last stored entry (Pakd.Build). ids readers start at 8 + 4 × (count + 1) (0x1402702a0, 0x1402bbcd0, 0x140350380). Gibbed PackFile (TotalSize).
after pack_size 4 × count u32[count] ids Optional ID table, one u32 per entry, present when there's room for it and unk_header_end: the first entry starts at or past 8 + 4 × (count + 1) + 4 × count + 4. In some of the packs checked some ID is non-zero. In most of the others there's room for the table before the first entry, but it's all zero, which can't be told from padding: the rebuild writes no table there and gives back the same bytes, so an all-zero table and no table are the same thing. Gibbed writes the table only when an ID is non-zero. What the game makes of an ID depends on the loader (Reading a pakd). confirmed The scan; the round trip (Pakd.Ids, Pakd.Build). Gibbed PackFile (RawId, read as a DirectoryId and a FileId).
next 4 u32 unk_header_end Always 0 (582/582 packs); no code reads it. Its presence shows in the data: battle_data_release.dat#0 has 25 entries, so 8 + 4 × 26 = 112 is already 16-aligned, yet its first entry is at 0x80. confirmed The scan; the round trip (Pakd.Build). Gibbed PackFile agrees (zero; rejects a pack where it isn't 0).
next varies raw[n] padding Zeros up to the first entry, at the next 16-byte boundary: the header is align16(8 + 4 × (count + 1) + 4) bytes, or align16(8 + 4 × (count + 1) + 4 × count + 4) with ids. confirmed The scan (582/582 packs: first entry at that offset, padding all zero); the round trip. Gibbed PackFile.GetHeaderSize.

How entries are stored:

How the game reads it

How the executable unwraps a file after reading it (filetable.md covers the read itself). Every fact here is static analysis of the unpacked executable (Ghidra 12.1.4, the decompiled code in local/exe/decomp/), so at most likely; the functions are in the names list.

Reading a pakd

So a rebuilt pakd needs correct count, offsets and, where it has them, ids; the rest of the header is kept for a byte-identical rebuild, not because the game needs it.

Rebuilding

How a payload is wrapped back into a .dat. Each layer is rebuilt as laid out above; the table gives what the rebuild has to work out. Every rule is proven by the round trip over the 551 files checked (see Method); whether the game loads a rebuilt file is not (below).

Layer Rule Confidence Source
Entries The format module that encodes an entry pads it as above: align16(size + 1) when the original's bytes after its data were exactly that, otherwise those bytes kept as they are. confirmed The round trip: 1,182/1,182 decoded entries byte-identical.
pakd Offsets and pack_size worked out from the entries; ids kept as is when the original had a non-zero one, and no table written otherwise. Nested packs are rebuilt first, bottom up, then padded with 16 zeros. confirmed The round trip: 551/551 payloads byte-identical. Gibbed PackFileTable agrees (offsets and TotalSize from the entries, nested packs closed before their parent, 16 bytes of padding after each).
ZIP As in ZIP layout, with CRC and sizes worked out. Name, flags and attributes come from the original's records when given; without one, the defaults are the values in those tables, which are every file's values. The game reads only the local header's method, sizes and name and extra lengths (How the game reads it), so the rest is kept for a byte-identical rebuild, not because the game needs it. confirmed The round trip; the scan (the defaults). What the game reads: 0x1401a2a00.
DEFLATE Raw DEFLATE (no zlib header) made by zlib's algorithm at level 9, memLevel 8, window 15, default strategy, through our own copy of ZLibDotNet. It reproduces the original stream of every RotXor + ZIP file but one. The other, Shaders/Shader.dat, was made by another tool (ZIP layout): no zlib setting gives back its stream (every level, memLevel, window and strategy tried). So when a payload is unchanged the rebuild keeps the original's stream as is, without compressing (since 2026-10-14, R32: compressing every unchanged payload again took 74% of an export); tor compression is what checks our compressor gives the original streams back. An edited payload is always compressed as above; the game needs only a valid stream (How the game reads it). confirmed The full round trip, 2026-10-11 (R28): every RotXor + ZIP file whose payload rebuilt is byte-identical; Shaders/Shader.dat only through its kept stream. tor compression local/game/Data, 2026-10-11: every stream but Shaders/Shader.dat's (27,999 bytes against its 27,414). The zlib settings: session scratchpad r28/zparams.cs. Gibbed PackFileTable also compresses at level 9.
RotXor Obfuscation, the deobfuscation steps in reverse. confirmed The round trip. Gibbed DataBogoCrypt.Encrypt and Gibbed.Reborn.EncryptData agree.
Whole file ZIP the payload, then obfuscate with RotXor; or the file's other wrapping (Files that are not RotXor + ZIP). With the original's ZIP record fields kept, this gives back every file in Data/ whose payload rebuilds, byte for byte. confirmed The full round trip, 2026-10-11 (R28): 34,224 of 34,227 files byte-identical; the other 3 are LocalizeText_de-de, _es-es and _fr-fr, whose text entries don't yet encode back (R19).

Not yet tested: whether the game loads a rebuilt file, which also needs a matching FileTable.bin. Static analysis says it should, as long as the local header and the DEFLATE stream are valid (How the game reads it).

Leads