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::{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 && !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 // Tell client to delete entities in the region
162 for (&uid, _) in (&uids, region.entities()).join() {
163 client.send_fallible(ServerGeneral::DeleteEntity(uid));
164 }
165 }
166 // Send deleted entities since they won't be processed for this client
167 // in entity sync
168 for uid in deleted_entities.get_deleted_in_region(key).iter() {
169 client.send_fallible(ServerGeneral::DeleteEntity(*uid));
170 }
171 }
172
173 for key in regions_in_vd(
174 pos.0,
175 (vd as f32 * chunk_size)
176 + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
177 ) {
178 // Send client initial info about the entities in this region if it was not
179 // already within the set of subscribed regions
180 if subscription.regions.insert(key)
181 && let Some(region) = region_map.get(key)
182 {
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/// Initialize region subscription
214pub fn initialize_region_subscription(world: &World, entity: specs::Entity) {
215 if let (Some(client_pos), Some(presence), Some(client)) = (
216 world.read_storage::<Pos>().get(entity),
217 world.read_storage::<Presence>().get(entity),
218 world.write_storage::<Client>().get(entity),
219 ) {
220 let fuzzy_chunk = (Vec2::<f32>::from(client_pos.0))
221 .as_::<i32>()
222 .wpos_to_cpos();
223 let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
224 let regions = regions_in_vd(
225 client_pos.0,
226 (presence.entity_view_distance.current() as f32 * chunk_size)
227 + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
228 );
229
230 let region_map = world.read_resource::<RegionMap>();
231 let tracked_comps = TrackedStorages::fetch(world);
232 for key in ®ions {
233 if let Some(region) = region_map.get(*key) {
234 (
235 &world.read_storage::<Pos>(), // We assume all these entities have a position
236 world.read_storage::<Vel>().maybe(),
237 world.read_storage::<Ori>().maybe(),
238 region.entities(),
239 &world.entities(),
240 )
241 .join()
242 // Don't send client its own components because we do that below
243 .filter(|t| t.4 != entity)
244 .filter_map(|(pos, vel, ori, _, entity)|
245 tracked_comps.create_entity_package(
246 entity,
247 Some(*pos),
248 vel.copied(),
249 ori.copied(),
250 )
251 )
252 .for_each(|msg| {
253 // Send message to create entity and tracked components and physics components
254 client.send_fallible(ServerGeneral::CreateEntity(msg));
255 });
256 }
257 }
258 // If client position was modified it might not be updated in the region system
259 // so we send its components here
260 if let Some(pkg) = tracked_comps.create_entity_package(
261 entity,
262 Some(*client_pos),
263 world.read_storage().get(entity).copied(),
264 world.read_storage().get(entity).copied(),
265 ) {
266 client.send_fallible(ServerGeneral::CreateEntity(pkg));
267 }
268
269 if let Err(e) = world.write_storage().insert(entity, RegionSubscription {
270 fuzzy_chunk,
271 last_entity_view_distance: presence.entity_view_distance.current(),
272 regions,
273 }) {
274 error!(?e, "Failed to insert region subscription component");
275 }
276 } else {
277 debug!(
278 ?entity,
279 "Failed to initialize region subscription. Couldn't retrieve all the neccesary \
280 components on the provided entity"
281 );
282 }
283}