diff --git a/services/wolf.sh b/services/wolf.sh index 3366b28..91e91ee 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -285,6 +285,84 @@ print(pick[0]["browser_download_url"] if pick else "") esac } +# Same job as _wolf_download_emulator_appimage above, but against GitLab's +# Releases API instead of GitHub's — needed for any project (ES-DE included) +# that's hosted on GitLab rather than GitHub, since GitHub's API obviously +# can't answer for a repo it doesn't host. Mirrors the same arch-matching / +# post-download ELF-header verification logic so both call sites behave +# identically from the caller's point of view. +_wolf_download_emulator_appimage_gitlab() { + local _display_name="$1" _project_path="$2" _existing_glob="$3" _dir="$4" + if ls "$_dir"/$_existing_glob 2>/dev/null | grep -q .; then + log_info "$_display_name already present in $_dir/" + return 0 + fi + local _get="" + echo "" + log_info "$_display_name can be auto-downloaded." + prompt_yn "Download $_display_name AppImage now? (y/n):" "y" _get + [[ "$_get" =~ ^[Yy]$ ]] || return 0 + + log_info "Fetching latest $_display_name release from GitLab..." + local _encoded_path + _encoded_path=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$_project_path") + local _url + _url=$(curl -fsSL "https://gitlab.com/api/v4/projects/${_encoded_path}/releases" \ + | HOST_ARCH="$(uname -m)" python3 -c ' +import sys, json, os +host = os.environ.get("HOST_ARCH", "") +arch_tags = { + "x86_64": ["x86_64", "amd64", "x64"], + "aarch64": ["aarch64", "arm64"], + "arm64": ["aarch64", "arm64"], +}.get(host, [host] if host else []) +all_arch_tags = ["x86_64", "amd64", "x64", "aarch64", "arm64", "armv7", "armhf", "i386", "i686"] +def has(name, tags): + n = name.lower() + return any(t in n for t in tags) +releases = json.load(sys.stdin) +# GitLab does not guarantee list order — sort explicitly instead of +# assuming index 0 is the newest (the mistake that would silently pick a +# stale/older release on some future API response ordering change). +releases = sorted(releases, key=lambda r: r.get("released_at") or "", reverse=True) +assets = [] +for r in releases: + for link in r.get("assets", {}).get("links", []): + url = link.get("direct_asset_url") or link.get("url") or "" + if url.endswith(".AppImage"): + assets.append({"name": link.get("name", url), "browser_download_url": url}) + if assets: + break +matching = [a for a in assets if arch_tags and has(a["name"], arch_tags)] +untagged = [a for a in assets if not has(a["name"], all_arch_tags)] +pick = matching or untagged or assets +print(pick[0]["browser_download_url"] if pick else "") +' 2>/dev/null) + if [[ -z "$_url" ]]; then + log_warning "Could not resolve download URL — get it manually from https://gitlab.com/${_project_path}/-/releases" + return 1 + fi + local _file="$_dir/$(basename "$_url")" + curl -fL --progress-bar -o "$_file" "$_url" \ + && chmod +x "$_file" \ + && chown "$ACTUAL_USER:$ACTUAL_USER" "$_file" \ + && log_success "$_display_name downloaded: $_file" \ + || { log_warning "Download failed — get it manually from https://gitlab.com/${_project_path}/-/releases"; return 1; } + + local _got_arch + _got_arch=$(file -b "$_file" 2>/dev/null) + case "$(uname -m)" in + x86_64) + echo "$_got_arch" | grep -qi 'x86-64\|x86_64' || \ + log_warning "$_file doesn't look like an x86_64 build ($_got_arch) — it will fail with 'exec format error'. Grab the x86_64 asset by hand from https://gitlab.com/${_project_path}/-/releases" + ;; + aarch64|arm64) + echo "$_got_arch" | grep -qi 'aarch64\|arm64' || \ + log_warning "$_file doesn't look like an aarch64 build ($_got_arch) — it may fail to run. Grab the aarch64 asset by hand from https://gitlab.com/${_project_path}/-/releases" + ;; + esac +} + install_wolf() { require_docker || return 1 @@ -939,6 +1017,85 @@ UDEV log_info "not something this installer can supply. Cemu's own First-Time Setup Wizard covers where" log_info "to put it once you have one." + # ── Optional: ES-DE and RetroArch as standalone AppImages (for Steam) ──── + # These are ADDITIONAL to the esde/retroarch Wolf catalog containers + # above, not a replacement — nothing here removes or changes those. The + # only reason to want this: once added as a Steam non-Steam game + # (./manage.sh steam-add-nonsteam-game, which the wolf mount fix above + # already extended to reach the same roms/saves/bios/retro-home/ + # retroarch paths the esde/retroarch containers use), Steam Input can + # give each of up to 4 identical-model controllers its own distinct + # identity by device path — the one thing Wolf's own 3-concrete-pad-type + # ceiling can't do for a 4th controller (see manage.sh's own "4 + # controllers (Cemu / Wii U games)" help text). Skip both prompts below + # if you're happy running Wii U/retro systems through the esde app + # directly and don't need Steam's per-device controller assignment. + # + # Both download to a FIXED, predictable filename (ES-DE.AppImage / + # RetroArch.AppImage) regardless of the real upstream release asset's + # own name — steam-add-nonsteam-game matches by substring against the + # actual filename on disk, and bash glob matching is case-sensitive, so + # a fixed name (same symlink trick already used for Dolphin above) is + # what makes './manage.sh steam-add-nonsteam-game es-de' reliably find + # it regardless of how the vendor's own release happens to be named. + if [ ! -f "$_EMU_DIR/ES-DE.AppImage" ]; then + echo "" + # ES-DE is hosted on GitLab, not GitHub (confirmed against its own + # project page) — a different Releases API than every other + # standalone emulator above, hence the separate _gitlab helper. + log_info "ES-DE also ships an official standalone Linux AppImage — separate from the esde Wolf" + log_info "app above. Only useful for adding to Steam (see this repo's wolf README); skip this if" + log_info "you'll only ever use the esde app directly." + local _GET_ESDE_APPIMAGE="" + prompt_yn "Download the ES-DE AppImage for use via Steam? (y/n):" "n" _GET_ESDE_APPIMAGE + if [[ "$_GET_ESDE_APPIMAGE" =~ ^[Yy]$ ]]; then + _wolf_download_emulator_appimage_gitlab \ + "ES-DE" "es-de/emulationstation-de" "ES-DE.AppImage" "$_EMU_DIR" + local _ESDE_REAL + _ESDE_REAL=$(ls "$_EMU_DIR"/*.AppImage 2>/dev/null \ + | grep -iE '/(es-?de|emulationstation)[^/]*\.AppImage$' \ + | grep -v '/ES-DE\.AppImage$' | head -1) + if [[ -n "$_ESDE_REAL" ]]; then + ln -sf "$(basename "$_ESDE_REAL")" "$_EMU_DIR/ES-DE.AppImage" + chown -h "$ACTUAL_USER:$ACTUAL_USER" "$_EMU_DIR/ES-DE.AppImage" 2>/dev/null || true + log_success "Linked $_EMU_DIR/ES-DE.AppImage -> $(basename "$_ESDE_REAL")" + log_info "Once Wolf is running, add it to Steam with: cd $WOLF_DIR && ./manage.sh steam-setup-frontends" + log_info "(waits for Steam sign-in — QR code via Moonlight — then wires this in automatically)" + fi + fi + fi + + if [ ! -f "$_EMU_DIR/RetroArch.AppImage" ]; then + echo "" + # Libretro's own buildbot doesn't publish through a GitHub/GitLab + # Releases API this installer can automate against — hizzlekizzle/ + # RetroArch-AppImage is the well-regarded THIRD-PARTY nightly-build + # project the AppImage community catalogs (appimage.github.io etc.) + # themselves point to, same "flagged, not silently offered as + # official" treatment as the Dolphin community build above. + log_info "RetroArch also has a standalone Linux AppImage, separate from the retroarch Wolf app" + log_info "above — same rationale as ES-DE just above (Steam Input's per-device controller" + log_info "assignment). This comes from hizzlekizzle/RetroArch-AppImage, a well-regarded but" + log_info "THIRD-PARTY nightly-build project, not an official libretro.org release — grab" + log_info "RetroArch's own build by hand instead if you'd rather not run that." + local _GET_RA_APPIMAGE="" + prompt_yn "Download the RetroArch AppImage for use via Steam? (y/n):" "n" _GET_RA_APPIMAGE + if [[ "$_GET_RA_APPIMAGE" =~ ^[Yy]$ ]]; then + _wolf_download_emulator_appimage \ + "RetroArch" "hizzlekizzle/RetroArch-AppImage" "RetroArch.AppImage" "$_EMU_DIR" + local _RA_REAL + _RA_REAL=$(ls "$_EMU_DIR"/*[Rr]etro[Aa]rch*.AppImage 2>/dev/null \ + | grep -v '/RetroArch\.AppImage$' | head -1) + if [[ -n "$_RA_REAL" ]]; then + ln -sf "$(basename "$_RA_REAL")" "$_EMU_DIR/RetroArch.AppImage" + chown -h "$ACTUAL_USER:$ACTUAL_USER" "$_EMU_DIR/RetroArch.AppImage" 2>/dev/null || true + log_success "Linked $_EMU_DIR/RetroArch.AppImage -> $(basename "$_RA_REAL")" + log_info "Once Wolf is running, add it to Steam with: cd $WOLF_DIR && ./manage.sh steam-setup-frontends" + log_info "(waits for Steam sign-in — QR code via Moonlight — then wires this in automatically)" + fi + fi + fi + # ── Optional: TI-99/4A as its own ES-DE system ──────────────────────────── # TI-99/4A has no libretro core and isn't one of ES-DE's built-in systems, # so getting it real ES-DE treatment (artwork scraping, gameplay-time @@ -2166,7 +2323,26 @@ CATALOG = { # (see below) — without it, an emulator AppImage (Cemu, etc.) # added as a non-Steam game (./manage.sh steam-add-nonsteam-game) # has no file to actually point Exe at from inside this container. - f'{games}/emulators:/home/retro/Applications:rw'], + f'{games}/emulators:/home/retro/Applications:rw', + # Same roms/saves/bios/retro-home/retroarch mounts as esde/ + # retroarch below — without these, a standalone ES-DE or + # RetroArch AppImage added here as a non-Steam game (same + # mechanism as Cemu above) would see none of the ROMs, cores, + # save states, BIOS files, or ES-DE's own settings/custom + # systems (TI-99, Wii U AntiMicroX command) that the esde/ + # retroarch containers already have — it'd start from a + # completely empty config instead of reusing what's already + # set up. Every path here is the exact same host directory + # those two containers mount, just also visible from Steam. + f'{games}/roms:/ROMs:rw', + f'{games}/saves:/mnt/games/saves:rw', + f'{games}/media:/media:rw', + f'{games}/bios:/home/retro/bioses:rw', + f'{games}/retro-home:/home/retro/.config:rw', + f'{games}/retro-home-data:/home/retro/.local/share:rw', + f'{games}/retroarch:/home/retro/.config/retroarch:rw', + f'{games}/esde-custom-systems:/home/retro/ES-DE/custom_systems:rw', + f'{games}/esde-settings:/home/retro/ES-DE/settings:rw'], env=['PROTON_LOG=1', 'RUN_SWAY=true', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'], cap_add=['SYS_ADMIN', 'SYS_NICE', 'SYS_PTRACE', 'NET_RAW', 'MKNOD', 'NET_ADMIN'], @@ -3554,7 +3730,7 @@ _manage_wolf_complete() { local commands="start stop restart logs status pin controllers update apps cores reorder add-web ge-proton games setup-swbf2 fix-ea-game wait-ea-app install-ea-app diagnose-ea fix-perms install-completion backup - steam-add-nonsteam-game cemu-clone-controller cemu-sync-controllers" + steam-add-nonsteam-game steam-setup-frontends cemu-clone-controller cemu-sync-controllers" COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) } # Register for both 'manage.sh' and './manage.sh' invocation styles @@ -4014,6 +4190,75 @@ VDFPY echo "'Force the use of a specific Steam Play compatibility tool' is OFF — Cemu and" echo "other native Linux AppImages don't run through Proton." ;; + steam-setup-frontends) + # One command covering what the ES-DE/RetroArch AppImage download + # step in setup.sh can't finish on its own: that step runs before + # Wolf/Steam containers even exist, so it can only download the + # AppImages and print instructions. This picks up from there — + # start Steam if needed, WAIT for it to be signed in (Steam Guard's + # QR-code sign-in itself can't be scripted: it needs a phone + # approving a prompt, so this only polls for the result, never + # performs the sign-in), then add whichever AppImage(s) already got + # downloaded as Steam non-Steam games via the existing + # steam-add-nonsteam-game command above (re-invoked, not + # reimplemented, so the two never drift apart). + if ! docker ps --format '{{.Names}}' | grep -qi WolfSteam; then + echo "Starting Wolf (docker compose up -d)..." + docker compose up -d + sleep 5 + fi + + _SSF_SIGNED_IN() { + local _home + _home=$(_steam_home) + [ -n "$_home" ] && [ -n "$(sudo ls "$_home/.steam/steam/userdata/" 2>/dev/null)" ] + } + + if _SSF_SIGNED_IN; then + echo "Steam is already signed in — proceeding." + else + echo "" + echo "Steam isn't signed in yet. In Moonlight:" + echo " 1. Connect to the Steam app." + echo " 2. On Steam's login screen, choose 'Sign in with QR code'." + echo " 3. Scan it with your phone's Steam app and approve the prompt." + echo "" + echo "Waiting for sign-in (up to 10 minutes, checking every 5s — Ctrl+C to give up" + echo "and finish this later by re-running './manage.sh steam-setup-frontends')..." + _SSF_WAITED=0 + until _SSF_SIGNED_IN; do + sleep 5 + _SSF_WAITED=$((_SSF_WAITED + 5)) + if [ "$_SSF_WAITED" -ge 600 ]; then + echo "Still not signed in after 10 minutes — giving up for now." + exit 1 + fi + done + echo "Signed in." + fi + + GAME_DIR=$(grep '^GAME_STORAGE_DIR=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2-) + if [ -z "$GAME_DIR" ]; then read -r -p " Game storage path: " GAME_DIR; fi + _SSF_EMU_DIR="$GAME_DIR/emulators" + _SSF_ADDED_ANY=0 + if [ -f "$_SSF_EMU_DIR/ES-DE.AppImage" ]; then + "$0" steam-add-nonsteam-game es-de "EmulationStation (ES-DE)" && _SSF_ADDED_ANY=1 + else + echo "ES-DE.AppImage not found in $_SSF_EMU_DIR — download it first: sudo ./setup.sh wolf" + fi + if [ -f "$_SSF_EMU_DIR/RetroArch.AppImage" ]; then + "$0" steam-add-nonsteam-game retroarch "RetroArch" && _SSF_ADDED_ANY=1 + else + echo "RetroArch.AppImage not found in $_SSF_EMU_DIR — download it first: sudo ./setup.sh wolf" + fi + + if [ "$_SSF_ADDED_ANY" = 1 ]; then + echo "" + echo "Cores/shaders/overlays live in the same retroarch/ directory the esde/retroarch apps" + echo "already use (shared mount — nothing new to configure there). If you haven't already:" + echo " ./manage.sh cores all (downloads every libretro core + shaders/overlays/database)" + fi + ;; cemu-clone-controller) # Clones a WORKING Cemu controller mapping onto a new device slot, # skipping Cemu's own Input Settings dialog entirely for that slot. @@ -4327,6 +4572,8 @@ SYNCPY echo " ./manage.sh install-completion - Enable tab-completion for this script" echo " ./manage.sh steam-add-nonsteam-game [name] [display name]" echo " - Add an emulator (emulators/) as a non-Steam game, no GUI needed" + echo " ./manage.sh steam-setup-frontends - Wait for Steam sign-in (QR code via Moonlight), then add" + echo " downloaded ES-DE/RetroArch AppImages to Steam" echo " ./manage.sh cemu-clone-controller [slot 0-3] [uuid] [display name]" echo " - Clone a working Cemu controller mapping onto a new device slot" echo " ./manage.sh cemu-sync-controllers - Auto-detect connected controllers via SDL and clone mappings onto all of them" @@ -4929,6 +5176,69 @@ container-created virtual controllers specifically (real hardware behaves this way; whether Steam Input sees Wolf's virtual joypads the same way is still to be tested) — worth trying before assuming it works. +## Running ES-DE and/or RetroArch through Steam instead of their own Wolf apps +The same Steam Input reasoning above applies beyond just Cemu: ES-DE and +RetroArch both ship their own official standalone Linux AppImages +(separate from the \`esde\`/\`retroarch\` Wolf catalog apps this installer +already runs), and adding one of those to Steam gets you the same +per-device controller assignment for every system it covers, not just +Cemu. \`sudo ./setup.sh wolf\` offers to download both (opt-in, default +no) right after the Cemu step — ES-DE's from its GitLab releases (it +isn't on GitHub), RetroArch's from \`hizzlekizzle/RetroArch-AppImage\` (a +well-regarded third-party nightly build — libretro.org's own buildbot +doesn't publish through an API this installer can automate against). +Both land in \`emulators/\` under a fixed name (\`ES-DE.AppImage\` / +\`RetroArch.AppImage\`) regardless of the real release asset's own +filename, so Steam's non-Steam-game matching finds them reliably. + +They reuse the exact same \`roms/\`, \`saves/\`, \`bios/\`, \`retro-home\`, and +\`retroarch\` (cores/shaders/overlays) directories the \`esde\`/\`retroarch\` +containers already use — the \`steam\` Wolf app mounts all of the same +paths, so nothing needs re-downloading or re-scraping just because it's +now also reachable from Steam. If you haven't already populated cores: +\`\`\`bash +cd $WOLF_DIR && ./manage.sh cores all +\`\`\` + +Finishing the Steam side needs one thing that can't be scripted — Steam +Guard's QR-code sign-in requires a phone approving a prompt — so this +polls for it instead of trying to script past it: +\`\`\`bash +cd $WOLF_DIR && ./manage.sh steam-setup-frontends +\`\`\` +Starts Wolf if it isn't already up, checks whether Steam's already +signed in (proceeds immediately if so), otherwise prints the QR-code +steps and waits (up to 10 minutes) for sign-in to complete, then adds +whichever of ES-DE.AppImage/RetroArch.AppImage was downloaded as a Steam +non-Steam game — re-using \`steam-add-nonsteam-game\` above rather than +duplicating its shortcuts.vdf-writing logic. Safe to re-run any time +(e.g. if it timed out waiting, or you downloaded the second AppImage +later) — already-added shortcuts are updated in place, not duplicated. + +**Returning to Steam from ES-DE, without a second instance:** if ES-DE +is running as a Steam non-Steam game (via the AppImage above, not the +separate \`esde\` Wolf app), Steam is the parent process the whole +time — quitting ES-DE drops you back into the same still-running Steam +Big Picture session rather than starting a new one. This only works for +the AppImage-in-Steam path; the standalone \`esde\` Wolf app is a +completely separate container, and Wolf has no supported way to hand off +from one running app to another mid-session (switching apps means +closing the Moonlight session and reconnecting to the other one). + +**Multiple devices, same Steam account, same controller mappings?** +Worth knowing before relying on it: Wolf gives each *paired client* its +own separate Steam container/home directory (confirmed against this +repo's own \`_steam_home()\` — it searches across multiple +\`.../Steam\` directories, not just one), so a second device connecting +to Wolf doesn't reuse the first device's Steam install and has to sign +in separately the first time. Once it's signed into the *same* Steam +account, Valve's own account-level Steam Cloud config sync should +replicate your Steam Input controller bindings across those separate +local installs (the same mechanism that syncs bindings between a Steam +Deck and a gaming PC) — but that's a Steam-account feature, not +something Wolf or this installer controls, and hasn't been confirmed +live in this specific setup. + ## Multiple controllers (same game/emulator can't tell them apart) **Symptom:** two or more controllers connected through the same Moonlight session, but the game/emulator only ever sees one — the first controller