Skip to content

Add global option to disable automatic controller switching#796

Open
adamrb wants to merge 4 commits intoisXander:multiversion/devfrom
adamrb:feature/auto-switch-controllers-toggle
Open

Add global option to disable automatic controller switching#796
adamrb wants to merge 4 commits intoisXander:multiversion/devfrom
adamrb:feature/auto-switch-controllers-toggle

Conversation

@adamrb
Copy link

@adamrb adamrb commented Feb 17, 2026

Summary

  • Adds an "Auto-Switch Controllers" toggle in Global Settings (default: enabled) that controls whether the active controller automatically switches when input is detected from a different controller
  • When disabled, a "Current Controller" dropdown appears allowing manual selection from connected controllers (including a "Keyboard & Mouse" option)
  • Persists the preferred controller UID in config — on game restart or controller hotplug, the preferred controller is automatically restored if available
  • Old configs are migrated via DataFixer (schema v2) to add the new fields with defaults

Motivation

The automatic controller switching makes it impossible to run multiple game instances side-by-side with different controllers, since pressing a button on controller B in one instance causes the other instance to also switch to controller B.

Implementation notes

  • The auto-switch guard is inside tickInactiveController() so inactive controllers still get ticked for state tracking
  • The preferred controller UID is only saved from the UI dropdown (explicit user action), not from fallback selections on disconnect, to avoid overwriting the preference
  • Controller list in the dropdown is built when the settings screen opens. If a controller is hotplugged while the screen is open, it requires closing and reopening to refresh (avoids memory leak from permanent event listeners)

Test plan

  • Open Global Settings and verify "Auto-Switch Controllers" toggle appears in Miscellaneous, defaulting to enabled
  • With toggle enabled: press a button on controller B while controller A is active — B should become active
  • With toggle disabled: press a button on controller B while controller A is active — A should remain active
  • With toggle disabled: verify "Current Controller" dropdown appears and lists all connected controllers
  • Verify preferred controller persists after restart (check config JSON for preferred_controller_uid)
  • Verify preferred controller is restored on hotplug reconnect
  • Verify old configs are migrated correctly (schema_version bumped to 2)

The automatic controller switching behavior (where the active controller
switches to whichever last provided input) makes it impossible to run
multiple game instances side-by-side with different controllers. This
adds a toggleable "Auto-Switch Controllers" option in global settings
(default: enabled) that gates the tickInactiveController() loop.

When disabled, a "Current Controller" dropdown appears allowing manual
controller selection. The dropdown dynamically refreshes on controller
hotplug events by rebuilding the settings screen.
When auto-switch is disabled, the manually selected controller is now
saved as preferredControllerUid in the config. On game startup or
controller hotplug, the preferred controller is automatically restored
if available. The preference is only updated from the UI dropdown to
avoid being overwritten by fallback selections on disconnect.
- Revert version bump in gradle.properties
- Move auto-switch guard inside tickInactiveController() so inactive
  controllers still get ticked
- Remove memory-leaking event listeners from GlobalSettingsScreenFactory
- Replace optionalFieldOf with proper DataFixer migration (schema v2)
  that adds auto_switch_controllers and preferred_controller_uid defaults
Comment on lines +32 to +37
if (global.get("auto_switch_controllers").result().isEmpty()) {
global = global.set("auto_switch_controllers", root.createBoolean(true));
}
if (global.get("preferred_controller_uid").result().isEmpty()) {
global = global.set("preferred_controller_uid", root.createString(""));
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single source of truth.

Allow this fix to take the GlobalSettings in its constructor, and refer to the defaults that way, rather than providing them again here.

if (selector != null) selector.setAvailable(!opt.pendingValue());
})
.build())
.option(Util.make(() -> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.option has a supplier overload. no need for Util.make here

.text(Component.translatable("controlify.gui.current_controller.tooltip"))
.build())
.binding(
"",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a comment or two for future reference explaining what an empty string represents'd be good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants