diff --git a/services/wolf.sh b/services/wolf.sh index 0343f43..0564a97 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -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 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//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,81 @@ cd $WOLF_DIR Run \`./manage.sh install-completion\` once, then re-open your shell (or \`source ~/.bashrc\`). After that, \`./manage.sh \` 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. There are only **3 concrete types** (confirmed against Wolf's own +virtual-pad source, \`inputtino\`) — not one per real controller brand: +- \`XBOX\` → an **Xbox One** controller specifically (not Series/360) +- \`PS\` → a **PlayStation 5 DualSense** specifically (no separate PS4 option) +- \`NINTENDO\` → a **Switch Pro Controller** +- \`AUTO\` → auto-detects from whatever the physical controller reports + (this is what causes the collision in the first place — e.g. every + 8BitDo pad set to "Switch mode" reports as Nintendo, so AUTO gives all + of them the identical GUID) + +e.g. controller 1 = \`NINTENDO\` (matches an 8BitDo pad's native Switch +mode), controller 2 = \`XBOX\`, controller 3 = \`PS\`. 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\`. + +Forcing a non-matching type doesn't break any buttons — Moonlight still +translates your controller's actual button presses onto whichever virtual +pad type you pick — it just means that controller's on-screen button +prompts (e.g. "press ✕") won't visually match what's printed on the +physical pad. Purely cosmetic. + +**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. + +### 4 controllers (Cemu / Wii U games) +Wii U hardware itself tops out at 4 local players, and everything upstream +supports at least that many: Cemu's own Input Settings allows up to 8 +controller slots (confirmed against Cemu's own wiki — well above what any +Wii U game actually uses), and Wolf's wire protocol tracks controller +slots via a bitmask with no hardcoded 4-controller limit (confirmed +against its own \`control/input_handler.cpp\` source) — the practical +ceiling is whatever your Moonlight *client* supports (commonly 4 on +PC/iOS). + +The catch: with only 3 concrete forced types (above), controller 4 can't +get its own guaranteed-unique GUID — it has to reuse \`XBOX\`, \`PS\`, or +\`NINTENDO\` and land back in the same ambiguous-GUID situation with +whichever one it matches. + +**Workaround for the 4th controller: bind it in Cemu one at a time.** +Disconnect (or just don't touch) every controller except the one you're +currently binding, then use Cemu's own "press a button to detect" step +for that slot. With only one Wolf virtual pad actually emitting input at +that moment, there's nothing for Cemu's picker to confuse it with, +regardless of which type it's sharing a GUID with. This is a reasoned +workaround, not one confirmed working live — if it doesn't pan out, +that's useful to know. + ## 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 +4316,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 +4368,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