Read and patch UE3 packages and soundbanks

This commit is contained in:
2026-10-09 19:38:57 +02:00
parent b3c9b84dc7
commit 61bbf26f3b
10 changed files with 734 additions and 11 deletions

38
docs/formats/upk.md Normal file
View File

@@ -0,0 +1,38 @@
# UE3 packages (`*.upk`)
Arkham City's 2,650 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).
## 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.