From 64594948132fbda84b3a8e3126896cc294be33ba Mon Sep 17 00:00:00 2001 From: HashimHS Date: Fri, 3 Apr 2026 19:55:25 +0200 Subject: [PATCH 1/8] Added controller settings Added Xinput support --- controller_helper.py | 654 ++++++++++++++++++ electron-builder.config.cjs | 2 + electron.cjs | 6 + electron_preload.cjs | 1 + src_backend/config_manager.cjs | 1 + src_backend/controller_mode_manager.cjs | 109 +++ src_backend/ipc_handlers.cjs | 6 +- src_backend/sdl_manager.cjs | 32 +- .../components/ControllerSettings.jsx | 251 +++++++ src_frontend/components/ControllerTester.jsx | 222 ++++++ src_frontend/components/SystemMenu.jsx | 13 + src_frontend/locale/locale.en.json | 18 + src_frontend/locale/locale.fa.json | 18 + src_frontend/locale/locale.it.json | 18 + src_frontend/locale/locale.sv.json | 18 + src_frontend/locale/locale.zh.json | 18 + src_frontend/styles/ControllerSettings.css | 33 + src_frontend/styles/ControllerTester.css | 240 +++++++ src_frontend/utils/ipc.js | 1 + 19 files changed, 1659 insertions(+), 2 deletions(-) create mode 100644 controller_helper.py create mode 100644 src_backend/controller_mode_manager.cjs create mode 100644 src_frontend/components/ControllerSettings.jsx create mode 100644 src_frontend/components/ControllerTester.jsx create mode 100644 src_frontend/styles/ControllerSettings.css create mode 100644 src_frontend/styles/ControllerTester.css diff --git a/controller_helper.py b/controller_helper.py new file mode 100644 index 0000000..0b50e51 --- /dev/null +++ b/controller_helper.py @@ -0,0 +1,654 @@ +#!/usr/bin/env python3 + +import argparse +import glob +import json +import os +import selectors +import signal +import sys +from pathlib import Path +from typing import Optional + +try: + import evdev + from evdev import ecodes +except Exception: + evdev = None + ecodes = None + + +CONTROLLER_KEYWORDS = ( + "controller", + "gamepad", + "joystick", + "xbox", + "dualsense", + "dualshock", + "wireless controller", + "8bitdo", + "pro controller", + "switch pro", + "joycon", + "joy-con", + "steam controller", +) + +# VID → controller family. Takes priority over name-based detection. +VENDOR_FAMILY_MAP = { + "054c": "playstation", # Sony Interactive Entertainment + "045e": "xbox", # Microsoft Corporation + "057e": "nintendo", # Nintendo Co., Ltd + "28de": "steam", # Valve Corporation + "2dc8": "8bitdo", # 8BitDo Technology + "0f0d": "generic", # Hori Co., Ltd + "0079": "generic", # DragonRise Inc. (generic USB gamepads) + "0810": "generic", # Personal Communication Systems + "046d": "generic", # Logitech +} + +# Sony PID → specific family (dualsense vs dualshock vs generic playstation) +SONY_PRODUCT_FAMILIES = { + "0ce6": "dualsense", # DualSense (PS5) + "0df2": "dualsense", # DualSense Edge + "0e5f": "dualsense", # DualSense Access Controller + "09cc": "dualshock", # DualShock 4 (CUH-ZCT2, 2nd gen) + "05c4": "dualshock", # DualShock 4 (CUH-ZCT1, 1st gen) + "0ba0": "dualshock", # DualShock 4 USB Wireless Adaptor +} + +DEFAULT_CAPABILITIES = { + "buttons": [ + "BTN_SOUTH", + "BTN_EAST", + "BTN_NORTH", + "BTN_WEST", + "BTN_TL", + "BTN_TR", + "BTN_SELECT", + "BTN_START", + "BTN_MODE", + "BTN_THUMBL", + "BTN_THUMBR", + "BTN_DPAD_UP", + "BTN_DPAD_DOWN", + "BTN_DPAD_LEFT", + "BTN_DPAD_RIGHT", + ], + "axes": [ + "ABS_X", + "ABS_Y", + "ABS_RX", + "ABS_RY", + "ABS_Z", + "ABS_RZ", + "ABS_HAT0X", + "ABS_HAT0Y", + ], +} + +RUNNING = True + +TARGET_DEVICE_CONFIGS = { + "xinput": { + "name": "Aegis Virtual Xbox Controller", + "vendor": 0x045E, + "product": 0x028E, + "version": 0x0110, + "bustype": 0x03, + }, + "dualsense": { + "name": "Sony Interactive Entertainment DualSense Wireless Controller", + "vendor": 0x054C, + "product": 0x0CE6, + "version": 0x8111, + "bustype": 0x05, + }, +} + + +def classify_family(name: str, vendor_id: str = "0000", product_id: str = "0000") -> str: + # VID/PID-based detection takes priority over name matching. + vid = (vendor_id or "0000").lower().zfill(4) + pid = (product_id or "0000").lower().zfill(4) + + if vid in VENDOR_FAMILY_MAP: + base_family = VENDOR_FAMILY_MAP[vid] + if base_family == "playstation": + return SONY_PRODUCT_FAMILIES.get(pid, "playstation") + return base_family + + # Fall back to name-based detection for unrecognised vendors. + value = (name or "").lower() + if "dualsense" in value or "ps5" in value: + return "dualsense" + if "dualshock" in value or "wireless controller" in value or "ps4" in value: + return "dualshock" + if "xbox" in value: + return "xbox" + if "8bitdo" in value: + return "8bitdo" + if ( + "switch" in value + or "joycon" in value + or "joy-con" in value + or "pro controller" in value + ): + return "nintendo" + if "steam controller" in value or ("valve" in value and "controller" in value): + return "steam" + if any(keyword in value for keyword in CONTROLLER_KEYWORDS): + return "generic" + return "unknown" + + +def infer_connection_type(sysfs_root: Path) -> str: + try: + path_str = str(sysfs_root.resolve()) + except Exception: + path_str = str(sysfs_root) + + lowered = path_str.lower() + if "bluetooth" in lowered: + return "bluetooth" + if "usb" in lowered: + return "usb" + return "unknown" + + +def detect_capabilities(name: str, evdev_caps: Optional[dict] = None) -> list: + family = classify_family(name) + capabilities = ["buttons", "sticks"] + + if evdev_caps is not None and ecodes is not None: + abs_codes = _normalize_abs_codes(evdev_caps.get(ecodes.EV_ABS, [])) + if ecodes.ABS_Z in abs_codes or ecodes.ABS_RZ in abs_codes: + capabilities.append("triggers") + ff_effects = evdev_caps.get(ecodes.EV_FF, []) + if ff_effects: + capabilities.append("rumble") + else: + # Nintendo Pro Controller has digital shoulder buttons, no analog triggers. + if family not in {"nintendo"}: + capabilities.append("triggers") + + # Gyro and touchpad are inferred from controller family since evdev exposes + # them on a separate input node that isn't the gamepad interface. + if family in {"dualsense", "dualshock"}: + capabilities.extend(["gyro", "touchpad"]) + elif family in {"nintendo", "steam"}: + capabilities.append("gyro") + + return sorted(set(capabilities)) + + +def _normalize_abs_codes(raw_abs_codes): + normalized = set() + for code in raw_abs_codes: + if isinstance(code, tuple): + normalized.add(code[0]) + else: + normalized.add(code) + return normalized + + +def _looks_like_gamepad(device) -> bool: + if evdev is None or ecodes is None: + return False + + capabilities = device.capabilities() + key_codes = set(capabilities.get(ecodes.EV_KEY, [])) + abs_codes = _normalize_abs_codes(capabilities.get(ecodes.EV_ABS, [])) + + has_gamepad_buttons = any( + code in key_codes + for code in ( + ecodes.BTN_SOUTH, + ecodes.BTN_EAST, + ecodes.BTN_NORTH, + ecodes.BTN_WEST, + ecodes.BTN_TL, + ecodes.BTN_TR, + ecodes.BTN_SELECT, + ecodes.BTN_START, + ecodes.BTN_MODE, + ecodes.BTN_THUMBL, + ecodes.BTN_THUMBR, + ) + ) + has_gamepad_axes = any( + code in abs_codes + for code in ( + ecodes.ABS_X, + ecodes.ABS_Y, + ecodes.ABS_RX, + ecodes.ABS_RY, + ecodes.ABS_HAT0X, + ecodes.ABS_HAT0Y, + ) + ) + + return has_gamepad_buttons and has_gamepad_axes + + +def _controller_from_evdev(device): + event_name = os.path.basename(device.path) + event_root = Path("/sys/class/input") / event_name / "device" + vendor_id = "0000" + product_id = "0000" + + try: + vendor_id = (event_root / "id" / "vendor").read_text(encoding="utf8").strip() + product_id = (event_root / "id" / "product").read_text(encoding="utf8").strip() + except Exception: + pass + + evdev_caps = device.capabilities() + family = classify_family(device.name, vendor_id, product_id) + + return { + "id": event_name, + "name": device.name, + "vendorId": vendor_id.lower(), + "productId": product_id.lower(), + "connectionType": infer_connection_type(event_root), + "family": family, + "capabilities": detect_capabilities(device.name, evdev_caps), + "eventPath": device.path, + } + + +def enumerate_input_candidates(): + controllers = [] + + if evdev is not None: + for device_path in evdev.list_devices(): + try: + device = evdev.InputDevice(device_path) + except Exception: + continue + + try: + if _looks_like_gamepad(device): + controllers.append(_controller_from_evdev(device)) + finally: + device.close() + + if controllers: + for index, controller in enumerate(controllers): + controller["isPrimary"] = index == 0 + return controllers + + for event_path in sorted(glob.glob("/sys/class/input/event*")): + event_root = Path(event_path) + device_name_path = event_root / "device" / "name" + if not device_name_path.exists(): + continue + + try: + name = device_name_path.read_text(encoding="utf8").strip() + except Exception: + continue + + lowered = name.lower() + if not any(keyword in lowered for keyword in CONTROLLER_KEYWORDS): + continue + + event_name = event_root.name + vendor_id = "0000" + product_id = "0000" + + try: + vendor_id = (event_root / "device" / "id" / "vendor").read_text(encoding="utf8").strip() + product_id = (event_root / "device" / "id" / "product").read_text(encoding="utf8").strip() + except Exception: + pass + + controllers.append( + { + "id": event_name, + "name": name, + "vendorId": vendor_id.lower(), + "productId": product_id.lower(), + "connectionType": infer_connection_type(event_root / "device"), + "family": classify_family(name, vendor_id, product_id), + "capabilities": detect_capabilities(name), + "eventPath": f"/dev/input/{event_name}", + } + ) + + for index, controller in enumerate(controllers): + controller["isPrimary"] = index == 0 + + return controllers + + +def build_probe(): + uinput_path = "/dev/uinput" + uinput_access = os.access(uinput_path, os.R_OK | os.W_OK) + return { + "python_ok": True, + "evdev_available": evdev is not None, + "uinput_present": os.path.exists(uinput_path), + "uinput_access": uinput_access, + "uinput_path": uinput_path, + "running_in_container": os.path.exists("/.dockerenv") + or os.path.exists("/run/.containerenv"), + "controller_count": len(enumerate_input_candidates()), + } + + +def create_virtual_capabilities(): + if evdev is None: + return None + + button_codes = [getattr(ecodes, code) for code in DEFAULT_CAPABILITIES["buttons"]] + axis_codes = [ + ( + getattr(ecodes, code), + evdev.AbsInfo(value=0, min=-32768, max=32767, fuzz=16, flat=128, resolution=0), + ) + for code in DEFAULT_CAPABILITIES["axes"][:4] + ] + trigger_codes = [ + ( + getattr(ecodes, code), + evdev.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0), + ) + for code in DEFAULT_CAPABILITIES["axes"][4:6] + ] + hat_codes = [ + ( + getattr(ecodes, code), + evdev.AbsInfo(value=0, min=-1, max=1, fuzz=0, flat=0, resolution=0), + ) + for code in DEFAULT_CAPABILITIES["axes"][6:] + ] + + return { + ecodes.EV_KEY: button_codes, + ecodes.EV_ABS: axis_codes + trigger_codes + hat_codes, + } + +def _scale_value(value, source_min, source_max, target_min, target_max): + if source_max == source_min: + return target_min + + ratio = (value - source_min) / (source_max - source_min) + scaled = target_min + ratio * (target_max - target_min) + return int(round(scaled)) + + +def _source_abs_info(device, code): + try: + return device.absinfo(code) + except Exception: + return None + + +def _target_abs_range(code): + if code in ( + ecodes.ABS_X, + ecodes.ABS_Y, + ecodes.ABS_RX, + ecodes.ABS_RY, + ): + return (-32768, 32767) + if code in (ecodes.ABS_Z, ecodes.ABS_RZ): + return (0, 255) + if code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y): + return (-1, 1) + return None + + +def _clamp(value, lower, upper): + return max(lower, min(upper, value)) + + +def _remap_abs_event(device, event): + target_range = _target_abs_range(event.code) + if target_range is None: + return event.code, event.value + + source_abs_info = _source_abs_info(device, event.code) + if source_abs_info is None: + return event.code, event.value + + source_min = source_abs_info.min + source_max = source_abs_info.max + target_min, target_max = target_range + + if source_min == target_min and source_max == target_max: + value = event.value + else: + value = _scale_value(event.value, source_min, source_max, target_min, target_max) + + return (event.code, _clamp(value, target_min, target_max)) + + +def _remap_key_event(event): + # Normalize Linux gamepad buttons onto the intended virtual layout. + key_map = { + ecodes.BTN_A: ecodes.BTN_SOUTH, + ecodes.BTN_B: ecodes.BTN_EAST, + ecodes.BTN_X: ecodes.BTN_WEST, + ecodes.BTN_Y: ecodes.BTN_NORTH, + ecodes.BTN_THUMB: ecodes.BTN_THUMBL, + ecodes.BTN_THUMB2: ecodes.BTN_THUMBR, + ecodes.BTN_TOP: ecodes.BTN_WEST, + ecodes.BTN_TOP2: ecodes.BTN_NORTH, + } + return key_map.get(event.code, event.code), event.value + + +def _remap_trigger_button_event(event): + if event.code == ecodes.BTN_TL2: + return ecodes.ABS_Z, 255 if event.value else 0 + if event.code == ecodes.BTN_TR2: + return ecodes.ABS_RZ, 255 if event.value else 0 + return None + + +def _update_dpad_state(dpad_state, event): + if event.code == ecodes.BTN_DPAD_LEFT: + dpad_state["left"] = bool(event.value) + elif event.code == ecodes.BTN_DPAD_RIGHT: + dpad_state["right"] = bool(event.value) + elif event.code == ecodes.BTN_DPAD_UP: + dpad_state["up"] = bool(event.value) + elif event.code == ecodes.BTN_DPAD_DOWN: + dpad_state["down"] = bool(event.value) + + hat_x = 0 + hat_y = 0 + + if dpad_state["left"] and not dpad_state["right"]: + hat_x = -1 + elif dpad_state["right"] and not dpad_state["left"]: + hat_x = 1 + + if dpad_state["up"] and not dpad_state["down"]: + hat_y = -1 + elif dpad_state["down"] and not dpad_state["up"]: + hat_y = 1 + + return hat_x, hat_y + + +def _write_dpad_state(ui, dpad_state): + hat_x = 0 + hat_y = 0 + + if dpad_state["left"] and not dpad_state["right"]: + hat_x = -1 + elif dpad_state["right"] and not dpad_state["left"]: + hat_x = 1 + + if dpad_state["up"] and not dpad_state["down"]: + hat_y = -1 + elif dpad_state["down"] and not dpad_state["up"]: + hat_y = 1 + + ui.write(ecodes.EV_ABS, ecodes.ABS_HAT0X, hat_x) + ui.write(ecodes.EV_ABS, ecodes.ABS_HAT0Y, hat_y) + ui.write(ecodes.EV_KEY, ecodes.BTN_DPAD_LEFT, int(dpad_state["left"])) + ui.write(ecodes.EV_KEY, ecodes.BTN_DPAD_RIGHT, int(dpad_state["right"])) + ui.write(ecodes.EV_KEY, ecodes.BTN_DPAD_UP, int(dpad_state["up"])) + ui.write(ecodes.EV_KEY, ecodes.BTN_DPAD_DOWN, int(dpad_state["down"])) + ui.syn() + + +def _update_dpad_state_from_hat(dpad_state, code, value): + if code == ecodes.ABS_HAT0X: + dpad_state["left"] = value < 0 + dpad_state["right"] = value > 0 + elif code == ecodes.ABS_HAT0Y: + dpad_state["up"] = value < 0 + dpad_state["down"] = value > 0 + + +def _create_virtual_device(mode): + config = TARGET_DEVICE_CONFIGS[mode] + return evdev.UInput( + create_virtual_capabilities(), + name=config["name"], + vendor=config["vendor"], + product=config["product"], + version=config["version"], + bustype=config["bustype"], + ) + + +def serve_virtual_controller(mode: str, controller_id: Optional[str], exclusive_grab: bool): + if evdev is None: + raise RuntimeError("python-evdev is not installed") + + controllers = enumerate_input_candidates() + selected = None + for controller in controllers: + if controller_id is None or controller["id"] == controller_id: + selected = controller + break + + if not selected: + raise RuntimeError("No compatible controller was found for the helper session") + + input_device = evdev.InputDevice(selected["eventPath"]) + ui = _create_virtual_device(mode) + + if exclusive_grab: + input_device.grab() + + selector = selectors.DefaultSelector() + selector.register(input_device.fd, selectors.EVENT_READ) + dpad_state = { + "left": False, + "right": False, + "up": False, + "down": False, + } + + print( + json.dumps( + { + "status": "ready", + "mode": mode, + "controller": selected["name"], + "exclusive": exclusive_grab, + } + ), + flush=True, + ) + + try: + while RUNNING: + for _key, _mask in selector.select(timeout=0.25): + for event in input_device.read(): + if event.type == ecodes.EV_KEY: + trigger_remap = _remap_trigger_button_event(event) + if trigger_remap is not None: + code, value = trigger_remap + ui.write(ecodes.EV_ABS, code, value) + ui.syn() + continue + + if event.code in ( + ecodes.BTN_DPAD_LEFT, + ecodes.BTN_DPAD_RIGHT, + ecodes.BTN_DPAD_UP, + ecodes.BTN_DPAD_DOWN, + ): + _update_dpad_state(dpad_state, event) + _write_dpad_state(ui, dpad_state) + continue + + code, value = _remap_key_event(event) + ui.write(event.type, code, value) + ui.syn() + elif event.type == ecodes.EV_ABS: + code, value = _remap_abs_event(input_device, event) + if code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y): + _update_dpad_state_from_hat(dpad_state, code, value) + _write_dpad_state(ui, dpad_state) + continue + ui.write(event.type, code, value) + ui.syn() + finally: + try: + if exclusive_grab: + input_device.ungrab() + except Exception: + pass + ui.close() + input_device.close() + + +def handle_signal(_signum, _frame): + global RUNNING + RUNNING = False + + +def main(): + signal.signal(signal.SIGTERM, handle_signal) + signal.signal(signal.SIGINT, handle_signal) + + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + + subparsers.add_parser("probe") + subparsers.add_parser("list") + + serve_parser = subparsers.add_parser("serve") + serve_parser.add_argument("--mode", default="xinput") + serve_parser.add_argument("--controller-id") + serve_parser.add_argument("--exclusive-grab", action="store_true") + + arguments = parser.parse_args() + + if arguments.command == "probe": + print(json.dumps(build_probe())) + return 0 + + if arguments.command == "list": + print(json.dumps({"controllers": enumerate_input_candidates()})) + return 0 + + if arguments.command == "serve": + if arguments.mode not in TARGET_DEVICE_CONFIGS: + raise RuntimeError(f"Unsupported serve mode: {arguments.mode}") + serve_virtual_controller( + arguments.mode, + arguments.controller_id, + arguments.exclusive_grab, + ) + return 0 + + return 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(json.dumps({"status": "error", "error": str(error)}), file=sys.stderr) + raise diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index 9337c65..0df4960 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -26,12 +26,14 @@ module.exports = { "package.json", "lutris_wrapper.sh", "lutris_wrapper.py", + "controller_helper.py", "src_backend/**/*", "!node_modules/**/{test,tests,example,examples,doc,docs}/**/*", ], asarUnpack: [ "lutris_wrapper.sh", "lutris_wrapper.py", + "controller_helper.py", "electron_preload.cjs", "src_backend/resources/gamecontrollerdb.txt", ], diff --git a/electron.cjs b/electron.cjs index 63983d0..29ea50c 100644 --- a/electron.cjs +++ b/electron.cjs @@ -1,6 +1,10 @@ const { app, Menu } = require("electron"); const { getAppConfig } = require("./src_backend/config_manager.cjs"); +const { + initControllerModeManager, + shutdownControllerModeManager, +} = require("./src_backend/controller_mode_manager.cjs"); const { closeRunningGameProcess, toggleGamePause, @@ -34,6 +38,7 @@ app.on("second-instance", () => { }); app.on("window-all-closed", () => { + shutdownControllerModeManager(); if (getAppConfig().keepGamesRunningOnQuit) { toggleGamePause({ forceStatus: "running" }); } else { @@ -88,6 +93,7 @@ app try { registerIpcHandlers(); + initControllerModeManager(); createWindow(() => { logInfo("Main window closed!"); if (!getAppConfig().keepGamesRunningOnQuit) { diff --git a/electron_preload.cjs b/electron_preload.cjs index 7c66b4c..7f5e3b7 100644 --- a/electron_preload.cjs +++ b/electron_preload.cjs @@ -96,4 +96,5 @@ contextBridge.exposeInMainWorld("electronAPI", { // Sdl pollGamepadsSdl: () => ipcRenderer.invoke("poll-gamepads-sdl"), + listControllers: () => ipcRenderer.invoke("list-controllers"), }); diff --git a/src_backend/config_manager.cjs b/src_backend/config_manager.cjs index d53cb16..f3ecb94 100644 --- a/src_backend/config_manager.cjs +++ b/src_backend/config_manager.cjs @@ -13,6 +13,7 @@ const defaultConfig = { showRunnerIcon: true, keepGamesRunningOnQuit: false, enableUiActionSoundFeedbacks: true, + controllerInputMode: "native", }; const SUBSCRIPTIONS = {}; diff --git a/src_backend/controller_mode_manager.cjs b/src_backend/controller_mode_manager.cjs new file mode 100644 index 0000000..e9595d8 --- /dev/null +++ b/src_backend/controller_mode_manager.cjs @@ -0,0 +1,109 @@ +const { spawn } = require("node:child_process"); + +const { + getAppConfig, + subscribeConfigValueChange, +} = require("./config_manager.cjs"); +const { localeAppFile, logError, logInfo, logWarn } = require("./utils.cjs"); + +let helperProcess = null; + +function getControllerHelperPath() { + return localeAppFile("controller_helper.py"); +} + +function startXinputHelper() { + if (helperProcess) { + logWarn("controller_mode_manager", "xinput helper already running"); + return; + } + + let helperPath; + try { + helperPath = getControllerHelperPath(); + } catch { + logError( + "controller_mode_manager", + "controller_helper.py not found, cannot start xinput mode", + ); + return; + } + + logInfo("controller_mode_manager", "starting xinput helper"); + + const proc = spawn( + "python3", + [helperPath, "serve", "--mode", "xinput", "--exclusive-grab"], + { stdio: ["ignore", "pipe", "pipe"] }, + ); + + const stdoutDecoder = new TextDecoder(); + const stderrDecoder = new TextDecoder(); + + proc.stdout.on("data", (data) => { + const text = stdoutDecoder.decode(data).trim(); + if (text) { + logInfo("controller_mode_manager", text); + } + }); + + proc.stderr.on("data", (data) => { + const text = stderrDecoder.decode(data).trim(); + if (text) { + logError("controller_mode_manager", text); + } + }); + + proc.on("close", (code) => { + logInfo("controller_mode_manager", `xinput helper exited with code ${code}`); + if (helperProcess === proc) { + helperProcess = null; + } + }); + + proc.on("error", (error) => { + logError("controller_mode_manager", "failed to start xinput helper", error); + if (helperProcess === proc) { + helperProcess = null; + } + }); + + helperProcess = proc; +} + +function stopXinputHelper() { + if (!helperProcess) { + return; + } + + logInfo("controller_mode_manager", "stopping xinput helper"); + helperProcess.kill("SIGTERM"); + helperProcess = null; +} + +function applyInputMode(mode) { + if (mode === "xinput") { + startXinputHelper(); + } else { + stopXinputHelper(); + } +} + +function initControllerModeManager() { + const config = getAppConfig(); + applyInputMode(config.controllerInputMode); + + subscribeConfigValueChange("controllerInputMode", (newMode) => { + logInfo("controller_mode_manager", `input mode changed to: ${newMode}`); + applyInputMode(newMode); + }); +} + +function shutdownControllerModeManager() { + stopXinputHelper(); +} + +module.exports = { + initControllerModeManager, + shutdownControllerModeManager, +}; diff --git a/src_backend/ipc_handlers.cjs b/src_backend/ipc_handlers.cjs index 6e23aa1..5d8e671 100644 --- a/src_backend/ipc_handlers.cjs +++ b/src_backend/ipc_handlers.cjs @@ -34,7 +34,7 @@ const { updateLutrisSetting, getLutrisRunners, } = require("./lutris_wrapper.cjs"); -const { mapSdlGamepadsToWebApi, pollGamepads } = require("./sdl_manager.cjs"); +const { mapSdlGamepadsToWebApi, pollGamepads, listControllers } = require("./sdl_manager.cjs"); const { getMainWindow } = require("./state.cjs"); const { getUserTheme } = require("./theme_manager.cjs"); const { @@ -303,6 +303,10 @@ function registerIpcHandlers() { ipcHandleWithError("poll-gamepads-sdl", async () => { return mapSdlGamepadsToWebApi(await pollGamepads()); }); + + ipcHandleWithError("list-controllers", async () => { + return await listControllers(); + }); } module.exports = { registerIpcHandlers }; diff --git a/src_backend/sdl_manager.cjs b/src_backend/sdl_manager.cjs index 6dc1be0..dd0c2d7 100644 --- a/src_backend/sdl_manager.cjs +++ b/src_backend/sdl_manager.cjs @@ -173,4 +173,34 @@ function mapSdlGamepadsToWebApi(gamepads) { })); } -module.exports = { pollGamepads, mapSdlGamepadsToWebApi }; +async function listControllers() { + const { sdl, activeControllers } = await getSdlHandle(); + + sdl.SDL_GameControllerUpdate(); + + const numJoysticks = sdl.SDL_NumJoysticks(); + const controllers = []; + + for (let i = 0; i < numJoysticks; i++) { + if (sdl.SDL_IsGameController(i) === 0) continue; + + let ptr = activeControllers.get(i); + if (!ptr) { + ptr = sdl.SDL_GameControllerOpen(i); + if (ptr) activeControllers.set(i, ptr); + } + + if (!ptr || sdl.SDL_GameControllerGetAttached(ptr) === 0) continue; + + const name = sdl.SDL_GameControllerName(ptr) || `Controller ${i + 1}`; + + controllers.push({ + index: i, + name, + }); + } + + return controllers; +} + +module.exports = { pollGamepads, mapSdlGamepadsToWebApi, listControllers }; diff --git a/src_frontend/components/ControllerSettings.jsx b/src_frontend/components/ControllerSettings.jsx new file mode 100644 index 0000000..2c251cc --- /dev/null +++ b/src_frontend/components/ControllerSettings.jsx @@ -0,0 +1,251 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { useTranslation } from "../contexts/TranslationContext"; +import * as api from "../utils/ipc"; + +import ControllerTester from "./ControllerTester"; +import DialogLayout from "./DialogLayout"; +import FocusableRow from "./FocusableRow"; +import LoadingIndicator from "./LoadingIndicator"; +import RowBasedMenu from "./RowBasedMenu"; + +import "../styles/ControllerSettings.css"; + +export const ControllerSettingsFocusId = "ControllerSettings"; +const ControllerDetailFocusId = "ControllerDetail"; + +const INPUT_MODES = ["native", "xinput"]; + +/* ── Detail view for a single controller ── */ + +const ControllerDetail = ({ controller, onBack }) => { + const { t } = useTranslation(); + const [inputMode, setInputMode] = useState("native"); + const [showTester, setShowTester] = useState(false); + const scrollParentReference = useRef(null); + + useEffect(() => { + api.getAppConfig().then((cfg) => { + if (cfg?.controllerInputMode) { + setInputMode(cfg.controllerInputMode); + } + }); + }, []); + + const cycleInputMode = useCallback(() => { + setInputMode((prev) => { + const next = + INPUT_MODES[(INPUT_MODES.indexOf(prev) + 1) % INPUT_MODES.length]; + api.setAppConfig("controllerInputMode", next); + return next; + }); + }, []); + + const handleAction = useCallback( + (actionName, item) => { + if (actionName === "B") { + onBack(); + return; + } + if (actionName === "A" && item) { + if (item.id === "input-mode") { + cycleInputMode(); + } else if (item.id === "test-controller") { + setShowTester(true); + } + } + }, + [onBack, cycleInputMode], + ); + + const modeLabel = inputMode === "native" ? t("Native") : t("Xinput"); + + const menuItems = useMemo( + () => [ + { + id: "input-mode", + label: t("Input Mode"), + value: modeLabel, + }, + { + id: "test-controller", + label: t("Test Controller"), + }, + ], + [modeLabel, t], + ); + + const renderItem = useCallback( + (item, isFocused, onMouseEnter) => ( + +
+ {item.label} + {item.value && {item.value}} +
+
+ ), + [], + ); + + const legendItems = useMemo( + () => [ + { button: "A", label: t("Select") }, + { button: "B", label: t("Back") }, + ], + [t], + ); + + if (showTester) { + return ( + setShowTester(false)} + /> + ); + } + + return ( + + item.id} + scrollParentRef={scrollParentReference} + /> + + ); +}; + +/* ── Controller list (top-level) ── */ + +const ControllerSettings = ({ onClose }) => { + const { t } = useTranslation(); + const [controllers, setControllers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [selectedController, setSelectedController] = useState(null); + const currentMenuItem = useRef(null); + const scrollParentReference = useRef(null); + + const fetchControllers = useCallback(async () => { + setIsLoading(true); + try { + const result = await api.listControllers(); + setControllers(result || []); + } catch { + setControllers([]); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + fetchControllers(); + }, [fetchControllers]); + + const handleAction = useCallback( + (actionName, item) => { + switch (actionName) { + case "A": { + if (item?.controller) { + setSelectedController(item.controller); + } + break; + } + case "B": { + onClose(); + break; + } + case "Y": { + fetchControllers(); + break; + } + } + }, + [onClose, fetchControllers], + ); + + const menuItems = useMemo( + () => + controllers.map((controller) => ({ + id: `controller-${controller.index}`, + label: controller.name, + controller, + })), + [controllers], + ); + + const renderItem = useCallback( + (item, isFocused, onMouseEnter) => ( + +
+ 🎮 + {item.label} +
+
+ ), + [], + ); + + const legendItems = useMemo( + () => [ + { button: "A", label: t("Select") }, + { button: "Y", label: t("Refresh"), onClick: fetchControllers }, + { button: "B", label: t("Close"), onClick: onClose }, + ], + [onClose, fetchControllers, t], + ); + + if (selectedController) { + return ( + setSelectedController(null)} + /> + ); + } + + return ( + + {isLoading ? ( +
+ +
+ ) : ( + item.id} + onFocusChange={(item) => { + currentMenuItem.current = item; + }} + scrollParentRef={scrollParentReference} + emptyMessage={t("No controllers detected.")} + /> + )} +
+ ); +}; + +export default ControllerSettings; diff --git a/src_frontend/components/ControllerTester.jsx b/src_frontend/components/ControllerTester.jsx new file mode 100644 index 0000000..1ccddbf --- /dev/null +++ b/src_frontend/components/ControllerTester.jsx @@ -0,0 +1,222 @@ +import { useEffect, useMemo, useRef, useState } from "react"; + +import { useTranslation } from "../contexts/TranslationContext"; +import { useScopedInput } from "../hooks/useScopedInput"; +import * as api from "../utils/ipc"; + +import DialogLayout from "./DialogLayout"; + +import "../styles/ControllerTester.css"; + +const BTN = { + A: 0, + B: 1, + X: 2, + Y: 3, + LB: 4, + RB: 5, + LT: 6, + RT: 7, + SELECT: 8, + START: 9, + L3: 10, + R3: 11, + DPAD_UP: 12, + DPAD_DOWN: 13, + DPAD_LEFT: 14, + DPAD_RIGHT: 15, + HOME: 16, +}; + +function getButtonValue(state, index) { + return state?.buttons?.[index]?.value ?? 0; +} + +function isPressed(state, index) { + return state?.buttons?.[index]?.pressed ?? false; +} + +function getAxis(state, index) { + return state?.axes?.[index] ?? 0; +} + +const AnalogStick = ({ x, y, pressed, label }) => { + const knobX = 50 + x * 36; + const knobY = 50 + y * 36; + return ( +
+
+
+
+
+
+ {label} +
+ ); +}; + +const TriggerBar = ({ value, label, side }) => ( +
+ {label} +
+
+
+ {Math.round(value * 100)}% +
+); + +const ShoulderButton = ({ pressed, label }) => ( +
{label}
+); + +const FaceButton = ({ pressed, label, color }) => ( +
+ {label} +
+); + +const DPad = ({ up, down, left, right }) => ( +
+
+
+
+
+
+
+
+
+); + +const CenterButton = ({ pressed, label }) => ( +
{label}
+); + +export const ControllerTesterFocusId = "ControllerTester"; + +const ControllerTester = ({ onClose, controllerIndex }) => { + const { t } = useTranslation(); + const [gamepadState, setGamepadState] = useState(null); + const rafRef = useRef(null); + + useScopedInput( + (event) => { + if (event.name === "B") { + onClose(); + event.isConsumed = true; + } + }, + ControllerTesterFocusId, + ); + + useEffect(() => { + let active = true; + + const poll = async () => { + if (!active) return; + try { + const gamepads = await api.pollGamepadsSdl(); + if (!active) return; + const gp = gamepads.find((g) => g.index === controllerIndex) ?? gamepads[0] ?? null; + setGamepadState(gp); + } catch { + // SDL not available — skip + } + if (active) { + rafRef.current = requestAnimationFrame(poll); + } + }; + + rafRef.current = requestAnimationFrame(poll); + return () => { + active = false; + if (rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [controllerIndex]); + + const legendItems = useMemo( + () => [{ button: "B", label: t("Close"), onClick: onClose }], + [onClose, t], + ); + + const s = gamepadState; + + return ( + + {!s ? ( +
{t("Waiting for controller input...")}
+ ) : ( +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + + +
+ +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+ )} +
+ ); +}; + +export default ControllerTester; diff --git a/src_frontend/components/SystemMenu.jsx b/src_frontend/components/SystemMenu.jsx index cedcc87..2f932f3 100644 --- a/src_frontend/components/SystemMenu.jsx +++ b/src_frontend/components/SystemMenu.jsx @@ -14,6 +14,7 @@ import * as api from "../utils/ipc"; import About from "./About"; import BluetoothMenu from "./BluetoothMenu"; import ConfirmationDialog from "./ConfirmationDialog"; +import ControllerSettings from "./ControllerSettings"; import DisplaySettings from "./DisplaySettings"; import LegendaContainer from "./LegendaContainer"; import LutrisSettingsFlow from "./LutrisSettingsFlow"; @@ -75,6 +76,13 @@ const SystemMenu = () => { setIsOpen(false); }, [showModal, setIsOpen]); + const openControllerSettingsModal = useCallback(() => { + showModal((hideThisModal) => ( + + )); + setIsOpen(false); + }, [showModal, setIsOpen]); + const openAboutModal = useCallback(() => { showModal((hideThisModal) => ); setIsOpen(false); @@ -132,6 +140,10 @@ const SystemMenu = () => { action: openBluetoothSettingsModal, disabled: staticSettings.DISABLE_BLUETOOTH_SETTINGS, }, + { + label: t("Controller Settings"), + action: openControllerSettingsModal, + }, { label: t("Open Lutris"), action: () => api.openLutris(), @@ -178,6 +190,7 @@ const SystemMenu = () => { openAudioSettingsModal, openDisplaySettingsModal, openBluetoothSettingsModal, + openControllerSettingsModal, settings.doubleConfirmPowerManagement, staticSettings.DISABLE_LUTRIS_SETTINGS, staticSettings.DISABLE_AUDIO_SETTINGS, diff --git a/src_frontend/locale/locale.en.json b/src_frontend/locale/locale.en.json index f2cda41..766d313 100644 --- a/src_frontend/locale/locale.en.json +++ b/src_frontend/locale/locale.en.json @@ -32,6 +32,23 @@ "Confirm": "Confirm", "Select": "Select" }, + "components/ControllerSettings.jsx": { + "Back": "Back", + "Close": "Close", + "Controller Settings": "Controller Settings", + "Input Mode": "Input Mode", + "Native": "Native", + "No controllers detected.": "No controllers detected.", + "Refresh": "Refresh", + "Select": "Select", + "Test Controller": "Test Controller", + "Xinput": "Xinput" + }, + "components/ControllerTester.jsx": { + "Close": "Close", + "Controller Test": "Controller Test", + "Waiting for controller input...": "Waiting for controller input..." + }, "components/ControlsOverlay.jsx": { "Clear Search": "Clear Search", "Force close": "Force close", @@ -168,6 +185,7 @@ "Continue with exiting the application?": "Continue with exiting the application?", "Continue with system power off?": "Continue with system power off?", "Continue with system reboot?": "Continue with system reboot?", + "Controller Settings": "Controller Settings", "Display Settings": "Display Settings", "Exit Application": "Exit Application", "Generate Bug Report": "Generate Bug Report", diff --git a/src_frontend/locale/locale.fa.json b/src_frontend/locale/locale.fa.json index 97bdb6a..6eb0596 100644 --- a/src_frontend/locale/locale.fa.json +++ b/src_frontend/locale/locale.fa.json @@ -32,6 +32,23 @@ "Confirm": "پذیرفتن", "Select": "انتخاب" }, + "components/ControllerSettings.jsx": { + "Back": "Back", + "Close": "Close", + "Controller Settings": "Controller Settings", + "Input Mode": "Input Mode", + "Native": "Native", + "No controllers detected.": "No controllers detected.", + "Refresh": "Refresh", + "Select": "Select", + "Test Controller": "Test Controller", + "Xinput": "Xinput" + }, + "components/ControllerTester.jsx": { + "Close": "Close", + "Controller Test": "Controller Test", + "Waiting for controller input...": "Waiting for controller input..." + }, "components/ControlsOverlay.jsx": { "Clear Search": "پاک‌کردن جستجو", "Force close": "بستن اجباری", @@ -168,6 +185,7 @@ "Continue with exiting the application?": "ادامه دادن به خروج از برنامه؟", "Continue with system power off?": "ادامه دادن به خاموش کردن سامانه؟", "Continue with system reboot?": "ادامه دادن به باز راه‌اندازی سامانه؟", + "Controller Settings": "Controller Settings", "Display Settings": "Display Settings", "Exit Application": "خروج از برنامه", "Generate Bug Report": "Generate Bug Report", diff --git a/src_frontend/locale/locale.it.json b/src_frontend/locale/locale.it.json index be3033e..13b4239 100644 --- a/src_frontend/locale/locale.it.json +++ b/src_frontend/locale/locale.it.json @@ -32,6 +32,23 @@ "Confirm": "Conferma", "Select": "Seleziona" }, + "components/ControllerSettings.jsx": { + "Back": "Back", + "Close": "Close", + "Controller Settings": "Controller Settings", + "Input Mode": "Input Mode", + "Native": "Native", + "No controllers detected.": "No controllers detected.", + "Refresh": "Refresh", + "Select": "Select", + "Test Controller": "Test Controller", + "Xinput": "Xinput" + }, + "components/ControllerTester.jsx": { + "Close": "Close", + "Controller Test": "Controller Test", + "Waiting for controller input...": "Waiting for controller input..." + }, "components/ControlsOverlay.jsx": { "Clear Search": "Annulla ricerca", "Force close": "Forza chiusura di", @@ -168,6 +185,7 @@ "Continue with exiting the application?": "Uscire dall'applicazione?", "Continue with system power off?": "Procedere con lo spegnimento del sistema?", "Continue with system reboot?": "Procedere con il riavvio del sistema?", + "Controller Settings": "Controller Settings", "Display Settings": "Display Settings", "Exit Application": "Esci", "Generate Bug Report": "Genera segnalazione bug", diff --git a/src_frontend/locale/locale.sv.json b/src_frontend/locale/locale.sv.json index 1bdb414..86ebc8c 100644 --- a/src_frontend/locale/locale.sv.json +++ b/src_frontend/locale/locale.sv.json @@ -32,6 +32,23 @@ "Confirm": "Bekräfta", "Select": "Välj" }, + "components/ControllerSettings.jsx": { + "Back": "Back", + "Close": "Close", + "Controller Settings": "Controller Settings", + "Input Mode": "Input Mode", + "Native": "Native", + "No controllers detected.": "No controllers detected.", + "Refresh": "Refresh", + "Select": "Select", + "Test Controller": "Test Controller", + "Xinput": "Xinput" + }, + "components/ControllerTester.jsx": { + "Close": "Close", + "Controller Test": "Controller Test", + "Waiting for controller input...": "Waiting for controller input..." + }, "components/ControlsOverlay.jsx": { "Clear Search": "Rensa sökning", "Force close": "Tvinga stängning av", @@ -168,6 +185,7 @@ "Continue with exiting the application?": "Fortsätt med att avsluta programmet?", "Continue with system power off?": "Fortsätt med att stänga av systemet?", "Continue with system reboot?": "Fortsätt med att starta om systemet?", + "Controller Settings": "Controller Settings", "Display Settings": "Display Settings", "Exit Application": "Avsluta programmet", "Generate Bug Report": "Generate Bug Report", diff --git a/src_frontend/locale/locale.zh.json b/src_frontend/locale/locale.zh.json index 3895111..d77097f 100644 --- a/src_frontend/locale/locale.zh.json +++ b/src_frontend/locale/locale.zh.json @@ -32,6 +32,23 @@ "Confirm": "确认", "Select": "选择" }, + "components/ControllerSettings.jsx": { + "Back": "Back", + "Close": "Close", + "Controller Settings": "Controller Settings", + "Input Mode": "Input Mode", + "Native": "Native", + "No controllers detected.": "No controllers detected.", + "Refresh": "Refresh", + "Select": "Select", + "Test Controller": "Test Controller", + "Xinput": "Xinput" + }, + "components/ControllerTester.jsx": { + "Close": "Close", + "Controller Test": "Controller Test", + "Waiting for controller input...": "Waiting for controller input..." + }, "components/ControlsOverlay.jsx": { "Clear Search": "清除搜索", "Force close": "强制关闭", @@ -168,6 +185,7 @@ "Continue with exiting the application?": "继续退出应用吗?", "Continue with system power off?": "继续关闭系统吗?", "Continue with system reboot?": "继续重启系统吗?", + "Controller Settings": "Controller Settings", "Display Settings": "显示设置", "Exit Application": "退出应用", "Generate Bug Report": "生成错误报告", diff --git a/src_frontend/styles/ControllerSettings.css b/src_frontend/styles/ControllerSettings.css new file mode 100644 index 0000000..b5751d1 --- /dev/null +++ b/src_frontend/styles/ControllerSettings.css @@ -0,0 +1,33 @@ +.cs-loading { + padding: 24px 0; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.cs-row-content { + display: flex; + align-items: center; + gap: 12px; + flex-grow: 1; + min-width: 0; + overflow: hidden; +} + +.cs-row-icon { + font-size: 1.4rem; + flex-shrink: 0; +} + +.cs-row-value { + margin-left: auto; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.cs-row-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src_frontend/styles/ControllerTester.css b/src_frontend/styles/ControllerTester.css new file mode 100644 index 0000000..7ff26c8 --- /dev/null +++ b/src_frontend/styles/ControllerTester.css @@ -0,0 +1,240 @@ +.ct-no-data { + padding: 32px 0; + text-align: center; + color: var(--text-secondary); + font-size: 1rem; +} + +.ct-layout { + display: flex; + flex-direction: column; + gap: 16px; + padding: 8px 0; +} + +.ct-shoulders { + display: flex; + justify-content: space-between; + padding: 0 32px; +} + +.ct-shoulder { + padding: 6px 24px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.15); + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + transition: background 0.1s, color 0.1s; +} + +.ct-shoulder.pressed { + background: rgba(100, 181, 246, 0.5); + border-color: #64b5f6; + color: #fff; +} + +.ct-triggers { + display: flex; + justify-content: space-between; + gap: 24px; + padding: 0 16px; +} + +.ct-trigger-wrapper { + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.ct-trigger-track { + flex: 1; + height: 10px; + background: rgba(255, 255, 255, 0.08); + border-radius: 5px; + overflow: hidden; +} + +.ct-trigger-fill { + height: 100%; + background: #64b5f6; + border-radius: 5px; + transition: width 0.05s linear; +} + +.ct-trigger-pct { + font-size: 0.75rem; + color: var(--text-secondary); + min-width: 32px; + text-align: right; +} + +.ct-main { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.ct-left-side, +.ct-right-side { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.ct-center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding-top: 16px; +} + +.ct-center-btn { + padding: 4px 14px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 600; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.15); + color: var(--text-secondary); + transition: background 0.1s; +} + +.ct-center-btn.pressed { + background: rgba(100, 181, 246, 0.5); + border-color: #64b5f6; + color: #fff; +} + +.ct-stick-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.ct-stick-area { + position: relative; + width: 90px; + height: 90px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.15); + overflow: hidden; +} + +.ct-stick-area.pressed { + border-color: #64b5f6; +} + +.ct-stick-crosshair-h, +.ct-stick-crosshair-v { + position: absolute; + background: rgba(255, 255, 255, 0.06); +} + +.ct-stick-crosshair-h { + top: 50%; + left: 0; + right: 0; + height: 1px; + transform: translateY(-50%); +} + +.ct-stick-crosshair-v { + left: 50%; + top: 0; + bottom: 0; + width: 1px; + transform: translateX(-50%); +} + +.ct-stick-knob { + position: absolute; + width: 18px; + height: 18px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + border: 2px solid rgba(255, 255, 255, 0.7); + transform: translate(-50%, -50%); + transition: background 0.08s; +} + +.ct-stick-knob.pressed { + background: #64b5f6; + border-color: #64b5f6; +} + +.ct-label { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.ct-face-buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.ct-face-row { + display: flex; + gap: 4px; +} + +.ct-face-btn { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 700; + background: rgba(255, 255, 255, 0.04); + border: 2px solid; + transition: background 0.08s; +} + +.ct-face-btn.pressed { + color: #fff !important; +} + +.ct-dpad { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.ct-dpad-row { + display: flex; + gap: 2px; +} + +.ct-dpad-cell { + width: 24px; + height: 24px; + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.12); + transition: background 0.08s; +} + +.ct-dpad-cell.pressed { + background: rgba(100, 181, 246, 0.5); + border-color: #64b5f6; +} + +.ct-dpad-center { + width: 24px; + height: 24px; + background: rgba(255, 255, 255, 0.03); + border-radius: 3px; +} diff --git a/src_frontend/utils/ipc.js b/src_frontend/utils/ipc.js index d01f61b..3f2ba6d 100644 --- a/src_frontend/utils/ipc.js +++ b/src_frontend/utils/ipc.js @@ -95,6 +95,7 @@ export const createBugReportFile = () => globalThis.electronAPI.createBugReportFile(); export const pollGamepadsSdl = () => globalThis.electronAPI.pollGamepadsSdl(); +export const listControllers = () => globalThis.electronAPI.listControllers(); export function encodeAppProtocolPath(filePath) { const sanitizedFilePath = filePath From eb20b989f6cf8d65463386e639e334b4368c16f9 Mon Sep 17 00:00:00 2001 From: HashimHS Date: Fri, 3 Apr 2026 20:32:03 +0200 Subject: [PATCH 2/8] Fix duplicate controllers when using xinput. --- controller_helper.py | 2 ++ src_backend/controller_mode_manager.cjs | 20 ++++++++++++++++++++ src_backend/game_manager.cjs | 21 ++++++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/controller_helper.py b/controller_helper.py index 0b50e51..b91bd7c 100644 --- a/controller_helper.py +++ b/controller_helper.py @@ -555,6 +555,8 @@ def serve_virtual_controller(mode: str, controller_id: Optional[str], exclusive_ "mode": mode, "controller": selected["name"], "exclusive": exclusive_grab, + "vendorId": selected.get("vendorId", "0000"), + "productId": selected.get("productId", "0000"), } ), flush=True, diff --git a/src_backend/controller_mode_manager.cjs b/src_backend/controller_mode_manager.cjs index e9595d8..82d5f40 100644 --- a/src_backend/controller_mode_manager.cjs +++ b/src_backend/controller_mode_manager.cjs @@ -7,6 +7,7 @@ const { const { localeAppFile, logError, logInfo, logWarn } = require("./utils.cjs"); let helperProcess = null; +let grabbedDevice = null; function getControllerHelperPath() { return localeAppFile("controller_helper.py"); @@ -44,6 +45,17 @@ function startXinputHelper() { const text = stdoutDecoder.decode(data).trim(); if (text) { logInfo("controller_mode_manager", text); + try { + const msg = JSON.parse(text); + if (msg.status === "ready" && msg.vendorId && msg.productId) { + grabbedDevice = { + vendorId: msg.vendorId, + productId: msg.productId, + }; + } + } catch { + // not JSON, ignore + } } }); @@ -58,6 +70,7 @@ function startXinputHelper() { logInfo("controller_mode_manager", `xinput helper exited with code ${code}`); if (helperProcess === proc) { helperProcess = null; + grabbedDevice = null; } }); @@ -65,6 +78,7 @@ function startXinputHelper() { logError("controller_mode_manager", "failed to start xinput helper", error); if (helperProcess === proc) { helperProcess = null; + grabbedDevice = null; } }); @@ -79,6 +93,7 @@ function stopXinputHelper() { logInfo("controller_mode_manager", "stopping xinput helper"); helperProcess.kill("SIGTERM"); helperProcess = null; + grabbedDevice = null; } function applyInputMode(mode) { @@ -103,7 +118,12 @@ function shutdownControllerModeManager() { stopXinputHelper(); } +function getGrabbedDevice() { + return grabbedDevice; +} + module.exports = { initControllerModeManager, shutdownControllerModeManager, + getGrabbedDevice, }; diff --git a/src_backend/game_manager.cjs b/src_backend/game_manager.cjs index ebc1550..2a2dffd 100644 --- a/src_backend/game_manager.cjs +++ b/src_backend/game_manager.cjs @@ -6,6 +6,7 @@ const path = require("node:path"); const { globalShortcut } = require("electron"); const { getAppConfig } = require("./config_manager.cjs"); +const { getGrabbedDevice } = require("./controller_mode_manager.cjs"); const { getCoverartPath, getRuntimeIconPath, @@ -280,13 +281,27 @@ function launchGame(gameId) { } const gameStartTime = Date.now(); + const appConfig = getAppConfig(); + + const spawnOptions = { + detached: appConfig.keepGamesRunningOnQuit, + }; + + const grabbed = getGrabbedDevice(); + if (appConfig.controllerInputMode === "xinput" && grabbed) { + const vid = `0x${grabbed.vendorId}`; + const pid = `0x${grabbed.productId}`; + spawnOptions.env = { + ...process.env, + SDL_GAMECONTROLLER_IGNORE_DEVICES: `${vid}/${pid}`, + SDL_JOYSTICK_IGNORE_DEVICES: `${vid}/${pid}`, + }; + } const newGameProcess = spawn( "bash", [getLutrisWrapperPath(), `lutris:rungameid/${gameId}`], - { - detached: getAppConfig().keepGamesRunningOnQuit, - }, + spawnOptions, ); const stdoutTextDecoder = new TextDecoder(); From 2fd8e63ae1e5bdf445262ebb71b5603731000a7f Mon Sep 17 00:00:00 2001 From: HashimHS Date: Fri, 3 Apr 2026 21:12:49 +0200 Subject: [PATCH 3/8] Display more details on listed controllers --- src_backend/sdl_bindings.cjs | 6 +++ src_backend/sdl_manager.cjs | 25 ++++++++++++ .../components/ControllerSettings.jsx | 34 +++++++++++++--- src_frontend/styles/ControllerSettings.css | 40 ++++++++++++++++--- 4 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src_backend/sdl_bindings.cjs b/src_backend/sdl_bindings.cjs index f392364..d4399dd 100644 --- a/src_backend/sdl_bindings.cjs +++ b/src_backend/sdl_bindings.cjs @@ -59,6 +59,12 @@ function bindSDL2(lib) { SDL_GameControllerName: lib.func( "const char* SDL_GameControllerName(SDL_GameController* gamecontroller)", ), + SDL_GameControllerGetVendor: lib.func( + "Uint16 SDL_GameControllerGetVendor(SDL_GameController* gamecontroller)", + ), + SDL_GameControllerGetProduct: lib.func( + "Uint16 SDL_GameControllerGetProduct(SDL_GameController* gamecontroller)", + ), }; sdl.SDL_GameControllerAddMappingsFromFile = function (file) { diff --git a/src_backend/sdl_manager.cjs b/src_backend/sdl_manager.cjs index dd0c2d7..f7bd728 100644 --- a/src_backend/sdl_manager.cjs +++ b/src_backend/sdl_manager.cjs @@ -173,6 +173,22 @@ function mapSdlGamepadsToWebApi(gamepads) { })); } +const VENDOR_FAMILY_MAP = { + "054c": "playstation", + "045e": "xbox", + "057e": "nintendo", + "28de": "steam", + "2dc8": "8bitdo", + "0f0d": "generic", + "0079": "generic", + "0810": "generic", + "046d": "generic", +}; + +function classifyFamily(vendorId) { + return VENDOR_FAMILY_MAP[vendorId] || "unknown"; +} + async function listControllers() { const { sdl, activeControllers } = await getSdlHandle(); @@ -193,10 +209,19 @@ async function listControllers() { if (!ptr || sdl.SDL_GameControllerGetAttached(ptr) === 0) continue; const name = sdl.SDL_GameControllerName(ptr) || `Controller ${i + 1}`; + const vendorId = sdl.SDL_GameControllerGetVendor(ptr) + .toString(16) + .padStart(4, "0"); + const productId = sdl.SDL_GameControllerGetProduct(ptr) + .toString(16) + .padStart(4, "0"); controllers.push({ index: i, name, + vendorId, + productId, + family: classifyFamily(vendorId), }); } diff --git a/src_frontend/components/ControllerSettings.jsx b/src_frontend/components/ControllerSettings.jsx index 2c251cc..f1bb7a5 100644 --- a/src_frontend/components/ControllerSettings.jsx +++ b/src_frontend/components/ControllerSettings.jsx @@ -134,14 +134,21 @@ const ControllerSettings = ({ onClose }) => { const [controllers, setControllers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedController, setSelectedController] = useState(null); + const [inputMode, setInputMode] = useState("native"); const currentMenuItem = useRef(null); const scrollParentReference = useRef(null); const fetchControllers = useCallback(async () => { setIsLoading(true); try { - const result = await api.listControllers(); + const [result, config] = await Promise.all([ + api.listControllers(), + api.getAppConfig(), + ]); setControllers(result || []); + if (config?.controllerInputMode) { + setInputMode(config.controllerInputMode); + } } catch { setControllers([]); } finally { @@ -192,13 +199,25 @@ const ControllerSettings = ({ onClose }) => { isFocused={isFocused} onMouseEnter={onMouseEnter} > -
- 🎮 - {item.label} +
+
+ {item.controller.name} + + {inputMode} + +
+ + {[ + item.controller.family, + `${item.controller.vendorId}:${item.controller.productId}`, + ] + .filter(Boolean) + .join(" · ")} +
), - [], + [inputMode], ); const legendItems = useMemo( @@ -214,7 +233,10 @@ const ControllerSettings = ({ onClose }) => { return ( setSelectedController(null)} + onBack={() => { + setSelectedController(null); + fetchControllers(); + }} /> ); } diff --git a/src_frontend/styles/ControllerSettings.css b/src_frontend/styles/ControllerSettings.css index b5751d1..3c7b2d0 100644 --- a/src_frontend/styles/ControllerSettings.css +++ b/src_frontend/styles/ControllerSettings.css @@ -15,14 +15,11 @@ overflow: hidden; } -.cs-row-icon { - font-size: 1.4rem; - flex-shrink: 0; -} - .cs-row-value { margin-left: auto; - color: var(--text-secondary); + color: var(--accent-color); + font-weight: 600; + text-transform: capitalize; font-size: 0.9rem; } @@ -31,3 +28,34 @@ overflow: hidden; text-overflow: ellipsis; } + +.controller-settings-device { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; +} + +.controller-settings-device-header { + display: flex; + align-items: center; + gap: 10px; +} + +.controller-settings-mode-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + background: rgba(255, 80, 80, 0.15); + color: #ff5050; +} + +.controller-settings-meta { + color: var(--text-secondary-color, rgba(255, 255, 255, 0.72)); + font-size: 0.9rem; + text-transform: capitalize; +} From c3834b943c8eefb8c92a8e2b43e19869c46b3d18 Mon Sep 17 00:00:00 2001 From: HashimHS Date: Sat, 4 Apr 2026 14:01:21 +0200 Subject: [PATCH 4/8] Beutify controller testing layout --- .../components/ControllerSettings.jsx | 1 + src_frontend/components/ControllerTester.jsx | 592 ++++++++++++++---- src_frontend/styles/ControllerTester.css | 232 +------ 3 files changed, 491 insertions(+), 334 deletions(-) diff --git a/src_frontend/components/ControllerSettings.jsx b/src_frontend/components/ControllerSettings.jsx index f1bb7a5..6939801 100644 --- a/src_frontend/components/ControllerSettings.jsx +++ b/src_frontend/components/ControllerSettings.jsx @@ -103,6 +103,7 @@ const ControllerDetail = ({ controller, onBack }) => { return ( setShowTester(false)} /> ); diff --git a/src_frontend/components/ControllerTester.jsx b/src_frontend/components/ControllerTester.jsx index 1ccddbf..58f5d92 100644 --- a/src_frontend/components/ControllerTester.jsx +++ b/src_frontend/components/ControllerTester.jsx @@ -40,74 +40,498 @@ function getAxis(state, index) { return state?.axes?.[index] ?? 0; } -const AnalogStick = ({ x, y, pressed, label }) => { - const knobX = 50 + x * 36; - const knobY = 50 + y * 36; +const btnFill = (pressed, color) => + pressed ? color : "rgba(255,255,255,0.06)"; +const btnStroke = (pressed, color) => + pressed ? color : "rgba(255,255,255,0.18)"; +const txtFill = (pressed, color) => + pressed ? "#fff" : `${color}aa`; + +const TriggerMeter = ({ x, y, width, label, value, accent, align = "start" }) => { + const barX = align === "center" ? -(width / 2) : align === "end" ? -width : 0; + const labelX = align === "center" ? 0 : align === "end" ? 0 : 0; + const textAnchor = align === "center" ? "middle" : align === "end" ? "end" : "start"; + return ( -
-
-
-
-
-
- {label} -
+ + + {label} {Math.round(value * 100)}% + + + + + ); +}; + +/* ═══════════════════════════════════════════════════════════════ + Xbox One Controller — e7d/gamepad-viewer SVG paths (MIT) + viewBox matches the original 750 × 630.455 base + ═══════════════════════════════════════════════════════════════ */ + +const XboxModel = ({ s }) => { + const stickTravel = 25; + const lx = 185.468 + getAxis(s, 0) * stickTravel; + const ly = 279.865 + getAxis(s, 1) * stickTravel; + const rx = 473.042 + getAxis(s, 2) * stickTravel; + const ry = 393.815 + getAxis(s, 3) * stickTravel; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + const accent = "#107c10"; + + return ( + + + {/* ── Base body (from e7d/gamepad-viewer xbox-one/base-black.svg, MIT) ── */} + + + + {/* ── D-Pad well & cross ── */} + + + + + {/* ── View / Menu buttons ── */} + + + + {/* ── Face plate / Top bar ── */} + + + + {/* ── Left stick well ── */} + + + + + + {/* ── Right stick well ── */} + + + + + + {/* ── Triggers (from e7d xbox-one/trigger.svg) ── */} + + + + + + + + + + {/* ── Xbox guide button ── */} + + + + {/* ── Face buttons (ABXY) — positions from e7d CSS ── */} + {/* A - bottom */} + + A + {/* B - right */} + + B + {/* X - left */} + + X + {/* Y - top */} + + Y + + {/* ── View button (select) ── */} + + + + + + {/* ── Menu button (start) ── */} + + + + + + {/* ── D-Pad interactive overlay ── */} + + {/* cross background */} + + + {/* Up */} + + {/* Down */} + + {/* Left */} + + {/* Right */} + + + + {/* ── Left bumper (LB) ── */} + + + + LB + + + + {/* ── Right bumper (RB) ── */} + + + + RB + + + + {/* ── Left stick knob ── */} + + + + + {/* ── Right stick knob ── */} + + + + + ); }; -const TriggerBar = ({ value, label, side }) => ( -
- {label} -
-
-
- {Math.round(value * 100)}% -
-); - -const ShoulderButton = ({ pressed, label }) => ( -
{label}
-); - -const FaceButton = ({ pressed, label, color }) => ( -
- {label} -
-); - -const DPad = ({ up, down, left, right }) => ( -
-
-
-
-
-
-
-
-
-); - -const CenterButton = ({ pressed, label }) => ( -
{label}
-); +/* ═══════════════════════════════════════════════════════════════ + DualShock 4 / DualSense — e7d/gamepad-viewer SVG paths (MIT) + viewBox matches the original 806 × 630 base (padded for HUD) + ═══════════════════════════════════════════════════════════════ */ + +const PlayStationModel = ({ s }) => { + const stickTravel = 25; + const lx = 274.65 + getAxis(s, 0) * stickTravel; + const ly = 355.967 + getAxis(s, 1) * stickTravel; + const rx = 531.25 + getAxis(s, 2) * stickTravel; + const ry = 355.967 + getAxis(s, 3) * stickTravel; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + const accent = "#006fcd"; + + return ( + + {/* ── Base body (from e7d/gamepad-viewer ds4/base-black.svg, MIT) ── */} + + + {/* ── Grip shading (left) ── */} + + {/* ── Grip shading (right) ── */} + + + {/* ── Left stick well ── */} + + + + + {/* ── Right stick well ── */} + + + + + {/* ── Touchpad ── */} + + + + {/* ── Face button / D-Pad background circles ── */} + + + + {/* ── D-Pad arrow indicators (from base) ── */} + + + {/* ── Create / Options button wells ── */} + + + + {/* ── Speaker grille dots ── */} + + + + + + + + + + + + + + + + {/* ── Light bar ── */} + + + {/* ── Trigger areas (from base) ── */} + + + + + + {/* ── Trigger 3D shapes (from base) ── */} + + + + {/* ── PS button (from base) ── */} + + + + + + {isPressed(s, BTN.HOME) && } + + {/* ═══ Interactive overlays ═══ */} + + {/* ── D-Pad arrows (from e7d ds4/dpad.svg, each path individually positioned via svgpathtools bbox) ── */} + {/* Up — path center (17.71,25.50) → target (154,207) */} + + {/* Down — path center (55.10,25.50) → target (154,281) */} + + {/* Left — path center (153.05,17.85) → target (118,243) */} + + {/* Right — path center (100.26,17.76) → target (191,243) */} + + + {/* ── Face buttons (PlayStation symbols — e7d ds4/buttons.svg paths, scaled to 70% of r=26 circle) ── */} + {/* Cross (A) - bottom */} + + + + + {/* Circle (B) - right */} + + + + + {/* Square (X) - left */} + + + + + {/* Triangle (Y) - top */} + + + + + + {/* ── Create button ── */} + + CRE + + {/* ── Options button ── */} + + OPT + + {/* ── L1 bumper ── */} + + + + L1 + + + + {/* ── R1 bumper ── */} + + + + R1 + + + + {/* ── Left stick knob ── */} + + + + + {/* ── Right stick knob ── */} + + + + + + ); +}; + +/* ═══════════════════════════════════════════════════════════════ + Nintendo Pro Controller — hand-crafted (no e7d template) + ═══════════════════════════════════════════════════════════════ */ + +const NintendoModel = ({ s }) => { + const stickR = 12; + const lx = 152 + getAxis(s, 0) * stickR; + const ly = 158 + getAxis(s, 1) * stickR; + const rx = 370 + getAxis(s, 2) * stickR; + const ry = 215 + getAxis(s, 3) * stickR; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + + return ( + + {/* ── Body shadow ── */} + + + {/* ── Main body (dark charcoal like real Pro Controller) ── */} + + + {/* ── Left grip ── */} + + + {/* ── Right grip ── */} + + + {/* ── Face plate highlight ── */} + + + {/* ── Left bumper (L) ── */} + + L + + {/* ── Right bumper (R) ── */} + + R + + {/* ── Left trigger (ZL) ── */} + + + {/* ── Right trigger (ZR) ── */} + + + {/* ── Left stick well ── */} + + + + + {/* Left stick knob */} + + + + {/* ── Right stick well ── */} + + + + + {/* Right stick knob */} + + + + {/* ── D-Pad (cross style) ── */} + + + + + + + + + + + {/* ── Face buttons (ABXY Nintendo layout) ── */} + + {/* X - top */} + + X + {/* B - bottom */} + + B + {/* Y - left */} + + Y + {/* A - right */} + + A + + + {/* ── Minus button ── */} + + + + {/* ── Plus button ── */} + + + + + {/* ── Home button ── */} + + + + {/* ── Capture button (square) ── */} + + + + ); +}; + +/* ═══════════════════════════════════════════════════════════════ */ + +const MODEL_MAP = { + xbox: XboxModel, + playstation: PlayStationModel, + nintendo: NintendoModel, +}; export const ControllerTesterFocusId = "ControllerTester"; -const ControllerTester = ({ onClose, controllerIndex }) => { +const ControllerTester = ({ onClose, controllerIndex, controllerFamily }) => { const { t } = useTranslation(); const [gamepadState, setGamepadState] = useState(null); const rafRef = useRef(null); + const ModelComponent = useMemo( + () => MODEL_MAP[controllerFamily] || XboxModel, + [controllerFamily], + ); + useScopedInput( (event) => { if (event.name === "B") { @@ -160,59 +584,7 @@ const ControllerTester = ({ onClose, controllerIndex }) => {
{t("Waiting for controller input...")}
) : (
-
- - -
- -
- - -
- -
-
- - -
- -
- - - -
- -
-
-
- -
-
- - -
-
- -
-
- -
-
+
)} diff --git a/src_frontend/styles/ControllerTester.css b/src_frontend/styles/ControllerTester.css index 7ff26c8..7afee6f 100644 --- a/src_frontend/styles/ControllerTester.css +++ b/src_frontend/styles/ControllerTester.css @@ -8,233 +8,17 @@ .ct-layout { display: flex; flex-direction: column; - gap: 16px; - padding: 8px 0; -} - -.ct-shoulders { - display: flex; - justify-content: space-between; - padding: 0 32px; -} - -.ct-shoulder { - padding: 6px 24px; - border-radius: 6px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.15); - font-size: 0.85rem; - font-weight: 600; - color: var(--text-secondary); - transition: background 0.1s, color 0.1s; -} - -.ct-shoulder.pressed { - background: rgba(100, 181, 246, 0.5); - border-color: #64b5f6; - color: #fff; -} - -.ct-triggers { - display: flex; - justify-content: space-between; - gap: 24px; - padding: 0 16px; -} - -.ct-trigger-wrapper { - display: flex; align-items: center; - gap: 8px; - flex: 1; -} - -.ct-trigger-track { - flex: 1; - height: 10px; - background: rgba(255, 255, 255, 0.08); - border-radius: 5px; - overflow: hidden; -} - -.ct-trigger-fill { - height: 100%; - background: #64b5f6; - border-radius: 5px; - transition: width 0.05s linear; -} - -.ct-trigger-pct { - font-size: 0.75rem; - color: var(--text-secondary); - min-width: 32px; - text-align: right; -} - -.ct-main { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 8px; -} - -.ct-left-side, -.ct-right-side { - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; -} - -.ct-center { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; - padding-top: 16px; -} - -.ct-center-btn { - padding: 4px 14px; - border-radius: 12px; - font-size: 0.7rem; - font-weight: 600; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.15); - color: var(--text-secondary); - transition: background 0.1s; -} - -.ct-center-btn.pressed { - background: rgba(100, 181, 246, 0.5); - border-color: #64b5f6; - color: #fff; -} - -.ct-stick-wrapper { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -.ct-stick-area { - position: relative; - width: 90px; - height: 90px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.15); - overflow: hidden; -} - -.ct-stick-area.pressed { - border-color: #64b5f6; -} - -.ct-stick-crosshair-h, -.ct-stick-crosshair-v { - position: absolute; - background: rgba(255, 255, 255, 0.06); -} - -.ct-stick-crosshair-h { - top: 50%; - left: 0; - right: 0; - height: 1px; - transform: translateY(-50%); -} - -.ct-stick-crosshair-v { - left: 50%; - top: 0; - bottom: 0; - width: 1px; - transform: translateX(-50%); -} - -.ct-stick-knob { - position: absolute; - width: 18px; - height: 18px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.5); - border: 2px solid rgba(255, 255, 255, 0.7); - transform: translate(-50%, -50%); - transition: background 0.08s; -} - -.ct-stick-knob.pressed { - background: #64b5f6; - border-color: #64b5f6; -} - -.ct-label { - font-size: 0.75rem; - color: var(--text-secondary); -} - -.ct-face-buttons { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -.ct-face-row { - display: flex; - gap: 4px; -} - -.ct-face-btn { - width: 36px; - height: 36px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.8rem; - font-weight: 700; - background: rgba(255, 255, 255, 0.04); - border: 2px solid; - transition: background 0.08s; -} - -.ct-face-btn.pressed { - color: #fff !important; -} - -.ct-dpad { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; -} - -.ct-dpad-row { - display: flex; - gap: 2px; -} - -.ct-dpad-cell { - width: 24px; - height: 24px; - background: rgba(255, 255, 255, 0.08); - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.12); - transition: background 0.08s; + padding: 12px 0; } -.ct-dpad-cell.pressed { - background: rgba(100, 181, 246, 0.5); - border-color: #64b5f6; +.ct-svg { + width: 100%; + max-width: 620px; + height: auto; + filter: drop-shadow(0 4px 24px rgba(0, 0, 0, 0.5)); } -.ct-dpad-center { - width: 24px; - height: 24px; - background: rgba(255, 255, 255, 0.03); - border-radius: 3px; +.ct-stick-anim { + transition: cx 0.04s linear, cy 0.04s linear, fill 0.08s; } From 719ba60b1f7f5b33b5164a842296a9dbb085a220 Mon Sep 17 00:00:00 2001 From: HashimHS Date: Sat, 4 Apr 2026 14:55:59 +0200 Subject: [PATCH 5/8] Fix xinput controller duplicates on listed cotrollers --- controller_helper.py | 28 ++++++++++ src_backend/controller_mode_manager.cjs | 12 +++++ src_backend/sdl_bindings.cjs | 17 ++++++ src_backend/sdl_manager.cjs | 54 ++++++++++++++++++-- src_frontend/components/ControllerTester.jsx | 17 +++--- 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/controller_helper.py b/controller_helper.py index b91bd7c..99b47de 100644 --- a/controller_helper.py +++ b/controller_helper.py @@ -519,6 +519,25 @@ def _create_virtual_device(mode): ) +def _get_uinput_event_path(ui) -> Optional[str]: + # Different evdev versions expose the device path through different attributes. + try: + if hasattr(ui, "device") and ui.device is not None: + for attr in ("path", "fn"): + value = getattr(ui.device, attr, None) + if value: + return value + except Exception: + pass + + for attr in ("devnode", "path", "fn"): + value = getattr(ui, attr, None) + if value: + return value + + return None + + def serve_virtual_controller(mode: str, controller_id: Optional[str], exclusive_grab: bool): if evdev is None: raise RuntimeError("python-evdev is not installed") @@ -535,6 +554,8 @@ def serve_virtual_controller(mode: str, controller_id: Optional[str], exclusive_ input_device = evdev.InputDevice(selected["eventPath"]) ui = _create_virtual_device(mode) + virtual_cfg = TARGET_DEVICE_CONFIGS[mode] + virtual_event_path = _get_uinput_event_path(ui) if exclusive_grab: input_device.grab() @@ -557,6 +578,13 @@ def serve_virtual_controller(mode: str, controller_id: Optional[str], exclusive_ "exclusive": exclusive_grab, "vendorId": selected.get("vendorId", "0000"), "productId": selected.get("productId", "0000"), + "virtualDevice": { + "name": virtual_cfg["name"], + "vendorId": f"{virtual_cfg['vendor']:04x}", + "productId": f"{virtual_cfg['product']:04x}", + "version": f"{virtual_cfg['version']:04x}", + "eventPath": virtual_event_path, + }, } ), flush=True, diff --git a/src_backend/controller_mode_manager.cjs b/src_backend/controller_mode_manager.cjs index 82d5f40..cde67dd 100644 --- a/src_backend/controller_mode_manager.cjs +++ b/src_backend/controller_mode_manager.cjs @@ -8,6 +8,7 @@ const { localeAppFile, logError, logInfo, logWarn } = require("./utils.cjs"); let helperProcess = null; let grabbedDevice = null; +let virtualDevice = null; function getControllerHelperPath() { return localeAppFile("controller_helper.py"); @@ -53,6 +54,9 @@ function startXinputHelper() { productId: msg.productId, }; } + if (msg.status === "ready" && msg.virtualDevice) { + virtualDevice = msg.virtualDevice; + } } catch { // not JSON, ignore } @@ -71,6 +75,7 @@ function startXinputHelper() { if (helperProcess === proc) { helperProcess = null; grabbedDevice = null; + virtualDevice = null; } }); @@ -79,6 +84,7 @@ function startXinputHelper() { if (helperProcess === proc) { helperProcess = null; grabbedDevice = null; + virtualDevice = null; } }); @@ -94,6 +100,7 @@ function stopXinputHelper() { helperProcess.kill("SIGTERM"); helperProcess = null; grabbedDevice = null; + virtualDevice = null; } function applyInputMode(mode) { @@ -122,8 +129,13 @@ function getGrabbedDevice() { return grabbedDevice; } +function getVirtualDevice() { + return virtualDevice; +} + module.exports = { initControllerModeManager, shutdownControllerModeManager, getGrabbedDevice, + getVirtualDevice, }; diff --git a/src_backend/sdl_bindings.cjs b/src_backend/sdl_bindings.cjs index d4399dd..6d9975d 100644 --- a/src_backend/sdl_bindings.cjs +++ b/src_backend/sdl_bindings.cjs @@ -22,6 +22,7 @@ function bindSDL2(lib) { SDL_Init: lib.func("int SDL_Init(Uint32 flags)"), SDL_Quit: lib.func("void SDL_Quit(void)"), + SDL_PumpEvents: lib.func("void SDL_PumpEvents(void)"), SDL_RWFromFile: lib.func( "void* SDL_RWFromFile(const char* file, const char* mode)", @@ -67,6 +68,22 @@ function bindSDL2(lib) { ), }; + try { + sdl.SDL_GameControllerGetProductVersion = lib.func( + "Uint16 SDL_GameControllerGetProductVersion(SDL_GameController* gamecontroller)", + ); + } catch { + sdl.SDL_GameControllerGetProductVersion = null; + } + + try { + sdl.SDL_GameControllerPath = lib.func( + "const char* SDL_GameControllerPath(SDL_GameController* gamecontroller)", + ); + } catch { + sdl.SDL_GameControllerPath = null; + } + sdl.SDL_GameControllerAddMappingsFromFile = function (file) { const rw = sdl.SDL_RWFromFile(file, "rb"); if (!rw) { diff --git a/src_backend/sdl_manager.cjs b/src_backend/sdl_manager.cjs index f7bd728..6396875 100644 --- a/src_backend/sdl_manager.cjs +++ b/src_backend/sdl_manager.cjs @@ -1,3 +1,5 @@ +const { getAppConfig } = require("./config_manager.cjs"); +const { getVirtualDevice } = require("./controller_mode_manager.cjs"); const { SDL2_LIBRARY_NAME, bindSDL2, @@ -189,13 +191,45 @@ function classifyFamily(vendorId) { return VENDOR_FAMILY_MAP[vendorId] || "unknown"; } +function isAppGeneratedXinputController(controller, virtualDevice) { + if (!virtualDevice) return false; + + // Best signal: exact event node path match. + if (virtualDevice.eventPath && controller.path) { + return virtualDevice.eventPath === controller.path; + } + + // Fallback: strict signature match. + if (!virtualDevice.name || !virtualDevice.vendorId || !virtualDevice.productId) { + return false; + } + + if (controller.name !== virtualDevice.name) { + return false; + } + + if (controller.vendorId !== virtualDevice.vendorId || controller.productId !== virtualDevice.productId) { + return false; + } + + if (virtualDevice.version && controller.version) { + return controller.version === virtualDevice.version; + } + + return true; +} + async function listControllers() { const { sdl, activeControllers } = await getSdlHandle(); + sdl.SDL_PumpEvents(); sdl.SDL_GameControllerUpdate(); const numJoysticks = sdl.SDL_NumJoysticks(); const controllers = []; + const appConfig = getAppConfig(); + const virtualDevice = + appConfig?.controllerInputMode === "xinput" ? getVirtualDevice() : null; for (let i = 0; i < numJoysticks; i++) { if (sdl.SDL_IsGameController(i) === 0) continue; @@ -215,14 +249,28 @@ async function listControllers() { const productId = sdl.SDL_GameControllerGetProduct(ptr) .toString(16) .padStart(4, "0"); - - controllers.push({ + const version = sdl.SDL_GameControllerGetProductVersion + ? sdl.SDL_GameControllerGetProductVersion(ptr).toString(16).padStart(4, "0") + : null; + const path = sdl.SDL_GameControllerPath + ? sdl.SDL_GameControllerPath(ptr) + : null; + + const controller = { index: i, name, vendorId, productId, + version, + path, family: classifyFamily(vendorId), - }); + }; + + if (isAppGeneratedXinputController(controller, virtualDevice)) { + continue; + } + + controllers.push(controller); } return controllers; diff --git a/src_frontend/components/ControllerTester.jsx b/src_frontend/components/ControllerTester.jsx index 58f5d92..f8904f8 100644 --- a/src_frontend/components/ControllerTester.jsx +++ b/src_frontend/components/ControllerTester.jsx @@ -48,13 +48,16 @@ const txtFill = (pressed, color) => pressed ? "#fff" : `${color}aa`; const TriggerMeter = ({ x, y, width, label, value, accent, align = "start" }) => { - const barX = align === "center" ? -(width / 2) : align === "end" ? -width : 0; - const labelX = align === "center" ? 0 : align === "end" ? 0 : 0; - const textAnchor = align === "center" ? "middle" : align === "end" ? "end" : "start"; + const alignment = { + center: { barX: -(width / 2), textAnchor: "middle" }, + end: { barX: -width, textAnchor: "end" }, + start: { barX: 0, textAnchor: "start" }, + }; + const { barX, textAnchor } = alignment[align] || alignment.start; return ( - + {label} {Math.round(value * 100)}% @@ -580,12 +583,12 @@ const ControllerTester = ({ onClose, controllerIndex, controllerFamily }) => { legendItems={legendItems} maxWidth="700px" > - {!s ? ( -
{t("Waiting for controller input...")}
- ) : ( + {s ? (
+ ) : ( +
{t("Waiting for controller input...")}
)} ); From 757971cd6ea5b75320b7e56a987c3329908156b9 Mon Sep 17 00:00:00 2001 From: HashimHS Date: Sat, 4 Apr 2026 17:29:13 +0200 Subject: [PATCH 6/8] Refactored and improved tester modularity. Improved models --- generate-gamepad-mapping.cjs | 68 ++- src_backend/controller_family_db.cjs | 72 +++ src_backend/sdl_manager.cjs | 29 +- .../assets/controller-models/index.js | 130 +++++ .../models/nintendo/JoyConModel.jsx | 5 + .../models/nintendo/SwitchProModel.jsx | 60 ++ .../nintendo/vendor/switch-pro-controller.svg | 84 +++ .../models/playstation/DualSenseModel.jsx | 285 ++++++++++ .../models/playstation/DualShock3Model.jsx | 53 ++ .../models/playstation/DualShock4Model.jsx | 177 ++++++ .../vendor/dualshock3-controller.svg | 36 ++ .../ps5-controller-visualizer/abxy-sprite.png | Bin 0 -> 26404 bytes .../arrows-sprite.png | Bin 0 -> 3181 bytes .../bumper-sprite.png | Bin 0 -> 1672 bytes .../controller-base.png | Bin 0 -> 100075 bytes .../ps5-controller-visualizer/dpad-sprite.png | Bin 0 -> 21421 bytes .../ps5-controller-visualizer/ps-color.png | Bin 0 -> 13691 bytes .../stick-sprite.png | Bin 0 -> 19368 bytes .../trigger-sprite.png | Bin 0 -> 5686 bytes .../models/xbox/Xbox360Model.jsx | 5 + .../models/xbox/XboxOneModel.jsx | 138 +++++ .../models/xbox/XboxSeriesModel.jsx | 5 + .../controller-models/shared/TriggerMeter.jsx | 50 ++ .../assets/controller-models/shared/utils.js | 40 ++ .../components/ControllerSettings.jsx | 16 +- src_frontend/components/ControllerTester.jsx | 536 +----------------- src_frontend/styles/ControllerTester.css | 22 + 27 files changed, 1254 insertions(+), 557 deletions(-) create mode 100644 src_backend/controller_family_db.cjs create mode 100644 src_frontend/assets/controller-models/index.js create mode 100644 src_frontend/assets/controller-models/models/nintendo/JoyConModel.jsx create mode 100644 src_frontend/assets/controller-models/models/nintendo/SwitchProModel.jsx create mode 100644 src_frontend/assets/controller-models/models/nintendo/vendor/switch-pro-controller.svg create mode 100644 src_frontend/assets/controller-models/models/playstation/DualSenseModel.jsx create mode 100644 src_frontend/assets/controller-models/models/playstation/DualShock3Model.jsx create mode 100644 src_frontend/assets/controller-models/models/playstation/DualShock4Model.jsx create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/dualshock3-controller.svg create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/abxy-sprite.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/arrows-sprite.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/bumper-sprite.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/controller-base.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/dpad-sprite.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/ps-color.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/stick-sprite.png create mode 100644 src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/trigger-sprite.png create mode 100644 src_frontend/assets/controller-models/models/xbox/Xbox360Model.jsx create mode 100644 src_frontend/assets/controller-models/models/xbox/XboxOneModel.jsx create mode 100644 src_frontend/assets/controller-models/models/xbox/XboxSeriesModel.jsx create mode 100644 src_frontend/assets/controller-models/shared/TriggerMeter.jsx create mode 100644 src_frontend/assets/controller-models/shared/utils.js diff --git a/generate-gamepad-mapping.cjs b/generate-gamepad-mapping.cjs index 1fa3ad2..41739a7 100644 --- a/generate-gamepad-mapping.cjs +++ b/generate-gamepad-mapping.cjs @@ -1,7 +1,19 @@ const fs = require("node:fs"); -const DB_URL = - "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt"; +const DB_SOURCES = [ + { + name: "mdqinc/SDL_GameControllerDB", + url: "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt", + }, + { + name: "mdqinc/SDL_GameControllerDB (raw master fallback)", + url: "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt", + }, + { + name: "gabomdq/SDL_GameControllerDB (legacy mirror)", + url: "https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt", + }, +]; const STD_BTN = { a: 0, @@ -128,24 +140,64 @@ function buildJson(textData) { } async function fetchAndBuildJson() { - const response = await fetch(DB_URL); - if (!response.ok) { - throw new Error(`Failed: ${response.status}`); + let response = null; + let sourceName = null; + let sourceUrl = null; + const failures = []; + + for (const source of DB_SOURCES) { + try { + const candidate = await fetch(source.url); + if (!candidate.ok) { + failures.push(`${source.name}: HTTP ${candidate.status}`); + continue; + } + + response = candidate; + sourceName = source.name; + sourceUrl = source.url; + break; + } catch (error) { + failures.push(`${source.name}: ${error?.message || String(error)}`); + } + } + + if (!response) { + throw new Error(`Failed to download controller DB from all sources:\n${failures.join("\n")}`); } const textData = await response.text(); const database = buildJson(textData); + const generatedAt = new Date().toISOString(); fs.writeFileSync( "./src_frontend/resources/gamepad_mapping_db.json", JSON.stringify(database), ); + fs.writeFileSync( + "./src_frontend/resources/gamepad_mapping_db.meta.json", + JSON.stringify( + { + generatedAt, + sourceName, + sourceUrl, + fallbackSources: DB_SOURCES.map((item) => ({ + name: item.name, + url: item.url, + })), + controllerCount: Object.keys(database).length, + }, + null, + 2, + ), + ); + fs.writeFileSync("./src_backend/resources/gamecontrollerdb.txt", textData); - console.log( - `Generated mappings for ${Object.keys(database).length} controllers.`, - ); + console.log(`Generated mappings for ${Object.keys(database).length} controllers.`); + console.log(`Source: ${sourceName}`); + console.log(`URL: ${sourceUrl}`); } if (require.main === module) { diff --git a/src_backend/controller_family_db.cjs b/src_backend/controller_family_db.cjs new file mode 100644 index 0000000..dc4b890 --- /dev/null +++ b/src_backend/controller_family_db.cjs @@ -0,0 +1,72 @@ +const { readFileSync } = require("node:fs"); + +const { localeAppFile, logWarn } = require("./utils.cjs"); + +let controllerDbCache = null; + +const FAMILY_PATTERNS = [ + { family: "playstation", pattern: /dualsense|dualshock|playstation|\bps4\b|\bps5\b|wireless controller|sony/i }, + { family: "xbox", pattern: /xbox|xinput|microsoft/i }, + { family: "nintendo", pattern: /\bswitch\b|joy-?con|pro controller|nintendo/i }, + { family: "steam", pattern: /steam deck|steam controller|valve/i }, + { family: "8bitdo", pattern: /8bitdo/i }, + { family: "generic", pattern: /gamepad|controller|joystick/i }, +]; + +function normalizeHex(value) { + return (value || "0000").toLowerCase().replace(/^0x/, "").padStart(4, "0"); +} + +function loadControllerDatabase() { + if (controllerDbCache) { + return controllerDbCache; + } + + const dbPath = localeAppFile("./src_frontend/resources/gamepad_mapping_db.json"); + if (dbPath instanceof Error) { + logWarn("controller_family_db", "controller mapping database not found"); + controllerDbCache = {}; + return controllerDbCache; + } + + try { + const raw = readFileSync(dbPath, "utf8"); + const parsed = JSON.parse(raw); + controllerDbCache = parsed && typeof parsed === "object" ? parsed : {}; + } catch (error) { + logWarn("controller_family_db", "unable to parse controller mapping database", error); + controllerDbCache = {}; + } + + return controllerDbCache; +} + +function inferFamilyFromName(name = "") { + const text = name; + for (const { family, pattern } of FAMILY_PATTERNS) { + if (pattern.test(text)) { + return family; + } + } + return "unknown"; +} + +function resolveControllerIdentity(vendorId, productId, fallbackName) { + const db = loadControllerDatabase(); + const key = `${normalizeHex(vendorId)}_${normalizeHex(productId)}`; + const entry = db[key]; + const isRecognized = Boolean(entry?.n); + const standardizedName = entry?.n || fallbackName; + const family = inferFamilyFromName(standardizedName || ""); + + return { + databaseKey: key, + isRecognized, + standardizedName, + family, + }; +} + +module.exports = { + resolveControllerIdentity, +}; diff --git a/src_backend/sdl_manager.cjs b/src_backend/sdl_manager.cjs index 6396875..66a1967 100644 --- a/src_backend/sdl_manager.cjs +++ b/src_backend/sdl_manager.cjs @@ -1,4 +1,5 @@ const { getAppConfig } = require("./config_manager.cjs"); +const { resolveControllerIdentity } = require("./controller_family_db.cjs"); const { getVirtualDevice } = require("./controller_mode_manager.cjs"); const { SDL2_LIBRARY_NAME, @@ -175,22 +176,6 @@ function mapSdlGamepadsToWebApi(gamepads) { })); } -const VENDOR_FAMILY_MAP = { - "054c": "playstation", - "045e": "xbox", - "057e": "nintendo", - "28de": "steam", - "2dc8": "8bitdo", - "0f0d": "generic", - "0079": "generic", - "0810": "generic", - "046d": "generic", -}; - -function classifyFamily(vendorId) { - return VENDOR_FAMILY_MAP[vendorId] || "unknown"; -} - function isAppGeneratedXinputController(controller, virtualDevice) { if (!virtualDevice) return false; @@ -204,7 +189,7 @@ function isAppGeneratedXinputController(controller, virtualDevice) { return false; } - if (controller.name !== virtualDevice.name) { + if (controller.rawName !== virtualDevice.name) { return false; } @@ -242,7 +227,7 @@ async function listControllers() { if (!ptr || sdl.SDL_GameControllerGetAttached(ptr) === 0) continue; - const name = sdl.SDL_GameControllerName(ptr) || `Controller ${i + 1}`; + const rawName = sdl.SDL_GameControllerName(ptr) || `Controller ${i + 1}`; const vendorId = sdl.SDL_GameControllerGetVendor(ptr) .toString(16) .padStart(4, "0"); @@ -255,15 +240,19 @@ async function listControllers() { const path = sdl.SDL_GameControllerPath ? sdl.SDL_GameControllerPath(ptr) : null; + const identity = resolveControllerIdentity(vendorId, productId, rawName); const controller = { index: i, - name, + name: identity.standardizedName, + rawName, vendorId, productId, version, path, - family: classifyFamily(vendorId), + databaseKey: identity.databaseKey, + isRecognized: identity.isRecognized, + family: identity.family, }; if (isAppGeneratedXinputController(controller, virtualDevice)) { diff --git a/src_frontend/assets/controller-models/index.js b/src_frontend/assets/controller-models/index.js new file mode 100644 index 0000000..87b80d0 --- /dev/null +++ b/src_frontend/assets/controller-models/index.js @@ -0,0 +1,130 @@ +import JoyConModel from "./models/nintendo/JoyConModel.jsx"; +import SwitchProModel from "./models/nintendo/SwitchProModel.jsx"; +import DualSenseModel from "./models/playstation/DualSenseModel.jsx"; +import DualShock3Model from "./models/playstation/DualShock3Model.jsx"; +import DualShock4Model from "./models/playstation/DualShock4Model.jsx"; +import Xbox360Model from "./models/xbox/Xbox360Model.jsx"; +import XboxOneModel from "./models/xbox/XboxOneModel.jsx"; +import XboxSeriesModel from "./models/xbox/XboxSeriesModel.jsx"; + +function getControllerText(controller) { + return [ + controller?.name, + controller?.rawName, + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); +} + +function nameMatches(controller, pattern) { + return pattern.test(getControllerText(controller)); +} + +export const MODEL_REGISTRY = [ + { + id: "playstation-dualsense", + component: DualSenseModel, + matchController: (controller) => + controller?.family === "playstation" && nameMatches(controller, /dualsense|ps5|playstation 5/), + }, + { + id: "playstation-dualshock3", + component: DualShock3Model, + matchController: (controller) => + controller?.family === "playstation" && nameMatches(controller, /dualshock\s*3|\bps3\b|playstation 3/), + }, + { + id: "playstation-dualshock4", + component: DualShock4Model, + matchController: (controller) => + controller?.family === "playstation" && nameMatches(controller, /dualshock\s*4|\bps4\b|playstation 4/), + }, + { + id: "playstation-dualshock4", + component: DualShock4Model, + matchController: (controller) => controller?.family === "playstation", + }, + { + id: "xbox-series", + component: XboxSeriesModel, + matchController: (controller) => + controller?.family === "xbox" && nameMatches(controller, /xbox\s*series|series\s*[xs]/), + }, + { + id: "xbox-360", + component: Xbox360Model, + matchController: (controller) => + controller?.family === "xbox" && nameMatches(controller, /xbox\s*360|x360/), + }, + { + id: "xbox-one", + component: XboxOneModel, + matchController: (controller) => + controller?.family === "xbox" && nameMatches(controller, /xbox\s*one|elite|xbox wireless controller/), + }, + { + id: "xbox-one", + component: XboxOneModel, + matchController: (controller) => controller?.family === "xbox", + }, + { + id: "nintendo-joycon", + component: JoyConModel, + matchController: (controller) => + controller?.family === "nintendo" && nameMatches(controller, /joy-?con|left joy|right joy|combined joy/), + }, + { + id: "nintendo-switch-pro", + component: SwitchProModel, + matchController: (controller) => + controller?.family === "nintendo" && nameMatches(controller, /switch\s*pro|switch 2 pro|pro controller/), + }, + { + id: "nintendo-switch-pro", + component: SwitchProModel, + matchController: (controller) => controller?.family === "nintendo", + }, +]; + +export const MODEL_COMPONENTS = { + "playstation-dualsense": DualSenseModel, + "playstation-dualshock3": DualShock3Model, + "playstation-dualshock4": DualShock4Model, + "xbox-series": XboxSeriesModel, + "xbox-360": Xbox360Model, + "xbox-one": XboxOneModel, + "nintendo-joycon": JoyConModel, + "nintendo-switch-pro": SwitchProModel, +}; + +export function resolveControllerModel(controller) { + for (const model of MODEL_REGISTRY) { + if (model.matchController(controller)) { + return model.id; + } + } + + if (controller?.family === "playstation") { + return "playstation-dualshock4"; + } + + if (controller?.family === "nintendo") { + return "nintendo-switch-pro"; + } + + if (controller?.family === "xbox") { + return "xbox-one"; + } + + return "xbox-one"; +} + +export { default as DualSenseModel } from "./models/playstation/DualSenseModel.jsx"; +export { default as DualShock3Model } from "./models/playstation/DualShock3Model.jsx"; +export { default as DualShock4Model } from "./models/playstation/DualShock4Model.jsx"; +export { default as JoyConModel } from "./models/nintendo/JoyConModel.jsx"; +export { default as SwitchProModel } from "./models/nintendo/SwitchProModel.jsx"; +export { default as Xbox360Model } from "./models/xbox/Xbox360Model.jsx"; +export { default as XboxOneModel } from "./models/xbox/XboxOneModel.jsx"; +export { default as XboxSeriesModel } from "./models/xbox/XboxSeriesModel.jsx"; diff --git a/src_frontend/assets/controller-models/models/nintendo/JoyConModel.jsx b/src_frontend/assets/controller-models/models/nintendo/JoyConModel.jsx new file mode 100644 index 0000000..1bcded8 --- /dev/null +++ b/src_frontend/assets/controller-models/models/nintendo/JoyConModel.jsx @@ -0,0 +1,5 @@ +import SwitchProModel from "./SwitchProModel.jsx"; + +const JoyConModel = (props) => ; + +export default JoyConModel; diff --git a/src_frontend/assets/controller-models/models/nintendo/SwitchProModel.jsx b/src_frontend/assets/controller-models/models/nintendo/SwitchProModel.jsx new file mode 100644 index 0000000..a415ffb --- /dev/null +++ b/src_frontend/assets/controller-models/models/nintendo/SwitchProModel.jsx @@ -0,0 +1,60 @@ +import switchProSvg from "./vendor/switch-pro-controller.svg"; +import TriggerMeter from "../../shared/TriggerMeter.jsx"; +import { + BTN, + getAxis, + getButtonValue, + isPressed, +} from "../../shared/utils.js"; + +// Keep this model asset-based using the Ryujinx ProCon SVG; do not hand-draw the body again. +const SwitchProModel = ({ s }) => { + const accent = "#e60012"; + const stickTravel = 14; + const lx = 213.37 + getAxis(s, 0) * stickTravel; + const ly = 206.39 + getAxis(s, 1) * stickTravel; + const rx = 623.77 + getAxis(s, 2) * stickTravel; + const ry = 345.09 + getAxis(s, 3) * stickTravel; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + + return ( +
+ Nintendo Switch Pro controller + + + + + + + + + + + + + + + + + X + B + Y + A + + + + + + {/* Redraw stick wells to mask static sticks from the base asset. */} + + + + + + +
+ ); +}; + +export default SwitchProModel; diff --git a/src_frontend/assets/controller-models/models/nintendo/vendor/switch-pro-controller.svg b/src_frontend/assets/controller-models/models/nintendo/vendor/switch-pro-controller.svg new file mode 100644 index 0000000..53eef82 --- /dev/null +++ b/src_frontend/assets/controller-models/models/nintendo/vendor/switch-pro-controller.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src_frontend/assets/controller-models/models/playstation/DualSenseModel.jsx b/src_frontend/assets/controller-models/models/playstation/DualSenseModel.jsx new file mode 100644 index 0000000..d904491 --- /dev/null +++ b/src_frontend/assets/controller-models/models/playstation/DualSenseModel.jsx @@ -0,0 +1,285 @@ +import { useLayoutEffect, useRef, useState } from "react"; +import controllerBasePng from "./vendor/ps5-controller-visualizer/controller-base.png"; +import stickSpritePng from "./vendor/ps5-controller-visualizer/stick-sprite.png"; +import abxySpritePng from "./vendor/ps5-controller-visualizer/abxy-sprite.png"; +import arrowsSpritePng from "./vendor/ps5-controller-visualizer/arrows-sprite.png"; +import dpadSpritePng from "./vendor/ps5-controller-visualizer/dpad-sprite.png"; +import bumperSpritePng from "./vendor/ps5-controller-visualizer/bumper-sprite.png"; +import triggerSpritePng from "./vendor/ps5-controller-visualizer/trigger-sprite.png"; +import psColorPng from "./vendor/ps5-controller-visualizer/ps-color.png"; +import { + BTN, + getAxis, + getButtonValue, + isPressed, +} from "../../shared/utils.js"; + +const SCENE_WIDTH = 807; +const SCENE_HEIGHT = 651; +const MAX_STICK_MOVE = 15; +const TOUCHPAD_BUTTON_INDEX = 17; + +const abs = (left, top, width, height) => ({ + position: "absolute", + left, + top, + width, + height, +}); + +const spriteStyle = (url) => ({ + backgroundImage: `url(${url})`, + backgroundRepeat: "no-repeat", +}); + +const DualSenseModel = ({ s }) => { + const hostRef = useRef(null); + const [scale, setScale] = useState(1); + + useLayoutEffect(() => { + const host = hostRef.current; + if (!host) return undefined; + + const updateScale = () => { + const next = Math.min(1, host.clientWidth / SCENE_WIDTH); + setScale(Number.isFinite(next) && next > 0 ? next : 1); + }; + + updateScale(); + const observer = new ResizeObserver(updateScale); + observer.observe(host); + + return () => observer.disconnect(); + }, []); + + const crossPressed = isPressed(s, BTN.A); + const circlePressed = isPressed(s, BTN.B); + const squarePressed = isPressed(s, BTN.X); + const trianglePressed = isPressed(s, BTN.Y); + const dpadUpPressed = isPressed(s, BTN.DPAD_UP); + const dpadDownPressed = isPressed(s, BTN.DPAD_DOWN); + const dpadLeftPressed = isPressed(s, BTN.DPAD_LEFT); + const dpadRightPressed = isPressed(s, BTN.DPAD_RIGHT); + const sharePressed = isPressed(s, BTN.SELECT); + const optionsPressed = isPressed(s, BTN.START); + const l1Pressed = isPressed(s, BTN.LB); + const r1Pressed = isPressed(s, BTN.RB); + const l3Pressed = isPressed(s, BTN.L3); + const r3Pressed = isPressed(s, BTN.R3); + const psPressed = isPressed(s, BTN.HOME); + const touchpadPressed = isPressed(s, TOUCHPAD_BUTTON_INDEX); + + const l2 = getButtonValue(s, BTN.LT); + const r2 = getButtonValue(s, BTN.RT); + + const lx = getAxis(s, 0) * MAX_STICK_MOVE; + const ly = getAxis(s, 1) * MAX_STICK_MOVE; + const rx = getAxis(s, 2) * MAX_STICK_MOVE; + const ry = getAxis(s, 3) * MAX_STICK_MOVE; + + return ( +
+
+ DualSense controller + +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
0.05 ? l2 : 0, + }} + /> +
0.05 ? r2 : 0, + }} + /> +
+ +
+ +
+
+
+ ); +}; + +export default DualSenseModel; diff --git a/src_frontend/assets/controller-models/models/playstation/DualShock3Model.jsx b/src_frontend/assets/controller-models/models/playstation/DualShock3Model.jsx new file mode 100644 index 0000000..e8e0c22 --- /dev/null +++ b/src_frontend/assets/controller-models/models/playstation/DualShock3Model.jsx @@ -0,0 +1,53 @@ +import dualShock3Svg from "./vendor/dualshock3-controller.svg"; +import { + BTN, + getAxis, + getButtonValue, + isPressed, +} from "../../shared/utils.js"; + +const DualShock3Model = ({ s }) => { + const stickTravel = 10; + const lx = 143.4 + getAxis(s, 0) * stickTravel; + const ly = 154 + getAxis(s, 1) * stickTravel; + const rx = 258 + getAxis(s, 2) * stickTravel; + const ry = 154 + getAxis(s, 3) * stickTravel; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + + return ( +
+ DualShock 3 controller + + + + + + + + + + + + Y + B + X + A + + + + + + + + + + + + + +
+ ); +}; + +export default DualShock3Model; diff --git a/src_frontend/assets/controller-models/models/playstation/DualShock4Model.jsx b/src_frontend/assets/controller-models/models/playstation/DualShock4Model.jsx new file mode 100644 index 0000000..ab38111 --- /dev/null +++ b/src_frontend/assets/controller-models/models/playstation/DualShock4Model.jsx @@ -0,0 +1,177 @@ +import TriggerMeter from "../../shared/TriggerMeter.jsx"; +import { BTN, getAxis, getButtonValue, isPressed } from "../../shared/utils.js"; + +const DualShock4Model = ({ s }) => { + const stickTravel = 25; + const lx = 274.65 + getAxis(s, 0) * stickTravel; + const ly = 355.967 + getAxis(s, 1) * stickTravel; + const rx = 531.25 + getAxis(s, 2) * stickTravel; + const ry = 355.967 + getAxis(s, 3) * stickTravel; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + const accent = "#006fcd"; + + return ( + + {/* ── Base body (from e7d/gamepad-viewer ds4/base-black.svg, MIT) ── */} + + + {/* ── Grip shading (left) ── */} + + {/* ── Grip shading (right) ── */} + + + {/* ── Left stick well ── */} + + + + + {/* ── Right stick well ── */} + + + + + {/* ── Touchpad ── */} + + + + {/* ── Face button / D-Pad background circles ── */} + + + + {/* ── D-Pad arrow indicators (from base) ── */} + + + {/* ── Create / Options button wells ── */} + + + + {/* ── Speaker grille dots ── */} + + + + + + + + + + + + + + + + {/* ── Light bar ── */} + + + {/* ── Trigger areas (from base) ── */} + + + + + + {/* ── Trigger 3D shapes (from base) ── */} + + + + {/* ── PS button (from base) ── */} + + + + + + {isPressed(s, BTN.HOME) && } + + {/* ═══ Interactive overlays ═══ */} + + {/* ── D-Pad arrows (from e7d ds4/dpad.svg, each path individually positioned via svgpathtools bbox) ── */} + {/* Up — path center (17.71,25.50) → target (154,207) */} + + {/* Down — path center (55.10,25.50) → target (154,281) */} + + {/* Left — path center (153.05,17.85) → target (118,243) */} + + {/* Right — path center (100.26,17.76) → target (191,243) */} + + + {/* ── Face buttons (PlayStation symbols — e7d ds4/buttons.svg paths, scaled to 70% of r=26 circle) ── */} + {/* Cross (A) - bottom */} + + + + + {/* Circle (B) - right */} + + + + + {/* Square (X) - left */} + + + + + {/* Triangle (Y) - top */} + + + + + + {/* ── Create button ── */} + + CRE + + {/* ── Options button ── */} + + OPT + + {/* ── L1 bumper ── */} + + + + L1 + + + + {/* ── R1 bumper ── */} + + + + R1 + + + + {/* ── Left stick knob ── */} + + + + + {/* ── Right stick knob ── */} + + + + + + ); +}; + +/* ═══════════════════════════════════════════════════════════════ + Nintendo Pro Controller — hand-crafted (no e7d template) + ═══════════════════════════════════════════════════════════════ */ + + +export default DualShock4Model; diff --git a/src_frontend/assets/controller-models/models/playstation/vendor/dualshock3-controller.svg b/src_frontend/assets/controller-models/models/playstation/vendor/dualshock3-controller.svg new file mode 100644 index 0000000..4e66fe9 --- /dev/null +++ b/src_frontend/assets/controller-models/models/playstation/vendor/dualshock3-controller.svg @@ -0,0 +1,36 @@ + + + + + + + + + +SELECT + +START + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/abxy-sprite.png b/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/abxy-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..725e8e17b1bc6af898b65f4e1cd4f8a89918312d GIT binary patch literal 26404 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 literal 0 HcmV?d00001 diff --git a/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/arrows-sprite.png b/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/arrows-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..d235e3f7929d77aa4ee796302e5cc5c86091def2 GIT binary patch literal 3181 zcmV-z43hJSP)fU#0nC@7%UTu-QZ-r%zFR4 zJG0Y0(|!B?RvjK@X1C{W=kNN3Pr46Xb#9&d?m2bNsj4Py1rcfibQvJD+h{kN7DTe; zdu2d`ngGuN^!QsKJhOq!Tfq09fiwVV1FPmnTaDioQ2<;3t_#Q*knRWa7LhHH#>RSY z+;E**&6j=qTp2(air39oNgyOJafozeW+s`OoXl3{_E2&y5xxcZog^8zO6^A`hLPVz z7|yqW{KK-Kh|n(sUKC?LkRKtjl*61i5Z#kT;J0SYrSn3|;RTb$m0zYaYArakX@IAY@7^7Y{dtGzq zo3Ouz(R{Jn?Pk9Jzr^TkkZ*~2tt>;V5fdAty8(080H(FF=0GH~M~~)}ecrr)W*GK5 z5ef+H*Vc}(10murTe(HVcO=WRBJJ%nz!4FtL1f+vU8U4~5Z3F1Hn8X9P5?T9xV~?- zXoCU<#u$>KAd#V`C{P$f!iCn_m>{s>PY5>S~qqZZ{d}c9Uo@Y5VRpRZpIjgVWP=wb7_}`}T!y3^Ms*gnGSSW;ynt z{EGJj`iK=fPDDN#!x*Yj2uei3O0iUa32-wWD!*vWsMG@J7N9y;uF&k0Pufcxg!NAY zDp<~YF<>qMdk!LN(G*c3;A9I3avgb!LVj4`?1e}HB$L1vh4V@h-fY4f)S-JJvMKhs z<@tY&ZIwwc4F(@6h)xjj5eWAQ=58DWZ-0wu@L^970Gc9INs=FiaD&K(1YBclr(X-h zTMv70Q*up_Dka&=B62P>_Tx#BBP|S=0hcwZu9FbcB{KMv^Hu*J%BKW0qvU4+d_f?e z>Swu|+XBV{o>mAP2M+UF1iTgp-d}9=p*JLVZjk=Gh5YecG&p#t-$O4=*KOQ1F-9sr zj@I{~;yC|oV1MbaE0r6ikc#6+A^i10o__NB!XmnH0<&hfLjY>NhlAA`#w(TUB=qw6 z(EFRs$%)X5gXBx8F~9fj+&x5Dy5&R&z~Oow$3{l(2=>UK&(`}+14RTh*tH$Yn|KQ%ele1B@EB(IK_}IaNIWvCzVx=;*RLh8v z2z~WxXMWej0N_r)kHsYY7zXLD?DX9*PJewqj(1HA0Ft7>Ot<4O_z!{QLhZ#t`q_S# zJ$-Mm_sD*2p#udq^w?=At{Q- zey*Q=e7o;liR0&@JUbNU`JVL)fVsf{0)Bx3Km^ZslX%Yqub>mhuu9Hue@67#&Y(Q~ z%UphPW&dy_-NRa2i(Z-mBF}DDUWlISrCDkElfnSN5pIgOA`+S%hW>}4zmy+;bUU&w z5eDHC{cYsb?P8hBD7T3+BI;^hcRaCcBt?PT*h9@YKDL$Y;DHOUttwutJV*EC&?&uh4Z%RnX5e0J5cPGU&{bY0{}s$YOYtRg5_d+Ys(3qE!%Q6*9DO*RVWOes(K|`F3ljoGrziYu(Kh4Mx-Jl zr=t?`+c2uG2PRq~ccrQ~uEm!qC5M*O=1Y3Hg(oa)B=*;jD{PG`no-W;Tt_v34m|Wuf{~5ZDA=>kO{e5J{ zAT!1OW}2Q@X|qvwI?nTIQQ}TOF(c4Ux>#}ES!r8E?r({D%ygyVlvo)yKH&bK0s_9dwtd45oQS-7uvXpP&Eyt1RI5RNuMc16ULq<8 zc=d3tw&(E)go&^Uz0ejfWv7 zM@Gce`uE#`_vI~TDMIH4$(Lhees`wZ#rzgGO^OJI>vbHh*XO`^=|T|v(`K^Ua){3- z$#JssmBL7KHtJI+Nf75b3TvU5;VFgDAi#K~g22&lg7C$55PW+(W3S-FIQ|R?eKpFn zADW8>=q4#Llfzhn!;HXjmUM)naaesr!MyTXrE>jYWA4b?Ki^AFD~->I$WH;71TYSu z1K=hB|0*OG+rIyu-Tu*~CBj#dCE2ypZ)K(&szHoVJD(Wxv9Cm#&b7_avs14f${{+3s58= zOF%+F{Q%N{CIG~iTlWFzzq&U7)#Z;>fEADhfFgiufchYmQm~b|T z@rml-V0EPi+s3%Plau{^JbqA=?;z3x5V{qFn*f+@Yik7p&_|2`L+B^Q01+&a4j8J$ zi7(VifBfe};>D}Kza=S_N(Eu>Zh_uOiaP*&6@b@mU8})F7$8ED8A-!PFeAl`G#jK0 zgI8%Qs+ep9_x&lGJO%)^Lz*{Cu2M;JD0hMJbyd2@0bhnH_1emd5g~0DDKE;J=hduP9VS95W_}G|576TgH#9YS3mJV7ME65+2tBgU7(jvo=zrJ_#Ck9ZjWP?}>@Hf+`itm+o{Vhe_!nK=%XG%-g!a<>*N-o0M9fmqDKJQw7QA_B)MtOpfOj>N^T%~ z+X`5c2ivv;i>2zmh@)?k;zA898f_GX^=s7&K;EMA2HAirksajksoXJ&a~_ z-AlUum^W?bcDJl_YDpX90bDyx$6Xj&` z)M=txEB&=Jqq$0DFNOL;B77S_{J-yK45Mf)Mm&pxwL3D?bE>E-LSNmyFDUye{|Zzl z0>zog^1E*i4Ez;Dn|2(u>I{!ePv;|JpN7&AT(u?k5hHI6My$oCXOYf;yTY_)OyI8?Q4{; zDmAAI4V6*=`Pg@OPmI1E))H@z7ih8D`Y!W7#tqNR9DvX-Vnnx(dLFK`1|!znR?jI> z#iDB9i&W~OpN6XE8YHKE&pY;3U*Ct_*4GN3yP<0LRH{E6W&Yk+%%f3jw|t5oDRe<( zsUdp77k4gHoF(im$8^PX1AbO3SdbzSdysR5PTFW zP6@e}{bcfuaiXZ(`r3g7fIXhPQnazLZi{IJUqu#0s&&$bK| zV(*@xA09S#U$=#WKqPQckb+x;H5Bhzj<{i^^eUEx68G$jI|r*t4h^HQ+1W`x{Lk%l}!BIc8J^M z+E`2Uf8VD$RaC{zEecN5;hqT-iD$iG#9xKmLhq^&m?{}-nX0i^(wX@@XIdz+Y-qyk0q18?Y5v@TH`<45sg&N S;eP%A0000ZwNOuTJr!*oZt&}t(ozk#$cXxM6*YAa& z_xBI@aX81b9=-SO+?i))o_TKQJ4I>SCr_V%Kp@<=GLkAF5DGI0gdB#64*bm?=pqF8 z1JzOdtr{lqv~;TrZo_tE()nrn-;hK?aF| zN=z}KltG^;32cx{l%bVkn5ERsgdiL75NCQz6xsl=k>m@!rSIUC8kLl@euFqvDH$|y|9~$Ls9iR2p0~5tFP5;b09^Bqu0F{ zyutx;N0M<#c{*jmOcuIj7pLNLKIb^7`}-O_Q3^wdS)TFw`r5XfeODCu5)1UH7&ahQ zsO^c~k_-IXZ{2I6B6ojBGheE9Jg$rFJ74F*Pxd76f?!w)sb}qv z{F|E_PU$*^AiyYEn#Mdyigaz9$d`=1cy)6Yq6jJ?(YoJ8lMNea;+SYlhoM`O_|(F# z&=UpjFg$0)L@r}HyF=#CtqY_qqy`eQi=~a1Q-OjU-jl*TouONfe?A=GqobpDhzoW8 zJAQ~t)>?JQ7MXI{xNlQKQQQ5_loKbFQ%A^_D3!45(O`>*N+M`V(7+S$p*jWV*`y>q zazGXvoEoOBAmWk~{51^Zz1sX4#V5F1wFS6Da#K16o}|9VZ-O~_uQY4AGo>nOsap*9 z;v~2eP2Oq0Y#G2>?T&y5y+U5%1ZDZF>%r9(=4?QPxK7bKwL6~7)R7OwG&Wtp>DblW zJvmVrj*d|<@I0R_Ia53ybfMI!Ga?Oizf-NisZ!};MjdWZI53MWr??WY>Y$qW-D+p=!&HiQ=r1R>A}9f#^f3t*iTCSUN=nat@lFl- z<>1Nt0d`H`TVw$tB@_T&^j$Sk@LiC@&-~r!S{R}Bi-8?EGZUqGK$8AATg2nMt-{vt z(LOuPnUSw7vsCh%FiH@+wKlu0^i&59^Sfd&xN0-psm5GI_D%Xz$;f$ zQW`XcuE&62cddwXv*OR)q2-F2V+f8#4hH#P9erX*(FsSp;iv8PFfy9c-;ch|<-`;x z>!pWzj5L33JG)txV4xUSXR8JgJD)CA@;(U!zXjRpS6YrwKG&-kSYwOQin3objxM#D zEW(4X8-Z$DcXo2juf|7pE3rn1NENVl(^RW=jbqYw_xJC%n>+qp2F7Dx27*Pcd?rg1 z4r;wk#5`2$_p&vFL?PpS6vCQfl97v_IwAY8xTHW7yAXXzz)%3BFM{e@o;%V=Z3RNc zk9sI@v7y21NkQ93mKvdWk^RS@C=~m{&nOtFr)rPh&s8oD-M^*)g}<&zIWOZf5pf8WjTFts*t-s5`Qk*v`s2T?Vw>Yt#2=2VR+N*9RK#a7Ey1Iy>48419af#p$7q zEwuk^vDlmVin^>^Jj4UQ`m_s@R(4DQhqC7Fg9BUSBN73sn38^Tn4&M9x6|EP@OB8H z%`inG#3?KGrC{qQA9Cd$nz!fVPcUi(&RhZlVzM zYJ1xEf6J?}o*g1tmp27-Ot?+Mb4B2U#N5ZD%hjCEb1nF%aja)4O+?hu#8NGooyW$%x^~MRV3kXl|Kf)-V^J8MtYf1 zTwL5+_L~8ujnts}!Y27#k)P*hjCXc{gc%;VYHMqZz;t$hQ9Pvj?Bc=)p(@CGxZKr^ zY;tqHFNbuCpHeFp9a=KvfR<}(F0Jf0BRT%ltxyZ|I7#NqV=l^hB0qYo^yn+B6(%I( z7h&;|(T6W-FlF-3>x;S{`iyx}W>SncNcNK1-{BxGi9mAm>*qTGv&)z;Qm$VpDprsK zE{CBnUnB7Ni&~D%fbd~j{9y#FocMHja;ygzu$o09PE3PCj%DqkC?p*d3sW)CjE?!-2nEXY(+d|&=Sk^ zaaq<9_FWfHoA1%xx7Q|}H?^uMh%yp5J-gnSnMCmJjMCZPzry4!AxR03mpWDN=7>UY zM?!TIdW()VZ~y7pzL~6aERi_e$@K^b6h}EmsmHh{`kb1Ys)JO||2uV{Ti0Me<tZr;Pwh$MjNDwa^4ODS z^s?@OTJwwZKlO@Pq=u+*o5b3cg42Gptn0y&)qWwaE?B*u6Ao_xZjpMnkNODdYOnS6 z7cG0s6hSfqHqpG5Y`;WK>H8woGZ?H7mfq`2OUGV??s`x+>tf(h39L*Lm@c$L$Djqo zo^Xb+USEpYy{Slg}>)< zxudOzW;b$L1YYrC9C+g$T0h-hlg4%~Qu8M30VeO}65w+!cZx*J2GMvf4JEaDXBj(r zS)4GN%(Zp*ZbvEABD3=8_b4bRfv?3LO`iGI#l?j_PBw)(Bir}EfU~k{S}*2Sdv-4m zW;velO||3jl@=`k;7CVXnUPOH-iulr-b)2q^Y*L!Ap;e7E^eAax)3DFV<7;}7-ooARbpi@#^3B_N9pHH2XWQxgc=@qH#O+yHKwOl%slQvF3*mY~RsbB_Gt+)wW*6 zss(=PBDXUFyTRKfO~Kh8r3BsW`QRYeIy)0!!JyCqp=AA5dTaxW58hSwRZghezt-y+ zd}m$urjCEI;3odT5@J401~MUI=vMi`NlCb=SOLDlI*g~49_sz4a)8g^=B;HuqBuPE zgy6{W(WqnS&;VE7C7aASzV5+z>C(Z0Yuh>)5$v|;W_&#E=eGUb!AAcNN>38rQ{+^5 zzhg&7hc{mHOnN`ufDPaK#~_^PSotsNHr(f2;zEygc-c{@mnZ3~v)Y$OK)T%mq9`UL z{!gzH!*$%0B4m~-hv939;O8_6VkiHG#oG>U4DHE!yOm}Lm6H%X2|UH@%5+7ADIOnG_XIVDm`B}(f*kQMBp|P!{xE{ zp1&*TrWNYfgF!IZrVlH)~V${Oqh!o~XHxa!N3%llV!XzGHE-E3UoX zrO4R9q5;vR_BLi+u=Zl`qzKq8Em2%jBo+7HB|e?~yd5D$EVOQ2TrRN9jF{MepQ{Z~ z^s{#7fV&Flr^K7wm6;it>NO8^3ugn+7#MP&GFe&jp3n?ZT^e@Z?dF6;rF)8rUUQXKaWB|Z2qlj zwLhle=#eaVZyTIk1M@RCR%?r)*|tLPt)T*K$m6-m$r>6O<|Y5~fa~9ibSb(?DcoA? z6JLq#JMCS37+%&+6jqX?1bQ3IF~Y7s3IPWmd?1a{pqHprH%_ALy#z~6?=Aya5PVZ> zD^v3nBDT-aHA_5`__(y^J6W4j(OqK(0jzS9@fq2=2-_#`vte`Yb7z@bLmyxI;!9dI z_u&FI@xi3O9;*SL=>jYVgR;-^%G$zTn(7oxLGIzY-Jh56CbjPGFnDi*v{KldD zyB+D_WoH)F@#Re%!IUUVc#TO=?Be%sV`@B-wm#_N`ES3U%4y2R3VqA)w{6TVx8;Cu zRTWAGYdo~B2^6zs|K}nG9AN8en|kZ_Q{p;_$$4k2^M_j9RP4v$!()(tCeO%H5LhmB`B?~dV?jgR;7 z->#{B4<*RSZ0oED9iE(Q^n6CpEgF}pZ44h(&4`%HxcY@}su8DXT=}T1Ra13=( zh@RKkD1kG+@?oah#LL2e1>aw-joTHnILsbG`*^M`UO~cYrjE3k z!JsV-d|poa-8>gWkwWaw-skz9?OUdp!yglgNiijkJW62k+uwK7oIFdF-v}xmTBVi7 zyvNmo2fT?nUpOdw6>r{D8rwfI@SfX5W2gFTFX6yY_=gy`$UFqyorL&+)25RHvyjR%|_gLmXW>_Twr!K>51|NhMw2(FafZN z^tQJLZ<4%HI@nYZz_QyVBfahSd}40#1BY?H%OuB_-?-?9>O-3TRm4}0KCBb{JetX z-&dRddoS%J-&&326Y&YW&qP?6AnXQZvr$SD?zte|K6286|8v&p(tT-PFC=jPD|OjQ zzMAh>fd^h{83^XM-n%P*fPCUs6HwIvE_szPrn?%6C5`K6xqfSb;*ft)xl4P${1)b@ z&?v06b#yQJ(G+n2KW4YqX5+$I8Yz~97B@4gy?ZpO!v^uji`(@m2d~&P$|ec|^n@c7 zT^mMykH0#NAN4`WdrGrEX?RV-m4bLr$|VH0J3{J3ek2M||rxRtMGKCsLPOu}e zzUtHXsZ`(ob)EN5i%9o49$BApY>g%Y;*p6*JS0mWE;-pD*z31}2IEsSdytOmk09JX z+D#a!XZVFDnnI18hi?t=B-!y`(+L2$BIpBLISDAdC;id+i!uEdS-+^Nzq=fpXZnSg za)T)p${-_69L%xzs{Ze3>f;_cDj_UTVJiQT%9BklN|FpjdH7;$S{fe2OY^L$V&D;{^|#uAaO|*z z;xWu?&9_?00DVE5o##;*3H3|r9d)Ru_$jc@(19CCf=}mj(RW>4Yk#^rDGH&arE;#)?W$vecsb4o%rSTl`Cs(3VeL5|r z>8_`KW_RkZEc|j_B1)k+4zJ1M=-cL2q?n}W23trzC6JlK0>GlSKF*@@-UT271KkT0 zh}xo3hv(Sxnah(WS{Q}u%FWcYk5GFofvH?YBZlo8aot$eV@KC-l?r|ljbC1UsvA6T z8yd2(1C7mR16JhbNs>ruJ0x!jReCuddX@$Bu8RmDMF8L>t zkm_Hd#Y*?>4)e>ansWu$=Tm52=OYBh3G>)q%A{1A-|*4`JA(nln=2 zpd@JnbCPg?=IY{ml=!O~!QrUcl4yd+2Vw}h#zhxh_(98oS@TlIe=aF$d^ycqJ%21` z^`y+m`%;hvuvuB8*u~le38SbvwMr-Wx=(8HArNn`$(CWI|A2KWOh@c|BvWCnR?NBVyP!lOo{I;BlG1t5 z`!wG25aB=MM#ftQ1ZF2~M)g#hzj(Fc+@Jh7NwQXS2q#1gFH)!IZrU#D_Xaf(*0!5_ zKFuFHqTf8aIiWCFHT+!pa$o3J_A0_s$bZk47@~KI1gWF0YS!Er5pY7CdZ?DSY8xW~ zARC+{Z_44T0JP0fCQ%5&_#Bg7O!_XYBo2t8NZgh^zWU3dmjAQIi*-~*e&s#2DP%pM zN+rWYoI77`Cs{yjrQ0|OS1qdxi;L}?KAI(JJvt!J{NVyr+k2l)JII_3-0?lz+sN;4 zA$;*BbZLVO3B4_4>>MI|ITrK@y>vhNTVM1;uUoHI$WaCVRU48WY=ym?v#+6nhNYOJ zFATnK#16u$B158j*SWa4Ka2lNA8%r@F!8oQY(N1PHLS!M3k3A)NH(AQz9M1@1yKnU zJOt(np4r|~SCzE1^d=33EB003HC<0zlk(dwpn~9VczS7R>4(n4!>riWa7un)<(w@! zrWkWOyI=!(+#uJ&E&+7Di2r34u6-5*%4%II2&BEMtLp`lvk3Ir$o30u9i80iX=}c) zmKHIV?4JFsSm1w4!&092e#%vq3a#2=e*<$05F|buT2?+p0zGtUQ1BK2ZKmxf(Irv) zKpS&xkC28Pcd1vb;{@s&8qypkPIL8c<$H;3hTM#VSfJO>6A-}BDsGg2bq-(wP@szS zrf@G+BE<{1E@?3y`0m zL%8_()FV7x2gd-<%cPS!)(4Qc5=GDK>u=WgfS*`7-(*G-AYDWDKdl4R=2JTa27<;fS zodepcyw1IQT!kZCh4rceo$m}ww+==tzM?l>`Dt_j#l^A{XDxQLw@+>ftQMBM7^hNy zXdKbZ*5ZiDy3dcLqj0QKSW$JTko7Ay;icu32SG;oIFZ z(~%OkJ4WDu|G)Ai8tbu4^U_GqjN_ANM0iu}m zoSMk2X;0y7faF=zjs4Cym6OwdWqWId2(uWG@&X3H5js`GbOr z-RXT`1m#xzmNo7>)8aIlOWLisY^&{sV1%GYLovOn`f^2);A^HB zEu@bMv3v#u*k(x@0u%U6?};D~gS+4A(!}-f#GKFqjbi$=K@<=W90ufXVw-UPy9UgD zkuY12*nU%PKE9yRQtq#jkxHNB_lVf;$RQ98n%i$Oj6)+MEBOdrQqzRHfR+ad1#_bG#$pU!s1Pm&Jz z_xH_hZSflLH7J0w3sAWjhjb0S)p7Pw${k&Y7XmED$=MHnyZF(%dh(}FTl;_Ti%H64 zt7z83^X&p!hdV9qhALZN&J{pgJXyaXVX!|xnH?+SNy$}?o}1r?6aa2GH*+B$(X0CSvxY+Rp`Wj=#^>ZE6v zQm_!DUpbeonLh_=XiAc5-H+@1CZDPzje%X7m%V9RNs$ANIv(auQ@iFMh=YrZlqg(U zkeSI~{_bR7AM1ZG7EPCtxa%Qx&JisW%e<@?9RtAD+NqliwA6aK?4(gH@*b3EyPBCJ zg*Goe<`!^pYs_?j5X-tE=jAPNRPjOebs*>NTAkFj`LwzqKwU+sj5WTMmxXU+WYlF^ zKH8JwRte$~78W*<>W)aC0!sE19mjpxfOj4GtN(Id$cS>QcgzWk^E+9=^*m1+rY?9$Q8;M4Von@@~4aDLVe;8ze?KLpyo!CHtt|)(Vy>E(g#p%GGzl|4g z0}_=g8ol$5Byp+$6q@mc|I-41Gxm}N_EW$a)~f+vE`{>F=1WLarp1NlF8Z{+F2vw8 zLVITjjw}Ssp!p6Ln;tYZrA#+IZe|V0KzZX*W!Bqvf0p-JQb6+KB51X|omP)8V>7`G z_8S|7%3Bsp&R$J7OCtZg5SQzgsr%q9T$q}3_BnLD;yOG$e4PyQM~>Dj@hPsa&?J!Z zQF(W5K(^WY?;U)zFi>;ZZyb=b;tr-#)y`LyUqaU34n9CI!!!kUw>QjdodyE%iVu~Y z07FCVT!1M74GRt^XH4bsyzPo zv;1mKNgW_Gz@#H*JGmO>0Lx>mHPBS&A;Iv-5}O;kPJe>j6j;gmkK`1*O*?6|q9(|a zaM2^+)5w1lY|EOY@&&j(#K7A-s3@v8O3(p{Inhgn-|Dc|4B7)O=a+QGqE`kNKLPGK zh{eX-82AE-A>_XI0W)tCQC7_!GQbs=n`iUk`m)Yo?+b!J?QQ${-3loqQ)h$Ctcbno za%QZL{&({Y7X9Ii66VEB0vaUn4Dwpo+{qpM@{B_^DXl0E=C^azeKE3pJ@8*bUGJ(J zpx2ISz-=u~)j$;osO&!>8j>tsInv|H31`XEDA5B`^I|bNPP2)oNNI@E6Tk6*ujwN6q0ob+-^FrQQPx@O!fx?XHW_WheZBn<~*@ z+!3V7|FEjXA1U9Hh4N?e{Y1QC-<5@PkMGL z1$*_s=s&^tbwRr9Y^F3a#YwdukM?}k#GB9?*CYq+jgS?lmS!jQHz4Qwx`gJ@1U4Va zn!obh!BK=MMrv$L6f*RQ9e+D~R!rEcu6qj?IEehOJ|h`mC`Hz35L`K)&PC*)VH(Zi7J6&?L;lcWwcv!9Oh=jnB%WV*#kEKm|7Yz~ zi@$o#oty=9U2o7h%C0K09avj|o+!B7c8cCS8|HU_xc?o?DlNv|%PK@`%iN|^Y@(IY z2r)3F0>U=L;(JRmC#RUC$>Boye$5nK7{sE~Cu?k0eJ$aHnAot8@d=L(tiGoKj@n&~ zsE-f}Pa^{gcqSfx+qn>mAmxECA)TwAtC-mXdm4^N1Jj8QuQCv2zoDX4M1*U;HD{>f z!zpRpyeI%Vv{l*6^5Nj)YounX(RY19VxkeRMi>floZH=W&JW}R1n0O`C}u6`MnGQx z9z83*^}&16n`)h`VxE5;8c={MvX!g;xg#ZS*eg-ARDXvq0j?bR8B?{sNedLVqP91C z@He-~-&wXTk)U(5^rbelRk+d-RD#(YQTY@=Twu}H+Qt7B;CDq^fveRglWj(d)JQt< zv|j%johVL)#FeHz@vwKqcOF=b+c6py?1ehNO1qFRb(mS!WZ$sJM2dwZ zrNh^`ew7C5DHG0aAge;~I6DzntFb?K$*(xRT?@W8@Gzn})5G@#kfeFwZAu9{o9f}K znosz6$|V>on;?p{HpzDC*U4Ut8BqU4b~;dr^dmNHsVFLhtgufJxl*QJeojGKu4q!W z`2=|3pFFdRem!#dj6q7puXK0il!#_;E?l*1-Xv?OY9kCUcLmaGq@?wlnO8``6DAVs>VOeaoMjW6omzqZ>rGgdH#g zt%+Lqi_f8oE-w6{kICpO9|D0`kD)H;raywc>rmh*BQ{kD<`88s;~|5yWV@!SGrMNI zXAUv9Xt-&X?6i0ULvp6ChNS`~McF^J-$Qvhd`h=U*sZkQw71uDp zR2~Cc0R=~r=nH;+jfy-HzGFNXITKEC%pZ8@F9w`q+=I%A8Zwp;zrPwF#)ghB>+LHwKyGJpq;dmF7mQJVy?}O9!G}-ww z3S;x3fY*cqqiUT>y~2;CsfMLp9*^ZZxlOWj=1lm4*d&}D4C6G!fh|;%!dm~%^7tix zqc!U&;Pj6E1-cp=8an!+d^0$P{z|q+kxJD2cCgxd+F`8hw~|hL<>!!@I2DWDGD68V zyb|06FZ5O3x>{Hh_}O z1>Ft_w76>k(Bdix!pWC3~T}9FL72` zeS1}UMnMs8q4+;?+?K}jRW*L8^o&vIp`~`hfvM%%>6H)&PBi=k)Ei=sOHimL`;tic zhYG8LW?!-kH^_H#QWIjJ7%rZRD zbG3m`d<_Rdj>h#oyPh?mhB2T9ed!l?1t5#4%q=aSl9Q9O^jTsgeFrFFdV}?zxbSVb zJDcx&+5BC^_~kRu)ADn*9y55^vHD3TfY)0xmB{C7+WTxC>?^ zweuO>$HQ#%1M0T1OeJxXV{Ts=I?AT4D z4?y=R>VCY^9$LEq;$?f3=?%}%r*J7ak`Oq3&zYvr{y-OC&YY3+EoPKG0N5WEl1K1b zBHuz58pqy5|&q%Y?5=^w@a1k9dPs@j^r$)VY3>6#gtKPoB6 zzSjA|oB@eZDtLs-@gZ-rV<$mhLFDSPx?kE28N@?&0cj+%G|!{+|9}0O@=aA*X;==X zio8#G^H_2`x|_l!i-G9`Fq+kU9x(M$YXY6FZD&Ne?OX}NSN%~W$_L_xK8sZ;lw_2% zbLZ%#rw2z9isQ&Z#Uzaqg?H@5F}bs1pE70qQ0bwgufU_R1qmt>XpaYfSw{{1%k1%)u>+cf(XScFzIlz zW6?gUL7!#CG6v9s7@kqWo-E~I_G_ONVbKkM zhY3KzNK|wg3HT5goV@4QM8fRgorG6D00$^>diWWT# ze`%LlG5HdnXZU#E!^WdI3U%4BO;{gw*hzJmv%gXXAwk!Pb6Z2xaLXZNK&Mdqtg}_) z4RI2ZJSH7FNLX5O@GUe=tI7LLZm)!EymGCeMXinc<9EmVy(yIK{+*F+1ULT-9h$5Si_(|s5Jl-(z;qsBrD z0iwnEz|YR09Rw{r9thWecE5sO#+P)K`;z`>}SPK)jjj>xmLO>5mg5 z@{S65Bu>h$3WTlS&KoUEc&~j01nuZI*pP-`zu&Z6lnQ__Y?3~%XO9xuZet#Rp1lbK z+tFrVgl;)Eb=ObQZc{ZUvkB-$1Qf};czL|8WXI02Xf0z0Gy^9t*<%%ru4mn0Dm{tg z)6>(}D`A`w826=#XxIeZG{PRnDP3egjJl15u&kgs3~0UDbaYSYhUv4ci6q}zklTR1 ziFd_#n89{A0{+(+u1BDy}SSBjF3mKFx z9|J;eQTrkfdO@Kj_4&1%%8Q)WQs5wturiepEVNp&EQcNyf7GYyj2+o+XsV&yVsKpr z_y>=r&nCjkI$6JUst+_@r7cpKdiYU+a#sNor6+B_y_pt+p-Zoh?8&}^SsHw=`c&H@ zdW2Yfk2&K|7TUzm&0T8u@#AYN0)MUK5<26Yq1oBG=#T~UB@WO5O4L!9F{&VyVrPq0 zv*Su-XPX=oi1-N_P+&uc!J{ukAoQHA*GumKy6w0rMcC~`^|N1+V5h25Lq(8E=sIpx zFGqeYV~H9jJ4w)9ETcBvuqEa&dzSe(aXwinWi}X>hsiJ~ikmL*MHVrPEfj}{WV9>< zGmE{9z3fX?RC#PqTyh$B_82)TZp<@uk}Y(Zc`lM7F-;8<>V&YMnl|OMt9HOY?0^9) z%}}`cRDdAQfcvp)J(WReN`kBK!Gaw{_rc6$N|i9nD4mSy_9-fXmlEc2agc%Vqs;fT zk-v*wc4~*m5J>13gu@ZT=zM zdB7@Ea%BVa zFpt;o@9#G&iMg@g3JC%8GMxk! zMvCf_v-14^6v@+}zI^$z5+dsD*;Y=M*{*tnZx=$*LF%V%1y-lzF7>0W%OXxu>2a3m zQMU+z%lBjv@U8^W;dU!y2PF}N5X-AVv-3W(vY91WiWA`K$l^vzO>$%uWZ5fYI_X6V z`wJ`XEi|l4Hr$A$X_cGySBn1}qA+f%Zxrf9@|pd`fY|-MpVzi}e6p#@p7WAsD22^6 z+swm@Ib#$MO0#6&p9A_$3=unDK(W8BFA0#>bI@#0PJ#w$Ozvl!FTe1vFtRTazn(rW z$)pH8?{Z?GZwEVzU9ySgIurK$EriN!Dp(g$u1r#DGEj9J1F zmu;tYhqb$w4L3raGQTxT>FsDf;|B^IDQ0t3J>n$qy9tUs8z6UyCIb=Y|MLkc*y92ZP8(n2A23c9*{vY{)CxzU zD3YQ~;%S+nEqS-AaE{XikoCf_voWPqNjQ4@zURC&)Q0P6O+1s+$_*ohe8DoJ6|K6; z#-Ik9)>K>#<|zE2rlz6s`M}(ISZ}+!`P;QJKTcls#!`s&T!}vVNZ47^Og}s=W@VY{ zjA%FkbbHt0k0JQ%(`Up$cdO+2`S~l%B_r8<7vzvYL5M-Y^8wgnQ!WMYi<jEMQ0fxbRl&gF ztgR^_fruUB5~WZyYul|0jxofd15)^)-CWIa!R@QKK&-)6q~O_VJAG*OZ3uXUNU{QN zNOT?1Awmu&kRO$Nl_m`klZQlD4Ay9!TAZ>e=|g|zyNShVqh-FPig;8#d$%RLbfbhn zmm>enn=UCtbZQ8!Gan_xC@}lQAn^|>K@=zPldF2SR~jtd#RBm+4+nLJ5oEjb#g7hnKmp^#k`Mrv!vN;E>>0WK65ZzKpJd1_dK-U*=2If*4((O1PGH=9w zEmbRKIsk|igiTI2`f&>j3lE9JZ_x?n3D3c^>jQ(%uy_aww?g@crjZsoGhV%@ujvgm zdcZ_0jslK%tAkiM$*9VYK=%|2zTB=y_2T88S$lGFVv$~hdxj>vq7K!U+D(bc_yOi5 zP>+X(b#D8Dt6lHkk;6D-rT_t?rNNcs%gu}Z*#z7@5U<%Xv{yJ2Q1(ed&qihQl~6RX zE{kgflx2Y_WdeTz@Q?^9eO|lnN?>Bx1U9LvsF-v4yEROMuAYo^e+L~D0 z+ArfhcndWz#`l@;7HSD<_NHKxd`3YzL}7c)+9eL?geJ%-2bXuw#Ec!?>Ow$spe-0} z5%v-~<|JJ^8a?c%Osa21MAKdP#fPN*Mqn&3q9*HR$|r!OGgTnNFK?N z(kDXk<}!AmmpqAtkdz+x9s`nq9zmzoP2&ElJ}xr^+n2GSsMgvu%2Hdzo8KQQpR&al zi<@bX!DfVpCMLdUvIl)=1ZDp&&o>?=LL>CKy6)Sc5cIh_J`^q2)^dd)3B9ht2D>0> z{u))62_1+~XWutRk(LGCOR>+9KW!i1m3SvBgnRY$H791*=G!G-gS=cCx204HH@T~r zLF#P*^{kt6_;IYGaa%KHd zHYR*v<@@CA$HpFOb93MUjWkb^q=baO9bX`C27LfIk+`QA^o$umTjAYVBvZu})kHzX zZI-1$!r@?~Hqariu`CoAlK|4O6Fq{I_u=UdItlvoz9iU-V`X_f{yk2{IVw==hIQxP zizQfA6Gez$<5r~=j!fBZQ-rMNc@@F+30>J=Xmc*_N2XY8S%h3^6k#18E_U7Qq+%&I% z*A8j~wKv7I^#ngrvzBRQ%(;!v z_II@l=UsUoZ#T;1Jr-ppBqRjOR+OtjOLj<}L5m4IE{{^7Qjd#+jStT+fX8ttud$D` z6U*MQ#HJ98zNplo_Zw0u_@!nVOG3m+h>Ht|_$jLvM#6C^`HjxLS87g-2R_?}m|H3` zP=`^UB|^X)T!jK|F<*;Rj!;Clmwvi?;w%re@?35^ zYoWYk1)=Hk5};!6kqOIcnCSgb1ma*UPTaTeliX zS6A2E#!3n{?R!=A+0Ye!+gD@JczC`36FcbLAt7=kVOvAq@aY>#atey}DM`|c`B8sk z9%bxOfNa3_`%vUdBkYLT9}ay{{o>|8|2OKavU2>Hi=56(LbkT*!_>@NO_k{rQzUwE zz8{GGWdlG3fMzzB+Q=R`OWk~#ntj%EiOe^P#wcgH%ym?Ai9)J+!!yUnh<_FeUsEHSYW9U)t#pwr3-p z3KAw2g@I?O$jg0Ky{@JYjsW+rS!<02o6%A3g(~UD@p2sQfP<0~iknWP2n?*@RFigd z&DnBfpEmwNsoc&?>W^;4J~TMIy$N^@ye+bMrM&O=tEf+tUENttmie2Z4Sr)`@ zZd7BGL<~swfye~QLP`gn;gm5HLO`vgS?Bz0<#p8id4oL(I&%FA1;%UYzt3C^Q-1sH0XJp%*;CGXVG7+3?tcG~$J4N8Ggm=U=e!unp<~wqq{$M8aoHESqo+1PGV`?b$#t%f zOe3T3XGoZ|X`D-K-Q^|h*tEch=^@{0Q3uhiHPKf9kLM4%1Y!0Ok2RuGm zUF>UC+d^P7vmcKv-o0ENiK8U17bMi|be5MPh)5Ig2l6{1vLrI<$Jvg`t@%b9_3K_B zS*1-s%oPYYpLkl7Wzr%s#$Z{`)Yv+FmPUwK*jcsr6h$N*?$I|_u;bt@v?cN89`CSm z{(MOCay=L-PyKP!K-FSNs^w%d^Ma4>ez`k_`9Wezg!`YJOFpf3i2g`SyhJlgQe;te z;jPul(XRs}IdvY+wN%m0ojqZ|{8R_i?U{b>C_Vo&^hTTe_NnpG7<;_uS^M}`-^kn~P*C%t8t zTkco_52T9tYoO~%J~TGC9>|0ncmAIi;LrUKrd+IG^g(iuV6ZGx%wXCpFL#gZi*_|U zJG?yn>M5Ff>co;@&R?g7K8JfBf+i2$7@QZ%DSz@kMF>d(QqUTr$?S?R>85Hjopu;BX5b>YcJv1ZqgY6EJn~>A)Ot=BDOe-S_lVOY~Be%iJ zjOaxxpoGbkg2lA~)MR)Es$mJ(KeT%)G|)-{p;-!u@IH4(U%XW5W}GNmlaSsCG?0Nm z-+m3qF^%1;R?S4uKYdj)1`dl>@9qk^@&1b9ISgevuug*s9kDBv3&H{KjLtId>g?b$0y-&94`seOdv{wQ;%v-q*tH*;F?` zC?mggb)+I~fA^XS?Mo&tp@47|+Mg*N@(%L`58%xHt%Yt&RgRJQYXZh6 zGU*{LKe^!?`vIt8GO?M=K`%;7$IIL_q~+V6rERc8e$+~EEYoYq^{-5 z?cOd`WVaWz(r>ArHb5q^AvGr50{1J!)@S^{EtCU~7dZKs+rk;OjTpA9yr|zsOQ4*X zgVC(Dnvna$aidxnB9VXVO$SbFbjZNobaotHHC^i%J1$Fi*v*xY(s(}9vDH&{;7bMU zPGWreyG0;eej$B%&i{ax$$)6YZZgPqM`}J<@10Haxxe~R?!k;Bt+)e_x!j=CRA9DM zBHFq7Y0mMGTKKXf*nU&jC9}W^D#YLfpdW9I^Bwv?vY-ahW>)31@Sn?XzADIr66JTF zJ{`!A;U;GJ%>GS)%YiKDw|X;JCO4xP8BnaVn`%FmZw3XuXSn6;W4cIxZD8(- ztdP%0%hWjmH-eQJ85*$$TJyOcn~&t23cMncp_BE{IB4H>UJTQ}8`nn+9bxK;c#?}= zw_F@uHhJO(iVrw0hAs+QdwP0`_->k=z9I+a@_s5km``5LOxiZ=JJ+A>M`kdOhK(0! z(06oRO{9o#*WUv0%A3Si2b7_3ydLB)j9l>?88TRF|Bt4tjH;^Zx|AS-bV_%3H%Ql| zySuwvq@+_iq@}yN^MZ7Pba(f6e8%^V;SYYf_uaA9TyxH~Ph5A+2_<3)XLonOC5G)R zfbd93`gd?jKZ>g3#aM6|+8n_kD&vf$Qzv{=gdz=%h()xwm)Yj>zs0s$aVX*q7WUDh zJL@P$zF(gSQ}8>Q;r4igLLxXC;_AE$aN4;T>y$tB0=lJa0P(FHAXVsR0OhOM|Me*; zE{woBYG`}0;Te+uCAn#{vh7^LN5`h4wdFU>4g&YXYGm_qj6Hi%F(IiTmh6PIajQ#S z(syMHuPOynR7oEf^vciCeWZak?IBQKm_?$w6AeB{K{!vd;L=^NOaie}35RBM#7PxG zvHNh(&zzu4{t#rpzf&{|R}}l?s5wvPVqy=y8!u#x1}zZ=ud|b6ODJ|kYqb;Vy141x zo)ce1q<3fn1E=hWNhtFvoLMmqn}^?pfL+LAHvdvMTn8geoWRZB`dI+XL|hp$yeCgS z&Eo2&?$15b?}=sbuwfv^NM3;e!D*`*=FxS{fO%|eEz0{-tjL0oRbb_EEa!y)6czmwIeT+E_!wf+)jSRGvQ*ZvM0>P9zk1Gy#u|?8MZetL@(flq*$#C1 zUHf?M&Y5s~-*7kDujzKG-$)-XH@!oEvU!8%lg<014r0|v2}NyOKBa_88j{B3Ny!zt zNJF6QCYo06Zfi@*{4)d;%1^f}?r_V%ph3VGDK?NOz>~)hiHR7k)tahB1{G=b|NV>N z+@fo_cIQ43k!sVw4GWC*y+WtyuL;pJ45M!QkK*hX!p_IV1uj5>ZC_v0aRuDtvcB)n z7_yv1@0fvGRg7BGJq|`Wee7I?m!WX_VMASydcND2R2Q;%TuCYo5d%;V#POb|dSqN- zk{74v{2w45E}H~ZFHt9CSH4{KmKb5sgaU1~i4v-4#E?*sP|VUKI@Ny`yJHyvk0nk^ zJ3RU)C|BR*dw=~1c-Bf?S=b_z zplsBudcy>7jN0E{iC+FJdfvIAFTEIG4K}_m{9`6No||y~;$9)v-7lHk3qALS7ps@I zB%Nx{KS_=yySdkzu->=aXY0GVF$_fGi9SrtJdf>)&p~yyuWaNdW1S_-R4|% zKc0_pYvkl2Ma}R%+?v};82L&=d0ikYq4qIwias_P*Z|(B9SoR$8y}D@+=uSnEwt1h zB=K>UR;6zg1Ik|jduX80N$NNgqG(ZoL&@%ey z7V7!5MAkJuJ9}{qmXDS_Uc;v3osRQ7CUY1xc(3XjQW07Wdp=+JKmx<+u@NZ79|MFQ zD2NzBzHpNvP+kp7NR!~zUg>S6-tv!qCX6_>k_FHK6y;}HNaEgI)L)SsIshy~AKgU_ z8Bv6%R5-2W;+G}1DKzToZY}&tKyX5yGPX(7`Jg6edb{p&H5}WKYn?TIlJEnsX5udS zIs9T4p(?wDmoxo&dyTK@Rl4K30R=FRgd!p$l1p5L3)xx8ys6SIDZa8Is%UW9>4r)A zpL}MPFKZv#p5SY9+Rs|s<(1`KuCZVHvikny6K7w#-d%BGD<%@%^WFCpv*fPjp%AkR zJqXI9qCy)JsHeW~@BG>>VD!OfAYeEC3Pk_%li!Sm2oSX`{1cf1vBoU?wG0&JxS5k5 zA=!&n#?og>-mk7|uDkd+liv-%;j6_!K@sHggk$#rUxKP|PxR*MV>4rsI0C~*Avchk z9AfY|E#J=F`MDQGI11nDBMfzjfZoEzduR9pCJ-8WAJ)!OJ+G9h3~?Y}enE&pnW7oO zudY3OdVA;~;P8soWXu`r7kjYMv#S8dB_Q`|N$q?JdAa!WKy-SvcDLkxZ${G1mPxy` z5@74oTC%AFJ|r)8U{OP7Lv(Fm{~DDNgEF{tzOYUs$L&e+K`|FY>7RtpPtxn6k+5r~ z3Hj}0yi%Fm%*vq=7sA!T@#a?&6|op>iU)_O~_`vl5V@}2eLjg=89!L__CEr@=%jj&#N z`d}2z>GhkI4!D?A<@3&QNzX~?4?qneh!uG`CWD0#;tM$`@Xo65zso$4$rtNN8{*oE zYJ#q{Q^!-TRrjgM!$DMGQjcRq&)y+|%pN78upwySF5{Q$XjFIL7<_xNwLEa&5mMe{ zFBk6ictr>jdVM_3H0=2 zo$l9~Oor~-h`pYwsecS1rpZvgeeMng&Pr#`gtXY*EzzT`jSCskPOH?v)a<#%r(pEj z%F*nxfb_Bxw!ydi`6A>i>X1U_VD5hQf8ijE$Q#=JywW~bOeU}`B|86I8y#YEu^oO< z(nBDi0y5d`yTQQ-0AC5llsz}K(U?c~KSEc}m{P&bW$&V^5gq&7F1z_2j=e`|j(2B2 zdzf4oMXT8Mn;Ot(a(lrloUg@p_&i@uGzmqy9nY&4rlb+oHx?`ixf~ogY|PZ%3eB}K zUBgevMs(kI1vFSaTDw%|lBmBbBd`@KCHHo{95*?h^@_JY6T}+sj$-E8O=P$IU<%mH zaXMzuxx5fX&P%WFVj>`axyMeCXC(UQOm2wwi@rOv8+(H#%FNE!nFFYNGw8VLJl_{| z0yW8qWrsel;DO3VOo9W>Ds4RLi=7F=V)6Sly6$iYy$?i88#i!s!P}k>n_Py^dQBRY znQ2@*25z{zI1sI`HaKFxpcjv3kdTV@b|*hiNC@E|Fax0?#lz05&( zd7blMl_?Z}y76>(UpC!kSHjcMFS(;Ff>Jok2wiM{QRE3&FPJ6}wZAbJ5wtL#piBI4 z9IG8w5a9PXBkjDp9lA&)@~>(%fr4nVUHP8#dJuNekFKzF5pRNdY_W&@8J>(0kN3T3 z{WVpg+9hw~pKNMrW9MrF0ON7~=Ny4xS&hmq=<@(KZ$_+EFve(998pLKqpl*ThN z#V3q$2Z|6kMin1PupLMy7;=s_8*|zs&oE6~>u70f@o*S1Ki}I+JhA7@;Eq8bVn}Gq zl1er!Q!MBwy@N%A5x!X;yK%pZ#f1oxfCz##hxP2rLH?1(X0d*a2-&{xw_f~m<($j` z5*Mc_EF947aA(WCmrdvw4Y4oy5mPxtN|pHeDSu+*mF07nP%}N>2dF^9icRETOMH$2 z9x4@=-<3fafzaY-!ADSJ@yz>7x|<_ZL^_RGL$QQg6Inh{v2Do$$;7WC&f3VCEuQD$ zsw?sEANNgBHEjp6&2p`38JqozV}g)RrAIZN@6UGTPg{dG-WB7ilE43&wAUk%KB)aV z`P&f$0~JV|y&g~8%HOtbn#a_Jbi#B+&hD<_g7@gfW6DD?)~GVW&S_u1r|-92JA_ss zlt8{+o!*&k&BqG4nMvRMNO$kQX2h0Ku#D6*%68@q{G&(`<;Tw8od2`57P66}Ikh|g z-B7pRloIWiqXEVM0t{3S`x$b`H_fohwEn$WyT|at)p;)V8Z+EOUACqAnuwyk#)4=n zDF=La*`=k(dw;$;_X56N9xaGA@RNaxU67$04lW@D7nP3|!?$fEwUE5;P`tct=-vTZ z>bjk-rh8>*!=JdkLsa91iBbAp@-?!oiSQLx`Lz>%vHJS>P+?*ghU0Ms(HAGyu*Ao;n|@%J~H>2hKZ>#zm6{Vewl$5$>ax* zb88;WY(jqv<8^RpDlka7k1N6Y@$?@G8csKf{mBw}D)=y#sI~T+XrS#&PQii5EYg;# z3x~Z9zH2h2{u1UecEGkar^EUD9n?T@8084s6IpOuGFo_(FFi#mC&a45Z5qm$Wf>cj zv2-|>JLpItclvFKPx^#^sE3w7&Vs?rdWPyo1r;a^@%IY$;5x&2Vi?iPliCvO9w_x3!E`}71di9_~J|0A280Z+pPV*Zw{v3X<9(l z(n695l}zL{_wu4SB?uP>LEK?_?N{=}Lzucs=eme-|2Ntn4m7b848zdDaKVO=x-RM( zDRCc3uHD@c*+VECLR_-3L%E~W?>9rntlH~s>hC!!c9N~6nTb5yZZaBK?#B*=z!^C* zZjxks062j}PV?lMz~H3nKl4THxC4}Ic~nxLZiC=6s8Bw_*hD-*=8axlW=p@c(@4+m zP^u$ttglOpTRA^(15~4uvU2Jy%Lm{W*+0Lk6uT;M^TPG=q(Ot19)#GzzyBLYMO__e zsWARXHF$9fa<&l1OrqFM8I<(9|qr7HM(9 zpT$!GJ?YrYdJk76{p8hu!;F~zVfP8LY-Rs9iin+E#grA-yEd>{1)*29#=N{}WvFQZ z5XNjTeu`UsmBEfyS@<*r8Bxgx-nymVVGdaJ*M_Udy7-2aTY%+R*K83bLI?0P)CPhEcSkVz#n?P#H<26yuH!!>S!f%tbK4b z7IE!1i5McfW^(X1r2>VChgHw+B=`HYu6K9ZX_S7ZEb6w9-cp|Ct&2fW=9i||NxoA8 zaRj}WC){bX_k(0(-&2k13W<~e=5F}Hk8#QRGxD0*O8hX$GPF!(u9&tdi6&SIM)$}h zHDKR5TV`z{cqpB|P@!L&!JJsf*|r&tY#+6PBrUMb%tp`+f-Q?H0mT#*LW}dbSnS61 z6Dmb&cj`4}P$}hnuV{4xe0#!4u|+r+G^l=?wl}0zq{HZ3*YrF9G=qDR#$A_e0i@S zO*U4QB<7lG3!?d`Um$`&I zR1dMdJOevN)70v$jDvv?Y$9seT!e@k3l5@Gtr$5P2?io6BCJ!}8cdK#L?l8<<*Klo z8-nnQc|B_YTM70rrFx>5(_$*(c+vcCx=5buJY`e3!tE{Cma`xJWJvTs) zkHp5yE8*cF)MR~p;q7;_+!Foj>Mh5Cd_CD*WN$t?piMB00;HQ}bZ%iC1`eml|A-o#itnd@QIGy3S3Nhjigx_4X&$ zyg^xlL(OBr%F_%H3PeoRyAFI1R~jA}p`@b9kk{edx_&x1IoZ--di&Pijt|P_9^I3l zqr0GCh06e6eFY?)^RvYovN&MwBds2w!I!~zeM_#7dgwjuqhoo?eId1n^u2bmr3vbOGv~Yo_TQ?wV?AtEvI{t~PBE%L5rk(?5p(SA%#M77ffAwoBY;Ny zUEmA%ltn#i4CcWpH6nzp2Qw^e7&W!r8Kb|CuvSr!7nfTKogO;s0mxnOTXms z$b`)D{um5<;#2_--W$xreMuvQTDi`?*n(S2W3I^>HUaz|CEZv!`3TTLnDBE~ZZs7H zfT$WLS3LzwxKdUPiD{GjWz8q`oAok=!Du_u#{BquOG3^o1KM3YfRpnn4ESr-Pv1jz+ z(ZY$242O_klY|b0fEMk$`HZ-E=V_5;#g5D4=qLo1XYhSI@0{2dyuNfRhTE=A3s6|N z1;DD6UTEyRo%TXV+5P)sUK)K#qi9@b(oak(phU%vAVm*9P_iI}%ydV8q(rXaWA+Xn zd3!xjQrPvLY<>MN2qzt{q4RMl+X`l;6HVm~pY&MCGv}2bOF28wq%HIEmi(rcJxU$h zoj+}Rnep)NUw`!>JV-U=IXPKv4e#J?(DiOyQ#1RuUg zCy2SpV*<8j?;XJ#ruk`F_jHJmHy~l9On3C@~tEmybk? zX?MPb)u>w2z2Glio0zc6SnOXwC zO8}5mheV@oiPk($hI60Nz_yhYf{OShM3(4rt3WFd@+4i6;2^D>-!dGPDffqdh{^jK zm()W5MPIWXVQ8pif+53dWrfM}9fLHkO-ocv%q4KXdx3-E$V*VC5J%3bu3lZzie?5M zU#wERVdlFUvU0k4I%9$TDyWsbq^Tt$Z}wQ;I(}kH1sF%Sb>0E(@@&bC)&{c;T+0}_ zwUFgxemO9*KIf$8D?FH6Sl>Ay3#--ykA0!S&E)k%$`KM)yA9Q< zl1}K~JEC<)J5YU`$?1@$#*cBAOC;~zWEOgU)R85m;#wI2?YODPqfJBd@B1_ch<)W7LB!hh(~J#SZR8U3FZKnc=IySQ@1RYPyf zap@{@32e?r? zon4YE8JxRHLfYJJA^7V9ke}~QVe>xRhr6zfj*RAcOK7l3CoI-^V0U>QBQknieZeeX zbI&N;c^L>rE?kb8Gu7jv!pb;g(vbSAS{=&cf5JQ<{^@l3iG@)=~!me1&?V+jeHV-{rbUvEE8502R_kn1r4Y2L?X)K00(wbUY1y zn7sFFZ-1FFxR}u}*hiQQh^U?3ftSLxD8n&nl~WoLi=E4ss?lQ(Z-i9BU_$vg&uJF~ zndh`^*^z}&#ujgk6hRq6Gb)qKcCi>v#_zsG9q>Ho;0SEi_uN0wljkQVdo~6E;YEAu z{@1cOIZb%;*3(N%@-{krKh^4HTB2Z~@yck?zTEc~(gDv^Lpxs8m@J-h0>36Ui~EzX z!8^n`MQVhaidKwMsPb!(JjcJFVrj{p%fj}#c&+s@YpkIJxvc`N#A8zfzOCr3g1iFH zKrYVIRD2IwK}bY?4p!gi)o$JQSkL`zRYqQ(0;!E4accOXIr1$K@@&=a??jgO-O19# z`O96EQ4Kj)*P{L0qM_T=U*})4PX(;b^@!}=iJdb9fB)_k=&KW>e4LKp(}NTyfrS=o^-yS^72tCs^+E?bbwxSi!1Wp-tpE}LVtW0)1 zNem9^^4}eh;d@#l*Orsd#fW)5k81(A&m1!YTGftPR4LiMc<6YyLggGPE-?`tFNp-| zbiWxEVKnHN==YD59*ZoUI^wynVH^ELW z-D}MLj_F7}*H{jF+{?q`)0jJWSbZwnK#&%-aXxf>y?yI>>FIaS3gX76M9J(Fbzn`e z)?}y0Lg37Bo41!Ns`I**;Iu2v&8`AS>a@K*;iedQ5nL4LVPWJd?5Mbq!4devLdiz^N1APW4r(dTEFPMt!le6G3tE{TbU2HC` zW##nMl8wgpAR0-$8 ziQd`UbH2XQLn0WC%IR^A!_=zPkvI5|Ez`h22Tu0@f%H04_Um;N3YKGBNzuX;J*wjI zxU5Uc9Efpz9oJ8sfm)?`jc|FHx5a&%69N>L_4<5&@#k%A$CRmq><0wxl_`cE75~y# zmtL%~5GeTSWc~buvG{e&=p(8RBC5RB|f#TFR^lu^&ipd!*8HRK{w|GrU^=9se z6;qbvR5nPcgW?xQWezCtl5v=6$b#29?fU}Pf%j9uGx9&gdfTs#@O&>3a`dknoa^eC zzeK}8so>(7?NaD=`TOW`Q$c7UjiK*P%0+6Jv9n8_1XlFf zC;VI^HAoglm{-jlu+9WbMg2pOUQ(- zq$Zxs(PvP+9P`s$UJ_(&V-sCF^0Em~+(-|v{{9#FDcG4PrdqwGtVYZ@EMGq3$V(u? zV$KxA1T8JdVCR z{9zIpc=KyZc<))DPB%C8Im0|vq3HHyQ@(FgPAhRORoTm z=BKedAC-y!wgJZST!UJC7DUJE>&r}}P-Y!hTi)=_?!Jny?hm$=@Hu13B|R|EFbzDq z8VVbFcyQ9oah87f6udpVw$=xi?GNP@1=TjB3zHIPD((rA>a|; z({qr%1%xJgYs9*x@kA zrFsK}hR<{(>29D2{fc+J$blq$=`+NEUH9wQjTa=w`<}iXw_uGVndBgeLXVj?EG*6i6r0)k!YTNw+sXa9?Xmz z7-Y&Z`#j-mW;H})6>Tb7RE*XzewQvSAhEC}of#pc8V_73kMo4*_n(lCnWtZO9RKVI zG_svvk|lM4V(<)jd!l)Hvs8M5%kgrSUP8JIx%LnEX3+k2mrx}%V_vp4+Wq&*8ANZM zc)KR+(Uk-srU;IVd9x|Gcak~Ig7g6Y$Njoll`8f zEH^L3H@We)2%d4{Y0Uo(Fz-gP11E}|z-2R~jn*T~iwXZi7h$2z@={b@QxM5y&MMfI zBh1XbZ(d-oosPaRHwSJwo|?$uYjnshYIPpDn^o1QHF-9-KA&~DePTc&T2|1~8p+$J z0x!n3`<4)%}-$S$=uB)oO^g zjI-MAdEP1}96%RIm@bwZ_Kg4t_)Uyl+!s@nmJA5KIG3!KnWL1c6)7zA(iD)%D*u<&9SD0>muiR(jW5@|z+x|HjHcbs_&_NUyE~vRGE4rp&_Zn*P@W zL)pN^#fTxIkK=7i%}Gh+$6Pj6c6M-oA8Ptsuojt;0k{)G$b%@X4ZN~gI!Yp?Trfv)TN=ZTd%X=rm$RDY2Ui7eM%G2dzYnCd`kZFCq|8* zEV#XmhAw=~+-x$Yf_poU!n|&9FnR`ap-Qi-VY$c*{{skGOM*zuSb+W3(&#sN2{9hd z)o@f4LdU=2ixbDq3-@lA7%%U7Rcuxaj+FO9dKi8fN=`HtTFf|}w6<+GDGK5h!0bU* zYgNde)pB4{5eataEF51=Zg^%MPFG!mFy8trS_)@0GOw9WDJ--@G4vuhU=o z4h38c+EbG$s>JJtU_mX6s_7*rvU97z?T7w8wUyX)yb|_e8&G_%XlnEuDBc zf}ZDeIUU_hS7l;kjLAbQlk-{y;GO1x@ydd43wA?Z|DvJ2F`f-i?^75hkainasOC)UqHeKh z{Q=0>rxVZQ6D!m-ZsL@bWwv*eq2nfRbbP!L;OqD(EG^CQe0L(3Zufk5mPsEfbbaO( zYJ&79=;QbUeQPCpj2QEm`Qq}d8S}U9e^bR0WOLC}nUK~md7k_%TXLqI@5!%nY63R5 z)2{JyGkYvxi$l_6kM4b09ohG4X_?U{BE;3QOJt475`X(`!dj741XcT~c64{J@HQS` zpT4{9%FKA$+Ot78HTe+yAv>YhwzdBA)KhL1+_%Y-G4A<_5E=D;)4&A}6C{<;AC~Q9 zpq%j;_zMW8pQICj3($VItb@=wbev)D0?5v`^KnGPM%BqIII0v?i?_wVs-~5fq*pLKx1$mt&t=c6YQ7bUjOZ5ScjDG1#Dky}KQc1WyPaL(vwJJp zOnK?Aa%{Rk>F48KYb5G5eOgA%WHgyg0I^!yA*2u8G~OXMc6&ywBs~_qST7 zGtL?>VIe_>^p;|fg8aS!@z!Wy$7^5|M9Zo7eZQ9dGE`Q!@ z#Qvj_4#B9-T6NK4{->N6D39whnJ|8t0!Wz|^Jg_9Ja*18ktRWFA#=5F5qa36R#xzR zeRVShZh98BzGtodvKsb()@IYw8ICe~3h?FQmR@b|K zeL0v>OqJ8r;BhtW;oqJ>2|`!$eFd|ftTzzIXLQ0I5M`bkenvv1EKu#5uhyxr^{8YQ zbbZN<)=;(^bJjwi2=AeOj|ENxj=~F(i!a^!FdByDe*UFD+s5mg!pD%W&4YUX$oC$H zfWvFxEj6=fzFcjhOp-hdq7P`}lk<6t1%$FLam34D3C>@#u#8+tS2nGTs)-+XHyQo?CG3?WOGW} z7OJfqp@1T6o&T1x>n8ACo)96duP#q$n-LrM$cfhXI2%~$-hwOaZj%yDvli2Km#eEW zJ*+T2?anWMzVzB=r+aR#z=igW%^(6Y z&UTMCUMA>=Opm@8J%xonwpIB@ehWBr!7?U&rANVtPZX=xlCyP;QE;nDJ2B4xW(=_; zr4sfh3Q#`MMqXo34FEJld)s#rZ z8$fZ zkmm)YKQP9IRr2If-s7ELFXl@s8!CC5aM);_>$f~lsjk%E1M_6bkPpZNG79Q7;yyXP z3)XjdF;IGj&hX?IB^Q^)yaouwx#VPkRE%CVDXHNxnNXQ~{A8L^`B2Yjhs!~K9Djum zU-!kd_OI#JC?(c!M}c^b)|1&S9^vU2N{HsHXA80ebbUpJvWaH831{lt2C%I7 ztc`m+H-|dA?qyo4BH%!+48rAX>IenXULNlqX31E5P=ww!DpDBLbMjEJ8~6qPJDkbC z$URtsHvwst*HMHRfr>%4Mus}5Cs>>HwM-RAhDh7?M0#XK)#-WHxK~MEs~Br~IAhdi zGw45wD|tSZB(^`*adG=^9h4)*2? z!@0{@L5s6)sJ&twbo0;?Nj+EqN!E_ zM%@))eUr>H-8s?+ykh1v{xpD_g)V_y{@6tDFjH)Lh6jF-kLofWJ=dw0yc`StCldB$ z2ohSZqXU zf2X6&&0tnef(2dR+e6ZC__;YpYJsdoPljY+U6FQ2P;w}s5Yc=@n~@J43BV*D39d|S zp8k{b72%)j@le;{Kr%&MDgj<`OBWqJ2qTx4iT~#wg(gNz%aZSd==uK9oXyNx&*jZ6 zvU5|emzoWxXfBEeUiv|HAIs(Qt8SNj(7xau$q8qCb~{SCIeS)z=Ka^wQ>ox6R}iAa zDZnFzx>ETY2~GJs6nQGOq6o2oVl#i#R~By7mttu9sg&r|48zw#O3zzb-v79Mw z?&>@F3=Zn)cIgdW44>R=OL6!9l+^&vHVU3isrqVi!*_E|^MZBhK_04N(Cw0~x;#cN zQ#boQIhc&9$I*9345Ob3u53W8#Lhfb@F??_>=@-8+*nV^1U zVAAYO?vA@pc27@m`}|ZGNKS`aYBCWBUUxwX4A1GupVgGe*HW(~OBzi|Ofy}Krx3dU z%O-1}=RP@33g^$5sl@Q%iE#qlI;~a5b5v#RIYb=5s+3b3_W^m(;f>LpfbEld2Rh`q_^2sHR!9jJA`-) zN<_c>4!X=`LYmF(ny-1U7m(Jzvx_gdRdU>XSH)2YODXO!SMEzPmsIS}b6XmY7?%{f zLUhRTPVtz?G?`r2+u7d-`*ceO)WD1;KXv!mb89V!OQtJdjn%N2;idE6lnz#?RV7l4et@S#lL_F%1 z9WZ3>w{#rUl1>sP?q1`&zi8HNzi3fVR#&erzl52}DeL+5M)kJ^kSR1-t=oExIRvtR z;}BwHrMKy-a2&CWk&4PwquIa3gIb;*j+Vi<42I3Scar{^g7_;H_f`fmy4-gk^PgJ@ zsn3K_f}d5+{U}{Z?Zb^`N4O&>0VMB6(RJU}1rpI`v;cL8`sl#L50B25g_5DBXK3?W z-)WU{CSCfxA1#EKB#4chZ?ly&8qt_Wxi~+`tZid~$!QC)M|3NKNd#8l6$-LGn2UqN zv^7XI$0=p(2!&&LsS^-~rD`J>ZGNR%9cH#Y{F8>JL*`>h6v0zovdWM=yg7$xzf{a! zjIahS=)Mow?vYJ)<}mrbsdx$yuKOP&GJq?BzmM376MclSw6@(l<(ecuH1a>@5*+%+ z^yW)TEm5FHn%8EwXg-UTo@&zLCeE8F%?2a_YZU%y4Kkm1D}YAl`F+_aoe)g9EJ_)? zWLwZrh*>=iX;mwl@x35bm{VnCaqJR0L&Y5MaiY-oR}9+6H5{~y(-+DvOvT52Gj4;6 zO#Uf@zQAu*NC7-RK?N|*TWfXLJh$_>&ezT~jEtmQOmHSp-i!V@Ie~#9fMpc;)`H1U zqrC57WKJQV1$+<;G=yR(MSU;_W{8NpotesU3V@n8>3%*c!77idnxVFb$}eSsLMW+G zd!F02b=vg9GJ`O8)1Tzolr3*bWqDBr=H1bajiV?P!MYBXRQRYN1po{efDa>EwZ(xb z#5c9mW@(4P`N`~sC6vx$lFWiDuVn?F^YS1G$PBBUekpt&c+EOGVb+RH<~%i86`FQf zszH!JBI4>8q?^n=!aoeDe~j%8o;v@t{gLArm0Ckn#R!eZ*y%8T`CRhmc?=b~q5hu+&U zE$+?a2qYcT|N zg%v^&ShD3RDXb+(RmZ#`SWH+9>S3gs9PEkV(8#B|-J(9g8$qrf_tR@LW8HsIf`UXn zJ0Jlc2*COb{4!?AV7B@)@ZoXCZsV)85{bC@2dg?*ErnE$#O>Lo;mGf={C?hd_qIHex2N3kgnD%gMawu`dqtszUe z_J=@2nnn7e_L=iJHNb@tBIUE+Z>;v-L0;IAW;IfowK6>63=#xBbY%KVX!)zVr)iXr ze8mBcqoS+8S3MJSMuqypg1Wkl_4Uq?1%+&G+U1XoKoIyfd=zfaal|WC!}kOvYvXPE znG1S5Tp;#IaR5xs@*as|IWcbjBQ+X;2^U1dzGh&9GqdmEb0#G{Jeck?v`g9+x+|93 z*w-C$D=L8Rc94*6-q;*6X%a~#1dmugGyy4@WV&;&-||Q$M2TMpiPVdN$Kp)HFTd#ZkqoX1UWpZB0JD~eYV&hNhU+nkDecRLK%X*j z#0kO&J8R zP-5!toIqy`U}^%=R^VJZp|YR3{44VXUrFP(NI`>(^O>$)%$zM6TJG4tR&9Ro7gSIV z51cSD&Q1qHiYKrVC!BbG(4xfBwx!F}w0ElxRu;tSFhZQ16NUGcXfdaxJOu99?>AMy zs7Ff|Yk{DpiXooYy;bN(UwNRHkS{Y<8bWOD{S-Pph82AQWY?!ZGhA}QT#_~*Q&C;K zP0QJW?xTTUqtrCETpRQ2Jq`&DVo|$f?img zkE*ul&7maIcm9)t;3YM1DrV>BAyF>injOzbWBHvwW~@mVyc0*zt90c0ik~)bOiMTf zsN(-Qb$7Y!rTEU$pj9^4i>?2_9)od07}96tt1m>DgPpSwyRhGPe7$)uiEx9d1r)zw zfE8gD(K!<+$Xl?8PV?svicC(LgP{^|F%WL$*G^Vzd1FfcCAZM0JwiqLDX&jF_GeJ8 z!*!nMXzx0Vt(7iKmGOWWXl81fG##NFgD((e?qAKuimsJh!*{Ys;66$>3Lx2@K#|Jz zX7I5?JNV7dhZ`_Ufl%e8X#it$rq@1{+vjME9YCbj?jNkkQ2hCrG6CDCO8Hg&E=l^&@DY7JDU}HsiQpQtu*C)!yerz{=#g4-X^B1}h<7J7rgoTVpv+H~104IIh0KCy zd1YOz;wjpoNK&1XvASVwEeiv{0|^j|=!Kz94~JLqcNC&gAC2|1;r(uVA~N zkYE*Z>_XV&ek|d&_vK>|KGMH6K91%k5){&}iWPQra>JGg3e4hbbl&QZH(P4ewv_nE zU&oYQRdJl?4ADv+WGu$H*z8>#6x(Tltqlp}W`;j*&F)Rmv-qwQr4Fn-e23p)ys(It zX|em7nmjIe>9zCP+VQJg$Ezp-6cpMxVsbFgS)%C5AGV=37P=G zgS!O}8r*`rJNLX_)ve+mMI|$5pYGnPpVf`1;o&o8A)7Mt+ZDi*&5xTWZfxFxn_17QQ+vm>_|Th8=$!Us7TtUZB^XDi2HDtZjflpx^s zW=gkaHU~1RCT=OBks0%{K)cQH34G7~wcFc?pEuB=l`aXv≀O#v^P3K#J8?x5S7c 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 z

Ko^gEFU5Q+Fc1mf(n(g04rFIHZy`f% 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 literal 0 HcmV?d00001 diff --git a/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/dpad-sprite.png b/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/dpad-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..b92f75d4d56d2d964913b6cd819b75bb221ecb7f GIT binary patch literal 21421 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 IjCt_?0eeBEO#lD@ literal 0 HcmV?d00001 diff --git a/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/stick-sprite.png b/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/stick-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..677db9b2020432a99e35550e073cad3c37af67e8 GIT binary patch literal 19368 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 literal 0 HcmV?d00001 diff --git a/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/trigger-sprite.png b/src_frontend/assets/controller-models/models/playstation/vendor/ps5-controller-visualizer/trigger-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..b71a9d7d255337f72c4a7f1ef9d90fcc2ae2e076 GIT binary patch literal 5686 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 ; + +export default Xbox360Model; diff --git a/src_frontend/assets/controller-models/models/xbox/XboxOneModel.jsx b/src_frontend/assets/controller-models/models/xbox/XboxOneModel.jsx new file mode 100644 index 0000000..336eb84 --- /dev/null +++ b/src_frontend/assets/controller-models/models/xbox/XboxOneModel.jsx @@ -0,0 +1,138 @@ +import TriggerMeter from "../../shared/TriggerMeter.jsx"; +import { + BTN, + btnFill, + btnStroke, + getAxis, + getButtonValue, + isPressed, +} from "../../shared/utils.js"; + +const XboxOneModel = ({ s }) => { + const stickTravel = 25; + const lx = 185.468 + getAxis(s, 0) * stickTravel; + const ly = 279.865 + getAxis(s, 1) * stickTravel; + const rx = 473.042 + getAxis(s, 2) * stickTravel; + const ry = 393.815 + getAxis(s, 3) * stickTravel; + const lt = getButtonValue(s, BTN.LT); + const rt = getButtonValue(s, BTN.RT); + const accent = "#107c10"; + + return ( + + + {/* ── Base body (from e7d/gamepad-viewer xbox-one/base-black.svg, MIT) ── */} + + + + {/* ── D-Pad well & cross ── */} + + + + + {/* ── View / Menu buttons ── */} + + + + {/* ── Face plate / Top bar ── */} + + + + {/* ── Left stick well ── */} + + + + + + {/* ── Right stick well ── */} + + + + + + {/* ── Triggers (from e7d xbox-one/trigger.svg) ── */} + + + + + + + + + + {/* ── Xbox guide button ── */} + + + + {/* ── Face buttons (ABXY) — positions from e7d CSS ── */} + {/* A - bottom */} + + A + {/* B - right */} + + B + {/* X - left */} + + X + {/* Y - top */} + + Y + + {/* ── View button (select) ── */} + + + + + + {/* ── Menu button (start) ── */} + + + + + + {/* ── D-Pad interactive overlay ── */} + + {/* cross background */} + + + {/* Up */} + + {/* Down */} + + {/* Left */} + + {/* Right */} + + + + {/* ── Left bumper (LB) ── */} + + + + LB + + + + {/* ── Right bumper (RB) ── */} + + + + RB + + + + {/* ── Left stick knob ── */} + + + + + {/* ── Right stick knob ── */} + + + + + + ); +}; + +export default XboxOneModel; diff --git a/src_frontend/assets/controller-models/models/xbox/XboxSeriesModel.jsx b/src_frontend/assets/controller-models/models/xbox/XboxSeriesModel.jsx new file mode 100644 index 0000000..6a82fa1 --- /dev/null +++ b/src_frontend/assets/controller-models/models/xbox/XboxSeriesModel.jsx @@ -0,0 +1,5 @@ +import XboxOneModel from "./XboxOneModel.jsx"; + +const XboxSeriesModel = (props) => ; + +export default XboxSeriesModel; diff --git a/src_frontend/assets/controller-models/shared/TriggerMeter.jsx b/src_frontend/assets/controller-models/shared/TriggerMeter.jsx new file mode 100644 index 0000000..4cf023f --- /dev/null +++ b/src_frontend/assets/controller-models/shared/TriggerMeter.jsx @@ -0,0 +1,50 @@ +const TriggerMeter = ({ + x, + y, + width, + label, + value, + accent, + align = "start", +}) => { + const alignment = { + center: { barX: -(width / 2), textAnchor: "middle" }, + end: { barX: -width, textAnchor: "end" }, + start: { barX: 0, textAnchor: "start" }, + }; + const { barX, textAnchor } = alignment[align] || alignment.start; + + return ( + + + {label} {Math.round(value * 100)}% + + + + + ); +}; + +export default TriggerMeter; diff --git a/src_frontend/assets/controller-models/shared/utils.js b/src_frontend/assets/controller-models/shared/utils.js new file mode 100644 index 0000000..1537c83 --- /dev/null +++ b/src_frontend/assets/controller-models/shared/utils.js @@ -0,0 +1,40 @@ +export const BTN = { + A: 0, + B: 1, + X: 2, + Y: 3, + LB: 4, + RB: 5, + LT: 6, + RT: 7, + SELECT: 8, + START: 9, + L3: 10, + R3: 11, + DPAD_UP: 12, + DPAD_DOWN: 13, + DPAD_LEFT: 14, + DPAD_RIGHT: 15, + HOME: 16, +}; + +export function getButtonValue(state, index) { + return state?.buttons?.[index]?.value ?? 0; +} + +export function isPressed(state, index) { + return state?.buttons?.[index]?.pressed ?? false; +} + +export function getAxis(state, index) { + return state?.axes?.[index] ?? 0; +} + +export const btnFill = (pressed, color) => + pressed ? color : "rgba(255,255,255,0.06)"; + +export const btnStroke = (pressed, color) => + pressed ? color : "rgba(255,255,255,0.18)"; + +export const txtFill = (pressed, color) => + pressed ? "#fff" : `${color}aa`; diff --git a/src_frontend/components/ControllerSettings.jsx b/src_frontend/components/ControllerSettings.jsx index 6939801..cfa8a76 100644 --- a/src_frontend/components/ControllerSettings.jsx +++ b/src_frontend/components/ControllerSettings.jsx @@ -25,11 +25,14 @@ const ControllerDetail = ({ controller, onBack }) => { const scrollParentReference = useRef(null); useEffect(() => { - api.getAppConfig().then((cfg) => { - if (cfg?.controllerInputMode) { - setInputMode(cfg.controllerInputMode); - } - }); + api + .getAppConfig() + .then((cfg) => { + if (cfg?.controllerInputMode) { + setInputMode(cfg.controllerInputMode); + } + }) + .catch(() => {}); }, []); const cycleInputMode = useCallback(() => { @@ -102,8 +105,7 @@ const ControllerDetail = ({ controller, onBack }) => { if (showTester) { return ( setShowTester(false)} /> ); diff --git a/src_frontend/components/ControllerTester.jsx b/src_frontend/components/ControllerTester.jsx index f8904f8..30f6c89 100644 --- a/src_frontend/components/ControllerTester.jsx +++ b/src_frontend/components/ControllerTester.jsx @@ -1,5 +1,9 @@ import { useEffect, useMemo, useRef, useState } from "react"; +import { + MODEL_COMPONENTS, + resolveControllerModel, +} from "../assets/controller-models/index.js"; import { useTranslation } from "../contexts/TranslationContext"; import { useScopedInput } from "../hooks/useScopedInput"; import * as api from "../utils/ipc"; @@ -8,532 +12,18 @@ import DialogLayout from "./DialogLayout"; import "../styles/ControllerTester.css"; -const BTN = { - A: 0, - B: 1, - X: 2, - Y: 3, - LB: 4, - RB: 5, - LT: 6, - RT: 7, - SELECT: 8, - START: 9, - L3: 10, - R3: 11, - DPAD_UP: 12, - DPAD_DOWN: 13, - DPAD_LEFT: 14, - DPAD_RIGHT: 15, - HOME: 16, -}; - -function getButtonValue(state, index) { - return state?.buttons?.[index]?.value ?? 0; -} - -function isPressed(state, index) { - return state?.buttons?.[index]?.pressed ?? false; -} - -function getAxis(state, index) { - return state?.axes?.[index] ?? 0; -} - -const btnFill = (pressed, color) => - pressed ? color : "rgba(255,255,255,0.06)"; -const btnStroke = (pressed, color) => - pressed ? color : "rgba(255,255,255,0.18)"; -const txtFill = (pressed, color) => - pressed ? "#fff" : `${color}aa`; - -const TriggerMeter = ({ x, y, width, label, value, accent, align = "start" }) => { - const alignment = { - center: { barX: -(width / 2), textAnchor: "middle" }, - end: { barX: -width, textAnchor: "end" }, - start: { barX: 0, textAnchor: "start" }, - }; - const { barX, textAnchor } = alignment[align] || alignment.start; - - return ( - - - {label} {Math.round(value * 100)}% - - - - - ); -}; - -/* ═══════════════════════════════════════════════════════════════ - Xbox One Controller — e7d/gamepad-viewer SVG paths (MIT) - viewBox matches the original 750 × 630.455 base - ═══════════════════════════════════════════════════════════════ */ - -const XboxModel = ({ s }) => { - const stickTravel = 25; - const lx = 185.468 + getAxis(s, 0) * stickTravel; - const ly = 279.865 + getAxis(s, 1) * stickTravel; - const rx = 473.042 + getAxis(s, 2) * stickTravel; - const ry = 393.815 + getAxis(s, 3) * stickTravel; - const lt = getButtonValue(s, BTN.LT); - const rt = getButtonValue(s, BTN.RT); - const accent = "#107c10"; - - return ( - - - {/* ── Base body (from e7d/gamepad-viewer xbox-one/base-black.svg, MIT) ── */} - - - - {/* ── D-Pad well & cross ── */} - - - - - {/* ── View / Menu buttons ── */} - - - - {/* ── Face plate / Top bar ── */} - - - - {/* ── Left stick well ── */} - - - - - - {/* ── Right stick well ── */} - - - - - - {/* ── Triggers (from e7d xbox-one/trigger.svg) ── */} - - - - - - - - - - {/* ── Xbox guide button ── */} - - - - {/* ── Face buttons (ABXY) — positions from e7d CSS ── */} - {/* A - bottom */} - - A - {/* B - right */} - - B - {/* X - left */} - - X - {/* Y - top */} - - Y - - {/* ── View button (select) ── */} - - - - - - {/* ── Menu button (start) ── */} - - - - - - {/* ── D-Pad interactive overlay ── */} - - {/* cross background */} - - - {/* Up */} - - {/* Down */} - - {/* Left */} - - {/* Right */} - - - - {/* ── Left bumper (LB) ── */} - - - - LB - - - - {/* ── Right bumper (RB) ── */} - - - - RB - - - - {/* ── Left stick knob ── */} - - - - - {/* ── Right stick knob ── */} - - - - - - ); -}; - -/* ═══════════════════════════════════════════════════════════════ - DualShock 4 / DualSense — e7d/gamepad-viewer SVG paths (MIT) - viewBox matches the original 806 × 630 base (padded for HUD) - ═══════════════════════════════════════════════════════════════ */ - -const PlayStationModel = ({ s }) => { - const stickTravel = 25; - const lx = 274.65 + getAxis(s, 0) * stickTravel; - const ly = 355.967 + getAxis(s, 1) * stickTravel; - const rx = 531.25 + getAxis(s, 2) * stickTravel; - const ry = 355.967 + getAxis(s, 3) * stickTravel; - const lt = getButtonValue(s, BTN.LT); - const rt = getButtonValue(s, BTN.RT); - const accent = "#006fcd"; - - return ( - - {/* ── Base body (from e7d/gamepad-viewer ds4/base-black.svg, MIT) ── */} - - - {/* ── Grip shading (left) ── */} - - {/* ── Grip shading (right) ── */} - - - {/* ── Left stick well ── */} - - - - - {/* ── Right stick well ── */} - - - - - {/* ── Touchpad ── */} - - - - {/* ── Face button / D-Pad background circles ── */} - - - - {/* ── D-Pad arrow indicators (from base) ── */} - - - {/* ── Create / Options button wells ── */} - - - - {/* ── Speaker grille dots ── */} - - - - - - - - - - - - - - - - {/* ── Light bar ── */} - - - {/* ── Trigger areas (from base) ── */} - - - - - - {/* ── Trigger 3D shapes (from base) ── */} - - - - {/* ── PS button (from base) ── */} - - - - - - {isPressed(s, BTN.HOME) && } - - {/* ═══ Interactive overlays ═══ */} - - {/* ── D-Pad arrows (from e7d ds4/dpad.svg, each path individually positioned via svgpathtools bbox) ── */} - {/* Up — path center (17.71,25.50) → target (154,207) */} - - {/* Down — path center (55.10,25.50) → target (154,281) */} - - {/* Left — path center (153.05,17.85) → target (118,243) */} - - {/* Right — path center (100.26,17.76) → target (191,243) */} - - - {/* ── Face buttons (PlayStation symbols — e7d ds4/buttons.svg paths, scaled to 70% of r=26 circle) ── */} - {/* Cross (A) - bottom */} - - - - - {/* Circle (B) - right */} - - - - - {/* Square (X) - left */} - - - - - {/* Triangle (Y) - top */} - - - - - - {/* ── Create button ── */} - - CRE - - {/* ── Options button ── */} - - OPT - - {/* ── L1 bumper ── */} - - - - L1 - - - - {/* ── R1 bumper ── */} - - - - R1 - - - - {/* ── Left stick knob ── */} - - - - - {/* ── Right stick knob ── */} - - - - - - ); -}; - -/* ═══════════════════════════════════════════════════════════════ - Nintendo Pro Controller — hand-crafted (no e7d template) - ═══════════════════════════════════════════════════════════════ */ - -const NintendoModel = ({ s }) => { - const stickR = 12; - const lx = 152 + getAxis(s, 0) * stickR; - const ly = 158 + getAxis(s, 1) * stickR; - const rx = 370 + getAxis(s, 2) * stickR; - const ry = 215 + getAxis(s, 3) * stickR; - const lt = getButtonValue(s, BTN.LT); - const rt = getButtonValue(s, BTN.RT); - - return ( - - {/* ── Body shadow ── */} - - - {/* ── Main body (dark charcoal like real Pro Controller) ── */} - - - {/* ── Left grip ── */} - - - {/* ── Right grip ── */} - - - {/* ── Face plate highlight ── */} - - - {/* ── Left bumper (L) ── */} - - L - - {/* ── Right bumper (R) ── */} - - R - - {/* ── Left trigger (ZL) ── */} - - - {/* ── Right trigger (ZR) ── */} - - - {/* ── Left stick well ── */} - - - - - {/* Left stick knob */} - - - - {/* ── Right stick well ── */} - - - - - {/* Right stick knob */} - - - - {/* ── D-Pad (cross style) ── */} - - - - - - - - - - - {/* ── Face buttons (ABXY Nintendo layout) ── */} - - {/* X - top */} - - X - {/* B - bottom */} - - B - {/* Y - left */} - - Y - {/* A - right */} - - A - - - {/* ── Minus button ── */} - - - - {/* ── Plus button ── */} - - + - - {/* ── Home button ── */} - - - - {/* ── Capture button (square) ── */} - - - - ); -}; - -/* ═══════════════════════════════════════════════════════════════ */ - -const MODEL_MAP = { - xbox: XboxModel, - playstation: PlayStationModel, - nintendo: NintendoModel, -}; - export const ControllerTesterFocusId = "ControllerTester"; -const ControllerTester = ({ onClose, controllerIndex, controllerFamily }) => { +const ControllerTester = ({ onClose, controller }) => { const { t } = useTranslation(); const [gamepadState, setGamepadState] = useState(null); const rafRef = useRef(null); - const ModelComponent = useMemo( - () => MODEL_MAP[controllerFamily] || XboxModel, - [controllerFamily], + const modelId = useMemo( + () => resolveControllerModel(controller), + [controller], ); + const ModelComponent = MODEL_COMPONENTS[modelId] || MODEL_COMPONENTS.xbox; useScopedInput( (event) => { @@ -553,7 +43,7 @@ const ControllerTester = ({ onClose, controllerIndex, controllerFamily }) => { try { const gamepads = await api.pollGamepadsSdl(); if (!active) return; - const gp = gamepads.find((g) => g.index === controllerIndex) ?? gamepads[0] ?? null; + const gp = gamepads.find((g) => g.index === controller?.index) ?? gamepads[0] ?? null; setGamepadState(gp); } catch { // SDL not available — skip @@ -566,9 +56,11 @@ const ControllerTester = ({ onClose, controllerIndex, controllerFamily }) => { rafRef.current = requestAnimationFrame(poll); return () => { active = false; - if (rafRef.current) cancelAnimationFrame(rafRef.current); + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } }; - }, [controllerIndex]); + }, [controller]); const legendItems = useMemo( () => [{ button: "B", label: t("Close"), onClick: onClose }], diff --git a/src_frontend/styles/ControllerTester.css b/src_frontend/styles/ControllerTester.css index 7afee6f..8e10001 100644 --- a/src_frontend/styles/ControllerTester.css +++ b/src_frontend/styles/ControllerTester.css @@ -22,3 +22,25 @@ .ct-stick-anim { transition: cx 0.04s linear, cy 0.04s linear, fill 0.08s; } + +.ct-model-wrap { + position: relative; + width: 100%; + max-width: 620px; + filter: drop-shadow(0 4px 24px rgba(0, 0, 0, 0.5)); +} + +.ct-model-base { + display: block; + width: 100%; + height: auto; + background: transparent; + border-radius: 12px; +} + +.ct-model-overlay { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} From d47dfa86734ee56cc325b04f4e6039692d6e032a Mon Sep 17 00:00:00 2001 From: HashimHS Date: Sat, 4 Apr 2026 17:34:56 +0200 Subject: [PATCH 7/8] restored file --- generate-gamepad-mapping.cjs | 68 +++++------------------------------- 1 file changed, 8 insertions(+), 60 deletions(-) diff --git a/generate-gamepad-mapping.cjs b/generate-gamepad-mapping.cjs index 41739a7..1fa3ad2 100644 --- a/generate-gamepad-mapping.cjs +++ b/generate-gamepad-mapping.cjs @@ -1,19 +1,7 @@ const fs = require("node:fs"); -const DB_SOURCES = [ - { - name: "mdqinc/SDL_GameControllerDB", - url: "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt", - }, - { - name: "mdqinc/SDL_GameControllerDB (raw master fallback)", - url: "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt", - }, - { - name: "gabomdq/SDL_GameControllerDB (legacy mirror)", - url: "https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt", - }, -]; +const DB_URL = + "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt"; const STD_BTN = { a: 0, @@ -140,64 +128,24 @@ function buildJson(textData) { } async function fetchAndBuildJson() { - let response = null; - let sourceName = null; - let sourceUrl = null; - const failures = []; - - for (const source of DB_SOURCES) { - try { - const candidate = await fetch(source.url); - if (!candidate.ok) { - failures.push(`${source.name}: HTTP ${candidate.status}`); - continue; - } - - response = candidate; - sourceName = source.name; - sourceUrl = source.url; - break; - } catch (error) { - failures.push(`${source.name}: ${error?.message || String(error)}`); - } - } - - if (!response) { - throw new Error(`Failed to download controller DB from all sources:\n${failures.join("\n")}`); + const response = await fetch(DB_URL); + if (!response.ok) { + throw new Error(`Failed: ${response.status}`); } const textData = await response.text(); const database = buildJson(textData); - const generatedAt = new Date().toISOString(); fs.writeFileSync( "./src_frontend/resources/gamepad_mapping_db.json", JSON.stringify(database), ); - fs.writeFileSync( - "./src_frontend/resources/gamepad_mapping_db.meta.json", - JSON.stringify( - { - generatedAt, - sourceName, - sourceUrl, - fallbackSources: DB_SOURCES.map((item) => ({ - name: item.name, - url: item.url, - })), - controllerCount: Object.keys(database).length, - }, - null, - 2, - ), - ); - fs.writeFileSync("./src_backend/resources/gamecontrollerdb.txt", textData); - console.log(`Generated mappings for ${Object.keys(database).length} controllers.`); - console.log(`Source: ${sourceName}`); - console.log(`URL: ${sourceUrl}`); + console.log( + `Generated mappings for ${Object.keys(database).length} controllers.`, + ); } if (require.main === module) { From 42c841e157a4501bb544c98ffd1d35b228ecbd45 Mon Sep 17 00:00:00 2001 From: HashimHS Date: Sat, 4 Apr 2026 17:49:00 +0200 Subject: [PATCH 8/8] Make Xinput option only available to non-xbox controllers --- .../components/ControllerSettings.jsx | 29 ++++++++++++++----- src_frontend/styles/ControllerSettings.css | 8 +++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src_frontend/components/ControllerSettings.jsx b/src_frontend/components/ControllerSettings.jsx index cfa8a76..d4a0d49 100644 --- a/src_frontend/components/ControllerSettings.jsx +++ b/src_frontend/components/ControllerSettings.jsx @@ -23,26 +23,36 @@ const ControllerDetail = ({ controller, onBack }) => { const [inputMode, setInputMode] = useState("native"); const [showTester, setShowTester] = useState(false); const scrollParentReference = useRef(null); + const isXboxFamily = controller?.family === "xbox"; useEffect(() => { api .getAppConfig() .then((cfg) => { - if (cfg?.controllerInputMode) { - setInputMode(cfg.controllerInputMode); + const configuredMode = cfg?.controllerInputMode || "native"; + const resolvedMode = isXboxFamily ? "native" : configuredMode; + + setInputMode(resolvedMode); + + if (isXboxFamily && configuredMode !== "native") { + api.setAppConfig("controllerInputMode", "native"); } }) .catch(() => {}); - }, []); + }, [isXboxFamily]); const cycleInputMode = useCallback(() => { + if (isXboxFamily) { + return; + } + setInputMode((prev) => { const next = INPUT_MODES[(INPUT_MODES.indexOf(prev) + 1) % INPUT_MODES.length]; api.setAppConfig("controllerInputMode", next); return next; }); - }, []); + }, [isXboxFamily]); const handleAction = useCallback( (actionName, item) => { @@ -69,13 +79,14 @@ const ControllerDetail = ({ controller, onBack }) => { id: "input-mode", label: t("Input Mode"), value: modeLabel, + disabled: isXboxFamily, }, { id: "test-controller", label: t("Test Controller"), }, ], - [modeLabel, t], + [isXboxFamily, modeLabel, t], ); const renderItem = useCallback( @@ -85,9 +96,13 @@ const ControllerDetail = ({ controller, onBack }) => { isFocused={isFocused} onMouseEnter={onMouseEnter} > -
+
{item.label} - {item.value && {item.value}} + {item.value && ( + + {item.value} + + )}
), diff --git a/src_frontend/styles/ControllerSettings.css b/src_frontend/styles/ControllerSettings.css index 3c7b2d0..74f31e0 100644 --- a/src_frontend/styles/ControllerSettings.css +++ b/src_frontend/styles/ControllerSettings.css @@ -15,6 +15,10 @@ overflow: hidden; } +.cs-row-content.disabled { + opacity: 0.45; +} + .cs-row-value { margin-left: auto; color: var(--accent-color); @@ -23,6 +27,10 @@ font-size: 0.9rem; } +.cs-row-value.disabled { + color: var(--text-secondary-color, rgba(255, 255, 255, 0.72)); +} + .cs-row-label { white-space: nowrap; overflow: hidden;