6.5 KiB
Building MPQ Patches for WoW 1.12.1
Reference for packaging files (addons, FrameXML overrides, art) into an MPQ patch archive that vanilla 1.12 picks up at boot. Captured because the MPQEditor docs are sparse, the script syntax is non-obvious from the GUI, and there's a 1.12 patch-MPQ naming constraint that's easy to miss.
When this matters
Useful when you want files to:
- Auto-load before user addons — engine reads MPQs in
<WoW>\Data\early in boot, alongside FrameXML. Anything inInterface\AddOns\<name>\inside an MPQ enumerates just like a filesystem-resident addon, but loads earlier than addons living inInterface\AddOns\on disk. - Be tamper-resistant — users can't accidentally disable an
MPQ-resident addon from the in-game addon list (it's not in the
filesystem
Interface\AddOns\they can browse to). - Override engine UI — drop
Interface\FrameXML\<name>.lua/Interface\FrameXML\<name>.xmlfiles to add or modify FrameXML. This is how the 3.3.5 reference for/dump,/framestack,/etracelives inChatFrame.lua— it could be replicated here via a smallInterface\FrameXML\AddedSlashCommands.xmlMPQ insert that callsUIParentLoadAddOnfor backported tools.
Confirmed empirically (1.12.1):
| Test | Result |
|---|---|
| Plain (non-Blizzard_-prefixed) addon in MPQ, auto-discovered | ✅ engine sees it via enum |
| Plain addon in MPQ, auto-loads on login | ✅ |
Blizzard_* addon in MPQ, no .toc.sig |
❌ engine refuses (signature check is name-based, not location-based) |
Blizzard_* addon on filesystem, no .toc.sig |
❌ same — refusal applies regardless of MPQ vs filesystem |
So MPQ residency is no help for Blizzard_* names — those need a
separate signature-bypass approach (see mpq-test/HANDOFF.md for the
hook target).
Tooling
We use Ladik's MPQ Editor (MPQEditor.exe), which ships with a
MoPaQ 2000 script interpreter. Run with /console <script> to execute
a build script. CLI form is also supported but the script form is more
useful for repeatable builds.
MPQEditor.exe /console build.txt
Get help on individual commands with help <command> inside a script
or interactive console (e.g., help add).
Filename constraint
Vanilla 1.12's MPQ loader only accepts patch archives matching the
single-character patch-X.mpq form. Names with multi-character
suffixes are silently rejected.
| Name | Loads? |
|---|---|
patch-Z.mpq |
✅ |
patch-3.mpq |
✅ (clashes with stock — avoid) |
patch-Z-myproject.mpq |
❌ |
patch-myproject.mpq |
❌ |
myproject.mpq |
❌ |
Use letters past Blizzard's stock range (Z, Y, X, W…) so your patch sorts after their MPQs and overrides take precedence on conflict. The exact ordering is alphabetical with later entries winning.
Script syntax
Build scripts are plain text, one command per line. Quote any path containing spaces. Important commands:
| Command | Form | Notes |
|---|---|---|
new |
new <mpq> [<maxFiles>] |
Create new archive. Default file limit is 0x1000; set higher if you'll add lots of files. |
open |
open <mpq> |
Open an existing archive for modification. |
add |
add <mpq> <sourceFile> <archivePath> [/options] |
First arg is the MPQ — NOT implicit from a prior open. Source can include wildcards; with /r it walks subdirectories preserving structure. |
extract |
extract <mpq> <mask> <destDir> [/fp] |
Extract files matching mask. /fp preserves full path. |
list |
list <mpq> |
Print archive contents. |
close |
close |
Close currently open archive (auto-runs at script end). |
add flags worth knowing:
/r— recurse subdirectories./auto— pick compression by file type. Use this for mixed text/binary trees./c— force compression./wave— add as WAVE file (sound-specific).
Working example
The script we used to build the test MPQ for empirical verification:
new C:\Git\ClassicAPI\mpq-test\patch-Z.mpq 64
add C:\Git\ClassicAPI\mpq-test\patch-Z.mpq "C:\Git\ClassicAPI\mpq-test\Interface\*" "Interface" /r /auto
close
What it does:
- Creates
patch-Z.mpqwith a 64-file capacity hint (small). - Walks
mpq-test\Interface\recursively (/r), adds every file under it to the archive at the matchingInterface\...path./autopicks LZ compression for.lua/.tocand stores binaries uncompressed. - Closes the archive.
Run via:
"C:\WoW\MPQEditor.exe" /console "C:\Git\ClassicAPI\mpq-test\build.txt"
The fixture lives in mpq-test/ for reference — it contains two stub
addons (ClassicAPITest_Plain, Blizzard_ClassicAPITest) used to
prove the enum-from-MPQ vs SIG-check claims in the table above.
Pitfalls we hit
-
addopens its first arg as an MPQ. If you forget the MPQ path and passadd <source> <target>, MPQEditor tries to open<source>as an MPQ archive and fails with "Failed to open the MPQ ... An attempt was made to load a program with an incorrect format." Always:add <mpq> <source> <target>. -
newdoesn't keep the MPQ "open" for subsequent commands. Eachaddre-opens by name. Don't rely on implicit state carrying across commands. -
patch-Z-foo.mpqwon't load. Multi-character suffixes afterpatch-get rejected silently — no log entry, no error. If your addon doesn't appear, first thing to check is the MPQ filename. -
Blizzard_*addons go through SIG verification regardless of where the TOC lives. PuttingBlizzard_DebugToolsinside an MPQ doesn't bypass the.toc.sigcheck. The signature gate is keyed on the name prefix, not the file's storage backend.
Deploying
Drop the built MPQ into <WoW root>\Data\ and launch normally:
Copy-Item "C:\Git\ClassicAPI\mpq-test\patch-Z.mpq" "C:\WoW\Octo\Data\"
After login, verify the addon enumerates:
/dump GetAddOnInfo("YourAddOnName")
If it returns data, the engine sees it. If nil, check the MPQ name
constraint and the internal directory layout.
See also
mpq-test/HANDOFF.md— the addon SIG-bypass research that motivated this MPQ exploration. Includes the verifier function offset, hook approach, and risk analysis for a separate DLL project.