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#[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 .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 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 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 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}