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:
+179
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user