azalea_client/plugins/packet/relative_updates.rs
1// How entity updates are processed (to avoid issues with shared worlds)
2// - each bot contains a map of { entity id: updates received }
3// - the shared world also contains a canonical "true" updates received for each
4// entity
5// - when a client loads an entity, its "updates received" is set to the same as
6// the global "updates received"
7// - when the shared world sees an entity for the first time, the "updates
8// received" is set to 1.
9// - clients can force the shared "updates received" to 0 to make it so certain
10// entities (i.e. other bots in our swarm) don't get confused and updated by
11// other bots
12// - when a client gets an update to an entity, we check if our "updates
13// received" is the same as the shared world's "updates received": if it is,
14// then process the update and increment the client's and shared world's
15// "updates received" if not, then we simply increment our local "updates
16// received" and do nothing else
17
18use std::sync::Arc;
19
20use azalea_core::entity_id::MinecraftEntityId;
21use azalea_entity::LocalEntity;
22use azalea_world::PartialWorld;
23use bevy_ecs::prelude::*;
24use derive_more::{Deref, DerefMut};
25use parking_lot::RwLock;
26use tracing::{debug, warn};
27
28use crate::packet::as_system;
29
30/// An [`EntityCommand`] that applies a "relative update" to an entity, which
31/// means this update won't be run multiple times by different clients in the
32/// same world.
33///
34/// This is used to avoid a bug where when there's multiple clients in the same
35/// world and an entity sends a relative move packet to all clients, its
36/// position gets desynced since the relative move is applied multiple times.
37///
38/// Don't use this unless you actually got an entity update packet that all
39/// other clients within render distance will get too. You usually don't need
40/// this when the change isn't relative either.
41pub struct RelativeEntityUpdate {
42 pub partial_world: Arc<RwLock<PartialWorld>>,
43 // a function that takes the entity and updates it
44 pub update: Box<dyn FnOnce(&mut EntityWorldMut) + Send + Sync>,
45}
46impl RelativeEntityUpdate {
47 pub fn new(
48 partial_world: Arc<RwLock<PartialWorld>>,
49 update: impl FnOnce(&mut EntityWorldMut) + Send + Sync + 'static,
50 ) -> Self {
51 Self {
52 partial_world,
53 update: Box::new(update),
54 }
55 }
56}
57
58/// A component that counts the number of times this entity has been modified.
59///
60/// This is used for making sure two clients don't do the same relative update
61/// on an entity.
62///
63/// If an entity is local (i.e. it's a client/LocalEntity), this component
64/// should NOT be present in the entity.
65#[derive(Component, Debug, Deref, DerefMut)]
66pub struct UpdatesReceived(u32);
67
68pub type EntityUpdateQuery<'world, 'state, 'a> = Query<
69 'world,
70 'state,
71 (
72 &'a MinecraftEntityId,
73 Option<&'a UpdatesReceived>,
74 Option<&'a LocalEntity>,
75 ),
76>;
77
78/// See [`RelativeEntityUpdate`] for details.
79///
80/// Calling this function will have the same effect as using the Command, but
81/// it's more performant than the Command.
82pub fn should_apply_entity_update(
83 commands: &mut Commands,
84 partial_world: &mut PartialWorld,
85 entity: Entity,
86 entity_update_query: EntityUpdateQuery,
87) -> bool {
88 let partial_entity_infos = &mut partial_world.entity_infos;
89
90 if Some(entity) == partial_entity_infos.owner_entity {
91 // if the entity owns this partial world, it's always allowed to update itself
92 return true;
93 };
94
95 let Ok((minecraft_entity_id, updates_received, local_entity)) = entity_update_query.get(entity)
96 else {
97 // this can happen when the entity despawns in the same Update that we got a
98 // relative update for it
99 debug!("called should_apply_entity_update on an entity with missing components {entity:?}");
100 return false;
101 };
102
103 if local_entity.is_some() {
104 // a client tried to update another client, which isn't allowed
105 return false;
106 }
107
108 let this_client_updates_received = partial_entity_infos
109 .updates_received
110 .get(minecraft_entity_id)
111 .copied();
112
113 let can_update = if let Some(updates_received) = updates_received {
114 this_client_updates_received.unwrap_or(1) == **updates_received
115 } else {
116 // no UpdatesReceived means the entity was just spawned
117 true
118 };
119 if can_update {
120 let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
121 partial_entity_infos
122 .updates_received
123 .insert(*minecraft_entity_id, new_updates_received);
124
125 commands
126 .entity(entity)
127 .insert(UpdatesReceived(new_updates_received));
128
129 return true;
130 }
131 false
132}
133
134impl EntityCommand for RelativeEntityUpdate {
135 fn apply(self, mut entity_mut: EntityWorldMut) {
136 let partial_world = self.partial_world.clone();
137 let mut should_update = false;
138 let entity = entity_mut.id();
139
140 entity_mut.world_scope(|ecs| {
141 as_system::<(Commands, EntityUpdateQuery)>(ecs, |(mut commands, query)| {
142 should_update = should_apply_entity_update(
143 &mut commands,
144 &mut partial_world.write(),
145 entity,
146 query,
147 );
148 });
149 });
150
151 if should_update {
152 (self.update)(&mut entity_mut);
153 }
154 }
155}
156
157/// A system that logs a warning if an entity has both [`UpdatesReceived`]
158/// and [`LocalEntity`].
159pub fn debug_detect_updates_received_on_local_entities(
160 query: Query<Entity, (With<LocalEntity>, With<UpdatesReceived>)>,
161) {
162 for entity in &query {
163 warn!("Entity {entity:?} has both LocalEntity and UpdatesReceived");
164 }
165}