From c57f760fdc25b91d8c9565bb8f07601806bdb64e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 18:03:45 +0000 Subject: [PATCH 1/2] manage.sh controllers: collapse duplicate-paired devices to one entry Live user feedback: being asked to pick between 6 numbered entries that were all the exact same device (client_id repeated 6x from re-pairing) was genuinely confusing, especially right when the user was already trying to get to the controller-type poll further down the flow. The picker now dedupes to distinct client_ids only, first-occurrence order, tagging a collapsed entry "(paired Nx)" - matches what Wolf's own get_client_by_id() actually resolves to anyway (first match for a given id), so nothing is lost by not offering the later duplicates as separate choices. Verified against the user's real 8-entry (6 duplicate + 2 unique) client list, both with and without an active session, confirming the controller log-poll still runs correctly right after selection in both cases. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6 --- services/wolf.sh | 52 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/services/wolf.sh b/services/wolf.sh index 6d0d9f4..e587926 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -3423,8 +3423,24 @@ COMPEOF # recognize ("oh, that's my gaming PC"). SESSIONS_JSON=$(sudo curl -fsS --unix-socket "$SOCK" http://localhost/api/v1/sessions 2>/dev/null) + # Wolf doesn't dedupe repeated pairings of the same device (confirmed + # live: re-pairing the same PC 6 times over produced 6 separate list + # entries, all with the identical client_id) — and Wolf's own + # get_client_by_id() (config.hpp) resolves by taking the FIRST match + # for a given id anyway, so every duplicate beyond the first is a + # dead ringer with no functional difference. Rather than force a + # choice between 6 identical-outcome options (confirmed live: this + # was a real, confusing thing to be asked to do), the picker only + # ever shows DISTINCT client_ids, first-occurrence order — matching + # what Wolf itself would actually resolve to regardless of which + # duplicate got picked. CLIENT_IDS=() - while IFS= read -r cid; do CLIENT_IDS+=("$cid"); done < <(echo "$CLIENTS_JSON" | python3 -c " + while IFS= read -r cid; do + for existing in "${CLIENT_IDS[@]}"; do + [ "$existing" = "$cid" ] && continue 2 + done + CLIENT_IDS+=("$cid") + done < <(echo "$CLIENTS_JSON" | python3 -c " import json, sys for c in json.load(sys.stdin)['clients']: print(c['client_id']) @@ -3435,7 +3451,7 @@ for c in json.load(sys.stdin)['clients']: fi echo "" - echo "Paired clients (ACTIVE = streaming from this IP right now — the" + echo "Paired devices (ACTIVE = streaming from this IP right now — the" echo "reliable way to tell which entry is yours. If nothing shows" echo "ACTIVE, start streaming from the device you want to configure," echo "leave it connected, and re-run this in another terminal):" @@ -3446,21 +3462,21 @@ try: active = {s['client_id']: s['client_ip'] for s in json.loads(sys.argv[1]).get('sessions', []) if s.get('client_id')} except Exception: active = {} -for i, c in enumerate(d['clients']): - ov = c['settings'].get('controllers_override') or [] +seen = [] +counts = {} +for c in d['clients']: cid = c['client_id'] + counts[cid] = counts.get(cid, 0) + 1 + if cid not in seen: + seen.append(cid) +for i, cid in enumerate(seen): + first = next(c for c in d['clients'] if c['client_id'] == cid) + ov = first['settings'].get('controllers_override') or [] tag = 'ACTIVE - streaming from ' + active[cid] if cid in active else 'not currently connected' - print(f\" {i+1}) {cid} [{tag}]\") + dup = f\" (paired {counts[cid]}x)\" if counts[cid] > 1 else '' + print(f\" {i+1}) {cid}{dup} [{tag}]\") print(f\" current override: {ov if ov else 'none - auto-detect'}\") " "$SESSIONS_JSON" - DUP_COUNT=$(printf '%s\n' "${CLIENT_IDS[@]}" | sort | uniq -d | wc -l) - if [ "$DUP_COUNT" -gt 0 ]; then - echo "" - echo "Note: some client_ids above are repeated — the same device was" - echo "paired more than once (each pairing gets its own entry, Wolf" - echo "doesn't dedupe). Harmless, but if you want a clean list, forget" - echo "/ re-pair this PC from Moonlight's own settings on the client." - fi ACTIVE_CLIENT_ID=$(python3 -c " import json, sys @@ -4246,10 +4262,12 @@ is actively streaming right now with its real IP address, e.g. it's used automatically with no prompt. **For the clearest result: start streaming from the device you want to configure, leave it connected, and run this command while it's still connected** — then there's no -guessing. It's also normal to see the same client_id listed more than -once (Wolf doesn't dedupe repeated pairings of the same device) — every -duplicate of an active ID is tagged ACTIVE together, so it doesn't matter -which one you'd have picked by hand. +guessing. Wolf doesn't dedupe repeated pairings of the same device — if +you've paired the same PC more than once, this tool collapses those +into a single listed entry tagged e.g. \`(paired 6x)\` instead of forcing +a choice between several options that all resolve identically (confirmed +live: being asked to pick between 6 identical-outcome entries was +genuinely confusing before this). **It labels controllers "1st"/"2nd"/"3rd"/"4th" — always meaning Wolf's own "controller 0"/"controller 1"/"controller 2"/"controller 3".** Wolf's own From 52207598b90b0f802894569d0cb31a556ade5b99 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 18:15:50 +0000 Subject: [PATCH 2/2] Fix real regression: ES-DE Run-in-background patch broke ROM discovery Confirmed live on a real install: after the previous commit, ES-DE found zero games on every system. Root-caused by walking GOW's own apps/es-de/build/scripts/startup.sh line by line: GOW's image bakes a FULL es_settings.xml template (100+ settings, including ROMDirectory=/ROMs, confirmed against the real template file in the games-on-whales/gow repo) and its own startup script copies that template into place exactly once, the very first time the container starts, gated on the settings file not already existing. The previous fix pre-created a minimal stub file (containing only RunInBackground) before the container had ever started - GOW's startup script saw that file "already there", skipped writing its own template entirely, and ES-DE fell back to vanilla upstream defaults for everything else in that file, including ROMDirectory, which is not /ROMs by default. Every system's games vanished as a result. Also corrects an earlier wrong claim in both the code comment and README: ES-DE's own *compiled* default for RunInBackground is false, but GOW's own template deliberately sets it to true - that's the real, confirmed source of the original background-launch bug, not an accidental toggle as previously assumed. Fix: only ever patch an es_settings.xml that already exists with real content (`[ -s "$_ESDE_SETTINGS" ]`) - never create one. On a genuinely fresh install, before EmulationStation has ever launched once, the installer now says so explicitly and defers rather than guessing, with clear instructions to connect once and re-run. Verified against GOW's actual real template: the patch flips just RunInBackground (line count unchanged, ROMDirectory and everything else untouched), is idempotent on re-run, and the skip-when-absent gate was verified directly against both the missing-file and populated-file cases. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6 --- services/wolf.sh | 109 +++++++++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/services/wolf.sh b/services/wolf.sh index e587926..7e34ae7 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -325,7 +325,8 @@ EOF echo " and Wii U (Cemu) in ES-DE via a second, opt-in 'Alternative emulators' command" echo " - Expose Wolf's REST API socket to the host (WOLF_SOCKET_PATH + /var/run/wolf mount)" echo " so './manage.sh controllers' can force distinct pad types per controller slot" - echo " - Force ES-DE's 'Run in background' off durably (mounts + writes esde-settings/es_settings.xml)" + echo " - Mount ~/ES-DE/settings durably + force 'Run in background' off (only once" + echo " GOW's own es_settings.xml template already exists — never pre-created)" return 0 fi @@ -1392,48 +1393,56 @@ PS2XMLPY log_success "PS2: added PCEE2 as the default RetroArch core (custom_systems/es_systems.xml) — ES-DE's" log_info "own build doesn't ship it yet (added upstream in the not-yet-released 3.5.0)" - # ── ES-DE: force "Run in background" off ──────────────────────────────── - # ES-DE's own compiled default for this IS already off (confirmed against - # its real Settings.cpp: mBoolMap["RunInBackground"] = {false, false}) — - # this write exists purely so that default stays durable and explicit - # rather than relying on it never having been toggled. Real, confirmed - # cause of a genuine bug otherwise: with it on, ES-DE keeps running and - # listening to every controller even after a game/emulator has launched - # and taken visual focus — a second controller's input can still reach - # ES-DE's own menu in the background and launch (and start playing audio - # for) a completely different game while the first one is still up - # front. ES-DE's own USERGUIDE.md explicitly names this failure mode - # ("make sure that the setting Run in background... is disabled"). - # Confirmed live: this affects any system, not just Cemu. + # ── ES-DE: force "Run in background" off (once GOW's own template exists) ── + # GOW's own image bakes a FULL es_settings.xml template (100+ settings — + # confirmed against apps/es-de/build/configs/es/es_settings.xml in the + # games-on-whales/gow repo) that deliberately sets RunInBackground=true + # (NOT ES-DE's own compiled default, which is false — GOW's template is + # what actually causes the real, confirmed cross-system bug this exists + # to fix: with it on, ES-DE keeps listening to every controller even + # after a game/emulator has focus, so a second controller's input can + # reach ES-DE's own menu and launch a different game underneath whatever + # is already running). GOW's own startup.sh copies that template exactly + # ONCE, the very first time the container starts, gated on + # `test -f $ES_CFG_DIR/settings/es_settings.xml` — if a file is already + # sitting there, GOW's own copy is skipped entirely and ES-DE falls back + # to its own vanilla defaults for EVERYTHING ELSE in that file, including + # ROMDirectory (GOW's template sets it to the /ROMs this whole repo + # mounts games at; ES-DE's own default is not that). # - # es_settings.xml is the user's OWN full settings state (theme, scraper - # prefs, everything) — unlike es_systems.xml this is never a full - # rewrite, only a surgical strip-and-reappend of this one entry, - # same idempotent pattern as everywhere else in this file. Written as a - # plain top-level element with no wrapper, matching what - # ES-DE's own Settings::saveFile() actually produces today (confirmed - # against its source — the "wrap everything in " format is - # loader-side forward-compatibility for a future ES-DE release, not - # what gets written now); either format loads fine either way. + # CONFIRMED LIVE, THE HARD WAY: an earlier version of this exact step + # pre-created a MINIMAL stub file (containing only the RunInBackground + # line) before the container had ever started — GOW's startup.sh saw + # that file "already existing", skipped its own copy, and ES-DE's + # ROMDirectory silently fell back to a default that isn't /ROMs, finding + # zero games on every single system. Root-caused by walking GOW's own + # startup.sh line by line against a real "ES-DE finds no game files" + # report. Never pre-create this file — only ever patch an ALREADY + # existing one (i.e. one GOW's own bootstrap has already populated in + # full, meaning ES-DE has been launched via Moonlight at least once). mkdir -p "$GAME_STORAGE_DIR/esde-settings" _ESDE_SETTINGS="$GAME_STORAGE_DIR/esde-settings/es_settings.xml" - backup_if_exists "$_ESDE_SETTINGS" - python3 - "$_ESDE_SETTINGS" << 'ESDESETTINGSPY' + if [ -s "$_ESDE_SETTINGS" ]; then + backup_if_exists "$_ESDE_SETTINGS" + python3 - "$_ESDE_SETTINGS" << 'ESDESETTINGSPY' import re, sys path = sys.argv[1] -try: - with open(path) as f: - content = f.read() -except FileNotFoundError: - content = '' +with open(path) as f: + content = f.read() content = re.sub(r'[ \t]*[ \t]*\n?', '', content) -line = ' \n' -content = (content.rstrip('\n') + '\n' + line) if content.strip() else line +line = '\n' +content = content.rstrip('\n') + '\n' + line with open(path, 'w') as f: f.write(content) ESDESETTINGSPY - chown "$ACTUAL_USER:$ACTUAL_USER" "$_ESDE_SETTINGS" - log_success "ES-DE: 'Run in background (while game is launched)' forced off (esde-settings/es_settings.xml)" + chown "$ACTUAL_USER:$ACTUAL_USER" "$_ESDE_SETTINGS" + log_success "ES-DE: 'Run in background (while game is launched)' forced off (esde-settings/es_settings.xml)" + else + log_info "ES-DE: 'Run in background' fix deferred — esde-settings/es_settings.xml doesn't" + log_info "exist yet (GOW writes its real, full template the first time EmulationStation" + log_info "actually launches). Connect to it via Moonlight once, then re-run" + log_info "'sudo ./setup.sh wolf' to apply this fix without touching anything else." + fi # ── App selection ───────────────────────────────────────────────────────── echo "" @@ -4359,15 +4368,31 @@ playing underneath whatever already has focus. setting. When it's on, ES-DE keeps running and listening to every controller even after something else has launched and taken visual focus — ES-DE's own USERGUIDE.md names this exact failure mode directly. -ES-DE's compiled default for this is already off, but nothing durably -enforced that in this setup before now, so it's easy for it to have -gotten toggled on at some point and stick that way. +ES-DE's own *compiled* default for this is off — but GOW's own bundled +\`es_settings.xml\` template (baked into \`ghcr.io/games-on-whales/es-de\`) +deliberately sets it **on**, which is the actual, confirmed source of the +default behavior here. -**This installer now forces it off automatically** (\`esde-settings/es_settings.xml\`, -written fresh on every install/reinstall) — no menu digging required. If -you're on an install from before this existed, re-run \`sudo ./setup.sh wolf\` -to pick it up, or set it by hand: **Main Menu → Other Settings → Run in -background (while game is launched) → off**. +**This installer forces it back off** by patching \`esde-settings/es_settings.xml\` +— but only an *already-populated* copy of that file, never by creating one +itself. That distinction matters: GOW's own container startup script only +writes its real template (100+ settings, including where ES-DE looks for +your ROMs) the very first time EmulationStation actually launches, gated +on that file not already existing — pre-creating even a minimal version of +it ourselves, before that first launch, would make GOW skip writing its +own template entirely and leave ES-DE on vanilla upstream defaults for +everything else in that file. **Confirmed live, the hard way:** an earlier +version of this exact fix did precisely that, and ROM discovery broke +completely (every system, not just one) because ES-DE's ROM path fell +back to something other than \`/ROMs\`. + +So in practice: if you've already launched EmulationStation via Moonlight +at least once, re-running \`sudo ./setup.sh wolf\` applies this immediately. +**If you haven't yet** (a genuinely fresh install), the installer says so +and skips the patch rather than guessing — connect to EmulationStation +once first, then re-run \`sudo ./setup.sh wolf\`. Setting it by hand works +too, any time: **Main Menu → Other Settings → Run in background (while +game is launched) → off**. **Why this needed its own mount, not just a one-time write:** \`~/ES-DE\` (settings, gamelists, scraped artwork, logs) had no bind mount onto the