From 2422ce1385a1c9afa922d68c14998f855033f4b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 02:25:22 +0000 Subject: [PATCH] =?UTF-8?q?wolf:=20add=20./manage.sh=20cemu-sync-controlle?= =?UTF-8?q?rs=20=E2=80=94=20full=20auto-clone=20via=20SDL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cemu-clone-controller still needed one manual Cemu GUI step per new device (click +, select it, map one button, Save) just to learn its real uuid. This eliminates that too: instead of getting the uuid from Cemu, it asks SDL directly (SDL_JoystickGetDeviceGUID/GetGUIDString via ctypes against libSDL2 — the exact library Cemu itself links against) for the live GUID of every controller connected to the active session, then clones the proven-working mapping onto each one automatically. Confirmed live end-to-end on the user's real box: querying SDL this way inside a running ES-DE session reproduced the exact uuids Cemu had already written by hand for two different controllers (Nintendo Switch Pro and Xbox One S) — byte-for-byte identical to their real controllerProfiles/controllerN.xml. This is what makes trusting SDL as the uuid source safe, after an earlier attempt to reverse-engineer the GUID's CRC16 portion by hand (tried 7 different CRC16 variants) failed to match either device. Also improved cemu-clone-controller's sibling: the synced profile's display_name now comes from SDL's own live device name (when the probe reports one) rather than always inheriting the mapping template's name, which was a real but purely cosmetic issue caught while re-testing. Verified end-to-end in an isolated harness against the real captured controller0.xml/controller1.xml: idempotent skip on already-synced slots (no needless backups, existing files left byte-identical), a new slot correctly cloned with the live name and same 24-pair mapping set, and valid XML output. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6 --- services/wolf.sh | 173 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/services/wolf.sh b/services/wolf.sh index 56e8ea3..d871ba0 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -3554,7 +3554,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 cemu-clone-controller" + steam-add-nonsteam-game cemu-clone-controller cemu-sync-controllers" COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) } # Register for both 'manage.sh' and './manage.sh' invocation styles @@ -4123,6 +4123,156 @@ CLONEPY 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 -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" "$_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 '[^<]*' "$_src" | head -1 | sed -E 's###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'[^<]*', f'{uuid}', template, count=1) +new = re.sub(r'[^<]*', f'{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 @@ -4179,6 +4329,7 @@ CLONEPY echo " - Add an emulator (emulators/) as a non-Steam game, no GUI needed" 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 @@ -4740,6 +4891,7 @@ cd $WOLF_DIR ./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 @@ -5265,6 +5417,25 @@ 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.