Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 16 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ component inside it. However, it's preferred to use required components when pos
it's better to require a [`Handle<T>`] 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
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 15 additions & 22 deletions src/shared/replication/command_markers.rs
Original file line number Diff line number Diff line change
@@ -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`].
Expand All @@ -27,20 +24,18 @@ pub trait AppMarkerExt {
///
/// This function registers markers with default [`MarkerConfig`].
/// See also [`Self::register_marker_with`].
fn register_marker<M: Component<Mutability = Mutable>>(&mut self) -> &mut Self;
fn register_marker<M: Component>(&mut self) -> &mut Self;

/// Same as [`Self::register_marker`], but also accepts marker configuration.
fn register_marker_with<M: Component<Mutability = Mutable>>(
&mut self,
config: MarkerConfig,
) -> &mut Self;
fn register_marker_with<M: Component>(&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`].

Expand Down Expand Up @@ -98,7 +93,7 @@ pub trait AppMarkerExt {
}

/// Removes component `C` and its history.
fn remove_history<C: Component<Mutability = Mutable>>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) {
fn remove_history<C: Component>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) {
ctx.commands.entity(entity.id()).remove::<History<C>>().remove::<C>();
}

Expand All @@ -118,7 +113,7 @@ pub trait AppMarkerExt {
struct Health(u32);
```
**/
fn set_marker_fns<M: Component<Mutability = Mutable>, C: Component<Mutability = Mutable>>(
fn set_marker_fns<M: Component, C: Component<Mutability: MutWrite<C>>>(
&mut self,
write: WriteFn<C>,
remove: RemoveFn,
Expand All @@ -128,25 +123,23 @@ 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<C: Component<Mutability = Mutable>>(
fn set_command_fns<C: Component<Mutability: MutWrite<C>>>(
&mut self,
write: WriteFn<C>,
remove: RemoveFn,
) -> &mut Self;
}

impl AppMarkerExt for App {
fn register_marker<M: Component<Mutability = Mutable>>(&mut self) -> &mut Self {
fn register_marker<M: Component>(&mut self) -> &mut Self {
self.register_marker_with::<M>(MarkerConfig::default())
}

fn register_marker_with<M: Component<Mutability = Mutable>>(
&mut self,
config: MarkerConfig,
) -> &mut Self {
fn register_marker_with<M: Component>(&mut self, config: MarkerConfig) -> &mut Self {
let component_id = self.world_mut().register_component::<M>();
let mut command_markers = self.world_mut().resource_mut::<CommandMarkers>();
let marker_id = command_markers.insert(CommandMarker {
Expand All @@ -160,7 +153,7 @@ impl AppMarkerExt for App {
self
}

fn set_marker_fns<M: Component<Mutability = Mutable>, C: Component<Mutability = Mutable>>(
fn set_marker_fns<M: Component, C: Component<Mutability: MutWrite<C>>>(
&mut self,
write: WriteFn<C>,
remove: RemoveFn,
Expand All @@ -176,7 +169,7 @@ impl AppMarkerExt for App {
self
}

fn set_command_fns<C: Component<Mutability = Mutable>>(
fn set_command_fns<C: Component<Mutability: MutWrite<C>>>(
&mut self,
write: WriteFn<C>,
remove: RemoveFn,
Expand Down
15 changes: 6 additions & 9 deletions src/shared/replication/replication_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<C: Component<Mutability = Mutable>>(
pub(super) fn set_marker_fns<C: Component<Mutability: MutWrite<C>>>(
&mut self,
world: &mut World,
marker_id: CommandMarkerIndex,
Expand All @@ -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<C: Component<Mutability = Mutable>>(
pub(super) fn set_command_fns<C: Component<Mutability: MutWrite<C>>>(
&mut self,
world: &mut World,
write: WriteFn<C>,
Expand All @@ -104,7 +101,7 @@ impl ReplicationRegistry {
///
/// Returned data can be assigned to a
/// [`ReplicationRule`](super::replication_rules::ReplicationRule)
pub fn register_rule_fns<C: Component<Mutability = Mutable>>(
pub fn register_rule_fns<C: Component<Mutability: MutWrite<C>>>(
&mut self,
world: &mut World,
rule_fns: RuleFns<C>,
Expand All @@ -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<C: Component<Mutability = Mutable>>(
fn init_component_fns<C: Component<Mutability: MutWrite<C>>>(
&mut self,
world: &mut World,
) -> (usize, ComponentId) {
Expand Down
47 changes: 43 additions & 4 deletions src/shared/replication/replication_registry/command_fns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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<C: Component<Mutability = Mutable>>() -> Self {
Self::new(default_write::<C>, default_remove::<C>)
pub(super) fn default_fns<C: Component<Mutability: MutWrite<C>>>() -> Self {
Self::new(C::Mutability::default_write_fn(), default_remove::<C>)
}

/// Creates a new instance by erasing the function pointer for `write`.
Expand Down Expand Up @@ -69,16 +72,36 @@ impl UntypedCommandFns {
}
}

/// Defines the default writing function for a [`Component`] based its [`Component::Mutability`].
pub trait MutWrite<C: Component> {
/// Returns [`default_write`] for [`Mutable`] and [`default_insert_write`] for [`Immutable`].
fn default_write_fn() -> WriteFn<C>;
}

impl<C: Component<Mutability = Self>> MutWrite<C> for Mutable {
fn default_write_fn() -> WriteFn<C> {
default_write::<C>
}
}

impl<C: Component<Mutability = Self>> MutWrite<C> for Immutable {
fn default_write_fn() -> WriteFn<C> {
default_insert_write::<C>
}
}

/// Signature of component writing function.
pub type WriteFn<C> = fn(&mut WriteCtx, &RuleFns<C>, &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<C: Component<Mutability = Mutable>>(
ctx: &mut WriteCtx,
rule_fns: &RuleFns<C>,
Expand All @@ -95,6 +118,22 @@ pub fn default_write<C: Component<Mutability = Mutable>>(
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<C: Component>(
ctx: &mut WriteCtx,
rule_fns: &RuleFns<C>,
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<C: Component>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) {
ctx.commands.entity(entity.id()).remove::<C>();
Expand Down
6 changes: 3 additions & 3 deletions src/shared/replication/replication_registry/component_fns.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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<C: Component<Mutability = Mutable>>(marker_slots: usize) -> Self {
pub(super) fn new<C: Component<Mutability: MutWrite<C>>>(marker_slots: usize) -> Self {
Self {
serialize: untyped_serialize::<C>,
write: untyped_write::<C>,
Expand Down
20 changes: 9 additions & 11 deletions src/shared/replication/replication_rules.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -27,15 +25,15 @@ pub trait AppRuleExt {
/// from the quick start guide.
fn replicate<C>(&mut self) -> &mut Self
where
C: Component<Mutability = Mutable> + Serialize + DeserializeOwned,
C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
{
self.replicate_with::<C>(RuleFns::default())
}

#[deprecated(note = "no longer needed, just use `replicate` instead")]
fn replicate_mapped<C>(&mut self) -> &mut Self
where
C: Component<Mutability = Mutable> + Serialize + DeserializeOwned + MapEntities,
C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned + MapEntities,
{
self.replicate::<C>()
}
Expand Down Expand Up @@ -298,7 +296,7 @@ pub trait AppRuleExt {
*/
fn replicate_with<C>(&mut self, rule_fns: RuleFns<C>) -> &mut Self
where
C: Component<Mutability = Mutable>;
C: Component<Mutability: MutWrite<C>>;

/**
Creates a replication rule for a group of components.
Expand Down Expand Up @@ -348,7 +346,7 @@ pub trait AppRuleExt {
impl AppRuleExt for App {
fn replicate_with<C>(&mut self, rule_fns: RuleFns<C>) -> &mut Self
where
C: Component<Mutability = Mutable>,
C: Component<Mutability: MutWrite<C>>,
{
let rule =
self.world_mut()
Expand Down Expand Up @@ -513,7 +511,7 @@ pub trait GroupReplication {

macro_rules! impl_registrations {
($($type:ident),*) => {
impl<$($type: Component<Mutability = Mutable> + Serialize + DeserializeOwned),*> GroupReplication for ($($type,)*) {
impl<$($type: Component<Mutability: MutWrite<$type>> + 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();
Expand Down
Loading