# UE3 packages (`*.upk`) Arkham City's 2,648 packages in `BmGame\CookedPCConsole` are Unreal Engine 3 packages, version 805, licensee 101. 2,647 are fully LZO-compressed, 2 are stored uncompressed, and `UnrealX.upk` (512 bytes) is not a readable package. ackit reads and patches them with `ackit/upk.py` and `ackit/lzo.py` (no external tools). Soundbanks inside them: `banks.md`. ## Summary (file start, little-endian) | field | notes | |-------|-------| | u32 tag `0x9E2A83C1`, u16 version 805, u16 licensee 101, i32 TotalHeaderSize | | | FString FolderName | i32 length incl. NUL, then bytes (`None`) | | 12 x i32 | PackageFlags, NameCount, NameOffset, ExportCount, ExportOffset, ImportCount, ImportOffset, DependsOffset, ImportExportGuidsOffset, ImportGuidsCount, ExportGuidsCount, ThumbnailTableOffset | | 16 bytes Guid | | | i32 N + N x {ExportCount, NameCount, NetObjectCount} | generations (N = 1) | | u32 EngineVersion 7748, u32 CookerVersion, u32 CompressionFlags | 2 = LZO, 0 = none | | i32 C + C x {i32 UncompressedOffset, UncompressedSize, CompressedOffset, CompressedSize} | chunk table | | u32 PackageSource, ... | not used by ackit | All offsets in the summary (names, exports, ...) refer to the **uncompressed** package. The uncompressed package = the summary without the chunk table, followed by the chunks' data at their UncompressedOffset (the first chunk starts right after the summary, at NameOffset). ## Name, import and export tables (uncompressed package, at NameOffset / ImportOffset / ExportOffset) - Name: FString (positive length: ANSI incl. NUL; negative: UTF-16) + u64 flags. - Import (28 bytes): ClassPackage (name index, number), ClassName (index, number), i32 OuterIndex, ObjectName (index, number). - Export: i32 ClassIndex, SuperIndex, OuterIndex, ObjectName (index, number), ArchetypeIndex, **one extra u32 (licensee 101)**, u64 ObjectFlags, i32 SerialSize, SerialOffset, u32 ExportFlags, i32 count + count x i32 (net objects), 16-byte guid, u32 package flags: 72 bytes when the net-object list is empty. Checked on every export of the game. A name with number n is shown as `NAME_` (e.g. the speech event `..._04`): such full names exist only in the export table. `Package.tables()` returns names, imports and exports. ## Compressed chunks Each chunk (at CompressedOffset): `u32 tag, u32 block size (0x20000), u32 compressed sum, u32 uncompressed sum`, then one `{u32 compressed size, u32 uncompressed size}` per block, then the blocks. Every block is an independent LZO1X stream. In all game packages the chunks follow each other without gaps, start right after the summary and end at the end of the file (checked for every package). ## How ackit reads and writes them - Positions of sounds are offsets in the uncompressed package, the same as in a decompressed copy (e.g. Gildor's decompress.exe output). Reading decompresses only the blocks that are needed. - Writing (`Package.patched`): every changed block is stored again as a single LZO literal run (a valid LZO1X stream without matches, `lzo.compress_stored`), the chunk headers of the changed chunks and the CompressedOffset/CompressedSize of all following chunks are updated; every other byte is copied, so a package without changes is rebuilt byte-for-byte. Replacements never change the uncompressed layout (sounds are replaced in place), so names, exports and their offsets stay valid.