use crate::ping::manager::PingManager;
use crate::timeline::sync::{
SyncAdjustment, SyncConfig, SyncContext, SyncTargetTimeline, SyncedTimeline,
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use bevy_reflect::Reflect;
use core::time::Duration;
use lightyear_core::tick::{Tick, TickDuration};
use lightyear_core::time::{TickDelta, TickInstant};
use lightyear_core::timeline::{NetworkTimeline, SyncEvent, Timeline, TimelineConfig};
use lightyear_link::{Link, LinkStats};
use tracing::trace;
#[derive(Debug, Component, Reflect)]
#[require(InputTimeline)]
pub struct InputTimelineConfig {
pub(crate) sync: SyncConfig,
pub(crate) input_delay_config: InputDelayConfig,
}
impl InputTimelineConfig {
pub fn new(sync_config: SyncConfig, input_delay: InputDelayConfig) -> Self {
Self {
sync: sync_config,
input_delay_config: input_delay,
}
}
pub fn with_input_delay(mut self, input_delay: InputDelayConfig) -> Self {
self.input_delay_config = input_delay;
self
}
pub fn with_sync_config(mut self, sync_config: SyncConfig) -> Self {
self.sync = sync_config;
self
}
#[inline]
pub fn is_lockstep(&self) -> bool {
self.input_delay_config.is_lockstep()
}
pub(crate) fn recompute_input_delay_on_sync(
trigger: On<SyncEvent<InputTimelineConfig>>,
tick_duration: Res<TickDuration>,
mut query: Query<(&Link, &mut InputTimeline, &InputTimelineConfig)>,
) {
if let Ok((link, mut timeline, config)) = query.get_mut(trigger.entity) {
timeline.input_delay_ticks = config.input_delay_config.input_delay_ticks(
link.stats,
&config.sync,
tick_duration.0,
);
trace!(
"Recomputing input delay on sync event! Input delay ticks: {}",
timeline.input_delay_ticks
);
}
}
pub(crate) fn recompute_input_delay_on_config_update(
trigger: On<Insert, InputTimelineConfig>,
tick_duration: Res<TickDuration>,
mut query: Query<(&Link, &mut InputTimeline, &InputTimelineConfig)>,
) {
if let Ok((link, mut timeline, config)) = query.get_mut(trigger.entity) {
timeline.input_delay_ticks = config.input_delay_config.input_delay_ticks(
link.stats,
&config.sync,
tick_duration.0,
);
trace!(
"Recomputing input delay on config update! Input delay ticks: {}. Config: {:?}",
timeline.input_delay_ticks, config.input_delay_config
);
}
}
}
impl Default for InputTimelineConfig {
fn default() -> Self {
Self {
sync: SyncConfig::default(),
input_delay_config: InputDelayConfig::no_input_delay(),
}
}
}
#[derive(Debug, Reflect)]
pub struct InputContext {
sync: SyncContext,
input_delay_ticks: u16,
relative_speed: f32,
is_synced: bool,
}
impl InputContext {
pub fn input_delay(&self) -> u16 {
self.input_delay_ticks
}
}
impl Default for InputContext {
fn default() -> Self {
Self {
sync: SyncContext::default(),
input_delay_ticks: 0,
relative_speed: 1.0,
is_synced: false,
}
}
}
#[derive(Debug, Clone, Copy, Reflect)]
pub struct InputDelayConfig {
pub minimum_input_delay_ticks: u16,
pub maximum_input_delay_before_prediction: u16,
pub maximum_predicted_ticks: u16,
}
impl InputDelayConfig {
pub fn balanced() -> Self {
Self {
minimum_input_delay_ticks: 0,
maximum_input_delay_before_prediction: 3,
maximum_predicted_ticks: 7,
}
}
pub fn no_input_delay() -> Self {
Self {
minimum_input_delay_ticks: 0,
maximum_input_delay_before_prediction: 0,
maximum_predicted_ticks: 100,
}
}
#[inline]
pub fn is_lockstep(&self) -> bool {
self.maximum_predicted_ticks == 0
}
pub fn no_prediction() -> Self {
Self {
minimum_input_delay_ticks: 0,
maximum_input_delay_before_prediction: 0,
maximum_predicted_ticks: 0,
}
}
pub fn fixed_input_delay(delay_ticks: u16) -> Self {
Self {
minimum_input_delay_ticks: delay_ticks,
maximum_input_delay_before_prediction: delay_ticks,
maximum_predicted_ticks: 100,
}
}
fn input_delay_ticks(
&self,
link_stats: LinkStats,
sync_config: &SyncConfig,
tick_interval: Duration,
) -> u16 {
let jitter_margin = sync_config.jitter_margin(link_stats.jitter);
let effective_rtt = link_stats.rtt + jitter_margin;
assert!(
self.minimum_input_delay_ticks <= self.maximum_input_delay_before_prediction,
"The minimum amount of input_delay should be less than or equal to the maximum_input_delay_before_prediction"
);
let mut rtt_ticks =
(effective_rtt.as_nanos() as f32 / tick_interval.as_nanos() as f32).ceil() as u16;
if self.is_lockstep() {
rtt_ticks += 2;
}
if rtt_ticks <= self.minimum_input_delay_ticks {
return self.minimum_input_delay_ticks;
}
if rtt_ticks <= self.maximum_input_delay_before_prediction {
return rtt_ticks;
}
if rtt_ticks <= (self.maximum_predicted_ticks + self.maximum_input_delay_before_prediction)
{
self.maximum_input_delay_before_prediction
} else {
rtt_ticks - self.maximum_predicted_ticks
}
}
}
#[derive(Component, Deref, DerefMut, Default, Debug, Reflect)]
pub struct InputTimeline(pub Timeline<InputTimelineConfig>);
impl TimelineConfig for InputTimelineConfig {
type Context = InputContext;
type Timeline = InputTimeline;
}
impl SyncedTimeline for InputTimeline {
fn sync_objective<T: SyncTargetTimeline>(
&self,
remote: &T,
config: &Self::Config,
ping_manager: &PingManager,
tick_duration: Duration,
) -> TickInstant {
let remote = remote.current_estimate();
let network_delay = TickDelta::from_duration(ping_manager.rtt() / 2, tick_duration);
let jitter_margin = TickDelta::from_duration(
config.sync.jitter_margin(ping_manager.jitter()),
tick_duration,
);
let input_delay: TickDelta = Tick(self.context.input_delay_ticks).into();
let mut obj = remote + network_delay + jitter_margin - input_delay;
if obj < remote {
obj = remote + TickDelta::from_i16(1)
}
trace!(
?remote,
?network_delay,
?jitter_margin,
?input_delay,
"InputTimeline objective: {:?}",
obj
);
obj
}
fn resync(&mut self, sync_objective: TickInstant) -> i16 {
let now = self.now();
self.now = sync_objective;
(sync_objective - now).to_i16()
}
fn sync<T: SyncTargetTimeline>(
&mut self,
main: &T,
config: &Self::Config,
ping_manager: &PingManager,
tick_duration: Duration,
) -> Option<i16> {
if ping_manager.pongs_recv < config.sync.handshake_pings as u32 {
return None;
}
let now = self.now();
let objective = self.sync_objective(main, config, ping_manager, tick_duration);
let error = now - objective;
let error_ticks = error.to_f32();
let adjustment = if !self.is_synced {
SyncAdjustment::Resync
} else {
self.sync.speed_adjustment(&config.sync, error_ticks)
};
trace!(
?now,
?objective,
?adjustment,
?error_ticks,
error_margin = ?config.sync.error_margin,
max_error_margin = ?config.sync.max_error_margin,
"InputTimeline sync"
);
self.is_synced = true;
match adjustment {
SyncAdjustment::Resync => {
return Some(self.resync(objective));
}
SyncAdjustment::SpeedAdjust(ratio) => {
self.set_relative_speed(ratio);
}
SyncAdjustment::DoNothing => {
let current = self.relative_speed();
if (current - 1.0).abs() > 0.001 {
let new_speed = current + (1.0 - current) * 0.1;
self.set_relative_speed(new_speed);
}
}
}
None
}
fn is_synced(&self) -> bool {
self.is_synced
}
fn relative_speed(&self) -> f32 {
self.relative_speed
}
fn set_relative_speed(&mut self, ratio: f32) {
self.relative_speed = ratio;
}
fn reset(&mut self) {
trace!("Resetting InputTimeline");
self.is_synced = false;
self.relative_speed = 1.0;
self.now = Default::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_input_delay_config() {
let sync_config = SyncConfig::default();
let config_1 = InputDelayConfig {
minimum_input_delay_ticks: 2,
maximum_input_delay_before_prediction: 3,
maximum_predicted_ticks: 7,
};
assert_eq!(
config_1.input_delay_ticks(
LinkStats {
rtt: Duration::from_millis(10),
..default()
},
&sync_config,
Duration::from_millis(16)
),
2
);
assert_eq!(
config_1.input_delay_ticks(
LinkStats {
rtt: Duration::from_millis(60),
..default()
},
&sync_config,
Duration::from_millis(16)
),
3
);
assert_eq!(
config_1.input_delay_ticks(
LinkStats {
rtt: Duration::from_millis(200),
..default()
},
&sync_config,
Duration::from_millis(16)
),
6
);
assert_eq!(
config_1.input_delay_ticks(
LinkStats {
rtt: Duration::from_millis(300),
..default()
},
&sync_config,
Duration::from_millis(16)
),
12
);
}
}