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 ®ions {
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}