977 lines
32 KiB
Rust
977 lines
32 KiB
Rust
//! Local developer overlays and their controller-driven palette.
|
|
|
|
use std::{collections::HashMap, time::Duration};
|
|
|
|
use bevy::{
|
|
camera::{CameraOutputMode, visibility::RenderLayers}, input::InputSystems, prelude::*, render::render_resource::BlendState, settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
|
};
|
|
use bevy_egui::{EguiContexts, EguiGlobalSettings, EguiPrimaryContextPass, PrimaryEguiContext, egui};
|
|
use bevy_ggrs::{RollbackFrameRate, RunGgrsSystems};
|
|
use enum_cycling::EnumCycle;
|
|
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)]
|
|
pub enum FighterDebugRowKind {
|
|
None,
|
|
CurrentState,
|
|
Velocity,
|
|
Input,
|
|
Position,
|
|
Facing,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
|
|
pub enum PresentationMotionMode {
|
|
#[default]
|
|
Interpolation,
|
|
Extrapolation,
|
|
}
|
|
|
|
impl PresentationMotionMode {
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Interpolation => "Interpolation",
|
|
Self::Extrapolation => "Extrapolation",
|
|
}
|
|
}
|
|
|
|
fn cycle(&mut self) {
|
|
*self = match self {
|
|
Self::Interpolation => Self::Extrapolation,
|
|
Self::Extrapolation => Self::Interpolation,
|
|
};
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumCycle, Reflect)]
|
|
pub enum GameSpeedDebug {
|
|
Full,
|
|
Half,
|
|
Quarter,
|
|
}
|
|
|
|
impl GameSpeedDebug {
|
|
pub const fn label(&self) -> &'static str {
|
|
match self {
|
|
GameSpeedDebug::Full => "1.0x",
|
|
GameSpeedDebug::Half => "0.5x",
|
|
GameSpeedDebug::Quarter => "0.25x",
|
|
}
|
|
}
|
|
|
|
pub fn frame_rate(&self) -> usize {
|
|
match self {
|
|
GameSpeedDebug::Full => 60,
|
|
GameSpeedDebug::Half => 30,
|
|
GameSpeedDebug::Quarter => 15,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FighterDebugRowKind {
|
|
const ALL: [Self; 6] = [
|
|
Self::None,
|
|
Self::CurrentState,
|
|
Self::Velocity,
|
|
Self::Input,
|
|
Self::Position,
|
|
Self::Facing,
|
|
];
|
|
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::None => "None",
|
|
Self::CurrentState => "Current state",
|
|
Self::Velocity => "Velocity",
|
|
Self::Input => "Input",
|
|
Self::Position => "Position",
|
|
Self::Facing => "Facing",
|
|
}
|
|
}
|
|
|
|
fn cycle(self, direction: i8) -> Self {
|
|
let index = Self::ALL
|
|
.iter()
|
|
.position(|kind| *kind == self)
|
|
.expect("all row kinds must be listed") as i8;
|
|
let index = (index + direction).rem_euclid(Self::ALL.len() as i8) as usize;
|
|
Self::ALL[index]
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum DebugFlag {
|
|
Network,
|
|
Ecb,
|
|
StageCollision,
|
|
AnimationBones,
|
|
AttackHitboxes,
|
|
Hurtboxes,
|
|
Camera,
|
|
DrawThroughGeometry,
|
|
FrameStepping,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum DebugMenuEntry {
|
|
StopRecording {
|
|
label: &'static str,
|
|
},
|
|
Toggle {
|
|
label: &'static str,
|
|
flag: DebugFlag,
|
|
},
|
|
Submenu {
|
|
label: &'static str,
|
|
entries: &'static [DebugMenuEntry],
|
|
},
|
|
PlayerSlots {
|
|
label: &'static str,
|
|
},
|
|
MotionSampling {
|
|
label: &'static str,
|
|
},
|
|
GameSpeed {
|
|
label: &'static str,
|
|
},
|
|
}
|
|
|
|
const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
|
DebugMenuEntry::PlayerSlots {
|
|
label: "Player windows",
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "ECB",
|
|
flag: DebugFlag::Ecb,
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "Animation bones",
|
|
flag: DebugFlag::AnimationBones,
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "Camera",
|
|
flag: DebugFlag::Camera,
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "Attack hitboxes",
|
|
flag: DebugFlag::AttackHitboxes,
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "Hurtboxes",
|
|
flag: DebugFlag::Hurtboxes,
|
|
},
|
|
];
|
|
|
|
const WORLD_MENU: &[DebugMenuEntry] = &[
|
|
DebugMenuEntry::Toggle {
|
|
label: "Stage collision",
|
|
flag: DebugFlag::StageCollision,
|
|
},
|
|
DebugMenuEntry::GameSpeed {
|
|
label: "Game speed",
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "Frame stepping",
|
|
flag: DebugFlag::FrameStepping,
|
|
},
|
|
];
|
|
|
|
const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
|
label: "Network stats",
|
|
flag: DebugFlag::Network,
|
|
}];
|
|
|
|
const RENDERING_MENU: &[DebugMenuEntry] = &[
|
|
DebugMenuEntry::MotionSampling {
|
|
label: "Motion sampling",
|
|
},
|
|
DebugMenuEntry::Toggle {
|
|
label: "Draw through geometry",
|
|
flag: DebugFlag::DrawThroughGeometry,
|
|
},
|
|
];
|
|
|
|
const ROOT_MENU: &[DebugMenuEntry] = &[
|
|
DebugMenuEntry::StopRecording {
|
|
label: "Stop replay recording",
|
|
},
|
|
DebugMenuEntry::Submenu {
|
|
label: "Fighter",
|
|
entries: FIGHTER_MENU,
|
|
},
|
|
DebugMenuEntry::Submenu {
|
|
label: "World",
|
|
entries: WORLD_MENU,
|
|
},
|
|
DebugMenuEntry::Submenu {
|
|
label: "Diagnostics",
|
|
entries: DIAGNOSTICS_MENU,
|
|
},
|
|
DebugMenuEntry::Submenu {
|
|
label: "Rendering",
|
|
entries: RENDERING_MENU,
|
|
},
|
|
];
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum DebugMenuPage {
|
|
Static(&'static [DebugMenuEntry]),
|
|
PlayerList,
|
|
PlayerRows(usize),
|
|
}
|
|
|
|
#[derive(Resource, SettingsGroup, Reflect)]
|
|
#[reflect(Resource, SettingsGroup, Default)]
|
|
pub struct DebugSettings {
|
|
#[reflect(ignore)]
|
|
pub menu_open: bool,
|
|
#[reflect(ignore)]
|
|
controller: Option<Entity>,
|
|
#[reflect(ignore)]
|
|
pages: Vec<DebugMenuPage>,
|
|
#[reflect(ignore)]
|
|
selections: Vec<usize>,
|
|
#[reflect(ignore)]
|
|
pub player_rows: HashMap<usize, [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]>,
|
|
#[reflect(ignore)]
|
|
pub frame_stepping: bool,
|
|
pub network: bool,
|
|
pub ecb: bool,
|
|
pub stage_collision: bool,
|
|
pub animation_bones: bool,
|
|
pub attack_hitboxes: bool,
|
|
pub hurtboxes: bool,
|
|
pub camera: bool,
|
|
pub draw_through_geometry: bool,
|
|
pub motion_sampling: PresentationMotionMode,
|
|
pub game_speed: GameSpeedDebug,
|
|
}
|
|
|
|
impl Default for DebugSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
menu_open: false,
|
|
controller: None,
|
|
pages: vec![DebugMenuPage::Static(ROOT_MENU)],
|
|
selections: vec![0],
|
|
player_rows: HashMap::new(),
|
|
frame_stepping: false,
|
|
network: false,
|
|
ecb: false,
|
|
stage_collision: false,
|
|
animation_bones: false,
|
|
attack_hitboxes: false,
|
|
hurtboxes: false,
|
|
camera: false,
|
|
draw_through_geometry: false,
|
|
motion_sampling: PresentationMotionMode::default(),
|
|
game_speed: GameSpeedDebug::Full,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DebugSettings {
|
|
pub fn player_rows(
|
|
&self,
|
|
handle: usize,
|
|
) -> Option<&[FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]> {
|
|
self.player_rows.get(&handle)
|
|
}
|
|
|
|
fn reset_navigation(&mut self) {
|
|
self.pages.truncate(1);
|
|
self.selections.truncate(1);
|
|
self.selections[0] = 0;
|
|
}
|
|
|
|
fn page(&self) -> DebugMenuPage {
|
|
*self
|
|
.pages
|
|
.last()
|
|
.expect("debug menu always has a root page")
|
|
}
|
|
|
|
fn selected(&self) -> usize {
|
|
*self
|
|
.selections
|
|
.last()
|
|
.expect("debug menu always has a root selection")
|
|
}
|
|
|
|
fn selected_mut(&mut self) -> &mut usize {
|
|
self.selections
|
|
.last_mut()
|
|
.expect("debug menu always has a root selection")
|
|
}
|
|
|
|
fn push_page(&mut self, page: DebugMenuPage) {
|
|
self.pages.push(page);
|
|
self.selections.push(0);
|
|
}
|
|
|
|
fn pop_page(&mut self) -> bool {
|
|
if self.pages.len() == 1 {
|
|
return false;
|
|
}
|
|
self.pages.pop();
|
|
self.selections.pop();
|
|
true
|
|
}
|
|
|
|
fn flag(&self, flag: DebugFlag) -> bool {
|
|
match flag {
|
|
DebugFlag::Network => self.network,
|
|
DebugFlag::Ecb => self.ecb,
|
|
DebugFlag::StageCollision => self.stage_collision,
|
|
DebugFlag::AnimationBones => self.animation_bones,
|
|
DebugFlag::AttackHitboxes => self.attack_hitboxes,
|
|
DebugFlag::Hurtboxes => self.hurtboxes,
|
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
|
DebugFlag::Camera => self.camera,
|
|
DebugFlag::FrameStepping => self.frame_stepping,
|
|
}
|
|
}
|
|
|
|
fn toggle_flag(&mut self, flag: DebugFlag) {
|
|
match flag {
|
|
DebugFlag::Network => self.network = !self.network,
|
|
DebugFlag::Ecb => self.ecb = !self.ecb,
|
|
DebugFlag::StageCollision => self.stage_collision = !self.stage_collision,
|
|
DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones,
|
|
DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes,
|
|
DebugFlag::Hurtboxes => self.hurtboxes = !self.hurtboxes,
|
|
DebugFlag::DrawThroughGeometry => {
|
|
self.draw_through_geometry = !self.draw_through_geometry
|
|
}
|
|
DebugFlag::Camera => self.camera = !self.camera,
|
|
DebugFlag::FrameStepping => self.frame_stepping = !self.frame_stepping,
|
|
}
|
|
}
|
|
|
|
fn cycle_motion_sampling(&mut self) {
|
|
self.motion_sampling.cycle();
|
|
}
|
|
|
|
fn rows_mut(&mut self, handle: usize) -> &mut [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT] {
|
|
self.player_rows
|
|
.entry(handle)
|
|
.or_insert([FighterDebugRowKind::None; PLAYER_DEBUG_ROW_COUNT])
|
|
}
|
|
}
|
|
|
|
pub struct DebugToolsPlugin;
|
|
|
|
#[derive(Resource, Default)]
|
|
struct FrameStepState {
|
|
requested: bool,
|
|
time_backup: Option<Time>,
|
|
last_step_time: Time
|
|
}
|
|
|
|
impl Plugin for DebugToolsPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
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(Startup, spawn_egui_camera)
|
|
.add_systems(PreUpdate, restore_frame_step_time.after(RunGgrsSystems))
|
|
.add_systems(
|
|
Update,
|
|
(handle_palette_input, sync_gizmo_depth_bias)
|
|
.chain()
|
|
.run_if(in_state(AppState::InMatch)),
|
|
)
|
|
.add_systems(EguiPrimaryContextPass, draw_palette);
|
|
}
|
|
}
|
|
|
|
pub fn spawn_egui_camera(mut commands: Commands, mut egui_global_settings: ResMut<EguiGlobalSettings>,) {
|
|
egui_global_settings.auto_create_primary_context = false;
|
|
commands.spawn((
|
|
// The `PrimaryEguiContext` component requires everything needed to render a primary context.
|
|
PrimaryEguiContext,
|
|
Camera2d,
|
|
// Setting RenderLayers to none makes sure we won't render anything apart from the UI.
|
|
RenderLayers::none(),
|
|
Camera {
|
|
order: 2,
|
|
output_mode: CameraOutputMode::Write {
|
|
blend_state: Some(BlendState::ALPHA_BLENDING),
|
|
clear_color: ClearColorConfig::None,
|
|
},
|
|
clear_color: ClearColorConfig::Custom(Color::NONE),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn player_handles(players: &Query<&Player>) -> Vec<usize> {
|
|
let mut handles = players
|
|
.iter()
|
|
.map(|player| player.handle)
|
|
.collect::<Vec<_>>();
|
|
handles.sort_unstable();
|
|
handles.dedup();
|
|
handles
|
|
}
|
|
|
|
fn page_len(page: DebugMenuPage, player_handles: &[usize]) -> usize {
|
|
match page {
|
|
DebugMenuPage::Static(entries) => entries.len(),
|
|
DebugMenuPage::PlayerList => player_handles.len(),
|
|
DebugMenuPage::PlayerRows(_) => PLAYER_DEBUG_ROW_COUNT,
|
|
}
|
|
}
|
|
|
|
fn queue_settings_save(commands: &mut Commands) {
|
|
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(
|
|
settings: &mut DebugSettings,
|
|
page_len: usize,
|
|
player_handles: &[usize],
|
|
commands: &mut Commands,
|
|
) {
|
|
if page_len == 0 {
|
|
return;
|
|
}
|
|
|
|
match settings.page() {
|
|
DebugMenuPage::Static(entries) => match entries[settings.selected()] {
|
|
DebugMenuEntry::StopRecording { .. } => {
|
|
commands.insert_resource(StopReplayRecording);
|
|
}
|
|
DebugMenuEntry::Toggle { flag, .. } => {
|
|
settings.toggle_flag(flag);
|
|
if !matches!(flag, DebugFlag::FrameStepping) {
|
|
queue_settings_save(commands);
|
|
}
|
|
}
|
|
DebugMenuEntry::Submenu { entries, .. } => {
|
|
settings.push_page(DebugMenuPage::Static(entries));
|
|
}
|
|
DebugMenuEntry::PlayerSlots { .. } => settings.push_page(DebugMenuPage::PlayerList),
|
|
DebugMenuEntry::MotionSampling { .. } => {
|
|
settings.cycle_motion_sampling();
|
|
queue_settings_save(commands);
|
|
}
|
|
DebugMenuEntry::GameSpeed { .. } => {
|
|
commands.insert_resource(RollbackFrameRate(settings.game_speed.frame_rate()));
|
|
queue_settings_save(commands);
|
|
}
|
|
},
|
|
DebugMenuPage::PlayerList => {
|
|
let handle = player_handles[settings.selected()];
|
|
settings.rows_mut(handle);
|
|
settings.push_page(DebugMenuPage::PlayerRows(handle));
|
|
}
|
|
DebugMenuPage::PlayerRows(_) => {}
|
|
}
|
|
}
|
|
|
|
fn cycle_selected_value(settings: &mut DebugSettings, direction: i8, commands: &mut Commands) {
|
|
match settings.page() {
|
|
DebugMenuPage::Static(entries) => {
|
|
if let Some(entry) = entries.get(settings.selected()) {
|
|
match entry {
|
|
DebugMenuEntry::MotionSampling { .. } => {
|
|
settings.cycle_motion_sampling();
|
|
queue_settings_save(commands);
|
|
}
|
|
DebugMenuEntry::GameSpeed { .. } => {
|
|
settings.game_speed = if direction > 0 {
|
|
settings.game_speed.up()
|
|
} else {
|
|
settings.game_speed.down()
|
|
};
|
|
commands
|
|
.insert_resource(RollbackFrameRate(settings.game_speed.frame_rate()));
|
|
queue_settings_save(commands);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
DebugMenuPage::PlayerRows(handle) => {
|
|
let selected = settings.selected();
|
|
let rows = settings.rows_mut(handle);
|
|
rows[selected] = rows[selected].cycle(direction);
|
|
queue_settings_save(commands);
|
|
}
|
|
DebugMenuPage::PlayerList => {}
|
|
}
|
|
}
|
|
|
|
fn handle_palette_input(
|
|
keyboard: Res<ButtonInput<KeyCode>>,
|
|
gamepads: Query<(Entity, &Gamepad)>,
|
|
players: Query<&Player>,
|
|
mut settings: ResMut<DebugSettings>,
|
|
mut commands: Commands,
|
|
) {
|
|
if keyboard.just_pressed(KeyCode::KeyT) {
|
|
settings.toggle_flag(DebugFlag::DrawThroughGeometry);
|
|
}
|
|
|
|
if keyboard.just_pressed(KeyCode::F3) {
|
|
settings.menu_open = !settings.menu_open;
|
|
settings.controller = None;
|
|
if settings.menu_open {
|
|
settings.reset_navigation();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if !settings.menu_open {
|
|
for (entity, gamepad) in &gamepads {
|
|
if gamepad.just_pressed(GamepadButton::Start) && gamepad.pressed(GamepadButton::DPadUp)
|
|
{
|
|
settings.menu_open = true;
|
|
settings.controller = Some(entity);
|
|
settings.reset_navigation();
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
let gamepad = if let Some(controller) = settings.controller {
|
|
let Ok((_, gamepad)) = gamepads.get(controller) else {
|
|
settings.menu_open = false;
|
|
settings.controller = None;
|
|
return;
|
|
};
|
|
Some(gamepad)
|
|
} else {
|
|
None
|
|
};
|
|
let player_handles = player_handles(&players);
|
|
let page = settings.page();
|
|
let page_len = page_len(page, &player_handles);
|
|
|
|
if page_len != 0 && settings.selected() >= page_len {
|
|
*settings.selected_mut() = 0;
|
|
}
|
|
|
|
if (gamepad.is_some_and(|pad| pad.just_pressed(GamepadButton::DPadUp))
|
|
|| keyboard.just_pressed(KeyCode::ArrowUp)
|
|
|| keyboard.just_pressed(KeyCode::KeyW))
|
|
&& page_len != 0
|
|
{
|
|
let selected = settings.selected_mut();
|
|
*selected = selected.checked_sub(1).unwrap_or(page_len - 1);
|
|
}
|
|
if (gamepad.is_some_and(|pad| pad.just_pressed(GamepadButton::DPadDown))
|
|
|| keyboard.just_pressed(KeyCode::ArrowDown)
|
|
|| keyboard.just_pressed(KeyCode::KeyS))
|
|
&& page_len != 0
|
|
{
|
|
let selected = settings.selected_mut();
|
|
*selected = (*selected + 1) % page_len;
|
|
}
|
|
|
|
let direction = if gamepad.is_some_and(|pad| pad.just_pressed(GamepadButton::DPadLeft))
|
|
|| keyboard.just_pressed(KeyCode::ArrowLeft)
|
|
|| keyboard.just_pressed(KeyCode::KeyA)
|
|
{
|
|
Some(-1)
|
|
} else if gamepad.is_some_and(|pad| pad.just_pressed(GamepadButton::DPadRight))
|
|
|| keyboard.just_pressed(KeyCode::ArrowRight)
|
|
|| keyboard.just_pressed(KeyCode::KeyD)
|
|
{
|
|
Some(1)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(direction) = direction {
|
|
cycle_selected_value(&mut settings, direction, &mut commands);
|
|
}
|
|
if (gamepad.is_some_and(|pad| pad.just_pressed(GamepadButton::South))
|
|
|| keyboard.just_pressed(KeyCode::Enter)
|
|
|| keyboard.just_pressed(KeyCode::Space))
|
|
&& page_len != 0
|
|
{
|
|
activate_selected(&mut settings, page_len, &player_handles, &mut commands);
|
|
}
|
|
if (gamepad.is_some_and(|pad| pad.just_pressed(GamepadButton::West))
|
|
|| keyboard.just_pressed(KeyCode::Escape))
|
|
&& !settings.pop_page()
|
|
{
|
|
settings.menu_open = false;
|
|
settings.controller = None;
|
|
}
|
|
}
|
|
|
|
fn sync_gizmo_depth_bias(settings: Res<DebugSettings>, mut configs: ResMut<GizmoConfigStore>) {
|
|
if !settings.is_changed() {
|
|
return;
|
|
}
|
|
let depth_bias = if settings.draw_through_geometry {
|
|
-1.0
|
|
} else {
|
|
0.0
|
|
};
|
|
for (_, config, _) in configs.iter_mut() {
|
|
config.depth_bias = depth_bias;
|
|
}
|
|
}
|
|
|
|
fn draw_palette(
|
|
mut contexts: EguiContexts,
|
|
mut settings: ResMut<DebugSettings>,
|
|
players: Query<&Player>,
|
|
mut commands: Commands,
|
|
) -> Result {
|
|
if !settings.menu_open {
|
|
return Ok(());
|
|
}
|
|
|
|
let player_handles = player_handles(&players);
|
|
let page = settings.page();
|
|
let page_len = page_len(page, &player_handles);
|
|
egui::Window::new("Debug")
|
|
.anchor(egui::Align2::RIGHT_TOP, egui::vec2(-12.0, 12.0))
|
|
.resizable(false)
|
|
.show(contexts.ctx_mut()?, |ui| {
|
|
ui.style_mut().override_font_id = Some(egui::FontId::proportional(20.0));
|
|
match page {
|
|
DebugMenuPage::Static(entries) => {
|
|
for (index, entry) in entries.iter().enumerate() {
|
|
ui.horizontal(|ui| {
|
|
ui.label(if index == settings.selected() {
|
|
">"
|
|
} else {
|
|
" "
|
|
});
|
|
match entry {
|
|
DebugMenuEntry::StopRecording { label } => {
|
|
if ui.button(*label).clicked() {
|
|
*settings.selected_mut() = index;
|
|
activate_selected(
|
|
&mut settings,
|
|
page_len,
|
|
&player_handles,
|
|
&mut commands,
|
|
);
|
|
}
|
|
}
|
|
DebugMenuEntry::Toggle { label, flag } => {
|
|
let mut value = settings.flag(*flag);
|
|
if ui.checkbox(&mut value, *label).changed() {
|
|
settings.toggle_flag(*flag);
|
|
if !matches!(flag, DebugFlag::FrameStepping) {
|
|
queue_settings_save(&mut commands);
|
|
}
|
|
}
|
|
}
|
|
DebugMenuEntry::Submenu { label, .. }
|
|
| DebugMenuEntry::PlayerSlots { label } => {
|
|
if ui.button(format!("{label} >")).clicked() {
|
|
*settings.selected_mut() = index;
|
|
activate_selected(
|
|
&mut settings,
|
|
page_len,
|
|
&player_handles,
|
|
&mut commands,
|
|
);
|
|
}
|
|
}
|
|
DebugMenuEntry::MotionSampling { label } => {
|
|
let old = settings.motion_sampling;
|
|
egui::ComboBox::from_id_salt(("debug-motion", index))
|
|
.width(180.0)
|
|
.selected_text(format!("{label}: {}", old.label()))
|
|
.show_ui(ui, |ui| {
|
|
ui.selectable_value(
|
|
&mut settings.motion_sampling,
|
|
PresentationMotionMode::Interpolation,
|
|
"Interpolation",
|
|
);
|
|
ui.selectable_value(
|
|
&mut settings.motion_sampling,
|
|
PresentationMotionMode::Extrapolation,
|
|
"Extrapolation",
|
|
);
|
|
});
|
|
if settings.motion_sampling != old {
|
|
queue_settings_save(&mut commands);
|
|
}
|
|
}
|
|
DebugMenuEntry::GameSpeed { label } => {
|
|
let old = settings.game_speed;
|
|
egui::ComboBox::from_id_salt(("debug-speed", index))
|
|
.width(180.0)
|
|
.selected_text(format!("{label}: {}", old.label()))
|
|
.show_ui(ui, |ui| {
|
|
for speed in [
|
|
GameSpeedDebug::Full,
|
|
GameSpeedDebug::Half,
|
|
GameSpeedDebug::Quarter,
|
|
] {
|
|
ui.selectable_value(
|
|
&mut settings.game_speed,
|
|
speed,
|
|
speed.label(),
|
|
);
|
|
}
|
|
});
|
|
if settings.game_speed != old {
|
|
commands.insert_resource(RollbackFrameRate(
|
|
settings.game_speed.frame_rate(),
|
|
));
|
|
queue_settings_save(&mut commands);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
DebugMenuPage::PlayerList => {
|
|
for (index, handle) in player_handles.iter().enumerate() {
|
|
ui.horizontal(|ui| {
|
|
ui.label(if index == settings.selected() {
|
|
">"
|
|
} else {
|
|
" "
|
|
});
|
|
if ui.button(format!("Player {} >", handle + 1)).clicked() {
|
|
*settings.selected_mut() = index;
|
|
activate_selected(
|
|
&mut settings,
|
|
page_len,
|
|
&player_handles,
|
|
&mut commands,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
if player_handles.is_empty() {
|
|
ui.label("No players in match");
|
|
}
|
|
}
|
|
DebugMenuPage::PlayerRows(handle) => {
|
|
let rows = settings
|
|
.player_rows(handle)
|
|
.copied()
|
|
.unwrap_or([FighterDebugRowKind::None; PLAYER_DEBUG_ROW_COUNT]);
|
|
ui.label(format!("Player {}", handle + 1));
|
|
for (index, row) in rows.iter().enumerate() {
|
|
ui.horizontal(|ui| {
|
|
ui.label(if index == settings.selected() {
|
|
">"
|
|
} else {
|
|
" "
|
|
});
|
|
egui::ComboBox::from_id_salt(("debug-row", handle, index))
|
|
.width(180.0)
|
|
.selected_text(format!("Row {}: {}", index + 1, row.label()))
|
|
.show_ui(ui, |ui| {
|
|
let rows = settings.rows_mut(handle);
|
|
for kind in FighterDebugRowKind::ALL {
|
|
ui.selectable_value(&mut rows[index], kind, kind.label());
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
ui.separator();
|
|
let controls = if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
|
"Arrows/WASD: select/change Escape/B: back"
|
|
} else {
|
|
"Arrows/WASD: select Enter/A: activate Escape/B: back"
|
|
};
|
|
ui.label(controls);
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
pub fn fighter_info_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings
|
|
.player_rows
|
|
.values()
|
|
.any(|rows| rows.iter().any(|row| *row != FighterDebugRowKind::None))
|
|
}
|
|
|
|
pub fn network_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.network
|
|
}
|
|
|
|
pub fn ecb_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.ecb
|
|
}
|
|
|
|
pub fn stage_collision_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.stage_collision
|
|
}
|
|
|
|
pub fn animation_bones_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.animation_bones
|
|
}
|
|
|
|
pub fn attack_hitboxes_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.attack_hitboxes
|
|
}
|
|
|
|
pub fn hurtboxes_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.hurtboxes
|
|
}
|
|
|
|
pub fn camera_debug_enabled(settings: Res<DebugSettings>) -> bool {
|
|
settings.camera
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn player_rows_default_to_none_and_wrap() {
|
|
let mut settings = DebugSettings::default();
|
|
let rows = settings.rows_mut(2);
|
|
|
|
assert_eq!(*rows, [FighterDebugRowKind::None; PLAYER_DEBUG_ROW_COUNT]);
|
|
rows[0] = rows[0].cycle(-1);
|
|
assert_eq!(rows[0], FighterDebugRowKind::Facing);
|
|
rows[0] = rows[0].cycle(1);
|
|
assert_eq!(rows[0], FighterDebugRowKind::None);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_navigation_preserves_parent_selection() {
|
|
let mut settings = DebugSettings::default();
|
|
*settings.selected_mut() = 2;
|
|
settings.push_page(DebugMenuPage::Static(DIAGNOSTICS_MENU));
|
|
|
|
assert!(settings.pop_page());
|
|
assert_eq!(settings.selected(), 2);
|
|
assert!(!settings.pop_page());
|
|
}
|
|
|
|
#[test]
|
|
fn player_row_configuration_is_slot_specific() {
|
|
let mut settings = DebugSettings::default();
|
|
settings.rows_mut(0)[0] = FighterDebugRowKind::CurrentState;
|
|
|
|
assert_eq!(
|
|
settings.player_rows(0).unwrap()[0],
|
|
FighterDebugRowKind::CurrentState
|
|
);
|
|
assert!(settings.player_rows(1).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn presentation_motion_mode_cycles_between_choices() {
|
|
let mut mode = PresentationMotionMode::Interpolation;
|
|
|
|
mode.cycle();
|
|
assert_eq!(mode, PresentationMotionMode::Extrapolation);
|
|
mode.cycle();
|
|
assert_eq!(mode, PresentationMotionMode::Interpolation);
|
|
}
|
|
|
|
#[test]
|
|
fn game_speed_has_expected_frame_rates() {
|
|
assert_eq!(GameSpeedDebug::Full.frame_rate(), 60);
|
|
assert_eq!(GameSpeedDebug::Half.frame_rate(), 30);
|
|
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]
|
|
fn empty_player_list_does_not_activate() {
|
|
let mut settings = DebugSettings::default();
|
|
settings.push_page(DebugMenuPage::PlayerList);
|
|
|
|
assert_eq!(page_len(settings.page(), &[]), 0);
|
|
assert_eq!(settings.selected(), 0);
|
|
}
|
|
}
|