Replaces three handfuls of custom code with upstream / static data:
- Per-app Steam launch options now declared via different-name/steam-
config-nix's `programs.steam.config.apps.<n>` instead of a custom
~70-line `apply_launch_options` Python function. The dropped writer
was racy: it edited localconfig.vdf without checking for a running
Steam, so any timer firing while Steam was open would lose its
changes on the next Steam shutdown. steam-config-nix's `closeSteam`
flag closes that race.
Also moves the GE-Proton compat-tool pin to declarative config —
one fewer manual click in Steam UI to remember.
- `mods.<>.launchOptions` option, the `launchOptionsData` aggregation,
and `LAUNCH_OPTIONS_DATA` are removed from desktop-game-mods.nix.
The module now does file-drops only; Steam config lives in its own
`programs.steam.config` namespace, where it belongs.
fh5-vkd3d-no-hvv (which existed only to set VKD3D_CONFIG) collapses
into the FH5 launchOptions block in hosts/yarn/default.nix.
- `unitConfig.X-ConfigHash` on game-mods.service is replaced with
`restartTriggers`. NixOS already emits `X-Restart-Triggers=<hash>`
on the unit; the workaround was redundant. The Type=oneshot,
RemainAfterExit=no semantics make `systemctl restart` re-run
ExecStart cleanly on hash change.
- The awk pipeline that patched OptiScaler's stock OptiScaler.ini at
build time is replaced with a hand-written hosts/yarn/optiscaler-
fh5-rdna3.ini containing only the keys we override (5 of them).
OptiScaler's Config::readString defaults missing keys to "auto"
(Config.cpp:1568), so a minimal file is sufficient. Side benefits:
one upstream-source dependency removed, a key-rename in upstream
becomes a behavior change rather than a silent awk-no-match.
Override values + sources:
Fsr4Update=true FH5 wiki, FSR4 Linux Setup
DlssReactiveMaskBias=0.65 FH5 wiki, "Known Issues"
FsrNonLinearColorSpace=true FSR4 wiki, "Image Quality"
EnableFsr2Inputs=false FH5 wiki, "Known Issues"
Dxgi=false FH5 wiki
- forza-trigger's three custom Python derivations (pydualsense,
hidapi-usb, fdp) factored out of default.nix into a sibling
python-packages.nix. Same logic, single-purpose file. Bumping a
version is now a one-place hash roll.
- pkgs.dualsensectl removed from the daemon's environment.system-
Packages. Single-shot writes from the CLI get clobbered by the BG
sendReport thread within ~4ms anyway, so the tool is only useful
with the daemon stopped — not worth the unconditional install.
Bring it in ad-hoc with `nix-shell -p dualsensectl`.
107 lines
4.2 KiB
Nix
107 lines
4.2 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
username,
|
|
...
|
|
}:
|
|
# Forza Horizon 4 / 5 → DualSense adaptive trigger bridge.
|
|
#
|
|
# Forza emits a fixed-format UDP telemetry stream ("Data Out") at 60 Hz on a
|
|
# user-configured port. We listen on that port, parse each packet via fdp
|
|
# (nettrom/forza_motorsport, MIT), and drive the PS5 DualSense's adaptive
|
|
# triggers via pydualsense (PyPI, MIT) which talks HID over hidraw.
|
|
#
|
|
# Setup on the user side, once enabled here:
|
|
# - plug the DualSense in over USB and disable Steam Input for the
|
|
# controller (Settings → Controller → "PlayStation Configuration Support":
|
|
# OFF). Bluetooth works too but the udev/hidraw path is more reliable
|
|
# over USB.
|
|
# - in Forza, HUD options → set Data Out: ON, Data Out IP: 127.0.0.1,
|
|
# Data Out IP Port: 5300, and (FM7 only) Data Out Packet Format: CAR DASH.
|
|
#
|
|
# System-interaction notes:
|
|
# - With multiple DualSense controllers connected, pydualsense picks one
|
|
# non-deterministically (`# TODO: implement multiple controllers working`
|
|
# in pydualsense's source). Forza Horizon is single-player so this is
|
|
# usually fine. If you need to pin a specific controller, the cleanest
|
|
# route is monkey-patching `pydualsense.__find_device`.
|
|
# - `pkgs.dualsensectl` is intentionally NOT installed by default
|
|
# (single-shot writes from it get overwritten by our BG thread within
|
|
# ~4 ms). Bring it in ad-hoc with `nix-shell -p dualsensectl` and stop
|
|
# this service first via `systemctl --user stop forza-trigger`.
|
|
# - Hot-plug recovery happens in-process: the daemon polls pydualsense's BG
|
|
# thread liveness and re-runs `pydualsense.init()` on disconnect. systemd's
|
|
# `Restart=on-failure` exists only as a crash-recovery safety net.
|
|
let
|
|
cfg = config.services.forzaTrigger;
|
|
pythonPackages = import ./python-packages.nix { inherit lib pkgs; };
|
|
inherit (pythonPackages) pydualsense fdp;
|
|
|
|
forzaTrigger = pkgs.writers.writePython3Bin "forza-trigger" {
|
|
libraries = [
|
|
pydualsense
|
|
fdp
|
|
];
|
|
# The wrapped binary doesn't need style enforcement — readability of
|
|
# the source file is what matters, and that lives in forza_trigger.py.
|
|
doCheck = false;
|
|
} (builtins.readFile ./forza_trigger.py);
|
|
|
|
in
|
|
{
|
|
options.services.forzaTrigger = {
|
|
enable = lib.mkEnableOption "Forza Horizon → DualSense adaptive trigger bridge";
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = username;
|
|
description = ''
|
|
User the trigger daemon runs as. Must be the user playing Forza so
|
|
the DualSense's hidraw uaccess ACL applies.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 5300;
|
|
description = ''
|
|
UDP port the daemon listens on for Forza Data Out packets. Must
|
|
match the value configured in Forza's HUD options.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# uaccess hands /dev/hidraw* of the connected PS5 DualSense to the
|
|
# active-seat user via ACL. Steam ships near-identical rules; declaring
|
|
# them here keeps the module self-contained (and works even if Steam
|
|
# isn't running).
|
|
services.udev.extraRules = ''
|
|
# PS5 DualSense (USB)
|
|
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", MODE="0660", TAG+="uaccess"
|
|
# PS5 DualSense Edge (USB)
|
|
KERNEL=="hidraw*", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0df2", MODE="0660", TAG+="uaccess"
|
|
# PS5 DualSense (Bluetooth)
|
|
KERNEL=="hidraw*", KERNELS=="*054C:0CE6*", MODE="0660", TAG+="uaccess"
|
|
# PS5 DualSense Edge (Bluetooth)
|
|
KERNEL=="hidraw*", KERNELS=="*054C:0DF2*", MODE="0660", TAG+="uaccess"
|
|
'';
|
|
|
|
environment.systemPackages = [ forzaTrigger ];
|
|
|
|
# User-level service so it inherits the seat-bound uaccess ACL on
|
|
# /dev/hidraw* and dies cleanly when the user logs out.
|
|
systemd.user.services.forza-trigger = {
|
|
description = "Forza Horizon → DualSense adaptive trigger bridge";
|
|
wantedBy = [ "default.target" ];
|
|
after = [ "graphical-session.target" ];
|
|
serviceConfig = {
|
|
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port}";
|
|
Restart = "on-failure";
|
|
RestartSec = 3;
|
|
};
|
|
};
|
|
};
|
|
}
|