Skip to main content

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}