diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eda9be7..def785c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for `no_std`. +- Seamless support for immutable components. For these components, replication is always applied via insertion. ### Changed diff --git a/src/lib.rs b/src/lib.rs index 382e4e7c..7d89e58e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,6 +188,22 @@ component inside it. However, it's preferred to use required components when pos it's better to require a [`Handle`] with a default value that doesn't point to any asset and initialize it later in a hook or observer. This way you avoid archetype moves in ECS. +#### Mutability + +There are two ways to change a component value on an entity: re-inserting it or mutating it. + +We use Bevy’s change detection to track and send changes. However, it does not distinguish between modifications +and re-insertions. This is why we simply send the list of changes and decide how to apply them on the client. +By default, this behavior is based on [`Component::Mutability`]. + +When a component is [`Mutable`](bevy::ecs::component::Mutable), we check whether it already exists on the entity. +If it doesn’t, we insert it. If it does, we mutate it. This means that if you insert a component into an entity +that already has it on the server, the client will treat it as a mutation. As a result, triggers may behave +differently on the client and server. If your game logic relies on this semantic, mark your component as +[`Immutable`](bevy::ecs::component::Immutable). For such components, replication will always be applied via insertion. + +This behavior is also configurable via [client markers](#client-markers). + #### Component relations Some components depend on each other. For example, [`ChildOf`] and [`Children`]. You can enable @@ -532,9 +548,6 @@ All events, inserts, removals and despawns will be applied to clients in the sam However, if you insert/mutate a component and immediately remove it, the client will only receive the removal because the component value won't exist in the [`World`] during the replication process. But removal followed by insertion will work as expected since we buffer removals. -Additionally, if you insert a component into an entity that already has it, the client will receive it as a mutation. -This happens because Bevy’s change detection, which we use to track changes, does not distinguish between modifications and re-insertions. -As a result, triggers may behave differently on the client and server. If your game logic relies on this behavior, remove components before re-inserting them. Entity component mutations are grouped by entity, and component groupings may be applied to clients in a different order than on the server. For example, if two entities are spawned in tick 1 on the server and their components are mutated in tick 2, diff --git a/src/shared/replication/command_markers.rs b/src/shared/replication/command_markers.rs index 9e303866..a0c8d795 100644 --- a/src/shared/replication/command_markers.rs +++ b/src/shared/replication/command_markers.rs @@ -1,13 +1,10 @@ use core::cmp::Reverse; -use bevy::{ - ecs::component::{ComponentId, Mutable}, - prelude::*, -}; +use bevy::{ecs::component::ComponentId, prelude::*}; use super::replication_registry::{ ReplicationRegistry, - command_fns::{RemoveFn, WriteFn}, + command_fns::{MutWrite, RemoveFn, WriteFn}, }; /// Marker-based functions for [`App`]. @@ -27,20 +24,18 @@ pub trait AppMarkerExt { /// /// This function registers markers with default [`MarkerConfig`]. /// See also [`Self::register_marker_with`]. - fn register_marker>(&mut self) -> &mut Self; + fn register_marker(&mut self) -> &mut Self; /// Same as [`Self::register_marker`], but also accepts marker configuration. - fn register_marker_with>( - &mut self, - config: MarkerConfig, - ) -> &mut Self; + fn register_marker_with(&mut self, config: MarkerConfig) -> &mut Self; /** Associates command functions with a marker for a component. If this marker is present on an entity and its priority is the highest, then these functions will be called for this component during replication - instead of [`default_write`](super::replication_registry::command_fns::default_write) and + instead of [`default_write`](super::replication_registry::command_fns::default_write) / + [`default_insert_write`](super::replication_registry::command_fns::default_insert_write) and [`default_remove`](super::replication_registry::command_fns::default_remove). See also [`Self::set_command_fns`]. @@ -98,7 +93,7 @@ pub trait AppMarkerExt { } /// Removes component `C` and its history. - fn remove_history>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) { + fn remove_history(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) { ctx.commands.entity(entity.id()).remove::>().remove::(); } @@ -118,7 +113,7 @@ pub trait AppMarkerExt { struct Health(u32); ``` **/ - fn set_marker_fns, C: Component>( + fn set_marker_fns>>( &mut self, write: WriteFn, remove: RemoveFn, @@ -128,10 +123,11 @@ pub trait AppMarkerExt { /// /// If there are no markers present on an entity, then these functions will /// be called for this component during replication instead of - /// [`default_write`](super::replication_registry::command_fns::default_write) and + /// [`default_write`](super::replication_registry::command_fns::default_write) / + /// [`default_insert_write`](super::replication_registry::command_fns::default_insert_write) and /// [`default_remove`](super::replication_registry::command_fns::default_remove). /// See also [`Self::set_marker_fns`]. - fn set_command_fns>( + fn set_command_fns>>( &mut self, write: WriteFn, remove: RemoveFn, @@ -139,14 +135,11 @@ pub trait AppMarkerExt { } impl AppMarkerExt for App { - fn register_marker>(&mut self) -> &mut Self { + fn register_marker(&mut self) -> &mut Self { self.register_marker_with::(MarkerConfig::default()) } - fn register_marker_with>( - &mut self, - config: MarkerConfig, - ) -> &mut Self { + fn register_marker_with(&mut self, config: MarkerConfig) -> &mut Self { let component_id = self.world_mut().register_component::(); let mut command_markers = self.world_mut().resource_mut::(); let marker_id = command_markers.insert(CommandMarker { @@ -160,7 +153,7 @@ impl AppMarkerExt for App { self } - fn set_marker_fns, C: Component>( + fn set_marker_fns>>( &mut self, write: WriteFn, remove: RemoveFn, @@ -176,7 +169,7 @@ impl AppMarkerExt for App { self } - fn set_command_fns>( + fn set_command_fns>>( &mut self, write: WriteFn, remove: RemoveFn, diff --git a/src/shared/replication/replication_registry.rs b/src/shared/replication/replication_registry.rs index 54993e52..d1050391 100644 --- a/src/shared/replication/replication_registry.rs +++ b/src/shared/replication/replication_registry.rs @@ -4,14 +4,11 @@ pub mod ctx; pub mod rule_fns; pub mod test_fns; -use bevy::{ - ecs::component::{ComponentId, Mutable}, - prelude::*, -}; +use bevy::{ecs::component::ComponentId, prelude::*}; use serde::{Deserialize, Serialize}; use super::command_markers::CommandMarkerIndex; -use command_fns::{RemoveFn, UntypedCommandFns, WriteFn}; +use command_fns::{MutWrite, RemoveFn, UntypedCommandFns, WriteFn}; use component_fns::ComponentFns; use ctx::DespawnCtx; use rule_fns::{RuleFns, UntypedRuleFns}; @@ -64,7 +61,7 @@ impl ReplicationRegistry { /// # Panics /// /// Panics if the marker wasn't registered. Use [`Self::register_marker`] first. - pub(super) fn set_marker_fns>( + pub(super) fn set_marker_fns>>( &mut self, world: &mut World, marker_id: CommandMarkerIndex, @@ -84,7 +81,7 @@ impl ReplicationRegistry { /// Sets default functions for a component when there are no markers. /// /// See also [`Self::set_marker_fns`]. - pub(super) fn set_command_fns>( + pub(super) fn set_command_fns>>( &mut self, world: &mut World, write: WriteFn, @@ -104,7 +101,7 @@ impl ReplicationRegistry { /// /// Returned data can be assigned to a /// [`ReplicationRule`](super::replication_rules::ReplicationRule) - pub fn register_rule_fns>( + pub fn register_rule_fns>>( &mut self, world: &mut World, rule_fns: RuleFns, @@ -119,7 +116,7 @@ impl ReplicationRegistry { /// /// If a [`ComponentFns`] has already been created for this component, /// then it returns its index instead of creating a new one. - fn init_component_fns>( + fn init_component_fns>>( &mut self, world: &mut World, ) -> (usize, ComponentId) { diff --git a/src/shared/replication/replication_registry/command_fns.rs b/src/shared/replication/replication_registry/command_fns.rs index 09a35b21..45b16b68 100644 --- a/src/shared/replication/replication_registry/command_fns.rs +++ b/src/shared/replication/replication_registry/command_fns.rs @@ -3,7 +3,10 @@ use core::{ mem, }; -use bevy::{ecs::component::Mutable, prelude::*}; +use bevy::{ + ecs::component::{Immutable, Mutable}, + prelude::*, +}; use bytes::Bytes; use super::{ @@ -24,8 +27,8 @@ pub(super) struct UntypedCommandFns { impl UntypedCommandFns { /// Creates a new instance with default command functions for `C`. - pub(super) fn default_fns>() -> Self { - Self::new(default_write::, default_remove::) + pub(super) fn default_fns>>() -> Self { + Self::new(C::Mutability::default_write_fn(), default_remove::) } /// Creates a new instance by erasing the function pointer for `write`. @@ -69,16 +72,36 @@ impl UntypedCommandFns { } } +/// Defines the default writing function for a [`Component`] based its [`Component::Mutability`]. +pub trait MutWrite { + /// Returns [`default_write`] for [`Mutable`] and [`default_insert_write`] for [`Immutable`]. + fn default_write_fn() -> WriteFn; +} + +impl> MutWrite for Mutable { + fn default_write_fn() -> WriteFn { + default_write:: + } +} + +impl> MutWrite for Immutable { + fn default_write_fn() -> WriteFn { + default_insert_write:: + } +} + /// Signature of component writing function. pub type WriteFn = fn(&mut WriteCtx, &RuleFns, &mut DeferredEntity, &mut Bytes) -> Result<()>; /// Signature of component removal functions. pub type RemoveFn = fn(&mut RemoveCtx, &mut DeferredEntity); -/// Default component writing function. +/// Default component writing function for [`Mutable`] components. /// /// If the component does not exist on the entity, it will be deserialized with [`RuleFns::deserialize`] and inserted via [`Commands`]. /// If the component exists on the entity, [`RuleFns::deserialize_in_place`] will be used directly on the entity's component. +/// +/// See also [`default_insert_write`]. pub fn default_write>( ctx: &mut WriteCtx, rule_fns: &RuleFns, @@ -95,6 +118,22 @@ pub fn default_write>( Ok(()) } +/// Default component writing function for [`Immutable`] components. +/// +/// The component will be deserialized with [`RuleFns::deserialize`] and inserted via [`Commands`]. +/// +/// Similar to [`default_write`], but always performs an insertion regardless of whether the component exists. +pub fn default_insert_write( + ctx: &mut WriteCtx, + rule_fns: &RuleFns, + entity: &mut DeferredEntity, + message: &mut Bytes, +) -> Result<()> { + let component: C = rule_fns.deserialize(ctx, message)?; + ctx.commands.entity(entity.id()).insert(component); + Ok(()) +} + /// Default component removal function. pub fn default_remove(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) { ctx.commands.entity(entity.id()).remove::(); diff --git a/src/shared/replication/replication_registry/component_fns.rs b/src/shared/replication/replication_registry/component_fns.rs index d37751c5..d41bdefc 100644 --- a/src/shared/replication/replication_registry/component_fns.rs +++ b/src/shared/replication/replication_registry/component_fns.rs @@ -1,8 +1,8 @@ -use bevy::{ecs::component::Mutable, prelude::*, ptr::Ptr}; +use bevy::{prelude::*, ptr::Ptr}; use bytes::Bytes; use super::{ - command_fns::UntypedCommandFns, + command_fns::{MutWrite, UntypedCommandFns}, ctx::{RemoveCtx, SerializeCtx, WriteCtx}, rule_fns::UntypedRuleFns, }; @@ -24,7 +24,7 @@ pub(crate) struct ComponentFns { impl ComponentFns { /// Creates a new instance for `C` with the specified number of empty marker function slots. - pub(super) fn new>(marker_slots: usize) -> Self { + pub(super) fn new>>(marker_slots: usize) -> Self { Self { serialize: untyped_serialize::, write: untyped_write::, diff --git a/src/shared/replication/replication_rules.rs b/src/shared/replication/replication_rules.rs index f7e456a8..5e376544 100644 --- a/src/shared/replication/replication_rules.rs +++ b/src/shared/replication/replication_rules.rs @@ -1,17 +1,15 @@ use core::cmp::Reverse; use bevy::{ - ecs::{ - archetype::Archetype, - component::{ComponentId, Mutable}, - entity::MapEntities, - }, + ecs::{archetype::Archetype, component::ComponentId, entity::MapEntities}, platform::collections::HashSet, prelude::*, }; use serde::{Serialize, de::DeserializeOwned}; -use super::replication_registry::{FnsId, ReplicationRegistry, rule_fns::RuleFns}; +use super::replication_registry::{ + FnsId, ReplicationRegistry, command_fns::MutWrite, rule_fns::RuleFns, +}; /// Replication functions for [`App`]. pub trait AppRuleExt { @@ -27,7 +25,7 @@ pub trait AppRuleExt { /// from the quick start guide. fn replicate(&mut self) -> &mut Self where - C: Component + Serialize + DeserializeOwned, + C: Component> + Serialize + DeserializeOwned, { self.replicate_with::(RuleFns::default()) } @@ -35,7 +33,7 @@ pub trait AppRuleExt { #[deprecated(note = "no longer needed, just use `replicate` instead")] fn replicate_mapped(&mut self) -> &mut Self where - C: Component + Serialize + DeserializeOwned + MapEntities, + C: Component> + Serialize + DeserializeOwned + MapEntities, { self.replicate::() } @@ -298,7 +296,7 @@ pub trait AppRuleExt { */ fn replicate_with(&mut self, rule_fns: RuleFns) -> &mut Self where - C: Component; + C: Component>; /** Creates a replication rule for a group of components. @@ -348,7 +346,7 @@ pub trait AppRuleExt { impl AppRuleExt for App { fn replicate_with(&mut self, rule_fns: RuleFns) -> &mut Self where - C: Component, + C: Component>, { let rule = self.world_mut() @@ -513,7 +511,7 @@ pub trait GroupReplication { macro_rules! impl_registrations { ($($type:ident),*) => { - impl<$($type: Component + Serialize + DeserializeOwned),*> GroupReplication for ($($type,)*) { + impl<$($type: Component> + Serialize + DeserializeOwned),*> GroupReplication for ($($type,)*) { fn register(world: &mut World, registry: &mut ReplicationRegistry) -> ReplicationRule { // TODO: initialize with capacity after stabilization: https://github.com/rust-lang/rust/pull/122808 let mut components = Vec::new(); diff --git a/tests/insertion.rs b/tests/insertion.rs index e6e52663..1285d60c 100644 --- a/tests/insertion.rs +++ b/tests/insertion.rs @@ -89,6 +89,56 @@ fn sparse_set_storage() { assert_eq!(components.iter(client_app.world()).count(), 1); } +#[test] +fn immutable() { + 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::(); + } + + server_app.connect_client(&mut client_app); + + let server_entity = server_app.world_mut().spawn(Replicated).id(); + + server_app.update(); + server_app.exchange_with_client(&mut client_app); + client_app.update(); + server_app.exchange_with_client(&mut client_app); + + server_app + .world_mut() + .entity_mut(server_entity) + .insert(ImmutableComponent(false)); + + server_app.update(); + server_app.exchange_with_client(&mut client_app); + client_app.update(); + + let mut components = client_app.world_mut().query::<&ImmutableComponent>(); + let component = components.single(client_app.world()).unwrap(); + assert!(!component.0); + + server_app + .world_mut() + .entity_mut(server_entity) + .insert(ImmutableComponent(true)); + + server_app.update(); + server_app.exchange_with_client(&mut client_app); + client_app.update(); + + let component = components.single(client_app.world()).unwrap(); + assert!(component.0); +} + #[test] fn mapped_existing_entity() { let mut server_app = App::new(); @@ -553,6 +603,10 @@ struct MappedComponent(#[entities] Entity); #[derive(Component, Deserialize, Serialize)] struct DummyComponent; +#[derive(Component, Deserialize, Serialize)] +#[component(immutable)] +struct ImmutableComponent(bool); + #[derive(Component, Deserialize, Serialize)] #[component(storage = "SparseSet")] struct SparseSetComponent;