From 77cd5033c3fe670f2550a834d5fd190bd5e693a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Tue, 15 Sep 2026 08:46:15 +0200 Subject: [PATCH] Add debug save states --- src/debug_tools.rs | 12 +++ src/debug_tools/save_state.rs | 196 ++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/debug_tools/save_state.rs diff --git a/src/debug_tools.rs b/src/debug_tools.rs index 28e27f2..29b226b 100644 --- a/src/debug_tools.rs +++ b/src/debug_tools.rs @@ -14,6 +14,8 @@ use enum_cycling_derive::EnumCycle; use crate::{AppState, player::Player, replay::StopReplayRecording}; +mod save_state; + pub const PLAYER_DEBUG_ROW_COUNT: usize = 6; #[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)] @@ -378,6 +380,9 @@ impl Plugin for DebugToolsPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() + .init_resource::() + .add_systems(OnEnter(AppState::InMatch), save_state::clear) + .add_systems(OnExit(AppState::InMatch), save_state::clear) .add_systems( PreUpdate, prepare_frame_step @@ -385,6 +390,13 @@ impl Plugin for DebugToolsPlugin { .before(RunGgrsSystems) .run_if(in_state(AppState::InMatch)), ) + .add_systems( + PreUpdate, + save_state::save_or_restore + .after(InputSystems) + .before(RunGgrsSystems) + .run_if(in_state(AppState::InMatch)), + ) .add_systems(PreUpdate, restore_frame_step_time.after(RunGgrsSystems)) .add_systems( Update, diff --git a/src/debug_tools/save_state.rs b/src/debug_tools/save_state.rs new file mode 100644 index 0000000..ec6fe33 --- /dev/null +++ b/src/debug_tools/save_state.rs @@ -0,0 +1,196 @@ +use bevy::prelude::*; + +use crate::{ + camera::MatchCamera, + fighter::{ + FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, + FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, + FighterVelocity, Grounded, + animation::FighterAnimationFrame, + attack::{ + FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, + FighterReceivedAttacks, FighterSolvedHurtboxes, FighterStaleMoveQueue, + }, + baked_animation::FighterBoneMatrices, + state::FighterState, + }, + input::FighterInput, + player::Player, + stage::line::StageCollision, +}; + +#[derive(Resource, Default)] +pub(super) struct DebugSaveState { + checkpoint: Option, +} + +#[derive(Clone)] +struct Checkpoint { + fighters: Vec, + stage_collision: StageCollision, +} + +#[derive(Clone)] +struct FighterCheckpoint { + entity: Entity, + handle: usize, + ecb: FighterECB, + previous_ecb: crate::fighter::ecb::FighterPreviousECB, + velocity: FighterVelocity, + translation: FighterTranslation, + previous_translation: FighterPreviousTranslation, + facing: FighterFacingDirection, + previous_facing: FighterPreviousFacingDirection, + animation: FighterAnimationFrame, + state: FighterState, + input: FighterInput, + damage: FighterDamage, + pending_hit_reaction: FighterPendingHitReaction, + hitstop: FighterHitstop, + knockback_velocity: FighterKnockbackVelocity, + stale_moves: FighterStaleMoveQueue, + hitboxes: FighterHitboxes, + received_attacks: FighterReceivedAttacks, + solved_hurtboxes: FighterSolvedHurtboxes, + bone_matrices: FighterBoneMatrices, + grounded: bool, +} + +pub(super) fn save_or_restore(world: &mut World) { + if world.resource::().menu_open { + return; + } + + let left = world + .query::<&Gamepad>() + .iter(world) + .any(|gamepad| gamepad.just_pressed(GamepadButton::DPadLeft)); + let right = world + .query::<&Gamepad>() + .iter(world) + .any(|gamepad| gamepad.just_pressed(GamepadButton::DPadRight)); + + if left == right { + return; + } + + let Some(roster) = world + .get_resource::() + .map(|roster| roster.0.clone()) + else { + return; + }; + if roster.len() != 1 { + warn!("Save states are only available in single-player matches"); + return; + } + + if left { + let Some(stage_collision) = world.get_resource::().cloned() else { + return; + }; + let mut checkpoint_fighters = Vec::with_capacity(roster.len()); + for entity in roster.iter().copied() { + let Some(checkpoint) = capture_fighter(world, entity) else { + warn!("Could not save the current fighter state"); + return; + }; + checkpoint_fighters.push(checkpoint); + } + world.resource_mut::().checkpoint = Some(Checkpoint { + fighters: checkpoint_fighters, + stage_collision, + }); + info!("Saved debug game state"); + return; + } + + let Some(checkpoint) = world.resource::().checkpoint.clone() else { + warn!("No debug game state has been saved"); + return; + }; + if checkpoint.fighters.len() != roster.len() + || checkpoint + .fighters + .iter() + .zip(roster.iter()) + .any(|(saved, current)| saved.entity != *current) + { + warn!("Saved debug game state belongs to another match"); + return; + } + + for saved in &checkpoint.fighters { + let Some(player) = world.entity(saved.entity).get::() else { + warn!("Could not restore the saved fighter state"); + return; + }; + if player.handle != saved.handle { + warn!("Saved debug game state belongs to another match"); + return; + } + + let mut entity = world.entity_mut(saved.entity); + entity.insert(saved.ecb); + entity.insert(saved.previous_ecb); + entity.insert(saved.velocity); + entity.insert(saved.translation); + entity.insert(saved.previous_translation); + entity.insert(saved.facing); + entity.insert(saved.previous_facing); + entity.insert(saved.animation); + entity.insert(saved.state.clone()); + entity.insert(saved.input); + entity.insert(saved.damage); + entity.insert(saved.pending_hit_reaction.clone()); + entity.insert(saved.hitstop); + entity.insert(saved.knockback_velocity); + entity.insert(saved.stale_moves.clone()); + entity.insert(saved.hitboxes.clone()); + entity.insert(saved.received_attacks.clone()); + entity.insert(saved.solved_hurtboxes.clone()); + entity.insert(saved.bone_matrices.clone()); + if saved.grounded { + entity.insert(Grounded); + } else { + entity.remove::(); + } + } + world.insert_resource(checkpoint.stage_collision); + for mut match_camera in world.query::<&mut MatchCamera>().iter_mut(world) { + match_camera.needs_snap = true; + } + info!("Restored debug game state"); +} + +fn capture_fighter(world: &World, entity: Entity) -> Option { + let fighter = world.entity(entity); + Some(FighterCheckpoint { + entity, + handle: fighter.get::()?.handle, + ecb: *fighter.get::()?, + previous_ecb: *fighter.get::()?, + velocity: *fighter.get::()?, + translation: *fighter.get::()?, + previous_translation: *fighter.get::()?, + facing: *fighter.get::()?, + previous_facing: *fighter.get::()?, + animation: *fighter.get::()?, + state: fighter.get::()?.clone(), + input: *fighter.get::()?, + damage: *fighter.get::()?, + pending_hit_reaction: fighter.get::()?.clone(), + hitstop: *fighter.get::()?, + knockback_velocity: *fighter.get::()?, + stale_moves: fighter.get::()?.clone(), + hitboxes: fighter.get::()?.clone(), + received_attacks: fighter.get::()?.clone(), + solved_hurtboxes: fighter.get::()?.clone(), + bone_matrices: fighter.get::()?.clone(), + grounded: fighter.contains::(), + }) +} + +pub(super) fn clear(mut save_state: ResMut) { + save_state.checkpoint = None; +}