veloren_server/sys/msg/
terrain.rs

1use crate::{
2    ChunkRequest, chunk_serialize::ChunkSendEntry, client::Client, lod::Lod,
3    metrics::NetworkRequestMetrics,
4};
5use common::{
6    comp::{Pos, Presence},
7    event::{ClientDisconnectEvent, EventBus},
8    spiral::Spiral2d,
9    terrain::{CoordinateConversions, TerrainChunkSize, TerrainGrid},
10    vol::RectVolSize,
11};
12use common_ecs::{Job, Origin, ParMode, Phase, System};
13use common_net::msg::{ClientGeneral, ServerGeneral};
14use rayon::prelude::*;
15use specs::{Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, Write, WriteStorage};
16use tracing::{debug, trace};
17
18/// This system will handle new messages from clients
19#[derive(Default)]
20pub struct Sys;
21impl<'a> System<'a> for Sys {
22    type SystemData = (
23        Entities<'a>,
24        Read<'a, EventBus<ClientDisconnectEvent>>,
25        Read<'a, EventBus<ChunkSendEntry>>,
26        ReadExpect<'a, TerrainGrid>,
27        ReadExpect<'a, Lod>,
28        ReadExpect<'a, NetworkRequestMetrics>,
29        Write<'a, Vec<ChunkRequest>>,
30        ReadStorage<'a, Pos>,
31        ReadStorage<'a, Presence>,
32        WriteStorage<'a, Client>,
33    );
34
35    const NAME: &'static str = "msg::terrain";
36    const ORIGIN: Origin = Origin::Server;
37    const PHASE: Phase = Phase::Create;
38
39    fn run(
40        job: &mut Job<Self>,
41        (
42            entities,
43            client_disconnect_events,
44            chunk_send_bus,
45            terrain,
46            lod,
47            network_metrics,
48            mut chunk_requests,
49            positions,
50            presences,
51            mut clients,
52        ): Self::SystemData,
53    ) {
54        job.cpu_stats.measure(ParMode::Rayon);
55        let mut new_chunk_requests = (&entities, &mut clients, (&presences).maybe())
56            .join()
57            // NOTE: Required because Specs has very poor work splitting for sparse joins.
58            .par_bridge()
59            .map_init(
60                || (chunk_send_bus.emitter(), client_disconnect_events.emitter()),
61                |(chunk_send_emitter, client_disconnect_emitter), (entity, client, maybe_presence)| {
62                    let mut chunk_requests = Vec::new();
63                    let _ = super::try_recv_all(client, 5, |client, msg| {
64                        // SPECIAL CASE: LOD zone requests can be sent by non-present players
65                        if let ClientGeneral::LodZoneRequest { key } = &msg {
66                            client.send(ServerGeneral::LodZoneUpdate {
67                                key: *key,
68                                zone: lod.zone(*key).clone(),
69                            })?;
70                        } else {
71                            let presence = match maybe_presence {
72                                Some(g) => g,
73                                None => {
74                                    debug!(?entity, "client is not in_game, ignoring msg");
75                                    trace!(?msg, "ignored msg content");
76                                    if matches!(msg, ClientGeneral::TerrainChunkRequest { .. }) {
77                                        network_metrics.chunks_request_dropped.inc();
78                                    }
79                                    return Ok(());
80                                },
81                            };
82                            match msg {
83                                ClientGeneral::TerrainChunkRequest { key } => {
84                                    let in_vd = if let Some(pos) = positions.get(entity) {
85                                        pos.0.xy().map(|e| e as f64).distance_squared(
86                                            key.map(|e| e as f64 + 0.5)
87                                                * TerrainChunkSize::RECT_SIZE.map(|e| e as f64),
88                                        ) < ((presence.terrain_view_distance.current() as f64 - 1.0
89                                            + 2.5 * 2.0_f64.sqrt())
90                                            * TerrainChunkSize::RECT_SIZE.x as f64)
91                                            .powi(2)
92                                    } else {
93                                        true
94                                    };
95                                    if in_vd {
96                                        if terrain.get_key_arc(key).is_some() {
97                                            network_metrics.chunks_served_from_memory.inc();
98                                            chunk_send_emitter.emit(ChunkSendEntry {
99                                                chunk_key: key,
100                                                entity,
101                                            });
102                                        } else {
103                                            network_metrics.chunks_generation_triggered.inc();
104                                            chunk_requests.push(ChunkRequest { entity, key });
105                                        }
106                                    } else {
107                                        network_metrics.chunks_request_dropped.inc();
108                                    }
109                                },
110                                _ => {
111                                    debug!(
112                                        "Kicking possibly misbehaving client due to invalud terrain \
113                                         request"
114                                    );
115                                    client_disconnect_emitter.emit(ClientDisconnectEvent(
116                                        entity,
117                                        common::comp::DisconnectReason::NetworkError,
118                                    ));
119                                },
120                            }
121                        }
122                        Ok(())
123                    });
124
125                    // Load a minimum radius of chunks around each player.
126                    // This is used to prevent view distance reloading exploits and make sure that
127                    // entity simulation occurs within a minimum radius around the
128                    // player.
129                    if let Some(pos) = positions.get(entity) {
130                        let player_chunk = pos
131                            .0
132                            .xy()
133                            .as_::<i32>()
134                            .wpos_to_cpos();
135                        for rpos in Spiral2d::new().take((crate::MIN_VD as usize + 1).pow(2)) {
136                            let key = player_chunk + rpos;
137                            if terrain.get_key(key).is_none() {
138                                // TODO: @zesterer do we want to be sending these chunk to the
139                                // client even if they aren't
140                                // requested? If we don't we could replace the
141                                // entity here with Option<Entity> and pass in None.
142                                chunk_requests.push(ChunkRequest { entity, key });
143                            }
144                        }
145                    }
146
147                    chunk_requests
148                },
149            )
150            .flatten()
151            .collect::<Vec<_>>();
152
153        job.cpu_stats.measure(ParMode::Single);
154
155        chunk_requests.append(&mut new_chunk_requests);
156    }
157}