Files
shinespark/src/input/mod.rs
T
EIREXE aca3886113
CI / build-and-test (push) Successful in 1m24s
CI / production-build (push) Successful in 11m47s
Rework input buffer to match HDR closer (#79)
## Summary

- Preserve distinct same-action bindings in rollback input frames.
- Add independent command lifetimes with tap and state-scoped hold buffering.
- Detect second shield bindings and consume shield commands for air dodge.
- Fix duplicate keyboard mappings being overwritten.

Reviewed-on: #79
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-10-03 09:20:56 +00:00

304 lines
11 KiB
Rust

//! Input, from physical device to the buffered per-fighter state that the
//! state machine reads.
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::{ActionBindingMask, DigitalAction, DigitalActionBindings, FighterInputFrame};
pub use map::{
BaseInputMap, GamepadBinding, GamepadInputMapElement, InputActionState, InputMapAction,
KeyboardInputMapElement, MenuGamepadInputMapElement, MenuInputMap, MenuInputMapAction,
MenuKeyboardInputMapElement,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LocalInputSource {
Keyboard,
Gamepad(Entity),
}
/// Persistent mapping from physical/menu controller slots to input sources.
#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
pub struct LocalInputAssignments(pub Vec<(usize, LocalInputSource)>);
impl LocalInputAssignments {
pub fn get_free_player_index(&self) -> Option<usize> {
for i in 0..16 {
if let None = self.iter().find(|(idx, _)| *idx == i) {
return Some(i);
}
}
None
}
pub fn get_from_slot(&self, slot: usize) -> Option<&LocalInputSource> {
self.0
.iter()
.find(|(c_slot, _)| slot == *c_slot)
.map(|(_, source)| source)
}
}
/// Match-local mapping from dense GGRS handles to physical input sources.
#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
pub struct MatchInputAssignments(pub Vec<Option<LocalInputSource>>);
impl MatchInputAssignments {
pub fn get_for_handle(&self, handle: usize) -> Option<LocalInputSource> {
self.0.get(handle).copied().flatten()
}
}
use crate::game_settings::GameSettings;
use crate::math::int::FGi32;
use crate::netcode::GGRSCfg;
use crate::schedule::GameplaySet;
/// Owns the input map, the local-input collection that feeds GGRS, and the
/// per-fighter input state derived from the rolled-back inputs.
#[derive(Default)]
pub struct FighterInputPlugin;
impl Plugin for FighterInputPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(BaseInputMap {
keyboard: vec![
// right
KeyboardInputMapElement {
key: KeyCode::KeyD,
action: InputMapAction::MovementXDir(1),
binding_slot: 0,
},
// left
KeyboardInputMapElement {
key: KeyCode::KeyA,
action: InputMapAction::MovementXDir(-1),
binding_slot: 0,
},
// up
KeyboardInputMapElement {
key: KeyCode::KeyW,
action: InputMapAction::MovementYDir(1),
binding_slot: 0,
},
// down
KeyboardInputMapElement {
key: KeyCode::KeyS,
action: InputMapAction::MovementYDir(-1),
binding_slot: 0,
},
// jump
KeyboardInputMapElement {
key: KeyCode::Space,
action: InputMapAction::Jump,
binding_slot: 0,
},
// shield/airdodge
KeyboardInputMapElement {
key: KeyCode::ShiftLeft,
action: InputMapAction::Shield,
binding_slot: 0,
},
// attack
KeyboardInputMapElement {
key: KeyCode::KeyJ,
action: InputMapAction::Attack,
binding_slot: 0,
},
// Menu
],
gamepad: vec![
// left stick
GamepadInputMapElement {
binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1),
action: InputMapAction::MovementXDir(1),
binding_slot: 0,
},
GamepadInputMapElement {
binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1),
action: InputMapAction::MovementYDir(1),
binding_slot: 0,
},
// jump
GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::North),
action: InputMapAction::Jump,
binding_slot: 0,
},
GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::LeftTrigger2),
action: InputMapAction::Shield,
binding_slot: 0,
},
GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::RightTrigger2),
action: InputMapAction::Shield,
binding_slot: 1,
},
// attack
GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::East),
action: InputMapAction::Attack,
binding_slot: 0,
},
],
})
.insert_resource(MenuInputMap {
keyboard: vec![
MenuKeyboardInputMapElement {
key: KeyCode::ArrowUp,
action: MenuInputMapAction::MovementYDir(1),
},
MenuKeyboardInputMapElement {
key: KeyCode::ArrowDown,
action: MenuInputMapAction::MovementYDir(-1),
},
MenuKeyboardInputMapElement {
key: KeyCode::ArrowLeft,
action: MenuInputMapAction::MovementXDir(-1),
},
MenuKeyboardInputMapElement {
key: KeyCode::ArrowRight,
action: MenuInputMapAction::MovementXDir(1),
},
MenuKeyboardInputMapElement {
key: KeyCode::Enter,
action: MenuInputMapAction::Accept,
},
MenuKeyboardInputMapElement {
key: KeyCode::Escape,
action: MenuInputMapAction::Back,
},
MenuKeyboardInputMapElement {
key: KeyCode::Space,
action: MenuInputMapAction::Start,
},
MenuKeyboardInputMapElement {
key: KeyCode::KeyW,
action: MenuInputMapAction::MovementYDir(1),
},
MenuKeyboardInputMapElement {
key: KeyCode::KeyS,
action: MenuInputMapAction::MovementYDir(-1),
},
MenuKeyboardInputMapElement {
key: KeyCode::KeyA,
action: MenuInputMapAction::MovementXDir(-1),
},
MenuKeyboardInputMapElement {
key: KeyCode::KeyD,
action: MenuInputMapAction::MovementXDir(1),
},
],
gamepad: vec![
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::DPadUp),
action: MenuInputMapAction::MovementYDir(1),
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::DPadDown),
action: MenuInputMapAction::MovementYDir(-1),
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::DPadLeft),
action: MenuInputMapAction::MovementXDir(-1),
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::DPadRight),
action: MenuInputMapAction::MovementXDir(1),
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1),
action: MenuInputMapAction::MovementXDir(1),
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1),
action: MenuInputMapAction::MovementYDir(1),
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::South),
action: MenuInputMapAction::Accept,
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::West),
action: MenuInputMapAction::Back,
},
MenuGamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::Start),
action: MenuInputMapAction::Start,
},
],
})
.init_resource::<LocalInputAssignments>()
.add_plugins(gamecube::GamecubeAdapterPlugin)
.add_systems(ReadInputs, read_local_inputs)
.add_systems(
GgrsSchedule,
buffer::postprocess_input.in_set(GameplaySet::Input),
)
.rollback_component_with_clone::<FighterInput>()
.checksum_component_with_hash::<FighterInput>();
}
}
fn preprocess_input_frame(frame: &mut FighterInputFrame, game_settings: &GameSettings) {
if frame.movement.x.abs() < game_settings.input_common.stick_deadzone {
frame.movement.x = FGi32::ZERO;
}
if frame.movement.y.abs() < game_settings.input_common.stick_deadzone {
frame.movement.y = FGi32::ZERO;
}
}
fn read_local_inputs(
mut commands: Commands,
key: Res<ButtonInput<KeyCode>>,
input_map: Res<BaseInputMap>,
gc_ports: Res<gamecube::GcPorts>,
gamepads: Query<&Gamepad>,
assignments: Res<MatchInputAssignments>,
local_players: Res<LocalPlayers>,
game_settings: Res<GameSettings>,
) {
let mut local_inputs = HashMap::new();
for handle in &local_players.0 {
let assigned = assignments.get_for_handle(*handle);
let gamepad = match assigned {
Some(LocalInputSource::Gamepad(entity)) => gamepads.get(entity).ok(),
Some(LocalInputSource::Keyboard) => None,
None => gc_ports
.entities
.get(*handle)
.copied()
.flatten()
.and_then(|entity| gamepads.get(entity).ok()),
};
let mut input_frame = match assigned {
Some(LocalInputSource::Keyboard) => keyboard::sample_keyboard(&key, &input_map),
Some(LocalInputSource::Gamepad(_)) => gamepad
.map(|pad| gamepad::sample_gamepad(pad, &input_map))
.unwrap_or_default(),
None => FighterInputFrame::default(),
};
// preprocess frame
preprocess_input_frame(&mut input_frame, &game_settings);
local_inputs.insert(*handle, input_frame);
}
commands.insert_resource(LocalInputs::<GGRSCfg>(local_inputs));
}