jevsnes.git / nix / mesen.nix
1# Mesen 2, launched with a set of settings.json keys ENFORCED.
2#
3# Mesen owns settings.json: it rewrites all ~220 KB of it (key maps,
4# per-system config, window state) on startup and exit, so the file cannot
5# be a store symlink. What can be declared is the handful of keys this
6# project depends on, merged in before every launch. Everything else in the
7# file stays Mesen's.
8#
9# None of these keys has a command-line equivalent that works:
10#
11#   - `Video.UseSoftwareRenderer` is read in the `MainWindow` constructor
12#     (UI/Windows/MainWindow.axaml.cs:81), before the command line is
13#     parsed, so a switch arrives after the renderer is already chosen.
14#     The GPU renderer gives a BLACK game surface with working audio
15#     whenever Mesa falls back to llvmpipe, which is what an ordinary WSLg
16#     session on this host gets (measured 2026-09-20, mesen 2.1.1; the same
17#     build renders correctly with the two variables the devshell sets —
18#     see `flake.nix`). The software renderer works in both cases.
19#   - `Debug.ScriptWindow.AllowIoOsAccess` / `AllowNetworkAccess` gate
20#     LuaSocket, default false, and have no switch at all
21#     (UI/Config/Debugger/ScriptWindowConfig.cs:29-30). Network needs I/O
22#     on as well (Core/Debugger/ScriptingContext.cpp:73).
23#
24# THE SEED, when no settings file exists yet
25#
26#   - `ConfigUpgrade = 1` is `ConfigUpgradeHint.FirstRun`, the only value
27#     for which `Configuration.InitializeDefaults` populates controller
28#     mappings. A bare `{}` deserialises as 0 (Uninitialized) and leaves
29#     every port UNMAPPED.
30#   - A settings file existing at all is what suppresses the first-run
31#     wizard — including under `--testRunner`, which otherwise hangs on it
32#     (UI/Program.cs:56-76).
33#
34# Mesen writes the file with a UTF-8 BOM, which jq rejects, hence the sed.
35{
36  lib,
37  writeShellApplication,
38  writeText,
39  mesen,
40  jq,
41  gnused,
42  coreutils,
43  # Attrset deep-merged over settings.json before every launch.
44  enforce,
45}:
46let
47  enforced = writeText "mesen-enforced.json" (builtins.toJSON enforce);
48  seed = writeText "mesen-seed.json" (
49    builtins.toJSON (
50      lib.recursiveUpdate {
51        ConfigUpgrade = 1;
52        DefaultKeyMappings = "Xbox, ArrowKeys";
53      } enforce
54    )
55  );
56in
57writeShellApplication {
58  # Same name as the upstream binary, which is deliberately NOT also put on
59  # PATH: there is then no unwrapped `Mesen` to launch by mistake.
60  name = "Mesen";
61  runtimeInputs = [
62    jq
63    gnused
64    coreutils
65  ];
66  text = ''
67    settings="''${XDG_CONFIG_HOME:-$HOME/.config}/Mesen2/settings.json"
68    strip_bom() { sed '1s/^\xEF\xBB\xBF//' "$settings"; }
69
70    if [ ! -s "$settings" ]; then
71      mkdir -p "$(dirname "$settings")"
72      cat ${seed} > "$settings"
73    elif ! strip_bom | jq -e --slurpfile e ${enforced} '(. * $e[0]) == .' > /dev/null; then
74      tmp="$(mktemp "$settings.XXXXXX")"
75      trap 'rm -f "$tmp"' EXIT
76      strip_bom | jq --slurpfile e ${enforced} '. * $e[0]' > "$tmp"
77      mv "$tmp" "$settings"
78      trap - EXIT
79    fi
80
81    exec ${mesen}/bin/Mesen "$@"
82  '';
83}