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:

  1. Find an existing export of the same class in the package to mirror class index, object flags, export flags and archetype from.
  2. Reserve the next free export index.
  3. Register the leaf name in the .namemap.
  4. Resolve the outer from the dotted path if possible, else inherit the template's.
  5. Write the .bin and 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-root you 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, const and var .uo files are reference material; packing ignores them silently. The summary line reports how many.
  • Pack the smallest set you can. Point pack-mod at just the subtree you edited. Every .uo it 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.