Building Mods
pack-mod compiles edited .uo files into per-export override blobs. It never rewrites a .upk —
the output is a set of replacement payloads for a loader to serve at runtime.
Workflow
# 1. extract
ue3-tools --game-root /path/to/game extract Game/CookedPCConsole/Interface.upk
# 2. edit output/Interface/**/*.uo, or swap a .dds / .gfx sidecar
# 3. pack
ue3-tools --game-root /path/to/game pack-mod output/Interface --out mymod/
Output:
mymod/
└── Interface/
├── UI_Menu.Backgrounds.MainMenu.bin
├── UI_Menu.Movies.hud.bin
├── Interface.namemap
└── Interface.newexports ← only when new objects were referenced
Output contract
<Dotted.Export.Path>.bin
The complete replacement serialized bytes for one export:
i32 net index if p_ver >= 684
... tagged property tags, terminated by "None"
... the native tail
The filename is the export's dotted outer chain — the same key a loader matches against when the game preloads that object.
<package>.namemap
The package's name table, one entry per line, in index order — the original entries unchanged
followed by any the packer had to append. A loader must apply this before deserializing the .bin,
because the property tags inside reference names by index.
The namemap is not optional
Even a purely numeric edit can grow the table — a new enum label, a new FName value, or a
property tag that wasn't in the original all append entries. Always ship the .namemap
alongside the .bin files from the same run.
<package>.newexports
Written only when the packer had to invent export slots. Tab-separated, with a header:
# ue3-tools newexports v1
# index path class class_index outer_index super_index archetype object_flags export_flags serial_size bin
2317 UI_Menu.Backgrounds.NewImage Texture2D -42 310 0 0 0x0000000000000001 0x00000000 4096 UI_Menu.Backgrounds.NewImage.bin
Rows whose class is ? are objects that were referenced by something you edited but never
defined by a .uo — the packer warns about each one. Either add a .uo defining it, or fix the
reference.
How merging works
The packer starts from the decoded original and applies only what changed:
| Situation | Result |
|---|---|
| Property in both | Overlaid value-by-value; the original's type is authoritative |
Property set to None |
Dropped from the output |
Property only in the .uo |
Created from the schema — requires --game-root |
| Struct field omitted from the original | Created from the schema when you set it |
| Property only in the original | Kept unchanged |
@native(...) block |
Skipped; the original native tail bytes are reused |
@sidecar listed |
Re-injected by the class's serializer |
Types are never coerced. Writing a string where an int was is an error naming the field:
FAIL output/Interface/UI_Menu/Backgrounds/MainMenu.uo — SizeX: type mismatch: original is Int but edit is string
Adding new objects
If a .uo's path matches no existing export, the packer creates one:
- Find an existing export of the same class in the package to mirror class index, object flags, export flags and archetype from.
- Reserve the next free export index.
- Register the leaf name in the
.namemap. - Resolve the outer from the dotted path if possible, else inherit the template's.
- Write the
.binand declare the slot in.newexports.
Object references to objects that don't exist yet get slots reserved the same way, so you can point at a new object from an edited one in the same run.
Requires loader support
New exports only work if the loader can extend the export table at runtime. If it can only replace existing exports, restrict edits to objects that already exist.
Practical notes
- Extract with the same
--game-rootyou pack with. The schema must agree, and a missing package turns a decoded array into@bytes(...)— which then round-trips as opaque bytes rather than as the value you wanted to edit. - Declaration files are skipped.
class,struct,enum,constandvar.uofiles are reference material; packing ignores them silently. The summary line reports how many. - Pack the smallest set you can. Point
pack-modat just the subtree you edited. Every.uoit finds is repacked, whether you touched it or not. - Check the summary. It ends with counts of overrides written, failures, and skipped definitions. A non-zero failure count means those objects were left out entirely.
Consuming the output
The .bin + .namemap layout is what
CU3ML reads: it indexes each mod's overrides, hooks the
engine's Preload, and substitutes the mod's serialized bytes — remapped through the .namemap —
before the object deserializes. The original package on disk is never modified.