From 9f287620f01340004fe1140b3273cd7ed6225a6c Mon Sep 17 00:00:00 2001 From: Charles Bournhonesque Date: Wed, 29 Nov 2023 18:22:45 -0500 Subject: [PATCH] fix CI and add component removal test --- .github/workflows/main.yaml | 6 + Cargo.toml | 2 +- lightyear/Cargo.toml | 2 + lightyear/src/client/connection.rs | 5 + .../src/client/interpolation/interpolate.rs | 255 ++++++++++ lightyear/src/client/plugin.rs | 4 +- lightyear/src/client/prediction/despawn.rs | 320 +++++++++++++ lightyear/src/client/prediction/rollback.rs | 437 ++++++++++++++++- lightyear/src/client/resource.rs | 13 +- lightyear/src/connection/events.rs | 2 +- lightyear/src/connection/mod.rs | 7 +- lightyear/src/lib.rs | 6 + lightyear/src/protocol/mod.rs | 130 +++--- lightyear/src/server/events.rs | 2 +- lightyear/src/server/plugin.rs | 1 - .../src/shared/replication/entity_map.rs | 4 +- lightyear/src/shared/replication/manager.rs | 4 +- lightyear/src/shared/replication/mod.rs | 94 ++++ {tests/src => lightyear/src/tests}/client.rs | 7 +- .../src/lib.rs => lightyear/src/tests/mod.rs | 0 .../src => lightyear/src/tests}/protocol.rs | 17 +- {tests/src => lightyear/src/tests}/server.rs | 6 +- {tests/src => lightyear/src/tests}/stepper.rs | 38 +- lightyear/src/transport/io.rs | 98 ++-- tests/Cargo.toml | 24 - tests/tests/interpolation_simple.rs | 251 ---------- tests/tests/prediction_despawn.rs | 334 ------------- .../tests/prediction_remove_add_component.rs | 442 ------------------ 28 files changed, 1299 insertions(+), 1212 deletions(-) rename {tests/src => lightyear/src/tests}/client.rs (94%) rename tests/src/lib.rs => lightyear/src/tests/mod.rs (100%) rename {tests/src => lightyear/src/tests}/protocol.rs (80%) rename {tests/src => lightyear/src/tests}/server.rs (93%) rename {tests/src => lightyear/src/tests}/stepper.rs (82%) delete mode 100644 tests/Cargo.toml delete mode 100644 tests/tests/interpolation_simple.rs delete mode 100644 tests/tests/prediction_despawn.rs delete mode 100644 tests/tests/prediction_remove_add_component.rs diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4b27328d6..e327e1c0c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -35,6 +35,9 @@ jobs: name: Lint runs-on: ubuntu-latest steps: + - name: Install alsa + run: sudo apt-get install -y libasound2-dev; sudo apt-get install -y libwebkit2gtk-4.0 ; + - name: Clone repo uses: actions/checkout@v4 @@ -54,6 +57,9 @@ jobs: name: Doctest runs-on: ubuntu-latest steps: + - name: Install alsa + run: sudo apt-get install -y libasound2-dev; sudo apt-get install -y libwebkit2gtk-4.0 ; + - name: Clone repo uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index a4bfa2c71..fd5dea9e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["examples", "macros", "lightyear", "tests"] +members = ["examples", "macros", "lightyear"] diff --git a/lightyear/Cargo.toml b/lightyear/Cargo.toml index a5f4110c5..6dbbb4626 100644 --- a/lightyear/Cargo.toml +++ b/lightyear/Cargo.toml @@ -67,8 +67,10 @@ rand = "0.8" ringbuffer = "0.15" bevy = { version = "0.12", default-features = false } taplo-cli = "0.8.1" +derive_more = "0.99.17" [dev-dependencies] +derive_more = { version = "0.99", features = ["add", "mul"] } mock_instant = { version = "0.3.1" } tracing-subscriber = "0.3.17" bitvec = "1.0" diff --git a/lightyear/src/client/connection.rs b/lightyear/src/client/connection.rs index 24a46519e..5ec05f7d5 100644 --- a/lightyear/src/client/connection.rs +++ b/lightyear/src/client/connection.rs @@ -78,4 +78,9 @@ impl Connection

{ } Ok(()) } + + #[cfg(test)] + pub fn base(&self) -> &crate::connection::Connection

{ + &self.base + } } diff --git a/lightyear/src/client/interpolation/interpolate.rs b/lightyear/src/client/interpolation/interpolate.rs index 6ea917927..967b96aff 100644 --- a/lightyear/src/client/interpolation/interpolate.rs +++ b/lightyear/src/client/interpolation/interpolate.rs @@ -100,3 +100,258 @@ pub(crate) fn interpolate( } } } + +#[cfg(test)] +mod tests { + // #![allow(unused_imports)] + // #![allow(unused_variables)] + // #![allow(dead_code)] + // + // use std::net::SocketAddr; + // use std::str::FromStr; + // use std::time::{Duration, Instant}; + // + // use bevy::log::LogPlugin; + // use bevy::prelude::{ + // App, Commands, Entity, EventReader, FixedUpdate, IntoSystemConfigs, PluginGroup, Query, Real, + // Res, ResMut, Startup, Time, With, + // }; + // use bevy::time::TimeUpdateStrategy; + // use bevy::winit::WinitPlugin; + // use bevy::{DefaultPlugins, MinimalPlugins}; + // use lightyear::client::components::Confirmed; + // use tracing::{debug, info}; + // use tracing_subscriber::fmt::format::FmtSpan; + // + // use lightyear::_reexport::*; + // use lightyear::prelude::client::*; + // use lightyear::prelude::*; + // use lightyear_tests::protocol::{protocol, Channel2, Component1, Component2, MyInput, MyProtocol}; + // use lightyear_tests::stepper::{BevyStepper, Step}; + // + // fn setup() -> (BevyStepper, Entity, Entity, u16) { + // let frame_duration = Duration::from_millis(10); + // let tick_duration = Duration::from_millis(10); + // let shared_config = SharedConfig { + // enable_replication: false, + // tick: TickConfig::new(tick_duration), + // ..Default::default() + // }; + // let link_conditioner = LinkConditionerConfig { + // incoming_latency: Duration::from_millis(40), + // incoming_jitter: Duration::from_millis(5), + // incoming_loss: 0.05, + // }; + // let sync_config = SyncConfig::default().speedup_factor(1.0); + // let prediction_config = PredictionConfig::default().disable(true); + // let interpolation_delay = Duration::from_millis(100); + // let interpolation_config = + // InterpolationConfig::default().with_delay(InterpolationDelay::Delay(interpolation_delay)); + // let mut stepper = BevyStepper::new( + // shared_config, + // sync_config, + // prediction_config, + // interpolation_config, + // link_conditioner, + // frame_duration, + // ); + // stepper.client_mut().set_synced(); + // + // // Create a confirmed entity + // let confirmed = stepper + // .client_app + // .world + // .spawn((Component1(0.0), ShouldBeInterpolated)) + // .id(); + // + // // Tick once + // stepper.frame_step(); + // assert_eq!(stepper.client().tick(), Tick(1)); + // let interpolated = stepper + // .client_app + // .world + // .get::(confirmed) + // .unwrap() + // .interpolated + // .unwrap(); + // + // assert_eq!( + // stepper + // .client_app + // .world + // .get::(confirmed) + // .unwrap(), + // &Component1(0.0) + // ); + // + // // check that the interpolated entity got spawned + // assert_eq!( + // stepper + // .client_app + // .world + // .get::(interpolated) + // .unwrap() + // .confirmed_entity, + // confirmed + // ); + // + // // check that the component history got created and is empty + // let history = ConfirmedHistory::::new(); + // assert_eq!( + // stepper + // .client_app + // .world + // .get::>(interpolated) + // .unwrap(), + // &history, + // ); + // // check that the confirmed component got replicated + // assert_eq!( + // stepper + // .client_app + // .world + // .get::(interpolated) + // .unwrap(), + // &Component1(0.0) + // ); + // // check that the interpolate status got updated + // assert_eq!( + // stepper + // .client_app + // .world + // .get::>(interpolated) + // .unwrap(), + // &InterpolateStatus:: { + // start: None, + // end: (Tick(0), Component1(0.0)).into(), + // current: Tick(1) - interpolation_tick_delay, + // } + // ); + // (stepper, confirmed, interpolated, interpolation_tick_delay) + // } + // + // // Test interpolation + // #[test] + // fn test_interpolation() -> anyhow::Result<()> { + // let (mut stepper, confirmed, interpolated, interpolation_tick_delay) = setup(); + // // reach interpolation start tick + // stepper.frame_step(); + // stepper.frame_step(); + // // check that the interpolate status got updated (end becomes start) + // assert_eq!( + // stepper + // .client_app + // .world + // .get::>(interpolated) + // .unwrap(), + // &InterpolateStatus:: { + // start: (Tick(0), Component1(0.0)).into(), + // end: None, + // current: Tick(3) - interpolation_tick_delay, + // } + // ); + // + // // receive server update + // stepper + // .client_mut() + // .set_latest_received_server_tick(Tick(2)); + // stepper + // .client_app + // .world + // .get_entity_mut(confirmed) + // .unwrap() + // .get_mut::() + // .unwrap() + // .0 = 2.0; + // + // stepper.frame_step(); + // // check that interpolation is working correctly + // assert_eq!( + // stepper + // .client_app + // .world + // .get::>(interpolated) + // .unwrap(), + // &InterpolateStatus:: { + // start: (Tick(0), Component1(0.0)).into(), + // end: (Tick(2), Component1(2.0)).into(), + // current: Tick(4) - interpolation_tick_delay, + // } + // ); + // assert_eq!( + // stepper + // .client_app + // .world + // .get::(interpolated) + // .unwrap(), + // &Component1(1.0) + // ); + // stepper.frame_step(); + // assert_eq!( + // stepper + // .client_app + // .world + // .get::>(interpolated) + // .unwrap(), + // &InterpolateStatus:: { + // start: (Tick(2), Component1(2.0)).into(), + // end: None, + // current: Tick(5) - interpolation_tick_delay, + // } + // ); + // assert_eq!( + // stepper + // .client_app + // .world + // .get::(interpolated) + // .unwrap(), + // &Component1(2.0) + // ); + // Ok(()) + // } + // + // // We are in the situation: S1 < I + // // where S1 is a confirmed ticks, and I is the interpolated tick + // // and we receive S1 < S2 < I + // // Then we should now start interpolating from S2 + // #[test] + // fn test_received_more_recent_start() -> anyhow::Result<()> { + // let (mut stepper, confirmed, interpolated, interpolation_tick_delay) = setup(); + // + // // reach interpolation start tick + // stepper.frame_step(); + // stepper.frame_step(); + // stepper.frame_step(); + // stepper.frame_step(); + // assert_eq!(stepper.client().tick(), Tick(5)); + // + // // receive server update + // stepper + // .client_mut() + // .set_latest_received_server_tick(Tick(1)); + // stepper + // .client_app + // .world + // .get_entity_mut(confirmed) + // .unwrap() + // .get_mut::() + // .unwrap() + // .0 = 1.0; + // + // stepper.frame_step(); + // // check the status uses the more recent server update + // assert_eq!( + // stepper + // .client_app + // .world + // .get::>(interpolated) + // .unwrap(), + // &InterpolateStatus:: { + // start: (Tick(1), Component1(1.0)).into(), + // end: None, + // current: Tick(6) - interpolation_tick_delay, + // } + // ); + // Ok(()) + // } +} diff --git a/lightyear/src/client/plugin.rs b/lightyear/src/client/plugin.rs index 715eb85c5..91ed2567f 100644 --- a/lightyear/src/client/plugin.rs +++ b/lightyear/src/client/plugin.rs @@ -9,7 +9,7 @@ use bevy::prelude::{ Plugin as PluginType, PostUpdate, PreUpdate, }; -use crate::client::events::{ConnectEvent, DisconnectEvent, EntitySpawnEvent}; +use crate::client::events::{ConnectEvent, DisconnectEvent, EntityDespawnEvent, EntitySpawnEvent}; use crate::client::input::InputPlugin; use crate::client::interpolation::plugin::InterpolationPlugin; use crate::client::prediction::plugin::{is_in_rollback, PredictionPlugin}; @@ -89,7 +89,6 @@ impl PluginType for ClientPlugin

{ )) // RESOURCES // .insert_resource(client) - .init_resource::() // SYSTEM SETS // .configure_sets(PreUpdate, MainSet::Receive) .configure_sets( @@ -109,6 +108,7 @@ impl PluginType for ClientPlugin

{ .add_event::() .add_event::() .add_event::() + .add_event::() // SYSTEMS // .add_systems( PreUpdate, diff --git a/lightyear/src/client/prediction/despawn.rs b/lightyear/src/client/prediction/despawn.rs index 294038170..2727a2dbc 100644 --- a/lightyear/src/client/prediction/despawn.rs +++ b/lightyear/src/client/prediction/despawn.rs @@ -103,3 +103,323 @@ pub(crate) fn remove_despawn_marker( .remove::(); } } + +#[cfg(test)] +mod tests { + use crate::_reexport::*; + use crate::prelude::client::*; + use crate::prelude::*; + use crate::tests::protocol::*; + use crate::tests::stepper::{BevyStepper, Step}; + use bevy::prelude::*; + use std::time::Duration; + + fn increment_component_and_despawn( + mut commands: Commands, + mut query: Query<(Entity, &mut Component1), With>, + ) { + for (entity, mut component) in query.iter_mut() { + component.0 += 1.0; + if component.0 == 5.0 { + commands.entity(entity).prediction_despawn::(); + } + } + } + + // Test that if a predicted entity gets despawned erroneously + // We are still able to rollback properly (the rollback re-adds the predicted entity, or prevents it from despawning) + #[test] + fn test_despawned_predicted_rollback() -> anyhow::Result<()> { + let frame_duration = Duration::from_millis(10); + let tick_duration = Duration::from_millis(10); + let shared_config = SharedConfig { + enable_replication: false, + tick: TickConfig::new(tick_duration), + ..Default::default() + }; + let link_conditioner = LinkConditionerConfig { + incoming_latency: Duration::from_millis(40), + incoming_jitter: Duration::from_millis(5), + incoming_loss: 0.05, + }; + let sync_config = SyncConfig::default().speedup_factor(1.0); + let prediction_config = PredictionConfig::default().disable(false); + let interpolation_delay = Duration::from_millis(100); + let interpolation_config = InterpolationConfig::default() + .with_delay(InterpolationDelay::Delay(interpolation_delay)); + let mut stepper = BevyStepper::new( + shared_config, + sync_config, + prediction_config, + interpolation_config, + link_conditioner, + frame_duration, + ); + stepper.client_mut().set_synced(); + stepper.client_app.add_systems( + FixedUpdate, + increment_component_and_despawn.in_set(FixedUpdateSet::Main), + ); + + // Create a confirmed entity + let confirmed = stepper + .client_app + .world + .spawn((Component1(0.0), ShouldBePredicted)) + .id(); + + // Tick once + stepper.frame_step(); + assert_eq!(stepper.client().tick(), Tick(1)); + let predicted = stepper + .client_app + .world + .get::(confirmed) + .unwrap() + .predicted + .unwrap(); + + // check that the predicted entity got spawned + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap() + .confirmed_entity, + confirmed + ); + + // check that the component history got created + let mut history = PredictionHistory::::default(); + history + .buffer + .add_item(Tick(0), ComponentState::Updated(Component1(0.0))); + history + .buffer + .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + // check that the confirmed component got replicated + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap(), + &Component1(1.0) + ); + + // advance five more frames, so that the component gets removed on predicted + for i in 0..5 { + stepper.frame_step(); + } + assert_eq!(stepper.client().tick(), Tick(6)); + + // check that the component got removed on predicted + assert!(stepper + .client_app + .world + .get::(predicted) + .is_none()); + // // check that predicted has the despawn marker + // assert_eq!( + // stepper + // .client_app + // .world + // .get::(predicted) + // .unwrap(), + // &PredictionDespawnMarker { + // death_tick: Tick(5) + // } + // ); + // check that the component history is still there and that the value of the component history is correct + let mut history = PredictionHistory::::default(); + for i in 0..5 { + history + .buffer + .add_item(Tick(i), ComponentState::Updated(Component1(i as f32))); + } + history.buffer.add_item(Tick(5), ComponentState::Removed); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + + // create a rollback situation + stepper.client_mut().set_synced(); + stepper + .client_mut() + .set_latest_received_server_tick(Tick(3)); + stepper + .client_app + .world + .get_mut::(confirmed) + .unwrap() + .0 = 1.0; + // update without incrementing time, because we want to force a rollback check + stepper.client_app.update(); + + // check that rollback happened + // predicted exists, and got the component re-added + stepper + .client_app + .world + .get_mut::(predicted) + .unwrap() + .0 = 4.0; + // check that the history is how we expect after rollback + let mut history = PredictionHistory::::default(); + for i in 3..7 { + history + .buffer + .add_item(Tick(i), ComponentState::Updated(Component1(i as f32 - 2.0))); + } + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history + ); + Ok(()) + } + + // Test that if another entity gets added during prediction, + // - either it should get despawned if there is a rollback that doesn't add it anymore + // - or we should just let it live? (imagine it's audio, etc.) + + fn increment_component_and_despawn_both( + mut commands: Commands, + mut query: Query<(Entity, &mut Component1)>, + ) { + for (entity, mut component) in query.iter_mut() { + component.0 += 1.0; + if component.0 == 5.0 { + commands.entity(entity).prediction_despawn::(); + } + } + } + + // Test that if a confirmed entity gets despawned, + // the corresponding predicted entity gets despawned as well + // Test that if a predicted entity gets despawned erroneously + // We are still able to rollback properly (the rollback re-adds the predicted entity, or prevents it from despawning) + #[test] + fn test_despawned_confirmed_rollback() -> anyhow::Result<()> { + let frame_duration = Duration::from_millis(10); + let tick_duration = Duration::from_millis(10); + let shared_config = SharedConfig { + enable_replication: false, + tick: TickConfig::new(tick_duration), + ..Default::default() + }; + let link_conditioner = LinkConditionerConfig { + incoming_latency: Duration::from_millis(40), + incoming_jitter: Duration::from_millis(5), + incoming_loss: 0.05, + }; + let sync_config = SyncConfig::default().speedup_factor(1.0); + let prediction_config = PredictionConfig::default().disable(false); + let interpolation_delay = Duration::from_millis(100); + let interpolation_config = InterpolationConfig::default() + .with_delay(InterpolationDelay::Delay(interpolation_delay)); + let mut stepper = BevyStepper::new( + shared_config, + sync_config, + prediction_config, + interpolation_config, + link_conditioner, + frame_duration, + ); + stepper.client_app.add_systems( + FixedUpdate, + increment_component_and_despawn_both.in_set(FixedUpdateSet::Main), + ); + + // Create a confirmed entity + let confirmed = stepper + .client_app + .world + .spawn((Component1(0.0), ShouldBePredicted)) + .id(); + + // Tick once + stepper.frame_step(); + assert_eq!(stepper.client().tick(), Tick(1)); + let predicted = stepper + .client_app + .world + .get::(confirmed) + .unwrap() + .predicted + .unwrap(); + + // check that the predicted entity got spawned + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap() + .confirmed_entity, + confirmed + ); + + // check that the component history got created + let mut history = PredictionHistory::::default(); + history + .buffer + .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + // check that the confirmed component got replicated + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap(), + &Component1(1.0) + ); + + // create a situation where the confirmed entity gets despawned during FixedUpdate::Main + stepper.client_mut().set_synced(); + stepper + .client_mut() + .set_latest_received_server_tick(Tick(0)); + // we set it to 5 so that it gets despawned during FixedUpdate::Main + stepper + .client_app + .world + .get_mut::(confirmed) + .unwrap() + .0 = 4.0; + // update without incrementing time, because we want to force a rollback check + stepper.frame_step(); + + // check that rollback happened + // confirmed and predicted both got despawned + assert!(stepper.client_app.world.get_entity(confirmed).is_none()); + assert!(stepper.client_app.world.get_entity(predicted).is_none()); + + Ok(()) + } +} diff --git a/lightyear/src/client/prediction/rollback.rs b/lightyear/src/client/prediction/rollback.rs index 214bfebfa..fc9a4c4e1 100644 --- a/lightyear/src/client/prediction/rollback.rs +++ b/lightyear/src/client/prediction/rollback.rs @@ -95,11 +95,6 @@ pub(crate) fn client_rollback_check( return; } if !client.is_synced() || !client.received_new_server_tick() { - trace!( - sync = ?client.is_synced(), - received_new_server_tick = ?client.received_new_server_tick(), - duration_since_last_server_tick = ?client.duration_since_latest_received_server_tick(), - "Not running rollback check because client is not synced or didn't receive new server tick"); return; } // TODO: can just enable bevy spans? @@ -299,3 +294,435 @@ pub(crate) fn increment_rollback_tick(mut rollback: ResMut) { *current_tick += 1; } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use bevy::prelude::*; + use tracing::{debug, info}; + + use crate::_reexport::*; + use crate::prelude::client::*; + use crate::prelude::*; + use crate::tests::protocol::*; + use crate::tests::stepper::{BevyStepper, Step}; + + fn increment_component( + mut commands: Commands, + mut query: Query<(Entity, &mut Component1), With>, + ) { + for (entity, mut component) in query.iter_mut() { + component.0 += 1.0; + if component.0 == 5.0 { + commands.entity(entity).remove::(); + } + } + } + + fn setup() -> BevyStepper { + let frame_duration = Duration::from_millis(10); + let tick_duration = Duration::from_millis(10); + let shared_config = SharedConfig { + enable_replication: false, + tick: TickConfig::new(tick_duration), + ..Default::default() + }; + let link_conditioner = LinkConditionerConfig { + incoming_latency: Duration::from_millis(40), + incoming_jitter: Duration::from_millis(5), + incoming_loss: 0.05, + }; + let sync_config = SyncConfig::default().speedup_factor(1.0); + let prediction_config = PredictionConfig::default().disable(false); + let interpolation_delay = Duration::from_millis(100); + let interpolation_config = InterpolationConfig::default() + .with_delay(InterpolationDelay::Delay(interpolation_delay)); + let mut stepper = BevyStepper::new( + shared_config, + sync_config, + prediction_config, + interpolation_config, + link_conditioner, + frame_duration, + ); + stepper.client_mut().set_synced(); + stepper.client_app.add_systems( + FixedUpdate, + increment_component.in_set(FixedUpdateSet::Main), + ); + stepper + } + + // Test that if a component gets removed from the predicted entity erroneously + // We are still able to rollback properly (the rollback adds the component to the predicted entity) + #[test] + fn test_removed_predicted_component_rollback() -> anyhow::Result<()> { + let mut stepper = setup(); + + // Create a confirmed entity + let confirmed = stepper + .client_app + .world + .spawn((Component1(0.0), ShouldBePredicted)) + .id(); + + // Tick once + stepper.frame_step(); + assert_eq!(stepper.client().tick(), Tick(1)); + let predicted = stepper + .client_app + .world + .get::(confirmed) + .unwrap() + .predicted + .unwrap(); + + // check that the predicted entity got spawned + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap() + .confirmed_entity, + confirmed + ); + + // check that the component history got created + let mut history = PredictionHistory::::default(); + // this is added during the first rollback call after we create the history + history + .buffer + .add_item(Tick(0), ComponentState::Updated(Component1(0.0))); + history + .buffer + .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + // check that the confirmed component got replicated + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap(), + &Component1(1.0) + ); + + // advance five more frames, so that the component gets removed on predicted + for i in 0..5 { + stepper.frame_step(); + } + assert_eq!(stepper.client().tick(), Tick(6)); + + // check that the component got removed on predicted + assert!(stepper + .client_app + .world + .get::(predicted) + .is_none()); + // check that the component history is still there and that the value of the component history is correct + let mut history = PredictionHistory::::default(); + for i in 0..5 { + history + .buffer + .add_item(Tick(i), ComponentState::Updated(Component1(i as f32))); + } + history.buffer.add_item(Tick(5), ComponentState::Removed); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + + // create a rollback situation + stepper.client_mut().set_synced(); + stepper + .client_mut() + .set_latest_received_server_tick(Tick(3)); + stepper + .client_app + .world + .get_mut::(confirmed) + .unwrap() + .0 = 1.0; + // update without incrementing time, because we want to force a rollback check + stepper.client_app.update(); + + // check that rollback happened + // predicted got the component re-added + stepper + .client_app + .world + .get_mut::(predicted) + .unwrap() + .0 = 4.0; + // check that the history is how we expect after rollback + let mut history = PredictionHistory::::default(); + for i in 3..7 { + history + .buffer + .add_item(Tick(i), ComponentState::Updated(Component1(i as f32 - 2.0))); + } + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history + ); + + Ok(()) + } + + // Test that if a component gets added to the predicted entity erroneously but didn't exist on the confirmed entity) + // We are still able to rollback properly (the rollback removes the component from the predicted entity) + #[test] + fn test_added_predicted_component_rollback() -> anyhow::Result<()> { + let mut stepper = setup(); + + // Create a confirmed entity + let confirmed = stepper.client_app.world.spawn(ShouldBePredicted).id(); + + // Tick once + stepper.frame_step(); + assert_eq!(stepper.client().tick(), Tick(1)); + let predicted = stepper + .client_app + .world + .get::(confirmed) + .unwrap() + .predicted + .unwrap(); + + // check that the predicted entity got spawned + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap() + .confirmed_entity, + confirmed + ); + + // add a new component to Predicted + stepper + .client_app + .world + .entity_mut(predicted) + .insert(Component1(1.0)); + + // create a rollback situation (confirmed doesn't have a component that predicted has) + stepper.client_mut().set_synced(); + stepper + .client_mut() + .set_latest_received_server_tick(Tick(1)); + // update without incrementing time, because we want to force a rollback check + stepper.client_app.update(); + + // check that rollback happened: the component got removed from predicted + assert!(stepper + .client_app + .world + .get::(predicted) + .is_none()); + + // check that history contains the removal + let mut history = PredictionHistory::::default(); + history.buffer.add_item(Tick(1), ComponentState::Removed); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + Ok(()) + } + + // Test that if a component gets removed from the confirmed entity + // We are still able to rollback properly (the rollback removes the component from the predicted entity) + #[test] + fn test_removed_confirmed_component_rollback() -> anyhow::Result<()> { + let mut stepper = setup(); + + // Create a confirmed entity + let confirmed = stepper + .client_app + .world + .spawn((Component1(0.0), ShouldBePredicted)) + .id(); + + // Tick once + stepper.frame_step(); + assert_eq!(stepper.client().tick(), Tick(1)); + let predicted = stepper + .client_app + .world + .get::(confirmed) + .unwrap() + .predicted + .unwrap(); + + // check that the predicted entity got spawned + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap() + .confirmed_entity, + confirmed + ); + + // check that the component history got created + let mut history = PredictionHistory::::default(); + history + .buffer + .add_item(Tick(0), ComponentState::Updated(Component1(0.0))); + history + .buffer + .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history, + ); + + // create a rollback situation by removing the component on confirmed + stepper.client_mut().set_synced(); + stepper + .client_mut() + .set_latest_received_server_tick(Tick(1)); + stepper + .client_app + .world + .entity_mut(confirmed) + .remove::(); + // update without incrementing time, because we want to force a rollback check + // (need duration_since_latest_received_server_tick = 0) + stepper.client_app.update(); + + // check that rollback happened + // predicted got the component removed + assert!(stepper + .client_app + .world + .get_mut::(predicted) + .is_none()); + + // check that the history is how we expect after rollback + let mut history = PredictionHistory::::default(); + history.buffer.add_item(Tick(1), ComponentState::Removed); + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history + ); + + Ok(()) + } + + // Test that if a component gets added to the confirmed entity (but didn't exist on the predicted entity) + // We are still able to rollback properly (the rollback adds the component to the predicted entity) + #[test] + fn test_added_confirmed_component_rollback() -> anyhow::Result<()> { + let mut stepper = setup(); + + // Create a confirmed entity + let confirmed = stepper.client_app.world.spawn(ShouldBePredicted).id(); + + // Tick once + stepper.frame_step(); + assert_eq!(stepper.client().tick(), Tick(1)); + let predicted = stepper + .client_app + .world + .get::(confirmed) + .unwrap() + .predicted + .unwrap(); + + // check that the predicted entity got spawned + assert_eq!( + stepper + .client_app + .world + .get::(predicted) + .unwrap() + .confirmed_entity, + confirmed + ); + + // check that the component history did not get created + assert!(stepper + .client_app + .world + .get::>(predicted) + .is_none()); + + // advance five more frames, so that the component gets removed on predicted + for i in 0..5 { + stepper.frame_step(); + } + assert_eq!(stepper.client().tick(), Tick(6)); + + // create a rollback situation by adding the component on confirmed + stepper.client_mut().set_synced(); + stepper + .client_mut() + .set_latest_received_server_tick(Tick(3)); + stepper + .client_app + .world + .entity_mut(confirmed) + .insert(Component1(1.0)); + // update without incrementing time, because we want to force a rollback check + stepper.client_app.update(); + + // check that rollback happened + // predicted got the component re-added + stepper + .client_app + .world + .get_mut::(predicted) + .unwrap() + .0 = 4.0; + // check that the history is how we expect after rollback + let mut history = PredictionHistory::::default(); + for i in 3..7 { + history + .buffer + .add_item(Tick(i), ComponentState::Updated(Component1(i as f32 - 2.0))); + } + assert_eq!( + stepper + .client_app + .world + .get::>(predicted) + .unwrap(), + &history + ); + + Ok(()) + } +} diff --git a/lightyear/src/client/resource.rs b/lightyear/src/client/resource.rs index 606b97fdc..d21d836bb 100644 --- a/lightyear/src/client/resource.rs +++ b/lightyear/src/client/resource.rs @@ -259,12 +259,9 @@ impl TickManaged for Client

{ } } -// TODO: make this only available for integration tests +// Access some internals for tests +#[cfg(test)] impl Client

{ - pub fn io(&self) -> &Io { - &self.io - } - pub fn set_latest_received_server_tick(&mut self, tick: Tick) { self.connection.sync_manager.latest_received_server_tick = tick; self.connection @@ -272,10 +269,8 @@ impl Client

{ .duration_since_latest_received_server_tick = Duration::default(); } - pub fn duration_since_latest_received_server_tick(&self) -> Duration { - self.connection - .sync_manager - .duration_since_latest_received_server_tick + pub fn connection(&self) -> &Connection

{ + &self.connection } pub fn set_synced(&mut self) { diff --git a/lightyear/src/connection/events.rs b/lightyear/src/connection/events.rs index 7ec1f3dbb..cf3a36521 100644 --- a/lightyear/src/connection/events.rs +++ b/lightyear/src/connection/events.rs @@ -410,7 +410,7 @@ impl IterComponentInsertEvent

for ConnectionEvents

{ #[cfg(test)] mod tests { - use crate::protocol::tests::{ + use crate::tests::protocol::{ Channel1, Channel2, Message1, Message2, MyMessageProtocol, MyProtocol, }; diff --git a/lightyear/src/connection/mod.rs b/lightyear/src/connection/mod.rs index 31473c5f3..ed8bd0dfa 100644 --- a/lightyear/src/connection/mod.rs +++ b/lightyear/src/connection/mod.rs @@ -29,11 +29,6 @@ use crate::shared::tick_manager::TickManager; use crate::shared::time_manager::TimeManager; use crate::utils::named::Named; -// NOTE: we cannot have a message manager exclusively for messages, and a message manager for replication -// because prior to calling message_manager.recv() we don't know if the packet is a message or a replication event -// Also it would be inefficient because we would send separate packets for messages or replications, even though -// we can put them in the same packet - /// Wrapper to send/receive messages via channels to a remote address pub struct Connection { pub ping_manager: PingManager, @@ -95,6 +90,7 @@ impl Connection

{ } pub fn buffer_despawn_entity(&mut self, entity: Entity, channel: ChannelKind) -> Result<()> { + // TODO: check with replication manager if we should send the despawn message let message = ProtocolMessage::Replication(ReplicationMessage::DespawnEntity(entity)); self.message_manager.buffer_send(message, channel) } @@ -118,6 +114,7 @@ impl Connection

{ component: P::ComponentKinds, channel: ChannelKind, ) -> Result<()> { + // TODO: check with replication manager if we should send the component remove // TODO: maybe don't send the component remove if the entity is despawning? let message = ProtocolMessage::Replication(ReplicationMessage::RemoveComponent(entity, component)); diff --git a/lightyear/src/lib.rs b/lightyear/src/lib.rs index a8840d103..91ec86fb4 100644 --- a/lightyear/src/lib.rs +++ b/lightyear/src/lib.rs @@ -15,6 +15,9 @@ You can find more information in the [book](https://cbournhonesque.github.io/lig pub mod _reexport { pub use enum_delegate; pub use enum_dispatch::enum_dispatch; + pub use lightyear_macros::{ + component_protocol_internal, message_protocol_internal, ChannelInternal, MessageInternal, + }; pub use paste::paste; pub use crate::channel::builder::TickBufferChannel; @@ -119,6 +122,9 @@ pub mod server; pub mod shared; +#[cfg(test)] +pub(crate) mod tests; + /// Provides an abstraction over an unreliable transport pub mod transport; diff --git a/lightyear/src/protocol/mod.rs b/lightyear/src/protocol/mod.rs index b4848aecc..cb800f27c 100644 --- a/lightyear/src/protocol/mod.rs +++ b/lightyear/src/protocol/mod.rs @@ -239,68 +239,68 @@ pub trait EventContext: Send + Sync + 'static {} impl EventContext for T {} -#[cfg(test)] -pub mod tests { - use bevy::prelude::Component; - use serde::Deserialize; - - use lightyear_macros::{ - component_protocol_internal, message_protocol_internal, ChannelInternal, MessageInternal, - }; - - use crate::prelude::{ChannelDirection, ChannelMode, ReliableSettings}; - - use super::*; - - // Messages - #[derive(MessageInternal, Serialize, Deserialize, Debug, PartialEq, Clone)] - pub struct Message1(pub String); - - #[derive(MessageInternal, Serialize, Deserialize, Debug, PartialEq, Clone)] - pub struct Message2(pub u32); - - // #[derive(Debug, PartialEq)] - #[message_protocol_internal(protocol = "MyProtocol")] - pub enum MyMessageProtocol { - Message1(Message1), - Message2(Message2), - } - - #[derive(Component, Serialize, Deserialize, Clone, Debug, PartialEq)] - pub struct Component1; - - // TODO: because we add ShouldBePredicted to the enum, we cannot derive stuff for the enum anymore! - // is it a problem? we could pass the derives through an attribute macro ... - // #[derive(Debug, PartialEq)] - #[component_protocol_internal(protocol = "MyProtocol")] - pub enum MyComponentsProtocol { - Component1(Component1), - } - - protocolize! { - Self = MyProtocol, - Message = MyMessageProtocol, - Component = MyComponentsProtocol, - Crate = crate, - } - - // Channels - #[derive(ChannelInternal)] - pub struct Channel1; - - #[derive(ChannelInternal)] - pub struct Channel2; - - pub fn test_protocol() -> MyProtocol { - let mut p = MyProtocol::default(); - p.add_channel::(ChannelSettings { - mode: ChannelMode::OrderedReliable(ReliableSettings::default()), - direction: ChannelDirection::Bidirectional, - }); - p.add_channel::(ChannelSettings { - mode: ChannelMode::UnorderedUnreliable, - direction: ChannelDirection::Bidirectional, - }); - p - } -} +// #[cfg(test)] +// pub mod tests { +// use bevy::prelude::Component; +// use serde::Deserialize; +// +// use lightyear_macros::{ +// component_protocol_internal, message_protocol_internal, ChannelInternal, MessageInternal, +// }; +// +// use crate::prelude::{ChannelDirection, ChannelMode, ReliableSettings}; +// +// use super::*; +// +// // Messages +// #[derive(MessageInternal, Serialize, Deserialize, Debug, PartialEq, Clone)] +// pub struct Message1(pub String); +// +// #[derive(MessageInternal, Serialize, Deserialize, Debug, PartialEq, Clone)] +// pub struct Message2(pub u32); +// +// // #[derive(Debug, PartialEq)] +// #[message_protocol_internal(protocol = "MyProtocol")] +// pub enum MyMessageProtocol { +// Message1(Message1), +// Message2(Message2), +// } +// +// #[derive(Component, Serialize, Deserialize, Clone, Debug, PartialEq)] +// pub struct Component1; +// +// // TODO: because we add ShouldBePredicted to the enum, we cannot derive stuff for the enum anymore! +// // is it a problem? we could pass the derives through an attribute macro ... +// // #[derive(Debug, PartialEq)] +// #[component_protocol_internal(protocol = "MyProtocol")] +// pub enum MyComponentsProtocol { +// Component1(Component1), +// } +// +// protocolize! { +// Self = MyProtocol, +// Message = MyMessageProtocol, +// Component = MyComponentsProtocol, +// Crate = crate, +// } +// +// // Channels +// #[derive(ChannelInternal)] +// pub struct Channel1; +// +// #[derive(ChannelInternal)] +// pub struct Channel2; +// +// pub fn test_protocol() -> MyProtocol { +// let mut p = MyProtocol::default(); +// p.add_channel::(ChannelSettings { +// mode: ChannelMode::OrderedReliable(ReliableSettings::default()), +// direction: ChannelDirection::Bidirectional, +// }); +// p.add_channel::(ChannelSettings { +// mode: ChannelMode::UnorderedUnreliable, +// direction: ChannelDirection::Bidirectional, +// }); +// p +// } +// } diff --git a/lightyear/src/server/events.rs b/lightyear/src/server/events.rs index 9ca17a41d..c46a1b933 100644 --- a/lightyear/src/server/events.rs +++ b/lightyear/src/server/events.rs @@ -182,7 +182,7 @@ pub type MessageEvent = crate::shared::events::MessageEvent; #[cfg(test)] mod tests { use crate::protocol::channel::ChannelKind; - use crate::protocol::tests::{ + use crate::tests::protocol::{ Channel1, Channel2, Message1, Message2, MyMessageProtocol, MyProtocol, }; diff --git a/lightyear/src/server/plugin.rs b/lightyear/src/server/plugin.rs index c3900250c..6eeb600e6 100644 --- a/lightyear/src/server/plugin.rs +++ b/lightyear/src/server/plugin.rs @@ -79,7 +79,6 @@ impl PluginType for ServerPlugin

{ .add_plugins(InputPlugin::

::default()) // RESOURCES // .insert_resource(server) - .init_resource::() // SYSTEM SETS // .configure_sets(PreUpdate, MainSet::Receive) .configure_sets( diff --git a/lightyear/src/shared/replication/entity_map.rs b/lightyear/src/shared/replication/entity_map.rs index a0a44c634..f896bff5f 100644 --- a/lightyear/src/shared/replication/entity_map.rs +++ b/lightyear/src/shared/replication/entity_map.rs @@ -18,11 +18,11 @@ impl EntityMap { self.local_to_remote.insert(local_entity, remote_entity); } - pub fn get_local(&mut self, remote_entity: Entity) -> Option<&Entity> { + pub fn get_local(&self, remote_entity: Entity) -> Option<&Entity> { self.remote_to_local.get(&remote_entity) } - pub fn get_remote(&mut self, local_entity: Entity) -> Option<&Entity> { + pub fn get_remote(&self, local_entity: Entity) -> Option<&Entity> { self.local_to_remote.get(&local_entity) } diff --git a/lightyear/src/shared/replication/manager.rs b/lightyear/src/shared/replication/manager.rs index e8ac44ca4..0425800d3 100644 --- a/lightyear/src/shared/replication/manager.rs +++ b/lightyear/src/shared/replication/manager.rs @@ -154,7 +154,6 @@ impl ReplicationManager

{ .map(|c| c.into()) .collect::>(); debug!(?entity, ?component_kinds, "Received spawn entity"); - // let local_entity = world.spawn(components.into()).id(); // TODO: we only run spawn_entity if we don't already have an entity in the process of being spawned // so we need a data-structure to keep track of entities that are being spawned @@ -167,7 +166,7 @@ impl ReplicationManager

{ } let mut local_entity_mut = world.spawn_empty(); - // TODO: optimize by using batch functions + // TODO: optimize by using batch functions? for component in components { component.insert(&mut local_entity_mut); } @@ -187,6 +186,7 @@ impl ReplicationManager

{ debug!(?entity, ?kind, "Received InsertComponent"); // it's possible that we received InsertComponent before the entity actually exists. // In that case, we need to spawn the entity first. + // TODO: this might not be what we want? imagine we receive a DespawnEntity or RemoveComponent right before that? let mut local_entity_mut = self.entity_map.get_by_remote_or_spawn(world, entity); // TODO: maybe check if the component already exists? component.insert(&mut local_entity_mut); diff --git a/lightyear/src/shared/replication/mod.rs b/lightyear/src/shared/replication/mod.rs index 4049dfd26..6d3d9f1fe 100644 --- a/lightyear/src/shared/replication/mod.rs +++ b/lightyear/src/shared/replication/mod.rs @@ -75,3 +75,97 @@ pub trait ReplicationSend: Resource { /// (for example collecting the individual single component updates into a single message) fn prepare_replicate_send(&mut self) -> Result<()>; } + +#[cfg(test)] +mod tests { + use crate::prelude::client::*; + use crate::prelude::*; + use crate::tests::protocol::*; + use crate::tests::stepper::{BevyStepper, Step}; + use std::time::Duration; + + // An entity gets replicated from server to client, + // then a component gets removed from that entity on server, + // that component should also removed on client as well. + #[test] + fn test_simple_component_remove() -> anyhow::Result<()> { + let frame_duration = Duration::from_millis(10); + let tick_duration = Duration::from_millis(10); + let shared_config = SharedConfig { + enable_replication: true, + tick: TickConfig::new(tick_duration), + ..Default::default() + }; + let link_conditioner = LinkConditionerConfig { + incoming_latency: Duration::from_millis(0), + incoming_jitter: Duration::from_millis(0), + incoming_loss: 0.0, + }; + let sync_config = SyncConfig::default().speedup_factor(1.0); + let prediction_config = PredictionConfig::default().disable(false); + let interpolation_config = InterpolationConfig::default(); + let mut stepper = BevyStepper::new( + shared_config, + sync_config, + prediction_config, + interpolation_config, + link_conditioner, + frame_duration, + ); + stepper.client_mut().connect(); + stepper.client_mut().set_synced(); + + // Advance the world to let the connection process complete + for _ in 0..20 { + stepper.frame_step(); + } + + // Create an entity on server + let server_entity = stepper + .server_app + .world + .spawn((Component1(0.0), Replicate::default())) + .id(); + // we need to step twice because we run client before server + stepper.frame_step(); + stepper.frame_step(); + + // Check that the entity is replicated to client + let client_entity = stepper + .client() + .connection() + .base() + .replication_manager + .entity_map + .get_local(server_entity) + .unwrap() + .clone(); + assert_eq!( + stepper + .client_app + .world + .entity(client_entity) + .get::() + .unwrap(), + &Component1(0.0) + ); + + // Remove the component on the server + stepper + .server_app + .world + .entity_mut(server_entity) + .remove::(); + stepper.frame_step(); + stepper.frame_step(); + + // Check that this removal was replicated + assert!(stepper + .client_app + .world + .entity(client_entity) + .get::() + .is_none()); + Ok(()) + } +} diff --git a/tests/src/client.rs b/lightyear/src/tests/client.rs similarity index 94% rename from tests/src/client.rs rename to lightyear/src/tests/client.rs index 661dadb23..f4d34fed7 100644 --- a/tests/src/client.rs +++ b/lightyear/src/tests/client.rs @@ -4,10 +4,9 @@ use std::time::Duration; use bevy::app::App; -use lightyear::prelude::client::*; -use lightyear::prelude::*; - -use crate::protocol::{protocol, MyProtocol}; +use crate::prelude::client::*; +use crate::prelude::*; +use crate::tests::protocol::{protocol, MyProtocol}; pub fn setup(auth: Authentication) -> anyhow::Result> { let addr = SocketAddr::from_str("127.0.0.1:0")?; diff --git a/tests/src/lib.rs b/lightyear/src/tests/mod.rs similarity index 100% rename from tests/src/lib.rs rename to lightyear/src/tests/mod.rs diff --git a/tests/src/protocol.rs b/lightyear/src/tests/protocol.rs similarity index 80% rename from tests/src/protocol.rs rename to lightyear/src/tests/protocol.rs index 24b6d0afd..fe4fc029e 100644 --- a/tests/src/protocol.rs +++ b/lightyear/src/tests/protocol.rs @@ -2,16 +2,17 @@ use bevy::prelude::Component; use derive_more::{Add, Mul}; use serde::{Deserialize, Serialize}; -use lightyear::prelude::*; +use crate::_reexport::*; +use crate::prelude::*; // Messages -#[derive(Message, Serialize, Deserialize, Debug, PartialEq, Clone)] +#[derive(MessageInternal, Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct Message1(pub String); -#[derive(Message, Serialize, Deserialize, Debug, PartialEq, Clone)] +#[derive(MessageInternal, Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct Message2(pub u32); -#[message_protocol(protocol = "MyProtocol")] +#[message_protocol_internal(protocol = "MyProtocol")] pub enum MyMessageProtocol { Message1(Message1), Message2(Message2), @@ -27,7 +28,7 @@ pub struct Component2(pub f32); #[derive(Component, Serialize, Deserialize, Clone, Debug, PartialEq, Add, Mul)] pub struct Component3(pub f32); -#[component_protocol(protocol = "MyProtocol")] +#[component_protocol_internal(protocol = "MyProtocol")] pub enum MyComponentsProtocol { #[sync(full)] Component1(Component1), @@ -41,6 +42,7 @@ pub enum MyComponentsProtocol { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct MyInput(pub i16); + impl UserInput for MyInput {} // Protocol @@ -50,13 +52,14 @@ protocolize! { Message = MyMessageProtocol, Component = MyComponentsProtocol, Input = MyInput, + Crate = crate, } // Channels -#[derive(Channel)] +#[derive(ChannelInternal)] pub struct Channel1; -#[derive(Channel)] +#[derive(ChannelInternal)] pub struct Channel2; pub fn protocol() -> MyProtocol { diff --git a/tests/src/server.rs b/lightyear/src/tests/server.rs similarity index 93% rename from tests/src/server.rs rename to lightyear/src/tests/server.rs index 272c308ea..4adcf7346 100644 --- a/tests/src/server.rs +++ b/lightyear/src/tests/server.rs @@ -5,9 +5,9 @@ use std::time::Duration; use bevy::app::App; -use crate::protocol::{protocol, MyProtocol}; -use lightyear::prelude::server::*; -use lightyear::prelude::*; +use crate::prelude::server::*; +use crate::prelude::*; +use crate::tests::protocol::{protocol, MyProtocol}; pub fn setup(protocol_id: u64, private_key: Key) -> anyhow::Result> { // create udp-socket based io diff --git a/tests/src/stepper.rs b/lightyear/src/tests/stepper.rs similarity index 82% rename from tests/src/stepper.rs rename to lightyear/src/tests/stepper.rs index 4c31efc86..d96a4bfda 100644 --- a/tests/src/stepper.rs +++ b/lightyear/src/tests/stepper.rs @@ -7,17 +7,15 @@ use bevy::time::TimeUpdateStrategy; use bevy::MinimalPlugins; use tracing_subscriber::fmt::format::FmtSpan; -use lightyear::client as lightyear_client; -use lightyear::netcode::generate_key; -use lightyear::prelude::client::{ +use crate::netcode::generate_key; +use crate::prelude::client::{ Authentication, Client, ClientConfig, InputConfig, InterpolationConfig, PredictionConfig, SyncConfig, }; -use lightyear::prelude::server::{NetcodeConfig, Server, ServerConfig}; -use lightyear::prelude::*; -use lightyear::server as lightyear_server; +use crate::prelude::server::{NetcodeConfig, Server, ServerConfig}; +use crate::prelude::*; -use crate::protocol::{protocol, MyProtocol}; +use crate::tests::protocol::{protocol, MyProtocol}; /// Helpers to setup a bevy app where I can just step the world easily @@ -54,20 +52,19 @@ impl BevyStepper { // .init(); // Use local channels instead of UDP for testing - let transport_1 = IoConfig::from_transport(TransportConfig::LocalChannel) - .with_conditioner(conditioner.clone()); - let addr_1 = transport_1.get_local_addr(); - let receiver_1 = transport_1.get_receiver(); - let sender_1 = transport_1.get_sender(); + let addr = SocketAddr::from_str("127.0.0.1:0").unwrap(); + let io_1 = IoConfig::from_transport(TransportConfig::LocalChannel) + .with_conditioner(conditioner.clone()) + .get_io(); + let (receiver_1, sender_1) = io_1.to_parts(); - let transport_2 = IoConfig::from_transport(TransportConfig::LocalChannel) - .with_conditioner(conditioner.clone()); - let addr_2 = transport_2.get_local_addr(); - let receiver_2 = transport_2.get_receiver(); - let sender_2 = transport_2.get_sender(); + let io_2 = IoConfig::from_transport(TransportConfig::LocalChannel) + .with_conditioner(conditioner.clone()) + .get_io(); + let (receiver_2, sender_2) = io_2.to_parts(); - let io_1 = Io::new(addr_1.clone(), sender_2, receiver_1); - let io_2 = Io::new(addr_2.clone(), sender_1, receiver_2); + let io_1 = Io::new(addr, sender_2, receiver_1); + let io_2 = Io::new(addr, sender_1, receiver_2); // Shared config let protocol_id = 0; @@ -93,7 +90,7 @@ impl BevyStepper { let mut client_app = App::new(); client_app.add_plugins(MinimalPlugins.build()); let auth = Authentication::Manual { - server_addr: addr_1, + server_addr: addr, protocol_id, private_key, client_id, @@ -156,7 +153,6 @@ impl Step for BevyStepper { .insert_resource(TimeUpdateStrategy::ManualInstant(self.current_time)); mock_instant::MockClock::advance(self.frame_duration); self.client_app.update(); - // TODO: maybe for testing use a local io via channels? // sleep a bit to make sure that local io receives the packets // std::thread::sleep(Duration::from_millis(1)); self.server_app.update(); diff --git a/lightyear/src/transport/io.rs b/lightyear/src/transport/io.rs index 5f2dfad27..87907a2dd 100644 --- a/lightyear/src/transport/io.rs +++ b/lightyear/src/transport/io.rs @@ -19,25 +19,37 @@ pub enum TransportConfig { } impl TransportConfig { - pub fn get_local_addr(&self) -> SocketAddr { + pub fn get_io(&self) -> Io { match self { - TransportConfig::UdpSocket(addr) => *addr, - TransportConfig::LocalChannel => LOCAL_SOCKET, - } - } - pub fn get_sender(&self) -> Box { - match self { - TransportConfig::UdpSocket(addr) => Box::new(UdpSocket::new(addr).unwrap()), - TransportConfig::LocalChannel => Box::new(LocalChannel::new()), - } - } - - pub fn get_receiver(&self) -> Box { - match self { - TransportConfig::UdpSocket(addr) => Box::new(UdpSocket::new(addr).unwrap()), - TransportConfig::LocalChannel => Box::new(LocalChannel::new()), + TransportConfig::UdpSocket(addr) => { + let socket = UdpSocket::new(addr).unwrap(); + Io::new(*addr, Box::new(socket.clone()), Box::new(socket)) + } + TransportConfig::LocalChannel => { + let channel = LocalChannel::new(); + Io::new(LOCAL_SOCKET, Box::new(channel.clone()), Box::new(channel)) + } } } + // pub fn get_local_addr(&self) -> SocketAddr { + // match self { + // TransportConfig::UdpSocket(addr) => *addr, + // TransportConfig::LocalChannel => LOCAL_SOCKET, + // } + // } + // pub fn get_sender(&self) -> Box { + // match self { + // TransportConfig::UdpSocket(addr) => Box::new(UdpSocket::new(addr).unwrap()), + // TransportConfig::LocalChannel => Box::new(LocalChannel::new()), + // } + // } + // + // pub fn get_receiver(&self) -> Box { + // match self { + // TransportConfig::UdpSocket(addr) => Box::new(UdpSocket::new(addr).unwrap()), + // TransportConfig::LocalChannel => Box::new(LocalChannel::new()), + // } + // } } #[derive(Clone)] @@ -67,20 +79,32 @@ impl IoConfig { self } - pub fn get_local_addr(&self) -> SocketAddr { - self.transport.get_local_addr() - } - pub fn get_sender(&self) -> Box { - self.transport.get_sender() - } - - pub fn get_receiver(&self) -> Box { - let mut receiver = self.transport.get_receiver(); + pub fn get_io(&self) -> Io { + let mut io = self.transport.get_io(); if let Some(conditioner) = &self.conditioner { - receiver = Box::new(ConditionedPacketReceiver::new(receiver, conditioner)); + io = Io::new( + io.local_addr, + io.sender, + Box::new(ConditionedPacketReceiver::new(io.receiver, conditioner)), + ); } - receiver - } + io + } + + // pub fn get_local_addr(&self) -> SocketAddr { + // self.transport.get_local_addr() + // } + // pub fn get_sender(&self) -> Box { + // self.transport.get_sender() + // } + // + // pub fn get_receiver(&self) -> Box { + // let mut receiver = self.transport.get_receiver(); + // if let Some(conditioner) = &self.conditioner { + // receiver = Box::new(ConditionedPacketReceiver::new(receiver, conditioner)); + // } + // receiver + // } } pub struct Io { @@ -91,10 +115,11 @@ pub struct Io { impl Io { pub fn from_config(config: &IoConfig) -> Self { - let local_addr = config.transport.get_local_addr(); - let sender = config.transport.get_sender(); - let receiver = config.transport.get_receiver(); - Self::new(local_addr, sender, receiver) + config.get_io() + // let local_addr = config.transport.get_local_addr(); + // let sender = config.transport.get_sender(); + // let receiver = config.transport.get_receiver(); + // Self::new(local_addr, sender, receiver) } pub fn new( @@ -109,6 +134,15 @@ impl Io { } } + pub fn to_parts( + self, + ) -> ( + Box, + Box, + ) { + (self.receiver, self.sender) + } + pub fn split( &mut self, ) -> ( diff --git a/tests/Cargo.toml b/tests/Cargo.toml deleted file mode 100644 index 3142ff58d..000000000 --- a/tests/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "lightyear_tests" -version = "0.1.0" -edition = "2021" -publish = false - -[features] -metrics = ["lightyear/metrics", "dep:metrics-exporter-prometheus"] -mock_time = ["lightyear/mock_time"] - -[dependencies] -lightyear = { path = "../lightyear" } -serde = { version = "1.0.188", features = ["derive"] } -anyhow = { version = "1.0.75", features = [] } -tracing = "0.1" -tracing-subscriber = "0.3.17" -bevy = { version = "0.12", features = ["bevy_core_pipeline"] } -rand = "0.8.5" -derive_more = { version = "0.99", features = ["add", "mul"] } - -mock_instant = "0.3" - -metrics-exporter-prometheus = { version = "0.12.1", optional = true } -bevy-inspector-egui = "0.21.0" diff --git a/tests/tests/interpolation_simple.rs b/tests/tests/interpolation_simple.rs deleted file mode 100644 index 4b604d02c..000000000 --- a/tests/tests/interpolation_simple.rs +++ /dev/null @@ -1,251 +0,0 @@ -// #![allow(unused_imports)] -// #![allow(unused_variables)] -// #![allow(dead_code)] -// -// use std::net::SocketAddr; -// use std::str::FromStr; -// use std::time::{Duration, Instant}; -// -// use bevy::log::LogPlugin; -// use bevy::prelude::{ -// App, Commands, Entity, EventReader, FixedUpdate, IntoSystemConfigs, PluginGroup, Query, Real, -// Res, ResMut, Startup, Time, With, -// }; -// use bevy::time::TimeUpdateStrategy; -// use bevy::winit::WinitPlugin; -// use bevy::{DefaultPlugins, MinimalPlugins}; -// use lightyear::client::components::Confirmed; -// use tracing::{debug, info}; -// use tracing_subscriber::fmt::format::FmtSpan; -// -// use lightyear::_reexport::*; -// use lightyear::prelude::client::*; -// use lightyear::prelude::*; -// use lightyear_tests::protocol::{protocol, Channel2, Component1, Component2, MyInput, MyProtocol}; -// use lightyear_tests::stepper::{BevyStepper, Step}; -// -// fn setup() -> (BevyStepper, Entity, Entity, u16) { -// let frame_duration = Duration::from_millis(10); -// let tick_duration = Duration::from_millis(10); -// let shared_config = SharedConfig { -// enable_replication: false, -// tick: TickConfig::new(tick_duration), -// ..Default::default() -// }; -// let link_conditioner = LinkConditionerConfig { -// incoming_latency: Duration::from_millis(40), -// incoming_jitter: Duration::from_millis(5), -// incoming_loss: 0.05, -// }; -// let sync_config = SyncConfig::default().speedup_factor(1.0); -// let prediction_config = PredictionConfig::default().disable(true); -// let interpolation_delay = Duration::from_millis(100); -// let interpolation_config = -// InterpolationConfig::default().with_delay(InterpolationDelay::Delay(interpolation_delay)); -// let mut stepper = BevyStepper::new( -// shared_config, -// sync_config, -// prediction_config, -// interpolation_config, -// link_conditioner, -// frame_duration, -// ); -// stepper.client_mut().set_synced(); -// -// // Create a confirmed entity -// let confirmed = stepper -// .client_app -// .world -// .spawn((Component1(0.0), ShouldBeInterpolated)) -// .id(); -// -// // Tick once -// stepper.frame_step(); -// assert_eq!(stepper.client().tick(), Tick(1)); -// let interpolated = stepper -// .client_app -// .world -// .get::(confirmed) -// .unwrap() -// .interpolated -// .unwrap(); -// -// assert_eq!( -// stepper -// .client_app -// .world -// .get::(confirmed) -// .unwrap(), -// &Component1(0.0) -// ); -// -// // check that the interpolated entity got spawned -// assert_eq!( -// stepper -// .client_app -// .world -// .get::(interpolated) -// .unwrap() -// .confirmed_entity, -// confirmed -// ); -// -// // check that the component history got created and is empty -// let history = ConfirmedHistory::::new(); -// assert_eq!( -// stepper -// .client_app -// .world -// .get::>(interpolated) -// .unwrap(), -// &history, -// ); -// // check that the confirmed component got replicated -// assert_eq!( -// stepper -// .client_app -// .world -// .get::(interpolated) -// .unwrap(), -// &Component1(0.0) -// ); -// // check that the interpolate status got updated -// assert_eq!( -// stepper -// .client_app -// .world -// .get::>(interpolated) -// .unwrap(), -// &InterpolateStatus:: { -// start: None, -// end: (Tick(0), Component1(0.0)).into(), -// current: Tick(1) - interpolation_tick_delay, -// } -// ); -// (stepper, confirmed, interpolated, interpolation_tick_delay) -// } -// -// // Test interpolation -// #[test] -// fn test_interpolation() -> anyhow::Result<()> { -// let (mut stepper, confirmed, interpolated, interpolation_tick_delay) = setup(); -// // reach interpolation start tick -// stepper.frame_step(); -// stepper.frame_step(); -// // check that the interpolate status got updated (end becomes start) -// assert_eq!( -// stepper -// .client_app -// .world -// .get::>(interpolated) -// .unwrap(), -// &InterpolateStatus:: { -// start: (Tick(0), Component1(0.0)).into(), -// end: None, -// current: Tick(3) - interpolation_tick_delay, -// } -// ); -// -// // receive server update -// stepper -// .client_mut() -// .set_latest_received_server_tick(Tick(2)); -// stepper -// .client_app -// .world -// .get_entity_mut(confirmed) -// .unwrap() -// .get_mut::() -// .unwrap() -// .0 = 2.0; -// -// stepper.frame_step(); -// // check that interpolation is working correctly -// assert_eq!( -// stepper -// .client_app -// .world -// .get::>(interpolated) -// .unwrap(), -// &InterpolateStatus:: { -// start: (Tick(0), Component1(0.0)).into(), -// end: (Tick(2), Component1(2.0)).into(), -// current: Tick(4) - interpolation_tick_delay, -// } -// ); -// assert_eq!( -// stepper -// .client_app -// .world -// .get::(interpolated) -// .unwrap(), -// &Component1(1.0) -// ); -// stepper.frame_step(); -// assert_eq!( -// stepper -// .client_app -// .world -// .get::>(interpolated) -// .unwrap(), -// &InterpolateStatus:: { -// start: (Tick(2), Component1(2.0)).into(), -// end: None, -// current: Tick(5) - interpolation_tick_delay, -// } -// ); -// assert_eq!( -// stepper -// .client_app -// .world -// .get::(interpolated) -// .unwrap(), -// &Component1(2.0) -// ); -// Ok(()) -// } -// -// // We are in the situation: S1 < I -// // where S1 is a confirmed ticks, and I is the interpolated tick -// // and we receive S1 < S2 < I -// // Then we should now start interpolating from S2 -// #[test] -// fn test_received_more_recent_start() -> anyhow::Result<()> { -// let (mut stepper, confirmed, interpolated, interpolation_tick_delay) = setup(); -// -// // reach interpolation start tick -// stepper.frame_step(); -// stepper.frame_step(); -// stepper.frame_step(); -// stepper.frame_step(); -// assert_eq!(stepper.client().tick(), Tick(5)); -// -// // receive server update -// stepper -// .client_mut() -// .set_latest_received_server_tick(Tick(1)); -// stepper -// .client_app -// .world -// .get_entity_mut(confirmed) -// .unwrap() -// .get_mut::() -// .unwrap() -// .0 = 1.0; -// -// stepper.frame_step(); -// // check the status uses the more recent server update -// assert_eq!( -// stepper -// .client_app -// .world -// .get::>(interpolated) -// .unwrap(), -// &InterpolateStatus:: { -// start: (Tick(1), Component1(1.0)).into(), -// end: None, -// current: Tick(6) - interpolation_tick_delay, -// } -// ); -// Ok(()) -// } diff --git a/tests/tests/prediction_despawn.rs b/tests/tests/prediction_despawn.rs deleted file mode 100644 index db7379079..000000000 --- a/tests/tests/prediction_despawn.rs +++ /dev/null @@ -1,334 +0,0 @@ -#![allow(unused_imports)] -#![allow(unused_variables)] -#![allow(dead_code)] - -use std::net::SocketAddr; -use std::str::FromStr; -use std::time::{Duration, Instant}; - -use bevy::log::LogPlugin; -use bevy::prelude::{ - App, Commands, Entity, EventReader, EventWriter, FixedUpdate, IntoSystemConfigs, PluginGroup, - Query, Real, Res, ResMut, Startup, Time, With, -}; -use bevy::time::TimeUpdateStrategy; -use bevy::winit::WinitPlugin; -use bevy::{DefaultPlugins, MinimalPlugins}; -use lightyear::client::prediction::PredictionCommandsExt; -use tracing::{debug, info}; -use tracing_subscriber::fmt::format::FmtSpan; - -use lightyear::_reexport::*; -use lightyear::prelude::client::*; -use lightyear::prelude::*; -use lightyear_tests::protocol::{protocol, Channel2, Component1, MyInput, MyProtocol}; -use lightyear_tests::stepper::{BevyStepper, Step}; - -fn increment_component_and_despawn( - mut commands: Commands, - mut query: Query<(Entity, &mut Component1), With>, -) { - for (entity, mut component) in query.iter_mut() { - component.0 += 1.0; - if component.0 == 5.0 { - commands.entity(entity).prediction_despawn::(); - } - } -} - -// Test that if a predicted entity gets despawned erroneously -// We are still able to rollback properly (the rollback re-adds the predicted entity, or prevents it from despawning) -#[test] -fn test_despawned_predicted_rollback() -> anyhow::Result<()> { - let frame_duration = Duration::from_millis(10); - let tick_duration = Duration::from_millis(10); - let shared_config = SharedConfig { - enable_replication: false, - tick: TickConfig::new(tick_duration), - ..Default::default() - }; - let link_conditioner = LinkConditionerConfig { - incoming_latency: Duration::from_millis(40), - incoming_jitter: Duration::from_millis(5), - incoming_loss: 0.05, - }; - let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); - let interpolation_delay = Duration::from_millis(100); - let interpolation_config = - InterpolationConfig::default().with_delay(InterpolationDelay::Delay(interpolation_delay)); - let mut stepper = BevyStepper::new( - shared_config, - sync_config, - prediction_config, - interpolation_config, - link_conditioner, - frame_duration, - ); - stepper.client_mut().set_synced(); - stepper.client_app.add_systems( - FixedUpdate, - increment_component_and_despawn.in_set(FixedUpdateSet::Main), - ); - - // Create a confirmed entity - let confirmed = stepper - .client_app - .world - .spawn((Component1(0.0), ShouldBePredicted)) - .id(); - - // Tick once - stepper.frame_step(); - assert_eq!(stepper.client().tick(), Tick(1)); - let predicted = stepper - .client_app - .world - .get::(confirmed) - .unwrap() - .predicted - .unwrap(); - - // check that the predicted entity got spawned - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap() - .confirmed_entity, - confirmed - ); - - // check that the component history got created - let mut history = PredictionHistory::::default(); - history - .buffer - .add_item(Tick(0), ComponentState::Updated(Component1(0.0))); - history - .buffer - .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - // check that the confirmed component got replicated - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap(), - &Component1(1.0) - ); - - // advance five more frames, so that the component gets removed on predicted - for i in 0..5 { - stepper.frame_step(); - } - assert_eq!(stepper.client().tick(), Tick(6)); - - // check that the component got removed on predicted - assert!(stepper - .client_app - .world - .get::(predicted) - .is_none()); - // // check that predicted has the despawn marker - // assert_eq!( - // stepper - // .client_app - // .world - // .get::(predicted) - // .unwrap(), - // &PredictionDespawnMarker { - // death_tick: Tick(5) - // } - // ); - // check that the component history is still there and that the value of the component history is correct - let mut history = PredictionHistory::::default(); - for i in 0..5 { - history - .buffer - .add_item(Tick(i), ComponentState::Updated(Component1(i as f32))); - } - history.buffer.add_item(Tick(5), ComponentState::Removed); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - - // create a rollback situation - stepper.client_mut().set_synced(); - stepper - .client_mut() - .set_latest_received_server_tick(Tick(3)); - stepper - .client_app - .world - .get_mut::(confirmed) - .unwrap() - .0 = 1.0; - // update without incrementing time, because we want to force a rollback check - stepper.client_app.update(); - - // check that rollback happened - // predicted exists, and got the component re-added - stepper - .client_app - .world - .get_mut::(predicted) - .unwrap() - .0 = 4.0; - // check that the history is how we expect after rollback - let mut history = PredictionHistory::::default(); - for i in 3..7 { - history - .buffer - .add_item(Tick(i), ComponentState::Updated(Component1(i as f32 - 2.0))); - } - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history - ); - Ok(()) -} - -// Test that if another entity gets added during prediction, -// - either it should get despawned if there is a rollback that doesn't add it anymore -// - or we should just let it live? (imagine it's audio, etc.) - -fn increment_component_and_despawn_both( - mut commands: Commands, - mut query: Query<(Entity, &mut Component1)>, -) { - for (entity, mut component) in query.iter_mut() { - component.0 += 1.0; - if component.0 == 5.0 { - commands.entity(entity).prediction_despawn::(); - } - } -} - -// Test that if a confirmed entity gets despawned, -// the corresponding predicted entity gets despawned as well -// Test that if a predicted entity gets despawned erroneously -// We are still able to rollback properly (the rollback re-adds the predicted entity, or prevents it from despawning) -#[test] -fn test_despawned_confirmed_rollback() -> anyhow::Result<()> { - let frame_duration = Duration::from_millis(10); - let tick_duration = Duration::from_millis(10); - let shared_config = SharedConfig { - enable_replication: false, - tick: TickConfig::new(tick_duration), - ..Default::default() - }; - let link_conditioner = LinkConditionerConfig { - incoming_latency: Duration::from_millis(40), - incoming_jitter: Duration::from_millis(5), - incoming_loss: 0.05, - }; - let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); - let interpolation_delay = Duration::from_millis(100); - let interpolation_config = - InterpolationConfig::default().with_delay(InterpolationDelay::Delay(interpolation_delay)); - let mut stepper = BevyStepper::new( - shared_config, - sync_config, - prediction_config, - interpolation_config, - link_conditioner, - frame_duration, - ); - stepper.client_app.add_systems( - FixedUpdate, - increment_component_and_despawn_both.in_set(FixedUpdateSet::Main), - ); - - // Create a confirmed entity - let confirmed = stepper - .client_app - .world - .spawn((Component1(0.0), ShouldBePredicted)) - .id(); - - // Tick once - stepper.frame_step(); - assert_eq!(stepper.client().tick(), Tick(1)); - let predicted = stepper - .client_app - .world - .get::(confirmed) - .unwrap() - .predicted - .unwrap(); - - // check that the predicted entity got spawned - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap() - .confirmed_entity, - confirmed - ); - - // check that the component history got created - let mut history = PredictionHistory::::default(); - history - .buffer - .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - // check that the confirmed component got replicated - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap(), - &Component1(1.0) - ); - - // create a situation where the confirmed entity gets despawned during FixedUpdate::Main - stepper.client_mut().set_synced(); - stepper - .client_mut() - .set_latest_received_server_tick(Tick(0)); - // we set it to 5 so that it gets despawned during FixedUpdate::Main - stepper - .client_app - .world - .get_mut::(confirmed) - .unwrap() - .0 = 4.0; - // update without incrementing time, because we want to force a rollback check - stepper.frame_step(); - - // check that rollback happened - // confirmed and predicted both got despawned - assert!(stepper.client_app.world.get_entity(confirmed).is_none()); - assert!(stepper.client_app.world.get_entity(predicted).is_none()); - - Ok(()) -} diff --git a/tests/tests/prediction_remove_add_component.rs b/tests/tests/prediction_remove_add_component.rs deleted file mode 100644 index 8e0e7e3e9..000000000 --- a/tests/tests/prediction_remove_add_component.rs +++ /dev/null @@ -1,442 +0,0 @@ -#![allow(unused_imports)] -#![allow(unused_variables)] -#![allow(dead_code)] - -use std::net::SocketAddr; -use std::str::FromStr; -use std::time::{Duration, Instant}; - -use bevy::log::LogPlugin; -use bevy::prelude::{ - App, Commands, Entity, EventReader, FixedUpdate, IntoSystemConfigs, PluginGroup, Query, Real, - Res, ResMut, Startup, Time, With, -}; -use bevy::time::TimeUpdateStrategy; -use bevy::winit::WinitPlugin; -use bevy::{DefaultPlugins, MinimalPlugins}; -use tracing::{debug, info}; -use tracing_subscriber::fmt::format::FmtSpan; - -use lightyear::_reexport::*; -use lightyear::prelude::client::*; -use lightyear::prelude::*; -use lightyear_tests::protocol::{protocol, Channel2, Component1, MyInput, MyProtocol}; -use lightyear_tests::stepper::{BevyStepper, Step}; - -fn increment_component( - mut commands: Commands, - mut query: Query<(Entity, &mut Component1), With>, -) { - for (entity, mut component) in query.iter_mut() { - component.0 += 1.0; - if component.0 == 5.0 { - commands.entity(entity).remove::(); - } - } -} - -fn setup() -> BevyStepper { - let frame_duration = Duration::from_millis(10); - let tick_duration = Duration::from_millis(10); - let shared_config = SharedConfig { - enable_replication: false, - tick: TickConfig::new(tick_duration), - ..Default::default() - }; - let link_conditioner = LinkConditionerConfig { - incoming_latency: Duration::from_millis(40), - incoming_jitter: Duration::from_millis(5), - incoming_loss: 0.05, - }; - let sync_config = SyncConfig::default().speedup_factor(1.0); - let prediction_config = PredictionConfig::default().disable(false); - let interpolation_delay = Duration::from_millis(100); - let interpolation_config = - InterpolationConfig::default().with_delay(InterpolationDelay::Delay(interpolation_delay)); - let mut stepper = BevyStepper::new( - shared_config, - sync_config, - prediction_config, - interpolation_config, - link_conditioner, - frame_duration, - ); - stepper.client_mut().set_synced(); - stepper.client_app.add_systems( - FixedUpdate, - increment_component.in_set(FixedUpdateSet::Main), - ); - stepper -} - -// Test that if a component gets removed from the predicted entity erroneously -// We are still able to rollback properly (the rollback adds the component to the predicted entity) -#[test] -fn test_removed_predicted_component_rollback() -> anyhow::Result<()> { - let mut stepper = setup(); - - // Create a confirmed entity - let confirmed = stepper - .client_app - .world - .spawn((Component1(0.0), ShouldBePredicted)) - .id(); - - // Tick once - stepper.frame_step(); - assert_eq!(stepper.client().tick(), Tick(1)); - let predicted = stepper - .client_app - .world - .get::(confirmed) - .unwrap() - .predicted - .unwrap(); - - // check that the predicted entity got spawned - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap() - .confirmed_entity, - confirmed - ); - - // check that the component history got created - let mut history = PredictionHistory::::default(); - // this is added during the first rollback call after we create the history - history - .buffer - .add_item(Tick(0), ComponentState::Updated(Component1(0.0))); - history - .buffer - .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - // check that the confirmed component got replicated - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap(), - &Component1(1.0) - ); - - // advance five more frames, so that the component gets removed on predicted - for i in 0..5 { - stepper.frame_step(); - } - assert_eq!(stepper.client().tick(), Tick(6)); - - // check that the component got removed on predicted - assert!(stepper - .client_app - .world - .get::(predicted) - .is_none()); - // check that the component history is still there and that the value of the component history is correct - let mut history = PredictionHistory::::default(); - for i in 0..5 { - history - .buffer - .add_item(Tick(i), ComponentState::Updated(Component1(i as f32))); - } - history.buffer.add_item(Tick(5), ComponentState::Removed); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - - // create a rollback situation - stepper.client_mut().set_synced(); - stepper - .client_mut() - .set_latest_received_server_tick(Tick(3)); - stepper - .client_app - .world - .get_mut::(confirmed) - .unwrap() - .0 = 1.0; - // update without incrementing time, because we want to force a rollback check - stepper.client_app.update(); - - // check that rollback happened - // predicted got the component re-added - stepper - .client_app - .world - .get_mut::(predicted) - .unwrap() - .0 = 4.0; - // check that the history is how we expect after rollback - let mut history = PredictionHistory::::default(); - for i in 3..7 { - history - .buffer - .add_item(Tick(i), ComponentState::Updated(Component1(i as f32 - 2.0))); - } - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history - ); - - Ok(()) -} - -// Test that if a component gets added to the predicted entity erroneously but didn't exist on the confirmed entity) -// We are still able to rollback properly (the rollback removes the component from the predicted entity) -#[test] -fn test_added_predicted_component_rollback() -> anyhow::Result<()> { - let mut stepper = setup(); - - // Create a confirmed entity - let confirmed = stepper.client_app.world.spawn(ShouldBePredicted).id(); - - // Tick once - stepper.frame_step(); - assert_eq!(stepper.client().tick(), Tick(1)); - let predicted = stepper - .client_app - .world - .get::(confirmed) - .unwrap() - .predicted - .unwrap(); - - // check that the predicted entity got spawned - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap() - .confirmed_entity, - confirmed - ); - - // add a new component to Predicted - stepper - .client_app - .world - .entity_mut(predicted) - .insert(Component1(1.0)); - - // create a rollback situation (confirmed doesn't have a component that predicted has) - stepper.client_mut().set_synced(); - stepper - .client_mut() - .set_latest_received_server_tick(Tick(1)); - // update without incrementing time, because we want to force a rollback check - stepper.client_app.update(); - - // check that rollback happened: the component got removed from predicted - assert!(stepper - .client_app - .world - .get::(predicted) - .is_none()); - - // check that history contains the removal - let mut history = PredictionHistory::::default(); - history.buffer.add_item(Tick(1), ComponentState::Removed); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - Ok(()) -} - -// Test that if a component gets removed from the confirmed entity -// We are still able to rollback properly (the rollback removes the component from the predicted entity) -#[test] -fn test_removed_confirmed_component_rollback() -> anyhow::Result<()> { - let mut stepper = setup(); - - // Create a confirmed entity - let confirmed = stepper - .client_app - .world - .spawn((Component1(0.0), ShouldBePredicted)) - .id(); - - // Tick once - stepper.frame_step(); - assert_eq!(stepper.client().tick(), Tick(1)); - let predicted = stepper - .client_app - .world - .get::(confirmed) - .unwrap() - .predicted - .unwrap(); - - // check that the predicted entity got spawned - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap() - .confirmed_entity, - confirmed - ); - - // check that the component history got created - let mut history = PredictionHistory::::default(); - history - .buffer - .add_item(Tick(0), ComponentState::Updated(Component1(0.0))); - history - .buffer - .add_item(Tick(1), ComponentState::Updated(Component1(1.0))); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history, - ); - - // create a rollback situation by removing the component on confirmed - stepper.client_mut().set_synced(); - stepper - .client_mut() - .set_latest_received_server_tick(Tick(1)); - stepper - .client_app - .world - .entity_mut(confirmed) - .remove::(); - // update without incrementing time, because we want to force a rollback check - // (need duration_since_latest_received_server_tick = 0) - stepper.client_app.update(); - - // check that rollback happened - // predicted got the component removed - assert!(stepper - .client_app - .world - .get_mut::(predicted) - .is_none()); - - // check that the history is how we expect after rollback - let mut history = PredictionHistory::::default(); - history.buffer.add_item(Tick(1), ComponentState::Removed); - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history - ); - - Ok(()) -} - -// Test that if a component gets added to the confirmed entity (but didn't exist on the predicted entity) -// We are still able to rollback properly (the rollback adds the component to the predicted entity) -#[test] -fn test_added_confirmed_component_rollback() -> anyhow::Result<()> { - let mut stepper = setup(); - - // Create a confirmed entity - let confirmed = stepper.client_app.world.spawn(ShouldBePredicted).id(); - - // Tick once - stepper.frame_step(); - assert_eq!(stepper.client().tick(), Tick(1)); - let predicted = stepper - .client_app - .world - .get::(confirmed) - .unwrap() - .predicted - .unwrap(); - - // check that the predicted entity got spawned - assert_eq!( - stepper - .client_app - .world - .get::(predicted) - .unwrap() - .confirmed_entity, - confirmed - ); - - // check that the component history did not get created - assert!(stepper - .client_app - .world - .get::>(predicted) - .is_none()); - - // advance five more frames, so that the component gets removed on predicted - for i in 0..5 { - stepper.frame_step(); - } - assert_eq!(stepper.client().tick(), Tick(6)); - - // create a rollback situation by adding the component on confirmed - stepper.client_mut().set_synced(); - stepper - .client_mut() - .set_latest_received_server_tick(Tick(3)); - stepper - .client_app - .world - .entity_mut(confirmed) - .insert(Component1(1.0)); - // update without incrementing time, because we want to force a rollback check - stepper.client_app.update(); - - // check that rollback happened - // predicted got the component re-added - stepper - .client_app - .world - .get_mut::(predicted) - .unwrap() - .0 = 4.0; - // check that the history is how we expect after rollback - let mut history = PredictionHistory::::default(); - for i in 3..7 { - history - .buffer - .add_item(Tick(i), ComponentState::Updated(Component1(i as f32 - 2.0))); - } - assert_eq!( - stepper - .client_app - .world - .get::>(predicted) - .unwrap(), - &history - ); - - Ok(()) -}