Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to Modrex are documented in this file. Each version's sectio
- Health Check stays open while it reinstalls mods or installs dependencies, reinstalls mods one at a time, and shows which ones failed.
- Installed mods show their source under the card instead of over the picture.
- Browse Mods names the mod source in every game.
- PAYDAY: The Heist no longer asks for PDTHModOverrides, which the game now includes.

### Fixed

Expand All @@ -36,6 +37,7 @@ All notable changes to Modrex are documented in this file. Each version's sectio
- Fixed deleting a folder also deleting its mods' .ucas and .utoc files.
- Fixed broken formatting, images and videos in mod descriptions.
- Fixed popups closing when resizing the window.
- Fixed PAYDAY: The Heist showing the wrong DAHM install status.

### Security

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ The payload shapes are typed end to end by tauri-specta: renaming or retyping a

- PD3 mods are `.pak` files. Active: `{gamePath}/PAYDAY3/Content/Paks/~mods/`. Disabled: `~mods/disabled/foo.pak.disabled`. `gamePath` is the game install root. State: `.modrex.json` inside `~mods/` (was `.pd3mm.json`; migrated transparently on first launch after upgrade; travels with the game folder on dual-boot setups).
- PDTH supports a fourth mod format: `.pdmod` — a ZipCrypto-encrypted ZIP (password `0$45'5))66S2ixF51a<6}L2UK`) containing `pdmod.json` (an `ItemQueue` manifest) plus replacement asset files. `BundlePath` and `BundleExtension` fields in the manifest are Bob Jenkins lookup8 hashes; `commands/mods/pdmod.rs` resolves them via an embedded 130k-entry hashlist (`pdmod_hashlist.txt`, sourced from HW12Dev/PDModExtractor) and writes each asset to `assets/mod_overrides/<mod_name>/<resolved_path>.<resolved_ext>`. `zip.rs::resolve_archive_download` intercepts `.pdmod` files before `detect_archive` (since they are valid ZIPs by magic bytes and would otherwise fall through to the Directory-unit path). Location tag is `"mod_overrides"`.
- PD2 mods come in three flavors: BLT mods (`mod.txt`) and BeardLib mods (`main.xml`) both live in `{gamePath}/mods/` — Modrex scans for either marker (`entry_markers: &["mod.txt", "main.xml"]`); asset-replacement mods (any directory) live in `{gamePath}/assets/mod_overrides/` (`entry_markers: &[]`). Disabled mods of any flavor move to their respective `disabled/` subdirectories. State: `.modrex.json` inside `{gamePath}/mods/`. No numeric priority prefix — BLT/BeardLib load alphabetically. Disabling moves the whole folder; no extension rename. PDTH has BLT mods (`mods/` + `mod.txt`) and DAHM sub-mods (`mods/` + `base.lua`); it also has `mod_overrides` like PD2. PDTH has two loader mods handled as hardcoded exceptions: PDTHModOverrides (id 53474, `DINPUT8.dll` + `PDTHModOverrides.dll`) and DAHM (id 14267, `lightfx.dll`) — both are detected by DLL presence in the game root and installed via dedicated commands rather than the normal mod-install flow. BeardLib mods declare their modworkshop id in `main.xml` (`<AssetUpdates provider="modworkshop" id="N">`); `get_installed` uses this to identify them when the SHA256 hash misses — see the `get_installed` identification pipeline.
- PD2 mods come in three flavors: BLT mods (`mod.txt`) and BeardLib mods (`main.xml`) both live in `{gamePath}/mods/` — Modrex scans for either marker (`entry_markers: &["mod.txt", "main.xml"]`); asset-replacement mods (any directory) live in `{gamePath}/assets/mod_overrides/` (`entry_markers: &[]`). Disabled mods of any flavor move to their respective `disabled/` subdirectories. State: `.modrex.json` inside `{gamePath}/mods/`. No numeric priority prefix — BLT/BeardLib load alphabetically. Disabling moves the whole folder; no extension rename. PDTH has BLT mods (`mods/` + `mod.txt`) and DAHM sub-mods (`mods/` + `base.lua`); it also has `mod_overrides` like PD2. PDTH reads `assets/mod_overrides` itself since Update 23, so its package binds PDTHModOverrides (id 53474) as a `built_in` loader: a dependency on it always reads as met and nothing is installed, because its `DINPUT8.dll` proxy would overwrite DAHM's. DAHM (id 14267) is detected by its `pdthhook.dll` payload, not the shared `dinput8.dll` proxy name or the pre-Update-23 `lightfx.dll` hook, and installed via a dedicated command rather than the normal mod-install flow. BeardLib mods declare their modworkshop id in `main.xml` (`<AssetUpdates provider="modworkshop" id="N">`); `get_installed` uses this to identify them when the SHA256 hash misses — see the `get_installed` identification pipeline.
- **RAID: World War II** (`game_id: "raid"`) is Diesel-engine like PD2/PDTH. Steam-only on PC: app id `414740`, install folder `RAID World War II`, exe `raid_win64_release.exe`, process `raid_win64_release`; no Epic/Xbox release and no native Linux build (unlike PD2, so no `.so` loader variant). **Unlike PD2/PDTH, RAID has a single blanket-accept `mods` target (`RAID_ENGINE`), not a `mods` + `assets/mod_overrides` pair.** The modern loader (RAID-SuperBLT + RAIDWW2-BeardLib, mod 49760) reads BLT script mods (`supermod.xml` for SuperBLT, `mod.xml` for legacy RaidBLT — `mod.txt` does not exist in RAID's fork) **and** asset override packs (textures, `soundbanks/` sound replacements, etc.) from one `{gamePath}/mods/<name>/` folder: the older `assets/mod_overrides` mount was removed (current builds show a "MOD OVERRIDES IS NO LONGER USED" migration dialog, and BeardLib's `FindOverrides` scans each `mods/<name>/` folder for override subfolders instead — verified against a real install). So the target has no markers; any folder in `mods/` is a user mod unless its name is on `RAID_INFRA_FOLDERS` (`base`/`downloads`/`logs`/`saves` — mirrors BeardLib's own `_ignore_folders`, minus BeardLib itself, which is a normal installable mod tracked like any other). State: `.modrex.json` inside `{gamePath}/mods/`. The top-level `base` skip in `find_untracked_paks` also covers `mods/base/supermod.xml`, which a blanket scan would otherwise treat as a user mod. Loader: **RAID-SuperBLT** (modworkshop id 49744) is detected by `WSOCK32.dll` or `IPHLPAPI.dll` in the game root and installed as a full-zip extraction to the game root, DAHM-style, because the zip ships the Lua basemod `mods/base/` inside (unlike PD2's DLL-only SuperBLT zip); `IPHLPAPI.dll` may also be the discontinued RaidBLT. Its detection markers and download endpoint live in the loader registry (`commands/loaders.rs`). RAID mods declare the loader as an instructs-template dependency on 49744, resolved to the loader via the registry and routed to `install_loader`, never the normal mod flow. Both BLT marker formats embed the mod's own modworkshop id — `supermod.xml`: update element with an `identifier` attribute, version on the root mod element; `mod.xml`: `auto_updates` element with `id` + `version` attributes — parsed by `embedded_modworkshop_id` as an identification fallback (asset packs carry no marker and identify by SHA256/name); both repos hash the marker file itself (`hashable_file_for_mod_dir` / modrex-index's `selectMarkerPath` agree on `main.xml` then `supermod.xml` then `mod.xml`).
- **Host-mod content packs** (PD2): some add-ons (e.g. Menu Backgrounds background sets) carry no marker and no asset structure — they install _inside another mod's_ folder, which the scan-target model can't infer. `host_mods.rs` recognizes them by content signature and routes them via `install_host_pack`; they're tracked with a `host:<id>:<subpath>` `location`. A flat folder of loose files matching no host falls back to `UNRECOGNIZED_ARCHIVE` (the UI shows the author's instructions) rather than being silently dropped into `mod_overrides`. The manager places files and stops: it never writes a host mod's runtime/in-game settings to "activate" a pack (selecting it inside the host mod is the user's job).
- "Launch without mods" for PD3 renames `~mods` → `PAYDAY3/Content/~mods.bak` (one level above `Paks/`) — must be outside `Paks/` because Unreal scans all subdirectories there (PD3 is UE 4.27, not UE5, but the pak-mount behavior is the same). For BLT/BeardLib games, `do_restore` and `launch_without_mods` iterate `cfg.targets` — each target gets its own backup (`mods.bak/`, `assets/mod_overrides.bak/`); a target is skipped if its backup doesn't exist. Only user mod subdirectories are moved; `base/` is excluded from the primary target only (BLT recreates it if missing, showing a "base mod missing" dialog). `fs::remove_dir` (not `remove_dir_all`) on cleanup so failed renames are never silently deleted.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/game-package/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ fn loader(value: &LoaderBinding) -> String {
),
LoaderBinding::Superblt { modworkshop_ids } => plain("Superblt", modworkshop_ids),
LoaderBinding::RaidSuperblt { modworkshop_ids } => plain("RaidSuperblt", modworkshop_ids),
LoaderBinding::PdthOverrides { modworkshop_ids } => plain("PdthOverrides", modworkshop_ids),
LoaderBinding::BuiltIn { modworkshop_ids } => plain("BuiltIn", modworkshop_ids),
LoaderBinding::Dahm { modworkshop_ids } => plain("Dahm", modworkshop_ids),
}
}
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/src-tauri/game-package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ pub enum LoaderBinding {
#[serde(default)]
modworkshop_ids: Vec<i64>,
},
PdthOverrides {
/// Mod pages whose loader the game provides itself.
BuiltIn {
#[serde(default)]
modworkshop_ids: Vec<i64>,
},
Expand All @@ -241,7 +242,7 @@ impl LoaderBinding {
LoaderBinding::Ue4ss { .. } => "ue4ss",
LoaderBinding::Superblt { .. } => "superblt",
LoaderBinding::RaidSuperblt { .. } => "raid_superblt",
LoaderBinding::PdthOverrides { .. } => "pdth_overrides",
LoaderBinding::BuiltIn { .. } => "built_in",
LoaderBinding::Dahm { .. } => "dahm",
}
}
Expand All @@ -256,7 +257,7 @@ impl LoaderBinding {
}
| LoaderBinding::Superblt { modworkshop_ids }
| LoaderBinding::RaidSuperblt { modworkshop_ids }
| LoaderBinding::PdthOverrides { modworkshop_ids }
| LoaderBinding::BuiltIn { modworkshop_ids }
| LoaderBinding::Dahm { modworkshop_ids } => modworkshop_ids,
}
}
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src-tauri/game-package/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ pub fn markdown(examples: &[(&str, &str)]) -> String {
`kind` names a loader the host implements; a kind that does not exist is a build error,\n\
and so is a field belonging to a different kind. `modworkshop_ids` lists the\n\
modworkshop mod ids the loader is published under, so a mod depending on one of them\n\
installs the loader.\n\n\
installs the loader. `built_in` lists mod pages whose loader the game provides\n\
itself, so a dependency on one of them is always met and nothing is installed.\n\n\
| Kind | Extra fields |\n| --- | --- |\n",
);
out.push_str(&row(
Expand All @@ -239,7 +240,7 @@ pub fn markdown(examples: &[(&str, &str)]) -> String {
));
out.push_str(&row("superblt", "none"));
out.push_str(&row("raid_superblt", "none"));
out.push_str(&row("pdth_overrides", "none"));
out.push_str(&row("built_in", "none"));
out.push_str(&row("dahm", "none"));
out.push_str("\n`storefronts` accepts: ");
out.push_str(
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src-tauri/game-package/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub fn check(id: &str, package: &GamePackage) -> Result<(), String> {
return Err(format!("declares the '{duplicate}' loader twice"));
}
for loader in &package.loaders {
if matches!(loader, LoaderBinding::BuiltIn { modworkshop_ids } if modworkshop_ids.is_empty())
{
return Err("the built_in loader needs at least one modworkshop id".to_string());
}
let LoaderBinding::Ue4ss {
storefronts,
proxy_dlls,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/commands/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Paths are relative to `apps/desktop`.

- **Windows `CREATE_NO_WINDOW` invariant**: The release binary sets `windows_subsystem = "windows"` (`main.rs:1`), so it has no console. Any `std::process::Command` on Windows without `creation_flags(0x08000000)` spawns a visible console window. In debug builds (`bun dev`) this is invisible because the process inherits the parent console. Every spawn of a **console-mode executable** (cmd, tasklist, PowerShell, cargo, pkill, pgrep) must use `use std::os::windows::process::CommandExt` and `.creation_flags(0x08000000)`. GUI shell programs (`explorer.exe`, `xdg-open`) are exempt — they never attach to a console regardless.

- **`loaders.rs`** — the mod-loader registry: `LoaderSpec { id, name, detect, install }` and `LOADER_REGISTRY`. `LoaderInfo.target` comes from the target whose loader field names that loader in the game package, so the Installed page puts each loader on its own location tab. A loader is a hook installed next to the game, never tracked in `state.json` and never uninstallable through Modrex, so the five differ only in detection and layout — both captured as data. `DetectStrategy::RootFiles(&[...])` covers SuperBLT (`WSOCK32.dll` current / `IPHLPAPI.dll` legacy / `libsuperblt_loader.so` Linux native, PD2 only), PDTHModOverrides (`DINPUT8.dll` is the proxy and the sole signal; `PDTHModOverrides.dll` is the payload and is installed but not detected), DAHM (`lightfx.dll`), and RAID-SuperBLT (`WSOCK32.dll`/`IPHLPAPI.dll`, no `.so` — RAID has no native Linux build; `IPHLPAPI.dll` may also be the discontinued RaidBLT). `DetectStrategy::Ue4ssProxy` delegates to `ue4ss.rs`, whose proxy DLL and destination vary per `(game_id, launcher)`. `InstallStrategy` is `ExtractEntries` (SuperBLT pulls only `WSOCK32.dll` — the BLT basemod is fetched by the loader itself on next launch; PDTHModOverrides pulls both DLLs), `ExtractAllFlat` (DAHM and RAID-SuperBLT ship framework modules / the Lua basemod inside, so the whole archive lands in the game root), or `ViaModFlow` (UE4SS has no canonical host — each release is a modworkshop mod page, so installs route through the normal mod flow via `zip.rs`'s `UE4SS_LOADER` sentinel). Commands: `list_loaders` (hands the registry to the renderer, which maps dependency ids to loaders instead of restating the tables), `check_loader`, `install_loader` — an unknown loader id is a hard error. `modworkshop_ids` is empty only for SuperBLT, which has no mod page and is matched by the renderer's `blt` name heuristic instead.
- **`loaders.rs`** — the mod-loader registry: `LoaderSpec { id, name, detect, install }` and `LOADER_REGISTRY`. `LoaderInfo.target` comes from the target whose loader field names that loader in the game package, so the Installed page puts each loader on its own location tab. A loader is a hook installed next to the game, never tracked in `state.json` and never uninstallable through Modrex, so the five differ only in detection and layout — both captured as data. `DetectStrategy::RootFiles(&[...])` covers SuperBLT (`WSOCK32.dll` current / `IPHLPAPI.dll` legacy / `libsuperblt_loader.so` Linux native, PD2 only), DAHM (`pdthhook.dll`, its payload, since other loaders also ship a `dinput8.dll` proxy), and RAID-SuperBLT (`WSOCK32.dll`/`IPHLPAPI.dll`, no `.so` — RAID has no native Linux build; `IPHLPAPI.dll` may also be the discontinued RaidBLT). `DetectStrategy::Ue4ssProxy` delegates to `ue4ss.rs`, whose proxy DLL and destination vary per `(game_id, launcher)`. The `built_in` entry pairs `DetectStrategy::BuiltIn` with `InstallStrategy::BuiltIn` for mod pages whose loader the game provides itself: always present, never installed. `InstallStrategy` is `ExtractEntries` (SuperBLT pulls only `WSOCK32.dll` — the BLT basemod is fetched by the loader itself on next launch), `ExtractAllFlat` (DAHM and RAID-SuperBLT ship framework modules / the Lua basemod inside, so the whole archive lands in the game root), or `ViaModFlow` (UE4SS has no canonical host — each release is a modworkshop mod page, so installs route through the normal mod flow via `zip.rs`'s `UE4SS_LOADER` sentinel). Commands: `list_loaders` (hands the registry to the renderer, which maps dependency ids to loaders instead of restating the tables), `check_loader`, `install_loader` — an unknown loader id is a hard error. `modworkshop_ids` is empty only for SuperBLT, which has no mod page and is matched by the renderer's `blt` name heuristic instead.

**UE4SS** (Lua scripting/native modding framework, supported on both Crime Boss and PD3) splits into two layers that are handled completely differently, and conflating them is the easiest way to get this wrong:

Expand Down
Loading
Loading