Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deployment & VFS

Overview

modde never copies mods into your game directory and never edits the original files. Instead it builds a symlink farm: a virtual filesystem (VFS) assembled from symlinks that point back at a content-addressed store. The game — and any external tool — sees a fully merged, modded Data/ (or ~mods/, or mods/, depending on the game), while the real install on disk stays pristine.

This is the Linux-native equivalent of Mod Organizer 2’s USVFS, with one decisive advantage: USVFS hooks file-system calls per-process, so only the hooked process sees the merged tree. modde’s symlinks live in the real filesystem, so they are globally visible to every process — the game, xEdit, a packer, a shell — without any API hooking.

The VFS symlink farm is the deploy path for Nexus and Manual (store-backed) profiles. Wabbajack profiles take a different, hardlink/copy path — see Wabbajack profiles below.

The deployment pipeline

Deployment is a typestate pipeline enforced at compile time:

SymlinkFarm<Built>        link map computed in memory (paths only, no I/O)
        │  .materialize()
        ▼
SymlinkFarm<Materialized> symlinks written into the profile staging dir
        │  .deploy_to() / deploy_to_install()
        ▼
   (symlinks in the game's mod directory)

Each stage is a distinct Rust type. You cannot call deploy_to() on a Built farm — the compiler rejects it — so it is impossible to deploy a farm that was never written to disk. The state marker is a zero-sized PhantomData, so this safety costs nothing at runtime.

1. Build

SymlinkFarm::build() computes a single HashMap<relative_path, source_path> where source_path is the absolute path of the winning file inside the store. No filesystem writes happen yet — this phase is pure path resolution.

The build takes:

InputMeaning
profile_nameSelects the staging directory location
resolvedThe mods in final priority order (first = lowest priority)
mod_filesPer-mod (relative_path, store_source_path) listings
overridesOptional profile-level override files (always win)
hiddenOptional (mod_id, relative_path) pairs to exclude

How files map to winners

The build walks resolved.order from lowest to highest priority and inserts each (rel_path → source) into the map. Because later inserts overwrite earlier ones for the same key, the highest-priority provider of each path wins. The effective priority chain, highest first:

  1. Profile overrides — files in the profile’s overrides directory are layered last and beat every mod.
  2. Later mods in the load order — install priority, last-wins.
  3. Earlier mods in the load order.

A file listed in hidden as (mod_id, rel_path) is skipped while walking that mod, so the next-highest provider of that path wins instead. This is how per-file hiding takes effect — the equivalent of MO2’s .mohidden. A mod that appears in resolved.order but has no entry in mod_files (its store directory is missing) is silently skipped during build; the deploy command logs the missing mods separately.

Install priority is not plugin load order. The symlink farm resolves which file wins on disk. Bethesda .esp/.esm/.esl ordering is a separate concern handled by LOOT and plugins.txt. See Conflicts & Load Order.

2. Materialize

materialize() writes the farm to the per-profile staging directory:

~/.local/share/modde/profiles/<profile>/staging/

It first removes any existing staging directory in full, recreates it, then for each (rel_path → source) it creates the parent directories and writes a symlink at staging/<rel_path> pointing at the absolute store source. Because materialize wipes and rebuilds staging from scratch, the staging tree always reflects exactly the current Built farm — there are no stale leftovers from a previous deploy.

The content store these links point into lives at:

~/.local/share/modde/store/<mod_id>/...

3. Deploy

The materialized staging tree is projected into the game’s resolved mod directory. For each relative path the deploy step removes any pre-existing entry at the destination and creates a symlink pointing at the corresponding file inside the staging directory (again an absolute path). The result is a two-hop chain:

<game>/Data/textures/sky.dds   ─sym→   staging/textures/sky.dds   ─sym→   store/<mod_id>/textures/sky.dds

Final placement is delegated to the game plugin via deploy_to_install(), so a game can apply its own deploy strategy (for example, UE4 titles route paks into Content/Paks/~mods/). After deployment modde runs the game’s post_deploy hook and configures Wine DLL overrides (WINEDLLOVERRIDES) for any proxy DLLs the mods deploy, such as version.dll for CET or winmm.dll for ASI loaders.

Both hops use the symlink target exactly as passed — these are absolute paths in modde, not relative ones. On Linux the call is a plain symlink(2); on Windows modde inspects the target to choose symlink_file vs symlink_dir.

If a store entry is missing, the symlink is still created but dangles. modde does not error at deploy time for a broken link — the missing-mod case is caught earlier during build (the mod is skipped and reported), and dangling links are surfaced by modde verify, which flags any broken symlink -> <target>.

Deploying

Manual deployment

# Deploy the active profile
modde deploy

# Deploy a specific profile
modde deploy --profile my-skyrim --game skyrim-se

Deploy via play

modde play deploys before launching the game:

modde play --game skyrim-se

Automatic deployment

After rebuilding your NixOS / home-manager configuration, modde deploys profiles via an activation script when their prerequisites are present. Wabbajack profiles can wait non-fatally for the game install: set installMode = "await-game" or leave gameDir unset until Steam or Heroic has installed the game, then set gameDir and rebuild. For Home Manager Wabbajack profiles, the install runs before deployment only when the configured gameDir exists and contains the expected game content directory (for example Data/ for Skyrim SE); otherwise activation prints an awaiting message and continues.

The symlink farm only ever creates symlinks. A second, distinct mechanism — link_or_copy — is used wherever modde must materialise an actual file rather than a link (notably the Wabbajack deploy path). It tries three strategies in order and reports which one it used:

StrategyWhen it appliesCost
HardlinkSource and destination on the same filesystemFree; shares inode/blocks
ReflinkHardlink crossed a filesystem (EXDEV) but the FS supports copy-on-write (btrfs, XFS, ZFS)Free until written; CoW
CopyNeither of the above workedFull byte copy

The destination is removed first if it exists. A cross-device error (EXDEV on Linux, ERROR_NOT_SAME_DEVICE on Windows) triggers the fall to reflink; a reflink failure falls to a plain copy. This keeps deployments cheap on a single CoW filesystem and correct everywhere else.

Rollback

modde rollback swaps the profile’s staging and staging.bak directories:

modde rollback --profile my-skyrim

The swap is performed with directory renames, which are atomic on a single filesystem:

  • If a current staging exists, it is renamed aside to staging.old, staging.bak is renamed into place as staging, and staging.old is removed.
  • If no current staging exists, staging.bak is renamed straight into place.

If there is no staging.bak for the profile, rollback fails with no backup staging found for profile '<name>'. modde does not silently create a backup on every deploy — materialize() rebuilds staging in place — so staging.bak is a deliberate restore point you (or higher-level tooling) preserve before a risky change. After a successful rollback you must re-run modde deploy to re-project the restored staging tree into the game directory.

Wabbajack profiles

Wabbajack-installed profiles do not use the symlink farm. A Wabbajack modlist ships a pre-built, already-conflict-resolved file layout under ~/.local/share/modde/staging/<profile>/mods/<mod>/..., so there is nothing for the VFS to resolve. Deploying one walks that mods/ tree and places each file into the game directory with link_or_copy (hardlink → reflink → copy):

  • MO2 metadata files (meta.ini, meta.json) are skipped.
  • A file already hardlinked to its source (same inode and device) is skipped, so re-deploys are near-instant.
  • Wine DLL overrides are configured afterwards, exactly as for store-backed profiles.

Hardlinking (rather than symlinking) is used here because the staging tree is the authoritative, fully-merged layout — the game sees real files, which is what Wabbajack lists and their tools expect. See the Wabbajack guide for the full install flow.

Verifying integrity

modde verify --profile my-skyrim

verify walks every enabled mod’s store directory, hashes each file with xxHash, and checks symlink health. It reports:

  • Total files checked
  • Missing — enabled mods absent from the store, or files that vanished
  • Mismatches/errors — files it could not hash, and any broken symlink -> <target> where the link target no longer exists

A clean run prints All files OK. This is the fastest way to catch a store entry that was deleted out from under a deployed profile.

Vanilla (stock) snapshots

A stock snapshot captures the original game installation so you can detect when something — a mod, a tool, a botched deploy — has modified files that should have stayed vanilla:

# Capture a snapshot of the detected install
modde stock snapshot skyrim-se

# Re-verify the install against the snapshot later
modde stock verify skyrim-se

snapshot detects the install directory via the game plugin, records the stock file set, and prints the snapshot path and file count. verify re-checks the current install against the recorded snapshot and prints either Stock snapshot for <game>: OK or Stock snapshot for <game>: MISMATCH (re-snapshot recommended). Take the snapshot before you first deploy mods, while the game is genuinely vanilla.

See also

  • Conflicts & Load Order — how the winning file is chosen and how Bethesda plugin order differs from install priority
  • Wabbajack — the hardlink/copy deploy path for modlists
  • Profiles — overrides, hidden files, and per-profile staging
  • Playingmodde play and launch integration
  • Troubleshooting — broken symlinks and missing-store fixes
  • Parity reference — VFS feature coverage vs MO2