Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions configuration.c
Original file line number Diff line number Diff line change
Expand Up @@ -2631,6 +2631,18 @@ static struct config_uint_setting *populate_settings_uint(
SETTING_UINT("split_joycon_p8", &settings->uints.input_split_joycon[7], true, 0, false);
#endif

/* Co-pilot accessibility: assistant joypad index per player port.
* COPILOT_PORT_DISABLED (255) = feature off. Persisted unconditionally
* so users do not lose their accessibility configuration on restart. */
SETTING_UINT("input_copilot_port_p1", &settings->uints.input_copilot_port[0], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p2", &settings->uints.input_copilot_port[1], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p3", &settings->uints.input_copilot_port[2], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p4", &settings->uints.input_copilot_port[3], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p5", &settings->uints.input_copilot_port[4], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p6", &settings->uints.input_copilot_port[5], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p7", &settings->uints.input_copilot_port[6], true, COPILOT_PORT_DISABLED, false);
SETTING_UINT("input_copilot_port_p8", &settings->uints.input_copilot_port[7], true, COPILOT_PORT_DISABLED, false);

#ifdef HAVE_SCREENSHOTS
SETTING_UINT("notification_show_screenshot_duration", &settings->uints.notification_show_screenshot_duration, true, DEFAULT_NOTIFICATION_SHOW_SCREENSHOT_DURATION, false);
SETTING_UINT("notification_show_screenshot_flash", &settings->uints.notification_show_screenshot_flash, true, DEFAULT_NOTIFICATION_SHOW_SCREENSHOT_FLASH, false);
Expand Down Expand Up @@ -3148,6 +3160,8 @@ void config_set_defaults(void *data)
settings->uints.input_analog_dpad_mode[i] = ANALOG_DPAD_LSTICK;
input_config_set_device((unsigned)i, RETRO_DEVICE_JOYPAD);
settings->uints.input_mouse_index[i] = (unsigned)i;
/* Co-pilot disabled by default for every port */
settings->uints.input_copilot_port[i] = COPILOT_PORT_DISABLED;
}

custom_vp->width = 0;
Expand Down
8 changes: 8 additions & 0 deletions configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ enum settings_glob_flags
SETTINGS_FLG_SKIP_WINDOW_POSITIONS = (1 << 1)
};

/* Co-pilot: sentinel value meaning "no co-pilot joypad assigned".
* Must not collide with valid physical joypad indices (0..MAX_INPUT_DEVICES-1).
* MAX_INPUT_DEVICES is typically 16, so 255 is safe. */
#define COPILOT_PORT_DISABLED 255

typedef struct settings
{
struct
Expand Down Expand Up @@ -163,6 +168,9 @@ typedef struct settings

unsigned input_split_joycon[MAX_USERS];
unsigned input_joypad_index[MAX_USERS];
/* Co-pilot: index of the assistant joypad for each port.
* COPILOT_PORT_DISABLED (255) means the feature is off for that port. */
unsigned input_copilot_port[MAX_USERS];
unsigned input_device[MAX_USERS];
unsigned input_mouse_index[MAX_USERS];

Expand Down
109 changes: 109 additions & 0 deletions input/input_driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -2167,6 +2167,73 @@ static int16_t input_state_internal(
}
} /* kb_mapping_blocked scope */

/* ── CO-PILOT ACCESSIBILITY: merge assistant joypad input ──────────────
* If a co-pilot joypad is configured for this port, read its raw state
* and merge it into the result.
* - Digital buttons : logical OR (either pad can press a button)
* - Analog axes : keep the value with the largest magnitude
* (whichever pad is pushed further wins)
* The co-pilot uses its own joy_idx so its autoconf profile is respected.
* No remapping is applied to the co-pilot — it mirrors the main player. */
{
unsigned copilot_joy = settings->uints.input_copilot_port[port];
if ( copilot_joy != COPILOT_PORT_DISABLED
&& copilot_joy < MAX_INPUT_DEVICES)
{
rarch_joypad_info_t cp_info;
cp_info.joy_idx = copilot_joy;
cp_info.auto_binds = input_autoconf_binds[copilot_joy];
cp_info.axis_threshold = settings->floats.input_axis_threshold;

if (device == RETRO_DEVICE_ANALOG)
{
int16_t cp_result = 0;
if (sec_joypad)
cp_result = input_joypad_analog_axis(
ANALOG_DPAD_NONE,
input_analog_deadzone,
input_analog_sensitivity,
sec_joypad,
&cp_info,
idx, id,
(*input_st->libretro_input_binds[port]));

if (joypad && (cp_result == 0))
cp_result = input_joypad_analog_axis(
ANALOG_DPAD_NONE,
input_analog_deadzone,
input_analog_sensitivity,
joypad,
&cp_info,
idx, id,
(*input_st->libretro_input_binds[port]));

if (cp_result != 0)
{
int16_t cp_abs = (cp_result >= 0) ? cp_result : -cp_result;
int16_t result_abs = (result >= 0) ? result : -result;
if (cp_abs > result_abs)
result = cp_result;
}
}
else
{
/* Digital: OR the full button mask from the co-pilot joypad */
int16_t cp_result = input_state_wrap(
input_st->current_driver,
input_st->current_data,
joypad,
sec_joypad,
&cp_info,
(*input_st->libretro_input_binds),
(input_st->flags & INP_FLAG_KB_MAPPING_BLOCKED) ? true : false,
port, device, idx,
bitmask_enabled ? RETRO_DEVICE_ID_JOYPAD_MASK : id);
result |= cp_result;
}
}
}

return result;
}

Expand Down Expand Up @@ -7490,6 +7557,48 @@ void input_driver_collect_system_input(input_driver_state_t *input_st,
&joypad_info,
settings->bools.input_hotkey_device_merge);

/* ── CO-PILOT ACCESSIBILITY: hotkeys from the assistant joypad ─────
* The co-pilot may be a different controller model with a different
* autoconf profile (e.g. different button layout or vendor mapping).
* We therefore call input_keys_pressed() a second time using the
* co-pilot's own joy_idx so its autoconf binds are correctly resolved.
* Both calls write into the same current_bits bitmask, so either pad
* can trigger any hotkey. */
{
unsigned copilot_joy = settings->uints.input_copilot_port[port];
if ( copilot_joy != COPILOT_PORT_DISABLED
&& copilot_joy < MAX_INPUT_DEVICES)
{
rarch_joypad_info_t cp_joypad_info;
const struct retro_keybind *cp_binds_norm =
&input_config_binds[port][RARCH_ENABLE_HOTKEY];
const struct retro_keybind *cp_binds_auto =
&input_autoconf_binds[copilot_joy][RARCH_ENABLE_HOTKEY];

cp_joypad_info.joy_idx = copilot_joy;
cp_joypad_info.auto_binds = input_autoconf_binds[copilot_joy];
cp_joypad_info.axis_threshold = settings->floats.input_axis_threshold;

input_keys_pressed(
port,
hotkey_port,
#ifdef HAVE_MENU
menu_is_alive,
#else
false,
#endif
block_delay,
current_bits,
(const retro_keybind_set *)input_config_binds,
cp_binds_norm,
cp_binds_auto,
joypad,
sec_joypad,
&cp_joypad_info,
settings->bools.input_hotkey_device_merge);
}
}

#ifdef HAVE_MENU
if (menu_is_alive)
{
Expand Down
146 changes: 146 additions & 0 deletions menu/menu_setting.c
Original file line number Diff line number Diff line change
Expand Up @@ -8279,6 +8279,112 @@ static size_t get_string_representation_split_joycon(
}
#endif

/* ── CO-PILOT ACCESSIBILITY ─────────────────────────────────────────────
* Callbacks for the "Co-pilot Joypad" setting in Port N Controls.
* The value stored is the physical joypad index (0-based) of the assistant
* controller, or COPILOT_PORT_DISABLED (255) when the feature is off.
* Navigation: left/right cycle through real devices + the Disabled sentinel.
* Display: shows the device name (same format as Device Index) or "Disabled".
*/

static size_t get_string_representation_copilot_port(
rarch_setting_t *setting, char *s, size_t len)
{
settings_t *settings = config_get_ptr();
size_t _len = 0;
unsigned map;

if (!setting || !settings)
return 0;

map = settings->uints.input_copilot_port[setting->index_offset];

if (map == COPILOT_PORT_DISABLED)
return strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_DISABLED), len);

if (map < MAX_INPUT_DEVICES)
{
const char *device_name = input_config_get_device_display_name(map)
? input_config_get_device_display_name(map)
: input_config_get_device_name(map);

_len = snprintf(s, len, "#%u: %s",
map + 1,
!string_is_empty(device_name)
? device_name
: msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_AVAILABLE));

if (!string_is_empty(device_name))
{
unsigned idx = input_config_get_device_name_index(map);
if (idx > 0)
_len += snprintf(s + _len, len - _len, " (%u)", idx);
}
}

if (string_is_empty(s))
_len = strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_DISABLED), len);
return _len;
}

static int setting_action_start_copilot_port(rarch_setting_t *setting)
{
settings_t *settings = config_get_ptr();
if (!setting || !settings)
return -1;
/* Default: disabled */
configuration_set_uint(settings,
settings->uints.input_copilot_port[setting->index_offset],
COPILOT_PORT_DISABLED);
return 0;
}

static int setting_action_left_copilot_port(
rarch_setting_t *setting, size_t idx, bool wraparound)
{
settings_t *settings = config_get_ptr();
unsigned *p;

if (!setting || !settings)
return -1;

p = &settings->uints.input_copilot_port[setting->index_offset];

/* Cycle order: DISABLED → (MAX_INPUT_DEVICES-1) → ... → 0 → DISABLED */
if (*p == COPILOT_PORT_DISABLED)
*p = MAX_INPUT_DEVICES - 1;
else if (*p == 0)
*p = COPILOT_PORT_DISABLED;
else
(*p)--;

settings->flags |= SETTINGS_FLG_MODIFIED;
return 0;
}

static int setting_action_right_copilot_port(
rarch_setting_t *setting, size_t idx, bool wraparound)
{
settings_t *settings = config_get_ptr();
unsigned *p;

if (!setting || !settings)
return -1;

p = &settings->uints.input_copilot_port[setting->index_offset];

/* Cycle order: DISABLED → 0 → 1 → ... → (MAX_INPUT_DEVICES-1) → DISABLED */
if (*p == COPILOT_PORT_DISABLED)
*p = 0;
else if (*p >= MAX_INPUT_DEVICES - 1)
*p = COPILOT_PORT_DISABLED;
else
(*p)++;

settings->flags |= SETTINGS_FLG_MODIFIED;
return 0;
}

static size_t get_string_representation_input_device_index(
rarch_setting_t *setting, char *s, size_t len)
{
Expand Down Expand Up @@ -9828,6 +9934,46 @@ static bool setting_append_list_input_player_options(
MENU_SETTINGS_LIST_CURRENT_ADD_ENUM_IDX_PTR(list, list_info,
(enum msg_hash_enums)(MENU_ENUM_LABEL_INPUT_DEVICE_INDEX + user));

/* ── CO-PILOT ACCESSIBILITY ────────────────────────────────────────
* "Co-pilot Joypad": a second physical controller that sends its
* input to the same logical port as this player. Intended to help
* users with disabilities by letting a caregiver or companion share
* control of a single player character.
* Value: physical joypad index (0-based), or COPILOT_PORT_DISABLED. */
{
char copilot_key[64];
char copilot_label[64];
snprintf(copilot_key, sizeof(copilot_key),
"input_copilot_port_p%u", user + 1);
snprintf(copilot_label, sizeof(copilot_label),
"Co-pilot Joypad (Player %u)", user + 1);

CONFIG_UINT_ALT(
list, list_info,
&settings->uints.input_copilot_port[user],
copilot_key,
copilot_label,
user,
&group_info,
&subgroup_info,
parent_group,
general_write_handler,
general_read_handler);
(*list)[list_info->index - 1].index = user + 1;
(*list)[list_info->index - 1].index_offset = user;
(*list)[list_info->index - 1].action_start = &setting_action_start_copilot_port;
(*list)[list_info->index - 1].action_left = &setting_action_left_copilot_port;
(*list)[list_info->index - 1].action_right = &setting_action_right_copilot_port;
(*list)[list_info->index - 1].action_select= &setting_action_right_copilot_port;
(*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
(*list)[list_info->index - 1].get_string_representation =
&get_string_representation_copilot_port;
/* Range: 0 .. MAX_INPUT_DEVICES-1 for real pads;
* COPILOT_PORT_DISABLED (255) is handled by the custom callbacks. */
menu_settings_list_current_add_range(list, list_info,
0, COPILOT_PORT_DISABLED, 1.0, true, true);
}

#ifdef HAVE_LIBNX
snprintf(split_joycon, sizeof(split_joycon),
"%s_%u", msg_hash_to_str(MENU_ENUM_LABEL_INPUT_SPLIT_JOYCON), user + 1);
Expand Down
Loading