Compare commits
12
Commits
33e4f64d69
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39387b5e0f | ||
|
|
e67ac50c61 | ||
|
|
3ebbeba672 | ||
|
|
07394769ca | ||
|
|
ae939c4085 | ||
|
|
28996eff57 | ||
|
|
93efe0d607 | ||
|
|
79861c72f3 | ||
|
|
2422ce1385 | ||
|
|
6fc6c3b84d | ||
|
|
e6522eadec | ||
|
|
2dcfaafc87 |
+641
-2
@@ -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"
|
||||
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,334 @@ 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.
|
||||
#
|
||||
# Why this is safe, not just convenient: confirmed against two real,
|
||||
# separately-hand-mapped profiles captured live (Nintendo Switch Pro
|
||||
# on controller0.xml, Xbox One S on controller1.xml) — both contain
|
||||
# the exact same 24 <mapping>/<button> pairs, just listed in a
|
||||
# different order. That's real proof (not the README's word for
|
||||
# it) that Cemu's <mappings> block uses universal
|
||||
# SDL_CONTROLLER_BUTTON_* semantics, not per-device raw indices, and
|
||||
# that Cemu doesn't care about entry order — so a proven-working
|
||||
# block can be dropped into a new device's file wholesale.
|
||||
#
|
||||
# This still needs ONE real, Cemu-written <uuid> per new device
|
||||
# type first — that value isn't something this script can compute
|
||||
# (it depends on the CRC16-of-device-name half of the SDL GUID,
|
||||
# which SDL derives from the exact joystick name string the device
|
||||
# reports at runtime — guessing it risks writing a uuid Cemu will
|
||||
# never actually match against the live device). Get it the
|
||||
# cheapest possible way: in Cemu's Input Settings for the new
|
||||
# controller slot, click + and select the device — even mapping
|
||||
# ONE button and hitting Save is enough, since only the <uuid> and
|
||||
# <display_name> this writes are kept; the whole <mappings> block
|
||||
# gets replaced by this command anyway.
|
||||
if [ -z "${3:-}" ]; then
|
||||
echo "Usage: ./manage.sh cemu-clone-controller <target-slot 0-3> <uuid> [display name]"
|
||||
echo ""
|
||||
echo " <target-slot> Which controllerN.xml to write (0-indexed, same as"
|
||||
echo " Cemu's own file naming and Wolf's own controller"
|
||||
echo " numbering — controller0.xml is Cemu's Controller 1)."
|
||||
echo " <uuid> The REAL uuid Cemu wrote for the new device — get this"
|
||||
echo " by adding it in Cemu's Input Settings once (map just one"
|
||||
echo " button, Save), then:"
|
||||
echo " cat controllerProfiles/controllerN.xml | grep uuid"
|
||||
echo " [display name] Cosmetic only. Defaults to the source profile's own name."
|
||||
exit 1
|
||||
fi
|
||||
_CCC_SLOT="${2:-}"
|
||||
_CCC_UUID="${3:-}"
|
||||
_CCC_NAME="${4:-}"
|
||||
|
||||
case "$_CCC_SLOT" in
|
||||
0|1|2|3) ;;
|
||||
*) echo "Target slot must be 0, 1, 2, or 3 (Cemu's own Controller 1-4, 0-indexed)."; exit 1 ;;
|
||||
esac
|
||||
case "$_CCC_UUID" in
|
||||
0_????????????????????????????????) ;;
|
||||
*) echo "That doesn't look like a real Cemu uuid (expected '0_' + 32 hex chars)."
|
||||
echo "Got: $_CCC_UUID"
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
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
|
||||
if [ -z "$GAME_DIR" ]; then echo "No game storage path."; exit 1; fi
|
||||
_CCC_DIR="$GAME_DIR/retro-home/Cemu/controllerProfiles"
|
||||
if [ ! -d "$_CCC_DIR" ]; then
|
||||
echo "No controllerProfiles/ yet at $_CCC_DIR — set up at least one controller"
|
||||
echo "in Cemu's own Input Settings first (Controller 1), so there's a real,"
|
||||
echo "working profile to clone the mapping from."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pick any existing, non-empty controllerN.xml as the mapping source —
|
||||
# prefer controller0.xml (Cemu's own Controller 1), since that's the
|
||||
# slot people set up first, but fall back to whichever one actually
|
||||
# exists so this isn't hard-wired to slot 0 specifically.
|
||||
_CCC_SRC=""
|
||||
for _n in 0 1 2 3; do
|
||||
_cand="$_CCC_DIR/controller${_n}.xml"
|
||||
if [ -s "$_cand" ] && [ "$_n" != "$_CCC_SLOT" ]; then
|
||||
_CCC_SRC="$_cand"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$_CCC_SRC" ]; then
|
||||
echo "No other working controllerN.xml found in $_CCC_DIR to clone from."
|
||||
echo "Set up at least one controller fully in Cemu's own Input Settings first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_CCC_TARGET="$_CCC_DIR/controller${_CCC_SLOT}.xml"
|
||||
[ -f "$_CCC_TARGET" ] && cp "$_CCC_TARGET" "$_CCC_TARGET.bak.$(date +%s)"
|
||||
|
||||
if [ -z "$_CCC_NAME" ]; then
|
||||
_CCC_NAME=$(grep -o '<display_name>[^<]*</display_name>' "$_CCC_SRC" | head -1 | sed -E 's#</?display_name>##g')
|
||||
fi
|
||||
|
||||
python3 - "$_CCC_SRC" "$_CCC_TARGET" "$_CCC_UUID" "$_CCC_NAME" << 'CLONEPY'
|
||||
import re, sys
|
||||
src_path, out_path, uuid, display_name = sys.argv[1:5]
|
||||
with open(src_path) as f:
|
||||
template = f.read()
|
||||
new = re.sub(r'<uuid>[^<]*</uuid>', f'<uuid>{uuid}</uuid>', template, count=1)
|
||||
new = re.sub(r'<display_name>[^<]*</display_name>', f'<display_name>{display_name}</display_name>', new, count=1)
|
||||
with open(out_path, 'w') as f:
|
||||
f.write(new)
|
||||
print(f"Wrote {out_path}")
|
||||
print(f" uuid: {uuid}")
|
||||
print(f" display_name: {display_name}")
|
||||
CLONEPY
|
||||
chown 1000:1000 "$_CCC_TARGET"
|
||||
|
||||
echo ""
|
||||
echo "Cloned the mapping from $(basename "$_CCC_SRC") onto controller${_CCC_SLOT}.xml."
|
||||
echo "Close Cemu COMPLETELY and relaunch for this to take effect — a hand-edited"
|
||||
echo "profile file isn't picked up by an already-running Cemu."
|
||||
;;
|
||||
cemu-sync-controllers)
|
||||
# Fully automates cemu-clone-controller above: queries SDL directly
|
||||
# (the exact same library Cemu itself links against) for the REAL
|
||||
# live GUID of every controller currently connected to the active
|
||||
# session's app container, then clones a proven-working mapping
|
||||
# onto each one's controllerN.xml — no Cemu GUI interaction at all,
|
||||
# not even the single-button-and-Save step cemu-clone-controller
|
||||
# still needs to learn a brand-new device's uuid.
|
||||
#
|
||||
# CONFIRMED LIVE: querying SDL_JoystickGetDeviceGUID() this way
|
||||
# inside a running ES-DE session reproduced the EXACT uuids Cemu
|
||||
# had already written by hand for two different real controllers
|
||||
# (Nintendo Switch Pro and Xbox One S) — byte-for-byte identical to
|
||||
# controllerProfiles/controller0.xml and controller1.xml. SDL's own
|
||||
# library is the authoritative source for this value — an earlier
|
||||
# attempt to reverse-engineer just the CRC16 portion of the GUID by
|
||||
# hand, tried against 7 different CRC16 variants, matched neither
|
||||
# device. This sidesteps that entirely by asking the same library
|
||||
# Cemu itself calls, instead of recomputing what it would return.
|
||||
#
|
||||
# Wolf's own controller numbering (0-indexed) matched SDL's
|
||||
# enumeration order for that session 1:1 in testing (index 0 was
|
||||
# the controller Wolf logged as "controller 0", index 1 was
|
||||
# "controller 1") — so the Nth joystick SDL reports maps directly
|
||||
# onto controllerN.xml, the same file-naming convention used
|
||||
# everywhere else in this script.
|
||||
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
|
||||
if [ -z "$GAME_DIR" ]; then echo "No game storage path."; exit 1; fi
|
||||
_CSC_DIR="$GAME_DIR/retro-home/Cemu/controllerProfiles"
|
||||
if [ ! -d "$_CSC_DIR" ]; then
|
||||
echo "No controllerProfiles/ yet at $_CSC_DIR — set up at least one controller"
|
||||
echo "in Cemu's own Input Settings first (Controller 1), so there's a real,"
|
||||
echo "working profile for this to clone the mapping from."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find whichever currently-running Wolf app container actually has
|
||||
# joysticks wired up — the active session's controllers could be
|
||||
# attached to ES-DE, Desktop, RetroArch, or Steam depending on what
|
||||
# you're connected to right now, so this isn't hard-wired to one app.
|
||||
_CSC_CONTAINER=""
|
||||
for _c in $(docker ps --format '{{.Names}}' | grep -i '^Wolf'); do
|
||||
if docker exec "$_c" test -e /dev/input/js0 2>/dev/null; then
|
||||
_CSC_CONTAINER="$_c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$_CSC_CONTAINER" ]; then
|
||||
echo "No running Wolf app container has any controller attached right now."
|
||||
echo "Connect via Moonlight with your controllers active, then re-run this."
|
||||
exit 1
|
||||
fi
|
||||
echo "Reading live controller GUIDs from $_CSC_CONTAINER..."
|
||||
|
||||
_CSC_PROBE=$(docker exec -i -u retro "$_CSC_CONTAINER" python3 - << 'PROBEPY'
|
||||
import ctypes, ctypes.util, sys
|
||||
|
||||
libname = ctypes.util.find_library("SDL2") or "libSDL2-2.0.so.0"
|
||||
try:
|
||||
sdl = ctypes.CDLL(libname)
|
||||
except OSError as e:
|
||||
print(f"ERROR: could not load libSDL2 ({libname}): {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
class SDL_JoystickGUID(ctypes.Structure):
|
||||
_fields_ = [("data", ctypes.c_uint8 * 16)]
|
||||
|
||||
sdl.SDL_Init.argtypes = [ctypes.c_uint32]
|
||||
sdl.SDL_Init.restype = ctypes.c_int
|
||||
sdl.SDL_NumJoysticks.restype = ctypes.c_int
|
||||
sdl.SDL_JoystickGetDeviceGUID.argtypes = [ctypes.c_int]
|
||||
sdl.SDL_JoystickGetDeviceGUID.restype = SDL_JoystickGUID
|
||||
sdl.SDL_JoystickGetGUIDString.argtypes = [SDL_JoystickGUID, ctypes.c_char_p, ctypes.c_int]
|
||||
sdl.SDL_JoystickNameForIndex.argtypes = [ctypes.c_int]
|
||||
sdl.SDL_JoystickNameForIndex.restype = ctypes.c_char_p
|
||||
|
||||
if sdl.SDL_Init(0x00000200) != 0:
|
||||
print("ERROR: SDL_Init(JOYSTICK) failed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
n = sdl.SDL_NumJoysticks()
|
||||
for i in range(n):
|
||||
guid = sdl.SDL_JoystickGetDeviceGUID(i)
|
||||
buf = ctypes.create_string_buffer(64)
|
||||
sdl.SDL_JoystickGetGUIDString(guid, buf, 64)
|
||||
name = sdl.SDL_JoystickNameForIndex(i)
|
||||
name = name.decode() if name else f"Controller {i}"
|
||||
print(f"{i} 0_{buf.value.decode()} {name}")
|
||||
PROBEPY
|
||||
)
|
||||
if [ -z "$_CSC_PROBE" ]; then
|
||||
echo "SDL reported zero joysticks inside $_CSC_CONTAINER — nothing to sync."
|
||||
echo "Make sure controllers are actually connected and active in this session."
|
||||
exit 1
|
||||
fi
|
||||
echo "$_CSC_PROBE"
|
||||
echo ""
|
||||
|
||||
_CSC_ANY_SYNCED=0
|
||||
while read -r _idx _uuid _live_name; do
|
||||
[ -z "$_idx" ] && continue
|
||||
_target="$_CSC_DIR/controller${_idx}.xml"
|
||||
|
||||
# Already has this exact uuid — nothing to do, don't churn a backup.
|
||||
if [ -f "$_target" ] && grep -qF "<uuid>$_uuid</uuid>" "$_target"; then
|
||||
echo "controller${_idx}.xml already has this device's mapping — skipping."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Pick any OTHER existing, non-empty controllerN.xml as the source.
|
||||
_src=""
|
||||
for _n in 0 1 2 3; do
|
||||
_cand="$_CSC_DIR/controller${_n}.xml"
|
||||
if [ -s "$_cand" ] && [ "$_n" != "$_idx" ]; then
|
||||
_src="$_cand"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$_src" ]; then
|
||||
echo "controller${_idx}.xml: no other working profile to clone from yet — skipping."
|
||||
continue
|
||||
fi
|
||||
|
||||
[ -f "$_target" ] && cp "$_target" "$_target.bak.$(date +%s)"
|
||||
# Use SDL's own live device name if it reported one — more useful
|
||||
# in Cemu's own UI than inheriting whatever device the mapping
|
||||
# template happened to come from.
|
||||
_name="${_live_name:-$(grep -o '<display_name>[^<]*</display_name>' "$_src" | head -1 | sed -E 's#</?display_name>##g')}"
|
||||
|
||||
python3 - "$_src" "$_target" "$_uuid" "$_name" << 'SYNCPY'
|
||||
import re, sys
|
||||
src_path, out_path, uuid, display_name = sys.argv[1:5]
|
||||
with open(src_path) as f:
|
||||
template = f.read()
|
||||
new = re.sub(r'<uuid>[^<]*</uuid>', f'<uuid>{uuid}</uuid>', template, count=1)
|
||||
new = re.sub(r'<display_name>[^<]*</display_name>', f'<display_name>{display_name}</display_name>', new, count=1)
|
||||
with open(out_path, 'w') as f:
|
||||
f.write(new)
|
||||
SYNCPY
|
||||
chown 1000:1000 "$_target"
|
||||
echo "controller${_idx}.xml: synced (uuid $_uuid, cloned from $(basename "$_src"))."
|
||||
_CSC_ANY_SYNCED=1
|
||||
done <<< "$_CSC_PROBE"
|
||||
|
||||
echo ""
|
||||
if [ "$_CSC_ANY_SYNCED" = 1 ]; then
|
||||
echo "Close Cemu COMPLETELY and relaunch for the synced profiles to take effect."
|
||||
fi
|
||||
;;
|
||||
pin)
|
||||
# Wolf logs: "Insert pin at http://SOMEIP:47989/pin/#HEXHASH"
|
||||
# Extract just the hash fragment and build URLs for every interface
|
||||
@@ -4068,6 +4572,11 @@ VDFPY
|
||||
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"
|
||||
echo " ./manage.sh backup - How to set up backups"
|
||||
;;
|
||||
esac
|
||||
@@ -4628,6 +5137,8 @@ cd $WOLF_DIR
|
||||
./manage.sh diagnose-ea # check link2ea:// state when game returns to Play
|
||||
./manage.sh install-completion # enable tab-completion for manage.sh
|
||||
./manage.sh steam-add-nonsteam-game # add an emulator as a non-Steam game, no GUI needed
|
||||
./manage.sh cemu-clone-controller # clone a working Cemu controller mapping onto a new device slot
|
||||
./manage.sh cemu-sync-controllers # auto-detect connected controllers via SDL, clone mappings onto all of them
|
||||
\`\`\`
|
||||
|
||||
### Tab completion
|
||||
@@ -4665,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
|
||||
@@ -5065,6 +5639,33 @@ time Cemu launches from ES-DE too.
|
||||
repo, not just reconnecting) reliably cleared it in testing, giving a
|
||||
fresh PulseAudio session to work with. Reconnect to a fresh Desktop
|
||||
session afterward and try again.
|
||||
|
||||
**If the TV device stays stuck on "Disabled" and won't take a real
|
||||
device at all** (not just a one-time hang), this narrows down whether
|
||||
it's PulseAudio itself with nothing to offer inside the container, or
|
||||
Cemu choking on a device it can actually see:
|
||||
\`\`\`bash
|
||||
# Has this been a full Wolf restart, not just a reconnect, since it broke?
|
||||
sudo ./setup.sh wolf
|
||||
|
||||
# Does PulseAudio inside the Desktop container see any output devices at all?
|
||||
CONTAINER=\$(docker ps --format '{{.Names}}' | grep -i WolfDesktop | head -1)
|
||||
docker exec "\$CONTAINER" pactl list short sinks
|
||||
docker exec "\$CONTAINER" pactl info | grep -i "default sink"
|
||||
|
||||
# Cemu's own log — it logs audio device init/errors directly
|
||||
GAME_DIR=\$(grep '^GAME_STORAGE_DIR=' $WOLF_DIR/.env | cut -d= -f2-)
|
||||
find "\$GAME_DIR/retro-home/Cemu" -iname "log.txt" -exec cat {} \\;
|
||||
|
||||
# What settings.xml currently has for Audio (even while stuck "disabled")
|
||||
cat "\$GAME_DIR/retro-home/Cemu/settings.xml" 2>/dev/null | grep -A5 -i "<Audio"
|
||||
\`\`\`
|
||||
An empty \`pactl list short sinks\` means there's no real fix on Cemu's
|
||||
side to chase — the container itself has no audio sink for anything to
|
||||
attach to. A populated sink list with Cemu still stuck points at Cemu's
|
||||
own device-switch path instead, and \`log.txt\` / the \`<Audio>\` block
|
||||
above are what to paste back for that to get root-caused rather than
|
||||
guessed at.
|
||||
6. **Adding a second controller and the first one ends up driving both
|
||||
characters?** That's not a Cemu-specific bug — see
|
||||
"Multiple controllers" above (\`./manage.sh controllers\`).
|
||||
@@ -5107,6 +5708,44 @@ ambiguity about which one Cemu picks. Back up the file first
|
||||
\`sudo\` needed, these files are written world-writable by Cemu itself.
|
||||
Close Cemu completely and relaunch for a hand-edited profile to take effect.
|
||||
|
||||
**\`./manage.sh cemu-clone-controller\` does this copy for you.** Once
|
||||
you've got 4 distinct controller *types* set up via \`./manage.sh controllers\`
|
||||
(so Cemu can tell the devices apart at all) and one slot fully mapped by
|
||||
hand, adding the rest doesn't need full manual button-mapping — Cemu's own
|
||||
\`<mappings>\` format is universal SDL_CONTROLLER_BUTTON_* semantics, not
|
||||
per-device raw indices, so the same mapping works for every device. All
|
||||
each new slot needs is its own real \`<uuid>\` — get that the cheap way (in
|
||||
Cemu's Input Settings, click **+** on the new controller, select the
|
||||
device, map ONE button, Save; the mapping itself is thrown away), then:
|
||||
\`\`\`bash
|
||||
cd $WOLF_DIR
|
||||
grep uuid "\$(grep '^GAME_STORAGE_DIR=' .env | cut -d= -f2-)/retro-home/Cemu/controllerProfiles/controller2.xml"
|
||||
./manage.sh cemu-clone-controller 2 0_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
\`\`\`
|
||||
Clones a fully-configured slot's mapping onto slot 2 with that device's own
|
||||
uuid — no per-button rebinding, no fighting the cut-off Save button for
|
||||
every single controller. Backs up any existing \`controllerN.xml\` first,
|
||||
and needs Cemu closed and relaunched afterward like any other hand-edit.
|
||||
|
||||
**\`./manage.sh cemu-sync-controllers\` skips the uuid step too — genuinely
|
||||
zero Cemu GUI interaction for any controller.** Instead of getting each
|
||||
new device's uuid from Cemu itself, this asks **SDL directly** — the exact
|
||||
library Cemu links against — for the live GUID of every controller
|
||||
currently connected to your active session, then clones the mapping onto
|
||||
each one automatically. **Confirmed live:** querying SDL this way inside a
|
||||
real session reproduced the *exact* uuids Cemu had already written by hand
|
||||
for two different controllers, byte-for-byte. With your controller types
|
||||
already assigned via \`./manage.sh controllers\` and at least one slot
|
||||
mapped once (by hand, or via \`cemu-clone-controller\` above), just:
|
||||
\`\`\`bash
|
||||
cd $WOLF_DIR && ./manage.sh cemu-sync-controllers
|
||||
\`\`\`
|
||||
Run it any time after connecting with controllers active — already-synced
|
||||
slots are detected and skipped (no needless backups), and it works
|
||||
regardless of which app (ES-DE, Desktop, RetroArch, Steam) currently has
|
||||
the controllers attached, since it looks for whichever running Wolf
|
||||
container actually has joysticks wired up.
|
||||
|
||||
ROMs are mounted at \`/ROMs\` inside Desktop too, so Cemu's own File → Load
|
||||
can browse straight to \`/ROMs/wiiu/\` without needing ES-DE at all.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user