Repository: simgine/bevy_replicon (Rust)

Issue:
Implement `With` and `Without` filters for replication groups
It would be quite convenient to support Bevy's filters in `replicate_with` as in queries, via second generic parameter. These rules will be applied when filtering archetypes.


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 670ee47c..351e6680 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- `*_filtered` methods for `AppRuleExt` that allow using `With`, `Without` and `Or` filters to define replication rules.
 - `ClientSet::PrepareSend` and `ServerSet::PrepareSend` system sets. Backends should use these sets to add `PostUpdate` logic that needs to run before sending data on clients and servers.
 - `compact_entity` with functions for `serde` to pack `Entity` more efficienly.
 - `postcard_utils::entity_to_extend_mut` and `postcard_utils::entity_from_buf` helpers that use `compact_entity`.
diff --git a/src/lib.rs b/src/lib.rs
index 177cb313..6cc35fb7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -135,6 +135,10 @@ or pass a tuple of [`RuleFns`] to [`AppRuleExt::replicate_with`]. The components
 be replicated if all of them are present on the entity. This also allows you to specialize
 serialization and deserialization based on specific entity components.
 
+All functions also have `_filtered` variants that additionally accept archetypal filters,
+similar to [`Query`]. They are separate functions because Rust doesn't allow default generics
+for functions. For more details, see [`AppRuleExt::replicate_filtered`].
+
 #### Required components
 
 You don't want to replicate all components because not all of them are
diff --git a/src/shared/replication/rules.rs b/src/shared/replication/rules.rs
index 1f45f576..74d7181b 100644
--- a/src/shared/replication/rules.rs
+++ b/src/shared/replication/rules.rs
@@ -1,4 +1,5 @@
 pub mod component;
+pub mod filter;
 
 use core::cmp::Reverse;
 
@@ -12,6 +13,7 @@ use serde::{Serialize, de::DeserializeOwned};
 use super::registry::{ReplicationRegistry, command_fns::MutWrite};
 use crate::prelude::*;
 use component::{BundleRules, ComponentRule, IntoComponentRules};
+use filter::{FilterRule, FilterRules};
 
 /// Replication functions for [`App`].
 pub trait AppRuleExt {
@@ -26,23 +28,74 @@ pub trait AppRuleExt {
     where
         C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
     {
-        self.replicate_with(RuleFns::<C>::default())
+        self.replicate_filtered::<C, ()>()
     }
 
-    /// Same as [`Self::replicate`], but uses [`SendRate::Once`].
+    /// Like [`Self::replicate`], but uses [`SendRate::Once`].
     fn replicate_once<C>(&mut self) -> &mut Self
     where
         C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
     {
-        self.replicate_with((RuleFns::<C>::default(), SendRate::Once))
+        self.replicate_once_filtered::<C, ()>()
     }
 
-    /// Sames as [`Self::replicate`], but uses [`SendRate::Periodic`] with the given tick period.
+    /// Like [`Self::replicate`], but uses [`SendRate::Periodic`] with the given tick period.
     fn replicate_periodic<C>(&mut self, period: u32) -> &mut Self
     where
         C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
     {
-        self.replicate_with((RuleFns::<C>::default(), SendRate::Periodic(period)))
+        self.replicate_periodic_filtered::<C, ()>(period)
+    }
+
+    /// Like [`Self::replicate`], but lets you specify archetype filters an entity must match to replicate.
+    ///
+    /// Supports [`With`], [`Without`], [`Or`], and tuples of them, similar to the second generic parameter of [`Query`].
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use bevy::prelude::*;
+    /// # use bevy_replicon::prelude::*;
+    /// # use serde::{Deserialize, Serialize};
+    /// # let mut app = App::new();
+    /// # app.add_plugins(RepliconPlugins);
+    /// app.replicate_filtered::<Transform, With<Player>>() // Replicate `Transform` only for players.
+    ///     .replicate_filtered::<Health, Or<(With<Player>, With<Enemy>)>>() // Replicate `Health` only for player and enemies.
+    ///     .replicate_filtered::<Platform, (With<Active>, Without<Moving>)>(); // Replicate only active and non-moving platforms.
+    /// # #[derive(Component)]
+    /// # struct Player;
+    /// # #[derive(Component)]
+    /// # struct Enemy;
+    /// # #[derive(Component, Serialize, Deserialize)]
+    /// # struct Health;
+    /// # #[derive(Component, Serialize, Deserialize)]
+    /// # struct Platform;
+    /// # #[derive(Component)]
+    /// # struct Moving;
+    /// # #[derive(Component)]
+    /// # struct Active;
+    /// ```
+    fn replicate_filtered<C, F: FilterRules>(&mut self) -> &mut Self
+    where
+        C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
+    {
+        self.replicate_with_filtered::<_, F>(RuleFns::<C>::default())
+    }
+
+    /// Like [`Self::replicate_filtered`], but for [`Self::replicate_once`].
+    fn replicate_once_filtered<C, F: FilterRules>(&mut self) -> &mut Self
+    where
+        C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
+    {
+        self.replicate_with_filtered::<_, F>((RuleFns::<C>::default(), SendRate::Once))
+    }
+
+    /// Like [`Self::replicate_filtered`], but for [`Self::replicate_periodic`].
+    fn replicate_periodic_filtered<C, F: FilterRules>(&mut self, period: u32) -> &mut Self
+    where
+        C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
+    {
+        self.replicate_with_filtered::<_, F>((RuleFns::<C>::default(), SendRate::Periodic(period)))
     }
 
     /**
@@ -341,14 +394,58 @@ pub trait AppRuleExt {
     ```
     **/
     fn replicate_with<R: IntoComponentRules>(&mut self, component_rules: R) -> &mut Self {
-        self.replicate_with_priority(R::DEFAULT_PRIORITY, component_rules)
+        self.replicate_with_filtered::<_, ()>(component_rules)
+    }
+
+    /// Like [`Self::replicate_filtered`], but for [`Self::replicate_with`].
+    ///
+    /// It’s recommended to omit the first parameter and let the compiler infer it from the arguments.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use bevy::prelude::*;
+    /// # use bevy_replicon::prelude::*;
+    /// # use serde::{Deserialize, Serialize};
+    /// # let mut app = App::new();
+    /// # app.add_plugins(RepliconPlugins);
+    /// app.replicate_with_filtered::<_, With<StaticBox>>((
+    ///     RuleFns::<Health>::default(),
+    ///     (RuleFns::<Transform>::default(), SendRate::Once),
+    /// ));
+    /// # #[derive(Component)]
+    /// # struct StaticBox;
+    /// # #[derive(Component, Serialize, Deserialize)]
+    /// # struct Health;
+    /// ```
+    fn replicate_with_filtered<R: IntoComponentRules, F: FilterRules>(
+        &mut self,
+        component_rules: R,
+    ) -> &mut Self {
+        self.replicate_with_priority_filtered::<_, F>(
+            R::DEFAULT_PRIORITY + F::DEFAULT_PRIORITY,
+            component_rules,
+        )
     }
 
     /// Same as [`Self::replicate_with`], but uses the specified priority instead of the default one.
+    ///
+    /// The default priority equals the total number of components in the rule
     fn replicate_with_priority<R: IntoComponentRules>(
         &mut self,
         priority: usize,
         component_rules: R,
+    ) -> &mut Self {
+        self.replicate_with_priority_filtered::<_, ()>(priority, component_rules)
+    }
+
+    /// Like [`Self::replicate_filtered`], but for [`Self::replicate_with_priority`].
+    ///
+    /// The default priority equals the total number of components **and** filters in the rule
+    fn replicate_with_priority_filtered<R: IntoComponentRules, F: FilterRules>(
+        &mut self,
+        priority: usize,
+        component_rules: R,
     ) -> &mut Self;
 
     /**
@@ -422,14 +519,25 @@ pub trait AppRuleExt {
     ```
     **/
     fn replicate_bundle<B: BundleRules>(&mut self) -> &mut Self {
-        self.replicate_bundle_with::<B>(B::DEFAULT_PRIORITY)
+        self.replicate_bundle_filtered::<B, ()>()
     }
 
-    fn replicate_bundle_with<B: BundleRules>(&mut self, priority: usize) -> &mut Self;
+    fn replicate_bundle_filtered<B: BundleRules, F: FilterRules>(&mut self) -> &mut Self {
+        self.replicate_bundle_with_filtered::<B, F>(B::DEFAULT_PRIORITY + F::DEFAULT_PRIORITY)
+    }
+
+    fn replicate_bundle_with<B: BundleRules>(&mut self, priority: usize) -> &mut Self {
+        self.replicate_bundle_with_filtered::<B, ()>(priority)
+    }
+
+    fn replicate_bundle_with_filtered<B: BundleRules, F: FilterRules>(
+        &mut self,
+        priority: usize,
+    ) -> &mut Self;
 }
 
 impl AppRuleExt for App {
-    fn replicate_with_priority<R: IntoComponentRules>(
+    fn replicate_with_priority_filtered<R: IntoComponentRules, F: FilterRules>(
         &mut self,
         priority: usize,
         component_rules: R,
@@ -444,17 +552,23 @@ impl AppRuleExt for App {
                     component_rules.into_rules(world, &mut registry)
                 });
 
+        let filters = F::filter_rules(self.world_mut());
+
         self.world_mut()
             .resource_mut::<ReplicationRules>()
             .insert(ReplicationRule {
                 priority,
                 components,
+                filters,
             });
 
         self
     }
 
-    fn replicate_bundle_with<B: BundleRules>(&mut self, priority: usize) -> &mut Self {
+    fn replicate_bundle_with_filtered<B: BundleRules, F: FilterRules>(
+        &mut self,
+        priority: usize,
+    ) -> &mut Self {
         self.world_mut()
             .resource_mut::<ProtocolHasher>()
             .replicate_bundle::<B>();
@@ -465,11 +579,14 @@ impl AppRuleExt for App {
                     B::component_rules(world, &mut registry)
                 });
 
+        let filters = F::filter_rules(self.world_mut());
+
         self.world_mut()
             .resource_mut::<ReplicationRules>()
             .insert(ReplicationRule {
                 priority,
                 components,
+                filters,
             });
 
         self
@@ -499,6 +616,8 @@ impl ReplicationRules {
 }
 
 /// Describes how component(s) will be replicated.
+///
+/// Created using methods from [`AppRuleExt`].
 #[derive(Clone, Debug)]
 pub struct ReplicationRule {
     /// Priority for this rule.
@@ -509,11 +628,19 @@ pub struct ReplicationRule {
 
     /// Components for the rule.
     pub components: Vec<ComponentRule>,
+
+    /// Associated filters.
+    pub filters: Vec<FilterRule>,
 }
 
 impl ReplicationRule {
     /// Determines whether an archetype contains all components required by the rule.
+    #[must_use]
     pub(crate) fn matches(&self, archetype: &Archetype) -> bool {
+        if !self.filters.iter().all(|filter| filter.matches(archetype)) {
+            return false;
+        }
+
         self.components
             .iter()
             .all(|component| archetype.contains(component.id))
@@ -550,69 +677,258 @@ mod tests {
     use super::*;
 
     #[test]
-    fn registration() {
+    fn single() {
         let mut app = App::new();
         app.init_resource::<ProtocolHasher>()
             .init_resource::<ReplicationRules>()
             .init_resource::<ReplicationRegistry>()
             .replicate::<A>()
-            .replicate_with((RuleFns::<A>::default(), RuleFns::<B>::default()))
             .replicate_once::<B>()
-            .replicate_with((
-                (RuleFns::<B>::default(), SendRate::Once),
-                (RuleFns::<C>::default(), SendRate::Periodic(2)),
+            .replicate_periodic::<C>(1);
+
+        let rules = app
+            .world_mut()
+            .remove_resource::<ReplicationRules>()
+            .unwrap();
+        let [rule_a, rule_b, rule_c] = rules.0.try_into().unwrap();
+        assert_eq!(rule_a.priority, 1);
+        assert_eq!(rule_b.priority, 1);
+        assert_eq!(rule_c.priority, 1);
+
+        let a = app.world_mut().spawn(A).archetype().id();
+        let b = app.world_mut().spawn(B).archetype().id();
+        let c = app.world_mut().spawn(C).archetype().id();
+
+        let a = app.world().archetypes().get(a).unwrap();
+        let b = app.world().archetypes().get(b).unwrap();
+        let c = app.world().archetypes().get(c).unwrap();
+
+        assert!(rule_a.matches(a));
+        assert!(!rule_a.matches(b));
+        assert!(!rule_a.matches(c));
+
+        assert!(!rule_b.matches(a));
+        assert!(rule_b.matches(b));
+        assert!(!rule_b.matches(c));
+
+        assert!(!rule_c.matches(a));
+        assert!(!rule_c.matches(b));
+        assert!(rule_c.matches(c));
+    }
+
+    #[test]
+    fn single_filtered() {
+        let mut app = App::new();
+        app.init_resource::<ProtocolHasher>()
+            .init_resource::<ReplicationRules>()
+            .init_resource::<ReplicationRegistry>()
+            .replicate_filtered::<A, With<B>>()
+            .replicate_once_filtered::<B, Or<(With<A>, With<C>)>>()
+            .replicate_periodic_filtered::<C, (Without<A>, With<B>)>(1);
+
+        let rules = app
+            .world_mut()
+            .remove_resource::<ReplicationRules>()
+            .unwrap();
+        let [rule_b_ac, rule_c_ab, rule_a_b] = rules.0.try_into().unwrap();
+        assert_eq!(rule_b_ac.priority, 3);
+        assert_eq!(rule_c_ab.priority, 3);
+        assert_eq!(rule_a_b.priority, 2);
+
+        let abc = app.world_mut().spawn((A, B, C)).archetype().id();
+        let bcd = app.world_mut().spawn((B, C, D)).archetype().id();
+        let cda = app.world_mut().spawn((C, D, A)).archetype().id();
+
+        let abc = app.world().archetypes().get(abc).unwrap();
+        let bcd = app.world().archetypes().get(bcd).unwrap();
+        let cda = app.world().archetypes().get(cda).unwrap();
+
+        assert!(rule_b_ac.matches(abc));
+        assert!(rule_b_ac.matches(bcd));
+        assert!(!rule_b_ac.matches(cda));
+
+        assert!(!rule_c_ab.matches(abc));
+        assert!(rule_c_ab.matches(bcd));
+        assert!(!rule_c_ab.matches(cda));
+
+        assert!(rule_a_b.matches(abc));
+        assert!(!rule_a_b.matches(bcd));
+        assert!(!rule_a_b.matches(cda));
+    }
+
+    #[test]
+    fn with() {
+        let mut app = App::new();
+        app.init_resource::<ProtocolHasher>()
+            .init_resource::<ReplicationRules>()
+            .init_resource::<ReplicationRegistry>()
+            .replicate_with((RuleFns::<A>::default(), SendRate::Once))
+            .replicate_with((RuleFns::<B>::default(), RuleFns::<C>::default()))
+            .replicate_with_priority(
+                4,
+                (
+                    RuleFns::<C>::default(),
+                    (RuleFns::<D>::default(), SendRate::Periodic(1)),
+                ),
+            );
+
+        let rules = app
+            .world_mut()
+            .remove_resource::<ReplicationRules>()
+            .unwrap();
+        let [rule_cd, rule_bc, rule_a] = rules.0.try_into().unwrap();
+        assert_eq!(rule_cd.priority, 4);
+        assert_eq!(rule_bc.priority, 2);
+        assert_eq!(rule_a.priority, 1);
+
+        let abc = app.world_mut().spawn((A, B, C)).archetype().id();
+        let bcd = app.world_mut().spawn((B, C, D)).archetype().id();
+        let cda = app.world_mut().spawn((C, D, A)).archetype().id();
+
+        let abc = app.world().archetypes().get(abc).unwrap();
+        let bcd = app.world().archetypes().get(bcd).unwrap();
+        let cda = app.world().archetypes().get(cda).unwrap();
+
+        assert!(!rule_cd.matches(abc));
+        assert!(rule_cd.matches(bcd));
+        assert!(rule_cd.matches(cda));
+
+        assert!(rule_bc.matches(abc));
+        assert!(rule_bc.matches(bcd));
+        assert!(!rule_bc.matches(cda));
+
+        assert!(rule_a.matches(abc));
+        assert!(!rule_a.matches(bcd));
+        assert!(rule_a.matches(cda));
+    }
+
+    #[test]
+    fn with_filtered() {
+        let mut app = App::new();
+        app.init_resource::<ProtocolHasher>()
+            .init_resource::<ReplicationRules>()
+            .init_resource::<ReplicationRegistry>()
+            .replicate_with_filtered::<_, With<B>>((RuleFns::<A>::default(), SendRate::Once))
+            .replicate_with_filtered::<_, Or<(With<A>, With<D>)>>((
+                RuleFns::<B>::default(),
+                RuleFns::<C>::default(),
             ))
-            .replicate_periodic::<C>(1)
-            .replicate_with_priority(4, (RuleFns::<C>::default(), RuleFns::<D>::default()))
-            .replicate_with((RuleFns::<D>::default(), SendRate::Once))
-            .replicate_bundle::<(A, B)>();
-
-        let a = app.world().component_id::<A>().unwrap();
-        let b = app.world().component_id::<B>().unwrap();
-        let c = app.world().component_id::<C>().unwrap();
-        let d = app.world().component_id::<D>().unwrap();
-
-        let rules = &**app.world().resource::<ReplicationRules>();
-
-        assert_eq!(rules[0].priority, 4);
-        assert_eq!(rules[0].components[0].id, c);
-        assert_eq!(rules[0].components[0].send_rate, SendRate::EveryTick);
-        assert_eq!(rules[0].components[1].id, d);
-        assert_eq!(rules[0].components[1].send_rate, SendRate::EveryTick);
-
-        assert_eq!(rules[1].priority, 2);
-        assert_eq!(rules[1].components[0].id, a);
-        assert_eq!(rules[1].components[0].send_rate, SendRate::EveryTick);
-        assert_eq!(rules[1].components[1].id, b);
-        assert_eq!(rules[1].components[1].send_rate, SendRate::EveryTick);
-
-        assert_eq!(rules[2].priority, 2);
-        assert_eq!(rules[2].components[0].id, b);
-        assert_eq!(rules[2].components[0].send_rate, SendRate::Once);
-        assert_eq!(rules[2].components[1].id, c);
-        assert_eq!(rules[2].components[1].send_rate, SendRate::Periodic(2));
-
-        assert_eq!(rules[3].priority, 2);
-        assert_eq!(rules[3].components[0].id, a);
-        assert_eq!(rules[3].components[0].send_rate, SendRate::EveryTick);
-        assert_eq!(rules[3].components[1].id, b);
-        assert_eq!(rules[3].components[1].send_rate, SendRate::EveryTick);
-
-        assert_eq!(rules[4].priority, 1);
-        assert_eq!(rules[4].components[0].id, a);
-        assert_eq!(rules[4].components[0].send_rate, SendRate::EveryTick);
-
-        assert_eq!(rules[5].priority, 1);
-        assert_eq!(rules[5].components[0].id, b);
-        assert_eq!(rules[5].components[0].send_rate, SendRate::Once);
-
-        assert_eq!(rules[6].priority, 1);
-        assert_eq!(rules[6].components[0].id, c);
-        assert_eq!(rules[6].components[0].send_rate, SendRate::Periodic(1));
-
-        assert_eq!(rules[7].priority, 1);
-        assert_eq!(rules[7].components[0].id, d);
-        assert_eq!(rules[7].components[0].send_rate, SendRate::Once);
+            .replicate_with_priority_filtered::<_, (Without<A>, With<B>)>(
+                5,
+                (
+                    RuleFns::<C>::default(),
+                    (RuleFns::<D>::default(), SendRate::Periodic(1)),
+                ),
+            );
+
+        let rules = app
+            .world_mut()
+            .remove_resource::<ReplicationRules>()
+            .unwrap();
+        let [rule_cd_ab, rule_bc_ad, rule_a_b] = rules.0.try_into().unwrap();
+        assert_eq!(rule_cd_ab.priority, 5);
+        assert_eq!(rule_bc_ad.priority, 4);
+        assert_eq!(rule_a_b.priority, 2);
+
+        let abc = app.world_mut().spawn((A, B, C)).archetype().id();
+        let bcd = app.world_mut().spawn((B, C, D)).archetype().id();
+        let cda = app.world_mut().spawn((C, D, A)).archetype().id();
+
+        let abc = app.world().archetypes().get(abc).unwrap();
+        let bcd = app.world().archetypes().get(bcd).unwrap();
+        let cda = app.world().archetypes().get(cda).unwrap();
+
+        assert!(!rule_cd_ab.matches(abc));
+        assert!(rule_cd_ab.matches(bcd));
+        assert!(!rule_cd_ab.matches(cda));
+
+        assert!(rule_bc_ad.matches(abc));
+        assert!(rule_bc_ad.matches(bcd));
+        assert!(!rule_bc_ad.matches(cda));
+
+        assert!(rule_a_b.matches(abc));
+        assert!(!rule_a_b.matches(bcd));
+        assert!(!rule_a_b.matches(cda));
+    }
+
+    #[test]
+    fn bundle() {
+        let mut app = App::new();
+        app.init_resource::<ProtocolHasher>()
+            .init_resource::<ReplicationRules>()
+            .init_resource::<ReplicationRegistry>()
+            .replicate_bundle::<(A, B)>()
+            .replicate_bundle::<(B, C)>()
+            .replicate_bundle_with::<(C, D)>(4);
+
+        let rules = app
+            .world_mut()
+            .remove_resource::<ReplicationRules>()
+            .unwrap();
+        let [rule_cd, rule_ab, rule_bc] = rules.0.try_into().unwrap();
+        assert_eq!(rule_cd.priority, 4);
+        assert_eq!(rule_ab.priority, 2);
+        assert_eq!(rule_bc.priority, 2);
+
+        let abc = app.world_mut().spawn((A, B, C)).archetype().id();
+        let bcd = app.world_mut().spawn((B, C, D)).archetype().id();
+        let cda = app.world_mut().spawn((C, D, A)).archetype().id();
+
+        let abc = app.world().archetypes().get(abc).unwrap();
+        let bcd = app.world().archetypes().get(bcd).unwrap();
+        let cda = app.world().archetypes().get(cda).unwrap();
+
+        assert!(!rule_cd.matches(abc));
+        assert!(rule_cd.matches(bcd));
+        assert!(rule_cd.matches(cda));
+
+        assert!(rule_ab.matches(abc));
+        assert!(!rule_ab.matches(bcd));
+        assert!(!rule_ab.matches(cda));
+
+        assert!(rule_bc.matches(abc));
+        assert!(rule_bc.matches(bcd));
+        assert!(!rule_bc.matches(cda));
+    }
+
+    #[test]
+    fn bundle_filtered() {
+        let mut app = App::new();
+        app.init_resource::<ProtocolHasher>()
+            .init_resource::<ReplicationRules>()
+            .init_resource::<ReplicationRegistry>()
+            .replicate_bundle_filtered::<(A, B), With<C>>()
+            .replicate_bundle_filtered::<(B, C), Or<(With<A>, With<D>)>>()
+            .replicate_bundle_with_filtered::<(C, D), (Without<A>, With<B>)>(5);
+
+        let rules = app
+            .world_mut()
+            .remove_resource::<ReplicationRules>()
+            .unwrap();
+        let [rule_cd_ab, rule_bc_ad, rule_ab_c] = rules.0.try_into().unwrap();
+        assert_eq!(rule_cd_ab.priority, 5);
+        assert_eq!(rule_bc_ad.priority, 4);
+        assert_eq!(rule_ab_c.priority, 3);
+
+        let abc = app.world_mut().spawn((A, B, C)).archetype().id();
+        let bcd = app.world_mut().spawn((B, C, D)).archetype().id();
+        let cda = app.world_mut().spawn((C, D, A)).archetype().id();
+
+        let abc = app.world().archetypes().get(abc).unwrap();
+        let bcd = app.world().archetypes().get(bcd).unwrap();
+        let cda = app.world().archetypes().get(cda).unwrap();
+
+        assert!(!rule_cd_ab.matches(abc));
+        assert!(rule_cd_ab.matches(bcd));
+        assert!(!rule_cd_ab.matches(cda));
+
+        assert!(rule_bc_ad.matches(abc));
+        assert!(rule_bc_ad.matches(bcd));
+        assert!(!rule_bc_ad.matches(cda));
+
+        assert!(rule_ab_c.matches(abc));
+        assert!(!rule_ab_c.matches(bcd));
+        assert!(!rule_ab_c.matches(cda));
     }
 
     #[derive(Serialize, Deserialize, Component)]
diff --git a/src/shared/replication/rules/filter.rs b/src/shared/replication/rules/filter.rs
new file mode 100644
index 00000000..b40cdb99
--- /dev/null
+++ b/src/shared/replication/rules/filter.rs
@@ -0,0 +1,179 @@
+use bevy::{
+    ecs::{archetype::Archetype, component::ComponentId},
+    prelude::*,
+};
+
+/// Filter for [`ReplicationRule`](super::ReplicationRule).
+#[derive(Clone, Debug)]
+pub enum FilterRule {
+    /// Corresponds to [`With`].
+    With(ComponentId),
+    /// Corresponds to [`Without`].
+    Without(ComponentId),
+    /// Corresponds to [`Or`].
+    Or(Vec<FilterRule>),
+}
+
+impl FilterRule {
+    pub(super) fn matches(&self, archetype: &Archetype) -> bool {
+        match self {
+            Self::With(id) => archetype.contains(*id),
+            Self::Without(id) => !archetype.contains(*id),
+            Self::Or(children) => children.iter().any(|child| child.matches(archetype)),
+        }
+    }
+}
+
+/// Types that produce filters for [`ReplicationRule`](super::ReplicationRule).
+pub trait FilterRules {
+    /// Number of top-level rules.
+    ///
+    /// Used for preallocation.
+    const ARITY: usize;
+
+    /// Priority that is added to the default components priority.
+    ///
+    /// Equal to the number of all rules, including nested.
+    const DEFAULT_PRIORITY: usize;
+
+    /// Returns all filters for the type.
+    fn filter_rules(world: &mut World) -> Vec<FilterRule> {
+        let mut filters = Vec::with_capacity(Self::ARITY);
+        Self::push_filters(world, &mut filters);
+        filters
+    }
+
+    /// Appends filters to a [`Vec`].
+    fn push_filters(world: &mut World, filters: &mut Vec<FilterRule>);
+}
+
+impl<T: Component> FilterRules for With<T> {
+    const ARITY: usize = 1;
+    const DEFAULT_PRIORITY: usize = 1;
+
+    fn push_filters(world: &mut World, filters: &mut Vec<FilterRule>) {
+        let id = world.register_component::<T>();
+        filters.push(FilterRule::With(id));
+    }
+}
+
+impl<T: Component> FilterRules for Without<T> {
+    const ARITY: usize = 1;
+    const DEFAULT_PRIORITY: usize = 1;
+
+    fn push_filters(world: &mut World, filters: &mut Vec<FilterRule>) {
+        let id = world.register_component::<T>();
+        filters.push(FilterRule::Without(id));
+    }
+}
+
+impl<F: FilterRules> FilterRules for Or<F> {
+    const ARITY: usize = 1;
+    const DEFAULT_PRIORITY: usize = F::DEFAULT_PRIORITY;
+
+    fn push_filters(world: &mut World, filters: &mut Vec<FilterRule>) {
+        filters.push(FilterRule::Or(F::filter_rules(world)));
+    }
+}
+
+macro_rules! impl_replication_filter {
+    ($($name:ident),*) => {
+        impl<$($name: FilterRules),*> FilterRules for ($($name,)*) {
+            const ARITY: usize = 0 $(+ $name::ARITY)*;
+            const DEFAULT_PRIORITY: usize = 0 $(+ $name::DEFAULT_PRIORITY)*;
+
+            fn push_filters(_world: &mut World, _filters: &mut Vec<FilterRule>) {
+                $($name::push_filters(_world, _filters);)*
+            }
+        }
+    };
+}
+
+variadics_please::all_tuples!(impl_replication_filter, 0, 15, F);
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn with() {
+        let mut world = World::new();
+        let [rule] = With::<A>::filter_rules(&mut world).try_into().unwrap();
+
+        assert!(rule.matches(world.spawn(A).archetype()));
+        assert!(rule.matches(world.spawn((A, B)).archetype()));
+        assert!(!rule.matches(world.spawn(B).archetype()));
+    }
+
+    #[test]
+    fn without() {
+        let mut world = World::new();
+        let [rule] = Without::<A>::filter_rules(&mut world).try_into().unwrap();
+
+        assert!(!rule.matches(world.spawn(A).archetype()));
+        assert!(!rule.matches(world.spawn((A, B)).archetype()));
+        assert!(rule.matches(world.spawn(B).archetype()));
+    }
+
+    #[test]
+    fn or() {
+        type Filters = Or<(With<A>, With<B>)>;
+        assert_eq!(Filters::ARITY, 1);
+        assert_eq!(Filters::DEFAULT_PRIORITY, 2);
+
+        let mut world = World::new();
+        let [rule] = Filters::filter_rules(&mut world).try_into().unwrap();
+
+        assert!(rule.matches(world.spawn(A).archetype()));
+        assert!(rule.matches(world.spawn(B).archetype()));
+        assert!(rule.matches(world.spawn((A, C)).archetype()));
+        assert!(rule.matches(world.spawn((B, C)).archetype()));
+        assert!(!rule.matches(world.spawn(C).archetype()));
+    }
+
+    #[test]
+    fn tuple() {
+        type Filters = (With<A>, Without<B>, Or<(With<B>, With<C>)>);
+        assert_eq!(Filters::DEFAULT_PRIORITY, 4);
+        assert_eq!(Filters::ARITY, 3);
+
+        let mut world = World::new();
+        let [with_a, without_b, with_b_or_c] =
+            Filters::filter_rules(&mut world).try_into().unwrap();
+
+        let a = world.spawn(A).archetype().id();
+        let b = world.spawn(B).archetype().id();
+        let ac = world.spawn((A, C)).archetype().id();
+        let ab = world.spawn((A, B)).archetype().id();
+
+        let a = world.archetypes().get(a).unwrap();
+        let b = world.archetypes().get(b).unwrap();
+        let ac = world.archetypes().get(ac).unwrap();
+        let ab = world.archetypes().get(ab).unwrap();
+
+        assert!(with_a.matches(a));
+        assert!(without_b.matches(a));
+        assert!(!with_b_or_c.matches(a));
+
+        assert!(!with_a.matches(b));
+        assert!(!without_b.matches(b));
+        assert!(with_b_or_c.matches(b));
+
+        assert!(with_a.matches(ac));
+        assert!(without_b.matches(ac));
+        assert!(with_b_or_c.matches(ac));
+
+        assert!(with_a.matches(ab));
+        assert!(!without_b.matches(ab));
+        assert!(with_b_or_c.matches(ab));
+    }
+
+    #[derive(Component)]
+    struct A;
+
+    #[derive(Component)]
+    struct B;
+
+    #[derive(Component)]
+    struct C;
+}


Test patch:
diff --git a/tests/mutations.rs b/tests/mutations.rs
index 3489c3be..d13820a8 100644
--- a/tests/mutations.rs
+++ b/tests/mutations.rs
@@ -347,6 +347,65 @@ fn periodic_with_miss() {
     assert!(component.0);
 }
 
+#[test]
+fn filtered() {
+    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,
+            RepliconPlugins.set(ServerPlugin {
+                tick_policy: TickPolicy::EveryFrame,
+                ..Default::default()
+            }),
+        ))
+        .replicate_filtered::<BoolComponent, Without<TestComponent>>()
+        .finish();
+    }
+
+    server_app.connect_client(&mut client_app);
+
+    let with_match = server_app
+        .world_mut()
+        .spawn((Replicated, BoolComponent(false)))
+        .id();
+    let without_match = server_app
+        .world_mut()
+        .spawn((Replicated, BoolComponent(false), TestComponent))
+        .id();
+
+    server_app.update();
+    server_app.exchange_with_client(&mut client_app);
+    client_app.update();
+    server_app.exchange_with_client(&mut client_app);
+
+    // Change values.
+    let mut component = server_app
+        .world_mut()
+        .get_mut::<BoolComponent>(with_match)
+        .unwrap();
+    component.0 = true;
+    let mut component = server_app
+        .world_mut()
+        .get_mut::<BoolComponent>(without_match)
+        .unwrap();
+    component.0 = true;
+
+    server_app.update();
+    server_app.exchange_with_client(&mut client_app);
+    client_app.update();
+
+    let component = client_app
+        .world_mut()
+        .query::<&BoolComponent>()
+        .single(client_app.world())
+        .unwrap();
+    assert!(
+        component.0,
+        "only entity that matches the filter should be replicated"
+    );
+}
+
 #[test]
 fn related() {
     let mut server_app = App::new();