Compare commits

..
3 Commits
Author SHA1 Message Date
EIREXE 856f65b5b5 Tighten hold buffer lifetime semantics
CI / build-and-test (pull_request) Successful in 2m18s
CI / production-build (pull_request) Successful in 11m5s
2026-09-29 22:00:54 +02:00
EIREXE 490a4885a4 Fix command persistence through jump squat
CI / production-build (pull_request) Successful in 10m57s
CI / build-and-test (pull_request) Successful in 2m8s
2026-09-29 21:55:16 +02:00
EIREXE 6bab92fe25 Add Ultimate-style input binding buffers
CI / build-and-test (pull_request) Successful in 2m19s
CI / production-build (pull_request) Successful in 11m44s
2026-09-29 19:59:50 +02:00
5 changed files with 42 additions and 40 deletions
+2 -5
View File
@@ -7,10 +7,9 @@ authors = [""]
[workspace] [workspace]
[features] [features]
default = ["development", "debug_features", "gamecube-adapter"] default = ["development", "debug_features"]
development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher"] development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher"]
debug_features = ["bevy/free_camera"] debug_features = ["bevy/free_camera"]
gamecube-adapter = ["dep:gc-adapter-neo"]
[dependencies] [dependencies]
bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] } bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] }
@@ -35,6 +34,7 @@ strum = "0.28.0"
strum_macros = "0.28.0" strum_macros = "0.28.0"
bevy_asset_loader = "0.27.0" bevy_asset_loader = "0.27.0"
ron_asset_manager = "0.9.0" ron_asset_manager = "0.9.0"
gc-adapter-neo = { version = "1.1.1", features = ["libusb"] }
rhai = "1.26.0" rhai = "1.26.0"
enum_cycling = "0.2.1" enum_cycling = "0.2.1"
enum_cycling_derive = "0.2.1" enum_cycling_derive = "0.2.1"
@@ -49,6 +49,3 @@ inherits = "release"
lto = "thin" lto = "thin"
codegen-units = 1 codegen-units = 1
strip = "symbols" strip = "symbols"
[target.'cfg(not(target_os = "windows"))'.dependencies]
gc-adapter-neo = { version = "1.1.1", features = ["libusb"], optional = true }
+4
View File
@@ -107,11 +107,15 @@ impl FighterStateImpl for JumpSquatState {
fn on_enter(&mut self, state_context: &mut EnterContext) { fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.play_animation(AnimKind::JumpSquat, false); state_context.play_animation(AnimKind::JumpSquat, false);
// Preserve buffered inputs while jump squat makes the transition
// temporarily unavailable, matching Ultimate/HDR-style hold input.
state_context.input.enable_hold_all(10); state_context.input.enable_hold_all(10);
state_context.input.clear_command(FighterCommands::Jump); state_context.input.clear_command(FighterCommands::Jump);
} }
fn on_exit(&mut self, state_context: &mut crate::fighter::state::ExitContext) { fn on_exit(&mut self, state_context: &mut crate::fighter::state::ExitContext) {
// The hold policy is state-scoped and must never leak into the next
// state. Command lifetime itself remains independent of this policy.
state_context.input.disable_hold_all(); state_context.input.disable_hold_all();
} }
+10 -1
View File
@@ -1,4 +1,9 @@
//! Rollback-safe input history and command buffering. //! Rollback-safe input history and command buffering.
//!
//! Raw mapped bindings, button edges, and command lifetimes are deliberately
//! separate. This preserves combinations such as pressing a second shield
//! binding while the first one is held, while still allowing a command to
//! survive after its source button is released.
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ggrs::{PlayerInputs, Rollback}; use bevy_ggrs::{PlayerInputs, Rollback};
@@ -271,12 +276,14 @@ impl FighterInput {
.saturating_sub(1); .saturating_sub(1);
} }
/// Holds all inputs for n frames
pub fn enable_hold_all(&mut self, max_frames: u16) { pub fn enable_hold_all(&mut self, max_frames: u16) {
self.hold_all = HoldLimit::from_max_frames(max_frames); self.hold_all = HoldLimit::from_max_frames(max_frames);
self.latch_active_commands(); self.latch_active_commands();
} }
/// Stops enrolling commands in the global hold buffer. Commands already
/// latched retain their remaining lifetime so they can cross a state
/// boundary, such as jump squat into the first airborne frame.
pub fn disable_hold_all(&mut self) { pub fn disable_hold_all(&mut self) {
self.hold_all = HoldLimit::Disabled; self.hold_all = HoldLimit::Disabled;
} }
@@ -546,6 +553,8 @@ pub fn postprocess_input(
input.set_attack_command_type(attack_type); input.set_attack_command_type(attack_type);
} }
// Advance each command's independently latched persistence. Physical
// source state is intentionally irrelevant after recognition.
for command in FighterCommands::iter() { for command in FighterCommands::iter() {
input.update_hold(command); input.update_hold(command);
} }
+10 -2
View File
@@ -7,8 +7,7 @@ use bevy::input::gamepad::{
}; };
use bevy::prelude::*; use bevy::prelude::*;
use super::GcPorts; use super::poll::{GcAdapterSnapshot, GcPortState};
use super::poll::GcAdapterSnapshot;
/// GameCube adapters report 4 ports. /// GameCube adapters report 4 ports.
pub const GC_PORT_COUNT: usize = 4; pub const GC_PORT_COUNT: usize = 4;
@@ -24,6 +23,15 @@ const GC_ADAPTER_PID: u16 = 0x0337;
#[derive(Component, Debug, Clone, Copy)] #[derive(Component, Debug, Clone, Copy)]
pub struct GcPort(pub u8); 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 { fn passthrough_gamepad_settings() -> GamepadSettings {
let mut settings = GamepadSettings::default(); let mut settings = GamepadSettings::default();
for axis in GamepadAxis::all() { for axis in GamepadAxis::all() {
+1 -17
View File
@@ -1,23 +1,10 @@
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub mod bridge; pub mod bridge;
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub mod poll; pub mod poll;
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))] pub use bridge::{GC_PORT_COUNT, GcPort, GcPorts};
pub use bridge::{GC_PORT_COUNT, GcPort};
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub use poll::{GcAdapterSnapshot, GcPortState}; pub use poll::{GcAdapterSnapshot, GcPortState};
#[derive(Resource, Default)]
pub struct GcPorts {
pub entities: [Option<Entity>; 4],
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub(super) last: [poll::GcPortState; 4],
}
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
use bevy::input::InputSystems; use bevy::input::InputSystems;
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system}; use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system};
use bevy::prelude::*; use bevy::prelude::*;
@@ -28,8 +15,6 @@ impl Plugin for GamecubeAdapterPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<GcPorts>(); app.init_resource::<GcPorts>();
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
{
let Ok(snapshot) = poll::spawn_poll_thread() else { let Ok(snapshot) = poll::spawn_poll_thread() else {
// No adapter connected; leave GcPorts empty and skip wiring the // No adapter connected; leave GcPorts empty and skip wiring the
// polling systems. // polling systems.
@@ -47,5 +32,4 @@ impl Plugin for GamecubeAdapterPlugin {
.in_set(InputSystems), .in_set(InputSystems),
); );
} }
}
} }