Skip to content

Commit ecc8466

Browse files
ShaturUkoeHB
andauthored
Immutable components (#468)
* Allow markers to be immutable * Add support for immutable components Immutable components were introduced in Bevy via: bevyengine/bevy#16372 These components can only be changed via re-insertion. This allows users to enforce invariants through triggers and hooks. When porting to 0.16, I initially added a `Mutability = Mutable` constraint. To support immutable components, I replaced this constraint with the `MutWrite` trait. It is implemented for both `Mutable` and `Immutable` markers to select the appropriate write function. For `Immutable` components, this function always performs insertion. * Update src/shared/replication/replication_registry/command_fns.rs Co-authored-by: UkoeHB <[email protected]> * Add reinsertion test * Apply clippy suggestions --------- Co-authored-by: UkoeHB <[email protected]>
1 parent 0a7e2a1 commit ecc8466

File tree

8 files changed

+147
-52
lines changed

8 files changed

+147
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Support for `no_std`.
13+
- Seamless support for immutable components. For these components, replication is always applied via insertion.
1314

1415
### Changed
1516

src/lib.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,22 @@ component inside it. However, it's preferred to use required components when pos
188188
it's better to require a [`Handle<T>`] with a default value that doesn't point to any asset
189189
and initialize it later in a hook or observer. This way you avoid archetype moves in ECS.
190190
191+
#### Mutability
192+
193+
There are two ways to change a component value on an entity: re-inserting it or mutating it.
194+
195+
We use Bevy’s change detection to track and send changes. However, it does not distinguish between modifications
196+
and re-insertions. This is why we simply send the list of changes and decide how to apply them on the client.
197+
By default, this behavior is based on [`Component::Mutability`].
198+
199+
When a component is [`Mutable`](bevy::ecs::component::Mutable), we check whether it already exists on the entity.
200+
If it doesn’t, we insert it. If it does, we mutate it. This means that if you insert a component into an entity
201+
that already has it on the server, the client will treat it as a mutation. As a result, triggers may behave
202+
differently on the client and server. If your game logic relies on this semantic, mark your component as
203+
[`Immutable`](bevy::ecs::component::Immutable). For such components, replication will always be applied via insertion.
204+
205+
This behavior is also configurable via [client markers](#client-markers).
206+
191207
#### Component relations
192208
193209
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
532548
533549
However, if you insert/mutate a component and immediately remove it, the client will only receive the removal because the component value
534550
won't exist in the [`World`] during the replication process. But removal followed by insertion will work as expected since we buffer removals.
535-
Additionally, if you insert a component into an entity that already has it, the client will receive it as a mutation.
536-
This happens because Bevy’s change detection, which we use to track changes, does not distinguish between modifications and re-insertions.
537-
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.
538551
539552
Entity component mutations are grouped by entity, and component groupings may be applied to clients in a different order than on the server.
540553
For example, if two entities are spawned in tick 1 on the server and their components are mutated in tick 2,

src/shared/replication/command_markers.rs

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
use core::cmp::Reverse;
22

3-
use bevy::{
4-
ecs::component::{ComponentId, Mutable},
5-
prelude::*,
6-
};
3+
use bevy::{ecs::component::ComponentId, prelude::*};
74

85
use super::replication_registry::{
96
ReplicationRegistry,
10-
command_fns::{RemoveFn, WriteFn},
7+
command_fns::{MutWrite, RemoveFn, WriteFn},
118
};
129

1310
/// Marker-based functions for [`App`].
@@ -27,20 +24,18 @@ pub trait AppMarkerExt {
2724
///
2825
/// This function registers markers with default [`MarkerConfig`].
2926
/// See also [`Self::register_marker_with`].
30-
fn register_marker<M: Component<Mutability = Mutable>>(&mut self) -> &mut Self;
27+
fn register_marker<M: Component>(&mut self) -> &mut Self;
3128

3229
/// Same as [`Self::register_marker`], but also accepts marker configuration.
33-
fn register_marker_with<M: Component<Mutability = Mutable>>(
34-
&mut self,
35-
config: MarkerConfig,
36-
) -> &mut Self;
30+
fn register_marker_with<M: Component>(&mut self, config: MarkerConfig) -> &mut Self;
3731

3832
/**
3933
Associates command functions with a marker for a component.
4034
4135
If this marker is present on an entity and its priority is the highest,
4236
then these functions will be called for this component during replication
43-
instead of [`default_write`](super::replication_registry::command_fns::default_write) and
37+
instead of [`default_write`](super::replication_registry::command_fns::default_write) /
38+
[`default_insert_write`](super::replication_registry::command_fns::default_insert_write) and
4439
[`default_remove`](super::replication_registry::command_fns::default_remove).
4540
See also [`Self::set_command_fns`].
4641
@@ -98,7 +93,7 @@ pub trait AppMarkerExt {
9893
}
9994
10095
/// Removes component `C` and its history.
101-
fn remove_history<C: Component<Mutability = Mutable>>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) {
96+
fn remove_history<C: Component>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) {
10297
ctx.commands.entity(entity.id()).remove::<History<C>>().remove::<C>();
10398
}
10499
@@ -118,7 +113,7 @@ pub trait AppMarkerExt {
118113
struct Health(u32);
119114
```
120115
**/
121-
fn set_marker_fns<M: Component<Mutability = Mutable>, C: Component<Mutability = Mutable>>(
116+
fn set_marker_fns<M: Component, C: Component<Mutability: MutWrite<C>>>(
122117
&mut self,
123118
write: WriteFn<C>,
124119
remove: RemoveFn,
@@ -128,25 +123,23 @@ pub trait AppMarkerExt {
128123
///
129124
/// If there are no markers present on an entity, then these functions will
130125
/// be called for this component during replication instead of
131-
/// [`default_write`](super::replication_registry::command_fns::default_write) and
126+
/// [`default_write`](super::replication_registry::command_fns::default_write) /
127+
/// [`default_insert_write`](super::replication_registry::command_fns::default_insert_write) and
132128
/// [`default_remove`](super::replication_registry::command_fns::default_remove).
133129
/// See also [`Self::set_marker_fns`].
134-
fn set_command_fns<C: Component<Mutability = Mutable>>(
130+
fn set_command_fns<C: Component<Mutability: MutWrite<C>>>(
135131
&mut self,
136132
write: WriteFn<C>,
137133
remove: RemoveFn,
138134
) -> &mut Self;
139135
}
140136

141137
impl AppMarkerExt for App {
142-
fn register_marker<M: Component<Mutability = Mutable>>(&mut self) -> &mut Self {
138+
fn register_marker<M: Component>(&mut self) -> &mut Self {
143139
self.register_marker_with::<M>(MarkerConfig::default())
144140
}
145141

146-
fn register_marker_with<M: Component<Mutability = Mutable>>(
147-
&mut self,
148-
config: MarkerConfig,
149-
) -> &mut Self {
142+
fn register_marker_with<M: Component>(&mut self, config: MarkerConfig) -> &mut Self {
150143
let component_id = self.world_mut().register_component::<M>();
151144
let mut command_markers = self.world_mut().resource_mut::<CommandMarkers>();
152145
let marker_id = command_markers.insert(CommandMarker {
@@ -160,7 +153,7 @@ impl AppMarkerExt for App {
160153
self
161154
}
162155

163-
fn set_marker_fns<M: Component<Mutability = Mutable>, C: Component<Mutability = Mutable>>(
156+
fn set_marker_fns<M: Component, C: Component<Mutability: MutWrite<C>>>(
164157
&mut self,
165158
write: WriteFn<C>,
166159
remove: RemoveFn,
@@ -176,7 +169,7 @@ impl AppMarkerExt for App {
176169
self
177170
}
178171

179-
fn set_command_fns<C: Component<Mutability = Mutable>>(
172+
fn set_command_fns<C: Component<Mutability: MutWrite<C>>>(
180173
&mut self,
181174
write: WriteFn<C>,
182175
remove: RemoveFn,

src/shared/replication/replication_registry.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@ pub mod ctx;
44
pub mod rule_fns;
55
pub mod test_fns;
66

7-
use bevy::{
8-
ecs::component::{ComponentId, Mutable},
9-
prelude::*,
10-
};
7+
use bevy::{ecs::component::ComponentId, prelude::*};
118
use serde::{Deserialize, Serialize};
129

1310
use super::command_markers::CommandMarkerIndex;
14-
use command_fns::{RemoveFn, UntypedCommandFns, WriteFn};
11+
use command_fns::{MutWrite, RemoveFn, UntypedCommandFns, WriteFn};
1512
use component_fns::ComponentFns;
1613
use ctx::DespawnCtx;
1714
use rule_fns::{RuleFns, UntypedRuleFns};
@@ -64,7 +61,7 @@ impl ReplicationRegistry {
6461
/// # Panics
6562
///
6663
/// Panics if the marker wasn't registered. Use [`Self::register_marker`] first.
67-
pub(super) fn set_marker_fns<C: Component<Mutability = Mutable>>(
64+
pub(super) fn set_marker_fns<C: Component<Mutability: MutWrite<C>>>(
6865
&mut self,
6966
world: &mut World,
7067
marker_id: CommandMarkerIndex,
@@ -84,7 +81,7 @@ impl ReplicationRegistry {
8481
/// Sets default functions for a component when there are no markers.
8582
///
8683
/// See also [`Self::set_marker_fns`].
87-
pub(super) fn set_command_fns<C: Component<Mutability = Mutable>>(
84+
pub(super) fn set_command_fns<C: Component<Mutability: MutWrite<C>>>(
8885
&mut self,
8986
world: &mut World,
9087
write: WriteFn<C>,
@@ -104,7 +101,7 @@ impl ReplicationRegistry {
104101
///
105102
/// Returned data can be assigned to a
106103
/// [`ReplicationRule`](super::replication_rules::ReplicationRule)
107-
pub fn register_rule_fns<C: Component<Mutability = Mutable>>(
104+
pub fn register_rule_fns<C: Component<Mutability: MutWrite<C>>>(
108105
&mut self,
109106
world: &mut World,
110107
rule_fns: RuleFns<C>,
@@ -119,7 +116,7 @@ impl ReplicationRegistry {
119116
///
120117
/// If a [`ComponentFns`] has already been created for this component,
121118
/// then it returns its index instead of creating a new one.
122-
fn init_component_fns<C: Component<Mutability = Mutable>>(
119+
fn init_component_fns<C: Component<Mutability: MutWrite<C>>>(
123120
&mut self,
124121
world: &mut World,
125122
) -> (usize, ComponentId) {

src/shared/replication/replication_registry/command_fns.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use core::{
33
mem,
44
};
55

6-
use bevy::{ecs::component::Mutable, prelude::*};
6+
use bevy::{
7+
ecs::component::{Immutable, Mutable},
8+
prelude::*,
9+
};
710
use bytes::Bytes;
811

912
use super::{
@@ -24,8 +27,8 @@ pub(super) struct UntypedCommandFns {
2427

2528
impl UntypedCommandFns {
2629
/// Creates a new instance with default command functions for `C`.
27-
pub(super) fn default_fns<C: Component<Mutability = Mutable>>() -> Self {
28-
Self::new(default_write::<C>, default_remove::<C>)
30+
pub(super) fn default_fns<C: Component<Mutability: MutWrite<C>>>() -> Self {
31+
Self::new(C::Mutability::default_write_fn(), default_remove::<C>)
2932
}
3033

3134
/// Creates a new instance by erasing the function pointer for `write`.
@@ -69,16 +72,36 @@ impl UntypedCommandFns {
6972
}
7073
}
7174

75+
/// Defines the default writing function for a [`Component`] based its [`Component::Mutability`].
76+
pub trait MutWrite<C: Component> {
77+
/// Returns [`default_write`] for [`Mutable`] and [`default_insert_write`] for [`Immutable`].
78+
fn default_write_fn() -> WriteFn<C>;
79+
}
80+
81+
impl<C: Component<Mutability = Self>> MutWrite<C> for Mutable {
82+
fn default_write_fn() -> WriteFn<C> {
83+
default_write::<C>
84+
}
85+
}
86+
87+
impl<C: Component<Mutability = Self>> MutWrite<C> for Immutable {
88+
fn default_write_fn() -> WriteFn<C> {
89+
default_insert_write::<C>
90+
}
91+
}
92+
7293
/// Signature of component writing function.
7394
pub type WriteFn<C> = fn(&mut WriteCtx, &RuleFns<C>, &mut DeferredEntity, &mut Bytes) -> Result<()>;
7495

7596
/// Signature of component removal functions.
7697
pub type RemoveFn = fn(&mut RemoveCtx, &mut DeferredEntity);
7798

78-
/// Default component writing function.
99+
/// Default component writing function for [`Mutable`] components.
79100
///
80101
/// If the component does not exist on the entity, it will be deserialized with [`RuleFns::deserialize`] and inserted via [`Commands`].
81102
/// If the component exists on the entity, [`RuleFns::deserialize_in_place`] will be used directly on the entity's component.
103+
///
104+
/// See also [`default_insert_write`].
82105
pub fn default_write<C: Component<Mutability = Mutable>>(
83106
ctx: &mut WriteCtx,
84107
rule_fns: &RuleFns<C>,
@@ -95,6 +118,22 @@ pub fn default_write<C: Component<Mutability = Mutable>>(
95118
Ok(())
96119
}
97120

121+
/// Default component writing function for [`Immutable`] components.
122+
///
123+
/// The component will be deserialized with [`RuleFns::deserialize`] and inserted via [`Commands`].
124+
///
125+
/// Similar to [`default_write`], but always performs an insertion regardless of whether the component exists.
126+
pub fn default_insert_write<C: Component>(
127+
ctx: &mut WriteCtx,
128+
rule_fns: &RuleFns<C>,
129+
entity: &mut DeferredEntity,
130+
message: &mut Bytes,
131+
) -> Result<()> {
132+
let component: C = rule_fns.deserialize(ctx, message)?;
133+
ctx.commands.entity(entity.id()).insert(component);
134+
Ok(())
135+
}
136+
98137
/// Default component removal function.
99138
pub fn default_remove<C: Component>(ctx: &mut RemoveCtx, entity: &mut DeferredEntity) {
100139
ctx.commands.entity(entity.id()).remove::<C>();

src/shared/replication/replication_registry/component_fns.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use bevy::{ecs::component::Mutable, prelude::*, ptr::Ptr};
1+
use bevy::{prelude::*, ptr::Ptr};
22
use bytes::Bytes;
33

44
use super::{
5-
command_fns::UntypedCommandFns,
5+
command_fns::{MutWrite, UntypedCommandFns},
66
ctx::{RemoveCtx, SerializeCtx, WriteCtx},
77
rule_fns::UntypedRuleFns,
88
};
@@ -24,7 +24,7 @@ pub(crate) struct ComponentFns {
2424

2525
impl ComponentFns {
2626
/// Creates a new instance for `C` with the specified number of empty marker function slots.
27-
pub(super) fn new<C: Component<Mutability = Mutable>>(marker_slots: usize) -> Self {
27+
pub(super) fn new<C: Component<Mutability: MutWrite<C>>>(marker_slots: usize) -> Self {
2828
Self {
2929
serialize: untyped_serialize::<C>,
3030
write: untyped_write::<C>,

src/shared/replication/replication_rules.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
use core::cmp::Reverse;
22

33
use bevy::{
4-
ecs::{
5-
archetype::Archetype,
6-
component::{ComponentId, Mutable},
7-
entity::MapEntities,
8-
},
4+
ecs::{archetype::Archetype, component::ComponentId, entity::MapEntities},
95
platform::collections::HashSet,
106
prelude::*,
117
};
128
use serde::{Serialize, de::DeserializeOwned};
139

14-
use super::replication_registry::{FnsId, ReplicationRegistry, rule_fns::RuleFns};
10+
use super::replication_registry::{
11+
FnsId, ReplicationRegistry, command_fns::MutWrite, rule_fns::RuleFns,
12+
};
1513

1614
/// Replication functions for [`App`].
1715
pub trait AppRuleExt {
@@ -27,15 +25,15 @@ pub trait AppRuleExt {
2725
/// from the quick start guide.
2826
fn replicate<C>(&mut self) -> &mut Self
2927
where
30-
C: Component<Mutability = Mutable> + Serialize + DeserializeOwned,
28+
C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned,
3129
{
3230
self.replicate_with::<C>(RuleFns::default())
3331
}
3432

3533
#[deprecated(note = "no longer needed, just use `replicate` instead")]
3634
fn replicate_mapped<C>(&mut self) -> &mut Self
3735
where
38-
C: Component<Mutability = Mutable> + Serialize + DeserializeOwned + MapEntities,
36+
C: Component<Mutability: MutWrite<C>> + Serialize + DeserializeOwned + MapEntities,
3937
{
4038
self.replicate::<C>()
4139
}
@@ -298,7 +296,7 @@ pub trait AppRuleExt {
298296
*/
299297
fn replicate_with<C>(&mut self, rule_fns: RuleFns<C>) -> &mut Self
300298
where
301-
C: Component<Mutability = Mutable>;
299+
C: Component<Mutability: MutWrite<C>>;
302300

303301
/**
304302
Creates a replication rule for a group of components.
@@ -348,7 +346,7 @@ pub trait AppRuleExt {
348346
impl AppRuleExt for App {
349347
fn replicate_with<C>(&mut self, rule_fns: RuleFns<C>) -> &mut Self
350348
where
351-
C: Component<Mutability = Mutable>,
349+
C: Component<Mutability: MutWrite<C>>,
352350
{
353351
let rule =
354352
self.world_mut()
@@ -513,7 +511,7 @@ pub trait GroupReplication {
513511

514512
macro_rules! impl_registrations {
515513
($($type:ident),*) => {
516-
impl<$($type: Component<Mutability = Mutable> + Serialize + DeserializeOwned),*> GroupReplication for ($($type,)*) {
514+
impl<$($type: Component<Mutability: MutWrite<$type>> + Serialize + DeserializeOwned),*> GroupReplication for ($($type,)*) {
517515
fn register(world: &mut World, registry: &mut ReplicationRegistry) -> ReplicationRule {
518516
// TODO: initialize with capacity after stabilization: https://github.com/rust-lang/rust/pull/122808
519517
let mut components = Vec::new();

0 commit comments

Comments
 (0)