Asset Payloads
Classes whose data lives outside the tagged property stream get a dedicated serializer. Each one reads the native tail, optionally writes a sidecar file, and (where it can) knows how to inject an edited sidecar back on the way in.
| Class | Sidecar | Round-trip |
|---|---|---|
Texture2D |
.dds |
yes |
SwfMovie, GFxMovieInfo |
.gfx |
yes |
SoundNodeWave |
.ogg / .wav / .mp3 / .xma (+ .raw.*, .xbox360.*, .ps3.*) |
yes |
StaticMesh, SkeletalMesh |
.glb |
extract only |
Font, MultiFont |
none (native block in the .uo) |
yes |
Material, MaterialInstanceConstant, MaterialInstanceTimeVarying |
none | tail preserved |
any other class with a RawData property |
.RawData.<ext> |
yes |
Serializers are resolved through the class chain, so one registered for a base class also covers its subclasses.
Bulk data
Everything below is built on UE3's bulk-data header:
u32 flags
i32 element count
i32 size on disk
i32 offset in file
... payload — inline only if BULKDATA_StoreInSeparateFile (0x1) is clear
| Flag | Meaning |
|---|---|
0x01 |
payload lives in a separate file (a .tfc) |
0x02 |
zlib compressed |
0x04 |
force single element serialization |
0x08 |
single use |
0x10 |
LZO compressed |
0x40 |
store only payload |
0x80 |
LZX compressed |
When a payload is written inline, the header's offset field has to point at the payload's absolute position in the package. The packer patches it after serialization, using the original export's serial offset as the base (see Building Mods).
Texture2D
The tail is: a source-art bulk block, then an indirect mip array, then a TFC GUID
(p_ver ≥ 567), then cached PVRTC mips (p_ver ≥ 674), then any trailing bytes.
Each mip is a bulk block plus its sizeX / sizeY. A mip is classified as:
| Source | When |
|---|---|
inline |
payload was stored in the export |
tfc('<stem>') |
separate-file flag set and a size on disk — resolved from the TextureFileCacheName property against the .tfc index |
missing |
neither, or the TFC couldn't be found |
The pixel format comes from the Format property (PF_DXT1, PF_DXT3, PF_DXT5, PF_A8R8G8B8,
PF_G8, PF_BC5, PF_FloatRGBA). BC5 and FloatRGBA are written with a DX10 extension header;
the rest use classic FourCC or RGB/luminance masks. Mips are sorted largest-first and written as one
.dds.
Only LZO-compressed TFC payloads can be inflated
A mip stored in a .tfc with a compression flag set is inflated through the shared chunk
decompressor. That handles LZO. A zlib- or LZX-compressed TFC payload reaches the
unimplemented branch of the decompressor and aborts the extraction.
Injecting a texture
On repack, the .dds is decoded and matched to the original mips by dimensions. Each matched
mip is rewritten inline: payload replaced, the separate-file and compression flags cleared, sizes
updated, offset zeroed (it is patched to the final absolute position when the .bin is
serialized). The TFC GUID is then zeroed so the game stops looking in the cache file.
To change a texture's resolution, update SizeX and SizeY in the .uo to match your DDS's top
mip — that authorises a full mip-list replacement. Without it, a DDS whose top mip matches no
original mip is an error, deliberately.
SwfMovie / GFxMovieInfo
Unlike the textures, the payload isn't in the native tail at all — it's the RawData tagged
property. Extraction pulls the bytes out (either a decoded byte array, or a raw blob with its
4-byte count prefix stripped) and writes <name>.gfx, marking the property @sidecar in the
.uo.
On extraction the magic is checked against GFX, CWS, FWS and ZWS; a mismatch is a warning,
not a failure. Injection also accepts CFX, and takes either a .gfx or a .swf file.
Injecting writes the bytes back into RawData, stripping a stale count prefix if present. If the
property doesn't exist on the export, a new ArrayProperty tag is created for it.
Other classes with a RawData property
SwfMovie isn't the only class that keeps an opaque byte array in RawData. For any class
without a more specific serializer, the RawData property is written to
<name>.RawData.<ext> and replaced in the .uo with a @sidecar reference. The extension is
sniffed from the content:
| Sniff | Extension |
|---|---|
GFX / CFX |
.gfx |
FWS / CWS / ZWS |
.swf |
DDS |
.dds |
| PNG signature | .png |
| JPEG signature | .jpg |
OggS |
.ogg |
RIFF |
.wav |
FXA |
.fxa |
ZIP signature (PK\x03\x04) |
.zip |
| gzip signature | .gz |
| anything else | .bin |
The same file is injected back into RawData on pack.
SoundNodeWave
The tail is four consecutive bulk blocks: raw PCM, compressed PC, compressed Xbox 360, compressed
PS3. Metadata (NumChannels, SampleRate, Duration, ChannelOffsets, ChannelSizes) comes from
the tagged properties.
Each block's contents are sniffed to pick an extension:
| Sniff | Extension |
|---|---|
OggS |
.ogg |
RIFF/WAVE |
.wav (or .xma if an XMA chunk is present in the first 64 bytes) |
ID3 or an MPEG frame sync |
.mp3 |
| anything else | .bin |
The sidecar names are <name>.<ext> for the PC block, <name>.raw.wav for raw PCM,
<name>.xbox360.<ext> and <name>.ps3.<ext>. Raw PCM is wrapped in a 16-bit WAV header using the
sample rate and channel count from the properties. If SampleRate is zero it's written as
headerless .raw.pcm instead.
Injecting audio
Drop a replacement next to the .uo. Which of the four blocks it replaces is decided by its
filename: .raw. replaces raw PCM, .xbox360. and .ps3. replace those blocks, and anything else
replaces the PC block. The PC block is expected to be Ogg; a payload that doesn't start with
OggS is injected anyway, with a warning.
After injection the sound's properties are synced from the new payload: NumChannels,
SampleRate, Duration, RawPCMDataSize and an estimated CompressionQuality. The bulk-data
offsets are patched to absolute positions when the .bin is serialized.
StaticMesh / SkeletalMesh
Both are written as a glTF binary (<name>.glb) so they can be opened in standard viewers.
Extraction logs the vertex count and LOD count.
StaticMeshexports its geometry.SkeletalMeshexports LOD 0 only, and is skinned when the mesh has bones. A mesh whose LOD 0 has no vertices or indices is skipped with a warning.
There is no injector: edits to the .glb are not packed back. Treat the file as a viewing and
conversion aid.
Material, MaterialInstanceConstant, MaterialInstanceTimeVarying
All three share one serializer. It writes no sidecar. The properties decode into the .uo as
normal and the native tail is preserved unchanged, so the object round-trips safely but its native
data cannot be edited.
Font and MultiFont
Both classes share a serializer. The native tail holds the character remap table (CharRemap), a
list of u16 pairs. In the .uo it appears in the @native block as an object of
{ key = value, ... } entries; each value must be a numeric literal (decimal or 0x hex).
That block is read back on pack, so you can edit the table by hand.
Below package version 511 the font uses the older layout, and the tail is rebuilt from the properties on injection.
Font generation
create-font builds a UE3 Font or MultiFont from a TrueType file rather than extracting one.
Glyphs are rasterized at size × dpi / 72 into pages of --tex-width × --tex-height, packed
row by row with the configured padding, overflowing into additional pages as needed. Each page
becomes a Texture2D .uo + .dds pair; the font itself becomes a .uo carrying the character
map, per-glyph UV cells, page indices, vertical offsets, em scale, ascent and descent.
- Class.
--type 1(default) generates aFont.--type 2generates aMultiFont, with one set of pages per target resolution from--resolutions(default480,720,1080). - Character set.
--charsand--unicode-range(hex ranges such as0020-007E,0400-04FF) are merged. If neither yields any characters, the default is printable ASCII, Ukrainian Cyrillic and—«»©™. Codepoints aboveU+FFFFare ignored. - Remapping. With
--remappedomitted, remapping turns on automatically when any codepoint is>= 0x80. A remapped font packs glyphs densely and carries aCharRemaptable. A non-remapped font needs one slot per codepoint up to the highest, so a sparse high range makes a large, mostly empty array; a warning is printed above 4096 slots.
--game-root is required so the target export path can be resolved and the generated properties
typed against the real Font and Texture2D classes.
Fallbacks
If a class has no registered serializer:
- Empty tail → nothing emitted.
- With a schema — decode the tail as a positional walk of the class's
CPF_Nativeproperties (see Property Decoding). - Otherwise — preserve the tail verbatim and render it as
@native(Raw)with a byte count and a 64-byte hex preview.
The raw path is lossless: the packer reuses those bytes untouched, so an object it doesn't understand can still be round-tripped safely.