diff --git a/Cargo.lock b/Cargo.lock index f23f59c..1e1a10a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ahash" version = "0.8.12" @@ -2197,6 +2203,26 @@ dependencies = [ "serde", ] +[[package]] +name = "binread" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258d2d861723c05ad02956cae4878ceddb5e05b0dc2841ca73a3479ab7a46f4d" +dependencies = [ + "binread_derive", +] + +[[package]] +name = "binread_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63ec80ac459fd10ae68854c26ee1c5ab5ac93866dba8f8a95cabbfeaa79b284" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bit-set" version = "0.9.1" @@ -3264,6 +3290,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -3483,12 +3519,24 @@ dependencies = [ "bitfields", "clap", "fixed", + "gc-adapter-neo", "jackdaw_runtime", "serde", "strum", "strum_macros", ] +[[package]] +name = "gc-adapter-neo" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd350c081bf33e501e9a5a11732294dfe266683d6b58a366b097146f3e633cb5" +dependencies = [ + "binread", + "modular-bitfield", + "rusb", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -4511,6 +4559,26 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + [[package]] name = "libloading" version = "0.8.9" @@ -4549,6 +4617,20 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libusb1-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be241693102a24766d0b8526c8988771edac2842630d7e730f8e9fbc014f3703" +dependencies = [ + "cc", + "libc", + "libflate", + "pkg-config", + "tar", + "vcpkg", +] + [[package]] name = "linebender_resource_handle" version = "0.1.1" @@ -4649,6 +4731,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -6070,6 +6173,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + [[package]] name = "robust" version = "1.2.0" @@ -6128,6 +6237,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rusb" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f32cd45962594f9f8cd4547b5757132715600e2c8840aa9ccd3d1a9ed6fdc6" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -6609,6 +6728,17 @@ dependencies = [ "zeno", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.119" @@ -6677,6 +6807,17 @@ dependencies = [ "slotmap", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "term" version = "1.2.1" @@ -7076,6 +7217,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ea29db9f94ff08bb619656b8120878f280526f71dc88b5262c958a510181812" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -8052,6 +8199,16 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "xcursor" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index 980d001..e4b1e12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ strum = "0.28.0" strum_macros = "0.28.0" bevy_common_assets = { version = "0.17.0", features = ["ron"] } bevy_asset_loader = "0.27.0" +gc-adapter-neo = { version = "1.0.0", features = ["libusb"] } # Enable a large amount of optimization in the dev profile for dependencies. [profile.dev.package."*"] diff --git a/assets/fighters/test/test.fighter.ron b/assets/fighters/test/test.fighter.ron index 2ef23e0..5ff0889 100644 --- a/assets/fighters/test/test.fighter.ron +++ b/assets/fighters/test/test.fighter.ron @@ -25,6 +25,10 @@ air_acceleration_base: "0.19", air_acceleration_stick: "0.09", air_friction: "0.05", + + air_dodge_duration: 49, + air_dodge_velocity: "2.790", + air_dodge_decay: "0.9" ), animations: { Wait: "PlyPurin5K_Share_ACTION_Wait", @@ -35,5 +39,6 @@ JumpForward: "PlyPurin5K_Share_ACTION_JumpF", JumpBack: "PlyPurin5K_Share_ACTION_JumpB", Landing: "PlyPurin5K_Share_ACTION_Landing", + AirDodge: "PlyPurin5K_Share_ACTION_EscapeAir", } ) diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index bb02aea..8eb15b2 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -51,8 +51,6 @@ impl FighterAttributes { let accel = accel + direction * self.base_dash_accel; let target_vel = input.movement.x * self.max_dash_vel; - info!("ACCEL {}", accel); - (accel, target_vel) } diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index ad4d1c0..a4041a1 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -93,6 +93,7 @@ impl FighterStateImpl for JumpSquatState { state_context.animations.clips[&AnimKind::JumpSquat], Duration::ZERO, ); + state_context.input.clear_buffer(); } } diff --git a/src/input/buffer.rs b/src/input/buffer.rs index c1a40fa..f9ce0b4 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -14,7 +14,7 @@ use crate::input::FighterInputFrame; use crate::netcode::GGRSCfg; use crate::player::Player; -#[derive(Clone, Copy, EnumIter)] +#[derive(Clone, Copy, EnumIter, Debug)] pub enum FighterCommands { SmashMoveLeft, SmashMoveRight, @@ -153,9 +153,8 @@ pub fn postprocess_input( } else { FighterCommands::SmashMoveLeft }; - input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size); - if input.current_frame.movement.x > 0.0 {} + input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size); } } } diff --git a/src/input/gamecube/bridge.rs b/src/input/gamecube/bridge.rs new file mode 100644 index 0000000..8295700 --- /dev/null +++ b/src/input/gamecube/bridge.rs @@ -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; 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, + mut ports: ResMut, + mut connection_events: MessageWriter, +) { + 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, + mut ports: ResMut, + mut raw_events: MessageWriter, + mut axis_events: MessageWriter, + mut button_events: MessageWriter, +) { + 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; +} diff --git a/src/input/gamecube/mod.rs b/src/input/gamecube/mod.rs new file mode 100644 index 0000000..a8c2f84 --- /dev/null +++ b/src/input/gamecube/mod.rs @@ -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::(); + + 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), + ); + } +} diff --git a/src/input/gamecube/poll.rs b/src/input/gamecube/poll.rs new file mode 100644 index 0000000..ed77502 --- /dev/null +++ b/src/input/gamecube/poll.rs @@ -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>, + shutdown: Arc, +} + +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 { + 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 }) +} diff --git a/src/input/gamepad.rs b/src/input/gamepad.rs new file mode 100644 index 0000000..2137c57 --- /dev/null +++ b/src/input/gamepad.rs @@ -0,0 +1,54 @@ +use bevy::math::Vec2; +use bevy::prelude::*; + +use crate::game_settings::GameSettings; +use crate::input::map::GamepadBinding; +use crate::input::{BaseInputMap, FighterInputFrame, InputMapAction}; +use crate::math::int::FGi32; +use crate::math::vec::FGVec2; + +pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap, game_settings: &GameSettings) -> FighterInputFrame { + let mut input_frame = FighterInputFrame::default(); + let mut movement = Vec2::ZERO; + + for gamepad_element in &input_map.gamepad { + match &gamepad_element.binding { + GamepadBinding::Button(button) => { + let pressed = pad.pressed(*button); + match gamepad_element.action { + InputMapAction::Jump => input_frame.jump |= pressed, + InputMapAction::Shield => input_frame.shield |= pressed, + // Digital buttons driving movement isn't used by the + // default bindings (the stick covers that), but stay + // consistent with the keyboard backend if one is added. + InputMapAction::MovementXDir(sign) if pressed => movement.x += sign as f32, + InputMapAction::MovementYDir(sign) if pressed => movement.y += sign as f32, + _ => {} + } + } + // gamepad does not have a separate axis for left and right, so sign here should be 1 + GamepadBinding::Axis(axis, sign) => { + let value = pad.get(*axis).unwrap_or(0.0) * (*sign as f32); + match gamepad_element.action { + InputMapAction::MovementXDir(_) => movement.x += value, + InputMapAction::MovementYDir(_) => movement.y += value, + _ => {} + } + } + } + } + + if movement.x.abs() < game_settings.input_common.stick_deadzone { + movement.x = 0.0; + } + if movement.y.abs() < game_settings.input_common.stick_deadzone { + movement.y = 0.0; + } + + if movement.length_squared() > 1.0 { + movement = movement.normalize(); + } + input_frame.movement = FGVec2::new(FGi32::from_num(movement.x), FGi32::from_num(movement.y)); + + input_frame +} diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index 58acfdc..8e9a913 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -1,57 +1,41 @@ -//! Keyboard backend: samples the keys into the local input frames GGRS sends -//! over the wire. +//! Keyboard backend: samples the keys into a [`FighterInputFrame`]. -use bevy::platform::collections::HashMap; use bevy::prelude::*; -use bevy_ggrs::{LocalInputs, LocalPlayers}; use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction}; use crate::math::int::FGi32; -use crate::netcode::GGRSCfg; -pub fn preprocess_keyboard_input( - mut commands: Commands, - key: Res>, - input_map: Res, - local_players: Res, -) { - // Keyboard input is only for player 0 +pub fn sample_keyboard(key: &ButtonInput, input_map: &BaseInputMap) -> FighterInputFrame { + let mut input_frame = FighterInputFrame::default(); + for keyboard_elment in &input_map.keyboard { + let action_state = if key.just_pressed(keyboard_elment.key) { + InputActionState::JustPressed + } else if key.just_released(keyboard_elment.key) { + InputActionState::JustReleased + } else if key.pressed(keyboard_elment.key) { + InputActionState::Pressed + } else { + InputActionState::Released + }; - let mut local_inputs = HashMap::new(); + let action_strength = match action_state { + InputActionState::JustPressed | InputActionState::Pressed => FGi32::ONE, + InputActionState::JustReleased | InputActionState::Released => FGi32::ZERO, + }; - for handle in &local_players.0 { - let mut input_frame = FighterInputFrame::default(); - for keyboard_elment in &input_map.keyboard { - let action_state = if key.just_pressed(keyboard_elment.key) { - InputActionState::JustPressed - } else if key.just_released(keyboard_elment.key) { - InputActionState::JustReleased - } else if key.pressed(keyboard_elment.key) { - InputActionState::Pressed - } else { - InputActionState::Released - }; - - let action_strength = match action_state { - InputActionState::JustPressed | InputActionState::Pressed => FGi32::ONE, - InputActionState::JustReleased | InputActionState::Released => FGi32::ZERO, - }; - - match keyboard_elment.action { - InputMapAction::MovementXDir(sign) => { - input_frame.movement.x += (sign) * action_strength - } - InputMapAction::MovementYDir(sign) => { - input_frame.movement.y += (sign) * action_strength - } - InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO, - InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO, + match keyboard_elment.action { + InputMapAction::MovementXDir(sign) => { + input_frame.movement.x += (sign) * action_strength } + InputMapAction::MovementYDir(sign) => { + input_frame.movement.y += (sign) * action_strength + } + InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO, + InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO, } - - input_frame.movement = input_frame.movement.normalize_or_zero(); - input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero(); - local_inputs.insert(*handle, input_frame); } - commands.insert_resource(LocalInputs::(local_inputs)); + + input_frame.movement = input_frame.movement.normalize_or_zero(); + input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero(); + input_frame } diff --git a/src/input/map.rs b/src/input/map.rs index 192867b..d18e471 100644 --- a/src/input/map.rs +++ b/src/input/map.rs @@ -1,7 +1,3 @@ -//! What a physical button means. Device-agnostic: a binding names an -//! [`InputMapAction`], and each device backend (see [`super::keyboard`]) -//! decides how to read it. - use bevy::prelude::*; use serde::{Deserialize, Serialize}; @@ -28,7 +24,20 @@ pub struct KeyboardInputMapElement { pub action: InputMapAction, } +#[derive(Debug)] +pub enum GamepadBinding { + Button(GamepadButton), + Axis(GamepadAxis, i32), +} + +#[derive(Debug)] +pub struct GamepadInputMapElement { + pub binding: GamepadBinding, + pub action: InputMapAction, +} + #[derive(Resource)] pub struct BaseInputMap { pub keyboard: Vec, + pub gamepad: Vec, } diff --git a/src/input/mod.rs b/src/input/mod.rs index 7c12207..57cc57a 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,23 +1,28 @@ //! Input, from physical device to the buffered per-fighter state that the //! state machine reads. -//! -//! The pipeline is: [`keyboard`] samples devices into [`FighterInputFrame`]s -//! that GGRS ships over the wire, then [`buffer::postprocess_input`] turns the -//! rolled-back frames into [`FighterInput`]. +use bevy::platform::collections::HashMap; use bevy::prelude::*; use bevy_ggrs::prelude::*; +use bevy_ggrs::{LocalInputs, LocalPlayers}; pub mod buffer; pub mod control; pub mod frame; +pub mod gamecube; +pub mod gamepad; pub mod keyboard; pub mod map; pub use buffer::{FighterCommands, FighterInput}; pub use frame::FighterInputFrame; -pub use map::{BaseInputMap, InputActionState, InputMapAction, KeyboardInputMapElement}; +pub use map::{ + BaseInputMap, GamepadBinding, GamepadInputMapElement, InputActionState, InputMapAction, + KeyboardInputMapElement, +}; +use crate::game_settings::GameSettings; +use crate::netcode::GGRSCfg; use crate::schedule::GameplaySet; /// Owns the input map, the local-input collection that feeds GGRS, and the @@ -60,8 +65,34 @@ impl Plugin for FighterInputPlugin { action: InputMapAction::Shield, }, ], + gamepad: vec![ + // left stick + GamepadInputMapElement { + binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1), + action: InputMapAction::MovementXDir(1), + }, + GamepadInputMapElement { + binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1), + action: InputMapAction::MovementYDir(1), + }, + // jump + GamepadInputMapElement { + binding: GamepadBinding::Button(GamepadButton::North), + action: InputMapAction::Jump, + }, + // shield/airdodge -- both shoulder buttons, Melee-style + GamepadInputMapElement { + binding: GamepadBinding::Button(GamepadButton::LeftTrigger), + action: InputMapAction::Shield, + }, + GamepadInputMapElement { + binding: GamepadBinding::Button(GamepadButton::RightTrigger), + action: InputMapAction::Shield, + }, + ], }) - .add_systems(ReadInputs, keyboard::preprocess_keyboard_input) + .add_plugins(gamecube::GamecubeAdapterPlugin) + .add_systems(ReadInputs, read_local_inputs) .add_systems( GgrsSchedule, buffer::postprocess_input.in_set(GameplaySet::Input), @@ -69,3 +100,32 @@ impl Plugin for FighterInputPlugin { .rollback_component_with_clone::(); } } + +fn read_local_inputs( + mut commands: Commands, + key: Res>, + input_map: Res, + gc_ports: Res, + gamepads: Query<&Gamepad>, + local_players: Res, + game_settings: Res +) { + let mut local_inputs = HashMap::new(); + + for handle in &local_players.0 { + let gamepad = gc_ports + .entities + .get(*handle) + .copied() + .flatten() + .and_then(|entity| gamepads.get(entity).ok()); + + let input_frame = match gamepad { + Some(pad) => gamepad::sample_gamepad(pad, &input_map, &game_settings), + None => keyboard::sample_keyboard(&key, &input_map), + }; + + local_inputs.insert(*handle, input_frame); + } + commands.insert_resource(LocalInputs::(local_inputs)); +} diff --git a/src/lib.rs b/src/lib.rs index 6e85fae..dbe7803 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,13 +57,6 @@ pub enum AppState { MatchLoadFailed, } -pub fn show_gamepads(q: Query<(&Name, &Gamepad)>) { - info!("GP:"); - for (name, gp) in q { - info!("Gamepad detected: {}", name); - } -} - /// Your game's Bevy plugin. The editor finds it by this name (override /// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the /// standalone binary adds it in `main.rs`. @@ -106,7 +99,6 @@ impl Plugin for GamePlugin { .on_failure_continue_to_state(AppState::CommonAssetLoadFailed) .load_collection::(), ) - .add_systems(Startup, show_gamepads) .add_systems( OnEnter(AppState::PrepareFighterManifests), fighter::manifest::prepare_fighter_manifests,