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