Skip to content

Commit

Permalink
Multiplayer: impl ping stats
Browse files Browse the repository at this point in the history
  • Loading branch information
Indy2222 committed Jul 8, 2023
1 parent 06eeb60 commit a51a750
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/multiplayer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ de_gui.workspace = true
de_net.workspace = true

# Other
ahash.workspace = true
async-std.workspace = true
bevy.workspace = true
bincode.workspace = true
Expand Down
3 changes: 3 additions & 0 deletions crates/multiplayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use bevy::{app::PluginGroupBuilder, prelude::*};
use game::GamePlugin;
use lifecycle::LifecyclePlugin;
use messages::MessagesPlugin;
use stats::StatsPlugin;

pub use crate::{
config::{NetGameConf, ServerPort},
Expand All @@ -25,6 +26,7 @@ mod lifecycle;
mod messages;
mod netstate;
mod network;
mod stats;

pub struct MultiplayerPluginGroup;

Expand All @@ -36,5 +38,6 @@ impl PluginGroup for MultiplayerPluginGroup {
.add(NetworkPlugin)
.add(MessagesPlugin)
.add(GamePlugin)
.add(StatsPlugin)
}
}
197 changes: 197 additions & 0 deletions crates/multiplayer/src/stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use std::{collections::VecDeque, time::Duration};

use bevy::prelude::*;
use de_core::baseset::GameSet;
use de_net::{FromGame, ToGame};

use crate::{
messages::{FromGameServerEvent, MessagesSet, ToGameServerEvent},
netstate::NetState,
};

const PING_INTERVAL: Duration = Duration::from_secs(10);
const MAX_DELAY_INTERVALS: u32 = 10;

pub(crate) struct StatsPlugin;

impl Plugin for StatsPlugin {
fn build(&self, app: &mut App) {
app.add_system(setup.in_schedule(OnEnter(NetState::Joined)))
.add_system(cleanup.in_schedule(OnExit(NetState::Joined)))
.add_system(
ping.in_base_set(GameSet::PostUpdate)
.before(MessagesSet::SendMessages),
)
.add_system(
pong.in_base_set(GameSet::PreMovement)
.run_if(on_event::<FromGameServerEvent>())
.in_set(StatsSet::Pong)
.after(MessagesSet::RecvMessages),
)
.add_system(
unresolved
.in_base_set(GameSet::PreMovement)
.after(StatsSet::Pong),
);
}
}

#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemSet)]
enum StatsSet {
Pong,
}

#[derive(Resource)]
struct PingTimer(Timer);

#[derive(Resource)]
struct PingTracker {
counter: u32,
times: VecDeque<PingRecord>,
}

struct PingRecord {
resolved: bool,
id: u32,
time: Duration,
}

impl PingTracker {
fn new() -> Self {
Self {
counter: 0,
times: VecDeque::new(),
}
}

/// Register a new ping send time and returns a new unique ID (wrapping)
/// for the ping.
fn start(&mut self, time: Duration) -> u32 {
let id = self.counter;
self.counter = id.wrapping_add(1);
self.times.push_back(PingRecord {
resolved: false,
id,
time,
});
id
}

/// Marks a ping record as resolved.
fn resolve(&mut self, id: u32) -> Option<Duration> {
for record in self.times.iter_mut() {
if record.id == id {
if record.resolved {
return None;
} else {
record.resolved = true;
return Some(record.time);
}
}
}

None
}

/// Trims the history of sent pings and pushes non-resolved trimmed ping
/// IDs to `ids`.
///
/// # Arguments
///
/// * `history` - maximum number of pings (resolved and unresolved) to
/// keep.
///
/// * `ids` - unresolved trimmed pings will be pushed to this Vec.
fn trim(&mut self, history: u32, ids: &mut Vec<u32>) {
let oldest_ok = self.counter.wrapping_sub(history);
while let Some(record) = self.times.front() {
if record.id >= oldest_ok {
break;
}
let record = self.times.pop_front().unwrap();
if !record.resolved {
ids.push(record.id);
}
}
}
}

fn setup(mut commands: Commands) {
commands.insert_resource(PingTimer(Timer::new(PING_INTERVAL, TimerMode::Repeating)));
commands.insert_resource(PingTracker::new());
}

fn cleanup(mut commands: Commands) {
commands.remove_resource::<PingTimer>();
commands.remove_resource::<PingTracker>();
}

fn ping(
time: Res<Time>,
mut timer: ResMut<PingTimer>,
mut tracker: ResMut<PingTracker>,
mut messages: EventWriter<ToGameServerEvent>,
) {
timer.0.tick(time.delta());
for _ in 0..timer.0.times_finished_this_tick() {
let id = tracker.start(time.delta());
messages.send(ToGame::Ping(id).into());
}
}

fn pong(
time: Res<Time>,
mut tracker: ResMut<PingTracker>,
mut messages: EventReader<FromGameServerEvent>,
) {
for event in messages.iter() {
if let FromGame::Pong(id) = event.message() {
match tracker.resolve(*id) {
Some(send_time) => {
let round_trip = time.delta() - send_time;
info!(
"Received Pong({}) with {}ms round trip..",
*id,
round_trip.as_millis()
);
}
None => {
warn!("Receive non-registered Pong({}).", *id);
}
}
}
}
}

fn unresolved(mut buffer: Local<Vec<u32>>, mut tracker: ResMut<PingTracker>) {
buffer.clear();
tracker.trim(MAX_DELAY_INTERVALS, &mut buffer);

for &id in buffer.iter() {
error!("Ping({id}) was not responded in time.");
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_tracker() {
let mut tracker = PingTracker::new();

assert_eq!(tracker.start(Duration::from_millis(500)), 0);
assert_eq!(tracker.start(Duration::from_millis(800)), 1);
assert_eq!(tracker.start(Duration::from_millis(900)), 2);

assert_eq!(tracker.resolve(2).unwrap(), Duration::from_millis(900));
assert_eq!(tracker.start(Duration::from_millis(1100)), 3);
assert_eq!(tracker.resolve(1).unwrap(), Duration::from_millis(800));
assert_eq!(tracker.start(Duration::from_millis(1300)), 4);
assert_eq!(tracker.start(Duration::from_millis(1800)), 5);

let mut ids = Vec::new();
tracker.trim(2, &mut ids);
assert_eq!(ids, vec![0, 3]);
}
}

0 comments on commit a51a750

Please sign in to comment.