#![allow(missing_docs)]
use async_channel::{Receiver, Sender};
use bones_asset::HasSchema;
use egui::CollapsingHeader;
use egui_plot::{Bar, BarChart, GridMark, Plot};
use ggrs::{NetworkStats, PlayerHandle};
use once_cell::sync::Lazy;
use crate::prelude::*;
pub mod prelude {
pub use super::NetworkDebugMenuState;
}
pub fn network_debug_session_plugin(session: &mut Session) {
session.add_system_to_stage(CoreStage::First, network_debug_window);
}
pub enum PlayerSyncState {
SyncInProgress,
Sychronized,
}
pub enum NetworkDebugMessage {
ResetData,
SkipFrame { frame: i32, count: u32 },
FrameUpdate { current: i32, last_confirmed: i32 },
FrameFroze { frame: i32 },
NetworkStats {
network_stats: Vec<(PlayerHandle, NetworkStats)>,
},
SetMaxPrediction(usize),
DisconnectedPlayers(Vec<usize>),
PlayerSync((PlayerSyncState, PlayerHandle)),
}
pub struct NetworkDebugChannel {
pub receiver: Receiver<NetworkDebugMessage>,
pub sender: Sender<NetworkDebugMessage>,
}
impl Default for NetworkDebugChannel {
fn default() -> Self {
let (sender, receiver) = async_channel::unbounded();
Self { sender, receiver }
}
}
#[allow(missing_docs)]
pub struct NetworkFrameData {
pub predicted_frames: i32,
pub current_frame: i32,
pub froze: bool,
}
impl NetworkFrameData {
pub fn new(confirmed: i32, current: i32) -> Self {
Self {
predicted_frames: current - std::cmp::max(confirmed, 0),
current_frame: current,
froze: false,
}
}
}
#[derive(HasSchema)]
#[schema(no_clone)]
pub struct NetworkDebug {
pub confirmed_frame: i32,
pub current_frame: i32,
pub last_skipped_frame_count: u32,
pub last_frame_with_skips: i32,
pub frame_buffer: Vec<NetworkFrameData>,
pub frame_buffer_display_size: usize,
pub paused: bool,
pub network_stats: Vec<(PlayerHandle, NetworkStats)>,
pub max_prediction_window: usize,
pub disconnected_players: Vec<usize>,
pub player_sync_state: HashMap<PlayerHandle, PlayerSyncState>,
}
impl Default for NetworkDebug {
fn default() -> Self {
Self {
confirmed_frame: 0,
current_frame: 0,
last_skipped_frame_count: 0,
last_frame_with_skips: -1,
frame_buffer: vec![],
frame_buffer_display_size: 64,
paused: false,
network_stats: vec![],
max_prediction_window: 0,
disconnected_players: vec![],
player_sync_state: default(),
}
}
}
impl NetworkDebug {
pub fn add_or_update_frame(&mut self, current: i32, confirmed: i32) {
if let Some(last) = self.frame_buffer.last_mut() {
if last.current_frame == current {
return;
}
}
self.current_frame = current;
self.confirmed_frame = confirmed;
self.frame_buffer
.push(NetworkFrameData::new(confirmed, current));
}
pub fn set_frozen(&mut self, frame: i32) {
let last = self.frame_buffer.last_mut().unwrap();
if last.current_frame == frame {
last.froze = true;
}
}
}
#[derive(Default, Clone)]
pub struct NetworkDebugMenuState {
pub open: bool,
}
pub static NETWORK_DEBUG_CHANNEL: Lazy<NetworkDebugChannel> =
Lazy::new(NetworkDebugChannel::default);
pub fn network_debug_window(
mut diagnostics: ResMutInit<NetworkDebug>,
egui_ctx: ResMut<EguiCtx>,
) {
let mut state = egui_ctx.get_state::<NetworkDebugMenuState>();
let show = &mut state.open;
if *show {
while let Ok(message) = NETWORK_DEBUG_CHANNEL.receiver.try_recv() {
if diagnostics.paused {
continue;
}
match message {
NetworkDebugMessage::ResetData => *diagnostics = NetworkDebug::default(),
NetworkDebugMessage::SkipFrame { frame, count } => {
diagnostics.last_frame_with_skips = frame;
diagnostics.last_skipped_frame_count = count;
}
NetworkDebugMessage::FrameUpdate {
current,
last_confirmed: confirmed,
} => {
diagnostics.add_or_update_frame(current, confirmed);
}
NetworkDebugMessage::FrameFroze { frame } => {
diagnostics.set_frozen(frame);
}
NetworkDebugMessage::NetworkStats { network_stats } => {
diagnostics.network_stats = network_stats;
}
NetworkDebugMessage::SetMaxPrediction(max_preiction_window) => {
diagnostics.max_prediction_window = max_preiction_window;
}
NetworkDebugMessage::DisconnectedPlayers(disconnected_players) => {
diagnostics.disconnected_players = disconnected_players;
}
NetworkDebugMessage::PlayerSync((sync_state, player)) => {
diagnostics.player_sync_state.insert(player, sync_state);
}
}
}
if *show {
let frame_localized = "frame";
let predicted_localized = "predicted";
egui::Window::new("Network Diagnostics")
.id(egui::Id::new("network-diagnostics"))
.open(show)
.show(&egui_ctx, |ui| {
ui.monospace(&format!(
"{label}: {current_frame}",
label = "Current Frame",
current_frame = diagnostics.current_frame
));
ui.monospace(&format!(
"{label}: {confirmed_frame}",
label = "Confirmed Frame",
confirmed_frame = diagnostics.confirmed_frame
));
if diagnostics.last_frame_with_skips != -1 {
ui.monospace(&format!(
"{label}: {last_skip_frame}",
label = "Last Frame With Skips",
last_skip_frame = diagnostics.last_frame_with_skips
));
ui.monospace(&format!(
"{label}: {skip_count}",
label = "Last Skipped Frame Count",
skip_count = diagnostics.last_skipped_frame_count
));
} else {
ui.monospace("No Frame Skips Detected");
}
let pause_label = if diagnostics.paused {
"resume"
} else {
"pause"
};
if ui.button(pause_label).clicked() {
diagnostics.paused = !diagnostics.paused;
}
let max_display_frame;
if let Some(last) = diagnostics.frame_buffer.last() {
max_display_frame = last.current_frame;
} else {
max_display_frame = diagnostics.frame_buffer_display_size as i32;
}
let min_display_frame =
max_display_frame - diagnostics.frame_buffer_display_size as i32;
let max_prediction_window = diagnostics.max_prediction_window;
Plot::new("Predicted Frames")
.allow_zoom(false)
.allow_scroll(false)
.allow_boxed_zoom(false)
.include_x(min_display_frame)
.include_x(max_display_frame)
.auto_bounds_y()
.include_y(max_prediction_window as f64)
.show_axes([false, true])
.y_grid_spacer(move |_grid_input| {
(0..(max_prediction_window + 1))
.map(|y| GridMark {
step_size: 1.0,
value: y as f64,
})
.collect()
})
.label_formatter({
move |_name, value| {
let frame_floor = value.x as i32;
format!("{frame_localized}: {frame_floor}")
}
})
.height(128.0)
.show(ui, |plot_ui| {
plot_ui.bar_chart(
BarChart::new(
diagnostics
.frame_buffer
.iter()
.map(|frame| {
let color = if frame.froze {
egui::Color32::YELLOW
} else {
egui::Color32::LIGHT_BLUE
};
Bar::new(
frame.current_frame as f64,
frame.predicted_frames as f64,
)
.fill(color)
})
.collect(),
)
.width(1.0)
.element_formatter(Box::new(move |bar, _chart| {
format!(
"{frame_localized}: {} {predicted_localized}: {}",
bar.argument as i32, bar.value as i32
)
})),
);
});
for (player_handle, stats) in diagnostics.network_stats.iter() {
let label = format!("{} {}", "player", player_handle);
CollapsingHeader::new(label)
.default_open(true)
.show(ui, |ui| {
if diagnostics.disconnected_players.contains(player_handle) {
ui.colored_label(Color::RED, "Disconnected!");
} else {
match diagnostics.player_sync_state.get(player_handle) {
Some(sync_state) => match sync_state {
PlayerSyncState::SyncInProgress => {
ui.colored_label(
Color::ORANGE,
"GGRS synchronization with player in progress...",
);
}
PlayerSyncState::Sychronized => {
ui.label("Synchronized with player.");
}
},
None => {
ui.colored_label(
Color::RED,
"Not synchronized with player.",
);
}
}
}
ui.monospace(&format!("{stats:?}"));
});
}
});
}
}
egui_ctx.set_state(state);
}