1. RPF: RAGE Package File
Container format for basically everything. Proprietary archive with an encrypted TOC.
Versions
| Version | Magic | Game |
|---|---|---|
| RPF0 | 0x52504630 |
Rockstar Games Presents Table Tennis |
| RPF2 | 0x52504632 |
GTA IV |
| RPF3 | 0x52504633 |
GTA IV Audio, Midnight Club: Los Angeles |
| RPF4 | 0x52504634 |
Max Payne 3 |
| RPF6 | 0x52504636 |
Red Dead Redemption |
| RPF7 | 0x52504637 |
GTA V / GTA Online / FiveM |
| RPF8 | 0x52504638 |
Red Dead Redemption 2 |
RPF1 and RPF5 don't exist publicly.
RPF7 header: 16 bytes
u32 Version 0x52504637 ("RPF7")
u32 EntryCount
u32 NamesLength NOT a plain length, see below:
bits 0-27 name table size in bytes
bits 28-30 3-bit field, meaning unknown
bit 31 flag, meaning unknown
u32 Encryption see below
Then EntryCount * 16 bytes of TOC entries, then the name table, then file data.
The third dword is a bitfield, not a length. Every tool reads it as a flat u32, and that works on retail archives because the top four bits are zero in all of them. The engine does not: it masks the value with 0xFFFFFFF to get the length, then pulls a 3-bit field out of bits 28-30 and a boolean out of bit 31 and stores both. Read from the header validator in a decrypted GTA5.exe, so this is what the game does, though what those four bits mean is not established.
Encryption types
| Name | Value | Notes |
|---|---|---|
NONE |
0 |
|
OPEN |
0x4E45504F |
ASCII "OPEN". OpenIV-style, unencrypted TOC |
AES |
0x0FFFFFF9 |
AES-256, fixed PC key |
NG |
0x0FEFFFFF |
Per-file keyed by entry name + size |
Do not copy
0x04E45504Ffrom CodeWalker's inline comment. That comment has a stray leading zero (nine hex digits). The real value is0x4E45504F= 1313165391. The bug is in the comment only; the enum is correct.
Crypto specifics (GTA V PC):
- Rijndael, KeySize 256, BlockSize 128,
CipherMode.ECB,PaddingMode.None. - One pass.
GTACrypto.DecryptAESData(data, key, rounds = 1). The 16-round figure you'll see quoted is GTA IV, documented on the gtamods Cryptography page. Don't apply it to V. - Trailing bytes past the last whole 16-byte block are left in plaintext (
length = data.Length - data.Length % 16). - TOC and name table are decrypted together as two blobs.
File data IS encrypted
Each file entry carries its own EncryptionType field (0 = plaintext, 1 = encrypted; anything else throws). When set, CodeWalker decrypts the data with DecryptAES(data) or DecryptNG(data, entry.Name, entry.FileSize) before inflating.
This is the entire reason NG keying takes the filename and length as inputs. File data is encrypted, not left in plaintext.
Compression, when present, is raw deflate with no zlib header.
Entry layouts (16 bytes each)
Directory entry
u32 NameOffset
u32 Ident must be 0x7FFFFF00, else throw
u32 EntriesIndex
u32 EntriesCount
Binary file entry
u16 NameOffset
u24 FileSize compressed size; 0 means uncompressed
u24 FileOffset in 512-byte units
u32 FileUncompressedSize
u32 EncryptionType 0 or 1
Resource file entry
u16 NameOffset
u24 FileSize 0xFFFFFF is a sentinel - see below
u24 FileOffset in 512-byte units, masked & 0x7FFFFF
u32 SystemFlags page layout, system/CPU segment
u32 GraphicsFlags page layout, graphics/GPU segment
Entry type is discriminated by reading the second u32 (x) before parsing:
x == 0x7fffff00→ directory(x & 0x80000000) == 0→ binary file- else → resource file
FileSize == 0xFFFFFF on a resource entry means "size ≥ 0xFFFFFF". Real size is reassembled from bytes at the data offset in a scrambled order: buf[7] | buf[14]<<8 | buf[5]<<16 | buf[2]<<24.
Size limits: where the 4 GB ceiling comes from
Offsets are stored in 512-byte units in a 24-bit field, but only 23 of those bits are usable, for both entry types. The reason is the entry-type tag. Lay the first 8 bytes of any entry out as one little-endian u64:
bits 0-15 NameOffset
bits 16-39 FileSize
bits 40-63 FileOffset <-- bit 63 is the top bit of this field
second u32 of the entry = bits 32-63,
so (x & 0x80000000) tests bit 63 = FileOffset's most significant bit
That is the same bit the reader branches on to tell a binary entry from a resource entry (see the discrimination rule above). A binary entry has to leave it clear or it parses as a resource entry; a resource entry has to have it set. Either way the offset itself only gets 23 bits:
| Entry type | Tag bit | Usable offset bits | Max addressable |
|---|---|---|---|
| Binary | must be 0 |
23 | 0x7FFFFF × 512 = 4.0 GB |
| Resource | must be 1 |
23 (& 0x7FFFFF) |
0x7FFFFF × 512 = 4.0 GB |
So the "4 GB RPF limit" is real, and it is a property of the 16-byte entry layout rather than of any particular tool. CodeWalker's & 0x7FFFFF on resource entries is correct parsing of a tagged field, not a conservative choice you can lift. The 16 bytes are fully spoken for in both layouts, so there is no spare bit to widen the field with either.
Rockstar's own archives agree. Measured across a current retail install, not one RPF crosses 4 GB. The largest is update/x64/dlcpacks/mpbattle/dlc.rpf at 3,981,039,616 bytes, which is 92.7% of the ceiling before it spills into a second archive. Where a pack needed more, it was split rather than extended: mpheist4 ships three archives, dlc.rpf (3.22 GiB), dlc1.rpf (3.23 GiB) and dlc2.rpf (1.26 GiB), for 7.71 GiB of content that never becomes one archive.
The ceiling is per archive, not per project. Offsets are relative to each archive's own start, so nested and sibling RPFs each get their own budget and the total is unbounded. Splitting is not a workaround either: it is a declared feature of the pack format, through subPackCount in setup2.xml (see §4). That is what the base game does, and it needs no patched executable.
What an over-size archive looks like
A measured specimen: a single dlc.rpf at 9,676,967,424 bytes, 2.25x the 4.0 GB ceiling, built by a packer (identified as grzyClothTool) that writes one archive with subPackCount hardcoded to 0 and no size guard. The header parses as valid RPF7 with no error, so the file looks fine until something inside it is missing.
What to check for:
- The header opens cleanly. An over-size RPF does not fail to parse, it parses and mounts a fraction of its content.
- Some
.rpfor.metaentries that should be binary read as resource entries instead, because the offset written for them set bit 23 of the 24-bit offset field, the same bit that tags entry type. In the measured specimen this hit four entries, all placed past the 4 GB mark. - Nested archives named by those mistyped entries never mount, and their content is simply absent at runtime with no error.
- Reported sizes for those entries are nonsense, because the last eight bytes of the entry are read as page-layout flags instead of size and encryption fields.
- A parser may resolve some but not all of the nested archives named in the table. In the measured specimen, 2 of 5 resolved.
| Measured | Value |
|---|---|
| Archive size | 9,676,967,424 bytes |
| Ceiling | 4,294,966,784 bytes |
| Mistyped entries | 4 |
| Nested archives resolved | 2 of 5 |
The remedy is not a table repair. No 16-byte entry can address past 0x7FFFFF blocks, so content placed beyond 4 GB is unaddressable regardless of how the entry bytes are edited. The archive has to be rebuilt split across sub-packs (dlc.rpf, dlc1.rpf, dlc2.rpf, and so on), each within its own 4 GB budget, using subPackCount as described above.
Why packers produce this silently
If you write tools on CodeWalker, this part matters. RpfBinaryFileEntry.Write emits the offset as three bytes with no range check:
var buf2 = new byte[] {
(byte)((FileOffset >> 0) & 0xFF),
(byte)((FileOffset >> 8) & 0xFF),
(byte)((FileOffset >> 16) & 0xFF) // no mask to 0x7F, no bounds check
};
Any FileOffset of 0x800000 blocks or more, meaning any file placed past 4 GB, sets bit 7 of that third byte. That bit is the entry-type tag, so the entry converts itself from binary to resource on the way out. RpfResourceFileEntry.Write sets the same bit deliberately with | 0x80, which is correct for a resource, and it clamps FileSize to 0xFFFFFF but likewise never clamps the offset.
So a packer with no size ceiling of its own inherits a writer that turns an out-of-range offset into a silent type change rather than an error. That is the whole mechanism. A tool built on this should either refuse to cross the boundary or shard, and should treat an offset at or past 0x800000 blocks as a bug rather than something to encode.
🟡 Raising the ceiling would mean changing what the bits mean, so an archive built that way could not be read by the game's own loader. It would need an ASI patch on the RPF reader, in the same territory OpenIV and OpenRPF already occupy, and the resulting archives would only load for people running that patch. The tractable variant is not re-tagging entries but widening the unit: the same 23 bits addressing 2048-byte blocks would reach 16 GB, at the cost of some alignment padding.
One part of that is no longer speculative. The 512 is an immediate baked into the instruction stream — the engine computes a byte offset as shr rax, 0x28 then shl eax, 9, with no header field behind the 9. There is nowhere in the format to declare a different block size, so a widened-unit archive could not signal itself to a stock loader even in principle; the block size would have to be patched in, at every site, alongside everything else.
Scanning the decrypted image for an offset extraction paired with that 23-bit mask finds 11 such decoders, across two clusters, three of which also carry the tag test. So there is no single place to patch: a widened-unit build would have to change every one of them consistently, and any decoder left behind would mount an archive and then read from the wrong place. That is the same silent failure the ceiling already produces. Nobody has shipped this, and splitting costs nothing. (Some of the 11 may be inlined copies of one routine rather than separate paths; distinguishing them needs control-flow analysis that has not been done.)
Confirmed against the engine itself. The bit-level reading above was originally inference from CodeWalker plus the retail archives. It has since been checked against a decrypted memory image of a running GTA5.exe, and the engine does exactly this. The header validator compares the first dword against 0x52504637 and sizes the entry table with shl edi, 4, confirming 16-byte entries. The entry decoder then loads the first eight bytes as one u64 and does:
mov r9d, 0x7fffff ; the engine's own 23-bit mask
shr rax, 0x28 ; >> 40, so FileOffset occupies bits 40-63
and rax, r9 ; 23 bits, not 24
cmp rax, r9 ; an all-ones offset is a sentinel
shr r8, 0x3f ; >> 63, the top bit of FileOffset
test r8b, 1 ; set = resource entry, clear = binary entry
The literal 0x7FFFFF is loaded by the game, in at least two independent decoders. So the ceiling is the engine's own arithmetic and CodeWalker's mask mirrors it exactly. A second decoder also shows the size split: a resource entry takes its size from bits 16-39 (shr 0x10, and 0xFFFFFF) while a binary entry reads it from the dword at entry + 8.
Two details fall out that are worth knowing. An offset of all ones is checked as a sentinel, separately from the range, so it is not simply a large offset. And the header's third dword, usually read as a flat NamesLength, is really 28 bits of length plus four bits of flags: the engine masks it with 0xFFFFFFF and pulls a 3-bit field from bits 28-30 and a boolean from bit 31. Reading it as a plain u32 works on retail archives only because those bits are zero there.
The ceiling is enforced twice, and the second one is invisible
A second pass over the same image (2026-08-15) found where the offset becomes a byte address. Five sites in the packfile code do this, and the read path is the last of them:
shr rax, 0x28 ; FileOffset into the low 24 bits of rax
shl eax, 9 ; * 512 <- note the 32-bit operand
add r8, rax ; archive base + entry offset
add r8, rdi ; + offset within the file
call qword ptr [r10 + 0x38] ; device read
shr rax, 0x28 leaves 24 bits: the 23-bit offset plus the entry-type tag sitting in bit 23. shl eax, 9 is a 32-bit operation, so bit 23 shifts into bit 32 and is discarded. Four of the five sites therefore do not mask at all, and do not need to — the largest byte offset any of them can produce is 0x7FFFFF << 9 = 0xFFFFFE00 = 4,294,966,784, the ceiling exactly.
This matters if you are writing a tool. An out-of-range offset does not overflow into a wrong-but-large address at those sites, it vanishes: the extra bit is silently dropped and the read lands somewhere plausible. That is why an over-ceiling archive mounts without complaint and then serves wrong bytes, and it is a second, independent reason the limit cannot be lifted by editing entries.
The 3-bit header field is the name-offset shift
The unexplained 3-bit field in the header's third dword has a job: it is the left-shift applied to every entry's 16-bit NameOffset. Six sites read it and all six do the same thing:
mov rax, qword ptr [this + 0x20] ; entry table
mov cl, byte ptr [this + 0xB8] ; the 3-bit field from the header
movzx r8d, word ptr [rax + idx*16] ; entry + 0, the 16-bit NameOffset
shl r8, cl ; scale it
add r8, qword ptr [this + 0x10] ; + the name heap base
A 16-bit name offset can only address 64 KB of names. The shift is how RAGE scales past that: with shift n, names sit at n-aligned positions and the heap reaches 65535 << n, which is what the 28-bit length field is sized for. Every retail archive carries 0 here, which is why reading the dword flat has always worked and why the field went unnoticed.
If you write archives, this is the same trap as the offset one wearing a different hat: CodeWalker never sets this field and never checks the name heap against 64 KB, so a pack with enough long paths would emit aliased names with no error. The boolean in bit 31 is parsed and stored by the engine but nothing in the packfile code reads it; its meaning is still unknown.
Error codes. ERR_GEN_ZLIB_2 is real and is a decompression failure — RAGE raises it from zlibInflater::InflateBegin whenever inflate() returns a negative code, with the stream name in the message (blanked in retail builds, which is why players see the code with no file attached). It is not RPF-specific, but on a modded install nearly everything inflated is an RPF entry, so "corrupt or badly compressed entry" lands in the right place.
The link from "oversized RPF" to ERR_FIL_PACK_3 does not hold up, because it requires the loader to notice the over-size condition and nothing does: there is no range check in any of the eleven decoders. An over-ceiling archive mounts, mistypes its entries and reads from the wrong place, so the failure surfaces downstream — as a resource whose pointers do not resolve (ERR_SYS_INVALIDRESOURCE_5, "Invalid fixup, address is neither virtual nor physical") or as garbage in the inflater (ERR_GEN_ZLIB_2). Expect one of those, not a packfile error. The packfile codes that do exist in RAGE are ERR_STR_PACK_1 and ERR_STR_PACK_2, and both are missing-file conditions.
Other RPF7 notes
- RPFs nest.
update.rpfcontainsx64/dlcpacks/…containing more.rpfs. Each level costs seek time. - Fragmentation is real. OpenIV Edit mode → right-click RPF → Defragment after heavy edits.
Base game archive layout
Grand Theft Auto V/
├─ common.rpf # data/, shaders, gameconfig
├─ x64a.rpf … x64w.rpf # models, textures, audio, levels
├─ update/
│ ├─ update.rpf # overrides base; common/data lives here
│ ├─ update2.rpf # newer patches
│ └─ x64/dlcpacks/ # where DLC packs mount
├─ x64/audio/sfx/*.rpf
└─ mods/ # OpenIV / OpenRPF mods mirror (SP only)
Load order: base x64*.rpf → update.rpf → update2.rpf → each DLC in dlclist.xml order. Last one wins.
2. Gen8 vs Gen9 (Legacy vs Enhanced)
Read this before anything else in the doc if you're on Enhanced.
GTA V now ships in two flavours: Legacy (Gen8, GTA5.exe) and Enhanced (Gen9, GTA5_Enhanced.exe). They are not mod-compatible.
| Legacy (Gen8) | Enhanced (Gen9) | |
|---|---|---|
| Mods loader | OpenIV.asi |
OpenRPF.asi (OpenIV.asi is obsolete here) |
| ASI loader | dinput8.dll (OpenIV) |
dsound.dll (bundled with OpenRPF) |
| Resource files | Gen8 versions | Gen9 versions |
rpf.cache |
n/a | OpenRPF bypasses it for mods-folder files (automatic since v0.2) |
Gen8 files loaded on Gen9 crash the game. The only sanctioned conversion path is dexyfex's converter inside CodeWalker, and it's imperfect: some resources still convert badly and produce crashes or visual anomalies. Report those on the CodeWalker Discord.
OpenRPF supports both native encrypted archives and OpenIV-style OPEN archives, so you don't have to re-encrypt. Latest known: v0.3, March 2026, compatible with Enhanced builds 1013.17+ (the "A Safehouse in the Hills" GTA Online update). Team: Transmet (OpenRPF.asi), GiZz (dsound.dll), Antasurris.
ScriptHookV has an Enhanced build (v3442.0/812.8, released March 2025). OIV packages install to either flavour via the CodeWalker-core-based OIV Package Installer, which validates game version against the package.
CodeWalker's ResourceBuilder.Build() takes a gen9 bool. Everything in §3's version table is Gen8.