File formats and PiP internals
How the games’ files are put together. This is about the files, not the tool: for how Seam Ripper edits them see How Seam Ripper works.
Container formats
.lu containers are not Lua bytecode. They are big-endian resource
containers: a header, a record table, and a data image stored raw or as
XMemCompress LZX pools. Script records hold Lua 5.1 bytecode compiled
big-endian for the 360.
Record layout. Each record starts at the first multiple of its own
alignment after the previous one ends, and the gap is filled with 0xBF. The
alignment is in the top byte of the record’s flags as a power of two (0x4 =
16 for scripts and most records, 0xb = 2048, 0xc = 4096 for textures and
mesh buffers). The engine relies on this, so a container with any other gap
opens fine in every tool but crashes the game at level setup. Seam Ripper
re-lays out every record after an edited one by this rule, and doing that to
unchanged records reproduces the retail files exactly.
Textures carry size, mip count and a Xenos format dword (low 6 bits:
0x12 DXT1, 0x13 DXT3, 0x14 DXT5). Pixel data is 16-bit byteswapped and
2D-tiled, with surfaces padded to 32x32 blocks.
Meshes have per-submesh descriptors pointing at 4 KB-aligned vertex and
index buffers. Vertices are big-endian with float3 positions and half-float
UVs; stride and UV offset are detected per submesh. Indices are 16-bit
triangle strips with 0xFFFF restart, or lists where the trailer says so.
Submeshes reference their textures by CRC32 hash pairs, which is how materials
get bound.
Panic in Paradise
PiP uses a new container format (LUH, magic 05 4C 55 48) alongside reused
NB1 containers. naughty_lu.py detects both, so every tool reads PiP files.
The LUH image is XMemCompress LZX in 1 MB segments. The 360’s XMemCompress
differs from CAB LZX in one place (no pad byte after odd-length uncompressed
blocks), and the decoder handles it.
pip_dump.py dumps a unit’s raw chunks with a manifest, textures as PNG,
skeleton reports, rigged character GLBs and prop models, the Lua source with
its original folder tree, .cu audio manifests, and the Scaleform UI movies
as .gfx (open them in JPEXS).
Text lives in per-language units (global.en_us.lu, …) as type
04d00013 string tables (UTF-16BE, keyed by CRC32). lu_strings.py extract
writes one HASH<TAB>text line per string, and apply rebuilds the container
from your edited file, including length changes. Rebuilt containers store
segments uncompressed (compressed size equal to uncompressed size), which the
engine accepts; files get bigger but load the same.
UI: pip_gfx.py pulls the Scaleform movies out of 04d00001 chunks as
.gfx, plus every texture they embed as PNG. The art is in the PNGs; the
.gfx files are mostly layout and ActionScript.
Scripts are plain Lua 5.1 source in 04b00000 chunks. Two rules decide
whether an edited script boots or hangs:
- It has to fit the original slot. Each chunk has a fixed span in the
image. Units like
global.luhang if a chunk grows past it and moves, even if its content is unchanged. The injector squeezes out whitespace to make an edit fit and refuses with a byte count if it still doesn’t. It also checks for unbalanced brackets and unterminated strings or comments first. Don’t shrink a file by stripping comments or reindenting: a reflowed data table can hang the game even though it parses. - Stay inside the game’s own data. Data tables have a fixed set of keys
and an expected range. Adding a key the game doesn’t use (like
Bonus = {rr=...}) hangs the loader, and so does a value far out of range: costumehpworks at 400, the game’s highest, but hangs at 30000. Check what the real data uses and stay inside it.