1#[cfg(feature = "persistent_world")]
2use crate::TerrainPersistence;
3use crate::{EditableSettings, Settings, client::Client};
4use common::{
5 comp::{
6 Admin, AdminRole, Body, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori,
7 Player, Pos, Presence, PresenceKind, Scale, SkillSet, Vel,
8 },
9 event::{self, EmitExt},
10 event_emitters,
11 link::Is,
12 mounting::{Rider, VolumeRider},
13 resources::{DeltaTime, PlayerPhysicsSetting, PlayerPhysicsSettings},
14 slowjob::SlowJobPool,
15 terrain::TerrainGrid,
16 vol::ReadVol,
17};
18use common_ecs::{Job, Origin, Phase, System};
19use common_net::msg::{ClientGeneral, ServerGeneral};
20use common_state::{AreasContainer, BlockChange, BuildArea};
21use core::mem;
22use rayon::prelude::*;
23use specs::{Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, Write, WriteStorage};
24use std::{borrow::Cow, time::Instant};
25use tracing::{debug, trace, warn};
26use vek::*;
27
28#[cfg(feature = "persistent_world")]
29pub type TerrainPersistenceData<'a> = Option<Write<'a, TerrainPersistence>>;
30#[cfg(not(feature = "persistent_world"))]
31pub type TerrainPersistenceData<'a> = core::marker::PhantomData<&'a mut ()>;
32
33struct RareWrites<'a, 'b> {
42 block_changes: &'b mut BlockChange,
43 _terrain_persistence: &'b mut TerrainPersistenceData<'a>,
44}
45
46event_emitters! {
47 struct Events[Emitters] {
48 exit_ingame: event::ExitIngameEvent,
49 request_site_info: event::RequestSiteInfoEvent,
50 update_map_marker: event::UpdateMapMarkerEvent,
51 client_disconnect: event::ClientDisconnectEvent,
52 }
53}
54
55impl Sys {
56 #[expect(clippy::too_many_arguments)]
57 fn handle_client_in_game_msg(
58 emitters: &mut Emitters,
59 entity: specs::Entity,
60 client: &Client,
61 maybe_presence: &mut Option<&mut Presence>,
62 terrain: &ReadExpect<'_, TerrainGrid>,
63 can_build: &ReadStorage<'_, CanBuild>,
64 is_rider: &ReadStorage<'_, Is<Rider>>,
65 is_volume_rider: &ReadStorage<'_, Is<VolumeRider>>,
66 force_update: Option<&&mut ForceUpdate>,
67 skill_set: &mut Option<Cow<'_, SkillSet>>,
68 healths: &ReadStorage<'_, Health>,
69 rare_writes: &parking_lot::Mutex<RareWrites<'_, '_>>,
70 position: Option<&mut Pos>,
71 controller: Option<&mut Controller>,
72 settings: &Read<'_, Settings>,
73 build_areas: &Read<'_, AreasContainer<BuildArea>>,
74 player_physics_setting: Option<&mut PlayerPhysicsSetting>,
75 server_physics_forced: bool,
76 maybe_admin: &Option<&Admin>,
77 time_for_vd_changes: Instant,
78 msg: ClientGeneral,
79 player_physics: &mut Option<(Pos, Vel, Ori)>,
80 ) -> Result<(), crate::error::Error> {
81 let presence = match maybe_presence.as_deref_mut() {
82 Some(g) => g,
83 None => {
84 debug!(?entity, "client is not in_game, ignoring msg");
85 trace!(?msg, "ignored msg content");
86 return Ok(());
87 },
88 };
89 match msg {
90 ClientGeneral::ExitInGame => {
92 emitters.emit(event::ExitIngameEvent { entity });
93 client.send(ServerGeneral::ExitInGameSuccess)?;
94 *maybe_presence = None;
95 },
96 ClientGeneral::SetViewDistance(view_distances) => {
97 let clamped_vds = view_distances.clamp(settings.max_view_distance);
98
99 presence
100 .terrain_view_distance
101 .set_target(clamped_vds.terrain, time_for_vd_changes);
102 presence
103 .entity_view_distance
104 .set_target(clamped_vds.entity, time_for_vd_changes);
105
106 if view_distances.terrain != clamped_vds.terrain {
108 client.send(ServerGeneral::SetViewDistance(clamped_vds.terrain))?;
109 }
110 },
111 ClientGeneral::ControllerInputs(inputs) => {
112 if presence.kind.controlling_char() {
113 if let Some(controller) = controller {
114 controller.inputs.update_with_new(*inputs);
115 }
116 }
117 },
118 ClientGeneral::ControlEvent(event) => {
119 if presence.kind.controlling_char()
120 && let Some(controller) = controller
121 {
122 let skip_respawn = matches!(event, ControlEvent::Respawn)
124 && healths.get(entity).is_none_or(|h| !h.is_dead);
125
126 if !skip_respawn {
127 controller.push_event(event);
128 }
129 }
130 },
131 ClientGeneral::ControlAction(event) => {
132 if presence.kind.controlling_char() {
133 if let Some(controller) = controller {
134 controller.push_action(event);
135 }
136 }
137 },
138 ClientGeneral::PlayerPhysics {
139 pos,
140 vel,
141 ori,
142 force_counter,
143 } => {
144 if presence.kind.controlling_char()
145 && force_update
146 .is_none_or(|force_update| force_update.counter() == force_counter)
147 && healths.get(entity).is_none_or(|h| !h.is_dead)
148 && is_rider.get(entity).is_none()
149 && is_volume_rider.get(entity).is_none()
150 && !server_physics_forced
151 && player_physics_setting
152 .as_ref()
153 .is_none_or(|s| !s.server_authoritative_physics_optin())
154 {
155 *player_physics = Some((pos, vel, ori));
156 }
157 },
158 ClientGeneral::BreakBlock(pos) => {
159 if let Some(comp_can_build) = can_build.get(entity) {
160 if comp_can_build.enabled {
161 for area in comp_can_build.build_areas.iter() {
162 if let Some(old_block) = build_areas
163 .areas()
164 .get(*area)
165 .filter(|aabb| aabb.contains_point(pos))
168 .and_then(|_| terrain.get(pos).ok())
169 {
170 let new_block = old_block.into_vacant();
171 let mut guard = rare_writes.lock();
173 let _was_set =
174 guard.block_changes.try_set(pos, new_block).is_some();
175 #[cfg(feature = "persistent_world")]
176 if _was_set {
177 if let Some(terrain_persistence) =
178 guard._terrain_persistence.as_mut()
179 {
180 terrain_persistence.set_block(pos, new_block);
181 }
182 }
183 }
184 }
185 }
186 }
187 },
188 ClientGeneral::PlaceBlock(pos, new_block) => {
189 if let Some(comp_can_build) = can_build.get(entity) {
190 if comp_can_build.enabled {
191 for area in comp_can_build.build_areas.iter() {
192 if build_areas
193 .areas()
194 .get(*area)
195 .filter(|aabb| aabb.contains_point(pos))
198 .is_some()
199 {
200 let mut guard = rare_writes.lock();
202 let _was_set =
203 guard.block_changes.try_set(pos, new_block).is_some();
204 #[cfg(feature = "persistent_world")]
205 if _was_set {
206 if let Some(terrain_persistence) =
207 guard._terrain_persistence.as_mut()
208 {
209 terrain_persistence.set_block(pos, new_block);
210 }
211 }
212 }
213 }
214 }
215 }
216 },
217 ClientGeneral::UnlockSkill(skill) => {
218 let _ = skill_set
220 .as_mut()
221 .map(|skill_set| {
222 SkillSet::unlock_skill_cow(skill_set, skill, |skill_set| skill_set.to_mut())
223 })
224 .transpose();
225 },
226 ClientGeneral::RequestSiteInfo(id) => {
227 emitters.emit(event::RequestSiteInfoEvent { entity, id });
228 },
229 ClientGeneral::RequestPlayerPhysics {
230 server_authoritative,
231 } => {
232 if let Some(setting) = player_physics_setting {
233 setting.client_optin = server_authoritative;
234 }
235 },
236 ClientGeneral::RequestLossyTerrainCompression {
237 lossy_terrain_compression,
238 } => {
239 presence.lossy_terrain_compression = lossy_terrain_compression;
240 },
241 ClientGeneral::UpdateMapMarker(update) => {
242 emitters.emit(event::UpdateMapMarkerEvent { entity, update });
243 },
244 ClientGeneral::SpectatePosition(pos) => {
245 if let Some(admin) = maybe_admin
246 && admin.0 >= AdminRole::Moderator
247 && presence.kind == PresenceKind::Spectator
248 {
249 if let Some(position) = position {
250 position.0 = pos;
251 }
252 }
253 },
254 ClientGeneral::RequestCharacterList
255 | ClientGeneral::CreateCharacter { .. }
256 | ClientGeneral::EditCharacter { .. }
257 | ClientGeneral::DeleteCharacter(_)
258 | ClientGeneral::Character(_, _)
259 | ClientGeneral::Spectate(_)
260 | ClientGeneral::TerrainChunkRequest { .. }
261 | ClientGeneral::LodZoneRequest { .. }
262 | ClientGeneral::ChatMsg(_)
263 | ClientGeneral::Command(..)
264 | ClientGeneral::Terminate
265 | ClientGeneral::RequestPlugins(_) => {
266 debug!("Kicking possibly misbehaving client due to invalid client in game request");
267 emitters.emit(event::ClientDisconnectEvent(
268 entity,
269 common::comp::DisconnectReason::NetworkError,
270 ));
271 },
272 }
273 Ok(())
274 }
275}
276
277#[derive(Default)]
279pub struct Sys;
280impl<'a> System<'a> for Sys {
281 type SystemData = (
282 Entities<'a>,
283 Events<'a>,
284 (
285 ReadExpect<'a, TerrainGrid>,
286 ReadExpect<'a, SlowJobPool>,
287 ReadExpect<'a, EditableSettings>,
288 ),
289 ReadStorage<'a, CanBuild>,
290 WriteStorage<'a, ForceUpdate>,
291 ReadStorage<'a, Is<Rider>>,
292 ReadStorage<'a, Is<VolumeRider>>,
293 WriteStorage<'a, SkillSet>,
294 ReadStorage<'a, Health>,
295 ReadStorage<'a, Body>,
296 ReadStorage<'a, Scale>,
297 Write<'a, BlockChange>,
298 WriteStorage<'a, Pos>,
299 WriteStorage<'a, Vel>,
300 WriteStorage<'a, Ori>,
301 WriteStorage<'a, Presence>,
302 WriteStorage<'a, Client>,
303 WriteStorage<'a, Controller>,
304 Read<'a, DeltaTime>,
305 Read<'a, Settings>,
306 Read<'a, AreasContainer<BuildArea>>,
307 Write<'a, PlayerPhysicsSettings>,
308 TerrainPersistenceData<'a>,
309 ReadStorage<'a, Player>,
310 ReadStorage<'a, Admin>,
311 );
312
313 const NAME: &'static str = "msg::in_game";
314 const ORIGIN: Origin = Origin::Server;
315 const PHASE: Phase = Phase::Create;
316
317 fn run(
318 _job: &mut Job<Self>,
319 (
320 entities,
321 events,
322 (terrain, slow_jobs, editable_settings),
323 can_build,
324 mut force_updates,
325 is_rider,
326 is_volume_rider,
327 mut skill_sets,
328 healths,
329 bodies,
330 scales,
331 mut block_changes,
332 mut positions,
333 mut velocities,
334 mut orientations,
335 mut presences,
336 mut clients,
337 mut controllers,
338 dt,
339 settings,
340 build_areas,
341 mut player_physics_settings_,
342 mut terrain_persistence,
343 players,
344 admins,
345 ): Self::SystemData,
346 ) {
347 let time_for_vd_changes = Instant::now();
348
349 let rare_writes = parking_lot::Mutex::new(RareWrites {
352 block_changes: &mut block_changes,
353 _terrain_persistence: &mut terrain_persistence,
354 });
355
356 let player_physics_settings = &*player_physics_settings_;
357 let mut deferred_updates = (
358 &entities,
359 &mut clients,
360 (&mut presences).maybe(),
361 players.maybe(),
362 admins.maybe(),
363 (&skill_sets).maybe(),
364 (&mut positions).maybe(),
365 (&mut velocities).maybe(),
366 (&mut orientations).maybe(),
367 (&mut controllers).maybe(),
368 (&mut force_updates).maybe(),
369 )
370 .join()
371 .par_bridge()
373 .map_init(
374 || events.get_emitters(),
375 |emitters, (
376 entity,
377 client,
378 mut maybe_presence,
379 maybe_player,
380 maybe_admin,
381 skill_set,
382 ref mut pos,
383 ref mut vel,
384 ref mut ori,
385 ref mut controller,
386 ref mut force_update,
387 )| {
388 let old_player_physics_setting = maybe_player.map(|p| {
389 player_physics_settings
390 .settings
391 .get(&p.uuid())
392 .copied()
393 .unwrap_or_default()
394 });
395 let mut new_player_physics_setting = old_player_physics_setting;
396 let is_server_physics_forced = maybe_player.is_none_or(|p| editable_settings.server_physics_force_list.contains_key(&p.uuid()));
397 let mut clearable_maybe_presence = maybe_presence.as_deref_mut();
400 let mut skill_set = skill_set.map(Cow::Borrowed);
401 let mut player_physics = None;
402 let _ = super::try_recv_all(client, 2, |client, msg| {
403 Self::handle_client_in_game_msg(
404 emitters,
405 entity,
406 client,
407 &mut clearable_maybe_presence,
408 &terrain,
409 &can_build,
410 &is_rider,
411 &is_volume_rider,
412 force_update.as_ref(),
413 &mut skill_set,
414 &healths,
415 &rare_writes,
416 pos.as_deref_mut(),
417 controller.as_deref_mut(),
418 &settings,
419 &build_areas,
420 new_player_physics_setting.as_mut(),
421 is_server_physics_forced,
422 &maybe_admin,
423 time_for_vd_changes,
424 msg,
425 &mut player_physics,
426 )
427 });
428
429 if let Some((new_pos, new_vel, new_ori)) = player_physics
430 && let Some(old_pos) = pos.as_deref_mut()
431 && let Some(old_vel) = vel.as_deref_mut()
432 && let Some(old_ori) = ori.as_deref_mut()
433 {
434 enum Rejection {
435 TooFar { old: Vec3<f32>, new: Vec3<f32> },
436 TooFast { vel: Vec3<f32> },
437 InsideTerrain,
438 }
439
440 let rejection = if maybe_admin.is_some() {
441 None
442 } else {
443 const MAX_H_VELOCITY: f32 = 75.0;
445 const MAX_V_VELOCITY: std::ops::Range<f32> = -100.0..80.0;
446
447 'rejection: {
448 let is_velocity_ok = new_vel.0.xy().magnitude_squared() < MAX_H_VELOCITY.powi(2)
449 && MAX_V_VELOCITY.contains(&new_vel.0.z);
450
451 if !is_velocity_ok {
452 break 'rejection Some(Rejection::TooFast { vel: new_vel.0 });
453 }
454
455 const POSITION_THRESHOLD: f32 = 16.0;
458
459 let is_position_ok = [old_vel.0, new_vel.0]
462 .into_iter()
463 .any(|ref_vel| {
464 let rpos = new_pos.0 - old_pos.0;
465 LineSegment3 {
468 start: Vec3::zero(),
469 end: ref_vel * dt.0,
470 }
471 .projected_point(rpos)
472 .distance_squared(rpos) < (rpos.magnitude() * 0.5 + 1.5 + POSITION_THRESHOLD).powi(2)
475 });
476
477 if !is_position_ok {
478 break 'rejection Some(Rejection::TooFar { old: old_pos.0, new: new_pos.0 });
479 }
480
481 if new_pos.0 != old_pos.0 {
483 let scale = scales.get(entity).map_or(1.0, |s| s.0);
485 let min_z = new_pos.0.z as i32;
486 let height = bodies.get(entity).map_or(0.0, |b| b.height()) * scale;
487 let head_pos_z = (new_pos.0.z + height) as i32;
488
489 if !(min_z..=head_pos_z).any(|z| {
490 let pos = new_pos.0.as_().with_z(z);
491
492 terrain
493 .get(pos)
494 .is_ok_and(|block| block.is_fluid())
495 }) {
496 break 'rejection Some(Rejection::InsideTerrain);
497 }
498 }
499
500 None
501 }
502 };
503
504 if let Some(rejection) = rejection {
505 let alias = maybe_player.map(|p| &p.alias);
507 match rejection {
508 Rejection::TooFar { old, new } => warn!("Rejected physics for player {alias:?} (new position {new:?} is too far from old position {old:?})"),
509 Rejection::TooFast { vel } => warn!("Rejected physics for player {alias:?} (new velocity {vel:?} is too fast)"),
510 Rejection::InsideTerrain => warn!("Rejected physics for player {alias:?}: Inside terrain."),
511 }
512
513 force_update.as_mut().map(|fu| fu.update());
523 } else {
524 *old_pos = new_pos;
525 *old_vel = new_vel;
526 *old_ori = new_ori;
527 }
528 }
529
530 if let Some(presence) = maybe_presence {
533 presence.terrain_view_distance.update(time_for_vd_changes);
534 presence.entity_view_distance.update(time_for_vd_changes);
535 }
536
537 let skill_set_update = skill_set.and_then(|skill_set| match skill_set {
540 Cow::Borrowed(_) => None,
541 Cow::Owned(skill_set) => Some((entity, skill_set)),
542 });
543 let physics_update = maybe_player.map(|p| p.uuid())
548 .zip(new_player_physics_setting
549 .filter(|_| old_player_physics_setting != new_player_physics_setting));
550 (skill_set_update, physics_update)
551 },
552 )
553 .filter(|(x, y)| x.is_some() || y.is_some())
556 .collect::<Vec<_>>();
560 let player_physics_settings = &mut *player_physics_settings_;
561 deferred_updates
569 .iter_mut()
570 .for_each(|(skill_set_update, physics_update)| {
571 if let Some((entity, new_skill_set)) = skill_set_update {
572 skill_sets
579 .get_mut(*entity)
580 .map(|mut old_skill_set| mem::swap(&mut *old_skill_set, new_skill_set));
581 }
582 if let &mut Some((uuid, player_physics_setting)) = physics_update {
583 player_physics_settings
586 .settings
587 .insert(uuid, player_physics_setting);
588 }
589 });
590 slow_jobs.spawn("CHUNK_DROP", move || {
592 drop(deferred_updates);
593 });
594 }
595}