use crate::nes::input::{Button, ControllerInput, SnesButton};
use crate::platform::emulator::Console;
use gilrs::{Axis, EventType, GamepadId, Gilrs, GilrsBuilder};
#[derive(Debug, PartialEq)]
pub enum GamepadChange {
Connected(u8),
Disconnected(u8),
}
use std::collections::HashMap;
const AXIS_DEAD_ZONE: f32 = 0.5;
const MAX_CONTROLLERS: usize = 2;
const MAX_CONTROLLERS_FOUR_SCORE: usize = 4;
pub fn map_button_to_nes(button: gilrs::Button) -> Option<Button> {
match button {
gilrs::Button::South => Some(Button::A),
gilrs::Button::East => Some(Button::B),
gilrs::Button::West => Some(Button::A),
gilrs::Button::North => Some(Button::B),
gilrs::Button::Start | gilrs::Button::RightTrigger2 => Some(Button::Start),
gilrs::Button::Select | gilrs::Button::LeftTrigger2 => Some(Button::Select),
gilrs::Button::DPadUp => Some(Button::Up),
gilrs::Button::DPadDown => Some(Button::Down),
gilrs::Button::DPadLeft => Some(Button::Left),
gilrs::Button::DPadRight => Some(Button::Right),
_ => None,
}
}
pub fn nes_dpad_to_snes(button: Button) -> Option<SnesButton> {
match button {
Button::Up => Some(SnesButton::Up),
Button::Down => Some(SnesButton::Down),
Button::Left => Some(SnesButton::Left),
Button::Right => Some(SnesButton::Right),
_ => None,
}
}
pub fn map_button_to_snes(button: gilrs::Button) -> Option<SnesButton> {
match button {
gilrs::Button::South => Some(SnesButton::B),
gilrs::Button::East => Some(SnesButton::A),
gilrs::Button::West => Some(SnesButton::Y),
gilrs::Button::North => Some(SnesButton::X),
gilrs::Button::Start => Some(SnesButton::Start),
gilrs::Button::Select => Some(SnesButton::Select),
gilrs::Button::LeftTrigger => Some(SnesButton::L),
gilrs::Button::RightTrigger => Some(SnesButton::R),
gilrs::Button::LeftTrigger2 => Some(SnesButton::L),
gilrs::Button::RightTrigger2 => Some(SnesButton::R),
gilrs::Button::DPadUp => Some(SnesButton::Up),
gilrs::Button::DPadDown => Some(SnesButton::Down),
gilrs::Button::DPadLeft => Some(SnesButton::Left),
gilrs::Button::DPadRight => Some(SnesButton::Right),
_ => None,
}
}
#[derive(Debug, Default)]
pub struct AxisState {
pub up: bool,
pub down: bool,
pub left: bool,
pub right: bool,
}
impl AxisState {
pub fn update_x(&mut self, value: f32) -> Vec<(Button, bool)> {
let mut changes = Vec::new();
let new_left = value < -AXIS_DEAD_ZONE;
let new_right = value > AXIS_DEAD_ZONE;
if new_left != self.left {
changes.push((Button::Left, new_left));
self.left = new_left;
}
if new_right != self.right {
changes.push((Button::Right, new_right));
self.right = new_right;
}
changes
}
pub fn update_y(&mut self, value: f32) -> Vec<(Button, bool)> {
let mut changes = Vec::new();
let new_up = value > AXIS_DEAD_ZONE;
let new_down = value < -AXIS_DEAD_ZONE;
if new_up != self.up {
changes.push((Button::Up, new_up));
self.up = new_up;
}
if new_down != self.down {
changes.push((Button::Down, new_down));
self.down = new_down;
}
changes
}
}
#[derive(Default)]
struct GamepadState {
axis: AxisState,
}
pub struct GamepadManager {
gilrs: Gilrs,
player_map: HashMap<GamepadId, u8>,
gamepad_states: HashMap<GamepadId, GamepadState>,
max_controllers: usize,
}
impl GamepadManager {
pub fn new(four_score: bool) -> Result<Self, String> {
let mut builder = GilrsBuilder::new()
.with_default_filters(true)
.add_env_mappings(true);
if let Ok(mappings) = std::fs::read_to_string("gamecontrollerdb.txt") {
builder = builder.add_mappings(&mappings);
}
let gilrs = builder
.build()
.map_err(|e| format!("failed to initialize gilrs: {e}"))?;
let max_controllers = if four_score {
MAX_CONTROLLERS_FOUR_SCORE
} else {
MAX_CONTROLLERS
};
let mut manager = Self {
gilrs,
player_map: HashMap::new(),
gamepad_states: HashMap::new(),
max_controllers,
};
manager.enumerate_connected();
manager.drain_pending_connections();
Ok(manager)
}
pub fn connected_count(&self) -> usize {
self.player_map.len()
}
pub fn process_events(&mut self, console: &mut Console) -> Vec<GamepadChange> {
let mut changes = Vec::new();
while let Some(event) = self.gilrs.next_event() {
match event.event {
EventType::ButtonPressed(button, _) => {
self.handle_button(console, event.id, button, true);
}
EventType::ButtonReleased(button, _) => {
self.handle_button(console, event.id, button, false);
}
EventType::AxisChanged(axis, value, _) => {
self.handle_axis(console, event.id, axis, value);
}
EventType::Connected => {
if let Some(change) = self.handle_connected(event.id) {
changes.push(change);
}
}
EventType::Disconnected => {
if let Some(change) = self.handle_disconnected(console, event.id) {
changes.push(change);
}
}
_ => {}
}
}
changes
}
fn enumerate_connected(&mut self) {
let connected_ids: Vec<GamepadId> = self
.gilrs
.gamepads()
.filter(|(_, gp)| gp.is_connected())
.map(|(id, _)| id)
.collect();
for id in connected_ids {
self.handle_connected(id);
}
}
fn drain_pending_connections(&mut self) {
while let Some(event) = self.gilrs.next_event() {
if let EventType::Connected = event.event {
self.handle_connected(event.id);
}
}
}
fn handle_connected(&mut self, id: GamepadId) -> Option<GamepadChange> {
if self.player_map.len() >= self.max_controllers {
return None;
}
if self.player_map.contains_key(&id) {
return None;
}
let player_num = self.next_available_player();
self.player_map.insert(id, player_num);
self.gamepad_states.insert(id, GamepadState::default());
if let Some(gamepad) = self.gilrs.connected_gamepad(id) {
crate::platform::debugging::log_info(format!(
"Gamepad connected for player {player_num}: {}",
gamepad.name()
));
}
Some(GamepadChange::Connected(player_num))
}
fn handle_disconnected(
&mut self,
console: &mut Console,
id: GamepadId,
) -> Option<GamepadChange> {
if let Some(player_num) = self.player_map.get(&id).copied() {
if let Console::Nes(nes) = console
&& let Some(port) = Self::assigned_port(nes, &self.player_map, player_num)
{
use Button::{A, B, Down, Left, Right, Select, Start, Up};
for button in [A, B, Select, Start, Up, Down, Left, Right] {
nes.set_button(port, button, false);
}
use SnesButton as S;
for snes_btn in [
S::A,
S::B,
S::X,
S::Y,
S::L,
S::R,
S::Start,
S::Select,
S::Up,
S::Down,
S::Left,
S::Right,
] {
nes.set_snes_button(port, snes_btn, false);
}
}
self.player_map.remove(&id);
self.gamepad_states.remove(&id);
crate::platform::debugging::log_info(format!(
"Gamepad disconnected (was player {player_num})"
));
self.reassign_players();
if !matches!(console, Console::Nes(_)) && self.player_map.is_empty() {
console.set_joypad_button_states(0, 0);
}
Some(GamepadChange::Disconnected(player_num))
} else {
None
}
}
fn next_available_player(&self) -> u8 {
let used: Vec<u8> = self.player_map.values().copied().collect();
for n in 1..=(self.max_controllers as u8) {
if !used.contains(&n) {
return n;
}
}
(self.player_map.len() + 1) as u8
}
fn reassign_players(&mut self) {
let mut ids: Vec<GamepadId> = self.player_map.keys().copied().collect();
ids.sort_by_key(|id| self.player_map[id]);
self.player_map.clear();
for (idx, id) in ids.into_iter().enumerate() {
self.player_map.insert(id, (idx + 1) as u8);
}
}
fn assigned_port(
nes: &crate::nes::console::Nes,
player_map: &HashMap<GamepadId, u8>,
player_num: u8,
) -> Option<u8> {
let gamepad_ports: Vec<u8> = (1..=4)
.filter(|&port| nes.controller_input_type(port) == Some(ControllerInput::Gamepad))
.collect();
if gamepad_ports.is_empty() {
return None;
}
let index = (player_num as usize).saturating_sub(1);
gamepad_ports.get(index).copied().and_then(|port| {
let assigned_count = player_map.len().min(gamepad_ports.len());
if index < assigned_count {
Some(port)
} else {
None
}
})
}
fn handle_button(
&self,
console: &mut Console,
id: GamepadId,
button: gilrs::Button,
pressed: bool,
) {
if let Console::Nes(nes) = console {
let Some(&player_num) = self.player_map.get(&id) else {
return;
};
let Some(port) = Self::assigned_port(nes, &self.player_map, player_num) else {
return;
};
if let Some(snes_btn) = map_button_to_snes(button)
&& nes.set_snes_button(port, snes_btn, pressed)
{
return;
}
if let Some(nes_btn) = map_button_to_nes(button) {
nes.set_button(port, nes_btn, pressed);
}
} else if let Some(nes_btn) = map_button_to_nes(button) {
console.set_button(0, nes_btn as u8, pressed);
}
}
fn handle_axis(&mut self, console: &mut Console, id: GamepadId, axis: Axis, value: f32) {
let state = self.gamepad_states.entry(id).or_default();
let changes = match axis {
Axis::LeftStickX => state.axis.update_x(value),
Axis::LeftStickY => state.axis.update_y(value),
_ => return,
};
if let Console::Nes(nes) = console {
let Some(&player_num) = self.player_map.get(&id) else {
return;
};
let Some(port) = Self::assigned_port(nes, &self.player_map, player_num) else {
return;
};
for (button, pressed) in changes {
if let Some(snes_btn) = nes_dpad_to_snes(button)
&& nes.set_snes_button(port, snes_btn, pressed)
{
continue;
}
nes.set_button(port, button, pressed);
}
} else {
for (btn, pressed) in changes {
console.set_button(0, btn as u8, pressed);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nes_dpad_to_snes_maps_directions() {
assert_eq!(nes_dpad_to_snes(Button::Up), Some(SnesButton::Up));
assert_eq!(nes_dpad_to_snes(Button::Down), Some(SnesButton::Down));
assert_eq!(nes_dpad_to_snes(Button::Left), Some(SnesButton::Left));
assert_eq!(nes_dpad_to_snes(Button::Right), Some(SnesButton::Right));
}
#[test]
fn nes_dpad_to_snes_returns_none_for_non_dpad() {
assert_eq!(nes_dpad_to_snes(Button::A), None);
assert_eq!(nes_dpad_to_snes(Button::B), None);
assert_eq!(nes_dpad_to_snes(Button::Start), None);
assert_eq!(nes_dpad_to_snes(Button::Select), None);
}
#[test]
fn nes_mapping_south_is_a() {
assert_eq!(map_button_to_nes(gilrs::Button::South), Some(Button::A));
}
#[test]
fn nes_mapping_east_is_b() {
assert_eq!(map_button_to_nes(gilrs::Button::East), Some(Button::B));
}
#[test]
fn nes_mapping_west_is_a_alternate() {
assert_eq!(map_button_to_nes(gilrs::Button::West), Some(Button::A));
}
#[test]
fn nes_mapping_north_is_b_alternate() {
assert_eq!(map_button_to_nes(gilrs::Button::North), Some(Button::B));
}
#[test]
fn nes_mapping_start() {
assert_eq!(map_button_to_nes(gilrs::Button::Start), Some(Button::Start));
}
#[test]
fn nes_mapping_select() {
assert_eq!(
map_button_to_nes(gilrs::Button::Select),
Some(Button::Select)
);
}
#[test]
fn nes_mapping_dpad_up() {
assert_eq!(map_button_to_nes(gilrs::Button::DPadUp), Some(Button::Up));
}
#[test]
fn nes_mapping_dpad_down() {
assert_eq!(
map_button_to_nes(gilrs::Button::DPadDown),
Some(Button::Down)
);
}
#[test]
fn nes_mapping_dpad_left() {
assert_eq!(
map_button_to_nes(gilrs::Button::DPadLeft),
Some(Button::Left)
);
}
#[test]
fn nes_mapping_dpad_right() {
assert_eq!(
map_button_to_nes(gilrs::Button::DPadRight),
Some(Button::Right)
);
}
#[test]
fn nes_mapping_shoulder_returns_none() {
assert_eq!(map_button_to_nes(gilrs::Button::LeftTrigger), None);
assert_eq!(map_button_to_nes(gilrs::Button::RightTrigger), None);
}
#[test]
fn nes_mapping_trigger2_maps_to_select_start() {
assert_eq!(
map_button_to_nes(gilrs::Button::LeftTrigger2),
Some(Button::Select)
);
assert_eq!(
map_button_to_nes(gilrs::Button::RightTrigger2),
Some(Button::Start)
);
}
#[test]
fn nes_mapping_unknown_returns_none() {
assert_eq!(map_button_to_nes(gilrs::Button::Unknown), None);
assert_eq!(map_button_to_nes(gilrs::Button::Mode), None);
}
#[test]
fn snes_mapping_south_is_b() {
assert_eq!(
map_button_to_snes(gilrs::Button::South),
Some(SnesButton::B)
);
}
#[test]
fn snes_mapping_east_is_a() {
assert_eq!(map_button_to_snes(gilrs::Button::East), Some(SnesButton::A));
}
#[test]
fn snes_mapping_west_is_y() {
assert_eq!(map_button_to_snes(gilrs::Button::West), Some(SnesButton::Y));
}
#[test]
fn snes_mapping_north_is_x() {
assert_eq!(
map_button_to_snes(gilrs::Button::North),
Some(SnesButton::X)
);
}
#[test]
fn snes_mapping_start() {
assert_eq!(
map_button_to_snes(gilrs::Button::Start),
Some(SnesButton::Start)
);
}
#[test]
fn snes_mapping_select() {
assert_eq!(
map_button_to_snes(gilrs::Button::Select),
Some(SnesButton::Select)
);
}
#[test]
fn snes_mapping_left_trigger_is_l() {
assert_eq!(
map_button_to_snes(gilrs::Button::LeftTrigger),
Some(SnesButton::L)
);
}
#[test]
fn snes_mapping_right_trigger_is_r() {
assert_eq!(
map_button_to_snes(gilrs::Button::RightTrigger),
Some(SnesButton::R)
);
}
#[test]
fn snes_mapping_left_trigger2_is_l() {
assert_eq!(
map_button_to_snes(gilrs::Button::LeftTrigger2),
Some(SnesButton::L)
);
}
#[test]
fn snes_mapping_right_trigger2_is_r() {
assert_eq!(
map_button_to_snes(gilrs::Button::RightTrigger2),
Some(SnesButton::R)
);
}
#[test]
fn snes_mapping_dpad() {
assert_eq!(
map_button_to_snes(gilrs::Button::DPadUp),
Some(SnesButton::Up)
);
assert_eq!(
map_button_to_snes(gilrs::Button::DPadDown),
Some(SnesButton::Down)
);
assert_eq!(
map_button_to_snes(gilrs::Button::DPadLeft),
Some(SnesButton::Left)
);
assert_eq!(
map_button_to_snes(gilrs::Button::DPadRight),
Some(SnesButton::Right)
);
}
#[test]
fn snes_mapping_unknown_returns_none() {
assert_eq!(map_button_to_snes(gilrs::Button::Unknown), None);
assert_eq!(map_button_to_snes(gilrs::Button::Mode), None);
}
#[test]
fn axis_x_neutral_produces_no_changes() {
let mut state = AxisState::default();
let changes = state.update_x(0.0);
assert!(changes.is_empty());
}
#[test]
fn axis_x_left_press() {
let mut state = AxisState::default();
let changes = state.update_x(-0.8);
assert_eq!(changes, vec![(Button::Left, true)]);
assert!(state.left);
assert!(!state.right);
}
#[test]
fn axis_x_right_press() {
let mut state = AxisState::default();
let changes = state.update_x(0.8);
assert_eq!(changes, vec![(Button::Right, true)]);
assert!(!state.left);
assert!(state.right);
}
#[test]
fn axis_x_left_release_on_return_to_neutral() {
let mut state = AxisState::default();
state.update_x(-0.8);
let changes = state.update_x(0.0);
assert_eq!(changes, vec![(Button::Left, false)]);
assert!(!state.left);
}
#[test]
fn axis_x_at_dead_zone_boundary_does_not_trigger() {
let mut state = AxisState::default();
let changes = state.update_x(-AXIS_DEAD_ZONE);
assert!(
changes.is_empty(),
"exactly at dead zone should not trigger"
);
}
#[test]
fn axis_x_just_beyond_dead_zone_triggers() {
let mut state = AxisState::default();
let changes = state.update_x(-AXIS_DEAD_ZONE - 0.01);
assert_eq!(changes, vec![(Button::Left, true)]);
}
#[test]
fn axis_y_neutral_produces_no_changes() {
let mut state = AxisState::default();
let changes = state.update_y(0.0);
assert!(changes.is_empty());
}
#[test]
fn axis_y_up_press() {
let mut state = AxisState::default();
let changes = state.update_y(0.8);
assert_eq!(changes, vec![(Button::Up, true)]);
assert!(state.up);
}
#[test]
fn axis_y_down_press() {
let mut state = AxisState::default();
let changes = state.update_y(-0.8);
assert_eq!(changes, vec![(Button::Down, true)]);
assert!(state.down);
}
#[test]
fn axis_y_release_on_return_to_neutral() {
let mut state = AxisState::default();
state.update_y(-0.8);
let changes = state.update_y(0.0);
assert_eq!(changes, vec![(Button::Down, false)]);
assert!(!state.down);
}
#[test]
fn axis_repeated_same_value_produces_no_changes() {
let mut state = AxisState::default();
state.update_x(-0.8);
let changes = state.update_x(-0.9);
assert!(
changes.is_empty(),
"same direction twice should not re-trigger"
);
}
#[test]
fn axis_snap_from_left_to_right() {
let mut state = AxisState::default();
state.update_x(-0.8);
let changes = state.update_x(0.8);
assert_eq!(changes, vec![(Button::Left, false), (Button::Right, true)]);
}
struct TestPlayerMap {
player_map: HashMap<usize, u8>,
max_controllers: usize,
}
impl TestPlayerMap {
fn new(max: usize) -> Self {
Self {
player_map: HashMap::new(),
max_controllers: max,
}
}
fn next_available_player(&self) -> u8 {
let used: Vec<u8> = self.player_map.values().copied().collect();
for n in 1..=(self.max_controllers as u8) {
if !used.contains(&n) {
return n;
}
}
(self.player_map.len() + 1) as u8
}
fn add(&mut self, id: usize) -> Option<u8> {
if self.player_map.len() >= self.max_controllers {
return None;
}
if self.player_map.contains_key(&id) {
return self.player_map.get(&id).copied();
}
let num = self.next_available_player();
self.player_map.insert(id, num);
Some(num)
}
fn remove(&mut self, id: usize) -> Option<u8> {
let removed = self.player_map.remove(&id);
if removed.is_some() {
self.reassign_players();
}
removed
}
fn reassign_players(&mut self) {
let mut ids: Vec<usize> = self.player_map.keys().copied().collect();
ids.sort_by_key(|id| self.player_map[id]);
self.player_map.clear();
for (idx, id) in ids.into_iter().enumerate() {
self.player_map.insert(id, (idx + 1) as u8);
}
}
}
#[test]
fn player_assignment_first_connected_is_p1() {
let mut map = TestPlayerMap::new(2);
let num = map.add(0);
assert_eq!(num, Some(1));
}
#[test]
fn player_assignment_second_connected_is_p2() {
let mut map = TestPlayerMap::new(2);
map.add(0);
let num = map.add(1);
assert_eq!(num, Some(2));
}
#[test]
fn player_assignment_rejects_third_in_normal_mode() {
let mut map = TestPlayerMap::new(2);
map.add(0);
map.add(1);
let num = map.add(2);
assert_eq!(num, None);
}
#[test]
fn player_assignment_allows_four_in_four_score() {
let mut map = TestPlayerMap::new(4);
assert_eq!(map.add(0), Some(1));
assert_eq!(map.add(1), Some(2));
assert_eq!(map.add(2), Some(3));
assert_eq!(map.add(3), Some(4));
}
#[test]
fn player_assignment_rejects_fifth_in_four_score() {
let mut map = TestPlayerMap::new(4);
for i in 0..4 {
map.add(i);
}
assert_eq!(map.add(4), None);
}
#[test]
fn player_assignment_duplicate_returns_existing() {
let mut map = TestPlayerMap::new(2);
map.add(0);
let second = map.add(0);
assert_eq!(second, Some(1));
assert_eq!(map.player_map.len(), 1);
}
#[test]
fn player_assignment_disconnect_and_reassign() {
let mut map = TestPlayerMap::new(2);
map.add(0);
map.add(1);
map.remove(0);
assert_eq!(map.player_map.get(&1), Some(&1));
}
#[test]
fn player_assignment_reconnect_after_disconnect() {
let mut map = TestPlayerMap::new(2);
map.add(0);
map.add(1);
map.remove(0);
let num = map.add(2);
assert_eq!(num, Some(2), "new gamepad should fill the P2 slot");
}
#[test]
fn player_assignment_disconnect_middle_reassigns_consecutively() {
let mut map = TestPlayerMap::new(4);
map.add(0);
map.add(1);
map.add(2);
map.remove(1);
assert_eq!(map.player_map.get(&0), Some(&1));
assert_eq!(map.player_map.get(&2), Some(&2));
}
}