Add gamecube adapter support
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
//! Turns [`GcAdapterSnapshot`] port states into real Bevy `Gamepad`
|
||||
//! entities, the same way `bevy_gilrs` turns OS gamepad events into them.
|
||||
|
||||
use bevy::input::gamepad::{
|
||||
GamepadAxis, GamepadButton, GamepadConnection, GamepadConnectionEvent, GamepadSettings,
|
||||
RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, RawGamepadEvent,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::poll::{GcAdapterSnapshot, GcPortState};
|
||||
|
||||
/// GameCube adapters report 4 ports.
|
||||
pub const GC_PORT_COUNT: usize = 4;
|
||||
|
||||
/// USB vendor/product ID of the official adapter, reported on the `Gamepad`
|
||||
/// component so it shows up the same way a real USB gamepad would.
|
||||
const GC_ADAPTER_VID: u16 = 0x057e;
|
||||
const GC_ADAPTER_PID: u16 = 0x0337;
|
||||
|
||||
/// Marks a `Gamepad` entity as backed by a specific GameCube adapter port
|
||||
/// (0-3), and is the join key [`crate::input::gamepad::sample_gamepad`]
|
||||
/// uses to map GGRS handles to ports.
|
||||
#[derive(Component, Debug, Clone, Copy)]
|
||||
pub struct GcPort(pub u8);
|
||||
|
||||
/// Tracks which entity (if any) represents each of the 4 adapter ports,
|
||||
/// and the last snapshot seen, so both systems below can diff instead of
|
||||
/// re-deriving state every frame.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct GcPorts {
|
||||
pub entities: [Option<Entity>; GC_PORT_COUNT],
|
||||
last: [GcPortState; GC_PORT_COUNT],
|
||||
}
|
||||
|
||||
fn passthrough_gamepad_settings() -> GamepadSettings {
|
||||
let mut settings = GamepadSettings::default();
|
||||
for axis in GamepadAxis::all() {
|
||||
let axis_settings = settings.axis_settings.entry(axis).or_default();
|
||||
// Order matters: each setter rejects a value that would cross the
|
||||
// *other* bound it's paired with, so widen the live zones before
|
||||
// shrinking the dead zones down to zero.
|
||||
axis_settings.set_livezone_lowerbound(-1.0);
|
||||
axis_settings.set_livezone_upperbound(1.0);
|
||||
axis_settings.set_deadzone_lowerbound(0.0);
|
||||
axis_settings.set_deadzone_upperbound(0.0);
|
||||
axis_settings.set_threshold(0.0);
|
||||
}
|
||||
for button in GamepadButton::all() {
|
||||
let button_axis_settings = settings.button_axis_settings.entry(button).or_default();
|
||||
button_axis_settings.low = 0.0;
|
||||
button_axis_settings.high = 1.0;
|
||||
button_axis_settings.threshold = 0.0;
|
||||
}
|
||||
settings
|
||||
}
|
||||
|
||||
/// Spawns/despawns `Gamepad` entities to match which adapter ports are
|
||||
/// connected. Must run before `gamepad_connection_system` so the entity it
|
||||
/// spawns already exists by the time Bevy inserts the `Gamepad` component
|
||||
/// on it (mirrors what `bevy_gilrs::gilrs_event_system` does).
|
||||
pub fn gc_connection_system(
|
||||
mut commands: Commands,
|
||||
snapshot: Res<GcAdapterSnapshot>,
|
||||
mut ports: ResMut<GcPorts>,
|
||||
mut connection_events: MessageWriter<GamepadConnectionEvent>,
|
||||
) {
|
||||
let current = snapshot.read();
|
||||
let ports = &mut *ports;
|
||||
|
||||
for i in 0..GC_PORT_COUNT {
|
||||
let was_connected = ports.last[i].connected;
|
||||
let is_connected = current[i].connected;
|
||||
|
||||
if is_connected && !was_connected {
|
||||
let entity = ports.entities[i].unwrap_or_else(|| {
|
||||
let entity = commands
|
||||
.spawn((GcPort(i as u8), passthrough_gamepad_settings()))
|
||||
.id();
|
||||
ports.entities[i] = Some(entity);
|
||||
entity
|
||||
});
|
||||
connection_events.write(GamepadConnectionEvent::new(
|
||||
entity,
|
||||
GamepadConnection::Connected {
|
||||
name: format!("GameCube Controller (Port {})", i + 1),
|
||||
vendor_id: Some(GC_ADAPTER_VID),
|
||||
product_id: Some(GC_ADAPTER_PID),
|
||||
},
|
||||
));
|
||||
} else if !is_connected && was_connected {
|
||||
if let Some(entity) = ports.entities[i] {
|
||||
connection_events.write(GamepadConnectionEvent::new(
|
||||
entity,
|
||||
GamepadConnection::Disconnected,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a GC unsigned trigger byte to the 0.0..=1.0 range
|
||||
/// `GamepadButton` analog values use.
|
||||
fn unsigned_axis_float(raw: u8) -> f32 {
|
||||
(raw as f32) / 255.0
|
||||
}
|
||||
|
||||
/// Diffs the current snapshot against last frame's and writes
|
||||
/// `RawGamepadEvent`s for anything that changed, which
|
||||
/// `gamepad_event_processing_system` turns into live `Gamepad` component
|
||||
/// state. Must run after `gc_connection_system` (so newly-connected ports
|
||||
/// have an entity/`GamepadSettings`) and before
|
||||
/// `gamepad_event_processing_system`.
|
||||
pub fn gc_event_system(
|
||||
snapshot: Res<GcAdapterSnapshot>,
|
||||
mut ports: ResMut<GcPorts>,
|
||||
mut raw_events: MessageWriter<RawGamepadEvent>,
|
||||
mut axis_events: MessageWriter<RawGamepadAxisChangedEvent>,
|
||||
mut button_events: MessageWriter<RawGamepadButtonChangedEvent>,
|
||||
) {
|
||||
let current = snapshot.read();
|
||||
|
||||
for i in 0..GC_PORT_COUNT {
|
||||
let Some(entity) = ports.entities[i] else {
|
||||
continue;
|
||||
};
|
||||
let prev = &ports.last[i];
|
||||
let now = ¤t[i];
|
||||
if !now.connected {
|
||||
continue;
|
||||
}
|
||||
|
||||
macro_rules! send_axis {
|
||||
($axis:expr, $value:expr) => {{
|
||||
let event = RawGamepadAxisChangedEvent::new(entity, $axis, $value);
|
||||
axis_events.write(event);
|
||||
raw_events.write(RawGamepadEvent::Axis(event));
|
||||
}};
|
||||
}
|
||||
macro_rules! send_button {
|
||||
($button:expr, $value:expr) => {{
|
||||
let event = RawGamepadButtonChangedEvent::new(entity, $button, $value);
|
||||
button_events.write(event);
|
||||
raw_events.write(RawGamepadEvent::Button(event));
|
||||
}};
|
||||
}
|
||||
|
||||
if prev.left_stick != now.left_stick {
|
||||
send_axis!(GamepadAxis::LeftStickX, now.left_stick.0);
|
||||
send_axis!(GamepadAxis::LeftStickY, now.left_stick.1);
|
||||
}
|
||||
if prev.right_stick != now.right_stick {
|
||||
send_axis!(
|
||||
GamepadAxis::RightStickX,
|
||||
now.right_stick.0
|
||||
);
|
||||
send_axis!(
|
||||
GamepadAxis::RightStickY,
|
||||
now.right_stick.1
|
||||
);
|
||||
}
|
||||
if prev.triggers != now.triggers {
|
||||
// The analog triggers are represented as button-axis values, as we have no GamepadAxis to use.
|
||||
send_button!(GamepadButton::LeftTrigger2, unsigned_axis_float(now.triggers.0));
|
||||
send_button!(GamepadButton::RightTrigger2, unsigned_axis_float(now.triggers.1));
|
||||
}
|
||||
|
||||
macro_rules! digital {
|
||||
($field:ident, $button:expr) => {
|
||||
if prev.$field != now.$field {
|
||||
send_button!($button, if now.$field { 1.0 } else { 0.0 });
|
||||
}
|
||||
};
|
||||
}
|
||||
digital!(a, GamepadButton::South);
|
||||
digital!(b, GamepadButton::West);
|
||||
digital!(x, GamepadButton::East);
|
||||
digital!(y, GamepadButton::North);
|
||||
digital!(z, GamepadButton::Z);
|
||||
digital!(start, GamepadButton::Start);
|
||||
digital!(dpad_up, GamepadButton::DPadUp);
|
||||
digital!(dpad_down, GamepadButton::DPadDown);
|
||||
digital!(dpad_left, GamepadButton::DPadLeft);
|
||||
digital!(dpad_right, GamepadButton::DPadRight);
|
||||
digital!(left_trigger_digital, GamepadButton::LeftTrigger);
|
||||
digital!(right_trigger_digital, GamepadButton::RightTrigger);
|
||||
}
|
||||
|
||||
ports.last = current;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
pub mod bridge;
|
||||
pub mod poll;
|
||||
|
||||
pub use bridge::{GcPort, GcPorts, GC_PORT_COUNT};
|
||||
pub use poll::{GcAdapterSnapshot, GcPortState};
|
||||
|
||||
use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system};
|
||||
use bevy::input::InputSystems;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GamecubeAdapterPlugin;
|
||||
|
||||
impl Plugin for GamecubeAdapterPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<GcPorts>();
|
||||
|
||||
let Ok(snapshot) = poll::spawn_poll_thread() else {
|
||||
// No adapter connected; leave GcPorts empty and skip wiring the
|
||||
// polling systems.
|
||||
return;
|
||||
};
|
||||
|
||||
app.insert_resource(snapshot).add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
bridge::gc_connection_system.before(gamepad_connection_system),
|
||||
bridge::gc_event_system
|
||||
.after(gamepad_connection_system)
|
||||
.before(gamepad_event_processing_system),
|
||||
)
|
||||
.in_set(InputSystems),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use gc_adapter_neo::{Controller, GcAdapter};
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub struct GcPortState {
|
||||
pub connected: bool,
|
||||
pub a: bool,
|
||||
pub b: bool,
|
||||
pub x: bool,
|
||||
pub y: bool,
|
||||
pub z: bool,
|
||||
pub start: bool,
|
||||
pub dpad_up: bool,
|
||||
pub dpad_down: bool,
|
||||
pub dpad_left: bool,
|
||||
pub dpad_right: bool,
|
||||
pub left_trigger_digital: bool,
|
||||
pub right_trigger_digital: bool,
|
||||
pub left_stick: (f32, f32),
|
||||
pub right_stick: (f32, f32),
|
||||
/// Raw trigger bytes, 0 is released, 255 is fully pressed.
|
||||
pub triggers: (u8, u8),
|
||||
}
|
||||
|
||||
impl GcPortState {
|
||||
fn make_port_state(c: &Controller, cal: &gc_adapter_neo::PortCalibration) -> Self {
|
||||
if !c.connected() {
|
||||
return Self::default();
|
||||
}
|
||||
let b = &c.buttons;
|
||||
Self {
|
||||
connected: true,
|
||||
a: b.a(),
|
||||
b: b.b(),
|
||||
x: b.x(),
|
||||
y: b.y(),
|
||||
z: b.z(),
|
||||
start: b.start(),
|
||||
dpad_up: b.dpad_up(),
|
||||
dpad_down: b.dpad_down(),
|
||||
dpad_left: b.dpad_left(),
|
||||
dpad_right: b.dpad_right(),
|
||||
left_trigger_digital: b.left_trigger(),
|
||||
right_trigger_digital: b.right_trigger(),
|
||||
left_stick: cal.left_stick(&c),
|
||||
right_stick: cal.right_stick(&c),
|
||||
triggers: (c.triggers.left.raw(), c.triggers.right.raw()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared, `Send + Sync` handle to the latest snapshot from the poll
|
||||
/// thread. Safe to store as an ordinary [`Resource`] (no `NonSend` needed)
|
||||
/// because the `GcAdapter` itself never leaves its thread.
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct GcAdapterSnapshot {
|
||||
ports: Arc<Mutex<[GcPortState; 4]>>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl GcAdapterSnapshot {
|
||||
pub fn read(&self) -> [GcPortState; 4] {
|
||||
*self.ports.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GcAdapterSnapshot {
|
||||
fn drop(&mut self) {
|
||||
// Only actually signals shutdown once the last clone (this resource
|
||||
// is never cloned in practice, but be defensive) is dropped.
|
||||
if Arc::strong_count(&self.shutdown) <= 1 {
|
||||
self.shutdown.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the first GameCube adapter found on the USB bus and starts
|
||||
/// polling it on a dedicated thread. Returns `None` (after logging a
|
||||
/// warning) if no adapter is plugged in
|
||||
pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError> {
|
||||
let mut adapter = match GcAdapter::from_usb()? {
|
||||
Some(adapter) => adapter,
|
||||
None => {
|
||||
warn!("No GameCube Controller Adapter found on USB; gamepad input disabled.");
|
||||
return Err(gc_adapter_neo::UsbError::Disconnected);
|
||||
}
|
||||
};
|
||||
|
||||
let ports = Arc::new(Mutex::new([GcPortState::default(); 4]));
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let thread_ports = ports.clone();
|
||||
let thread_shutdown = shutdown.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("gc-adapter-poll".into())
|
||||
.spawn(move || {
|
||||
// Flush whatever the adapter had buffered before we started
|
||||
if let Err(payload) =
|
||||
panic::catch_unwind(AssertUnwindSafe(|| adapter.refresh_inputs()))
|
||||
{
|
||||
error!(
|
||||
"GameCube Controller Adapter USB refresh panicked on startup: {payload:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let mut rate_window_start = std::time::Instant::now();
|
||||
#[cfg(debug_assertions)]
|
||||
let mut rate_window_count: u32 = 0;
|
||||
|
||||
while !thread_shutdown.load(Ordering::Relaxed) {
|
||||
let result =
|
||||
panic::catch_unwind(AssertUnwindSafe(|| adapter.read_controllers()));
|
||||
|
||||
let controllers = match result {
|
||||
Ok(Ok(controllers)) => controllers,
|
||||
// Ignore this, for some reason we get a bunch of these when we start reading
|
||||
Ok(Err(
|
||||
gc_adapter_neo::AdapterError::Parse(_)
|
||||
| gc_adapter_neo::AdapterError::ShortRead(_),
|
||||
)) => continue,
|
||||
Ok(Err(gc_adapter_neo::AdapterError::Usb(err))) => {
|
||||
error!(
|
||||
"GameCube Controller Adapter USB read failed ({err:?}); stopping \
|
||||
polling. Gamepad input will stop updating until the game is \
|
||||
restarted."
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(payload) => {
|
||||
error!(
|
||||
"GameCube Controller Adapter USB read panicked: {payload:?}; \
|
||||
stopping polling. Gamepad input will stop updating until the \
|
||||
game is restarted."
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut states = [GcPortState::default(); 4];
|
||||
for i in 0..4 {
|
||||
states[i] = GcPortState::make_port_state(&controllers[i], adapter.calibration(i));
|
||||
}
|
||||
*thread_ports.lock().unwrap_or_else(|e| e.into_inner()) = states;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
rate_window_count += 1;
|
||||
let elapsed = rate_window_start.elapsed();
|
||||
if elapsed >= std::time::Duration::from_secs(1) {
|
||||
debug!(
|
||||
"GameCube adapter snapshot publish rate: {:.1} Hz",
|
||||
rate_window_count as f64 / elapsed.as_secs_f64()
|
||||
);
|
||||
rate_window_count = 0;
|
||||
rate_window_start = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn gc-adapter poll thread");
|
||||
|
||||
Ok(GcAdapterSnapshot { ports, shutdown })
|
||||
}
|
||||
Reference in New Issue
Block a user