yarn: forza dualsense adaptive trigger bridge
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/result
|
/result
|
||||||
/result-*
|
/result-*
|
||||||
|
__pycache__
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
./impermanence.nix
|
./impermanence.nix
|
||||||
./lact.nix
|
./lact.nix
|
||||||
./vr.nix
|
./vr.nix
|
||||||
|
./forza-trigger
|
||||||
|
|
||||||
inputs.impermanence.nixosModules.impermanence
|
inputs.impermanence.nixosModules.impermanence
|
||||||
];
|
];
|
||||||
@@ -95,14 +96,9 @@
|
|||||||
# live in ./optiscaler-fh5-rdna3.ini; keys not listed there fall through
|
# live in ./optiscaler-fh5-rdna3.ini; keys not listed there fall through
|
||||||
# to OptiScaler's "auto" defaults.
|
# to OptiScaler's "auto" defaults.
|
||||||
#
|
#
|
||||||
# Required one-time per-game setup the user has to do in Steam (no API):
|
# Required one-time in-game setup the user has to do in FH5 (no API):
|
||||||
# - Properties > Compatibility: pick the GE-Proton tool by hand. The
|
# - Switch the Upscaling option from FSR 2.2 to DLSS or XeSS (FSR 2
|
||||||
# `compatTool` option is intentionally unset \u2014 nixpkgs registers
|
# inputs aren't intercepted). Press Insert to open the OptiScaler
|
||||||
# proton-ge-bin under its versioned id (e.g. GE-Proton10-34), and
|
|
||||||
# writing the generic "GE-Proton" string silently falls back to
|
|
||||||
# bundled Proton.
|
|
||||||
# - In-game: switch the Upscaling option from FSR 2.2 to DLSS or XeSS
|
|
||||||
# (FSR 2 inputs aren't intercepted). Press Insert to open the Opti
|
|
||||||
# overlay and set the FFX upscaler to FSR 4.
|
# overlay and set the FFX upscaler to FSR 4.
|
||||||
#
|
#
|
||||||
# OptiScaler.ini is dropped with mode = "init" so in-game overlay edits
|
# OptiScaler.ini is dropped with mode = "init" so in-game overlay edits
|
||||||
@@ -125,8 +121,10 @@
|
|||||||
{
|
{
|
||||||
enable = true;
|
enable = true;
|
||||||
closeSteam = true;
|
closeSteam = true;
|
||||||
|
defaultCompatTool = "proton_10";
|
||||||
apps."fh5" = {
|
apps."fh5" = {
|
||||||
id = 1551360;
|
id = 1551360;
|
||||||
|
compatTool = "proton_10";
|
||||||
launchOptions.env = {
|
launchOptions.env = {
|
||||||
# OptiScaler FSR 4 INT8 path on this RDNA 3 (Navi 32) box.
|
# OptiScaler FSR 4 INT8 path on this RDNA 3 (Navi 32) box.
|
||||||
# PROTON_FSR4_UPGRADE opts FH5 into Proton's FSR 4 DLL upgrade;
|
# PROTON_FSR4_UPGRADE opts FH5 into Proton's FSR 4 DLL upgrade;
|
||||||
@@ -172,4 +170,8 @@
|
|||||||
] fromOpti;
|
] fromOpti;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
# PS5 DualSense adaptive triggers in Forza Horizon 4 / 5.
|
||||||
|
services.forzaTrigger.enable = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,19 +20,6 @@
|
|||||||
# - in Forza, HUD options → set Data Out: ON, Data Out IP: 127.0.0.1,
|
# - 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.
|
# 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
|
let
|
||||||
cfg = config.services.forzaTrigger;
|
cfg = config.services.forzaTrigger;
|
||||||
pythonPackages = import ./python-packages.nix { inherit lib pkgs; };
|
pythonPackages = import ./python-packages.nix { inherit lib pkgs; };
|
||||||
@@ -90,16 +77,45 @@ in
|
|||||||
|
|
||||||
environment.systemPackages = [ forzaTrigger ];
|
environment.systemPackages = [ forzaTrigger ];
|
||||||
|
|
||||||
# User-level service so it inherits the seat-bound uaccess ACL on
|
# Socket-activated by Forza's Data Out UDP stream on port 5300.
|
||||||
# /dev/hidraw* and dies cleanly when the user logs out.
|
# The service starts when Forza Horizon 4/5 sends its first telemetry
|
||||||
systemd.user.services.forza-trigger = {
|
# packet, runs for the entire session, and exits on 3 s of silence
|
||||||
|
# (--exit-on-idle). systemd restarts automatically on the next launch.
|
||||||
|
#
|
||||||
|
# Runs as a system service (not user service) so there is exactly one
|
||||||
|
# instance regardless of how many users have active sessions. The service
|
||||||
|
# runs as `cfg.user` (primary) for hidraw uaccess compatibility with the
|
||||||
|
# udev TAG+="uaccess" rules.
|
||||||
|
#
|
||||||
|
# Crash-recovery: Restart=on-failure restarts the daemon mid-session if
|
||||||
|
# it dies abnormally (non-zero exit). The clean exit-on-idle path returns
|
||||||
|
# 0, which systemd leaves alone — the socket unit stays listening.
|
||||||
|
systemd.services.forza-trigger = {
|
||||||
description = "Forza Horizon → DualSense adaptive trigger bridge";
|
description = "Forza Horizon → DualSense adaptive trigger bridge";
|
||||||
wantedBy = [ "default.target" ];
|
# No wantedBy — socket activation pulls it in when Forza sends UDP.
|
||||||
after = [ "graphical-session.target" ];
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port}";
|
ExecStart = "${forzaTrigger}/bin/forza-trigger --host 127.0.0.1 --port ${toString cfg.port} --exit-on-idle";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 3;
|
RestartSec = 3;
|
||||||
|
User = cfg.user;
|
||||||
|
# KillMode=control-group is the default, but be explicit.
|
||||||
|
# systemd sends SIGTERM by default; Python doesn't route that to
|
||||||
|
# KeyboardInterrupt, so we tell systemd to send SIGINT instead.
|
||||||
|
# The daemon registers handlers for both for robustness.
|
||||||
|
KillMode = "control-group";
|
||||||
|
KillSignal = "SIGINT";
|
||||||
|
TimeoutStopSec = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.sockets.forza-trigger = {
|
||||||
|
wantedBy = [ "sockets.target" ];
|
||||||
|
socketConfig = {
|
||||||
|
ListenDatagram = "127.0.0.1:${toString cfg.port}";
|
||||||
|
# Guard against rapid re-triggering. One activation per Forza session
|
||||||
|
# (minutes+) is normal; > 3 within 10 s is pathological (bug).
|
||||||
|
TriggerLimitIntervalSec = 10;
|
||||||
|
TriggerLimitBurst = 3;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
1516
hosts/yarn/forza-trigger/forza_trigger.py
Normal file
1516
hosts/yarn/forza-trigger/forza_trigger.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,10 +14,7 @@
|
|||||||
"steam-run"
|
"steam-run"
|
||||||
];
|
];
|
||||||
|
|
||||||
programs.steam = {
|
programs.steam.enable = true;
|
||||||
enable = true;
|
|
||||||
extraCompatPackages = with pkgs; [ proton-ge-bin ];
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
steamtinkerlaunch
|
steamtinkerlaunch
|
||||||
|
|||||||
Reference in New Issue
Block a user