A Rust modding utility for Project Wingman (PW), inspired by Project Sicario.
- Status
- V1 Scope — 1:1 parity with the Project Sicario merger
- Parity status
- Merging mods without metadata (
combine) - Translation mods (
.locres) - Game audio (
.ogg) - Documentation
- Building
- Usage
- Project Structure
- License
v1 (Sicario-merger parity) is essentially complete and byte-verified against the C# merger. Working today:
- PAK I/O (
info,list,unpack,pack,scan), including compressed entries (Zlib, Gzip, Zstd, LZ4);pack --compressionwrites Zlib or Gzip, the methods Project Wingman can load. .dtm/.dtppatch parsing, Fragment DSL, every Sicario patch type- Template rendering (Fluid-compatible:
vars/inputs+ the full Sicario filter set) - Verified DataTable engine (byte-exact walker + same-size splice + length-changing rowops)
- HexPatch engine (
filePatches) with the C# stream-walk semantics and the.uexplength auto-correct - Merge pipeline: component model (embeddedPresets → loosePresets → sicarioRequests), engine-major phases,
.uexp/.uassetsidecar handling buildwrites a real mergedSicarioMerge_P.pak;preset-packbuilds standalone preset packs with the preset embedded atContent/sicario/;--reportwrites the C#-identical merge reportcombinemerges conflicting override mods that carry no Sicario metadata (the common case for 2024+ mods), three-way and field-level, with a full conflict report
In-game acceptance passed (SPEAR Unlock + Improved Chimera verified in Project Wingman 2.1.1A).
The v1 goal is 1:1 parity with the Sicario merger.
A drop-in replacement for the Sicario merger (SicarioPatch.Loader) against Project Wingman 2.1.1A / UE 4.27 / Pak V11:
- Inputs: identical
.dtm/.dtpWingmanMod JSON, with_vars/_inputstemplating. - Patch semantics: every Sicario fragment type, every Sicario patch type, identical matching and value behavior.
- Merge semantics: identical multi-mod merging to Sicario's engine.
- Output: a Pak V3 mod pack (mount
../../../) for the game's~mods/directory, same as the merger. - Zero .NET dependency: a single native binary.
Verified against the C# merger at byte level (oracle harness in ~/modding/project-wingman/sicario-oracle):
| Surface | Status |
|---|---|
| DataTable patches (propertyValue, modify, array, text, duplicate*, delete) | ✅ byte-exact (uassets identical; uexps modulo random FText keys) |
filePatches hex engine (all types, windows, filters, length fix-up) |
✅ byte-exact, including the destructive absent-value path |
objectRef (import-table writes) + customSkins PSM slot merging |
✅ byte-exact + in-game verified (AJS-37 skin slot live) |
| Multi-mod merge order + conflict semantics | ✅ oracle-verified |
Components, parameters/inputs, engine-version gate, GetLabel |
✅ |
Merge report (--report) |
✅ byte-identical |
preset-pack |
✅ byte-identical output pak |
| In-game acceptance | ✅ |
All known v1 parity gaps are closed. customSkins skin-slot merging works with real
PSM skin paks (ProjectWingman/Content/Assets/Skins/<aircraft-row>/…), live-verified in-game.
- Self-contained merges (default): the merged pak embeds detected skin files, so one pak
installs everything (
--no-embed-skinsrestores the strict C# output). - No-metadata merging (
combine): three-way, field-level datatable merging for plain override mods, with import carrying and conflict reports. - End-of-merge reports for both
buildandcombine: merge order, field-level conflicts (which mod overwrote which value), and warnings.
Project Sicario's merge metadata (.dtp presets, embedded requests) was never updated for
Project Wingman 2.0 / UE 4.27, so most 2024+ mods are plain file overrides. When two of
them edit the same uasset, the game loads only one. modman combine merges them anyway:
modman combine <pak-or-dir...> --install-path <game> --output <dir>- DataTables are merged three-way (vanilla + each mod's delta, later mods win) at field level: different fields of the same row all survive, and genuine same-field clashes resolve later-wins and are reported.
- Mods that add object references (textures, materials) keep working: import-table entries are carried with all FName and package-index references remapped.
- Non-datatable files (maps, models, textures) pass through single-winner, with a warning when two mods fight over one. Those file types cannot be combined.
- Every run ends with a merge report: the order, every conflict (with mod names and values), and warnings.
https://emberglazee.github.io/project-modman/: the same merge engine compiled to WebAssembly, running entirely in your browser:
- Drop your conflicting
*_P.pakfiles, point at your game'spakchunk0-WindowsNoEditor.pak, merge, and download the combined pak. - Nothing is uploaded. The game pak is read in place through the browser's File API (a few MB of it, even from a ~16 GB file), and the page makes zero network requests after loading.
- Shows the same merge report (order + conflicts) with a friendly UI.
- This is the no-metadata
combinepath: it does not apply.dtm/.dtppatches. For those, use the desktop Build presets tab ormodman build.
modman-gui is the same engine in a native window: egui, no browser engine, one
binary at about 13.0 MB. Download it from the release page and double-click; it
finds the game on its own, and asks if it cannot.
- Merge mods: drag
.rar,.7z,.zipor.pakfiles onto the window. Archives are extracted in-process (rars,sevenz-rust2,zip, all pure Rust, so nothing has to be installed) and merged; reorder the list to choose which mod wins, and the conflict check updates as you move entries. One button then installs the result into~mods, keeping a timestamped backup of whatever it replaced. - Build presets: build a Sicario preset set into one
SicarioMerge_P.pak. Accepts.dtp,.dtmand.jsonpresets plus folders; this is the metadata path. Merge mods is the metadata-lesscombineand does not apply patches. - Game audio: list the sound assets in a pak and extract them as
.ogg. - Translations: pull a
.locresout of the game pak, export it as CSV, and patch a translated CSV back in. - Pak files: inspect, unpack and repack.
- Scan & inspect: list the installed Sicario components, or inspect a
.dtm/.dtppatch file.
Every tab calls the same modman-core function the command line calls, so the
window cannot produce output the CLI would not. Linux and Windows builds; on
macOS run the Windows build under Whisky or CrossOver.
In the browser and the desktop app, the list is the order. Drag a row, or use its up and down buttons; new files append. The Priority switch decides which end wins a conflict: Later in the list wins applies the list top to bottom, and Earlier in the list wins applies it bottom to top. The winning end is marked.
Both interfaces check conflicts after a reorder, with a 400 ms debounce, and a Check now button forces the check. It runs the same merge engine and reports the exact field conflicts, the files that cannot be combined, and warnings, without writing a pak. The check is not free because it reads every datatable, but the 29-pak aircraft set probes in about 0.4 seconds; the desktop app keeps the Merge button available while it runs.
The command line still sorts paks alphabetically by full path, so renaming is only needed there.
The Output menu next to the Priority switch writes the merged pak
uncompressed, Zlib, or Gzip. The merged pak is V3, and Project Wingman has no
zstd or LZ4 decompressor, so those methods are read-only.
modman combine --compression zlib|gzip writes the same compressed output from
the command line.
- Windows: double-clicking
modman.exeshows a welcome screen (it pauses so you can read it) pointing at the browser option; with arguments it runs as a normal CLI (modman --help). Windows may show a SmartScreen prompt for a new unsigned binary: More info → Run anyway. - macOS: the binaries are unsigned, so Gatekeeper blocks the first run. Right-click
→ Open once, or run
xattr -d com.apple.quarantine modman. - Linux:
chmod +x modmanand run it from a terminal. - Or skip all of that: the web page, or the offline
modman-merge.html, one file you double-click with no install, no server and no network.
These binaries are unsigned, so there is no publisher name to check. What there is instead is a signed build provenance statement: every release binary is built by this repository's CI, and that claim is cryptographically attested by GitHub. Anybody can check it, and it takes one command with the GitHub CLI (2.49 or newer):
gh attestation verify ./modman-linux-amd64 \
--repo emberglazee/project-modman \
--signer-workflow emberglazee/project-modman/.github/workflows/rust-build.ymlThe command hashes the file, fetches the attestation GitHub stores for that digest, and checks the signature against the workflow identity. A pass means the file is the exact output of a build of this repository at a recorded commit. A fail means the file is not that, whatever else it is. The same works for the GUI binaries and for the Windows builds:
gh attestation verify ./modman-gui-linux-amd64 --repo emberglazee/project-modmanSHA256SUMS on each release still serves the older workflow: it proves the download
is intact, not who built it. The attestation proves both.
The game's text lives in Unreal localization binaries at
ProjectWingman/Content/Localization/ProjectWingman/<lang>/ProjectWingman.locres.
modman reads and patches them with full structural fidelity. Every namespace and key
hash, entry order and string reference count is preserved, so in-game lookups never
miss. (Verified: parse → write reproduces all nine shipped language files
byte-for-byte.)
# 1) Pull the language file straight out of the game's pak
# (finds the install automatically; --pak <file> to use another pak)
modman locres extract --lang en-US -o ProjectWingman.locres
# ...or go straight to the spreadsheet: add --csv
# ...or omit --lang to dump every language at once
# 2) Dump a locres to a translator-friendly CSV (key,source,Translation)
modman locres read ProjectWingman.locres -o translations.csv
# 3) Translate the third column — the CSV matches the UEExtractor format,
# so the existing translator tooling and workflows apply as-is
# 4) Build a ready-to-install translation mod in one step
modman locres pak ProjectWingman.locres translations.csv --lang en-US -o MyTranslation_P.pak
# (--dedup merges identical strings, matching other community tools — a
# rebuild of an existing mod comes out byte-identical to it, verified
# against the ":3" mod)
# Bonus: see exactly what a mod changes vs the game
modman locres diff Original.locres Mod.locres --csv changes.csv --limit 40Drop the result into ~mods and you're done.
The game stores its sound as Ogg Vorbis inside the pak, so extraction is a strip rather than a transcode:
# List the sound assets (with no path, reads the game's own pak)
modman audio list --filter Dialog
# Write every SoundWave as .ogg, mirroring the asset tree
modman audio extract -o pw-audio --filter "Dialog/C_M6"
modman audio extract -o pw-audio --manifest audio.csv # the whole gameThe output mirrors the asset path, so Assets/Audio/Dialog/C_M6/... stays
navigable. The long music tracks are stored streamed, split between the .uexp
and a .ubulk; extract reassembles them, so each track arrives as one
complete file rather than a 20 second fragment.
In-depth guides live in docs/:
- Architecture: crate and module map, data flow for each command, the naming rules, and where a new feature belongs.
- Merging conflicting mods: the no-metadata merge, conflict semantics, what can and can't merge, reading the report.
- Translation mods: the full
.locresworkflow, the CSV format, tips, and binary format notes for tool authors. - FAQ: install paths, first-run warnings (SmartScreen, Gatekeeper), merge conflicts, browser-page privacy.
- Game audio: listing and extracting the game's sounds as
.ogg, the streamed-track trap, and how to check a result.
Requires Rust 2021 edition or later.
# Build all crates
cargo build --workspace
# Build the CLI only
cargo build -p modman
# Run tests
cargo testmodman --help
modman --version
# Merge mods/presets into a mod pak (like `ProjectSicario build`)
modman build <paths...> --install-path <game> --output <dir> --report merge-report.json
# Build standalone preset packs (preset embedded at Content/sicario/)
modman preset-pack <preset.dtp...> -n MyPack --install-path <game>
# Merge conflicting override mods — no metadata required
modman combine <pak-or-dir...> --install-path <game> --output <dir>
# Pack a folder; zlib and gzip are the only methods Project Wingman loads
modman pack <dir> -o out.pak --version V11 --compression zlibcrates/
├── modman-core/ Data models, patch engine, merge pipeline, components,
│ locres reader/writer
├── modman-uasset/ UE4 .uasset binary parser (standalone)
├── modman-pak/ PAK file operations (wraps repak)
├── modman-cli/ CLI binary
└── modman-wasm/ WebAssembly frontend (wasm-bindgen) powering the web UI
web/ GitHub Pages site: hosted page + offline single-file build
MIT