Package Format

The on-disk layout as implemented. Little-endian unless the package is a byte-swapped console build. Every optional field is gated on the package version p_ver against a named constant in versions.rs.

Field Type Condition
signature u32 must be 0x9E2A83C1
package version i16
licensee version i16
header size i32
folder name i32 len + bytes negative length ⇒ UTF-16, byte count is len × -2
package flags u32
name count / offset i32 ×2
export count / offset i32 ×2
import count / offset i32 ×2
depends offset i32 p_ver ≥ 415
import/export GUID offset + counts i32, u32, u32 p_ver ≥ 623
thumbnail table offset u32 p_ver ≥ 584
GUID i32 ×4
generations i32 count + {export, name, netobj} netobj only if p_ver ≥ 322
engine version i32 p_ver ≥ 245
cooker version i32 p_ver ≥ 277
compression method + chunk count + chunks u32, u32, chunk[] p_ver ≥ 334
package source i32 p_ver ≥ 334
additional packages to cook i32 + FString[] p_ver ≥ 516
texture allocations FTextureAllocations p_ver ≥ 767

A compressed chunk is four u32s — decompressed offset, decompressed size, compressed offset, compressed size. FTextureAllocations is a count-prefixed array of {sizeX, sizeY, mips, format, createFlags, exportIndices[]}.

The header can also be written, mirroring the same gates — used when flattening a compressed package into an uncompressed image.

Package flags

Bit Name Bit Name
0x1 AllowDownload 0x100000 DisallowLazyLoading
0x2 ClientOptional 0x200000 ContainsScript
0x4 ServerSideOnly 0x400000 ContainsDebugInfo
0x8 Cooked 0x800000 RequireImportsAlreadyLoaded
0x10 Unsecure 0x2000000 StoreCompressed
0x20 SavedWithNewerVersion 0x4000000 StoreFullyCompressed
0x8000 Need 0x10000000 ContainsFaceFxData
0x20000 ContainsMap 0x20000000 NoExportAllowed
0x40000 Trash 0x40000000 StrippedSource
0x80000000 FilterEditorOnly

Compression

Two levels of chunking.

Outer — the header's chunk table maps compressed regions into the decompressed image.

Inner — at each chunk's compressed offset:

u32  tag           PACKAGE_FILE_TAG, or byte-swapped on console builds
u32  block size    if it reads back as the tag itself, use the default 131072
u32  summary       (unused)
u32  total size    total uncompressed bytes in this chunk

then ceil(total / block) pairs of (compressed_size, decompressed_size), then the blocks back to back. If the tag matched only after a swap, every subsequent size word is swapped too.

LZO only

Zlib and LZX are recognised as compression-method values but decompressing them is not implemented. Run decompress on a package first if you want to check.

Name table

i32 length + characters + u64 flags. Positive length ⇒ ANSI, decoded as Latin-1 (one byte per char, not UTF-8). Negative ⇒ UTF-16 with -length code units. Trailing NUL stripped.

An FName reference is two i32s: table index and instance number. Instance 0 prints bare; instance n > 0 prints as Name_<n-1>.

Export table

Field Type Condition
class index i32 package index
super index i32
outer index i32
object name FName
archetype i32
object flags u64
serial size i32
serial offset i32 if size ≠ 0, or p_ver ≥ 249
component map i32 + (FName, i32)[] p_ver < 543
export flags u32 p_ver ≥ 247
generation net object counts i32 + i32[] p_ver ≥ 322
package GUID i32 ×4 p_ver ≥ 322
package flags u32 p_ver ≥ 543

Import table

Four fields, no version gates: class package FName, class name FName, outer index i32, object name FName.

Package indices

One i32 addresses both tables, the engine's own convention:

Value Means
n > 0 export n - 1
n < 0 import -n - 1
0 null / the package itself

Path names

The outer chain is walked and joined with ., except that : is used when the object being appended is not a Package and its outer is one — UE3's subobject notation.

Texture2D UI_Menu.Backgrounds.MainMenu
DistributionFloatUniform ParticleSystem.Emitter:Distribution_0

That maps to a filesystem path by replacing separators with / and appending the class as an extension on the leaf: UI_Menu/Backgrounds/MainMenu.Texture2D.

Export payload

Each export's serial range holds, in order:

  1. a net index (i32) if p_ver ≥ 684
  2. the tagged property stream, terminated by a property named None
  3. the class's native serialization — whatever the C++ Serialize override wrote

No boundary marker

There is no length field between (2) and (3). The split is found by parsing (2) until it terminates; everything left is (3). This is why property decoding is written to fail safely — a mis-parse silently moves bytes from one side to the other rather than erroring.

Not parsed

The depends table, the import/export GUID table, and the thumbnail table are located by the header but never read. Their offsets survive a header rewrite unchanged.