Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77cd5033c3 | ||
|
|
cd9c39b70c |
@@ -10,7 +10,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Check out source
|
- name: Check out source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
+110
-3
@@ -3,16 +3,19 @@
|
|||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
|
input::InputSystems,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
||||||
};
|
};
|
||||||
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
||||||
use bevy_ggrs::RollbackFrameRate;
|
use bevy_ggrs::{RollbackFrameRate, RunGgrsSystems};
|
||||||
use enum_cycling::EnumCycle;
|
use enum_cycling::EnumCycle;
|
||||||
use enum_cycling_derive::EnumCycle;
|
use enum_cycling_derive::EnumCycle;
|
||||||
|
|
||||||
use crate::{AppState, player::Player, replay::StopReplayRecording};
|
use crate::{AppState, player::Player, replay::StopReplayRecording};
|
||||||
|
|
||||||
|
mod save_state;
|
||||||
|
|
||||||
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
|
||||||
@@ -114,6 +117,7 @@ enum DebugFlag {
|
|||||||
Hurtboxes,
|
Hurtboxes,
|
||||||
Camera,
|
Camera,
|
||||||
DrawThroughGeometry,
|
DrawThroughGeometry,
|
||||||
|
FrameStepping,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -174,6 +178,10 @@ const WORLD_MENU: &[DebugMenuEntry] = &[
|
|||||||
DebugMenuEntry::GameSpeed {
|
DebugMenuEntry::GameSpeed {
|
||||||
label: "Game speed",
|
label: "Game speed",
|
||||||
},
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Frame stepping",
|
||||||
|
flag: DebugFlag::FrameStepping,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
||||||
@@ -233,6 +241,8 @@ pub struct DebugSettings {
|
|||||||
selections: Vec<usize>,
|
selections: Vec<usize>,
|
||||||
#[reflect(ignore)]
|
#[reflect(ignore)]
|
||||||
pub player_rows: HashMap<usize, [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]>,
|
pub player_rows: HashMap<usize, [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]>,
|
||||||
|
#[reflect(ignore)]
|
||||||
|
pub frame_stepping: bool,
|
||||||
pub network: bool,
|
pub network: bool,
|
||||||
pub ecb: bool,
|
pub ecb: bool,
|
||||||
pub stage_collision: bool,
|
pub stage_collision: bool,
|
||||||
@@ -253,6 +263,7 @@ impl Default for DebugSettings {
|
|||||||
pages: vec![DebugMenuPage::Static(ROOT_MENU)],
|
pages: vec![DebugMenuPage::Static(ROOT_MENU)],
|
||||||
selections: vec![0],
|
selections: vec![0],
|
||||||
player_rows: HashMap::new(),
|
player_rows: HashMap::new(),
|
||||||
|
frame_stepping: false,
|
||||||
network: false,
|
network: false,
|
||||||
ecb: false,
|
ecb: false,
|
||||||
stage_collision: false,
|
stage_collision: false,
|
||||||
@@ -325,6 +336,7 @@ impl DebugSettings {
|
|||||||
DebugFlag::Hurtboxes => self.hurtboxes,
|
DebugFlag::Hurtboxes => self.hurtboxes,
|
||||||
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
||||||
DebugFlag::Camera => self.camera,
|
DebugFlag::Camera => self.camera,
|
||||||
|
DebugFlag::FrameStepping => self.frame_stepping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +352,7 @@ impl DebugSettings {
|
|||||||
self.draw_through_geometry = !self.draw_through_geometry
|
self.draw_through_geometry = !self.draw_through_geometry
|
||||||
}
|
}
|
||||||
DebugFlag::Camera => self.camera = !self.camera,
|
DebugFlag::Camera => self.camera = !self.camera,
|
||||||
|
DebugFlag::FrameStepping => self.frame_stepping = !self.frame_stepping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,9 +369,35 @@ impl DebugSettings {
|
|||||||
|
|
||||||
pub struct DebugToolsPlugin;
|
pub struct DebugToolsPlugin;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct FrameStepState {
|
||||||
|
requested: bool,
|
||||||
|
time_backup: Option<Time>,
|
||||||
|
last_step_time: Time
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for DebugToolsPlugin {
|
impl Plugin for DebugToolsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<DebugSettings>()
|
app.init_resource::<DebugSettings>()
|
||||||
|
.init_resource::<FrameStepState>()
|
||||||
|
.init_resource::<save_state::DebugSaveState>()
|
||||||
|
.add_systems(OnEnter(AppState::InMatch), save_state::clear)
|
||||||
|
.add_systems(OnExit(AppState::InMatch), save_state::clear)
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
prepare_frame_step
|
||||||
|
.after(InputSystems)
|
||||||
|
.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(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(handle_palette_input, sync_gizmo_depth_bias)
|
(handle_palette_input, sync_gizmo_depth_bias)
|
||||||
@@ -391,6 +430,61 @@ fn queue_settings_save(commands: &mut Commands) {
|
|||||||
commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1)));
|
commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn frame_step_duration(frame_rate: usize) -> Duration {
|
||||||
|
Duration::from_nanos(1_000_000_000u64 / frame_rate as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_frame_step(
|
||||||
|
settings: Res<DebugSettings>,
|
||||||
|
gamepads: Query<&Gamepad>,
|
||||||
|
key: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut state: ResMut<FrameStepState>,
|
||||||
|
mut time: ResMut<Time>,
|
||||||
|
frame_rate: Res<RollbackFrameRate>,
|
||||||
|
) {
|
||||||
|
if !settings.frame_stepping {
|
||||||
|
state.requested = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pressed = gamepads
|
||||||
|
.iter()
|
||||||
|
.any(|gamepad| gamepad.pressed(GamepadButton::DPadDown));
|
||||||
|
|
||||||
|
let just_pressed = gamepads
|
||||||
|
.iter()
|
||||||
|
.any(|gamepad| gamepad.just_pressed(GamepadButton::DPadDown));
|
||||||
|
|
||||||
|
let just_pressed = key.just_pressed(KeyCode::F4) || just_pressed;
|
||||||
|
let pressed = key.pressed(KeyCode::F4) || pressed;
|
||||||
|
|
||||||
|
if just_pressed {
|
||||||
|
state.last_step_time = time.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let can_repress = pressed && (time.elapsed() - state.last_step_time.elapsed()).as_millis() > 500;
|
||||||
|
|
||||||
|
if !settings.menu_open && (just_pressed || can_repress)
|
||||||
|
{
|
||||||
|
state.requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.time_backup = Some(*time);
|
||||||
|
let delta = if state.requested {
|
||||||
|
frame_step_duration(**frame_rate)
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
};
|
||||||
|
time.advance_by(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_frame_step_time(mut state: ResMut<FrameStepState>, mut time: ResMut<Time>) {
|
||||||
|
if let Some(backup) = state.time_backup.take() {
|
||||||
|
*time = backup;
|
||||||
|
state.requested = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_selected(
|
fn activate_selected(
|
||||||
settings: &mut DebugSettings,
|
settings: &mut DebugSettings,
|
||||||
page_len: usize,
|
page_len: usize,
|
||||||
@@ -408,7 +502,9 @@ fn activate_selected(
|
|||||||
}
|
}
|
||||||
DebugMenuEntry::Toggle { flag, .. } => {
|
DebugMenuEntry::Toggle { flag, .. } => {
|
||||||
settings.toggle_flag(flag);
|
settings.toggle_flag(flag);
|
||||||
queue_settings_save(commands);
|
if !matches!(flag, DebugFlag::FrameStepping) {
|
||||||
|
queue_settings_save(commands);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DebugMenuEntry::Submenu { entries, .. } => {
|
DebugMenuEntry::Submenu { entries, .. } => {
|
||||||
settings.push_page(DebugMenuPage::Static(entries));
|
settings.push_page(DebugMenuPage::Static(entries));
|
||||||
@@ -623,7 +719,9 @@ fn draw_palette(
|
|||||||
let mut value = settings.flag(*flag);
|
let mut value = settings.flag(*flag);
|
||||||
if ui.checkbox(&mut value, *label).changed() {
|
if ui.checkbox(&mut value, *label).changed() {
|
||||||
settings.toggle_flag(*flag);
|
settings.toggle_flag(*flag);
|
||||||
queue_settings_save(&mut commands);
|
if !matches!(flag, DebugFlag::FrameStepping) {
|
||||||
|
queue_settings_save(&mut commands);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DebugMenuEntry::Submenu { label, .. }
|
DebugMenuEntry::Submenu { label, .. }
|
||||||
@@ -839,6 +937,15 @@ mod tests {
|
|||||||
assert_eq!(GameSpeedDebug::Quarter.frame_rate(), 15);
|
assert_eq!(GameSpeedDebug::Quarter.frame_rate(), 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_stepping_is_disabled_by_default_and_uses_game_speed_timestep() {
|
||||||
|
let settings = DebugSettings::default();
|
||||||
|
|
||||||
|
assert!(!settings.frame_stepping);
|
||||||
|
assert_eq!(frame_step_duration(60), Duration::from_nanos(16_666_666));
|
||||||
|
assert_eq!(frame_step_duration(30), Duration::from_nanos(33_333_333));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_player_list_does_not_activate() {
|
fn empty_player_list_does_not_activate() {
|
||||||
let mut settings = DebugSettings::default();
|
let mut settings = DebugSettings::default();
|
||||||
|
|||||||
@@ -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<Checkpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Checkpoint {
|
||||||
|
fighters: Vec<FighterCheckpoint>,
|
||||||
|
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::<super::DebugSettings>().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::<crate::fighter::FighterRoster>()
|
||||||
|
.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::<StageCollision>().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::<DebugSaveState>().checkpoint = Some(Checkpoint {
|
||||||
|
fighters: checkpoint_fighters,
|
||||||
|
stage_collision,
|
||||||
|
});
|
||||||
|
info!("Saved debug game state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(checkpoint) = world.resource::<DebugSaveState>().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::<Player>() 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::<Grounded>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<FighterCheckpoint> {
|
||||||
|
let fighter = world.entity(entity);
|
||||||
|
Some(FighterCheckpoint {
|
||||||
|
entity,
|
||||||
|
handle: fighter.get::<Player>()?.handle,
|
||||||
|
ecb: *fighter.get::<FighterECB>()?,
|
||||||
|
previous_ecb: *fighter.get::<crate::fighter::ecb::FighterPreviousECB>()?,
|
||||||
|
velocity: *fighter.get::<FighterVelocity>()?,
|
||||||
|
translation: *fighter.get::<FighterTranslation>()?,
|
||||||
|
previous_translation: *fighter.get::<FighterPreviousTranslation>()?,
|
||||||
|
facing: *fighter.get::<FighterFacingDirection>()?,
|
||||||
|
previous_facing: *fighter.get::<FighterPreviousFacingDirection>()?,
|
||||||
|
animation: *fighter.get::<FighterAnimationFrame>()?,
|
||||||
|
state: fighter.get::<FighterState>()?.clone(),
|
||||||
|
input: *fighter.get::<FighterInput>()?,
|
||||||
|
damage: *fighter.get::<FighterDamage>()?,
|
||||||
|
pending_hit_reaction: fighter.get::<FighterPendingHitReaction>()?.clone(),
|
||||||
|
hitstop: *fighter.get::<FighterHitstop>()?,
|
||||||
|
knockback_velocity: *fighter.get::<FighterKnockbackVelocity>()?,
|
||||||
|
stale_moves: fighter.get::<FighterStaleMoveQueue>()?.clone(),
|
||||||
|
hitboxes: fighter.get::<FighterHitboxes>()?.clone(),
|
||||||
|
received_attacks: fighter.get::<FighterReceivedAttacks>()?.clone(),
|
||||||
|
solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(),
|
||||||
|
bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(),
|
||||||
|
grounded: fighter.contains::<Grounded>(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear(mut save_state: ResMut<DebugSaveState>) {
|
||||||
|
save_state.checkpoint = None;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user