diff --git a/src/lib.rs b/src/lib.rs index bd8eb885..09655dde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -432,8 +432,9 @@ pub mod prelude { NetworkChannels, ReplicationChannel, RepliconCorePlugin, }, server::{ - clients_info::ClientsInfo, has_authority, ClientEntityMap, ClientMapping, ServerPlugin, - ServerSet, TickPolicy, SERVER_ID, + clients_info::{client_visibility::ClientVisibility, ClientInfo, ClientsInfo}, + has_authority, ClientEntityMap, ClientMapping, ServerPlugin, ServerSet, TickPolicy, + VisibilityPolicy, SERVER_ID, }, ReplicationPlugins, }; diff --git a/src/network_event/server_event.rs b/src/network_event/server_event.rs index 34e5e6ba..e4a7ed07 100644 --- a/src/network_event/server_event.rs +++ b/src/network_event/server_event.rs @@ -323,10 +323,7 @@ pub fn send_with( } SendMode::Direct(client_id) => { if client_id != SERVER_ID { - if let Some(client_info) = clients_info - .iter() - .find(|client_info| client_info.id() == client_id) - { + if let Some(client_info) = clients_info.get_client(client_id) { let message = serialize_with(client_info, None, &serialize_fn)?; server.send_message(client_info.id(), channel, message.bytes); } diff --git a/src/server.rs b/src/server.rs index d41055c5..59a61dd5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -27,7 +27,7 @@ use bevy_renet::{ use crate::replicon_core::{ replication_rules::ReplicationRules, replicon_tick::RepliconTick, ReplicationChannel, }; -use clients_info::{ClientBuffers, ClientInfo, ClientsInfo}; +use clients_info::{client_visibility::Visibility, ClientBuffers, ClientInfo, ClientsInfo}; use despawn_buffer::{DespawnBuffer, DespawnBufferPlugin}; use removal_buffer::{RemovalBuffer, RemovalBufferPlugin}; use replicated_archetypes_info::ReplicatedArchetypesInfo; @@ -39,6 +39,9 @@ pub struct ServerPlugin { /// Tick configuration. pub tick_policy: TickPolicy, + /// Visibility configuration. + pub visibility_policy: VisibilityPolicy, + /// The time after which updates will be considered lost if an acknowledgment is not received for them. /// /// In practice updates will live at least `update_timeout`, and at most `2*update_timeout`. @@ -49,6 +52,7 @@ impl Default for ServerPlugin { fn default() -> Self { Self { tick_policy: TickPolicy::MaxTickRate(30), + visibility_policy: Default::default(), update_timeout: Duration::from_secs(10), } } @@ -62,9 +66,9 @@ impl Plugin for ServerPlugin { RenetServerPlugin, NetcodeServerPlugin, )) - .init_resource::() .init_resource::() .init_resource::() + .insert_resource(ClientsInfo::new(self.visibility_policy)) .configure_sets(PreUpdate, ServerSet::Receive.after(RenetReceive)) .configure_sets(PostUpdate, ServerSet::Send.before(RenetSend)) .add_systems( @@ -291,9 +295,12 @@ fn collect_changes( }; for entity in archetype.entities() { - for (init_message, update_message) in messages.iter_mut() { + for (init_message, update_message, client_info) in messages.iter_mut_with_info() { init_message.start_entity_data(entity.entity()); update_message.start_entity_data(entity.entity()); + client_info + .visibility_mut() + .cache_visibility(entity.entity()); } // SAFETY: all replicated archetypes have marker component with table storage. @@ -326,7 +333,12 @@ fn collect_changes( let mut shared_bytes = None; for (init_message, update_message, client_info) in messages.iter_mut_with_info() { - let new_entity = marker_added || client_info.just_connected; + let visibility = client_info.visibility().cached_visibility(); + if visibility == Visibility::Hidden { + continue; + } + + let new_entity = marker_added || visibility == Visibility::Gained; if new_entity || ticks.is_added(change_tick.last_run(), change_tick.this_run()) { init_message.write_component( @@ -352,7 +364,12 @@ fn collect_changes( } for (init_message, update_message, client_info) in messages.iter_mut_with_info() { - let new_entity = marker_added || client_info.just_connected; + let visibility = client_info.visibility().cached_visibility(); + if visibility == Visibility::Hidden { + continue; + } + + let new_entity = marker_added || visibility == Visibility::Gained; if new_entity || init_message.entity_data_size() != 0 { // If there is any insertion or we must initialize, include all updates into init message // and bump the last acknowledged tick to keep entity updates atomic. @@ -367,8 +384,7 @@ fn collect_changes( } } - for (init_message, _, client_info) in messages.iter_mut_with_info() { - client_info.just_connected = false; + for (init_message, _) in messages.iter_mut() { init_message.end_array()?; } @@ -422,7 +438,11 @@ fn collect_despawns( } } - for (message, _) in messages.iter_mut() { + for (message, _, client_info) in messages.iter_mut_with_info() { + for entity in client_info.drain_lost_visibility() { + message.write_entity(&mut None, entity)?; + } + message.end_array()?; } @@ -496,6 +516,18 @@ pub enum TickPolicy { Manual, } +/// Controls how visibility will be managed via [`ClientVisibility`](clients_info::client_visibility::ClientVisibility). +#[derive(Default, Debug, Clone, Copy)] +pub enum VisibilityPolicy { + /// All entities are visible by default and visibility can't be changed. + #[default] + All, + /// All entities are hidden by default and should be explicitly registered to be visible. + Blacklist, + /// All entities are visible by default and should be explicitly registered to be hidden. + Whitelist, +} + /** A resource that exists on the server for mapping server entities to entities that clients have already spawned. The mappings are sent to clients as part of replication diff --git a/src/server/clients_info.rs b/src/server/clients_info.rs index 833bbb28..34b7c7a9 100644 --- a/src/server/clients_info.rs +++ b/src/server/clients_info.rs @@ -1,3 +1,5 @@ +pub mod client_visibility; + use std::mem; use bevy::{ @@ -7,11 +9,15 @@ use bevy::{ }; use bevy_renet::renet::ClientId; -use crate::replicon_core::replicon_tick::RepliconTick; +use crate::{replicon_core::replicon_tick::RepliconTick, server::VisibilityPolicy}; +use client_visibility::ClientVisibility; /// Stores meta-information about connected clients. -#[derive(Default, Resource)] -pub struct ClientsInfo(Vec); +#[derive(Resource, Default)] +pub struct ClientsInfo { + info: Vec, + policy: VisibilityPolicy, +} /// Reusable buffers for [`ClientsInfo`] and [`ClientInfo`]. #[derive(Default, Resource)] @@ -28,19 +34,73 @@ pub(crate) struct ClientBuffers { } impl ClientsInfo { - /// Returns an iterator over clients information. - pub(crate) fn iter(&self) -> impl Iterator { - self.0.iter() + pub(super) fn new(policy: VisibilityPolicy) -> Self { + Self { + info: Default::default(), + policy, + } + } + + /// Returns a reference to a connected client's info. + /// + /// This operation is *O*(*n*). + /// See also [`Self::get_client`] for the fallible version. + /// + /// # Panics + /// + /// Panics if the passed client ID is not connected. + pub fn client(&self, client_id: ClientId) -> &ClientInfo { + self.get_client(client_id) + .unwrap_or_else(|| panic!("{client_id:?} should be connected")) } - /// Returns a mutable iterator over clients information. - pub(super) fn iter_mut(&mut self) -> impl Iterator { - self.0.iter_mut() + /// Returns a mutable reference to a connected client's info. + /// + /// This operation is *O*(*n*). + /// See also [`Self::get_client_mut`] for the fallible version. + /// + /// # Panics + /// + /// Panics if the passed client ID is not connected. + pub fn client_mut(&mut self, client_id: ClientId) -> &mut ClientInfo { + self.get_client_mut(client_id) + .unwrap_or_else(|| panic!("{client_id:?} should be connected")) } - /// Returns number of connected clients. - pub(super) fn len(&self) -> usize { - self.0.len() + /// Returns a reference to a connected client's info. + /// + /// This operation is *O*(*n*). + /// See also [`Self::client`] for the panicking version. + pub fn get_client(&self, client_id: ClientId) -> Option<&ClientInfo> { + self.info.iter().find(|info| info.id == client_id) + } + + /// Returns a mutable reference to a connected client's info. + /// + /// This operation is *O*(*n*). + /// See also [`Self::client`] for the panicking version. + pub fn get_client_mut(&mut self, client_id: ClientId) -> Option<&mut ClientInfo> { + self.info.iter_mut().find(|info| info.id == client_id) + } + + /// Returns an iterator over client information. + pub fn iter(&self) -> impl Iterator { + self.info.iter() + } + + /// Returns a mutable iterator over client information. + pub fn iter_mut(&mut self) -> impl Iterator { + self.info.iter_mut() + } + + /// Returns the number of connected clients. + pub fn len(&self) -> usize { + self.info.len() + } + + /// Returns `true` if no clients are connected. + pub fn is_empty(&self) -> bool { + self.info.is_empty() } /// Initializes a new [`ClientInfo`] for this client. @@ -51,10 +111,10 @@ impl ClientsInfo { client_info.reset(client_id); client_info } else { - ClientInfo::new(client_id) + ClientInfo::new(client_id, self.policy) }; - self.0.push(client_info); + self.info.push(client_info); } /// Removes info for the client. @@ -62,11 +122,11 @@ impl ClientsInfo { /// Keeps allocated memory in the buffers for reuse. pub(super) fn remove(&mut self, client_buffers: &mut ClientBuffers, client_id: ClientId) { let index = self - .0 + .info .iter() .position(|info| info.id == client_id) .expect("clients info should contain all connected clients"); - let mut client_info = self.0.remove(index); + let mut client_info = self.info.remove(index); client_buffers.entities.extend(client_info.drain_entities()); client_buffers.info.push(client_info); } @@ -75,23 +135,23 @@ impl ClientsInfo { /// /// Keeps allocated memory in the buffers for reuse. pub(super) fn clear(&mut self, client_buffers: &mut ClientBuffers) { - for mut client_info in self.0.drain(..) { + for mut client_info in self.info.drain(..) { client_buffers.entities.extend(client_info.drain_entities()); client_buffers.info.push(client_info); } } } -pub(crate) struct ClientInfo { +pub struct ClientInfo { /// Client's ID. id: ClientId, - /// Indicates whether the client connected in this tick. - pub(super) just_connected: bool, - /// Lowest tick for use in change detection for each entity. ticks: EntityHashMap, + /// Entity visibility settings. + visibility: ClientVisibility, + /// The last tick in which a replicated entity was spawned, despawned, or gained/lost a component from the perspective /// of the client. /// @@ -108,11 +168,11 @@ pub(crate) struct ClientInfo { } impl ClientInfo { - fn new(id: ClientId) -> Self { + fn new(id: ClientId, policy: VisibilityPolicy) -> Self { Self { id, - just_connected: true, ticks: Default::default(), + visibility: ClientVisibility::new(policy), change_tick: Default::default(), updates: Default::default(), next_update_index: Default::default(), @@ -124,6 +184,16 @@ impl ClientInfo { self.id } + /// Returns a reference to the client's visibility settings. + pub fn visibility(&self) -> &ClientVisibility { + &self.visibility + } + + /// Returns a mutable reference to the client's visibility settings. + pub fn visibility_mut(&mut self) -> &mut ClientVisibility { + &mut self.visibility + } + /// Clears all entities for unacknowledged updates, returning them as an iterator. /// /// Keeps the allocated memory for reuse. @@ -138,7 +208,7 @@ impl ClientInfo { /// Keeps the allocated memory for reuse. fn reset(&mut self, id: ClientId) { self.id = id; - self.just_connected = true; + self.visibility.clear(); self.ticks.clear(); self.updates.clear(); self.next_update_index = 0; @@ -229,10 +299,20 @@ impl ClientInfo { /// Removes a despawned entity tracked by this client. pub fn remove_despawned(&mut self, entity: Entity) { self.ticks.remove(&entity); + self.visibility.remove_despawned(entity); // We don't clean up `self.updates` for efficiency reasons. // `Self::acknowledge()` will properly ignore despawned entities. } + /// Drains all entities for which visibility was lost during this tick. + /// + /// Internal cleanup happens lazily during the iteration. + pub(super) fn drain_lost_visibility(&mut self) -> impl Iterator + '_ { + self.visibility.drain_lost_visibility().inspect(|entity| { + self.ticks.remove(entity); + }) + } + /// Removes all updates older then `min_timestamp`. /// /// Keeps allocated memory in the buffers for reuse. diff --git a/src/server/clients_info/client_visibility.rs b/src/server/clients_info/client_visibility.rs new file mode 100644 index 00000000..ebd6bd5f --- /dev/null +++ b/src/server/clients_info/client_visibility.rs @@ -0,0 +1,658 @@ +use bevy::{ + prelude::*, + utils::{hashbrown::hash_map::Entry, EntityHashMap, EntityHashSet}, +}; + +use super::VisibilityPolicy; + +/// Entity visibility settings for a client. +pub struct ClientVisibility { + filter: VisibilityFilter, + + /// Visibility for a specific entity that has been cached for re-referencing. + /// + /// Used as an optimization by server replication. + cached_visibility: Visibility, +} + +impl ClientVisibility { + /// Creates a new instance based on the preconfigured policy. + pub(super) fn new(policy: VisibilityPolicy) -> Self { + match policy { + VisibilityPolicy::All => Self::with_filter(VisibilityFilter::All { + just_connected: true, + }), + VisibilityPolicy::Blacklist => Self::with_filter(VisibilityFilter::Blacklist { + list: Default::default(), + added: Default::default(), + removed: Default::default(), + }), + VisibilityPolicy::Whitelist => Self::with_filter(VisibilityFilter::Whitelist { + list: Default::default(), + added: Default::default(), + removed: Default::default(), + }), + } + } + + /// Creates a new instance with a specific filter. + fn with_filter(filter: VisibilityFilter) -> Self { + Self { + filter, + cached_visibility: Default::default(), + } + } + + /// Resets the filter state to as it was after [`Self::new`]. + /// + /// `cached_visibility` remains untouched. + pub(super) fn clear(&mut self) { + match &mut self.filter { + VisibilityFilter::All { just_connected } => *just_connected = true, + VisibilityFilter::Blacklist { + list, + added, + removed, + } => { + list.clear(); + added.clear(); + removed.clear(); + } + VisibilityFilter::Whitelist { + list, + added, + removed, + } => { + list.clear(); + added.clear(); + removed.clear(); + } + } + } + + /// Updates list information and its sets based on the filter. + /// + /// Should be called after each tick. + pub(crate) fn update(&mut self) { + match &mut self.filter { + VisibilityFilter::All { just_connected } => *just_connected = false, + VisibilityFilter::Blacklist { + list, + added, + removed, + } => { + // Remove all entities queued for removal. + for entity in removed.drain() { + list.remove(&entity); + } + added.clear(); + } + VisibilityFilter::Whitelist { + list, + added, + removed, + } => { + // Change all recently added entities to `WhitelistInfo::Visible` + // from `WhitelistInfo::JustVisible`. + for entity in added.drain() { + list.insert(entity, WhitelistInfo::Visible); + } + removed.clear(); + } + } + } + + /// Removes a despawned entity tracked by this client. + pub(super) fn remove_despawned(&mut self, entity: Entity) { + match &mut self.filter { + VisibilityFilter::All { .. } => (), + VisibilityFilter::Blacklist { + list, + added, + removed, + } => { + if list.remove(&entity).is_some() { + added.remove(&entity); + removed.remove(&entity); + } + } + VisibilityFilter::Whitelist { + list, + added, + removed, + } => { + if list.remove(&entity).is_some() { + added.remove(&entity); + removed.remove(&entity); + } + } + } + } + + /// Drains all entities for which visibility was lost during this tick. + pub(super) fn drain_lost_visibility(&mut self) -> impl Iterator + '_ { + match &mut self.filter { + VisibilityFilter::All { .. } => VisibilityLostIter::AllVisible, + VisibilityFilter::Blacklist { added, .. } => VisibilityLostIter::Lost(added.drain()), + VisibilityFilter::Whitelist { removed, .. } => { + VisibilityLostIter::Lost(removed.drain()) + } + } + } + + /// Sets visibility for a specific entity. + /// + /// Does nothing if the visibility policy for the server plugin is set to [`VisibilityPolicy::All`]. + pub fn set_visibility(&mut self, entity: Entity, visibile: bool) { + match &mut self.filter { + VisibilityFilter::All { .. } => { + if visibile { + debug!( + "ignoring visibility enable due to {:?}", + VisibilityPolicy::All + ); + } else { + warn!( + "ignoring visibility disable due to {:?}", + VisibilityPolicy::All + ); + } + } + VisibilityFilter::Blacklist { + list, + added, + removed, + } => { + if visibile { + // If the entity is already visibile, do nothing. + let Entry::Occupied(mut entry) = list.entry(entity) else { + return; + }; + + // If the entity was previously added in this tick, then undo it. + if added.remove(&entity) { + entry.remove(); + return; + } + + // For blacklisting an entity we don't remove the entity right away. + // Instead we mark it as queued for removal and remove it + // later in `Self::update`. This allows us to avoid accessing + // the blacklist's `removed` field in `Self::get_visibility_state`. + entry.insert(BlacklistInfo::QueuedForRemoval); + removed.insert(entity); + } else { + // If the entity is already registered, reset its removal status. + if list.insert(entity, BlacklistInfo::Hidden).is_some() { + removed.remove(&entity); + return; + }; + + added.insert(entity); + } + } + VisibilityFilter::Whitelist { + list, + added, + removed, + } => { + if visibile { + // Similar to blacklist removal, we don't just add the entity to the list. + // Instead we mark it as `WhitelistInfo::JustAdded` and then set it to + // 'WhitelistInfo::Visible' in `Self::update`. + // This allows us to avoid accessing the whitelist's `added` field in + // `Self::get_visibility_state`. + if *list.entry(entity).or_insert(WhitelistInfo::JustAdded) + == WhitelistInfo::JustAdded + { + // Do not mark an entry as newly added if the entry was already in the list. + added.insert(entity); + } + removed.remove(&entity); + } else { + // If the entity is not in the whitelist, do nothing. + if list.remove(&entity).is_none() { + return; + } + + // If the entity was added in this tick, then undo it. + if added.remove(&entity) { + return; + } + + removed.insert(entity); + } + } + } + } + + /// Checks if a specific entity is visible. + pub fn is_visible(&self, entity: Entity) -> bool { + match self.get_visibility_state(entity) { + Visibility::Hidden => false, + Visibility::Gained | Visibility::Visible => true, + } + } + + /// Caches visibility for a specific entity. + /// + /// Can be obtained later from [`Self::cached_visibility`]. + pub(crate) fn cache_visibility(&mut self, entity: Entity) { + self.cached_visibility = self.get_visibility_state(entity); + } + + /// Returns visibility cached by the last call of [`Self::cache_visibility`]. + pub(crate) fn cached_visibility(&self) -> Visibility { + self.cached_visibility + } + + /// Returns visibility of a specific entity. + fn get_visibility_state(&self, entity: Entity) -> Visibility { + match &self.filter { + VisibilityFilter::All { just_connected } => { + if *just_connected { + Visibility::Gained + } else { + Visibility::Visible + } + } + VisibilityFilter::Blacklist { list, .. } => match list.get(&entity) { + Some(BlacklistInfo::QueuedForRemoval) => Visibility::Gained, + Some(BlacklistInfo::Hidden) => Visibility::Hidden, + None => Visibility::Visible, + }, + VisibilityFilter::Whitelist { list, .. } => match list.get(&entity) { + Some(WhitelistInfo::JustAdded) => Visibility::Gained, + Some(WhitelistInfo::Visible) => Visibility::Visible, + None => Visibility::Hidden, + }, + } + } +} + +/// Filter for [`ClientVisibility`] based on [`VisibilityPolicy`]. +enum VisibilityFilter { + All { + /// Indicates that the client has just connected to the server. + /// + /// If true, then visibility of all entities has been gained. + just_connected: bool, + }, + Blacklist { + /// All blacklisted entities and an indicator of whether they are in the queue for deletion + /// at the end of this tick. + list: EntityHashMap, + + /// All entities that were removed from the list in this tick. + /// + /// Visibility of these entities has been lost. + added: EntityHashSet, + + /// All entities that were added to the list in this tick. + /// + /// Visibility of these entities has been gained. + removed: EntityHashSet, + }, + Whitelist { + /// All whitelisted entities and an indicator of whether they were added to the list in + /// this tick. + list: EntityHashMap, + + /// All entities that were added to the list in this tick. + /// + /// Visibility of these entities has been gained. + added: EntityHashSet, + + /// All entities that were removed from the list in this tick. + /// + /// Visibility of these entities has been lost. + removed: EntityHashSet, + }, +} + +#[derive(PartialEq, Clone, Copy)] +enum WhitelistInfo { + Visible, + JustAdded, +} + +#[derive(PartialEq, Clone, Copy)] +enum BlacklistInfo { + Hidden, + QueuedForRemoval, +} + +/// Visibility state for an entity in the current tick, from the perspective of one client. +/// +/// Note that the distinction between 'lost visibility' and 'don't have visibility' is not exposed here. +/// There is only `Visibility::Hidden` to encompass both variants. +/// +/// Lost visibility is handled separately with [`ClientVisibility::drain_lost_visibility`]. +#[derive(PartialEq, Default, Clone, Copy)] +pub(crate) enum Visibility { + /// The client does not have visibility of the entity in this tick. + #[default] + Hidden, + /// The client gained visibility of the entity in this tick (it was not visible in the previous tick). + Gained, + /// The entity is visible to the client (and was visible in the previous tick). + Visible, +} + +enum VisibilityLostIter { + AllVisible, + Lost(T), +} + +impl Iterator for VisibilityLostIter { + type Item = T::Item; + + fn next(&mut self) -> Option { + match self { + VisibilityLostIter::AllVisible => None, + VisibilityLostIter::Lost(entities) => entities.next(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::All); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + visibility.set_visibility(Entity::PLACEHOLDER, true); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + visibility.set_visibility(Entity::PLACEHOLDER, false); + assert!( + visibility.is_visible(Entity::PLACEHOLDER), + "shouldn't have any effect for this policy" + ); + } + + #[test] + fn blacklist_insertion() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Blacklist); + visibility.set_visibility(Entity::PLACEHOLDER, false); + assert!(!visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + + visibility.update(); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn blacklist_empty_removal() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Blacklist); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + visibility.set_visibility(Entity::PLACEHOLDER, true); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn blacklist_removal() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Blacklist); + visibility.set_visibility(Entity::PLACEHOLDER, false); + visibility.update(); + visibility.set_visibility(Entity::PLACEHOLDER, true); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(removed.contains(&Entity::PLACEHOLDER)); + + visibility.update(); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn blacklist_insertion_removal() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Blacklist); + + // Insert and remove from the list. + visibility.set_visibility(Entity::PLACEHOLDER, false); + visibility.set_visibility(Entity::PLACEHOLDER, true); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn blacklist_duplicate_insertion() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Blacklist); + visibility.set_visibility(Entity::PLACEHOLDER, false); + visibility.update(); + + // Duplicate insertion. + visibility.set_visibility(Entity::PLACEHOLDER, false); + assert!(!visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Blacklist { + list, + added, + removed, + } = visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn whitelist_insertion() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Whitelist); + visibility.set_visibility(Entity::PLACEHOLDER, true); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a whitelist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + + visibility.update(); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn whitelist_empty_removal() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Whitelist); + assert!(!visibility.is_visible(Entity::PLACEHOLDER)); + + visibility.set_visibility(Entity::PLACEHOLDER, false); + assert!(!visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = visibility.filter + else { + panic!("filter should be a whitelist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn whitelist_removal() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Whitelist); + visibility.set_visibility(Entity::PLACEHOLDER, true); + visibility.update(); + visibility.set_visibility(Entity::PLACEHOLDER, false); + assert!(!visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a whitelist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(removed.contains(&Entity::PLACEHOLDER)); + + visibility.update(); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = &visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn whitelist_insertion_removal() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Whitelist); + + // Insert and remove from the list. + visibility.set_visibility(Entity::PLACEHOLDER, true); + visibility.set_visibility(Entity::PLACEHOLDER, false); + assert!(!visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(!list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } + + #[test] + fn whitelist_duplicate_insertion() { + let mut visibility = ClientVisibility::new(VisibilityPolicy::Whitelist); + visibility.set_visibility(Entity::PLACEHOLDER, true); + visibility.update(); + + // Duplicate insertion. + visibility.set_visibility(Entity::PLACEHOLDER, true); + assert!(visibility.is_visible(Entity::PLACEHOLDER)); + + let VisibilityFilter::Whitelist { + list, + added, + removed, + } = visibility.filter + else { + panic!("filter should be a blacklist"); + }; + + assert!(list.contains_key(&Entity::PLACEHOLDER)); + assert!(!added.contains(&Entity::PLACEHOLDER)); + assert!(!removed.contains(&Entity::PLACEHOLDER)); + } +} diff --git a/src/server/replication_messages.rs b/src/server/replication_messages.rs index 2af2dbad..5b91883b 100644 --- a/src/server/replication_messages.rs +++ b/src/server/replication_messages.rs @@ -94,6 +94,7 @@ impl ReplicationMessages { tick, timestamp, )?; + client_info.visibility_mut().update(); } let clients_info = mem::take(&mut self.clients_info); diff --git a/tests/replication.rs b/tests/replication.rs index bab93f45..d6a2b257 100644 --- a/tests/replication.rs +++ b/tests/replication.rs @@ -677,6 +677,7 @@ fn update_replication_cleanup() { ReplicationPlugins.set(ServerPlugin { tick_policy: TickPolicy::EveryFrame, update_timeout: Duration::ZERO, // Will cause dropping updates after each frame. + ..Default::default() }), )) .replicate::(); @@ -751,6 +752,278 @@ fn update_replication_cleanup() { ); } +#[test] +fn all_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + let server_entity = server_app.world.spawn((Replication, TableComponent)).id(); + + let client_transport = client_app.world.resource::(); + let client_id = ClientId::from_raw(client_transport.client_id()); + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, false); // Shouldn't have any effect for this policy. + + server_app.update(); + client_app.update(); + + client_app + .world + .query_filtered::<(), (With, With)>() + .single(&client_app.world); + + // Reverse visibility back. + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, true); + + server_app.update(); + client_app.update(); + + client_app + .world + .query_filtered::<(), (With, With)>() + .single(&client_app.world); +} + +#[test] +fn empty_blacklist_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + visibility_policy: VisibilityPolicy::Blacklist, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + server_app.world.spawn((Replication, TableComponent)); + + server_app.update(); + client_app.update(); + + client_app + .world + .query_filtered::<(), (With, With)>() + .single(&client_app.world); +} + +#[test] +fn blacklist_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + visibility_policy: VisibilityPolicy::Blacklist, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + let server_entity = server_app.world.spawn((Replication, TableComponent)).id(); + + let client_transport = client_app.world.resource::(); + let client_id = ClientId::from_raw(client_transport.client_id()); + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, false); + + server_app.update(); + client_app.update(); + + assert!(client_app.world.entities().is_empty()); + + // Reverse visibility back. + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, true); + + server_app.update(); + client_app.update(); + + client_app + .world + .query_filtered::<(), (With, With)>() + .single(&client_app.world); +} + +#[test] +fn blacklist_despawn_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + visibility_policy: VisibilityPolicy::Blacklist, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + let server_entity = server_app.world.spawn(Replication).id(); + + let client_transport = client_app.world.resource::(); + let client_id = ClientId::from_raw(client_transport.client_id()); + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, false); + server_app.world.despawn(server_entity); + + server_app.update(); + client_app.update(); + + assert!(client_app.world.entities().is_empty()); + + let clients_info = server_app.world.resource::(); + let visibility = clients_info.client(client_id).visibility(); + assert!(visibility.is_visible(server_entity)); // The missing entity must be removed from the list, so this should return `true`. +} + +#[test] +fn empty_whitelist_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + visibility_policy: VisibilityPolicy::Whitelist, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + server_app.world.spawn((Replication, TableComponent)); + + server_app.update(); + client_app.update(); + + assert!( + client_app.world.entities().is_empty(), + "no entities should be replicated without adding to whitelist" + ); +} + +#[test] +fn whitelist_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + visibility_policy: VisibilityPolicy::Whitelist, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + let server_entity = server_app.world.spawn((Replication, TableComponent)).id(); + + let client_transport = client_app.world.resource::(); + let client_id = ClientId::from_raw(client_transport.client_id()); + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, true); + + server_app.update(); + client_app.update(); + + client_app + .world + .query_filtered::<(), (With, With)>() + .single(&client_app.world); + + // Reverse visibility. + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, false); + + server_app.update(); + client_app.update(); + + assert!( + client_app.world.entities().is_empty(), + "entity should be despawned after removing from whitelist" + ); +} + +#[test] +fn whitelist_despawn_visibility() { + 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 { + tick_policy: TickPolicy::EveryFrame, + visibility_policy: VisibilityPolicy::Whitelist, + ..Default::default() + }), + )) + .replicate::(); + } + + connect::single_client(&mut server_app, &mut client_app); + + let server_entity = server_app.world.spawn(Replication).id(); + + let client_transport = client_app.world.resource::(); + let client_id = ClientId::from_raw(client_transport.client_id()); + let mut clients_info = server_app.world.resource_mut::(); + let visibility = clients_info.client_mut(client_id).visibility_mut(); + visibility.set_visibility(server_entity, true); + server_app.world.despawn(server_entity); + + server_app.update(); + client_app.update(); + + assert!(client_app.world.entities().is_empty()); + + let clients_info = server_app.world.resource::(); + let visibility = clients_info.client(client_id).visibility(); + assert!(!visibility.is_visible(server_entity)); +} + #[test] fn replication_into_scene() { let mut app = App::new();