---
name: nix-config-umu-game
description:
  Use when installing a Windows game launcher (二次元 / gacha or any non-Steam game) on a NixOS
  desktop via umu-launcher, given an installer URL or an .exe, when such a launcher opens an
  invisible, transparent, black, or empty window under Wine, or when its download crawls behind the
  proxy.
---

# Installing a Windows game launcher with umu

The bundled `scripts/umu-install.nu` renders the files in `scripts/templates/`. It creates a prefix,
runs the installer, and writes launchers. Use the Nix DW-Proton default unless research identifies a
specific installed release; then select it with `PROTONPATH` and pin that path in the game's `conf`.
**Every per-game detail lives under `~/Games/`**, never in this repo.

## Bundled files

- `scripts/umu-install.nu`: installer and template renderer
- `scripts/templates/*.tpl`: non-executable templates; placeholders use `@NAME@`
- `scripts/tests/test_install.py`: offline regression tests

## Layout

- `~/Games/global.conf`: optional, sourced by every game before its own `conf`
- `~/Games/<name>/prefix`: the Wine prefix (`WINEPREFIX`)
- `~/Games/<name>/conf`: optional per-game overrides (`ENABLE_MANGOHUD=0`, ...)
- `~/Games/<name>/prelaunch`: optional executable, run before every launch
- `~/Games/<name>/run`: generated launcher
- `~/Games/<name>/exec`: generated helper: `exec <exe-or-tool> [args...]`
- `~/Games/<name>/<name>.desktop`: generated desktop entry
- `~/Games/<name>/uninstall`: generated: drop the desktop entry and (with `y`) the dir

Run, from the repo root:

`nu .agents/skills/nix-config-umu-game/scripts/umu-install.nu <name> <setup.exe> <launcher> [gameid] [setup-args...]`

`<launcher>` is relative to the prefix and `GAMEID` defaults to `umu-default`. At install,
`PROTONPATH` resolves from `$PROTONPATH`, `$UMU_PROTONPATH` (the Nix-provided DW-Proton), then
Steam's per-user `compatibilitytools.d`. At launch, the generated `run` script checks the current
`$PROTONPATH` and `$UMU_PROTONPATH` before its saved default. A game's `conf` is sourced after those
defaults, so set `PROTONPATH` there to pin a particular installed version across launches.

## Procedure

1. **Research first.** Mandatory before inventing any fix: a documented fix beats a custom patch
   every time. Check the game's Lutris installer YAML as data only
   (`https://lutris.net/api/installers/<game-slug>` encodes `winetricks` verbs, `write_file`
   content, and `prelaunch_command`; do not install or run Lutris), then ProtonDB
   (`protondb.com/app/<id>`), Steam Community / r/linux_gaming threads, and GitHub issues for the
   launcher and Proton. Record whether reports require a Proton family and exact release. If no
   exact release is required, use the Nix-provided DW-Proton default. If one is required, inspect
   `$UMU_PROTONPATH` and installed Steam tools under `~/.local/share/Steam/compatibilitytools.d/` or
   `~/.steam/root/compatibilitytools.d/`; select the matching installed path with `PROTONPATH`.
   Prefix the installer command with `PROTONPATH="<selected-tool-directory>"` and set the same value
   in the game's `conf` to pin it across launches; otherwise the runtime Nix `UMU_PROTONPATH` may
   take precedence. If the required release is absent from Nix and Steam, stop and ask the user
   before downloading it from elsewhere. Do not substitute a different release or use
   ProtonPlus/Lutris to fetch one.
2. **Slug.** Pick a lowercase `<name>` (e.g. `wuthering-waves`).
3. **GAMEID.** The umu database maps a title to a `GAMEID` whose protonfixes add CJK fonts, drop the
   `SteamOS`/`SteamDeck` vars, and keep Wine's `Documents` inside the prefix. **Without it some
   games save into the host home or the wrong prefix.** Fetch
   `https://raw.githubusercontent.com/Open-Wine-Components/umu-database/main/umu-database.csv`,
   match the title, and use its `umu-<id>`; if nothing matches, leave the default.
4. **Download.** `curl -fL <url> -o ~/Games/<name>/setup.exe`. The CN store pages are JS/token
   driven, so the user normally supplies the URL or the file itself.
5. **Install.** Run the bundled script. The first run also downloads umu's Steam runtime and the
   protonfix fonts, so it is slow. The installer is usually a GUI the user clicks through, but
   **extra args after `[gameid]` go to `setup.exe`**, so try a silent flag first (`/S`, `/quiet`,
   `--silent`); if it ignores them, fall back to clicking. On some CN installers the last page
   auto-starts the launcher. The script writes `run`/`exec` even if the installer exits non-zero, so
   a killed installer is not fatal.
6. **Find the launcher.** If `<launcher>` is only known after install, locate it under
   `~/Games/<name>/prefix/drive_c` and set `LAUNCHER` in `~/Games/<name>/conf`. Re-running the
   installer script invokes `setup.exe` again; only do so if that installer is known to handle
   reruns.
7. **Per-game fix.** Port step 1 findings into `~/Games/<name>/prelaunch` (bash, `chmod +x`):
   `winetricks` verbs via `~/Games/<name>/exec winetricks <verbs>`, file patches or registry tweaks
   via `~/Games/<name>/exec`. Never commit the fix to this repo.
8. **Re-run every launch, not once.** A launcher that self-updates overwrites files it patches, so
   those patches belong in `prelaunch`.
9. **Verify.** `~/Games/<name>/run` must open a visible launcher window. MangoHud is enabled by
   default with FPS and frame time at the top-left; set `ENABLE_MANGOHUD=0` in `conf` to disable it,
   or override `MANGOHUD_CONFIG`. Re-running `prelaunch` must be idempotent. The launcher may then
   download the game body itself (tens of GB) -- that is the launcher's job, not this skill's, so
   the install is **done** once the window is usable. A crawl usually means the proxy: the launcher
   probes the CDN itself and mihomo routes that probe through a distant node, so the download starts
   from a far mirror; routing is in the
   [mihomo README](../../../modules/nixos/desktop/networking/mihomo/README.md). `ENABLE_LOG=1`
   captures `last-run.log` when something misbehaves.

## Invisible / transparent launcher window

A WebView2/.NET (WPF) launcher that shows an empty or fully transparent window is rendering with
`AllowsTransparency`. Fix the DLL, not Wine:

1. `find ~/Games/<name>/prefix/drive_c -iname 'launcher_main.dll'` (usually under a `X.Y.Z.W`
   version dir).
2. Rename that WPF property in place, keeping the byte length so PE offsets stay valid:
   `bbe -e 's/\x12AllowsTransparency/\x09IsEnabled\x1bA\x00\x03AAAAA/' <dll>`.
3. Put it in `~/Games/<name>/prelaunch` so it is re-applied on every launch, because a launcher
   self-update restores the original DLL. Restart the launcher after patching.

## Half-width tile instead of fullscreen

Niri tiles a new window into a column of `default-column-width { proportion 0.500000; }` (50%), and
a launcher that only opens a large borderless window cannot take over the screen by itself: with a
4K panel at `scale 1.5` Niri's logical space is 2560x1440, so a window asking for the physical
3840x2160 becomes an ordinary half-width tile. Nothing is broken -- the umu app-id is just not
covered by a rule.

Every umu/Proton game reports `steam_app_<GAMEID>`. In this repo
`home/linux/gui/niri/conf/windowrules.kdl` matches `app-id="^steam_app_[0-9]+$"` and sets
`open-fullscreen true`, so games open fullscreen without a per-game rule. `niri validate` checks the
live config, `Mod+Shift+F` (`fullscreen-window`) toggles an already-open window, and setting the
in-game resolution to the compositor's logical size (2560x1440 here) avoids the mismatch too.

A launcher shares that app id with its game, so the rule also catches the launcher's own windows: a
helper that is still untitled while it maps, and the announcement popup (公告 / Announcement), which
on this launcher renders as a pure white rectangle. Fullscreening either one buries the UI that
opened it, so both are excluded. A window rule applies when a window opens, so an already-open
offender needs `Mod+Shift+F` on it or a relaunch; drop `open-fullscreen true` altogether if a
launcher keeps surprising you -- the game then costs one `Mod+Shift+F` per session instead.

## Mojibake that is not a missing font

Latin-looking garbage such as `æˆ‘å·²é˜…è¯»` where the launcher should say 我已阅读并同意, while
other Chinese on the same window renders fine, is UTF-8 decoded as Windows-1252. Confirm the
pattern:

```bash
python3 -c "print('我已阅读并同意'.encode('utf-8').decode('cp1252','replace'))"
```

If that prints exactly what the launcher shows, the bug is identified and you can stop here.

The string itself is fine and so are the fonts: the same launcher
renders 请输入手机号 and 登录 correctly, and the garbled label exists as correct UTF-16 in
`KRSDKEx.dll`. The bytes are simply decoded with a single-byte Western codepage.

**This is not the prefix locale.** Setting `LANG=zh_CN.UTF-8` in `~/Games/<name>/conf` does change
the prefix (verified in this repo: `"ACP"` went `1252` -> `936` and `"LocaleName"` `en-US` ->
`zh-CN`), and the garbling stayed **byte-for-byte identical**. The launcher decodes those bytes as
Latin-1 no matter what Windows codepage it is told to use, so this is an upstream bug in its own
text pipeline: report it, or accept it -- the launcher still works. `LANG` is nevertheless the lever
for the prefix locale, not `LC_ALL`: Proton clears `LC_ALL` unless `HOST_LC_ALL` is set.

So: check the bytes first, do not install more CJK fonts, and do not promise a locale fix.

## Silent install

`umu-install.nu <name> <setup.exe> <launcher> <gameid> /S` passes `/S` to the installer. Not all
installers honour it; try `/quiet` or `--silent` too, then fall back to the GUI.

## Desktop entry and persistence

The script also writes `~/.local/share/applications/<name>.desktop` (and a copy at
`~/Games/<name>/<name>.desktop`), so the game shows up in the desktop launcher. Under an
impermanence setup both `~/Games` and `~/.local/share/applications` must be in the host's
`preservation.preserveAt` list, in `hosts/<dir>/preservation.nix`.

## Uninstall

Run `~/Games/<name>/uninstall`: it removes the desktop entry and then asks before deleting
`~/Games/<name>` (prefix + game files). To keep the game and only drop the menu entry, delete the
`.desktop` file by hand.

## Optimize the launch

`~/Games/<name>/conf` (or `~/Games/global.conf`) is a shell file the generated `run` sources.
Defaults are `ENABLE_MANGOHUD=1`, `MANGOHUD_CONFIG="fps=1,frametime=1,position=top-left"`,
`ENABLE_GAMEMODE=0`, `ENABLE_GAMESCOPE=0`, `GAMESCOPE_ARGS="-f"`, and `ENABLE_LOG=0`. Set
`ENABLE_MANGOHUD=0` to hide the overlay, `ENABLE_GAMESCOPE=1` +
`GAMESCOPE_ARGS="-f -w 3840 -h 2160"` for the handheld / multi-monitor case, `ENABLE_GAMEMODE=1` for
GameMode, and `ENABLE_LOG=1` to capture the run. A dGPU wrapper still goes outside:
`nvidia-offload ~/Games/<name>/run`. Shader caches are kept in `~/Games/<name>/shader-cache`.

## Escape hatches

- `~/Games/<name>/exec <exe-or-tool> [args...]` runs anything in the prefix with the right env: an
  absolute path to a game exe or repair tool, or a bare Wine tool (`winecfg`, `explorer`, `regedit`,
  `uninstaller`), which it runs as `umu-run <Proton's wine> <tool>`. Both forms are handed to
  `umu-run`, so they execute inside umu's Steam runtime container -- that is what makes them work on
  a non-FHS distro like NixOS, where Proton's wine cannot start on its own.
- `~/Games/<name>/run <game args>` passes extra args to the launcher.
- Kill a stuck prefix with `pkill -f '/Games/<name>/prefix'`, or
  `rm -f ~/Games/<name>/prefix/pfx.lock`.

## Common mistakes

- Committing a game name, URL, or patch to this repo instead of `~/Games/<name>/`.
- Patching once instead of in `prelaunch` -- the next launcher self-update undoes it.
- Dropping the `GAMEID` -- the in-game CJK fonts and the save location go wrong.
- Installing more CJK fonts when the text is mojibake -- decode the bytes first (see above).
- Expecting umu to fetch DW-Proton: it only auto-manages GE-Proton / UMU-Proton; use Nix or Steam
  installed paths. Ask the user before obtaining a required release from elsewhere.
- Running a Wine tool without umu. `umu-run winecfg` does not work -- umu only special-cases
  `winetricks` -- and `$PROTONPATH/files/bin/wine winecfg` fails on any non-FHS distro (on NixOS the
  32-bit loader it needs lives only inside the Steam runtime container). Use
  `~/Games/<name>/exec winecfg`; `exec` goes through `umu-run`, which provides that container.
- Waiting for the game download before calling the install done: the launcher's own download is out
  of scope here.
- On NixOS a game needing 32-bit or Vulkan needs `hardware.graphics.enable32Bit`, and Steam is a
  module rather than a package: `modules/nixos/desktop/gaming.nix`.

## Regression tests

From the repo root:

```bash
python3 .agents/skills/nix-config-umu-game/scripts/tests/test_install.py
```

Requires `python3`, `nu`, `bash`, and `shellcheck`. Uses temporary homes and a stub `umu-run`; no
game downloads, Wine windows, or changes to existing games. It checks template permissions, rendered
shell syntax and lint, installer arguments and failures, configuration precedence, prelaunch,
logging, launch wrappers, literal paths, runtime Proton overrides, tool routing, and uninstall
confirmation.
