veloren_server/sys/
subscription.rs

1use super::sentinel::{DeletedEntities, TrackedStorages};
2use crate::{
3    client::Client,
4    presence::{self, RegionSubscription},
5};
6use common::{
7    comp::{Ori, Pos, Presence, Vel},
8    region::{Event as RegionEvent, RegionMap, region_in_vd, regions_in_vd},
9    terrain::{CoordinateConversions, TerrainChunkSize},
10    uid::Uid,
11    vol::RectVolSize,
12};
13use common_ecs::{Job, Origin, Phase, System};
14use common_net::msg::ServerGeneral;
15use specs::{
16    Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, SystemData, World, WorldExt,
17    WriteStorage,
18};
19use tracing::{debug, error};
20use vek::*;
21
22/// This system will update region subscriptions based on client positions
23#[derive(Default)]
24pub struct Sys;
25impl<'a> System<'a> for Sys {
26    type SystemData = (
27        Entities<'a>,
28        ReadExpect<'a, RegionMap>,
29        ReadStorage<'a, Uid>,
30        ReadStorage<'a, Pos>,
31        ReadStorage<'a, Vel>,
32        ReadStorage<'a, Ori>,
33        ReadStorage<'a, Presence>,
34        ReadStorage<'a, Client>,
35        WriteStorage<'a, RegionSubscription>,
36        Read<'a, DeletedEntities>,
37        TrackedStorages<'a>,
38    );
39
40    const NAME: &'static str = "subscription";
41    const ORIGIN: Origin = Origin::Server;
42    const PHASE: Phase = Phase::Create;
43
44    fn run(
45        _job: &mut Job<Self>,
46        (
47            entities,
48            region_map,
49            uids,
50            positions,
51            velocities,
52            orientations,
53            presences,
54            clients,
55            mut subscriptions,
56            deleted_entities,
57            tracked_comps,
58        ): Self::SystemData,
59    ) {
60        // To update subscriptions
61        // 1. Iterate through clients
62        // 2. Calculate current chunk position
63        // 3. If chunk is different (use fuzziness) or the client view distance has
64        //    changed continue, otherwise return
65        // 4. Iterate through subscribed regions
66        // 5. Check if region is still in range (use fuzziness)
67        // 6. If not in range
68        //     - remove from hashset
69        //     - inform client of which entities to remove
70        // 7. Determine list of regions that are in range and iterate through it
71        //    - check if in hashset (hash calc) if not add it
72        let mut regions_to_remove = Vec::new();
73        for (subscription, pos, presence, client_entity, client) in (
74            &mut subscriptions,
75            &positions,
76            &presences,
77            &entities,
78            &clients,
79        )
80            .join()
81        {
82            let vd = presence.entity_view_distance.current();
83            // Calculate current chunk
84            let chunk = (Vec2::<f32>::from(pos.0)).as_::<i32>().wpos_to_cpos();
85            // Only update regions when moving to a new chunk or if view distance has
86            // changed.
87            //
88            // Uses a fuzzy border to prevent rapid triggering when moving along chunk
89            // boundaries.
90            if chunk != subscription.fuzzy_chunk
91                && (subscription
92                    .fuzzy_chunk
93                    .map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
94                        (e as f32 + 0.5) * sz as f32
95                    })
96                    - Vec2::from(pos.0))
97                .map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
98                    e.abs() > (sz / 2 + presence::CHUNK_FUZZ) as f32
99                })
100                .reduce_or()
101                || subscription.last_entity_view_distance != vd
102            {
103                // Update the view distance
104                subscription.last_entity_view_distance = vd;
105                // Update current chunk
106                subscription.fuzzy_chunk = Vec2::<f32>::from(pos.0).as_::<i32>().wpos_to_cpos();
107                // Use the largest side length as our chunk size
108                let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
109                // Iterate through currently subscribed regions
110                for key in &subscription.regions {
111                    // Check if the region is not within range anymore
112                    if !region_in_vd(
113                        *key,
114                        pos.0,
115                        (vd as f32 * chunk_size)
116                            + (presence::CHUNK_FUZZ as f32
117                                + presence::REGION_FUZZ as f32
118                                + chunk_size)
119                                * 2.0f32.sqrt(),
120                    ) {
121                        // Add to the list of regions to remove
122                        regions_to_remove.push(*key);
123                    }
124                }
125
126                // Iterate through regions to remove
127                for key in regions_to_remove.drain(..) {
128                    // Remove region from this client's set of subscribed regions
129                    subscription.regions.remove(&key);
130                    // Tell the client to delete the entities in that region if it exists in the
131                    // RegionMap
132                    if let Some(region) = region_map.get(key) {
133                        // Process entity left events since they won't be processed during entity
134                        // sync because this region is no longer subscribed to
135                        // TODO: consider changing system ordering??
136                        for event in region.events() {
137                            match event {
138                                RegionEvent::Entered(_, _) => {
139                                    // These don't need to be processed because
140                                    // this region is being thrown out anyway
141                                },
142                                RegionEvent::Left(id, maybe_key) => {
143                                    // Lookup UID for entity
144                                    // Doesn't overlap with entity deletion in sync packages
145                                    // because the uid would not be available if the entity was
146                                    // deleted
147                                    if let Some(&uid) = uids.get(entities.entity(*id)) {
148                                        if !maybe_key
149                                            .as_ref()
150                                            // Don't need to check that this isn't also in the
151                                            // regions to remove since the entity will be removed 
152                                            // when we get to that one.
153                                            .map(|key| subscription.regions.contains(key))
154                                            .unwrap_or(false)
155                                        {
156                                            client.send_fallible(ServerGeneral::DeleteEntity(uid));
157                                        }
158                                    }
159                                },
160                            }
161                        }
162                        // Tell client to delete entities in the region
163                        for (&uid, _) in (&uids, region.entities()).join() {
164                            client.send_fallible(ServerGeneral::DeleteEntity(uid));
165                        }
166                    }
167                    // Send deleted entities since they won't be processed for this client
168                    // in entity sync
169                    for uid in deleted_entities.get_deleted_in_region(key).iter() {
170                        client.send_fallible(ServerGeneral::DeleteEntity(*uid));
171                    }
172                }
173
174                for key in regions_in_vd(
175                    pos.0,
176                    (vd as f32 * chunk_size)
177                        + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
178                ) {
179                    // Send client initial info about the entities in this region if it was not
180                    // already within the set of subscribed regions
181                    if subscription.regions.insert(key) {
182                        if let Some(region) = region_map.get(key) {
183                            (
184                                &positions,
185                                velocities.maybe(),
186                                orientations.maybe(),
187                                region.entities(),
188                                &entities,
189                            )
190                                .join()
191                                .filter(|(_, _, _, _, e)| *e != client_entity)
192                                .filter_map(|(pos, vel, ori, _, entity)| {
193                                    tracked_comps.create_entity_package(
194                                        entity,
195                                        Some(*pos),
196                                        vel.copied(),
197                                        ori.copied(),
198                                    )
199                                })
200                                // TODO: batch this into a single message
201                                .for_each(|msg| {
202                                    // Send message to create entity and tracked components and
203                                    // physics components
204                                    client.send_fallible(ServerGeneral::CreateEntity(msg));
205                                })
206                        }
207                    }
208                }
209            }
210        }
211    }
212}
213
214/// Initialize region subscription
215pub fn initialize_region_subscription(world: &World, entity: specs::Entity) {
216    if let (Some(client_pos), Some(presence), Some(client)) = (
217        world.read_storage::<Pos>().get(entity),
218        world.read_storage::<Presence>().get(entity),
219        world.write_storage::<Client>().get(entity),
220    ) {
221        let fuzzy_chunk = (Vec2::<f32>::from(client_pos.0))
222            .as_::<i32>()
223            .wpos_to_cpos();
224        let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
225        let regions = regions_in_vd(
226            client_pos.0,
227            (presence.entity_view_distance.current() as f32 * chunk_size)
228                + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
229        );
230
231        let region_map = world.read_resource::<RegionMap>();
232        let tracked_comps = TrackedStorages::fetch(world);
233        for key in &regions {
234            if let Some(region) = region_map.get(*key) {
235                (
236                    &world.read_storage::<Pos>(), // We assume all these entities have a position
237                    world.read_storage::<Vel>().maybe(),
238                    world.read_storage::<Ori>().maybe(),
239                    region.entities(),
240                    &world.entities(),
241                )
242                .join()
243                // Don't send client its own components because we do that below
244                .filter(|t| t.4 != entity)
245                .filter_map(|(pos, vel, ori, _, entity)|
246                    tracked_comps.create_entity_package(
247                        entity,
248                        Some(*pos),
249                        vel.copied(),
250                        ori.copied(),
251                    )
252                )
253                .for_each(|msg| {
254                    // Send message to create entity and tracked components and physics components
255                    client.send_fallible(ServerGeneral::CreateEntity(msg));
256                });
257            }
258        }
259        // If client position was modified it might not be updated in the region system
260        // so we send its components here
261        if let Some(pkg) = tracked_comps.create_entity_package(
262            entity,
263            Some(*client_pos),
264            world.read_storage().get(entity).copied(),
265            world.read_storage().get(entity).copied(),
266        ) {
267            client.send_fallible(ServerGeneral::CreateEntity(pkg));
268        }
269
270        if let Err(e) = world.write_storage().insert(entity, RegionSubscription {
271            fuzzy_chunk,
272            last_entity_view_distance: presence.entity_view_distance.current(),
273            regions,
274        }) {
275            error!(?e, "Failed to insert region subscription component");
276        }
277    } else {
278        debug!(
279            ?entity,
280            "Failed to initialize region subscription. Couldn't retrieve all the neccesary \
281             components on the provided entity"
282        );
283    }
284}