diff --git a/crates/pf-client-core/src/gamepad.rs b/crates/pf-client-core/src/gamepad.rs index a0a5eac3..2ef54988 100644 --- a/crates/pf-client-core/src/gamepad.rs +++ b/crates/pf-client-core/src/gamepad.rs @@ -876,10 +876,16 @@ impl Worker { ); return; }; - let pref = self - .pad_info(id) - .map(|p| p.pref) - .unwrap_or(GamepadPref::Xbox360); + let pref = match self.pad_info(id) { + // Steam Input's virtual pad standing in front of the Deck's built-in controls (the + // only-pad-forwarded case, [`Self::forwarded_ids`]): declare the DECK kind, not the + // wrapper's Xbox 360 identity. [`Self::auto_pref`] already resolves the SESSION + // default this way, but a current host honors the per-pad arrival over the session + // default — so without this the host builds an X-Box 360 pad on a real Deck. + Some(p) if p.steam_virtual && is_steam_deck() => GamepadPref::SteamDeck, + Some(p) => p.pref, + None => GamepadPref::Xbox360, + }; match self.subsystem.open(sdl3::sys::joystick::SDL_JoystickID(id)) { Ok(pad) => { let mut slot = Slot::new(id, index, pref, pad); diff --git a/crates/punktfunk-core/src/reject.rs b/crates/punktfunk-core/src/reject.rs index 8b62ac1a..0529d077 100644 --- a/crates/punktfunk-core/src/reject.rs +++ b/crates/punktfunk-core/src/reject.rs @@ -146,7 +146,7 @@ impl std::fmt::Display for RejectReason { mod tests { use super::*; - const ALL: [RejectReason; 9] = [ + const ALL: [RejectReason; 10] = [ RejectReason::PairingNotArmed, RejectReason::PairingBoundToOtherDevice, RejectReason::PairingRateLimited, @@ -156,6 +156,7 @@ mod tests { RejectReason::Superseded, RejectReason::WireVersionMismatch, RejectReason::Busy, + RejectReason::SetupFailed, ]; #[test] @@ -177,7 +178,7 @@ mod tests { fn foreign_codes_stay_untyped() { // Bare closes, the client's own pair-done codes, and the deliberate-end codes must // never read as a host rejection. - for code in [0u32, 1, 0x41, 0x51, 0x52, 0x5f, 0x68, u32::MAX] { + for code in [0u32, 1, 0x41, 0x51, 0x52, 0x5f, 0x69, u32::MAX] { assert_eq!(RejectReason::from_close_code(code), None); } } diff --git a/crates/punktfunk-host/src/native.rs b/crates/punktfunk-host/src/native.rs index 2ae31258..1ad10eef 100644 --- a/crates/punktfunk-host/src/native.rs +++ b/crates/punktfunk-host/src/native.rs @@ -463,7 +463,7 @@ pub(crate) async fn serve( } conn_err.close( punktfunk_core::reject::SETUP_FAILED_CLOSE_CODE.into(), - detail[..cut].as_bytes(), + &detail.as_bytes()[..cut], ); tracing::warn!(%peer, error = %detail, "session ended with error") } diff --git a/crates/punktfunk-host/src/plugins.rs b/crates/punktfunk-host/src/plugins.rs index a37a3acd..9e8e829c 100644 --- a/crates/punktfunk-host/src/plugins.rs +++ b/crates/punktfunk-host/src/plugins.rs @@ -158,9 +158,25 @@ pub(crate) fn runner_command() -> Result<(std::path::PathBuf, Vec)> { if bun.exists() && runner.exists() { return Ok((bun, vec![runner.to_string_lossy().into_owned()])); } + // Immutable-/usr distros (SteamOS): scripts/steamdeck/install.sh lays the SAME payload + // out user-scoped under ~/.local — wrapper, private bun, and bundle mirroring the deb's + // /usr layout — because a system package can't exist there. + if let Ok(home) = std::env::var("HOME") { + let home = std::path::Path::new(&home); + let wrapper = home.join(".local/bin/punktfunk-scripting"); + if wrapper.exists() { + return Ok((wrapper, Vec::new())); + } + let bun = home.join(".local/lib/punktfunk-scripting/bun"); + let runner = home.join(".local/share/punktfunk-scripting/runner-cli.js"); + if bun.exists() && runner.exists() { + return Ok((bun, vec![runner.to_string_lossy().into_owned()])); + } + } bail!( "the plugin runner isn't installed — install it first (Debian/Ubuntu: \ - `sudo apt install punktfunk-scripting`)" + `sudo apt install punktfunk-scripting`; SteamOS: re-run \ + scripts/steamdeck/install.sh)" ) } } diff --git a/scripts/steamdeck/install.sh b/scripts/steamdeck/install.sh index 633c4d6c..b870f24d 100755 --- a/scripts/steamdeck/install.sh +++ b/scripts/steamdeck/install.sh @@ -153,6 +153,35 @@ cd '$SRC/web' && bun install --frozen-lockfile && bun run build ok "web console built" fi +# --- 2b. plugin runner (scripting) ----------------------------------------- +# The console's plugin store and `punktfunk-host plugins …` shell out to the scripting runner — +# the SDK's runner CLI bundled to one self-contained JS, run on a pinned bun (it import()s the +# operator's .ts plugins; see packaging/debian/build-scripting-deb.sh). The .deb lays it out under +# /usr, which is read-only here, so ship the SAME payload user-scoped — wrapper + private bun + +# bundle under ~/.local, where the host's runner discovery looks after the /usr layouts. +log "Building the plugin runner (scripting)" +mkdir -p "$HOME/.local/bin" "$HOME/.local/lib/punktfunk-scripting" "$HOME/.local/share/punktfunk-scripting" +distrobox enter "$BOX" -- bash -lc " +set -e +export PATH=\$HOME/.bun/bin:\$PATH +cd '$SRC/sdk' +bun install --frozen-lockfile --ignore-scripts +bun build src/runner-cli.ts --target=bun --outfile \"\$HOME/.local/share/punktfunk-scripting/runner-cli.js\" +# Pin the runtime: copy the box's bun next to the bundle so a bun self-update (or a box rebuild) +# never changes what the runner executes under. +install -m0755 \"\$(command -v bun)\" \"\$HOME/.local/lib/punktfunk-scripting/bun\" +" +grep -q 'attempt=' "$HOME/.local/share/punktfunk-scripting/runner-cli.js" \ + || die "runner bundle missing the dynamic plugin import — wrong build" +cat > "$HOME/.local/bin/punktfunk-scripting" <<'WRAP' +#!/bin/sh +# Generated by scripts/steamdeck/install.sh — user-scoped punktfunk-scripting (the .deb's /usr/bin +# wrapper, relocated): the runner bundle on its private pinned bun. +exec "$HOME/.local/lib/punktfunk-scripting/bun" "$HOME/.local/share/punktfunk-scripting/runner-cli.js" "$@" +WRAP +chmod 0755 "$HOME/.local/bin/punktfunk-scripting" +ok "plugin runner: ~/.local/bin/punktfunk-scripting" + # --- 3. config ------------------------------------------------------------- log "Configuration ($CONFIG)" mkdir -p "$CONFIG" @@ -306,6 +335,13 @@ EOF ok "punktfunk-web.service (port $WEB_PORT)" fi +# The runner's user unit (OPT-IN, matching the .deb: installed but NOT enabled — the runner is +# inert until you add scripts/plugins). ExecStart is rewritten from the packaged /usr wrapper to +# the user-scoped one section 2b installed. +sed 's|^ExecStart=.*|ExecStart=%h/.local/bin/punktfunk-scripting|' \ + "$SRC/scripts/punktfunk-scripting.service" > "$UNITS/punktfunk-scripting.service" +ok "punktfunk-scripting.service (opt-in: systemctl --user enable --now punktfunk-scripting)" + systemctl --user daemon-reload loginctl show-user "$USER" 2>/dev/null | grep -q 'Linger=yes' || { sudo loginctl enable-linger "$USER" 2>/dev/null && ok "enabled linger (services run without login)" || warn "could not enable linger — services stop when you log out (sudo loginctl enable-linger $USER)"; } # enable + restart (not `enable --now`): restart picks up unit-file changes on a re-run, where diff --git a/scripts/steamdeck/update.sh b/scripts/steamdeck/update.sh index bbd5c2b0..4c5350b4 100755 --- a/scripts/steamdeck/update.sh +++ b/scripts/steamdeck/update.sh @@ -8,6 +8,9 @@ set -euo pipefail log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m ok\033[0m %s\n' "$*"; } +# warn was USED below but never defined — under `set -e` the first warn call ("command not +# found") aborted the whole update before the service restarts. +warn() { printf '\033[1;33m !!\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } SRC="${PUNKTFUNK_SRC:-$HOME/punktfunk}" @@ -30,6 +33,25 @@ if [ "$WEB" = 1 ]; then ok "web rebuilt" fi +# Plugin runner (scripting): rebuild the user-scoped runner payload (install.sh §2b) — also +# RETROFITS it onto older installs that predate it (the "plugin runner isn't installed" console +# state on SteamOS). +log "Rebuilding plugin runner (scripting)" +mkdir -p "$HOME/.local/bin" "$HOME/.local/lib/punktfunk-scripting" "$HOME/.local/share/punktfunk-scripting" +distrobox enter "$BOX" -- bash -lc "set -e; export PATH=\$HOME/.bun/bin:\$PATH; cd '$SRC/sdk' && bun install --frozen-lockfile --ignore-scripts && bun build src/runner-cli.ts --target=bun --outfile \"\$HOME/.local/share/punktfunk-scripting/runner-cli.js\" && install -m0755 \"\$(command -v bun)\" \"\$HOME/.local/lib/punktfunk-scripting/bun\"" +grep -q 'attempt=' "$HOME/.local/share/punktfunk-scripting/runner-cli.js" \ + || die "runner bundle missing the dynamic plugin import — wrong build" +cat > "$HOME/.local/bin/punktfunk-scripting" <<'WRAP' +#!/bin/sh +# Generated by scripts/steamdeck/update.sh — user-scoped punktfunk-scripting (see install.sh §2b). +exec "$HOME/.local/lib/punktfunk-scripting/bun" "$HOME/.local/share/punktfunk-scripting/runner-cli.js" "$@" +WRAP +chmod 0755 "$HOME/.local/bin/punktfunk-scripting" +sed 's|^ExecStart=.*|ExecStart=%h/.local/bin/punktfunk-scripting|' \ + "$SRC/scripts/punktfunk-scripting.service" > "$HOME/.config/systemd/user/punktfunk-scripting.service" +systemctl --user daemon-reload +ok "plugin runner rebuilt (opt-in service: systemctl --user enable --now punktfunk-scripting)" + # Retrofit config that install.sh now writes but older installs predate (both idempotent): # RADV_PERFTEST — Van Gogh RADV still gates VK_KHR_video_encode_* behind it; without it the # Vulkan backend can't open and sessions silently fall back to libav VAAPI. The KWin .desktop —