//! 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, BlastZones, StageCollision, SpawnPositions, AnimationBones, AttackHitboxes, Hurtboxes, Camera, LedgeGrab, 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: "Ledge grab", flag: DebugFlag::LedgeGrab, }, DebugMenuEntry::Toggle { label: "Hurtboxes", flag: DebugFlag::Hurtboxes, }, ]; const WORLD_MENU: &[DebugMenuEntry] = &[ DebugMenuEntry::Toggle { label: "Stage collision", flag: DebugFlag::StageCollision, }, DebugMenuEntry::Toggle { label: "Blast zones", flag: DebugFlag::BlastZones, }, DebugMenuEntry::Toggle { label: "Spawn points", flag: DebugFlag::SpawnPositions, }, 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, #[reflect(ignore)] pages: Vec, #[reflect(ignore)] selections: Vec, #[reflect(ignore)] pub player_rows: HashMap, #[reflect(ignore)] pub frame_stepping: bool, pub network: bool, pub ecb: bool, pub blast_zones: bool, pub stage_collision: bool, pub spawn_positions: bool, pub animation_bones: bool, pub attack_hitboxes: bool, pub hurtboxes: bool, pub camera: bool, pub ledge_grab: 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, blast_zones: false, stage_collision: false, spawn_positions: false, animation_bones: false, attack_hitboxes: false, hurtboxes: false, camera: false, ledge_grab: 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::BlastZones => self.blast_zones, DebugFlag::SpawnPositions => self.spawn_positions, 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::LedgeGrab => self.ledge_grab, 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::BlastZones => self.blast_zones = !self.blast_zones, DebugFlag::StageCollision => self.stage_collision = !self.stage_collision, DebugFlag::SpawnPositions => self.spawn_positions = !self.spawn_positions, 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::LedgeGrab => self.ledge_grab = !self.ledge_grab, 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