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.

  • StaticMesh exports its geometry.
  • SkeletalMesh exports 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 a Font. --type 2 generates a MultiFont, with one set of pages per target resolution from --resolutions (default 480,720,1080).
  • Character set. --chars and --unicode-range (hex ranges such as 0020-007E,0400-04FF) are merged. If neither yields any characters, the default is printable ASCII, Ukrainian Cyrillic and —«»©™. Codepoints above U+FFFF are ignored.
  • Remapping. With --remapped omitted, remapping turns on automatically when any codepoint is >= 0x80. A remapped font packs glyphs densely and carries a CharRemap table. 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:

  1. Empty tail → nothing emitted.
  2. With a schema — decode the tail as a positional walk of the class's CPF_Native properties (see Property Decoding).
  3. 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.