Skip to content

Commit 0c6cce1

Browse files
authored
Ignore mutations for client-despawned entities (#623)
1 parent c286bb8 commit 0c6cce1

11 files changed

Lines changed: 253 additions & 142 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Ignore received replication for entities that was despawned by the client.
13+
1014
## [0.37.0] - 2025-12-03
1115

1216
### Added

src/client.rs

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -460,20 +460,23 @@ fn apply_removals(
460460
message_tick: RepliconTick,
461461
) -> Result<()> {
462462
let server_entity = postcard_utils::entity_from_buf(message)?;
463+
let data_size: usize = postcard_utils::from_buf(message)?;
463464

464-
let mut client_entity = match params.entity_map.server_entry(server_entity) {
465-
EntityEntry::Occupied(entry) => {
466-
DeferredEntity::new(world.get_entity_mut(entry.get())?, params.changes)
467-
}
468-
EntityEntry::Vacant(entry) => {
469-
// It's possible to receive a removal when an entity is spawned and has a component removed in the same tick.
470-
// We could serialize the size of the removals instead of the total number of removals and just advance the cursor,
471-
// but it's a very rare case and not worth optimizing for.
472-
let mut client_entity = DeferredEntity::new(world.spawn_empty(), params.changes);
473-
client_entity.insert(Replicated);
474-
entry.insert(client_entity.id());
475-
client_entity
476-
}
465+
// Server never sends removals for entities that weren't received by the client.
466+
let client_entity = *params
467+
.entity_map
468+
.to_client()
469+
.get(&server_entity)
470+
.ok_or_else(|| format!("received removal for unknown server's `{server_entity}`"))?;
471+
472+
let Ok(mut client_entity) = world
473+
.get_entity_mut(client_entity)
474+
.map(|entity| DeferredEntity::new(entity, params.changes))
475+
else {
476+
// Client could predict despawn.
477+
debug!("ignoring removals for despawned `{client_entity}`");
478+
message.advance(data_size);
479+
return Ok(());
477480
};
478481

479482
params
@@ -482,8 +485,9 @@ fn apply_removals(
482485

483486
confirm_tick(&mut client_entity, params.replicated, message_tick);
484487

485-
let len = apply_array(ArrayKind::Sized, message, |message| {
486-
let fns_id = postcard_utils::from_buf(message)?;
488+
let mut data = message.split_to(data_size);
489+
let len = apply_array(ArrayKind::Dynamic, &mut data, |data| {
490+
let fns_id = postcard_utils::from_buf(data)?;
487491
let (_, component_id, fns) = params.registry.get(fns_id);
488492
let mut ctx = RemoveCtx {
489493
message_tick,
@@ -516,6 +520,7 @@ fn apply_changes(
516520
message_tick: RepliconTick,
517521
) -> Result<()> {
518522
let server_entity = postcard_utils::entity_from_buf(message)?;
523+
let data_size: usize = postcard_utils::from_buf(message)?;
519524

520525
let world_cell = world.as_unsafe_world_cell();
521526
let entities = world_cell.entities();
@@ -525,7 +530,14 @@ fn apply_changes(
525530

526531
let mut client_entity = match params.entity_map.server_entry(server_entity) {
527532
EntityEntry::Occupied(entry) => {
528-
DeferredEntity::new(world.get_entity_mut(entry.get())?, params.changes)
533+
let Ok(client_entity) = world.get_entity_mut(entry.get()) else {
534+
// Client could predict despawn.
535+
debug!("ignoring changes for despawned `{}`", entry.get());
536+
message.advance(data_size);
537+
return Ok(());
538+
};
539+
540+
DeferredEntity::new(client_entity, params.changes)
529541
}
530542
EntityEntry::Vacant(entry) => {
531543
let mut client_entity = DeferredEntity::new(world.spawn_empty(), params.changes);
@@ -541,8 +553,9 @@ fn apply_changes(
541553

542554
confirm_tick(&mut client_entity, params.replicated, message_tick);
543555

544-
let len = apply_array(ArrayKind::Sized, message, |message| {
545-
let fns_id = postcard_utils::from_buf(message)?;
556+
let mut data = message.split_to(data_size);
557+
let len = apply_array(ArrayKind::Dynamic, &mut data, |data| {
558+
let fns_id = postcard_utils::from_buf(data)?;
546559
let (_, component_id, fns) = params.registry.get(fns_id);
547560
let mut ctx = WriteCtx {
548561
entity_map: params.entity_map,
@@ -557,7 +570,7 @@ fn apply_changes(
557570
client_entity.id(),
558571
);
559572

560-
fns.write(&mut ctx, params.entity_markers, &mut client_entity, message)?;
573+
fns.write(&mut ctx, params.entity_markers, &mut client_entity, data)?;
561574

562575
Ok(())
563576
})?;
@@ -645,8 +658,16 @@ fn apply_mutations(
645658
// The latter won't apply any structural changes until `flush`, and `Entities` won't be used afterward.
646659
let world = unsafe { world_cell.world_mut() };
647660

648-
let mut client_entity =
649-
DeferredEntity::new(world.get_entity_mut(client_entity)?, params.changes);
661+
let Ok(mut client_entity) = world
662+
.get_entity_mut(client_entity)
663+
.map(|entity| DeferredEntity::new(entity, params.changes))
664+
else {
665+
// Client could predict despawn.
666+
debug!("ignoring mutations for despawned `{client_entity}`");
667+
message.advance(data_size);
668+
return Ok(());
669+
};
670+
650671
params
651672
.entity_markers
652673
.read(params.command_markers, &*client_entity);

src/server/replication_messages.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod change_ranges;
1+
mod entity_ranges;
22
pub(super) mod mutations;
33
pub(super) mod serialized_data;
44
pub(super) mod updates;

src/server/replication_messages/change_ranges.rs

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use alloc::vec::Vec;
2+
use core::ops::Range;
3+
4+
use bevy::prelude::*;
5+
use postcard::experimental::serialized_size;
6+
7+
/// Component insertions, mutations or removals for an entity in form of serialized ranges
8+
/// from [`SerializedData`](super::serialized_data::SerializedData).
9+
///
10+
/// Used inside [`Updates`](super::updates::Updates) and
11+
/// [`Mutations`](super::mutations::Mutations).
12+
///
13+
/// For data, we serialize the size in bytes rather than the number of elements to
14+
/// allow entities to be skipped during deserialization. For example, received mutations
15+
/// might be outdated, or the entity might have been despawned via client-side prediction.
16+
pub(super) struct EntityRanges {
17+
pub(super) entity: Range<usize>,
18+
pub(super) data: Vec<Range<usize>>,
19+
}
20+
21+
impl EntityRanges {
22+
/// Returns serialized size.
23+
pub(super) fn size(&self) -> Result<usize> {
24+
let data_size = self.data_size();
25+
let len_size = serialized_size(&data_size)?;
26+
Ok(self.entity.len() + len_size + data_size)
27+
}
28+
29+
pub(super) fn data_size(&self) -> usize {
30+
self.data.iter().map(|range| range.len()).sum()
31+
}
32+
33+
pub(super) fn add_data(&mut self, data: Range<usize>) {
34+
if let Some(last) = self.data.last_mut() {
35+
// Append to previous range if possible.
36+
if last.end == data.start {
37+
last.end = data.end;
38+
return;
39+
}
40+
}
41+
42+
self.data.push(data);
43+
}
44+
45+
pub(super) fn extend(&mut self, other: &Self) {
46+
self.data.extend(other.data.iter().cloned());
47+
}
48+
}

src/server/replication_messages/mutations.rs

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bevy::{ecs::component::Tick, prelude::*};
44
use log::trace;
55
use postcard::experimental::{max_size::MaxSize, serialized_size};
66

7-
use super::{change_ranges::ChangeRanges, serialized_data::SerializedData};
7+
use super::{entity_ranges::EntityRanges, serialized_data::SerializedData};
88
use crate::{
99
postcard_utils,
1010
prelude::*,
@@ -67,10 +67,9 @@ impl Mutations {
6767
) {
6868
let mutations = EntityMutations {
6969
entity,
70-
ranges: ChangeRanges {
70+
ranges: EntityRanges {
7171
entity: entity_range,
72-
components_len: 0,
73-
components: pools.take_ranges(),
72+
data: pools.take_ranges(),
7473
},
7574
components: pools.take_components(),
7675
};
@@ -97,7 +96,7 @@ impl Mutations {
9796
})
9897
.expect("entity should be written before adding components");
9998

100-
mutations.ranges.add_component(component);
99+
mutations.ranges.add_data(component);
101100
}
102101

103102
/// Removes last added entity from [`Self::add_entity`] and returns it.
@@ -164,7 +163,7 @@ impl Mutations {
164163
for chunk in chunks.iter_mut() {
165164
let mut mutations_size = 0;
166165
for mutations in &mut *chunk {
167-
mutations_size += mutations.ranges.size_with_components_size()?;
166+
mutations_size += mutations.ranges.size()?;
168167
}
169168

170169
// Try to pack back first, then try to pack forward.
@@ -233,8 +232,8 @@ impl Mutations {
233232
postcard_utils::to_extend_mut(&split.mutate_index, &mut message)?;
234233
for mutations in chunks.iter_flatten(split.chunks_range.clone()) {
235234
message.extend_from_slice(&serialized[mutations.ranges.entity.clone()]);
236-
postcard_utils::to_extend_mut(&mutations.ranges.components_size(), &mut message)?;
237-
for component in &mutations.ranges.components {
235+
postcard_utils::to_extend_mut(&mutations.ranges.data_size(), &mut message)?;
236+
for component in &mutations.ranges.data {
238237
message.extend_from_slice(&serialized[component.clone()]);
239238
}
240239
}
@@ -262,7 +261,7 @@ impl Mutations {
262261
.iter_mut()
263262
.chain(iter::once(&mut self.standalone))
264263
{
265-
pools.recycle_ranges(entities.drain(..).map(|m| m.ranges.components));
264+
pools.recycle_ranges(entities.drain(..).map(|m| m.ranges.data));
266265
// We don't take component masks because they are moved to `MutateInfo` during sending.
267266
}
268267
}
@@ -294,12 +293,7 @@ pub(crate) struct EntityMutations {
294293
/// Serialized as a list of pairs of entity chunk and multiple chunks with mutated components.
295294
/// Components are stored in multiple chunks because some clients may acknowledge mutations,
296295
/// while others may not.
297-
///
298-
/// Unlike with [`Updates`](super::updates::Updates), we serialize the number
299-
/// of chunk bytes instead of the number of components. This is because, during deserialization,
300-
/// some entities may be skipped if they have already been updated (as mutations are sent until
301-
/// the client acknowledges them).
302-
pub(super) ranges: ChangeRanges,
296+
pub(super) ranges: EntityRanges,
303297

304298
/// Components written in [`Self::ranges`].
305299
///

0 commit comments

Comments
 (0)