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 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 extract only

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

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.

Compressed TFC payloads are skipped

A mip stored in a .tfc with a compression flag set isn't inflated — the mip is marked missing and left out of the .dds. Uncompressed TFC payloads work.

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. 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 other two, 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.

The magic is checked against GFX, CFX, CWS, FWS, ZWS. A mismatch is a warning, not a failure.

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.

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

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.

Sound is extract-only

SoundNodeWave has no injector. Replacing audio requires editing the native tail by hand.

Font generation

create-font builds a UE3 Font 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.

The character set defaults to printable ASCII plus Ukrainian Cyrillic; override it with --chars. --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.