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#?display_name>##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.