13 Commits
Author SHA1 Message Date
Outis d95bd7fd3c Merge pull request #445 from outis1one/claude/gitea-webhook-base-url-9ofg1n
Fix inactivity sync being skipped when remember_me is unchanged
2026-09-09 09:45:38 -04:00
Claude 3cd9a1ece3 Fix _authelia_set_remember_me skipping inactivity sync on a no-op remember_me
The "already equal" early-exit compared only remember_me against the typed
value, so re-entering an unchanged remember_me (the exact case for anyone
who'd set it before the earlier fix existed) skipped the inactivity write
entirely, leaving inactivity stuck at its old mismatched value. Now only
skips when both keys already match the typed duration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148pWopbt3tEKZWHYuHTb3c
2026-09-09 13:20:32 +00:00
Outis 5c13054cdd Merge pull request #444 from outis1one/claude/gitea-webhook-base-url-9ofg1n
Fix Authelia remember_me not actually keeping sessions alive
2026-09-09 09:16:51 -04:00
Outis 7d5674aad8 Merge pull request #443 from outis1one/claude/wolf-controller-setup-vl05t5
wolf: fix stale command list in the post-install summary, surface the…
2026-09-09 09:16:19 -04:00
Claude 2d82b2b278 Fix Authelia remember_me not actually keeping sessions alive
inactivity (idle timeout) was independent of remember_me and stayed at a
much shorter default (2h), so a long remember_me got silently overridden
by ordinary daily gaps between visits. install_authelia()'s template now
defaults inactivity to match remember_me, and the "Change remember me
duration" menu option now writes both keys together instead of just one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148pWopbt3tEKZWHYuHTb3c
2026-09-08 22:43:20 +00:00
Claude 08a2617b06 wolf: fix stale command list in the post-install summary, surface the Steam Input workflow
The final echo summary still advertised a removed `apps` command and left out
everything added since (cores, backup, controllers, steam-add-nonsteam-game,
steam-setup-frontends, cemu-clone-controller, cemu-sync-controllers,
install-completion, etc.). Also add a short pointer to the Steam-as-4-controller-hub
workflow (documented in depth further down in README.md) right in the install
summary, since it's currently only discoverable by reading the generated README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V99t5754SyXdnMTpVe2ba5
2026-09-07 22:29:55 +00:00
Outis 39387b5e0f Merge pull request #442 from outis1one/claude/wolf-cemu-four-controllers-373xn8
Claude/wolf cemu four controllers 373xn8
2026-09-05 15:00:15 -04:00
Claude e67ac50c61 wolf: document the ES-DE/RetroArch-in-Steam workflow in the generated README
The steam-setup-frontends command and the ES-DE/RetroArch AppImage
download step had no matching section in the ~/docker/wolf/README.md
content this file generates, unlike every other manage.sh command. Adds
one, alongside the existing Cemu-in-Steam section: why you'd want it,
how the mounts/cores are already shared, and the two honest caveats
(returning to Steam from ES-DE only works via this path, and whether
controller mappings sync across separately-paired Wolf clients is
expected but unconfirmed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
2026-09-05 18:58:04 +00:00
Claude 3ebbeba672 wolf: add steam-setup-frontends to wait for Steam sign-in then auto-wire ES-DE/RetroArch
Steam Guard's QR-code sign-in can't be scripted (needs a phone approving
a prompt), so this polls for it instead: starts Wolf if needed, waits
for Steam's userdata/ to appear (or proceeds immediately if already
signed in), then re-invokes the existing steam-add-nonsteam-game command
for whichever of ES-DE.AppImage/RetroArch.AppImage was downloaded during
install. Points to ./manage.sh cores all for the shared cores/shaders/
overlays directory rather than duplicating that download logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
2026-09-04 21:30:24 +00:00
Claude 07394769ca wolf: add optional ES-DE/RetroArch AppImage downloads for Steam Input
Both projects ship official standalone Linux AppImages separate from
the esde/retroarch Wolf catalog containers this repo already runs.
Adding either one to Steam as a non-Steam game (steam-add-nonsteam-game,
now reaching the same roms/saves/bios/retro-home/retroarch mounts as
the esde/retroarch containers) lets Steam Input assign a 4th controller
its own identity by device path, past Wolf's 3-concrete-pad-type ceiling.

ES-DE is hosted on GitLab rather than GitHub, so this adds a GitLab
Releases API counterpart to the existing GitHub-based download helper.
RetroArch's own buildbot doesn't publish through either API, so this
uses hizzlekizzle/RetroArch-AppImage, the community nightly-build
project the AppImage catalogs themselves point to (flagged as
third-party, same treatment this file already gives the Dolphin
community build). Both downloads are opt-in (default no) and symlink
to a fixed filename so steam-add-nonsteam-game's case-sensitive
substring match finds them regardless of the vendor's own asset name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
2026-09-04 21:04:00 +00:00
Claude ae939c4085 wolf: mount ROMs/saves/BIOS/retro-home/retroarch into the Steam container
Lets a manually-downloaded ES-DE or RetroArch AppImage, added via
./manage.sh steam-add-nonsteam-game, see the same library, cores, and
ES-DE settings/custom systems (TI-99, Wii U) the esde/retroarch
containers already have, instead of starting from an empty config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
2026-09-04 14:15:33 +00:00
Outis 28996eff57 Merge pull request #441 from outis1one/claude/wolf-pair-port-conflict-7nz8qg
wolf: fix cemu-sync-controllers silently finding zero joysticks
2026-09-03 22:48:40 -04:00
Claude 93efe0d607 wolf: fix cemu-sync-controllers silently finding zero joysticks
docker exec needs an explicit -i flag to forward stdin into the
container process; without it, the heredoc piped into `python3 -`
never reached the containerized script, which ran empty and printed
nothing. cemu-sync-controllers then misread that empty output as
"SDL reported zero joysticks" — a false negative with a working set of
4 controllers already confirmed live in ES-DE, not an actual SDL or
GUID problem. Root-caused against the user's own live output: the
exact same probe script, run directly (not via the manage.sh command),
found real device nodes (js0-js4) present in the same container at the
same time cemu-sync-controllers reported zero.

Audited every other docker exec call in this file for the same
stdin-via-heredoc pattern; this was the only one missing -i.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
2026-09-04 02:48:08 +00:00
3 changed files with 395 additions and 15 deletions
+20 -1
View File
@@ -623,6 +623,25 @@ in `services/authelia.sh`) — prompts for a new duration (`12h`, `7d`,
Sessions persist through reboots regardless of duration (Redis stores Sessions persist through reboots regardless of duration (Redis stores
session state in a volume). session state in a volume).
**`inactivity` must track `remember_me`, or a long remember_me is a lie.**
`inactivity` is a separate session field — how long a session can sit idle
before Authelia ends it — and it is NOT extended or bypassed by the
"Remember me" checkbox; the two are independent. Confirmed live: a user
set `remember_me: 1y` expecting "won't be asked to log in again for a
year," but the install default left `inactivity` at a much shorter value
(2h at the time), so ordinary daily gaps between visits (overnight, a
workday) ended the session on inactivity grounds well before remember_me
ever came into play — the 1y setting was doing nothing. Fixed at both ends
so this can't recur silently: `install_authelia()`'s own template now sets
`inactivity: 7d`, matching its `remember_me: 7d` default instead of a
shorter one, and `_authelia_set_remember_me()` now writes the SAME new
duration into both keys on every change, not just `remember_me` alone. If
you ever hand-edit `session:` instead of using the menu option, keep
`inactivity` and `remember_me` equal — a mismatch here is exactly the bug
above, not a valid intentional configuration. `expiration` (the cap for a
session that never checked "Remember me") is a legitimately different,
shorter-by-design setting and is untouched by any of this.
**The config key is `remember_me`, not `remember_me_duration`.** Authelia **The config key is `remember_me`, not `remember_me_duration`.** Authelia
renamed it in 4.38; this repo pins `4.39.20`. A stale `remember_me_duration` renamed it in 4.38; this repo pins `4.39.20`. A stale `remember_me_duration`
key doesn't error, Authelia just silently ignores it — confirmed against key doesn't error, Authelia just silently ignores it — confirmed against
@@ -635,7 +654,7 @@ touch this by hand instead of the menu option, the current schema is:
session: session:
secret: 'your-existing-secret' secret: 'your-existing-secret'
expiration: 1h expiration: 1h
inactivity: 5m inactivity: 1y
remember_me: 1y remember_me: 1y
cookies: cookies:
- domain: 'example.com' - domain: 'example.com'
+51 -10
View File
@@ -242,7 +242,8 @@ install_authelia() {
echo " 7) Reconfigure from scratch (regenerates secrets/users — breaks" echo " 7) Reconfigure from scratch (regenerates secrets/users — breaks"
echo " existing sessions for every domain already on this instance)" echo " existing sessions for every domain already on this instance)"
echo " 8) Show who has universal vs. service-scoped access" echo " 8) Show who has universal vs. service-scoped access"
echo " 9) Change \"Remember me\" session duration (stay logged in longer)" echo " 9) Change \"Remember me\" session duration (stay logged in longer — also"
echo " raises the inactivity timeout to match, so it can't cut it short)"
echo " 10) Protect an existing site with this instance (pick a local Caddy site," echo " 10) Protect an existing site with this instance (pick a local Caddy site,"
echo " or type one on a different box — gates it with a login, same as any" echo " or type one on a different box — gates it with a login, same as any"
echo " other service already protected this way)" echo " other service already protected this way)"
@@ -501,7 +502,12 @@ access_control:
session: session:
name: authelia_session name: authelia_session
expiration: 12h expiration: 12h
inactivity: 2h # Matches remember_me below, not a shorter default — an idle timeout
# shorter than remember_me silently cuts a "remembered" session short
# regardless of its own duration. See _authelia_set_remember_me()'s
# comment for the live case this caused. Change both together (that
# function does exactly this) rather than one at a time.
inactivity: 7d
remember_me: 7d remember_me: 7d
cookies: cookies:
- domain: ${AUTHELIA_DOMAIN} - domain: ${AUTHELIA_DOMAIN}
@@ -2383,6 +2389,19 @@ _authelia_report_access_scope() {
# earlier version of this very file's own README section) uses the old # earlier version of this very file's own README section) uses the old
# name, which Authelia would just silently ignore rather than error on. # name, which Authelia would just silently ignore rather than error on.
# #
# Also writes the SAME value into `inactivity` — a separate session field
# (default 2h, set alongside remember_me in install_authelia()'s own
# template) that ends a session after that much idle time regardless of
# remember_me, since it isn't disabled or extended by the "Remember me"
# checkbox. Confirmed live: a user who'd set remember_me to 1y still got
# logged out after ordinary daily gaps (overnight, a workday) because
# inactivity was still sitting at its 2h default — remember_me alone does
# NOT deliver "won't be asked to log in again for the duration I set"
# without this. Tying the two together is what actually delivers that.
# `expiration` (the session cap when "Remember me" is NOT checked) is left
# alone — a shorter default there for an un-remembered session is correct,
# separate behavior, not the same gap.
#
# This only controls AUTHELIA's own session — it does not touch how long # This only controls AUTHELIA's own session — it does not touch how long
# a native-OIDC app's (Gitea/Mealie/ActualBudget) own session/token lasts # a native-OIDC app's (Gitea/Mealie/ActualBudget) own session/token lasts
# after logging in via Authelia. A long remember_me makes re-authenticating # after logging in via Authelia. A long remember_me makes re-authenticating
@@ -2393,27 +2412,48 @@ _authelia_set_remember_me() {
local config_file="$DOCKER_DIR/authelia/config/configuration.yml" local config_file="$DOCKER_DIR/authelia/config/configuration.yml"
[ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; } [ -f "$config_file" ] || { log_warning "No configuration.yml found — install Authelia first."; return 1; }
local current local current current_inactivity
current="$(grep -E '^ remember_me:' "$config_file" | awk '{print $2}' | tr -d "'\"")" current="$(grep -E '^ remember_me:' "$config_file" | awk '{print $2}' | tr -d "'\"")"
current_inactivity="$(grep -E '^ inactivity:' "$config_file" | awk '{print $2}' | tr -d "'\"")"
echo "" echo ""
echo " Current \"remember me\" duration: ${current:-not set}" echo " Current \"remember me\" duration: ${current:-not set} (inactivity timeout: ${current_inactivity:-not set})"
echo " How long a session lasts when someone checks \"Remember me\" at login —" echo " How long a session lasts when someone checks \"Remember me\" at login —"
echo " applies to every domain this Authelia instance protects." echo " applies to every domain this Authelia instance protects. Also sets"
echo " \"inactivity\" (idle timeout) to the same value, so a gap between visits"
echo " shorter than this can't log you out early — otherwise inactivity's own"
echo " separate, much shorter default cuts a long remember_me short."
echo " Examples: 12h, 7d, 1M (month), 1y. Set to -1 to disable Remember Me entirely." echo " Examples: 12h, 7d, 1M (month), 1y. Set to -1 to disable Remember Me entirely."
local new_duration="" local new_duration=""
prompt_text " New duration [${current:-7d}]:" "${current:-7d}" new_duration prompt_text " New duration [${current:-7d}]:" "${current:-7d}" new_duration
if [ -z "$new_duration" ] || [ "$new_duration" = "$current" ]; then if [ -z "$new_duration" ]; then
log_info "No change made." log_info "No change made."
return 0 return 0
fi fi
# Only truly a no-op if BOTH keys already match — remember_me alone
# matching isn't enough to skip, or an install still carrying the old
# mismatched inactivity default (from before this function synced the
# two) could never actually get inactivity fixed by re-entering the
# same remember_me value. Confirmed live: this is exactly what
# happened on a box that had already set remember_me: 1y before this
# sync existed — re-running with "1y" again hit this early return and
# left inactivity untouched.
if [ "$new_duration" = "$current" ] && [ "$new_duration" = "$current_inactivity" ]; then
log_info "No change made — remember_me and inactivity already both ${new_duration}."
return 0
fi
if grep -qE '^ remember_me:' "$config_file"; then if grep -qE '^ remember_me:' "$config_file"; then
sed -i "s/^ remember_me:.*/ remember_me: '${new_duration}'/" "$config_file" sed -i "s/^ remember_me:.*/ remember_me: '${new_duration}'/" "$config_file"
else else
sed -i "/^session:\$/a\\ remember_me: '${new_duration}'" "$config_file" sed -i "/^session:\$/a\\ remember_me: '${new_duration}'" "$config_file"
fi fi
if grep -qE '^ inactivity:' "$config_file"; then
sed -i "s/^ inactivity:.*/ inactivity: '${new_duration}'/" "$config_file"
else
sed -i "/^ remember_me:/a\\ inactivity: '${new_duration}'" "$config_file"
fi
chown 1000:1000 "$config_file" 2>/dev/null || true chown 1000:1000 "$config_file" 2>/dev/null || true
log_success "\"Remember me\" duration set to ${new_duration}." log_success "\"Remember me\" duration and inactivity timeout both set to ${new_duration}."
local restart_auth="" local restart_auth=""
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
@@ -2425,9 +2465,10 @@ _authelia_set_remember_me() {
echo "" echo ""
log_info "Takes effect for NEW logins where \"Remember me\" is checked at Authelia's" log_info "Takes effect for NEW logins where \"Remember me\" is checked at Authelia's"
log_info "login page — existing sessions keep whatever expiration they already had." log_info "login page — existing sessions keep whatever expiration/inactivity they"
log_info "The checkbox itself is already on the login form by default; this only" log_info "already had. The checkbox itself is already on the login form by default;"
log_info "changes how long checking it actually keeps you signed in." log_info "this only changes how long checking it actually keeps you signed in, and"
log_info "stops the separate inactivity timeout from cutting that short."
} }
# Export/import accounts (+ optionally 2FA/session state) — for migrating to # Export/import accounts (+ optionally 2FA/session state) — for migrating to
+324 -4
View File
@@ -285,6 +285,84 @@ print(pick[0]["browser_download_url"] if pick else "")
esac 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() { install_wolf() {
require_docker || return 1 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 "not something this installer can supply. Cemu's own First-Time Setup Wizard covers where"
log_info "to put it once you have one." 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 ──────────────────────────── # ── 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, # 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 # 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.) # (see below) — without it, an emulator AppImage (Cemu, etc.)
# added as a non-Steam game (./manage.sh steam-add-nonsteam-game) # added as a non-Steam game (./manage.sh steam-add-nonsteam-game)
# has no file to actually point Exe at from inside this container. # 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', env=['PROTON_LOG=1', 'RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'], 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'SYS_NICE', 'SYS_PTRACE', 'NET_RAW', 'MKNOD', 'NET_ADMIN'], 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 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 add-web ge-proton games setup-swbf2 fix-ea-game wait-ea-app
install-ea-app diagnose-ea fix-perms install-completion backup 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") ) COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
} }
# Register for both 'manage.sh' and './manage.sh' invocation styles # 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 "'Force the use of a specific Steam Play compatibility tool' is OFF — Cemu and"
echo "other native Linux AppImages don't run through Proton." 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) cemu-clone-controller)
# Clones a WORKING Cemu controller mapping onto a new device slot, # Clones a WORKING Cemu controller mapping onto a new device slot,
# skipping Cemu's own Input Settings dialog entirely for that slot. # skipping Cemu's own Input Settings dialog entirely for that slot.
@@ -4178,7 +4423,7 @@ CLONEPY
fi fi
echo "Reading live controller GUIDs from $_CSC_CONTAINER..." echo "Reading live controller GUIDs from $_CSC_CONTAINER..."
_CSC_PROBE=$(docker exec -u retro "$_CSC_CONTAINER" python3 - << 'PROBEPY' _CSC_PROBE=$(docker exec -i -u retro "$_CSC_CONTAINER" python3 - << 'PROBEPY'
import ctypes, ctypes.util, sys import ctypes, ctypes.util, sys
libname = ctypes.util.find_library("SDL2") or "libSDL2-2.0.so.0" libname = ctypes.util.find_library("SDL2") or "libSDL2-2.0.so.0"
@@ -4327,6 +4572,8 @@ SYNCPY
echo " ./manage.sh install-completion - Enable tab-completion for this script" echo " ./manage.sh install-completion - Enable tab-completion for this script"
echo " ./manage.sh steam-add-nonsteam-game [name] [display name]" echo " ./manage.sh steam-add-nonsteam-game [name] [display name]"
echo " - Add an emulator (emulators/) as a non-Steam game, no GUI needed" 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 " ./manage.sh cemu-clone-controller [slot 0-3] [uuid] [display name]"
echo " - Clone a working Cemu controller mapping onto a new device slot" 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" echo " ./manage.sh cemu-sync-controllers - Auto-detect connected controllers via SDL and clone mappings onto all of them"
@@ -4819,7 +5066,17 @@ PYEOF
echo " Online together → each player launches their own session," echo " Online together → each player launches their own session,"
echo " all connect to the same game server" echo " all connect to the same game server"
echo "" echo ""
echo "Manage: cd $WOLF_DIR && ./manage.sh {start|stop|restart|logs|status|pin|update|apps|reorder|add-web|ge-proton|fix-ea-game}" echo " 4 controllers, one game (e.g. Cemu/Wii U)? Steam Input tells identical"
echo " controllers apart by device path, not just SDL GUID — add the emulator"
echo " to Steam as a non-Steam game instead of launching it from ES-DE:"
echo " ./manage.sh steam-setup-frontends # ES-DE/RetroArch AppImages into Steam"
echo " ./manage.sh steam-add-nonsteam-game cemu"
echo " ./manage.sh cemu-clone-controller / cemu-sync-controllers"
echo " For the ES-DE/RetroArch Wolf apps directly, force distinct virtual pad"
echo " types per slot instead: ./manage.sh controllers (see README.md)"
echo ""
echo "Manage: cd $WOLF_DIR && ./manage.sh {start|stop|restart|logs|status|update|cores|reorder|add-web|backup|ge-proton|fix-ea-game|controllers|steam-add-nonsteam-game|steam-setup-frontends|pin}"
echo " ./manage.sh install-completion # tab-complete every command above"
echo "" echo ""
echo "── EA GAMES (Battlefront II, etc.) ───────────────────" echo "── EA GAMES (Battlefront II, etc.) ───────────────────"
echo "" echo ""
@@ -4929,6 +5186,69 @@ container-created virtual controllers specifically (real hardware behaves
this way; whether Steam Input sees Wolf's virtual joypads the same way is this way; whether Steam Input sees Wolf's virtual joypads the same way is
still to be tested) — worth trying before assuming it works. 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) ## Multiple controllers (same game/emulator can't tell them apart)
**Symptom:** two or more controllers connected through the same Moonlight **Symptom:** two or more controllers connected through the same Moonlight
session, but the game/emulator only ever sees one — the first controller session, but the game/emulator only ever sees one — the first controller