From f3701bd8b072f521426b227a50c6ee78c2b0fc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Fri, 11 Sep 2026 22:01:03 +0200 Subject: [PATCH] Formatting --- src/debug_tools.rs | 2 +- src/fighter/animation.rs | 2 +- src/fighter/attack.rs | 56 +++++---- src/fighter/collision.rs | 34 ++--- src/fighter/hurtbox.rs | 13 +- src/fighter/mod.rs | 17 ++- src/fighter/state/ground_attack.rs | 5 +- src/game_settings.rs | 3 +- src/input/buffer.rs | 22 ++-- src/input/gamecube/bridge.rs | 20 +-- src/input/gamecube/mod.rs | 4 +- src/input/gamecube/poll.rs | 13 +- src/input/gamepad.rs | 14 ++- src/input/keyboard.rs | 1 - src/input/mod.rs | 125 ++++++++++++++----- src/math/segment.rs | 1 - src/replay.rs | 191 +++++++++++++++++++++++------ 17 files changed, 372 insertions(+), 151 deletions(-) diff --git a/src/debug_tools.rs b/src/debug_tools.rs index 78d0816..52b6d6e 100644 --- a/src/debug_tools.rs +++ b/src/debug_tools.rs @@ -11,7 +11,7 @@ use bevy_ggrs::RollbackFrameRate; use enum_cycling::EnumCycle; use enum_cycling_derive::EnumCycle; -use crate::{player::Player, replay::StopReplayRecording, AppState}; +use crate::{AppState, player::Player, replay::StopReplayRecording}; pub const PLAYER_DEBUG_ROW_COUNT: usize = 6; diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index 95c9dba..f17bb3a 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -64,7 +64,7 @@ pub fn apply_animation( } #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, EnumIter + Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, EnumIter, )] pub enum AnimKind { Wait, diff --git a/src/fighter/attack.rs b/src/fighter/attack.rs index 1d01133..cf1d274 100644 --- a/src/fighter/attack.rs +++ b/src/fighter/attack.rs @@ -6,11 +6,18 @@ use serde::{Deserialize, Serialize}; use crate::{ fighter::{ - Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation, animation::{AnimKind, FighterAnimationFrame}, baked_animation::{FighterBoneMatrices, FixedMat4}, hurtbox::{FixedAffineCapsule, FixedCapsule}, manifest::FighterManifest, - }, math::{ + Fighter, FighterAttributes, FighterFacingDirection, FighterHitboxes, FighterTranslation, + animation::{AnimKind, FighterAnimationFrame}, + baked_animation::{FighterBoneMatrices, FixedMat4}, + hurtbox::{FixedAffineCapsule, FixedCapsule}, + manifest::FighterManifest, + }, + math::{ int::{FGi32, FGi32Ext}, vec3::FGVec3, - }, schedule::GameplaySet, scripting::FighterAttackScript, + }, + schedule::GameplaySet, + scripting::FighterAttackScript, }; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct FighterDamage(FGi32); @@ -207,18 +214,23 @@ pub fn solve_hitboxes( &FighterTranslation, &FighterFacingDirection, &mut FighterHitboxes, - &FighterAnimationFrame + &FighterAnimationFrame, )>, attack_scripts: Res>, ) { for (matrices, translation, facing_direction, mut hitboxes, animation_frame) in &mut query { hitboxes.active_hitboxes_solved.clear(); if let Some(ref attack_script) = hitboxes.attack_script { - let attack_script = attack_scripts.get(attack_script).expect("Attack script should be valid"); + let attack_script = attack_scripts + .get(attack_script) + .expect("Attack script should be valid"); let fighter_trf = translation.get_3d_transform(facing_direction); let active_hitboxes = hitboxes.active_hitboxes.clone(); for hitbox_idx in active_hitboxes { - let hitbox = attack_script.hitboxes.get(hitbox_idx).expect("Hitbox IDX should be valid"); + let hitbox = attack_script + .hitboxes + .get(hitbox_idx) + .expect("Hitbox IDX should be valid"); let matrices = matrices.get(&hitbox.bone); if let None = matrices { @@ -233,23 +245,19 @@ pub fn solve_hitboxes( if hitbox.start_frame == animation_frame.frame { // Sphere only - hitboxes.active_hitboxes_solved.push( - FixedCapsule { - start: curr_hbox_pos, - end: curr_hbox_pos, - radius: hitbox.radius - } - ) + hitboxes.active_hitboxes_solved.push(FixedCapsule { + start: curr_hbox_pos, + end: curr_hbox_pos, + radius: hitbox.radius, + }) } else { let prev_hbox_matrix = fighter_trf.mul(prev_bone_matrix); let prev_hbox_pos = prev_hbox_matrix.transform_point(hitbox.offset); - hitboxes.active_hitboxes_solved.push( - FixedCapsule { - start: prev_hbox_pos, - end: curr_hbox_pos, - radius: hitbox.radius - } - ); + hitboxes.active_hitboxes_solved.push(FixedCapsule { + start: prev_hbox_pos, + end: curr_hbox_pos, + radius: hitbox.radius, + }); } } } @@ -303,7 +311,10 @@ pub fn solve_hurtboxes( } } -pub fn intersect_attacks_with_hurtboxes(query: Query<(Entity, &FighterSolvedHurtboxes, &FighterHitboxes)>, attack_scripts: Res>) { +pub fn intersect_attacks_with_hurtboxes( + query: Query<(Entity, &FighterSolvedHurtboxes, &FighterHitboxes)>, + attack_scripts: Res>, +) { /*for (entity_hurting, _, hitboxes) in query { if hitboxes.attack_script.is_none() { continue; @@ -338,7 +349,8 @@ impl Plugin for FighterAttackPlugin { fn build(&self, app: &mut App) { app.add_systems( GgrsSchedule, - (solve_hurtboxes, solve_hitboxes).chain() + (solve_hurtboxes, solve_hitboxes) + .chain() .after(crate::fighter::baked_animation::update_fighter_bone_matrices) .in_set(GameplaySet::Animation), ); diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs index d485a80..827be14 100644 --- a/src/fighter/collision.rs +++ b/src/fighter/collision.rs @@ -49,23 +49,27 @@ pub fn air_collide_with_stage( for (poly_idx, poly) in state_context.stage_collision.stage_polys.iter().enumerate() { if let Some(intersect_result) = poly.intersect_ray(ray_segment) { - intersect_results.push((AirCollisionWithStageResult { - line_id: { - StageLineID { - polygon: poly_idx, - segment: intersect_result.segment_idx, - } + intersect_results.push(( + AirCollisionWithStageResult { + line_id: { + StageLineID { + polygon: poly_idx, + segment: intersect_result.segment_idx, + } + }, + hit_position: intersect_result.position, + hit_normal: intersect_result.normal, }, - hit_position: intersect_result.position, - hit_normal: intersect_result.normal, - }, intersect_result.position.distance_squared(ray_segment.point2()))); + intersect_result + .position + .distance_squared(ray_segment.point2()), + )); } } - intersect_results.sort_by(| (_, a), (_, b) | { - a.cmp(b) - }); + intersect_results.sort_by(|(_, a), (_, b)| a.cmp(b)); - intersect_results.iter().last().map(|(val, _)| { - val.to_owned().clone() - }) + intersect_results + .iter() + .last() + .map(|(val, _)| val.to_owned().clone()) } diff --git a/src/fighter/hurtbox.rs b/src/fighter/hurtbox.rs index e0d6890..6f2b221 100644 --- a/src/fighter/hurtbox.rs +++ b/src/fighter/hurtbox.rs @@ -73,7 +73,6 @@ impl FixedAffineCapsule { // current simplex to fixed-point precision, so conservatively collide. true } - } trait SupportMap { @@ -243,18 +242,10 @@ mod tests { #[test] fn affine_scale_changes_capsule_intersection() { - let sphere = FixedAffineCapsule::new( - FixedMat4::IDENTITY, - FGi32::ZERO, - FGi32::lit("1"), - ); + let sphere = FixedAffineCapsule::new(FixedMat4::IDENTITY, FGi32::ZERO, FGi32::lit("1")); let mut squashed_transform = FixedMat4::IDENTITY; squashed_transform.cols[1][1] = FGi32::lit("0.5"); - let squashed = FixedAffineCapsule::new( - squashed_transform, - FGi32::ZERO, - FGi32::lit("1"), - ); + let squashed = FixedAffineCapsule::new(squashed_transform, FGi32::ZERO, FGi32::lit("1")); let target = FixedCapsule::new( FGVec3::lit("0", "1.4", "0"), FGVec3::lit("0", "1.4", "0"), diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 0ea3bb8..6ac4e19 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -30,8 +30,19 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity use crate::{ fighter::{ - attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes}, baked_animation::FighterBoneMatrices, hurtbox::FixedCapsule, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations, - }, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision, + attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes}, + baked_animation::FighterBoneMatrices, + hurtbox::FixedCapsule, + manifest::FighterManifest, + state::{FighterState, fall::FallState}, + visual::FighterAnimations, + }, + input::FighterInput, + math::{int::FGi32, vec::FGVec2}, + player::Player, + schedule::GameplaySet, + scripting::FighterAttackScript, + stage::line::StageCollision, }; use state::state_interrupt_system; @@ -221,7 +232,7 @@ pub fn spawn_fighter( FighterHitboxes { attack_script: None, active_hitboxes: vec![], - active_hitboxes_solved: vec![] + active_hitboxes_solved: vec![], }, ), ( diff --git a/src/fighter/state/ground_attack.rs b/src/fighter/state/ground_attack.rs index be6aa81..2b52ee4 100644 --- a/src/fighter/state/ground_attack.rs +++ b/src/fighter/state/ground_attack.rs @@ -71,7 +71,10 @@ pub fn check_interrupt( .input .has_command(crate::input::FighterCommands::Attack) { - let attack_kind = state_context.input.get_attack_command_type().to_attack_kind_grounded(); + let attack_kind = state_context + .input + .get_attack_command_type() + .to_attack_kind_grounded(); Some(FighterState::GroundAttack(GroundAttackState { attack_kind, grounded_common: grounded_common.clone(), diff --git a/src/game_settings.rs b/src/game_settings.rs index c090379..ce1a838 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use bevy_asset_loader::asset_collection::AssetCollection; +use ron_asset_manager::prelude::RonAsset; use serde::{Deserialize, Serialize}; use crate::{math::int::FGi32, menus::style::FGUiStyle}; @@ -23,7 +24,7 @@ pub struct InputSettingsCommon { pub input_buffer_size: u8, } -#[derive(Reflect, Serialize, Deserialize, Resource, Asset, Clone)] +#[derive(Reflect, Serialize, Deserialize, Resource, Asset, Clone, RonAsset)] pub struct GameSettings { pub fighter_common: FighterSettingsCommon, pub input_common: InputSettingsCommon, diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 82c5241..ea12d6f 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -177,7 +177,8 @@ pub fn postprocess_input( { input.frames_in_x_flick_deadzone = 1; } else { - input.frames_in_x_flick_deadzone = input.frames_in_x_flick_deadzone.saturating_add(1); + input.frames_in_x_flick_deadzone = + input.frames_in_x_flick_deadzone.saturating_add(1); } } @@ -190,7 +191,8 @@ pub fn postprocess_input( { input.frames_in_y_flick_deadzone = 1; } else { - input.frames_in_y_flick_deadzone = input.frames_in_y_flick_deadzone.saturating_add(1); + input.frames_in_y_flick_deadzone = + input.frames_in_y_flick_deadzone.saturating_add(1); } } @@ -228,9 +230,11 @@ pub fn postprocess_input( input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size); } - let mut y_flick_detected = y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold; - let mut x_flick_detected = x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold; - + let mut y_flick_detected = + y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold; + let mut x_flick_detected = + x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold; + // if both flicks are detected, only one wins if x_flick_detected && y_flick_detected { @@ -239,7 +243,7 @@ pub fn postprocess_input( } let attack_detected = input.current_frame.attack && !input.prev_frame.attack; - + // Tilt/smash up/down if attack_detected { let attack_type = if y_flick_detected { @@ -270,11 +274,13 @@ pub fn postprocess_input( FighterAttackCommandType::Neutral }; - input.set_lifetime(FighterCommands::Attack, game_settings.input_common.input_buffer_size); + input.set_lifetime( + FighterCommands::Attack, + game_settings.input_common.input_buffer_size, + ); input.set_attack_command_type(attack_type); } - if x_flick_detected { input.clear_command(FighterCommands::SmashMoveLeft); input.clear_command(FighterCommands::SmashMoveRight); diff --git a/src/input/gamecube/bridge.rs b/src/input/gamecube/bridge.rs index 8295700..e5fb9dc 100644 --- a/src/input/gamecube/bridge.rs +++ b/src/input/gamecube/bridge.rs @@ -149,19 +149,19 @@ pub fn gc_event_system( 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 - ); + 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)); + send_button!( + GamepadButton::LeftTrigger2, + unsigned_axis_float(now.triggers.0) + ); + send_button!( + GamepadButton::RightTrigger2, + unsigned_axis_float(now.triggers.1) + ); } macro_rules! digital { diff --git a/src/input/gamecube/mod.rs b/src/input/gamecube/mod.rs index a8c2f84..35ac83c 100644 --- a/src/input/gamecube/mod.rs +++ b/src/input/gamecube/mod.rs @@ -1,11 +1,11 @@ pub mod bridge; pub mod poll; -pub use bridge::{GcPort, GcPorts, GC_PORT_COUNT}; +pub use bridge::{GC_PORT_COUNT, GcPort, GcPorts}; pub use poll::{GcAdapterSnapshot, GcPortState}; -use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system}; use bevy::input::InputSystems; +use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system}; use bevy::prelude::*; #[derive(Default)] diff --git a/src/input/gamecube/poll.rs b/src/input/gamecube/poll.rs index ed77502..87c82cb 100644 --- a/src/input/gamecube/poll.rs +++ b/src/input/gamecube/poll.rs @@ -98,12 +98,9 @@ pub fn spawn_poll_thread() -> Result Result controllers, @@ -142,7 +138,8 @@ pub fn spawn_poll_thread() -> Result FighterInputFr // 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 }, + 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 @@ -32,7 +40,7 @@ pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFr InputMapAction::MovementXDir(_) => movement.x += value, InputMapAction::MovementYDir(_) => movement.y += value, _ => {} - } + } } } } diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index 34edd92..dffa7b6 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -33,7 +33,6 @@ pub fn sample_keyboard(key: &ButtonInput, input_map: &BaseInputMap) -> InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO, InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO, InputMapAction::Attack => input_frame.attack = action_strength != FGi32::ZERO, - } } diff --git a/src/input/mod.rs b/src/input/mod.rs index d509ab3..b7cc7b7 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -41,6 +41,10 @@ impl LocalInputAssignments { } None } + + pub fn get_from_slot(&self, slot: usize) -> Option<&LocalInputSource> { + self.0.iter().find(|(c_slot, _)| slot == *c_slot).map(|(_, source)| source) + } } use crate::game_settings::GameSettings; @@ -92,8 +96,6 @@ impl Plugin for FighterInputPlugin { key: KeyCode::KeyJ, action: InputMapAction::Attack, }, - - // Menu ], gamepad: vec![ @@ -125,33 +127,92 @@ impl Plugin for FighterInputPlugin { binding: GamepadBinding::Button(GamepadButton::East), action: InputMapAction::Attack, }, - ], }) .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) }, + 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 }, + 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::() @@ -183,16 +244,24 @@ fn read_local_inputs( gamepads: Query<&Gamepad>, assignments: Res, local_players: Res, - game_settings: Res + game_settings: Res, ) { let mut local_inputs = HashMap::new(); for handle in &local_players.0 { - let assigned = assignments.0.iter().find(|(player_handle, _)| player_handle == handle); + let assigned = assignments + .0 + .iter() + .find(|(player_handle, _)| player_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()), + None => gc_ports + .entities + .get(*handle) + .copied() + .flatten() + .and_then(|entity| gamepads.get(entity).ok()), }; let mut input_frame = match assigned { @@ -201,8 +270,8 @@ fn read_local_inputs( .map(|pad| gamepad::sample_gamepad(pad, &input_map)) .unwrap_or_default(), None => match gamepad { - Some(pad) => gamepad::sample_gamepad(pad, &input_map), - None => keyboard::sample_keyboard(&key, &input_map), + Some(pad) => gamepad::sample_gamepad(pad, &input_map), + None => keyboard::sample_keyboard(&key, &input_map), }, }; diff --git a/src/math/segment.rs b/src/math/segment.rs index 207ed9d..d66cfb1 100644 --- a/src/math/segment.rs +++ b/src/math/segment.rs @@ -1,4 +1,3 @@ - use bevy::prelude::*; use crate::math::{ diff --git a/src/replay.rs b/src/replay.rs index 90b322c..0045415 100644 --- a/src/replay.rs +++ b/src/replay.rs @@ -1,13 +1,24 @@ //! Deterministic input recording and playback. -use std::{collections::BTreeMap, fs, path::{Path, PathBuf}}; +use std::{ + collections::BTreeMap, + fs, + path::{Path, PathBuf}, +}; use bevy::prelude::*; -use bevy_ggrs::{prelude::*, PlayerInputs, RollbackFrameCount, SaveWorld}; +use bevy_ggrs::{PlayerInputs, RollbackFrameCount, SaveWorld, prelude::*}; use clap::ValueEnum; use serde::{Deserialize, Serialize}; -use crate::{args::Args, input::FighterInputFrame, match_loading::{MatchPlayer, PendingMatch}, netcode::GGRSCfg, schedule::GameplaySet, AppState}; +use crate::{ + AppState, + args::Args, + input::FighterInputFrame, + match_loading::{MatchPlayer, PendingMatch}, + netcode::GGRSCfg, + schedule::GameplaySet, +}; const MAGIC: &[u8; 8] = b"SPRPLY01"; pub const REPLAY_VERSION: u32 = 1; @@ -79,7 +90,12 @@ impl Plugin for ReplayPlugin { .in_set(GameplaySet::Input) .before(crate::input::buffer::postprocess_input), ) - .add_systems(SaveWorld, record_checksum.after(SaveWorldSystems::Checksum).before(SaveWorldSystems::Snapshot)) + .add_systems( + SaveWorld, + record_checksum + .after(SaveWorldSystems::Checksum) + .before(SaveWorldSystems::Snapshot), + ) .add_systems(OnExit(AppState::InMatch), finish_recording) .add_systems(Update, stop_recording.run_if(in_state(AppState::InMatch))) .add_systems(Update, finish_playback.run_if(in_state(AppState::InMatch))); @@ -87,7 +103,8 @@ impl Plugin for ReplayPlugin { } pub fn load(path: &Path) -> Result { - let bytes = fs::read(path).map_err(|error| format!("cannot read replay {}: {error}", path.display()))?; + let bytes = fs::read(path) + .map_err(|error| format!("cannot read replay {}: {error}", path.display()))?; let replay = if bytes.starts_with(MAGIC) { if bytes.len() < MAGIC.len() + 4 { return Err("replay binary header is truncated".into()); @@ -96,7 +113,8 @@ pub fn load(path: &Path) -> Result { if version != REPLAY_VERSION { return Err(format!("unsupported replay version {version}")); } - bincode::deserialize(&bytes[12..]).map_err(|error| format!("invalid binary replay: {error}"))? + bincode::deserialize(&bytes[12..]) + .map_err(|error| format!("invalid binary replay: {error}"))? } else { serde_json::from_slice(&bytes).map_err(|error| format!("invalid JSON replay: {error}"))? }; @@ -110,10 +128,14 @@ pub fn save(path: &Path, format: ReplayFormat, replay: &Replay) -> Result<(), St ReplayFormat::Binary => { let mut bytes = MAGIC.to_vec(); bytes.extend(REPLAY_VERSION.to_le_bytes()); - bytes.extend(bincode::serialize(replay).map_err(|error| format!("cannot encode replay: {error}"))?); + bytes.extend( + bincode::serialize(replay) + .map_err(|error| format!("cannot encode replay: {error}"))?, + ); bytes } - ReplayFormat::Json => serde_json::to_vec_pretty(replay).map_err(|error| format!("cannot encode replay JSON: {error}"))?, + ReplayFormat::Json => serde_json::to_vec_pretty(replay) + .map_err(|error| format!("cannot encode replay JSON: {error}"))?, }; let temporary = path.with_extension("replay.tmp"); fs::write(&temporary, bytes).map_err(|error| format!("cannot write replay: {error}"))?; @@ -122,7 +144,10 @@ pub fn save(path: &Path, format: ReplayFormat, replay: &Replay) -> Result<(), St fn validate(replay: &Replay) -> Result<(), String> { if replay.replay_version != REPLAY_VERSION { - return Err(format!("unsupported replay version {}", replay.replay_version)); + return Err(format!( + "unsupported replay version {}", + replay.replay_version + )); } if replay.players_are_invalid() { return Err("replay player handles must cover 0..player_count".into()); @@ -130,10 +155,18 @@ fn validate(replay: &Replay) -> Result<(), String> { if replay.frames.is_empty() { return Err("replay must contain at least one frame".into()); } - if replay.frames.windows(2).any(|frames| frames[0].frame >= frames[1].frame) { + if replay + .frames + .windows(2) + .any(|frames| frames[0].frame >= frames[1].frame) + { return Err("replay frames must be strictly ordered".into()); } - if replay.frames.iter().any(|frame| frame.inputs.len() != replay.match_config.players.len()) { + if replay + .frames + .iter() + .any(|frame| frame.inputs.len() != replay.match_config.players.len()) + { return Err("replay frame input count does not match player count".into()); } Ok(()) @@ -141,7 +174,12 @@ fn validate(replay: &Replay) -> Result<(), String> { impl Replay { fn players_are_invalid(&self) -> bool { - let mut handles: Vec<_> = self.match_config.players.iter().map(|player| player.handle).collect(); + let mut handles: Vec<_> = self + .match_config + .players + .iter() + .map(|player| player.handle) + .collect(); handles.sort_unstable(); handles != (0..handles.len()).collect::>() } @@ -149,23 +187,44 @@ impl Replay { fn start_replay(mut commands: Commands, args: Res, loaded: Option>) { let mode = if let Some(replay) = loaded { - ReplayMode::Playback { replay: replay.0.clone(), mismatch: false } + ReplayMode::Playback { + replay: replay.0.clone(), + mismatch: false, + } } else if let Some(path) = args.record_replay.clone() { - ReplayMode::Record { path, format: args.replay_format } + ReplayMode::Record { + path, + format: args.replay_format, + } } else { return; }; - commands.insert_resource(ReplayRuntime { mode, frames: BTreeMap::new(), checksums: BTreeMap::new(), finished: false }); + commands.insert_resource(ReplayRuntime { + mode, + frames: BTreeMap::new(), + checksums: BTreeMap::new(), + finished: false, + }); } -fn replay_inputs(mut inputs: ResMut>, frame: Res, runtime: Option>) { +fn replay_inputs( + mut inputs: ResMut>, + frame: Res, + runtime: Option>, +) { let Some(mut runtime) = runtime else { return }; match &mut runtime.mode { ReplayMode::Record { .. } => { - runtime.frames.insert(frame.0, inputs.iter().map(|(input, _)| *input).collect()); + runtime + .frames + .insert(frame.0, inputs.iter().map(|(input, _)| *input).collect()); } ReplayMode::Playback { replay, mismatch } if !*mismatch => { - let Some(recorded) = replay.frames.iter().find(|recorded| recorded.frame == frame.0) else { + let Some(recorded) = replay + .frames + .iter() + .find(|recorded| recorded.frame == frame.0) + else { error!("Replay has no input for frame {}", frame.0); *mismatch = true; return; @@ -178,14 +237,26 @@ fn replay_inputs(mut inputs: ResMut>, frame: Res, checksum: Res, runtime: Option>) { +fn record_checksum( + frame: Res, + checksum: Res, + runtime: Option>, +) { let Some(mut runtime) = runtime else { return }; if matches!(&runtime.mode, ReplayMode::Record { .. }) { runtime.checksums.insert(frame.0, checksum.0); } else if let ReplayMode::Playback { replay, mismatch } = &mut runtime.mode { - if let Some(expected) = replay.checksums.iter().find(|value| value.frame == frame.0).map(|value| value.checksum) { + if let Some(expected) = replay + .checksums + .iter() + .find(|value| value.frame == frame.0) + .map(|value| value.checksum) + { if expected != checksum.0 { - error!("Replay desync at frame {}: expected {:X}, got {:X}", frame.0, expected, checksum.0); + error!( + "Replay desync at frame {}: expected {:X}, got {:X}", + frame.0, expected, checksum.0 + ); *mismatch = true; } } @@ -205,7 +276,11 @@ fn stop_recording( finish_recording_inner(&mut commands, runtime, request); } -fn finish_recording(mut commands: Commands, runtime: Option>, request: Option>) { +fn finish_recording( + mut commands: Commands, + runtime: Option>, + request: Option>, +) { finish_recording_inner(&mut commands, runtime, request); } @@ -215,27 +290,59 @@ fn finish_recording_inner( request: Option>, ) { let Some(mut runtime) = runtime else { return }; - let ReplayMode::Record { path, format } = &runtime.mode else { return }; - if runtime.finished || runtime.frames.is_empty() { return; } + let ReplayMode::Record { path, format } = &runtime.mode else { + return; + }; + if runtime.finished || runtime.frames.is_empty() { + return; + } let Some(request) = request else { return }; let path = path.clone(); let format = *format; let replay = Replay { replay_version: REPLAY_VERSION, game_version: env!("CARGO_PKG_VERSION").into(), - match_config: ReplayMatch { stage: request.stage, players: request.players.clone() }, - frames: runtime.frames.iter().map(|(&frame, inputs)| ReplayFrame { frame, inputs: inputs.clone() }).collect(), - checksums: runtime.checksums.iter().map(|(&frame, &checksum)| ReplayChecksum { frame, checksum }).collect(), + match_config: ReplayMatch { + stage: request.stage, + players: request.players.clone(), + }, + frames: runtime + .frames + .iter() + .map(|(&frame, inputs)| ReplayFrame { + frame, + inputs: inputs.clone(), + }) + .collect(), + checksums: runtime + .checksums + .iter() + .map(|(&frame, &checksum)| ReplayChecksum { frame, checksum }) + .collect(), }; - if let Err(error) = save(&path, format, &replay) { error!("Failed to save replay: {error}"); } + if let Err(error) = save(&path, format, &replay) { + error!("Failed to save replay: {error}"); + } runtime.finished = true; commands.remove_resource::(); } -fn finish_playback(mut commands: Commands, mut next_state: ResMut>, frame: Res, runtime: Option>) { +fn finish_playback( + mut commands: Commands, + mut next_state: ResMut>, + frame: Res, + runtime: Option>, +) { let Some(runtime) = runtime else { return }; - let ReplayMode::Playback { replay, mismatch } = &runtime.mode else { return }; - if *mismatch || replay.frames.last().is_some_and(|last| frame.0 >= last.frame) { + let ReplayMode::Playback { replay, mismatch } = &runtime.mode else { + return; + }; + if *mismatch + || replay + .frames + .last() + .is_some_and(|last| frame.0 >= last.frame) + { commands.remove_resource::(); commands.remove_resource::(); commands.insert_resource(ReplayComplete); @@ -246,7 +353,12 @@ fn finish_playback(mut commands: Commands, mut next_state: ResMut Replay { Replay { @@ -254,13 +366,22 @@ mod tests { game_version: "test".into(), match_config: ReplayMatch { stage: StageId::TestStage, - players: vec![MatchPlayer { handle: 0, fighter: FighterId::TestFighter }], + players: vec![MatchPlayer { + handle: 0, + fighter: FighterId::TestFighter, + }], }, frames: vec![ReplayFrame { frame: 1, - inputs: vec![FighterInputFrame { movement: FGVec2::new(FGi32::from_num(1), FGi32::ZERO), ..default() }], + inputs: vec![FighterInputFrame { + movement: FGVec2::new(FGi32::from_num(1), FGi32::ZERO), + ..default() + }], + }], + checksums: vec![ReplayChecksum { + frame: 1, + checksum: 42, }], - checksums: vec![ReplayChecksum { frame: 1, checksum: 42 }], } }