Add ./manage.sh controllers to fix multi-controller disambiguation

Root cause (confirmed against inputtino's own uinput device-creation
source, and against Wolf's real virtual-pad architecture): every
virtual gamepad Wolf creates of the same type gets an identical SDL
GUID, since a GUID identifies a controller model, not a physical
instance - the same behavior two real identical controllers would have.
This isn't a Cemu bug specifically; it's why any app whose own
controller picker keys off GUID (Cemu confirmed live) can't reliably
tell two same-type Wolf pads apart, driving every player from
controller 1.

Wolf's own per-client `controllers_override` setting sidesteps this at
the root by forcing each controller slot to a genuinely different pad
type (e.g. slot 1 = Xbox, slot 2 = PlayStation), so their vendor/product
IDs - and SDL GUIDs - actually differ. `./manage.sh controllers` drives
this via Wolf's real REST API (confirmed against its OpenAPI schema,
not its docs page's own stale example command) rather than hand-editing
config.toml: lists paired clients, prompts for a pad type per slot, and
POSTs the update. Needs Wolf's API socket exposed to the host
(WOLF_SOCKET_PATH + a /var/run/wolf bind mount, matching Wolf's own
documented pattern), added to the wolf service's docker-compose.yml.

Full flow verified with a standalone test harness (mocked curl against
real API response shapes, a real AF_UNIX socket file) covering: single-
vs multi-client selection, invalid selection, invalid slot count,
lowercase/unrecognized type input, and the missing-socket path.

Also documents live user feedback that AntiMicroX (added last session)
is confirmed not working for TI-99/4A - flagged in the README rather
than left silently unverified, with this fix noted as the actively
maintained path for the multi-controller problem instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
This commit is contained in:
Claude
2026-09-03 15:30:26 +00:00
parent 8aaaf993ac
commit 2005534b12
+179 -1
View File
@@ -323,6 +323,8 @@ EOF
echo " symlinked to Dolphin_Emulator.AppImage so ES-DE's own find-rules can see it)"
echo " - Offer AntiMicroX (gamepad -> keyboard/mouse remapping), scoped to just TI-99/4A"
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"
return 0
fi
@@ -1496,11 +1498,21 @@ services:
- WOLF_INTERNAL_MAC=${WOLF_MAC}
- WOLF_RENDER_NODE=${WOLF_RENDER_NODE}
- LD_LIBRARY_PATH=/usr/nvidia/lib:/usr/nvidia/lib32
# Exposes Wolf's REST API socket at this same path on the HOST (matches
# Wolf's own docs' recommended pattern exactly) — without this it only
# exists inside the container at its default \$XDG_RUNTIME_DIR-relative
# path, unreachable from manage.sh. Used by 'manage.sh controllers' to
# set per-client controllers_override (see that command's own comments
# for why this exists — distinguishing multiple same-model virtual
# gamepads, e.g. two Wii U Pro Controllers, so games like Cemu can
# actually tell them apart).
- WOLF_SOCKET_PATH=/var/run/wolf/wolf.sock
volumes:
- \${WOLF_STATE_DIR}:\${WOLF_STATE_DIR}:rw
- /var/run/docker.sock:/var/run/docker.sock:rw
- /dev/:/dev/:rw
- /run/udev:/run/udev:rw
- /var/run/wolf:/var/run/wolf:rw
- nvidia-driver-vol:/usr/nvidia:rw
devices:
- /dev/dri
@@ -3277,7 +3289,7 @@ PYEOF
cat > "$COMP_FILE" << 'COMPEOF'
_manage_wolf_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local commands="start stop restart logs status pin 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
install-ea-app diagnose-ea fix-perms install-completion backup"
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
@@ -3292,6 +3304,123 @@ COMPEOF
echo "Open a new terminal (or run: source \"$COMP_FILE\"), then:"
echo " ./manage.sh <TAB><TAB> shows all commands"
;;
controllers)
# Fixes a real, confirmed root cause — not a bug in Wolf or in any
# single emulator/game: every virtual gamepad Wolf creates of the
# SAME type (e.g. two Wii U Pro Controllers) gets the IDENTICAL SDL
# GUID, because an SDL GUID identifies a controller MODEL, not a
# physical instance — completely normal, expected behavior (two
# real identical physical controllers behave the exact same way).
# Some apps' own controller-picker UI just doesn't reliably tell
# apart two same-GUID devices though (confirmed with Cemu: the
# first controller ends up driving every player). Wolf's own
# per-client controllers_override setting (its docs' "Override the
# default joypad mapping" section) sidesteps this at the root:
# force each controller SLOT to a DIFFERENT pad type (e.g. slot 1 =
# Xbox, slot 2 = PlayStation) so their vendor/product IDs — and
# therefore their SDL GUIDs — genuinely differ. This isn't
# Cemu-specific: it fixes controller disambiguation the same way
# for every system/app that reads SDL joystick GUIDs.
#
# Needs Wolf's REST API socket, which is why the wolf service now
# sets WOLF_SOCKET_PATH and bind-mounts /var/run/wolf to the host
# (Wolf's own docs' recommended pattern for host access). Endpoint
# shapes below are confirmed against Wolf's real OpenAPI schema
# (docs/modules/dev/partials/spec.json in its own repo) — NOT the
# docs PAGE's own example curl command, which is stale: it shows
# "PATCH .../clients/<id>/settings", but the real, current endpoint
# is "POST /api/v1/clients/settings" with client_id in the JSON
# body, not the URL path.
SOCK="/var/run/wolf/wolf.sock"
if [ ! -S "$SOCK" ]; then
echo "Wolf's API socket isn't up at $SOCK yet."
echo " sudo ./setup.sh wolf # regenerates docker-compose.yml with the socket mount"
echo " docker compose up -d # recreates the wolf container so it takes effect"
exit 1
fi
CLIENTS_JSON=$(curl -fsS --unix-socket "$SOCK" http://localhost/api/v1/clients 2>/dev/null)
if [ -z "$CLIENTS_JSON" ]; then
echo "Could not reach Wolf's API — is Wolf running? (./manage.sh status)"
exit 1
fi
CLIENT_IDS=()
while IFS= read -r cid; do CLIENT_IDS+=("$cid"); done < <(echo "$CLIENTS_JSON" | python3 -c "
import json, sys
for c in json.load(sys.stdin)['clients']:
print(c['client_id'])
")
if [ "${#CLIENT_IDS[@]}" -eq 0 ]; then
echo "No paired Moonlight clients yet — pair one first (./manage.sh pin), then re-run this."
exit 1
fi
echo ""
echo "Paired clients:"
echo "$CLIENTS_JSON" | python3 -c "
import json, sys
d = json.load(sys.stdin)
for i, c in enumerate(d['clients']):
ov = c['settings'].get('controllers_override') or []
print(f\" {i+1}) {c['client_id']} (current: {ov if ov else 'none - auto-detect'})\")
"
if [ "${#CLIENT_IDS[@]}" -eq 1 ]; then
CLIENT_ID="${CLIENT_IDS[0]}"
echo ""
echo "Only one paired client — using it."
else
echo ""
read -r -p "Which client [1-${#CLIENT_IDS[@]}]: " PICK
if ! [[ "$PICK" =~ ^[0-9]+$ ]] || [ "$PICK" -lt 1 ] || [ "$PICK" -gt "${#CLIENT_IDS[@]}" ]; then
echo "Invalid selection."
exit 1
fi
CLIENT_ID="${CLIENT_IDS[$((PICK-1))]}"
fi
echo ""
read -r -p "How many controller slots to force a type for [2]: " NSLOTS
NSLOTS="${NSLOTS:-2}"
if ! [[ "$NSLOTS" =~ ^[0-9]+$ ]] || [ "$NSLOTS" -lt 1 ]; then
echo "Invalid number."
exit 1
fi
OVERRIDE_TYPES=()
for i in $(seq 1 "$NSLOTS"); do
read -r -p " Slot $i type (AUTO/XBOX/PS/NINTENDO) [AUTO]: " T
T="${T:-AUTO}"
T="$(echo "$T" | tr '[:lower:]' '[:upper:]')"
case "$T" in
AUTO|XBOX|PS|NINTENDO) ;;
*) echo " Unrecognized '$T' — using AUTO."; T="AUTO" ;;
esac
OVERRIDE_TYPES+=("$T")
done
BODY=$(python3 - "$CLIENT_ID" "${OVERRIDE_TYPES[@]}" << 'BODYPY'
import json, sys
client_id = sys.argv[1]
overrides = sys.argv[2:]
print(json.dumps({"client_id": client_id, "app_state_folder": None,
"settings": {"controllers_override": overrides}}))
BODYPY
)
RESP=$(curl -fsS --unix-socket "$SOCK" -X POST http://localhost/api/v1/clients/settings \
-H 'Content-Type: application/json' -d "$BODY")
echo "$RESP" | python3 -c "
import json, sys
try:
d = json.load(sys.stdin)
print('Updated.' if d.get('success') else 'Failed: ' + json.dumps(d))
except Exception:
print('Unexpected response from Wolf API.')
"
echo ""
echo "Reconnect (or fully restart) the Moonlight stream for this to take effect —"
echo "it's applied when Wolf creates each controller's virtual pad, not retroactively"
echo "to one that already exists in an open session."
;;
pin)
# Wolf logs: "Insert pin at http://SOMEIP:47989/pin/#HEXHASH"
# Extract just the hash fragment and build URLs for every interface
@@ -3329,6 +3458,7 @@ COMPEOF
echo " ./manage.sh logs - Follow Wolf logs"
echo " ./manage.sh status - Show Wolf + app containers"
echo " ./manage.sh pin - Show recent Moonlight pairing PIN link"
echo " ./manage.sh controllers - Force distinct pad types per controller slot (multi-controller fix)"
echo " ./manage.sh update - Pull latest Wolf image and restart"
echo " ./manage.sh apps - Add / update game launchers in Wolf"
echo " ./manage.sh cores [all|common] - Download/refresh RetroArch cores (retro ROMs)"
@@ -3869,6 +3999,7 @@ cd $WOLF_DIR
./manage.sh restart # restart
./manage.sh logs # live logs
./manage.sh status # container status
./manage.sh controllers # force distinct pad types per controller slot (multi-controller fix)
./manage.sh update # pull latest image and restart
./manage.sh apps # add / update game launchers
./manage.sh cores # download/refresh RetroArch cores (retro ROMs)
@@ -3886,6 +4017,41 @@ cd $WOLF_DIR
Run \`./manage.sh install-completion\` once, then re-open your shell (or
\`source ~/.bashrc\`). After that, \`./manage.sh <TAB><TAB>\` lists all commands.
## 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
ends up driving every player, or a second controller's own binding just
doesn't do anything (first reported with Cemu/Wii U, but this isn't
Cemu-specific — it affects any system/app that reads SDL joystick GUIDs).
**Why.** Every virtual gamepad Wolf creates of the *same type* (e.g. two
Wii U Pro Controllers) gets the **identical SDL GUID** — a GUID identifies
a controller *model*, not a physical instance, so this is actually normal,
expected SDL behavior (two real identical physical controllers behave the
exact same way). Some apps' own controller-picker UI just doesn't reliably
tell apart two same-GUID devices though.
**Fix: force each controller slot to a *different* pad type**, so their
vendor/product IDs — and therefore their SDL GUIDs — genuinely differ:
\`\`\`bash
cd $WOLF_DIR && ./manage.sh controllers
\`\`\`
Picks your paired Moonlight client (auto-selected if there's only one),
then asks how many controllers to configure and what type to force each
one to (\`AUTO\` / \`XBOX\` / \`PS\` / \`NINTENDO\`) — e.g. controller 1 =
Xbox, controller 2 = PlayStation. This calls Wolf's own REST API
(\`controllers_override\`, per Wolf's own docs' "Override the default
joypad mapping" section) rather than hand-editing \`config.toml\`.
**Reconnect (or fully restart) the Moonlight stream afterward** — this
applies when Wolf creates each controller's virtual pad for a *new*
session, not retroactively to one already open.
If \`./manage.sh controllers\` says the API socket isn't up yet, re-run
\`sudo ./setup.sh wolf\` (regenerates \`docker-compose.yml\` with the socket
mount this command needs) and then \`docker compose up -d\` to recreate the
Wolf container.
## EA games (Battlefront II 2017, etc.)
EA titles require GE-Proton and the EA App to be installed inside the Wine
prefix. The EA App handles authentication — without it, the game launches
@@ -4110,6 +4276,9 @@ 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.
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\`).
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.
@@ -4159,6 +4328,15 @@ against that same doc): Alt+1 DEL, Alt+2 INS, Alt+3 ERASE, Alt+4 CLEAR,
Alt+5 BEGIN, Alt+6 PROC'D, Alt+7 AID, Alt+8 REDO, Alt+9 BACK, Alt+= QUIT.
## AntiMicroX: extra gamepad buttons for TI-99/4A and Wii U
**Status: confirmed NOT working for TI-99/4A in live testing** (remapped
buttons didn't do anything); Wii U untested. Root cause not yet found —
possible suspects are the \`--eventgen uinput\` backend not actually
injecting into ES-DE's Sway session from inside that same container, or
AntiMicroX's own \`--hidden\` mode needing a display it doesn't have. Not
under active investigation right now — see "Multiple controllers" above
for the actively-maintained fix for the "second controller doesn't work"
problem instead, which doesn't depend on AntiMicroX at all.
If you said yes to the AntiMicroX prompt during install, both TI-99/4A and
Wii U (Cemu) get a second, separately-labeled launch command in ES-DE —
pick it via ES-DE's own **Alternative emulators** option (per-game or