From 8158d01d8d0eabed05999c4595b5b0434c35448c Mon Sep 17 00:00:00 2001 From: Christopher Uljasz Date: Mon, 31 Aug 2026 19:17:33 -0400 Subject: [PATCH 1/6] Fixed gamepad not working in some games due to product and vendor ids not being recognized, fixed by spoofing an Xbox 360 Controller instead. Added a new setting to allow external gamepad to advertise their real device name and product ids (this can for example be enabled if a Dualshock gamepad is connected and the game you're plyaing has custom layout or playstation icons when a PS controller is connected) --- app/src/main/cpp/winlator/fakeinput.cpp | 111 +++++++++++++-- app/src/main/feature/library/GameSettings.kt | 63 +++++++++ .../ShortcutSettingsComposeDialog.kt | 7 + app/src/main/res/values/strings.xml | 5 + .../GuestProgramLauncherComponent.java | 10 +- .../display/winhandler/WinHandler.java | 23 ++++ .../input/controls/GamepadIdentityStore.java | 128 ++++++++++++++++++ 7 files changed, 333 insertions(+), 14 deletions(-) create mode 100644 app/src/main/runtime/input/controls/GamepadIdentityStore.java diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index 855d5c76e..384f41501 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -38,10 +39,11 @@ #define EXPORT __attribute__((visibility("default"))) extern "C" -static constexpr uint16_t GAMEPAD_VENDOR_ID_BASE = 0x1234; -static constexpr uint16_t GAMEPAD_PRODUCT_ID_BASE = 0x5678; +static constexpr uint16_t GAMEPAD_VENDOR_ID_DEFAULT = 0x045E; // Microsoft +static constexpr uint16_t GAMEPAD_PRODUCT_ID_DEFAULT = 0x028E; // Xbox 360 Controller static constexpr uint16_t GAMEPAD_VERSION = 0x0110; -static constexpr const char *GAMEPAD_NAME_TEMPLATE = "Generic HID Gamepad %d"; +static constexpr uint32_t GAMEPAD_IDENTITY_MAX_SLOTS = 4; +static constexpr const char *GAMEPAD_NAME_TEMPLATE = "Xbox 360 Controller (%d)"; static constexpr const char *GAMEPAD_PHYS_TEMPLATE = "usb-fakeinput/input%d"; static constexpr const char *GAMEPAD_UNIQ_TEMPLATE = "0000000000%02d"; static constexpr uint8_t GAMEPAD_AXIS_COUNT = 8; @@ -80,6 +82,16 @@ static constexpr size_t FAKE_INPUT_RING_SIZE = FAKE_INPUT_RING_HEADER_SIZE + (FAKE_INPUT_RING_CAPACITY * FAKE_INPUT_EVENT_SIZE); +// Per-slot real-controller identity, published by the host when the user opts +// into "Force External Gamepad Identity" so games with dedicated non-Xbox +// controller support (e.g. PlayStation) can recognize the real pad instead +// of the spoofed Xbox 360 Controller identity. +struct GamepadIdentity { + uint16_t vendor; + uint16_t product; + std::string name; +}; + struct FakeController { char *event = nullptr; int slot = -1; @@ -93,6 +105,7 @@ struct FakeController { size_t keyframe_remaining = 0; int32_t keyframe_axes[8] = {0, 0, 0, 0, 0, 0, 0, 0}; uint32_t keyframe_buttons = 0; + GamepadIdentity identity; }; struct NeutralEventSpec { @@ -354,6 +367,69 @@ get_ring_path_for_slot(int slot) { return it == ring_paths.end() ? nullptr : it->second.c_str(); } +// Reads the slot's identity straight out of its udev entry +// ("/c13:"), which the host keeps up to date as pads are +// bound and released. +// +// Returns false when the entry is missing or incomplete, which leaves the caller +// on the compiled-in Xbox 360 default. +__attribute__((visibility("hidden"))) static bool +read_gamepad_identity(int slot, GamepadIdentity *out) { + if (!udev_data_dir || !*udev_data_dir || !my_open) + return false; + if (slot < 0 || slot >= GAMEPAD_IDENTITY_MAX_SLOTS) + return false; + + char path[PATH_MAX]; + snprintf(path, PATH_MAX, "%s/c13:%u", udev_data_dir, + FAKE_INPUT_EVENT_MINOR_BASE + slot); + + int fd = my_open(path, O_RDONLY); + if (fd < 0) + return false; + + // A udev entry for one of our pads is a few hundred bytes; read it whole so a + // truncated tail can never hide the NAME line. + char buffer[2048]; + ssize_t length = syscall(SYS_read, fd, buffer, sizeof(buffer) - 1); + syscall(SYS_close, fd); + if (length <= 0) + return false; + buffer[length] = '\0'; + + GamepadIdentity parsed; + parsed.vendor = 0; + parsed.product = 0; + bool have_vendor = false; + bool have_product = false; + char *saveptr = nullptr; + for (char *line = strtok_r(buffer, "\n", &saveptr); line; + line = strtok_r(nullptr, "\n", &saveptr)) { + if (!strncmp(line, "E:ID_VENDOR_ID=", 15)) { + parsed.vendor = static_cast(strtoul(line + 15, nullptr, 16)); + have_vendor = true; + } else if (!strncmp(line, "E:ID_MODEL_ID=", 14)) { + parsed.product = static_cast(strtoul(line + 14, nullptr, 16)); + have_product = true; + } else if (!strncmp(line, "E:NAME=", 7)) { + // udev quotes the value; the name itself never contains a quote because + // the host strips them before publishing. + const char *value = line + 7; + size_t value_length = strlen(value); + if (value_length >= 2 && value[0] == '"' && value[value_length - 1] == '"') + parsed.name.assign(value + 1, value_length - 2); + else + parsed.name.assign(value); + } + } + + if (!have_vendor || !have_product || parsed.name.empty()) + return false; + + *out = parsed; + return true; +} + __attribute__((visibility("hidden"))) static uint64_t ring_write_seq(const FakeInputRingHeader *ring) { return __atomic_load_n(&ring->write_seq, __ATOMIC_ACQUIRE); @@ -471,8 +547,7 @@ open_fake_input_ring(const char *event, int flags) { return -1; } - FakeInputRingHeader *ring = - reinterpret_cast(mapping); + FakeInputRingHeader *ring = reinterpret_cast(mapping); if (!ring_header_is_valid(ring)) { munmap(mapping, FAKE_INPUT_RING_SIZE); syscall(SYS_close, fd); @@ -487,6 +562,15 @@ open_fake_input_ring(const char *event, int flags) { controller.mapping_size = FAKE_INPUT_RING_SIZE; controller.read_seq = ring_write_seq(ring); controller.generation = ring_generation(ring); + + if (!read_gamepad_identity(slot, &controller.identity)) { + char buf[32]; + snprintf(buf, sizeof(buf), GAMEPAD_NAME_TEMPLATE, slot); + controller.identity.name = std::string(buf); + controller.identity.vendor = static_cast(GAMEPAD_VENDOR_ID_DEFAULT); + controller.identity.product = static_cast(GAMEPAD_PRODUCT_ID_DEFAULT); + } + // Emit the current absolute state as the first frame so a guest that opens // mid-hold (or reopens after a slot hand-off) starts already in sync. capture_keyframe(controller, "open", fd); @@ -506,6 +590,15 @@ copy_slot_ioctl_string(int op, void *argp, const char *format, int event_number) snprintf(static_cast(argp), size, format, event_number); } +__attribute__((visibility("hidden"))) static void +copy_ioctl_string(int op, void *argp, const char *value) { + size_t size = _IOC_SIZE(op); + if (!argp || size == 0) + return; + + snprintf(static_cast(argp), size, "%s", value); +} + __attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) { return controller_map.find(fd) != controller_map.end(); } @@ -877,14 +970,14 @@ EXPORT int ioctl(int fd, int op, ...) { struct input_id id; memset(&id, 0, sizeof(id)); id.bustype = 0x03; - id.vendor = static_cast(GAMEPAD_VENDOR_ID_BASE + event_number); - id.product = static_cast(GAMEPAD_PRODUCT_ID_BASE + event_number); + id.vendor = controller->second.identity.vendor; + id.product = controller->second.identity.product; id.version = GAMEPAD_VERSION; memcpy(argp, (void *)&id, sizeof(id)); return 0; } else if (type == 0x45 && number == 0x6) { Logger::log("Hooking ioctl EVIOCGNAME for event %s\n", event); - copy_slot_ioctl_string(op, argp, GAMEPAD_NAME_TEMPLATE, event_number); + copy_ioctl_string(op, argp, controller->second.identity.name.c_str()); return 0; } else if (type == 0x45 && number == 0x7) { Logger::log("Hooking ioctl EVIOCGPHYS for event %s\n", event); @@ -1008,7 +1101,7 @@ EXPORT int ioctl(int fd, int op, ...) { return 0; } else if (type == 0x6A && number == 0x13) { Logger::log("Hooking ioctl JSIOCGNAME(len) for event %s\n", event); - copy_slot_ioctl_string(op, argp, GAMEPAD_NAME_TEMPLATE, event_number); + copy_ioctl_string(op, argp, controller->second.identity.name.c_str()); return 0; } else { Logger::log("Unhandled evdev ioctl, type %d number %d\n", type, number); diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index 0e5aa9e1c..9376c18eb 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -552,6 +552,7 @@ class GameSettingsStateHolder { val numControllersEntries = mutableStateOf>(emptyList()) val selectedNumControllers = mutableIntStateOf(0) val disableXInput = mutableStateOf(false) + val forceExternalGamepadIdentity = mutableStateOf(false) val simTouchScreen = mutableStateOf(false) val screenTouchMode = mutableIntStateOf(0) val gestureProfileEntries = mutableStateOf>(emptyList()) @@ -4870,6 +4871,68 @@ private fun InputSection(state: GameSettingsStateHolder) { Spacer(Modifier.height(4.dp)) + // Force External Gamepad Identity, reports the real controller's + // name/VID/PID instead of the spoofed Xbox 360 identity, for games with a + // dedicated non-Xbox (e.g. PlayStation) input path. + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.weight(1f)) { + SettingCheckbox( + label = stringResource(R.string.shortcuts_properties_force_external_gamepad_identity), + checked = state.forceExternalGamepadIdentity.value, + onCheckedChange = { state.forceExternalGamepadIdentity.value = it } + ) + } + var showForceExternalGamepadIdentityHelp by remember { mutableStateOf(false) } + val forceExternalGamepadIdentityHelpOffset = rememberSmartDropdownOffset() + Box { + Box( + modifier = Modifier + .size(30.dp) + .clip(RoundedCornerShape(6.dp)) + .background(InputSurface) + .border(1.dp, InputBorder, RoundedCornerShape(6.dp)) + .paneNavItem( + cornerRadius = 6.dp, + onActivate = { showForceExternalGamepadIdentityHelp = !showForceExternalGamepadIdentityHelp }, + highlightColor = NavHighlight + ) + .smartDropdownAnchor(offset = forceExternalGamepadIdentityHelpOffset) { + showForceExternalGamepadIdentityHelp = !showForceExternalGamepadIdentityHelp + }, + contentAlignment = Alignment.Center + ) { + Icon( + Icons.AutoMirrored.Outlined.HelpOutline, + contentDescription = null, + tint = TextPrimary, + modifier = Modifier.size(18.dp) + ) + } + DropdownMenu( + expanded = showForceExternalGamepadIdentityHelp, + onDismissRequest = { showForceExternalGamepadIdentityHelp = false }, + offset = forceExternalGamepadIdentityHelpOffset.value, + shape = RoundedCornerShape(8.dp), + containerColor = CardSurface, + modifier = Modifier + .padding(10.dp) + .width(280.dp) + ) { + HtmlText( + stringResource(R.string.shortcuts_properties_help_force_external_gamepad_identity), + color = TextPrimary, + fontSize = SettingLabelSize, + lineHeight = 16.sp + ) + } + } + } + + Spacer(Modifier.height(4.dp)) + // Touch input mode (Trackpad / Touchscreen / Map to Right Stick) val gesturesOff = state.selectedGestureProfile.intValue == 0 val onSelectMode: (Int) -> Unit = { mode -> diff --git a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt index 7cd29d128..3ab17f31e 100644 --- a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt +++ b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt @@ -386,6 +386,8 @@ class ShortcutSettingsComposeDialog private constructor( state.selectedDInputMapperType.intValue = if ((inputType and WinHandler.FLAG_DINPUT_MAPPER_STANDARD.toInt()) == WinHandler.FLAG_DINPUT_MAPPER_STANDARD.toInt()) 0 else 1 state.disableXInput.value = shortcut.getExtra("disableXinput", "0") == "1" + state.forceExternalGamepadIdentity.value = + shortcut.getExtra("forceExternalGamepadIdentity", "0") == "1" state.shortcutExclusiveXInput.value = shortcut.getExtra("exclusiveXInput", "").let { if (it.isEmpty()) container.isExclusiveXInput() else it == "1" } @@ -1252,6 +1254,11 @@ class ShortcutSettingsComposeDialog private constructor( shortcut.putExtra("disableXinput", disableXinputValue) if (disableXinputValue != null) hasContainerOverride = true + val forceExternalGamepadIdentityValue = + if (state.forceExternalGamepadIdentity.value) "1" else null + shortcut.putExtra("forceExternalGamepadIdentity", forceExternalGamepadIdentityValue) + if (forceExternalGamepadIdentityValue != null) hasContainerOverride = true + shortcut.putExtra("exclusiveXInput", if (state.shortcutExclusiveXInput.value) "1" else "0") if (state.shortcutExclusiveXInput.value != container.isExclusiveXInput()) hasContainerOverride = true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eaae663b0..9410dc08a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -335,6 +335,11 @@ Disable Xinput (For Exclusive M/KB Control) Number of Controllers Exclusive Input + Force External Gamepad Identity + + Reports the real name, vendor ID and product ID of your connected physical controller to the game, instead of the spoofed <i><b>Xbox 360 Controller</b></i> identity.<br/><br/> + Enable this for games with a dedicated input path for other controller types, such as <i><b>PlayStation</b></i> gamepads. Leave this off unless your game fails to recognize your controller correctly. + Custom Game Settings Use Legacy Launcher Legacy Steam emulation: runs the game through the ColdClient launcher and strips SteamStub DRM from the exe. Use only for older games that don\'t work with the Steam Launcher. diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 0e3e76f15..42465e4af 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -19,6 +19,7 @@ import com.winlator.cmod.runtime.display.environment.EnvironmentComponent; import com.winlator.cmod.runtime.display.environment.ImageFs; import com.winlator.cmod.runtime.input.controls.FakeInputWriter; +import com.winlator.cmod.runtime.input.controls.GamepadIdentityStore; import com.winlator.cmod.runtime.system.GPUInformation; import com.winlator.cmod.runtime.system.ProcessHelper; import com.winlator.cmod.runtime.wine.EnvVars; @@ -792,14 +793,13 @@ private void prepareFakeInputUdevMetadata(ImageFs imageFs, File devInputDir, Env if (!byIdDir.exists()) byIdDir.mkdirs(); int numControllers = getConfiguredControllerCount(); + String vendor = GamepadIdentityStore.formatId(GamepadIdentityStore.DEFAULT_VENDOR_ID); + String product = GamepadIdentityStore.formatId(GamepadIdentityStore.DEFAULT_PRODUCT_ID); + for (int slot = 0; slot < numControllers; slot++) { - int vendorId = 0x1234 + slot; - int productId = 0x5678 + slot; int eventMinor = 64 + slot; - String name = "Generic HID Gamepad " + slot; + String name = GamepadIdentityStore.getDefaultName(slot); File udevData = new File(udevDataDir, "c13:" + eventMinor); - String vendor = String.format(java.util.Locale.US, "%04x", vendorId); - String product = String.format(java.util.Locale.US, "%04x", productId); String symlink = "input/by-id/usb-WinNative_Generic_HID_Gamepad_" + slot + "-event-joystick"; String content = "I:" diff --git a/app/src/main/runtime/display/winhandler/WinHandler.java b/app/src/main/runtime/display/winhandler/WinHandler.java index 453c404c0..084f185b1 100644 --- a/app/src/main/runtime/display/winhandler/WinHandler.java +++ b/app/src/main/runtime/display/winhandler/WinHandler.java @@ -23,6 +23,7 @@ import com.winlator.cmod.runtime.input.controls.ControlsProfile; import com.winlator.cmod.runtime.input.controls.ExternalController; import com.winlator.cmod.runtime.input.controls.FakeInputWriter; +import com.winlator.cmod.runtime.input.controls.GamepadIdentityStore; import com.winlator.cmod.runtime.input.controls.GamepadState; import com.winlator.cmod.runtime.input.rumble.GamepadRumbleManager; import com.winlator.cmod.runtime.input.rumble.GcmRumbleMode; @@ -838,6 +839,24 @@ private void ensureWriterForSlot(int slot) { this.writers[slot] = new FakeInputWriter(this.fakeInputBasePath, slot); this.writers[slot].open(); } + // Republish even when the writer already existed: the slot may have just changed + // hands (virtual pad -> physical pad, or one pad for another). + GamepadIdentityStore.refreshSlot(slot, resolveDeviceForSlot(slot)); + } + + // The physical pad currently bound to a fake input slot, for the per-game "Force + // External Gamepad Identity" option. The virtual on-screen pad has no real identity, + // so it leaves the slot on the default Xbox 360 spoof. + private InputDevice resolveDeviceForSlot(int slot) { + for (Map.Entry entry : this.deviceToSlot.entrySet()) { + if (entry.getKey() != OSC_DEVICE_ID && entry.getValue() == slot) { + InputDevice device = InputDevice.getDevice(entry.getKey()); + if (device != null) { + return device; + } + } + } + return null; } private boolean isPhysicalSlotOccupied(int slot) { @@ -1101,6 +1120,9 @@ private void releaseSlot(int deviceId) { + slot + " still used by sibling sub-device."); } + // Drop the unplugged pad's published identity, or fall back to the surviving + // sibling sub-device's, so the guest stops seeing a pad that is gone. + GamepadIdentityStore.refreshSlot(slot, resolveDeviceForSlot(slot)); this.controllers.remove(deviceId); if (deviceId != OSC_DEVICE_ID) { if (!slotStillInUse) { @@ -1374,6 +1396,7 @@ public void closeFakeInputWriter() { } } FakeInputWriter.releaseAllRingSlots(); + GamepadIdentityStore.reset(); this.deviceToSlot.clear(); this.descriptorToSlot.clear(); this.deviceToDescriptor.clear(); diff --git a/app/src/main/runtime/input/controls/GamepadIdentityStore.java b/app/src/main/runtime/input/controls/GamepadIdentityStore.java new file mode 100644 index 000000000..80ffcc8f3 --- /dev/null +++ b/app/src/main/runtime/input/controls/GamepadIdentityStore.java @@ -0,0 +1,128 @@ +package com.winlator.cmod.runtime.input.controls; + +import android.util.Log; +import android.view.InputDevice; +import com.winlator.cmod.shared.io.FileUtils; +import java.io.File; +import java.util.Locale; + +/** + * Publishes the real identity (name, vendor ID, product ID) of the physical controller bound to + * each fake input slot, for the "Force External Gamepad Identity" option. + * + *

The slot's udev entry is the only place the identity lives: libudev-based enumeration in the + * guest already reads it, and the native ioctl hooks parse the same file, so the two can never + * disagree. Rewriting it as pads come and go is what makes hotplug work without relaunching. + */ +public final class GamepadIdentityStore { + public static final int DEFAULT_VENDOR_ID = 0x045E; // Microsoft + public static final int DEFAULT_PRODUCT_ID = 0x028E; // Xbox 360 Controller + + private static final String TAG = "GamepadIdentityStore"; + private static final int MAX_SLOTS = 4; + private static final int EVENT_MINOR_BASE = 64; + private static final int MAX_NAME_LENGTH = 80; + + private static File udevDataDir; + + private GamepadIdentityStore() {} + + public static String formatId(int id) { + return String.format(Locale.US, "%04x", id & 0xFFFF); + } + + public static String getDefaultName(int slot) { + return "Xbox 360 Controller (" + slot + ")"; + } + + /** Disables publishing and restores every slot to the default Xbox 360 spoof. */ + public static synchronized void reset() { + if (udevDataDir != null) { + for (int slot = 0; slot < MAX_SLOTS; slot++) { + clearSlotLocked(slot); + } + } + udevDataDir = null; + } + + /** Re-publishes the identity for one slot; call whenever its bound device may have changed. */ + public static synchronized void refreshSlot(int slot, InputDevice slotDevice) { + if (udevDataDir == null) { + return; + } + refreshSlotLocked(slot, slotDevice); + } + + private static void refreshSlotLocked(int slot, InputDevice device) { + if (device == null) { + clearSlotLocked(slot); + return; + } + + String name = sanitizeName(device.getName()); + String vendor = formatId(device.getVendorId()); + String product = formatId(device.getProductId()); + if (writeIdentityLocked(slot, vendor, product, name)) { + Log.d( + TAG, + "Published gamepad identity for slot " + + slot + + ": " + + name + + " (" + + vendor + + ":" + + product + + ")"); + } + } + + private static void clearSlotLocked(int slot) { + writeIdentityLocked( + slot, formatId(DEFAULT_VENDOR_ID), formatId(DEFAULT_PRODUCT_ID), getDefaultName(slot)); + } + + /** + * Rewrites just the identity fields of the slot's udev entry, leaving the rest of it (discovery + * tags, device node, symlink) untouched. + */ + private static boolean writeIdentityLocked( + int slot, String vendor, String product, String name) { + if (udevDataDir == null) { + return false; + } + File udevData = new File(udevDataDir, "c13:" + (EVENT_MINOR_BASE + slot)); + if (!udevData.isFile()) { + return false; + } + + StringBuilder content = new StringBuilder(); + for (String line : FileUtils.readLines(udevData)) { + if (line.startsWith("E:ID_VENDOR_ID=")) { + line = "E:ID_VENDOR_ID=" + vendor; + } else if (line.startsWith("E:ID_MODEL_ID=")) { + line = "E:ID_MODEL_ID=" + product; + } else if (line.startsWith("E:NAME=")) { + line = "E:NAME=\"" + name + "\""; + } + content.append(line).append('\n'); + } + if (!FileUtils.writeString(udevData, content.toString())) { + Log.w(TAG, "Failed to publish gamepad identity for slot " + slot); + return false; + } + return true; + } + + // The name lands in udev's quoted, newline-delimited NAME field, so strip anything that would + // break that parser on either side. + private static String sanitizeName(String name) { + if (name == null) { + return ""; + } + String sanitized = name.replaceAll("[\\r\\n\"]", " ").trim(); + return sanitized.length() > MAX_NAME_LENGTH + ? sanitized.substring(0, MAX_NAME_LENGTH).trim() + : sanitized; + } +} From 6810b35d6f4c63e925efcd635ec78ea6ff424a0c Mon Sep 17 00:00:00 2001 From: Christopher Uljasz Date: Tue, 1 Sep 2026 17:02:49 -0400 Subject: [PATCH 2/6] Cleanup duplicated constants --- app/src/main/cpp/winlator/fakeinput.cpp | 6 ++---- .../GuestProgramLauncherComponent.java | 4 ++-- .../runtime/display/winhandler/WinHandler.java | 4 +++- .../input/controls/GamepadIdentityStore.java | 16 +++++++++------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index 384f41501..f1a6463b7 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -381,8 +381,7 @@ read_gamepad_identity(int slot, GamepadIdentity *out) { return false; char path[PATH_MAX]; - snprintf(path, PATH_MAX, "%s/c13:%u", udev_data_dir, - FAKE_INPUT_EVENT_MINOR_BASE + slot); + snprintf(path, PATH_MAX, "%s/c13:%u", udev_data_dir, FAKE_INPUT_EVENT_MINOR_BASE + slot); int fd = my_open(path, O_RDONLY); if (fd < 0) @@ -538,8 +537,7 @@ open_fake_input_ring(const char *event, int flags) { if (fd < 0) return -1; - void *mapping = - mmap(nullptr, FAKE_INPUT_RING_SIZE, PROT_READ, MAP_SHARED, fd, 0); + void *mapping = mmap(nullptr, FAKE_INPUT_RING_SIZE, PROT_READ, MAP_SHARED, fd, 0); if (mapping == MAP_FAILED) { int saved_errno = errno; syscall(SYS_close, fd); diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 42465e4af..39be008cd 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -797,9 +797,7 @@ private void prepareFakeInputUdevMetadata(ImageFs imageFs, File devInputDir, Env String product = GamepadIdentityStore.formatId(GamepadIdentityStore.DEFAULT_PRODUCT_ID); for (int slot = 0; slot < numControllers; slot++) { - int eventMinor = 64 + slot; String name = GamepadIdentityStore.getDefaultName(slot); - File udevData = new File(udevDataDir, "c13:" + eventMinor); String symlink = "input/by-id/usb-WinNative_Generic_HID_Gamepad_" + slot + "-event-joystick"; String content = "I:" @@ -834,6 +832,8 @@ private void prepareFakeInputUdevMetadata(ImageFs imageFs, File devInputDir, Env + name + "\"\n" + "E:TAGS=:uaccess:\n"; + + File udevData = GamepadIdentityStore.getUdevDataFile(udevDataDir, slot); FileUtils.writeString(udevData, content); File eventNode = new File(devInputDir, "event" + slot); diff --git a/app/src/main/runtime/display/winhandler/WinHandler.java b/app/src/main/runtime/display/winhandler/WinHandler.java index 084f185b1..597e94e07 100644 --- a/app/src/main/runtime/display/winhandler/WinHandler.java +++ b/app/src/main/runtime/display/winhandler/WinHandler.java @@ -62,10 +62,10 @@ public class WinHandler { public static final byte FLAG_DINPUT_MAPPER_STANDARD = 1; public static final byte FLAG_DINPUT_MAPPER_XINPUT = 2; public static final byte INPUT_TYPE_MIXED = 2; + public static final int MAX_CONTROLLERS = 4; private static final int GAMEPAD_SOURCE_NONE = 0; private static final int GAMEPAD_SOURCE_VIRTUAL = 1; private static final int GAMEPAD_SOURCE_CONTROLLER = 2; - private static final int MAX_CONTROLLERS = 4; private static final int OSC_DEVICE_ID = -1; private static final short SERVER_PORT = 7947; private static final long VIRTUAL_REBALANCE_AFTER_PHYSICAL_DISCONNECT_MS = 200; @@ -1120,9 +1120,11 @@ private void releaseSlot(int deviceId) { + slot + " still used by sibling sub-device."); } + // Drop the unplugged pad's published identity, or fall back to the surviving // sibling sub-device's, so the guest stops seeing a pad that is gone. GamepadIdentityStore.refreshSlot(slot, resolveDeviceForSlot(slot)); + this.controllers.remove(deviceId); if (deviceId != OSC_DEVICE_ID) { if (!slotStillInUse) { diff --git a/app/src/main/runtime/input/controls/GamepadIdentityStore.java b/app/src/main/runtime/input/controls/GamepadIdentityStore.java index 80ffcc8f3..3340252bd 100644 --- a/app/src/main/runtime/input/controls/GamepadIdentityStore.java +++ b/app/src/main/runtime/input/controls/GamepadIdentityStore.java @@ -2,6 +2,7 @@ import android.util.Log; import android.view.InputDevice; +import com.winlator.cmod.runtime.display.winhandler.WinHandler; import com.winlator.cmod.shared.io.FileUtils; import java.io.File; import java.util.Locale; @@ -10,23 +11,20 @@ * Publishes the real identity (name, vendor ID, product ID) of the physical controller bound to * each fake input slot, for the "Force External Gamepad Identity" option. * - *

The slot's udev entry is the only place the identity lives: libudev-based enumeration in the - * guest already reads it, and the native ioctl hooks parse the same file, so the two can never - * disagree. Rewriting it as pads come and go is what makes hotplug work without relaunching. + * The slot's udev entry is the only place the identity lives: libudev-based enumeration in the + * guest already reads it, and the native ioctl hooks parse the same file. */ public final class GamepadIdentityStore { public static final int DEFAULT_VENDOR_ID = 0x045E; // Microsoft public static final int DEFAULT_PRODUCT_ID = 0x028E; // Xbox 360 Controller private static final String TAG = "GamepadIdentityStore"; - private static final int MAX_SLOTS = 4; + private static final int MAX_SLOTS = WinHandler.MAX_CONTROLLERS; private static final int EVENT_MINOR_BASE = 64; private static final int MAX_NAME_LENGTH = 80; private static File udevDataDir; - private GamepadIdentityStore() {} - public static String formatId(int id) { return String.format(Locale.US, "%04x", id & 0xFFFF); } @@ -35,6 +33,10 @@ public static String getDefaultName(int slot) { return "Xbox 360 Controller (" + slot + ")"; } + public static File getUdevDataFile(File udevDir, int slot) { + return new File(udevDir, "c13:" + (EVENT_MINOR_BASE + slot)); + } + /** Disables publishing and restores every slot to the default Xbox 360 spoof. */ public static synchronized void reset() { if (udevDataDir != null) { @@ -91,7 +93,7 @@ private static boolean writeIdentityLocked( if (udevDataDir == null) { return false; } - File udevData = new File(udevDataDir, "c13:" + (EVENT_MINOR_BASE + slot)); + File udevData = getUdevDataFile(slot); if (!udevData.isFile()) { return false; } From 0cb0e441a6bba409a8c53b5924a06987e0ec4556 Mon Sep 17 00:00:00 2001 From: Christopher Uljasz Date: Thu, 3 Sep 2026 14:48:27 -0400 Subject: [PATCH 3/6] Rework the setting to be per external gamepad instead of a global toggle, which makes more sense and hides it when not relevant --- app/src/main/cpp/winlator/fakeinput.cpp | 2 +- app/src/main/feature/library/GameSettings.kt | 63 ------------------- .../settings/input/InputControlsFragment.kt | 20 ++++++ .../settings/input/InputControlsScreen.kt | 49 +++++++++++++++ .../ShortcutSettingsComposeDialog.kt | 7 --- app/src/main/res/values/strings.xml | 8 +-- .../GuestProgramLauncherComponent.java | 2 + .../display/winhandler/WinHandler.java | 26 ++------ .../input/controls/ControlsProfile.java | 4 +- .../input/controls/ExternalController.java | 6 ++ .../input/controls/GamepadIdentityStore.java | 29 +++++---- 11 files changed, 103 insertions(+), 113 deletions(-) diff --git a/app/src/main/cpp/winlator/fakeinput.cpp b/app/src/main/cpp/winlator/fakeinput.cpp index f1a6463b7..3d3625bef 100644 --- a/app/src/main/cpp/winlator/fakeinput.cpp +++ b/app/src/main/cpp/winlator/fakeinput.cpp @@ -377,7 +377,7 @@ __attribute__((visibility("hidden"))) static bool read_gamepad_identity(int slot, GamepadIdentity *out) { if (!udev_data_dir || !*udev_data_dir || !my_open) return false; - if (slot < 0 || slot >= GAMEPAD_IDENTITY_MAX_SLOTS) + if (slot < 0 || slot >= static_cast(GAMEPAD_IDENTITY_MAX_SLOTS)) return false; char path[PATH_MAX]; diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index 9376c18eb..0e5aa9e1c 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -552,7 +552,6 @@ class GameSettingsStateHolder { val numControllersEntries = mutableStateOf>(emptyList()) val selectedNumControllers = mutableIntStateOf(0) val disableXInput = mutableStateOf(false) - val forceExternalGamepadIdentity = mutableStateOf(false) val simTouchScreen = mutableStateOf(false) val screenTouchMode = mutableIntStateOf(0) val gestureProfileEntries = mutableStateOf>(emptyList()) @@ -4871,68 +4870,6 @@ private fun InputSection(state: GameSettingsStateHolder) { Spacer(Modifier.height(4.dp)) - // Force External Gamepad Identity, reports the real controller's - // name/VID/PID instead of the spoofed Xbox 360 identity, for games with a - // dedicated non-Xbox (e.g. PlayStation) input path. - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Box(Modifier.weight(1f)) { - SettingCheckbox( - label = stringResource(R.string.shortcuts_properties_force_external_gamepad_identity), - checked = state.forceExternalGamepadIdentity.value, - onCheckedChange = { state.forceExternalGamepadIdentity.value = it } - ) - } - var showForceExternalGamepadIdentityHelp by remember { mutableStateOf(false) } - val forceExternalGamepadIdentityHelpOffset = rememberSmartDropdownOffset() - Box { - Box( - modifier = Modifier - .size(30.dp) - .clip(RoundedCornerShape(6.dp)) - .background(InputSurface) - .border(1.dp, InputBorder, RoundedCornerShape(6.dp)) - .paneNavItem( - cornerRadius = 6.dp, - onActivate = { showForceExternalGamepadIdentityHelp = !showForceExternalGamepadIdentityHelp }, - highlightColor = NavHighlight - ) - .smartDropdownAnchor(offset = forceExternalGamepadIdentityHelpOffset) { - showForceExternalGamepadIdentityHelp = !showForceExternalGamepadIdentityHelp - }, - contentAlignment = Alignment.Center - ) { - Icon( - Icons.AutoMirrored.Outlined.HelpOutline, - contentDescription = null, - tint = TextPrimary, - modifier = Modifier.size(18.dp) - ) - } - DropdownMenu( - expanded = showForceExternalGamepadIdentityHelp, - onDismissRequest = { showForceExternalGamepadIdentityHelp = false }, - offset = forceExternalGamepadIdentityHelpOffset.value, - shape = RoundedCornerShape(8.dp), - containerColor = CardSurface, - modifier = Modifier - .padding(10.dp) - .width(280.dp) - ) { - HtmlText( - stringResource(R.string.shortcuts_properties_help_force_external_gamepad_identity), - color = TextPrimary, - fontSize = SettingLabelSize, - lineHeight = 16.sp - ) - } - } - } - - Spacer(Modifier.height(4.dp)) - // Touch input mode (Trackpad / Touchscreen / Map to Right Stick) val gesturesOff = state.selectedGestureProfile.intValue == 0 val onSelectMode: (Int) -> Unit = { mode -> diff --git a/app/src/main/feature/settings/input/InputControlsFragment.kt b/app/src/main/feature/settings/input/InputControlsFragment.kt index 154f293f4..e2b502a51 100644 --- a/app/src/main/feature/settings/input/InputControlsFragment.kt +++ b/app/src/main/feature/settings/input/InputControlsFragment.kt @@ -36,6 +36,7 @@ import com.winlator.cmod.runtime.input.controls.ControlElement import com.winlator.cmod.runtime.input.controls.ControlsProfile import com.winlator.cmod.runtime.input.controls.ExternalController import com.winlator.cmod.runtime.input.controls.ExternalControllerBinding +import com.winlator.cmod.runtime.input.controls.GamepadIdentityStore import com.winlator.cmod.runtime.input.controls.GestureProfileManager import com.winlator.cmod.runtime.input.controls.InputControlsManager import com.winlator.cmod.runtime.input.ui.InputControlsView @@ -218,6 +219,7 @@ class InputControlsFragment : Fragment() { onExportProfile = ::exportProfile, onControllerExpandedToggle = ::toggleControllerExpanded, onRemoveController = ::removeController, + onReportRealIdentityChanged = ::setReportRealIdentity, onBindingTypeClick = ::showBindingTypePicker, onBindingValueClick = ::showBindingValuePicker, onRemoveBinding = ::removeBinding, @@ -357,12 +359,29 @@ class InputControlsFragment : Fragment() { } else { emptyList() }, + reportRealIdentity = controller.isUseRealIdentity, ) }, dialog = dialogState, ) } + // Per-pad opt-in to reporting the real name/VID/PID instead of the Xbox 360 spoof. + private fun setReportRealIdentity( + controllerId: String, + useRealIdentity: Boolean, + ) { + val controller = findVisibleController(controllerId) ?: return + controller.setUseRealIdentity(useRealIdentity) + currentProfile?.putController(controller) + lifecycleScope.launch(Dispatchers.IO) { + currentProfile?.save() + launch(Dispatchers.Main) { + publishUiState() + } + } + } + private fun buildBindingState( controller: ExternalController, bindingTypeEntries: Array, @@ -404,6 +423,7 @@ class InputControlsFragment : Fragment() { for (i in 0 until pController.controllerBindingCount) { liveMatch.addControllerBinding(pController.getControllerBindingAt(i)) } + liveMatch.setUseRealIdentity(pController.isUseRealIdentity()); visibleControllers.add(liveMatch) } else { visibleControllers.add(pController) diff --git a/app/src/main/feature/settings/input/InputControlsScreen.kt b/app/src/main/feature/settings/input/InputControlsScreen.kt index a25818a64..d9ae6466a 100644 --- a/app/src/main/feature/settings/input/InputControlsScreen.kt +++ b/app/src/main/feature/settings/input/InputControlsScreen.kt @@ -229,6 +229,7 @@ data class InputControllerCardState( val expanded: Boolean, val showBindings: Boolean, val bindings: List = emptyList(), + val reportRealIdentity: Boolean = false, ) data class InputControllerBindingState( @@ -286,6 +287,7 @@ data class InputControlsScreenActions( val onExportProfile: () -> Unit, val onControllerExpandedToggle: (String) -> Unit, val onRemoveController: (String) -> Unit, + val onReportRealIdentityChanged: (String, Boolean) -> Unit, val onBindingTypeClick: (String, Int) -> Unit, val onBindingValueClick: (String, Int) -> Unit, val onRemoveBinding: (String, Int) -> Unit, @@ -2688,6 +2690,53 @@ private fun ControllerCard( } } + // Only offer this for a pad that is actually attached: the identity comes from the + // live InputDevice, so there is nothing to report for a remembered-but-absent pad. + if (state.connected) { + Spacer(Modifier.height(InputCompactGap)) + Row( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(InputFieldCorner)) + .background(InputSubcard) + .border(1.dp, InputOutline, RoundedCornerShape(InputFieldCorner)) + .paneNavItem( + cornerRadius = InputFieldCorner, + onActivate = { + actions.onReportRealIdentityChanged( + state.controllerId, + !state.reportRealIdentity, + ) + }, + highlightColor = InputNavHighlight, + ).padding(horizontal = 8.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.input_controls_report_real_identity_title), + color = InputTextPrimary, + fontSize = InputPrimaryTextSize, + ) + Spacer(Modifier.height(1.dp)) + Text( + text = stringResource(R.string.input_controls_report_real_identity_summary), + color = InputTextSecondary, + fontSize = InputSecondaryTextSize, + lineHeight = 14.sp, + ) + } + Spacer(Modifier.width(InputCompactGap)) + AppSwitch( + checked = state.reportRealIdentity, + onCheckedChange = { enabled -> + actions.onReportRealIdentityChanged(state.controllerId, enabled) + }, + ) + } + } + if (state.showBindings) { Spacer(Modifier.height(InputCompactGap)) Subcard( diff --git a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt index 3ab17f31e..7cd29d128 100644 --- a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt +++ b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt @@ -386,8 +386,6 @@ class ShortcutSettingsComposeDialog private constructor( state.selectedDInputMapperType.intValue = if ((inputType and WinHandler.FLAG_DINPUT_MAPPER_STANDARD.toInt()) == WinHandler.FLAG_DINPUT_MAPPER_STANDARD.toInt()) 0 else 1 state.disableXInput.value = shortcut.getExtra("disableXinput", "0") == "1" - state.forceExternalGamepadIdentity.value = - shortcut.getExtra("forceExternalGamepadIdentity", "0") == "1" state.shortcutExclusiveXInput.value = shortcut.getExtra("exclusiveXInput", "").let { if (it.isEmpty()) container.isExclusiveXInput() else it == "1" } @@ -1254,11 +1252,6 @@ class ShortcutSettingsComposeDialog private constructor( shortcut.putExtra("disableXinput", disableXinputValue) if (disableXinputValue != null) hasContainerOverride = true - val forceExternalGamepadIdentityValue = - if (state.forceExternalGamepadIdentity.value) "1" else null - shortcut.putExtra("forceExternalGamepadIdentity", forceExternalGamepadIdentityValue) - if (forceExternalGamepadIdentityValue != null) hasContainerOverride = true - shortcut.putExtra("exclusiveXInput", if (state.shortcutExclusiveXInput.value) "1" else "0") if (state.shortcutExclusiveXInput.value != container.isExclusiveXInput()) hasContainerOverride = true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9410dc08a..714ea60f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -335,11 +335,6 @@ Disable Xinput (For Exclusive M/KB Control) Number of Controllers Exclusive Input - Force External Gamepad Identity - - Reports the real name, vendor ID and product ID of your connected physical controller to the game, instead of the spoofed <i><b>Xbox 360 Controller</b></i> identity.<br/><br/> - Enable this for games with a dedicated input path for other controller types, such as <i><b>PlayStation</b></i> gamepads. Leave this off unless your game fails to recognize your controller correctly. - Custom Game Settings Use Legacy Launcher Legacy Steam emulation: runs the game through the ColdClient launcher and strips SteamStub DRM from the exe. Use only for older games that don\'t work with the Steam Launcher. @@ -1104,6 +1099,9 @@ E.g. META for META key, \n Auto-hide on Controller Hide touchscreen controls when a controller is connected + Report Real Identity + Send this device\'s own name and IDs to games instead of the Xbox 360 spoof. + Gyroscope Calibrate Gyroscope diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 39be008cd..f759ad2ac 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -789,6 +789,8 @@ private void prepareFakeInputUdevMetadata(ImageFs imageFs, File devInputDir, Env envVars.put("FAKE_UDEV_DATA_DIR", udevDataDir.getAbsolutePath()); + GamepadIdentityStore.configure(udevDataDir); + File byIdDir = new File(devInputDir, "by-id"); if (!byIdDir.exists()) byIdDir.mkdirs(); diff --git a/app/src/main/runtime/display/winhandler/WinHandler.java b/app/src/main/runtime/display/winhandler/WinHandler.java index 597e94e07..4487e2361 100644 --- a/app/src/main/runtime/display/winhandler/WinHandler.java +++ b/app/src/main/runtime/display/winhandler/WinHandler.java @@ -839,24 +839,6 @@ private void ensureWriterForSlot(int slot) { this.writers[slot] = new FakeInputWriter(this.fakeInputBasePath, slot); this.writers[slot].open(); } - // Republish even when the writer already existed: the slot may have just changed - // hands (virtual pad -> physical pad, or one pad for another). - GamepadIdentityStore.refreshSlot(slot, resolveDeviceForSlot(slot)); - } - - // The physical pad currently bound to a fake input slot, for the per-game "Force - // External Gamepad Identity" option. The virtual on-screen pad has no real identity, - // so it leaves the slot on the default Xbox 360 spoof. - private InputDevice resolveDeviceForSlot(int slot) { - for (Map.Entry entry : this.deviceToSlot.entrySet()) { - if (entry.getKey() != OSC_DEVICE_ID && entry.getValue() == slot) { - InputDevice device = InputDevice.getDevice(entry.getKey()); - if (device != null) { - return device; - } - } - } - return null; } private boolean isPhysicalSlotOccupied(int slot) { @@ -905,6 +887,9 @@ private void bindDeviceToSlot(int deviceId, String descriptor, int slot) { this.deviceToDescriptor.put(deviceId, descriptor); } ensureWriterForSlot(slot); + // Republish even when the writer already existed: the slot may have just changed + // hands (virtual pad -> physical pad, or one pad for another). + GamepadIdentityStore.refreshSlot(slot, getController(deviceId)); } private boolean moveVirtualGamepadToSlot(int targetSlot, boolean releaseVacatedSlot) { @@ -1110,6 +1095,7 @@ private void releaseSlot(int deviceId) { this.writers[slot] = null; } this.usedSlots.remove(slot); + GamepadIdentityStore.refreshSlot(slot, null); Log.d("WinHandler", "Device " + deviceId + " disconnected. Slot " + slot + " released."); } else { Log.d( @@ -1121,10 +1107,6 @@ private void releaseSlot(int deviceId) { + " still used by sibling sub-device."); } - // Drop the unplugged pad's published identity, or fall back to the surviving - // sibling sub-device's, so the guest stops seeing a pad that is gone. - GamepadIdentityStore.refreshSlot(slot, resolveDeviceForSlot(slot)); - this.controllers.remove(deviceId); if (deviceId != OSC_DEVICE_ID) { if (!slotStillInUse) { diff --git a/app/src/main/runtime/input/controls/ControlsProfile.java b/app/src/main/runtime/input/controls/ControlsProfile.java index e25119de9..e623ae11b 100644 --- a/app/src/main/runtime/input/controls/ControlsProfile.java +++ b/app/src/main/runtime/input/controls/ControlsProfile.java @@ -246,9 +246,9 @@ public ArrayList loadControllers() { controller.setContext(context); controller.setId(id); controller.setName(controllerJSONObject.getString("name")); + controller.setUseRealIdentity(controllerJSONObject.optBoolean("useRealIdentity")); - JSONArray controllerBindingsJSONArray = - controllerJSONObject.getJSONArray("controllerBindings"); + JSONArray controllerBindingsJSONArray = controllerJSONObject.getJSONArray("controllerBindings"); for (int j = 0; j < controllerBindingsJSONArray.length(); j++) { JSONObject controllerBindingJSONObject = controllerBindingsJSONArray.getJSONObject(j); ExternalControllerBinding controllerBinding = new ExternalControllerBinding(); diff --git a/app/src/main/runtime/input/controls/ExternalController.java b/app/src/main/runtime/input/controls/ExternalController.java index 3dd54a9c6..64e814ec1 100644 --- a/app/src/main/runtime/input/controls/ExternalController.java +++ b/app/src/main/runtime/input/controls/ExternalController.java @@ -41,6 +41,7 @@ public class ExternalController { private String id; private String name; private int deviceId = -1; + private boolean useRealIdentity = false; private byte triggerType = TRIGGER_IS_AXIS; // Device exposes an analog trigger axis; gates the analog path so a stale "as button" // pref can't kill an analog pad's triggers. Default true = historical behavior. @@ -190,6 +191,10 @@ public void setTriggerType(byte mode) { this.triggerType = mode; } + public boolean isUseRealIdentity() { return useRealIdentity; } + + public void setUseRealIdentity(boolean useRealIdentity) { this.useRealIdentity = useRealIdentity; } + public void setContext(Context context) { this.context = context; if (context != null) { @@ -292,6 +297,7 @@ public JSONObject toJSONObject() throws JSONException { JSONObject controllerJSONObject = new JSONObject(); controllerJSONObject.put("id", this.id); controllerJSONObject.put("name", this.name); + controllerJSONObject.put("useRealIdentity", this.useRealIdentity); JSONArray controllerBindingsJSONArray = new JSONArray(); Iterator it = this.controllerBindings.iterator(); while (it.hasNext()) { diff --git a/app/src/main/runtime/input/controls/GamepadIdentityStore.java b/app/src/main/runtime/input/controls/GamepadIdentityStore.java index 3340252bd..649d2bcec 100644 --- a/app/src/main/runtime/input/controls/GamepadIdentityStore.java +++ b/app/src/main/runtime/input/controls/GamepadIdentityStore.java @@ -1,19 +1,17 @@ package com.winlator.cmod.runtime.input.controls; +import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import android.view.InputDevice; +import androidx.preference.PreferenceManager; import com.winlator.cmod.runtime.display.winhandler.WinHandler; import com.winlator.cmod.shared.io.FileUtils; import java.io.File; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; -/** - * Publishes the real identity (name, vendor ID, product ID) of the physical controller bound to - * each fake input slot, for the "Force External Gamepad Identity" option. - * - * The slot's udev entry is the only place the identity lives: libudev-based enumeration in the - * guest already reads it, and the native ioctl hooks parse the same file. - */ public final class GamepadIdentityStore { public static final int DEFAULT_VENDOR_ID = 0x045E; // Microsoft public static final int DEFAULT_PRODUCT_ID = 0x028E; // Xbox 360 Controller @@ -37,7 +35,10 @@ public static File getUdevDataFile(File udevDir, int slot) { return new File(udevDir, "c13:" + (EVENT_MINOR_BASE + slot)); } - /** Disables publishing and restores every slot to the default Xbox 360 spoof. */ + public static synchronized void configure(File udevDir) { + udevDataDir = udevDir; + } + public static synchronized void reset() { if (udevDataDir != null) { for (int slot = 0; slot < MAX_SLOTS; slot++) { @@ -47,20 +48,22 @@ public static synchronized void reset() { udevDataDir = null; } - /** Re-publishes the identity for one slot; call whenever its bound device may have changed. */ - public static synchronized void refreshSlot(int slot, InputDevice slotDevice) { + public static synchronized void refreshSlot(int slot, ExternalController slotDevice) { if (udevDataDir == null) { return; } refreshSlotLocked(slot, slotDevice); } - private static void refreshSlotLocked(int slot, InputDevice device) { - if (device == null) { + private static void refreshSlotLocked(int slot, ExternalController controller) { + // No pad in the slot (or the virtual on-screen one), or a pad the user did not opt in: + // either way the slot keeps the Xbox 360 identity every game already recognizes. + if (controller == null || !controller.isUseRealIdentity()) { clearSlotLocked(slot); return; } + InputDevice device = InputDevice.getDevice(controller.getDeviceId()); String name = sanitizeName(device.getName()); String vendor = formatId(device.getVendorId()); String product = formatId(device.getProductId()); @@ -93,7 +96,7 @@ private static boolean writeIdentityLocked( if (udevDataDir == null) { return false; } - File udevData = getUdevDataFile(slot); + File udevData = getUdevDataFile(udevDataDir, slot); if (!udevData.isFile()) { return false; } From 489d1f4fd3916b7585e5884b3cb099bedf43d5c1 Mon Sep 17 00:00:00 2001 From: Christopher Uljasz Date: Thu, 3 Sep 2026 16:11:28 -0400 Subject: [PATCH 4/6] Added settings translations --- app/src/main/res/values-b+es+419/strings.xml | 3 +++ app/src/main/res/values-da/strings.xml | 4 ++++ app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-es/strings.xml | 4 ++++ app/src/main/res/values-fi/strings.xml | 3 +++ app/src/main/res/values-fr/strings.xml | 4 ++++ app/src/main/res/values-hi/strings.xml | 4 ++++ app/src/main/res/values-it/strings.xml | 4 ++++ app/src/main/res/values-ja/strings.xml | 3 +++ app/src/main/res/values-ko/strings.xml | 4 ++++ app/src/main/res/values-no/strings.xml | 3 +++ app/src/main/res/values-pl/strings.xml | 4 ++++ app/src/main/res/values-pt-rBR/strings.xml | 4 ++++ app/src/main/res/values-pt/strings.xml | 3 +++ app/src/main/res/values-ro/strings.xml | 4 ++++ app/src/main/res/values-ru/strings.xml | 4 ++++ app/src/main/res/values-sv/strings.xml | 3 +++ app/src/main/res/values-th/strings.xml | 3 +++ app/src/main/res/values-tr/strings.xml | 3 +++ app/src/main/res/values-uk/strings.xml | 4 ++++ app/src/main/res/values-zh-rCN/strings.xml | 4 ++++ app/src/main/res/values-zh-rTW/strings.xml | 4 ++++ app/src/main/res/values/strings.xml | 4 ++-- 23 files changed, 82 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 05ef0a4c4..17bd27947 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -1023,6 +1023,9 @@ Por ejemplo, META para la tecla META, \n Ocultar automáticamente con el control Oculta los controles táctiles cuando hay un control conectado + Usar identidad real + Envía a los juegos el nombre y los ID propios de este dispositivo en lugar de simular un control Xbox 360. + Giroscopio Calibrar giroscopio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 326546a8e..e05efb911 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -738,6 +738,10 @@ F.eks. META for META-tast, \n Berøringskontroller Skjul automatisk ved controller Skjul skærmkontroller, når en controller er tilsluttet + + Brug rigtig identitet + Send denne enheds eget navn og ID\'er til spil i stedet for at efterligne en Xbox 360-controller. + Gyroskop Kalibrer gyroskop Aktiver gyroskopisk bevægelse for højre stick diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index efa2cdb23..28b6c35c8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -738,6 +738,10 @@ Z. B. META für Meta-Taste, \n Touchscreen-Steuerung Bei Controller automatisch ausblenden Touchscreen-Steuerung ausblenden, wenn ein Controller verbunden ist + + Echte Identität verwenden + Den eigenen Namen und die IDs dieses Geräts an Spiele senden, anstatt einen Xbox 360-Controller zu simulieren. + Gyroskop Gyroskop kalibrieren Gyroskopische Bewegung des rechten Sticks aktivieren diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index fd8f989d7..deba56d6c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -738,6 +738,10 @@ Ej. META para la tecla META, \n Controles táctiles Ocultar automáticamente con mando Oculta los controles táctiles cuando se conecta un mando + + Usar identidad real + Envía a los juegos el nombre y los ID propios de este dispositivo en lugar de simular un mando de Xbox 360. + Giroscopio Calibrar giroscopio Activar movimiento giroscópico del joystick derecho diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 1d538c72f..ee2111957 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1023,6 +1023,9 @@ E.g. META for META-näppäin, \n Piilota automaattisesti ohjaimella Piilota kosketusnäytön ohjaimet, kun ohjain on liitetty + Käytä todellista tunnistetta + Lähetä peleille tämän laitteen oma nimi ja tunnisteet sen sijaan, että se esiintyisi Xbox 360 -ohjaimena. + Gyroskooppi Kalibroi gyroskooppi diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4ec6afe3b..dd6a77d78 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -738,6 +738,10 @@ Par ex. META pour la touche META, \n Commandes tactiles Masquer automatiquement avec une manette Masquer les commandes tactiles lorsqu\'une manette est connectée + + Utiliser l\'identité réelle + Envoyer aux jeux le nom et les identifiants propres à cet appareil au lieu de simuler une manette Xbox 360. + Gyroscope Calibrer le gyroscope Activer le mouvement gyroscopique du stick droit diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index dd517f0db..07c047e03 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -850,6 +850,10 @@ टचस्क्रीन नियंत्रण कंट्रोलर पर अपने आप छिपाएँ कंट्रोलर कनेक्ट होने पर टचस्क्रीन नियंत्रण छिपाएँ + + वास्तविक पहचान का उपयोग करें + Xbox 360 कंट्रोलर की नकल करने के बजाय इस डिवाइस का अपना नाम और आईडी गेम को भेजें। + जाइरोस्कोप जाइरोस्कोप कैलिब्रेट करें राइट स्टिक जाइरोस्कोपिक मोशन सक्षम करें diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4ce2ae886..c95b87909 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -738,6 +738,10 @@ Ad es. META per il tasto META, \n Controlli touchscreen Nascondi automaticamente con controller Nascondi i controlli touchscreen quando è collegato un controller + + Usa identità reale + Invia ai giochi il nome e gli ID di questo dispositivo invece di simulare un controller Xbox 360. + Giroscopio Calibra giroscopio Abilita movimento giroscopico sulla levetta destra diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8474bb0e9..a7504dedb 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1023,6 +1023,9 @@ コントローラー接続時に自動的に隠す コントローラーが接続されているときにタッチスクリーンコントロールを隠す + 実際のIDを使用 + Xbox 360 コントローラーを偽装する代わりに、このデバイス本来の名前とIDをゲームに送信します。 + ジャイロスコープ ジャイロスコープをキャリブレーション diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 580587d57..0a0ec7bea 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -738,6 +738,10 @@ 터치스크린 컨트롤 컨트롤러 연결 시 자동 숨김 컨트롤러가 연결되면 터치스크린 컨트롤 숨기기 + + 실제 ID 사용 + Xbox 360 컨트롤러로 위장하는 대신 이 기기의 실제 이름과 ID를 게임에 전달합니다. + 자이로스코프 자이로스코프 보정 오른쪽 스틱 자이로스코프 모션 활성화 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 180f9f9cb..82bd6efbc 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -1023,6 +1023,9 @@ F.eks. META for META-tast, \n Skjul automatisk med kontroller Skjul berøringsskjermkontroller når en kontroller er tilkoblet + Bruk faktisk identitet + Send denne enhetens eget navn og ID-er til spill i stedet for å etterligne en Xbox 360-kontroller. + Gyroskop Kalibrer gyroskop diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 28543e5d6..4c614d4f6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -744,6 +744,10 @@ Np. META dla klawisza META, \n Sterowanie dotykowe Ukryj automatycznie przy kontrolerze Ukryj sterowanie dotykowe, gdy podłączony jest kontroler + + Użyj prawdziwej tożsamości + Przekazuj grom własną nazwę i identyfikatory tego urządzenia zamiast podszywać się pod kontroler Xbox 360. + Żyroskop Kalibruj żyroskop Włącz ruch żyroskopowy prawej gałki diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0d809623a..3c8e9355d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -738,6 +738,10 @@ Ex. META para tecla META, \n Controles de toque Ocultar automaticamente com controle Ocultar os controles de toque quando um controle estiver conectado + + Usar identidade real + Enviar aos jogos o nome e os IDs próprios deste dispositivo em vez de simular um controle Xbox 360. + Giroscopio Calibrar Giroscopio Ativar Movimento Giroscopico do Analogico Direito diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 249768561..a70fa6883 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1023,6 +1023,9 @@ Por ex. META para tecla META, \n Ocultar automaticamente com comando Ocultar os controlos de ecrã tátil quando um comando está ligado + Usar identidade real + Enviar aos jogos o nome e os IDs próprios deste dispositivo em vez de simular um comando Xbox 360. + Giroscópio Calibrar giroscópio diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 096d26e29..4e4ec86f4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -738,6 +738,10 @@ De ex. META pentru tasta META, \n Comenzi tactile Ascunde automat la controler Ascunde comenzile tactile când este conectat un controler + + Folosește identitatea reală + Trimite jocurilor numele și ID-urile proprii ale acestui dispozitiv în loc să simuleze un controler Xbox 360. + Giroscop Calibreaza giroscopul Activeaza miscarea giroscopica pe stick-ul drept diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index eb1001396..3d2083eb6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -808,6 +808,10 @@ Сенсорное управление Автоскрытие при контроллере Скрывать сенсорное управление при подключении контроллера + + Использовать настоящие данные устройства + Передавать играм собственное имя и идентификаторы этого устройства вместо имитации контроллера Xbox 360. + Гироскоп Калибровка гироскопа Включить управление правым стиком через гироскоп diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 399bec9f3..b09ebae1f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1023,6 +1023,9 @@ T.ex. META för META-tangent, \n Dölj automatiskt med handkontroll Dölj pekskärmskontroller när en handkontroll är ansluten + Använd verklig identitet + Skicka den här enhetens eget namn och ID:n till spel i stället för att imitera en Xbox 360-handkontroll. + Gyroskop Kalibrera gyroskop diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index d7ca6999f..78d63ea5e 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1023,6 +1023,9 @@ ซ่อนอัตโนมัติเมื่อมีคอนโทรลเลอร์ ซ่อนปุ่มควบคุมบนหน้าจอสัมผัสเมื่อเชื่อมต่อคอนโทรลเลอร์ + ใช้ข้อมูลระบุตัวตนจริง + ส่งชื่อและ ID ของอุปกรณ์นี้ไปยังเกมแทนการปลอมเป็นคอนโทรลเลอร์ Xbox 360 + ไจโรสโคป ปรับเทียบไจโรสโคป diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b83379d67..d0d750493 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1023,6 +1023,9 @@ E.g. META için META tuşu, \n Kumandada Otomatik Gizle Bir kumanda bağlıyken dokunmatik ekran denetimlerini gizle + Gerçek kimliği kullan + Xbox 360 kumandası gibi görünmek yerine bu cihazın kendi adını ve kimliklerini oyunlara gönder. + Jiroskop Jiroskopu Kalibre Et diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2ae3a8812..afd64210e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -744,6 +744,10 @@ Сенсорне керування Автоприховування з контролером Приховувати сенсорне керування, коли під\'єднано контролер + + Використовувати справжні дані пристрою + Надсилати іграм власну назву та ідентифікатори цього пристрою замість імітації контролера Xbox 360. + Гіроскоп Калібрувати гіроскоп Увімкнути гіроскопічний рух правого стіка diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f1427cf0f..4bf3192ff 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -738,6 +738,10 @@ 触摸屏控制 连接手柄时自动隐藏 连接手柄时隐藏触摸屏控制 + + 使用真实身份 + 向游戏发送本设备自身的名称和 ID,而不是伪装成 Xbox 360 手柄。 + 陀螺仪 校准陀螺仪 启用右摇杆陀螺仪运动 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8cd3dbbe2..40609c909 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -738,6 +738,10 @@ 觸控螢幕控制 連接控制器時自動隱藏 連接控制器時隱藏觸控螢幕控制 + + 使用真實身分 + 向遊戲傳送本裝置自身的名稱和 ID,而不是偽裝成 Xbox 360 控制器。 + 陀螺儀 校準陀螺儀 啟用右搖桿陀螺儀動態感應 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 714ea60f8..d23ba9b01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1099,8 +1099,8 @@ E.g. META for META key, \n Auto-hide on Controller Hide touchscreen controls when a controller is connected - Report Real Identity - Send this device\'s own name and IDs to games instead of the Xbox 360 spoof. + Use Real Identity + Send this device\'s own name and IDs to games instead of spoofing an Xbox 360 controller. Gyroscope From 105be3a64c4daa55d5cad32d63b53be307a24629 Mon Sep 17 00:00:00 2001 From: Christopher Uljasz Date: Thu, 3 Sep 2026 18:40:30 -0400 Subject: [PATCH 5/6] Fix the whole mess of saving the setting through a profile and instead use the pref manager --- .../settings/input/InputControlsFragment.kt | 6 +++-- .../input/controls/ControlsProfile.java | 4 ++-- .../input/controls/ExternalController.java | 24 ++++++++++++++----- .../input/controls/PreferenceKeys.java | 1 + 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/feature/settings/input/InputControlsFragment.kt b/app/src/main/feature/settings/input/InputControlsFragment.kt index e2b502a51..9dfb7e701 100644 --- a/app/src/main/feature/settings/input/InputControlsFragment.kt +++ b/app/src/main/feature/settings/input/InputControlsFragment.kt @@ -372,10 +372,10 @@ class InputControlsFragment : Fragment() { useRealIdentity: Boolean, ) { val controller = findVisibleController(controllerId) ?: return + val activity = activity ?: return controller.setUseRealIdentity(useRealIdentity) - currentProfile?.putController(controller) + controller.savePreferences(activity) lifecycleScope.launch(Dispatchers.IO) { - currentProfile?.save() launch(Dispatchers.Main) { publishUiState() } @@ -439,6 +439,8 @@ class InputControlsFragment : Fragment() { visibleControllers.addAll(ExternalController.getControllers()) } + visibleControllers.forEach { controller -> controller.loadPreferences(activity) } + activeBindingController = activeId?.let { id -> visibleControllers.firstOrNull { it.id == id } diff --git a/app/src/main/runtime/input/controls/ControlsProfile.java b/app/src/main/runtime/input/controls/ControlsProfile.java index e623ae11b..e25119de9 100644 --- a/app/src/main/runtime/input/controls/ControlsProfile.java +++ b/app/src/main/runtime/input/controls/ControlsProfile.java @@ -246,9 +246,9 @@ public ArrayList loadControllers() { controller.setContext(context); controller.setId(id); controller.setName(controllerJSONObject.getString("name")); - controller.setUseRealIdentity(controllerJSONObject.optBoolean("useRealIdentity")); - JSONArray controllerBindingsJSONArray = controllerJSONObject.getJSONArray("controllerBindings"); + JSONArray controllerBindingsJSONArray = + controllerJSONObject.getJSONArray("controllerBindings"); for (int j = 0; j < controllerBindingsJSONArray.length(); j++) { JSONObject controllerBindingJSONObject = controllerBindingsJSONArray.getJSONObject(j); ExternalControllerBinding controllerBinding = new ExternalControllerBinding(); diff --git a/app/src/main/runtime/input/controls/ExternalController.java b/app/src/main/runtime/input/controls/ExternalController.java index 64e814ec1..01840c171 100644 --- a/app/src/main/runtime/input/controls/ExternalController.java +++ b/app/src/main/runtime/input/controls/ExternalController.java @@ -129,10 +129,24 @@ private static float getFloatPref(SharedPreferences prefs, String key, float def } } - private void loadPreferences() { - if (context == null) return; + public void savePreferences(Context context) { + if (context != null) { + // Save input device specific preferences (for now just the device id) + SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); + prefs.putBoolean(PreferenceKeys.USE_REAL_IDENTITY + this.id, this.useRealIdentity); + prefs.apply(); + } + } + public void loadPreferences(Context context) { + if (context == null) { + return; + } + // Load the device specific preferences (such as real device ids, etc.) SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + this.useRealIdentity = prefs.getBoolean(PreferenceKeys.USE_REAL_IDENTITY + this.id, false); + + // These don't seem to be used (yet?) this.deadzoneLeft = getFloatPref(prefs, PreferenceKeys.DEADZONE_LEFT, 0.1f); this.deadzoneRight = getFloatPref(prefs, PreferenceKeys.DEADZONE_RIGHT, 0.1f); this.sensitivityLeft = getFloatPref(prefs, PreferenceKeys.SENSITIVITY_LEFT, 1.0f); @@ -145,8 +159,7 @@ private void loadPreferences() { // Honor an explicit choice; else auto-detect. Runs per instance via setContext. this.hasAnalogTriggerAxis = deviceHasAnalogTriggerAxis(); int triggerTypePref = prefs.getInt(PreferenceKeys.TRIGGER_TYPE, TRIGGER_TYPE_UNSET); - this.triggerType = - triggerTypePref == TRIGGER_TYPE_UNSET ? autoTriggerType() : (byte) triggerTypePref; + this.triggerType = triggerTypePref == TRIGGER_TYPE_UNSET ? autoTriggerType() : (byte) triggerTypePref; } /** Default trigger mode (no explicit choice): derived from capability. */ @@ -198,7 +211,7 @@ public void setTriggerType(byte mode) { public void setContext(Context context) { this.context = context; if (context != null) { - loadPreferences(); + loadPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.registerOnSharedPreferenceChangeListener(prefChangeListener); } @@ -297,7 +310,6 @@ public JSONObject toJSONObject() throws JSONException { JSONObject controllerJSONObject = new JSONObject(); controllerJSONObject.put("id", this.id); controllerJSONObject.put("name", this.name); - controllerJSONObject.put("useRealIdentity", this.useRealIdentity); JSONArray controllerBindingsJSONArray = new JSONArray(); Iterator it = this.controllerBindings.iterator(); while (it.hasNext()) { diff --git a/app/src/main/runtime/input/controls/PreferenceKeys.java b/app/src/main/runtime/input/controls/PreferenceKeys.java index a28eaf7c2..30d219127 100644 --- a/app/src/main/runtime/input/controls/PreferenceKeys.java +++ b/app/src/main/runtime/input/controls/PreferenceKeys.java @@ -11,4 +11,5 @@ public class PreferenceKeys { public static final String INVERT_RIGHT_Y = "invert_right_y"; public static final String SQUARE_DEADZONE_LEFT = "square_deadzone_left"; public static final String TRIGGER_TYPE = "trigger_type"; + public static final String USE_REAL_IDENTITY = "use_real_id"; } From ac42ea7602da58e129967c11d2d3ef10fdb2aaba Mon Sep 17 00:00:00 2001 From: Christopher ULJASZ Date: Mon, 7 Sep 2026 19:23:34 -0400 Subject: [PATCH 6/6] Added a controller mapping hack for the AYN Thor where the device and product id is for some reason overwritten by the device mapper --- .../GuestProgramLauncherComponent.java | 43 +-------- .../display/winhandler/WinHandler.java | 1 + .../input/controls/GamepadIdentityStore.java | 90 +++++++++++++++---- 3 files changed, 74 insertions(+), 60 deletions(-) diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 7c344962b..452a52068 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -796,54 +796,13 @@ private void prepareFakeInputUdevMetadata(ImageFs imageFs, File devInputDir, Env envVars.put("FAKE_UDEV_DATA_DIR", udevDataDir.getAbsolutePath()); - GamepadIdentityStore.configure(udevDataDir); - File byIdDir = new File(devInputDir, "by-id"); if (!byIdDir.exists()) byIdDir.mkdirs(); int numControllers = getConfiguredControllerCount(); - String vendor = GamepadIdentityStore.formatId(GamepadIdentityStore.DEFAULT_VENDOR_ID); - String product = GamepadIdentityStore.formatId(GamepadIdentityStore.DEFAULT_PRODUCT_ID); for (int slot = 0; slot < numControllers; slot++) { - String name = GamepadIdentityStore.getDefaultName(slot); - String symlink = "input/by-id/usb-WinNative_Generic_HID_Gamepad_" + slot + "-event-joystick"; - String content = - "I:" - + slot - + "\n" - + "N:input/event" - + slot - + "\n" - + "S:" - + symlink - + "\n" - + "E:DEVNAME=/dev/input/event" - + slot - + "\n" - + "E:ID_INPUT=1\n" - + "E:ID_INPUT_JOYSTICK=1\n" - + "E:ID_BUS=usb\n" - + "E:ID_VENDOR=WinNative\n" - + "E:ID_VENDOR_ID=" - + vendor - + "\n" - + "E:ID_MODEL=Generic_HID_Gamepad_" - + slot - + "\n" - + "E:ID_MODEL_ID=" - + product - + "\n" - + "E:ID_SERIAL=WinNative_Generic_HID_Gamepad_" - + slot - + "\n" - + "E:NAME=\"" - + name - + "\"\n" - + "E:TAGS=:uaccess:\n"; - - File udevData = GamepadIdentityStore.getUdevDataFile(udevDataDir, slot); - FileUtils.writeString(udevData, content); + GamepadIdentityStore.configureSlot(slot, udevDataDir); File eventNode = new File(devInputDir, "event" + slot); if (!eventNode.exists()) { diff --git a/app/src/main/runtime/display/winhandler/WinHandler.java b/app/src/main/runtime/display/winhandler/WinHandler.java index 4487e2361..c994536bc 100644 --- a/app/src/main/runtime/display/winhandler/WinHandler.java +++ b/app/src/main/runtime/display/winhandler/WinHandler.java @@ -209,6 +209,7 @@ public int preAssignConnectedControllers() { } } + GamepadIdentityStore.configurePreAssignedControllers(this.controllers, this.deviceToSlot); Log.d("WinHandler", "Pre-assigned " + assignedCount + " controller(s) before Wine startup."); return assignedCount; } diff --git a/app/src/main/runtime/input/controls/GamepadIdentityStore.java b/app/src/main/runtime/input/controls/GamepadIdentityStore.java index 649d2bcec..45d191d96 100644 --- a/app/src/main/runtime/input/controls/GamepadIdentityStore.java +++ b/app/src/main/runtime/input/controls/GamepadIdentityStore.java @@ -8,13 +8,17 @@ import com.winlator.cmod.runtime.display.winhandler.WinHandler; import com.winlator.cmod.shared.io.FileUtils; import java.io.File; +import java.util.HashMap; import java.util.HashSet; import java.util.Locale; +import java.util.Map; import java.util.Set; public final class GamepadIdentityStore { public static final int DEFAULT_VENDOR_ID = 0x045E; // Microsoft public static final int DEFAULT_PRODUCT_ID = 0x028E; // Xbox 360 Controller + public static final int SONY_VENDOR_ID = 0x054C; // Sony + public static final int DS4_PRODUCT_ID = 0x05C4; // Dualshock 4 private static final String TAG = "GamepadIdentityStore"; private static final int MAX_SLOTS = WinHandler.MAX_CONTROLLERS; @@ -22,6 +26,8 @@ public final class GamepadIdentityStore { private static final int MAX_NAME_LENGTH = 80; private static File udevDataDir; + private static Map controllers; + private static Map deviceToSlot; public static String formatId(int id) { return String.format(Locale.US, "%04x", id & 0xFFFF); @@ -35,8 +41,68 @@ public static File getUdevDataFile(File udevDir, int slot) { return new File(udevDir, "c13:" + (EVENT_MINOR_BASE + slot)); } - public static synchronized void configure(File udevDir) { + public static void configurePreAssignedControllers(Map whandlerControllers, Map whandlerDeviceToSlot) { + controllers = whandlerControllers; + deviceToSlot = whandlerDeviceToSlot; + } + + public static synchronized void configureSlot(int slot, File udevDir) { udevDataDir = udevDir; + if (udevDataDir != null) { + String vendor = formatId(DEFAULT_VENDOR_ID); + String product = formatId(DEFAULT_PRODUCT_ID); + String name = getDefaultName(slot); + + // Find a device id bound to this slot if any + for (Map.Entry entry : deviceToSlot.entrySet()) { + if (entry.getValue() == slot) { + int deviceId = entry.getKey(); + ExternalController controller = controllers.getOrDefault(deviceId, null); + if (controller != null && controller.isUseRealIdentity()) + { + InputDevice device = InputDevice.getDevice(controller.getDeviceId()); + name = sanitizeName(device.getName()); + vendor = formatId(device.getVendorId()); + product = formatId(device.getProductId()); + + // AYN Thor specific hack, for some reason the Thor rewrites all vendor and product ids + // to 2020:0111, so use the device name to workaround that here + if (vendor.equals("2020") && product.equals("0111")) { + if (name.contains("XBOX") || name.contains("Xbox")) { // Matches Xbox One controllers and XBOX 360 + vendor = formatId(DEFAULT_VENDOR_ID); + product = formatId(DEFAULT_PRODUCT_ID); + } else if (name.contains("PLAYSTATION") || name.contains("DualShock")) { // Matches PLAYSTATION(R)3 Controller and DS4/5 + vendor = formatId(SONY_VENDOR_ID); + product = formatId(DS4_PRODUCT_ID); + } + } + + Log.d(TAG, "Published gamepad identity for slot " + + slot + ": " + name + " (" + vendor + ":" + product + ")"); + } + } + } + + String symlink = "input/by-id/usb-WinNative_Generic_HID_Gamepad_" + slot + "-event-joystick"; + String content = + "I:" + slot + "\n" + + "N:input/event" + slot + "\n" + + "S:" + symlink + "\n" + + "E:DEVNAME=/dev/input/event" + slot + "\n" + + "E:ID_INPUT=1\n" + + "E:ID_INPUT_JOYSTICK=1\n" + + "E:ID_BUS=usb\n" + + "E:ID_VENDOR=WinNative\n" + + "E:ID_VENDOR_ID=" + vendor + "\n" + + "E:ID_MODEL=Generic_HID_Gamepad_" + slot + "\n" + + "E:ID_MODEL_ID=" + product + "\n" + + "E:ID_SERIAL=WinNative_Generic_HID_Gamepad_" + slot + "\n" + + "E:NAME=\"" + name + "\"\n" + + "E:TAGS=:uaccess:\n"; + + File udevData = GamepadIdentityStore.getUdevDataFile(udevDataDir, slot); + FileUtils.writeString(udevData, content); + } } public static synchronized void reset() { @@ -46,6 +112,8 @@ public static synchronized void reset() { } } udevDataDir = null; + controllers = null; + deviceToSlot = null; } public static synchronized void refreshSlot(int slot, ExternalController slotDevice) { @@ -68,17 +136,8 @@ private static void refreshSlotLocked(int slot, ExternalController controller) { String vendor = formatId(device.getVendorId()); String product = formatId(device.getProductId()); if (writeIdentityLocked(slot, vendor, product, name)) { - Log.d( - TAG, - "Published gamepad identity for slot " - + slot - + ": " - + name - + " (" - + vendor - + ":" - + product - + ")"); + Log.d(TAG, "Published gamepad identity for slot " + + slot + ": " + name + " (" + vendor + ":" + product + ")"); } } @@ -87,12 +146,7 @@ private static void clearSlotLocked(int slot) { slot, formatId(DEFAULT_VENDOR_ID), formatId(DEFAULT_PRODUCT_ID), getDefaultName(slot)); } - /** - * Rewrites just the identity fields of the slot's udev entry, leaving the rest of it (discovery - * tags, device node, symlink) untouched. - */ - private static boolean writeIdentityLocked( - int slot, String vendor, String product, String name) { + private static boolean writeIdentityLocked(int slot, String vendor, String product, String name) { if (udevDataDir == null) { return false; }