From af6a5d1428b885234dca8164e8f0e24ea6c65b52 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 2 Oct 2023 10:32:01 +0100 Subject: [PATCH] Make changes to NetworkTick, now a Resource, trigger sends. Allows for more flexible fixedtimestep integration, with manual incrementing of the network tick. --- examples/tic_tac_toe.rs | 2 +- src/client.rs | 67 ++++++++++---- src/lib.rs | 26 ++++-- src/replicon_core.rs | 17 ++-- src/replicon_core/replication_rules.rs | 71 +++++++++++++-- src/server.rs | 57 ++++++------ tests/common/mod.rs | 5 +- tests/replication.rs | 118 ++++++++++++++++++++++++- tests/server_event.rs | 3 + 9 files changed, 303 insertions(+), 63 deletions(-) diff --git a/examples/tic_tac_toe.rs b/examples/tic_tac_toe.rs index c382d208..1177dfdd 100644 --- a/examples/tic_tac_toe.rs +++ b/examples/tic_tac_toe.rs @@ -34,7 +34,7 @@ fn main() { }), ..Default::default() })) - .add_plugins((ReplicationPlugins, TicTacToePlugin)) + .add_plugins((ReplicationPlugins::default(), TicTacToePlugin)) .run(); } diff --git a/src/client.rs b/src/client.rs index 8a63e320..f821d2fe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,11 +10,14 @@ use bevy_renet::{renet::RenetClient, transport::NetcodeClientPlugin, RenetClient use bincode::{DefaultOptions, Options}; use crate::replicon_core::{ - replication_rules::{Mapper, Replication, ReplicationRules}, + replication_rules::{EntityDespawnFn, Mapper, Replication, ReplicationRules}, NetworkTick, REPLICATION_CHANNEL_ID, }; -pub struct ClientPlugin; +#[derive(Default)] +pub struct ClientPlugin { + despawn_fn: Option, +} impl Plugin for ClientPlugin { fn build(&self, app: &mut App) { @@ -45,10 +48,24 @@ impl Plugin for ClientPlugin { Self::reset_system.run_if(resource_removed::()), ), ); + if let Some(entity_despawn_fn) = self.despawn_fn { + app.world + .resource_scope(|_, mut replication_rules: Mut| { + replication_rules.set_despawn_fn(entity_despawn_fn); + }); + } } } impl ClientPlugin { + /// only useful in case you need to replace the default entity despawn function. + /// otherwis just use `ClientPlugin::default()` + pub fn new(despawn_fn: EntityDespawnFn) -> Self { + Self { + despawn_fn: Some(despawn_fn), + } + } + fn diff_receiving_system(world: &mut World) -> Result<(), bincode::Error> { world.resource_scope(|world, mut client: Mut| { world.resource_scope(|world, mut entity_map: Mut| { @@ -57,7 +74,9 @@ impl ClientPlugin { let end_pos: u64 = message.len().try_into().unwrap(); let mut cursor = Cursor::new(message); - if !deserialize_tick(&mut cursor, world)? { + let (network_tick, was_updated) = deserialize_tick(&mut cursor, world)?; + + if !was_updated { continue; } if cursor.position() == end_pos { @@ -70,6 +89,7 @@ impl ClientPlugin { &mut entity_map, &replication_rules, DiffKind::Change, + network_tick, )?; if cursor.position() == end_pos { continue; @@ -81,12 +101,19 @@ impl ClientPlugin { &mut entity_map, &replication_rules, DiffKind::Removal, + network_tick, )?; if cursor.position() == end_pos { continue; } - deserialize_despawns(&mut cursor, world, &mut entity_map)?; + deserialize_despawns( + &mut cursor, + world, + &mut entity_map, + network_tick, + replication_rules.get_despawn_fn(), + )?; } Ok(()) @@ -109,16 +136,19 @@ impl ClientPlugin { /// Deserializes server tick and applies it to [`LastTick`] if it is newer. /// -/// Returns true if [`LastTick`] has been updated. -fn deserialize_tick(cursor: &mut Cursor, world: &mut World) -> Result { - let tick = bincode::deserialize_from(cursor)?; +/// Returns (network_tick, true) if [`LastTick`] has been updated, otherwise (network_tick, false). +fn deserialize_tick( + cursor: &mut Cursor, + world: &mut World, +) -> Result<(NetworkTick, bool), bincode::Error> { + let network_tick = bincode::deserialize_from(cursor)?; let mut last_tick = world.resource_mut::(); - if last_tick.0 < tick { - last_tick.0 = tick; - Ok(true) + if last_tick.0 < network_tick { + last_tick.0 = network_tick; + Ok((network_tick, true)) } else { - Ok(false) + Ok((network_tick, false)) } } @@ -129,6 +159,7 @@ fn deserialize_component_diffs( entity_map: &mut NetworkEntityMap, replication_rules: &ReplicationRules, diff_kind: DiffKind, + tick: NetworkTick, ) -> Result<(), bincode::Error> { let entities_count: u16 = bincode::deserialize_from(&mut *cursor)?; for _ in 0..entities_count { @@ -141,9 +172,9 @@ fn deserialize_component_diffs( let replication_info = unsafe { replication_rules.get_info_unchecked(replication_id) }; match diff_kind { DiffKind::Change => { - (replication_info.deserialize)(&mut entity, entity_map, cursor)? + (replication_info.deserialize)(&mut entity, entity_map, cursor, tick)? } - DiffKind::Removal => (replication_info.remove)(&mut entity), + DiffKind::Removal => (replication_info.remove)(&mut entity, tick), } } } @@ -156,6 +187,8 @@ fn deserialize_despawns( cursor: &mut Cursor, world: &mut World, entity_map: &mut NetworkEntityMap, + tick: NetworkTick, + custom_entity_despawn_fn: Option, ) -> Result<(), bincode::Error> { let entities_count: u16 = bincode::deserialize_from(&mut *cursor)?; for _ in 0..entities_count { @@ -163,11 +196,15 @@ fn deserialize_despawns( // with the last diff, but the server might not yet have received confirmation // from the client and could include the deletion in the latest diff. let server_entity = deserialize_entity(&mut *cursor)?; - if let Some(client_entity) = entity_map + if let Some(mut client_entity) = entity_map .remove_by_server(server_entity) .and_then(|entity| world.get_entity_mut(entity)) { - client_entity.despawn_recursive(); + if let Some(despawn_fn) = custom_entity_despawn_fn { + (despawn_fn)(&mut client_entity, tick); + } else { + client_entity.despawn_recursive(); + } } } diff --git a/src/lib.rs b/src/lib.rs index 8c1c9d51..4467933f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,9 +19,10 @@ app.add_plugins(MinimalPlugins) .add_plugins(ReplicationPlugins); ``` -This group contains necessary replication stuff and setups server and client -plugins to let you host and join games from the same application. If you -planning to separate client and server you can use +This group contains necessary replication stuff and sets up the server and client +plugins to let you host and join games from the same application. + +If you are planning to separate client and server you can use `disable()` to disable [`ClientPlugin`] or [`ServerPlugin`]. You can also configure how often updates are sent from server to clients with [`ServerPlugin`]'s [`TickPolicy`].: @@ -115,6 +116,7 @@ fn deserialize_transform( entity: &mut EntityMut, _entity_map: &mut NetworkEntityMap, cursor: &mut Cursor, + _tick: NetworkTick, ) -> Result<(), bincode::Error> { let translation: Vec3 = bincode::deserialize_from(cursor)?; entity.insert(Transform::from_translation(translation)); @@ -131,6 +133,18 @@ will be replicated. If you need to disable replication for specific component for specific entity, you can insert [`Ignored`] component and replication will be skipped for `T`. +### NetworkTick, and fixed timestep games. + +The [`ServerPlugin`] sends replication data in `PostUpdate` any time the [`NetworkTick`] resource +changes. By default, NetworkTick is incremented in PostUpdate per the [`TickPolicy`]. + +If you set `TickPolicy::Manual`, you can increment [`NetworkTick`] at the start of your +`FixedTimestep` game loop. This value can represent your simulation step, and is made available +to the client in the custom deserialization, despawn andcomponent removal functions. + +One use for this is rollback networking: you may want to rollback time and apply the update +for the NetworkTick frame, which is in the past, then resimulate. + ### "Blueprints" pattern The idea was borrowed from [iyes_scene_tools](https://github.com/IyesGames/iyes_scene_tools#blueprints-pattern). @@ -175,7 +189,7 @@ fn player_init_system( #[derive(Component, Deserialize, Serialize)] struct Player; # fn serialize_transform(_: Ptr, _: &mut Cursor>) -> Result<(), bincode::Error> { unimplemented!() } -# fn deserialize_transform(_: &mut EntityMut, _: &mut NetworkEntityMap, _: &mut Cursor) -> Result<(), bincode::Error> { unimplemented!() } +# fn deserialize_transform(_: &mut EntityMut, _: &mut NetworkEntityMap, _: &mut Cursor, _: NetworkTick) -> Result<(), bincode::Error> { unimplemented!() } ``` This pairs nicely with server state serialization and keeps saves clean. @@ -393,6 +407,8 @@ pub use bevy_renet::*; pub use bincode; use prelude::*; +/// Plugin Group for all replicon plugins. +#[derive(Default)] pub struct ReplicationPlugins; impl PluginGroup for ReplicationPlugins { @@ -400,7 +416,7 @@ impl PluginGroup for ReplicationPlugins { PluginGroupBuilder::start::() .add(RepliconCorePlugin) .add(ParentSyncPlugin) - .add(ClientPlugin) + .add(ClientPlugin::default()) .add(ServerPlugin::default()) } } diff --git a/src/replicon_core.rs b/src/replicon_core.rs index ac806026..654b7456 100644 --- a/src/replicon_core.rs +++ b/src/replicon_core.rs @@ -1,10 +1,9 @@ pub mod replication_rules; -use std::cmp::Ordering; - use bevy::prelude::*; use bevy_renet::renet::{ChannelConfig, SendType}; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use replication_rules::ReplicationRules; @@ -74,11 +73,19 @@ fn channel_configs(channels: &[SendType]) -> Vec { channel_configs } -/// Corresponds to the number of server update. +/// A tick that increments each time we need the server to compute and sends an update. +/// This is mapped to the bevy Tick within [`ServerTicks`]. /// /// See also [`crate::server::TickPolicy`]. -#[derive(Clone, Copy, Default, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct NetworkTick(u32); +#[derive(Clone, Copy, Default, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Resource)] +pub struct NetworkTick(pub u32); + +impl std::ops::Deref for NetworkTick { + type Target = u32; + fn deref(&self) -> &Self::Target { + &self.0 + } +} impl NetworkTick { /// Creates a new [`NetworkTick`] wrapping the given value. diff --git a/src/replicon_core/replication_rules.rs b/src/replicon_core/replication_rules.rs index 79a3c157..0b65f912 100644 --- a/src/replicon_core/replication_rules.rs +++ b/src/replicon_core/replication_rules.rs @@ -12,6 +12,8 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::client::{ClientMapper, NetworkEntityMap}; +use super::NetworkTick; + pub trait AppReplicationExt { /// Marks component for replication. /// @@ -36,6 +38,17 @@ pub trait AppReplicationExt { ) -> &mut Self where C: Component; + + /// Same as [`Self::replicate`], but uses the specified functions for serialization and deserialization, + /// and uses the specified removal function for removing components from entities. + fn replicate_and_remove_with( + &mut self, + serialize: SerializeFn, + deserialize: DeserializeFn, + remove: RemoveComponentFn, + ) -> &mut Self + where + C: Component; } impl AppReplicationExt for App { @@ -54,6 +67,18 @@ impl AppReplicationExt for App { } fn replicate_with(&mut self, serialize: SerializeFn, deserialize: DeserializeFn) -> &mut Self + where + C: Component, + { + self.replicate_and_remove_with::(serialize, deserialize, remove_component::) + } + + fn replicate_and_remove_with( + &mut self, + serialize: SerializeFn, + deserialize: DeserializeFn, + remove: RemoveComponentFn, + ) -> &mut Self where C: Component, { @@ -63,7 +88,7 @@ impl AppReplicationExt for App { ignored_id, serialize, deserialize, - remove: remove_component::, + remove, }; let mut replication_rules = self.world.resource_mut::(); @@ -89,6 +114,11 @@ pub(crate) struct ReplicationRules { /// ID of [`Replication`] component. marker_id: ComponentId, + + /// Custom function to handle entity despawning. + /// The default does despawn_recursive() – but if you're doing rollback networking you + /// may need to intercept the despawn and do it differently. + despawn_entity_fn: Option, } impl ReplicationRules { @@ -114,6 +144,18 @@ impl ReplicationRules { Some((replication_id, replication_info)) } + /// Gets custom function used to despawn entities, if provided. + #[inline] + pub(crate) fn get_despawn_fn(&self) -> Option { + self.despawn_entity_fn + } + + /// Sets a custom function for despawning entities on the client, when the server reports the despawn; + /// without this, the default implementation uses despawn_recursive(). + pub(crate) fn set_despawn_fn(&mut self, despawn_entity_fn: EntityDespawnFn) { + self.despawn_entity_fn = Some(despawn_entity_fn); + } + /// Returns meta information about replicated component. /// /// # Safety @@ -133,6 +175,7 @@ impl FromWorld for ReplicationRules { infos: Default::default(), ids: Default::default(), marker_id: world.init_component::(), + despawn_entity_fn: Default::default(), } } } @@ -141,8 +184,18 @@ impl FromWorld for ReplicationRules { pub type SerializeFn = fn(Ptr, &mut Cursor>) -> Result<(), bincode::Error>; /// Signature of component deserialization functions. -pub type DeserializeFn = - fn(&mut EntityMut, &mut NetworkEntityMap, &mut Cursor) -> Result<(), bincode::Error>; +pub type DeserializeFn = fn( + &mut EntityMut, + &mut NetworkEntityMap, + &mut Cursor, + NetworkTick, +) -> Result<(), bincode::Error>; + +/// Signature of component removal functions. +pub type RemoveComponentFn = fn(&mut EntityMut, NetworkTick); + +/// Signature of entity despawning functions. +pub type EntityDespawnFn = fn(&mut EntityMut, NetworkTick); /// Stores meta information about replicated component. pub(crate) struct ReplicationInfo { @@ -156,7 +209,7 @@ pub(crate) struct ReplicationInfo { pub(crate) deserialize: DeserializeFn, /// Function that removes specific component from [`EntityMut`]. - pub(crate) remove: fn(&mut EntityMut), + pub(crate) remove: RemoveComponentFn, } /// Marks entity for replication. @@ -192,7 +245,7 @@ pub trait Mapper { } /// Default serialization function. -fn serialize_component( +pub fn serialize_component( component: Ptr, cursor: &mut Cursor>, ) -> Result<(), bincode::Error> { @@ -202,10 +255,11 @@ fn serialize_component( } /// Default deserialization function. -fn deserialize_component( +pub fn deserialize_component( entity: &mut EntityMut, _entity_map: &mut NetworkEntityMap, cursor: &mut Cursor, + _tick: NetworkTick, ) -> Result<(), bincode::Error> { let component: C = DefaultOptions::new().deserialize_from(cursor)?; entity.insert(component); @@ -214,10 +268,11 @@ fn deserialize_component( } /// Like [`deserialize_component`], but also maps entities before insertion. -fn deserialize_mapped_component( +pub fn deserialize_mapped_component( entity: &mut EntityMut, entity_map: &mut NetworkEntityMap, cursor: &mut Cursor, + _tick: NetworkTick, ) -> Result<(), bincode::Error> { let mut component: C = DefaultOptions::new().deserialize_from(cursor)?; @@ -231,6 +286,6 @@ fn deserialize_mapped_component(entity: &mut EntityMut) { +pub fn remove_component(entity: &mut EntityMut, _tick: NetworkTick) { entity.remove::(); } diff --git a/src/server.rs b/src/server.rs index ea44fc67..e339b4a1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -21,7 +21,6 @@ use bevy_renet::{ RenetServerPlugin, }; use bincode::{DefaultOptions, Options}; -use derive_more::Constructor; use crate::replicon_core::{ replication_rules::{ReplicationId, ReplicationInfo, ReplicationRules}, @@ -32,7 +31,6 @@ use removal_tracker::{RemovalTracker, RemovalTrackerPlugin}; pub const SERVER_ID: u64 = 0; -#[derive(Constructor)] pub struct ServerPlugin { tick_policy: TickPolicy, } @@ -54,13 +52,17 @@ impl Plugin for ServerPlugin { DespawnTrackerPlugin, )) .init_resource::() + .init_resource::() .configure_set( PreUpdate, ServerSet::Receive.after(NetcodeServerPlugin::update_system), ) + // sending happens each time the NetworkTick resource changes .configure_set( PostUpdate, - ServerSet::Send.before(NetcodeServerPlugin::send_packets), + ServerSet::Send + .before(NetcodeServerPlugin::send_packets) + .run_if(resource_changed::()), ) .add_systems( PreUpdate, @@ -81,12 +83,26 @@ impl Plugin for ServerPlugin { if let TickPolicy::MaxTickRate(max_tick_rate) = self.tick_policy { let tick_time = Duration::from_millis(1000 / max_tick_rate as u64); - app.configure_set(PostUpdate, ServerSet::Send.run_if(on_timer(tick_time))); + app.add_systems( + PostUpdate, + increment_network_tick + .before(Self::diffs_sending_system) + .run_if(on_timer(tick_time)), + ); } } } +/// calls NetworkTick.increment() which causes the server to send a diff packet this frame +pub fn increment_network_tick(mut network_tick: ResMut) { + network_tick.increment(); +} + impl ServerPlugin { + pub fn new(tick_policy: TickPolicy) -> Self { + Self { tick_policy } + } + fn acks_receiving_system( mut server_ticks: ResMut, mut server: ResMut, @@ -131,11 +147,11 @@ impl ServerPlugin { replication_rules: Res, despawn_tracker: Res, removal_trackers: Query<(Entity, &RemovalTracker)>, + network_tick: Res, ) -> Result<(), bincode::Error> { let mut server_ticks = set.p2(); - server_ticks.increment(change_tick.this_run()); - - let buffers = prepare_buffers(&mut buffers, &server_ticks)?; + server_ticks.register_network_tick(*network_tick, change_tick.this_run()); + let buffers = prepare_buffers(&mut buffers, &server_ticks, *network_tick)?; collect_changes( buffers, set.p0(), @@ -178,6 +194,7 @@ impl ServerPlugin { fn prepare_buffers<'a>( buffers: &'a mut Vec, server_ticks: &ServerTicks, + network_tick: NetworkTick, ) -> Result<&'a mut [ReplicationBuffer], bincode::Error> { buffers.reserve(server_ticks.acked_ticks.len()); for (index, (&client_id, &tick)) in server_ticks.acked_ticks.iter().enumerate() { @@ -187,12 +204,12 @@ fn prepare_buffers<'a>( .unwrap_or(&Tick::new(0)); if let Some(buffer) = buffers.get_mut(index) { - buffer.reset(client_id, system_tick, server_ticks.current_tick)?; + buffer.reset(client_id, system_tick, network_tick)?; } else { buffers.push(ReplicationBuffer::new( client_id, system_tick, - server_ticks.current_tick, + network_tick, )?); } } @@ -384,9 +401,6 @@ pub enum TickPolicy { /// Used only on server. #[derive(Resource, Default)] pub struct ServerTicks { - /// Current server tick. - current_tick: NetworkTick, - /// Last acknowledged server ticks for all clients. acked_ticks: HashMap, @@ -396,9 +410,8 @@ pub struct ServerTicks { impl ServerTicks { /// Increments current tick by 1 and makes corresponding system tick mapping for it. - fn increment(&mut self, system_tick: Tick) { - self.current_tick.increment(); - self.system_ticks.insert(self.current_tick, system_tick); + fn register_network_tick(&mut self, network_tick: NetworkTick, system_tick: Tick) { + self.system_ticks.insert(network_tick, system_tick); } /// Removes system tick mappings for acks that was acknowledged by everyone. @@ -410,12 +423,6 @@ impl ServerTicks { }) } - /// Returns current server tick. - #[inline] - pub fn current_tick(&self) -> NetworkTick { - self.current_tick - } - /// Returns last acknowledged server ticks for all clients. #[inline] pub fn acked_ticks(&self) -> &HashMap { @@ -466,10 +473,10 @@ impl ReplicationBuffer { fn new( client_id: u64, system_tick: Tick, - current_tick: NetworkTick, + network_tick: NetworkTick, ) -> Result { let mut message = Default::default(); - bincode::serialize_into(&mut message, ¤t_tick)?; + bincode::serialize_into(&mut message, &network_tick)?; Ok(Self { client_id, system_tick, @@ -492,14 +499,14 @@ impl ReplicationBuffer { &mut self, client_id: u64, system_tick: Tick, - current_tick: NetworkTick, + network_tick: NetworkTick, ) -> Result<(), bincode::Error> { self.client_id = client_id; self.system_tick = system_tick; self.message.set_position(0); self.message.get_mut().clear(); self.arrays_with_data = 0; - bincode::serialize_into(&mut self.message, ¤t_tick)?; + bincode::serialize_into(&mut self.message, &network_tick)?; Ok(()) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7b85944c..cfc712cc 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -20,6 +20,7 @@ use bevy_renet::renet::{ ChannelConfig, ConnectionConfig, RenetClient, RenetServer, }; use bevy_replicon::prelude::*; +use bevy_replicon::server::increment_network_tick; use serde::{ de::{self, DeserializeSeed, SeqAccess, Visitor}, ser::SerializeStruct, @@ -47,7 +48,9 @@ pub(super) fn connect(server_app: &mut App, client_app: &mut App) { server_app .insert_resource(server) - .insert_resource(server_transport); + .insert_resource(server_transport) + // send every tick. have to increment ourselves because of TickPolicy::Manual + .add_systems(Update, increment_network_tick); client_app .insert_resource(client) diff --git a/tests/replication.rs b/tests/replication.rs index 237060ff..f9bd0183 100644 --- a/tests/replication.rs +++ b/tests/replication.rs @@ -1,7 +1,7 @@ mod common; -use bevy::prelude::*; -use bevy_replicon::{prelude::*, server}; +use bevy::{ecs::world::EntityMut, prelude::*}; +use bevy_replicon::{prelude::*, replicon_core::replication_rules, server}; use bevy_renet::renet::transport::NetcodeClientTransport; use serde::{Deserialize, Serialize}; @@ -190,6 +190,63 @@ fn removal_replication() { assert!(client_entity.contains::()); } +/// custom component removal. stores a copy in WrapperComponent and then removes. +fn custom_removal_fn(entity: &mut EntityMut, tick: NetworkTick) { + let oldval: &T = entity.get::().unwrap(); + entity + .insert(WrapperComponent(oldval.clone(), tick)) + .remove::(); +} + +#[test] +fn custom_removal_replication() { + let mut server_app = App::new(); + let mut client_app = App::new(); + for app in [&mut server_app, &mut client_app] { + app.add_plugins(( + MinimalPlugins, + ReplicationPlugins.set(ServerPlugin::new(TickPolicy::Manual)), + )) + .replicate_and_remove_with::( + replication_rules::serialize_component::, + replication_rules::deserialize_component::, + custom_removal_fn::, + ); + } + + common::connect(&mut server_app, &mut client_app); + + let server_entity = server_app + .world + .spawn((Replication, TableComponent, NonReplicatingComponent)) + .id(); + + server_app.update(); + + server_app + .world + .entity_mut(server_entity) + .remove::(); + + let client_entity = client_app + .world + .spawn((Replication, TableComponent, NonReplicatingComponent)) + .id(); + + client_app + .world + .resource_mut::() + .insert(server_entity, client_entity); + + server_app.update(); + client_app.update(); + + let client_entity = client_app.world.entity(client_entity); + assert!(!client_entity.contains::()); + assert!(client_entity.contains::>()); + assert!(client_entity.contains::()); +} + #[test] fn despawn_replication() { let mut server_app = App::new(); @@ -237,6 +294,58 @@ fn despawn_replication() { assert!(entity_map.to_server().is_empty()); } +#[derive(Component)] +struct DespawnMarker(u32); + +fn custom_despawn_fn(e: &mut EntityMut, tick: NetworkTick) { + e.insert(DespawnMarker(*tick)); +} + +#[test] +fn custom_despawn_replication() { + let mut server_app = App::new(); + let mut client_app = App::new(); + for app in [&mut server_app, &mut client_app] { + app.add_plugins(( + MinimalPlugins, + ReplicationPlugins + .set(ServerPlugin::new(TickPolicy::Manual)) + .set(ClientPlugin::new(custom_despawn_fn)), + )); + } + + common::connect(&mut server_app, &mut client_app); + + let server_entity = server_app.world.spawn(Replication).id(); + + server_app.update(); + + server_app.world.despawn(server_entity); + + let client_entity = client_app.world.spawn_empty().id(); + + let mut entity_map = client_app.world.resource_mut::(); + entity_map.insert(server_entity, client_entity); + + server_app.update(); + client_app.update(); + + // rather than being despawned, our custom despawn fn will insert a DespawnMarker. + assert!(client_app + .world + .get_entity(client_entity) + .unwrap() + .contains::()); + + // it is correct that the NetworkEntityMap has removed this entity, because once it's + // despawned on the server, it's gone forever. + // even if the client keeps it around for a few frames, the server won't be sending us + // any more updates about it. + let entity_map = client_app.world.resource::(); + assert!(entity_map.to_client().is_empty()); + assert!(entity_map.to_server().is_empty()); +} + #[test] fn replication_into_scene() { let mut app = App::new(); @@ -280,9 +389,12 @@ impl MapNetworkEntities for MappedComponent { } } -#[derive(Component, Deserialize, Serialize)] +#[derive(Component, Deserialize, Serialize, Clone)] struct TableComponent; +#[derive(Component, Deserialize, Serialize)] +struct WrapperComponent(T, NetworkTick); + #[derive(Component, Deserialize, Serialize)] #[component(storage = "SparseSet")] struct SparseSetComponent; diff --git a/tests/server_event.rs b/tests/server_event.rs index 654f4541..c9cc95d3 100644 --- a/tests/server_event.rs +++ b/tests/server_event.rs @@ -218,6 +218,9 @@ fn local_resending() { )) .add_server_event::(SendPolicy::Ordered); + // send every tick. have to increment ourselves because of TickPolicy::Manual + app.add_systems(Update, bevy_replicon::server::increment_network_tick); + const DUMMY_CLIENT_ID: u64 = 1; for (mode, events_count) in [ (SendMode::Broadcast, 1),