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