elura-netcode 0.2.4

Transport-independent tick synchronization and redundant input primitives for Elura
Documentation
use std::time::Duration;

use elura_netcode::{
    NetcodeError, TickSyncConfig, TickSyncRequest, TickSyncResponse, TickSyncSample,
    TickSynchronizer,
};

fn config() -> TickSyncConfig {
    let mut config = TickSyncConfig::default();
    config.tick_rate = 20;
    config.input_delay_ticks = 2;
    config.smoothing = 0.5;
    config.max_round_trip_time = Duration::from_secs(1);
    config.max_offset_correction_ticks = 2.0;
    config
}

#[test]
fn estimates_server_tick_from_network_rtt() {
    let mut sync = TickSynchronizer::new(config()).unwrap();
    let report = sync
        .observe(TickSyncSample {
            local_tick: 100.0,
            server_tick: 104,
            client_sent_at: Duration::from_secs(10),
            client_received_at: Duration::from_millis(10_120),
            server_processing_time: Duration::from_millis(20),
        })
        .unwrap();

    assert_eq!(report.round_trip_time, Duration::from_millis(120));
    assert_eq!(report.network_round_trip_time, Duration::from_millis(100));
    assert_eq!(report.one_way_delay, Duration::from_millis(50));
    assert!((report.estimated_server_tick - 105.0).abs() < 1e-9);
    assert!((report.offset_ticks - 5.0).abs() < 1e-9);
    assert_eq!(report.recommended_input_tick, 108);
}

#[test]
fn later_samples_are_smoothed_and_correction_is_bounded() {
    let mut sync = TickSynchronizer::new(config()).unwrap();
    sync.observe(TickSyncSample {
        local_tick: 10.0,
        server_tick: 10,
        client_sent_at: Duration::ZERO,
        client_received_at: Duration::ZERO,
        server_processing_time: Duration::ZERO,
    })
    .unwrap();

    let report = sync
        .observe(TickSyncSample {
            local_tick: 10.0,
            server_tick: 100,
            client_sent_at: Duration::from_secs(1),
            client_received_at: Duration::from_secs(1),
            server_processing_time: Duration::ZERO,
        })
        .unwrap();
    assert!((report.raw_offset_ticks - 90.0).abs() < 1e-9);
    assert!((report.offset_ticks - 1.0).abs() < 1e-9);
}

#[test]
fn invalid_sample_does_not_mutate_estimator() {
    let mut sync = TickSynchronizer::new(config()).unwrap();
    let result = sync.observe(TickSyncSample {
        local_tick: 1.0,
        server_tick: 1,
        client_sent_at: Duration::from_secs(2),
        client_received_at: Duration::from_secs(1),
        server_processing_time: Duration::ZERO,
    });
    assert!(matches!(result, Err(NetcodeError::InvalidSample(_))));
    assert_eq!(sync.samples(), 0);
    assert_eq!(sync.network_round_trip_time(), None);
}

#[test]
fn response_derives_server_processing_duration() {
    let response = TickSyncResponse {
        sequence: 7,
        client_sent_at: Duration::from_secs(4),
        server_received_at: Duration::from_secs(20),
        server_sent_at: Duration::from_millis(20_010),
        server_tick: 80,
    };
    let request = TickSyncRequest {
        sequence: 7,
        client_sent_at: Duration::from_secs(4),
    };
    let sample = response
        .sample(request, Duration::from_millis(4_110), 75.0)
        .unwrap();
    assert_eq!(sample.server_processing_time, Duration::from_millis(10));
    assert_eq!(sample.server_tick, 80);
}

#[test]
fn response_must_match_the_original_probe() {
    let response = TickSyncResponse {
        sequence: 8,
        client_sent_at: Duration::from_secs(4),
        server_received_at: Duration::from_secs(20),
        server_sent_at: Duration::from_secs(20),
        server_tick: 80,
    };
    let request = TickSyncRequest {
        sequence: 7,
        client_sent_at: Duration::from_secs(4),
    };
    assert!(matches!(
        response.sample(request, Duration::from_secs(5), 75.0),
        Err(NetcodeError::InvalidSample(_))
    ));
}