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 if let Some(controller) = controller {
117 controller.inputs.update_with_new(*inputs);
118 }
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 if let Some(controller) = controller {
137 controller.push_action(event);
138 }
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 if comp_can_build.enabled {
164 for area in comp_can_build.build_areas.iter() {
165 if let Some(old_block) = build_areas
166 .areas()
167 .get(*area)
168 .filter(|aabb| aabb.contains_point(pos))
171 .and_then(|_| terrain.get(pos).ok())
172 {
173 let new_block = old_block.into_vacant();
174 let mut guard = rare_writes.lock();
176 let _was_set =
177 guard.block_changes.try_set(pos, new_block).is_some();
178 #[cfg(feature = "persistent_world")]
179 if _was_set {
180 if 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 }
190 },
191 ClientGeneral::PlaceBlock(pos, new_block) => {
192 if let Some(comp_can_build) = can_build.get(entity) {
193 if comp_can_build.enabled {
194 for area in comp_can_build.build_areas.iter() {
195 if build_areas
196 .areas()
197 .get(*area)
198 .filter(|aabb| aabb.contains_point(pos))
201 .is_some()
202 {
203 let mut guard = rare_writes.lock();
205 let _was_set =
206 guard.block_changes.try_set(pos, new_block).is_some();
207 #[cfg(feature = "persistent_world")]
208 if _was_set {
209 if let Some(terrain_persistence) =
210 guard._terrain_persistence.as_mut()
211 {
212 terrain_persistence.set_block(pos, new_block);
213 }
214 }
215 }
216 }
217 }
218 }
219 },
220 ClientGeneral::UnlockSkill(skill) => {
221 let _ = skill_set
223 .as_mut()
224 .map(|skill_set| {
225 SkillSet::unlock_skill_cow(skill_set, skill, |skill_set| skill_set.to_mut())
226 })
227 .transpose();
228 },
229 ClientGeneral::RequestSiteInfo(id) => {
230 emitters.emit(event::RequestSiteInfoEvent { entity, id });
231 },
232 ClientGeneral::RequestPlayerPhysics {
233 server_authoritative,
234 } => {
235 if let Some(setting) = player_physics_setting {
236 setting.client_optin = server_authoritative;
237 }
238 },
239 ClientGeneral::RequestLossyTerrainCompression {
240 lossy_terrain_compression,
241 } => {
242 presence.lossy_terrain_compression = lossy_terrain_compression;
243 },
244 ClientGeneral::UpdateMapMarker(update) => {
245 emitters.emit(event::UpdateMapMarkerEvent { entity, update });
246 },
247 ClientGeneral::SpectatePosition(pos) => {
248 if let Some(admin) = maybe_admin
249 && admin.0 >= AdminRole::Moderator
250 && presence.kind == PresenceKind::Spectator
251 {
252 if let Some(position) = position {
253 position.0 = pos;
254 }
255 }
256 },
257 ClientGeneral::SpectateEntity(uid) => {
258 if let Some(admin) = maybe_admin
259 && admin.0 >= AdminRole::Moderator
260 {
261 *spectating_entity = Some(uid);
262 }
263 },
264 ClientGeneral::SetBattleMode(battle_mode) => {
265 emitters.emit(event::SetBattleModeEvent {
266 entity,
267 battle_mode,
268 });
269 },
270 ClientGeneral::RequestCharacterList
271 | ClientGeneral::CreateCharacter { .. }
272 | ClientGeneral::EditCharacter { .. }
273 | ClientGeneral::DeleteCharacter(_)
274 | ClientGeneral::Character(_, _)
275 | ClientGeneral::Spectate(_)
276 | ClientGeneral::TerrainChunkRequest { .. }
277 | ClientGeneral::LodZoneRequest { .. }
278 | ClientGeneral::ChatMsg(_)
279 | ClientGeneral::Command(..)
280 | ClientGeneral::Terminate
281 | ClientGeneral::RequestPlugins(_) => {
282 debug!("Kicking possibly misbehaving client due to invalid client in game request");
283 emitters.emit(event::ClientDisconnectEvent(
284 entity,
285 common::comp::DisconnectReason::NetworkError,
286 ));
287 },
288 }
289 Ok(())
290 }
291}
292
293#[derive(Default)]
295pub struct Sys;
296impl<'a> System<'a> for Sys {
297 type SystemData = (
298 Entities<'a>,
299 Events<'a>,
300 (
301 ReadExpect<'a, TerrainGrid>,
302 ReadExpect<'a, SlowJobPool>,
303 ReadExpect<'a, EditableSettings>,
304 ),
305 (
306 Read<'a, IdMaps>,
307 Read<'a, DeltaTime>,
308 Read<'a, Settings>,
309 Read<'a, AreasContainer<BuildArea>>,
310 ),
311 ReadStorage<'a, CanBuild>,
312 WriteStorage<'a, ForceUpdate>,
313 ReadStorage<'a, Is<Rider>>,
314 ReadStorage<'a, Is<VolumeRider>>,
315 WriteStorage<'a, SkillSet>,
316 ReadStorage<'a, Health>,
317 ReadStorage<'a, Body>,
318 ReadStorage<'a, Scale>,
319 Write<'a, BlockChange>,
320 WriteStorage<'a, Pos>,
321 WriteStorage<'a, Vel>,
322 WriteStorage<'a, Ori>,
323 WriteStorage<'a, Presence>,
324 WriteStorage<'a, Client>,
325 WriteStorage<'a, Controller>,
326 WriteStorage<'a, SpectatingEntity>,
327 Write<'a, PlayerPhysicsSettings>,
328 TerrainPersistenceData<'a>,
329 ReadStorage<'a, Player>,
330 ReadStorage<'a, Admin>,
331 );
332
333 const NAME: &'static str = "msg::in_game";
334 const ORIGIN: Origin = Origin::Server;
335 const PHASE: Phase = Phase::Create;
336
337 fn run(
338 _job: &mut Job<Self>,
339 (
340 entities,
341 events,
342 (terrain, slow_jobs, editable_settings),
343 (id_maps, dt, settings, build_areas),
344 can_build,
345 mut force_updates,
346 is_rider,
347 is_volume_rider,
348 mut skill_sets,
349 healths,
350 bodies,
351 scales,
352 mut block_changes,
353 mut positions,
354 mut velocities,
355 mut orientations,
356 mut presences,
357 mut clients,
358 mut controllers,
359 mut spectating_entities,
360 mut player_physics_settings_,
361 mut terrain_persistence,
362 players,
363 admins,
364 ): Self::SystemData,
365 ) {
366 let time_for_vd_changes = Instant::now();
367
368 let rare_writes = parking_lot::Mutex::new(RareWrites {
371 block_changes: &mut block_changes,
372 _terrain_persistence: &mut terrain_persistence,
373 });
374
375 let player_physics_settings = &*player_physics_settings_;
376 let mut deferred_updates = (
377 &entities,
378 &mut clients,
379 (&mut presences).maybe(),
380 players.maybe(),
381 admins.maybe(),
382 (&skill_sets).maybe(),
383 (&mut positions).maybe(),
384 (&mut velocities).maybe(),
385 (&mut orientations).maybe(),
386 (&mut controllers).maybe(),
387 (&mut force_updates).maybe(),
388 )
389 .join()
390 .par_bridge()
392 .map_init(
393 || events.get_emitters(),
394 |emitters, (
395 entity,
396 client,
397 mut maybe_presence,
398 maybe_player,
399 maybe_admin,
400 skill_set,
401 ref mut pos,
402 ref mut vel,
403 ref mut ori,
404 ref mut controller,
405 ref mut force_update,
406 )| {
407 let old_player_physics_setting = maybe_player.map(|p| {
408 player_physics_settings
409 .settings
410 .get(&p.uuid())
411 .copied()
412 .unwrap_or_default()
413 });
414 let mut new_player_physics_setting = old_player_physics_setting;
415 let is_server_physics_forced = maybe_player.is_none_or(|p| editable_settings.server_physics_force_list.contains_key(&p.uuid()));
416 let mut clearable_maybe_presence = maybe_presence.as_deref_mut();
419 let mut skill_set = skill_set.map(Cow::Borrowed);
420 let mut player_physics = None;
421 let mut spectating_entity = None;
422 let _ = super::try_recv_all(client, 2, |client, msg| {
423 Self::handle_client_in_game_msg(
424 emitters,
425 entity,
426 client,
427 &mut clearable_maybe_presence,
428 &terrain,
429 &can_build,
430 &is_rider,
431 &is_volume_rider,
432 force_update.as_ref(),
433 &mut skill_set,
434 &healths,
435 &rare_writes,
436 pos.as_deref_mut(),
437 &mut spectating_entity,
438 controller.as_deref_mut(),
439 &settings,
440 &build_areas,
441 new_player_physics_setting.as_mut(),
442 is_server_physics_forced,
443 &maybe_admin,
444 time_for_vd_changes,
445 msg,
446 &mut player_physics,
447 )
448 });
449
450 if let Some((new_pos, new_vel, new_ori)) = player_physics
451 && let Some(old_pos) = pos.as_deref_mut()
452 && let Some(old_vel) = vel.as_deref_mut()
453 && let Some(old_ori) = ori.as_deref_mut()
454 {
455 enum Rejection {
456 TooFar { old: Vec3<f32>, new: Vec3<f32> },
457 TooFast { vel: Vec3<f32> },
458 InsideTerrain,
459 }
460
461 let rejection = if maybe_admin.is_some() {
462 None
463 } else {
464 const MAX_H_VELOCITY: f32 = 75.0;
466 const MAX_V_VELOCITY: std::ops::Range<f32> = -100.0..80.0;
467
468 'rejection: {
469 let is_velocity_ok = new_vel.0.xy().magnitude_squared() < MAX_H_VELOCITY.powi(2)
470 && MAX_V_VELOCITY.contains(&new_vel.0.z);
471
472 if !is_velocity_ok {
473 break 'rejection Some(Rejection::TooFast { vel: new_vel.0 });
474 }
475
476 const POSITION_THRESHOLD: f32 = 16.0;
479
480 let is_position_ok = [old_vel.0, new_vel.0]
483 .into_iter()
484 .any(|ref_vel| {
485 let rpos = new_pos.0 - old_pos.0;
486 LineSegment3 {
489 start: Vec3::zero(),
490 end: ref_vel * dt.0,
491 }
492 .projected_point(rpos)
493 .distance_squared(rpos) < (rpos.magnitude() * 0.5 + 1.5 + POSITION_THRESHOLD).powi(2)
496 });
497
498 if !is_position_ok {
499 break 'rejection Some(Rejection::TooFar { old: old_pos.0, new: new_pos.0 });
500 }
501
502 if new_pos.0 != old_pos.0 {
504 let scale = scales.get(entity).map_or(1.0, |s| s.0);
506 let min_z = new_pos.0.z as i32;
507 let height = bodies.get(entity).map_or(0.0, |b| b.height()) * scale;
508 let head_pos_z = (new_pos.0.z + height) as i32;
509
510 if !(min_z..=head_pos_z).any(|z| {
511 let pos = new_pos.0.as_().with_z(z);
512
513 terrain
514 .get(pos)
515 .is_ok_and(|block| block.is_fluid())
516 }) {
517 break 'rejection Some(Rejection::InsideTerrain);
518 }
519 }
520
521 None
522 }
523 };
524
525 if let Some(rejection) = rejection {
526 let alias = maybe_player.map(|p| &p.alias);
528 match rejection {
529 Rejection::TooFar { old, new } => warn!("Rejected physics for player {alias:?} (new position {new:?} is too far from old position {old:?})"),
530 Rejection::TooFast { vel } => warn!("Rejected physics for player {alias:?} (new velocity {vel:?} is too fast)"),
531 Rejection::InsideTerrain => warn!("Rejected physics for player {alias:?}: Inside terrain."),
532 }
533
534 force_update.as_mut().map(|fu| fu.update());
544 } else {
545 *old_pos = new_pos;
546 *old_vel = new_vel;
547 *old_ori = new_ori;
548 }
549 }
550
551 if let Some(presence) = maybe_presence {
554 presence.terrain_view_distance.update(time_for_vd_changes);
555 presence.entity_view_distance.update(time_for_vd_changes);
556 }
557
558 let skill_set_update = skill_set.and_then(|skill_set| match skill_set {
561 Cow::Borrowed(_) => None,
562 Cow::Owned(skill_set) => Some((entity, skill_set)),
563 });
564 let physics_update = maybe_player.map(|p| p.uuid())
569 .zip(new_player_physics_setting
570 .filter(|_| old_player_physics_setting != new_player_physics_setting));
571 let spectating_entity_update = spectating_entity.map(|e| (entity, e));
572 (skill_set_update, spectating_entity_update, physics_update)
573 },
574 )
575 .filter(|(x, y, z)| x.is_some() || y.is_some() || z.is_some())
578 .collect::<Vec<_>>();
582 let player_physics_settings = &mut *player_physics_settings_;
583 deferred_updates.iter_mut().for_each(
591 |(skill_set_update, spectating_entity_update, physics_update)| {
592 if let Some((entity, new_skill_set)) = skill_set_update {
593 skill_sets
600 .get_mut(*entity)
601 .map(|mut old_skill_set| mem::swap(&mut *old_skill_set, new_skill_set));
602 }
603 if let &mut Some((entity, spectating_uid)) = spectating_entity_update {
604 if let Some(uid) = spectating_uid
605 && let Some(spectated_entity) = id_maps.uid_entity(uid)
606 {
607 let _ =
609 spectating_entities.insert(entity, SpectatingEntity(spectated_entity));
610 } else {
611 spectating_entities.remove(entity);
612 }
613 }
614 if let &mut Some((uuid, player_physics_setting)) = physics_update {
615 player_physics_settings
618 .settings
619 .insert(uuid, player_physics_setting);
620 }
621 },
622 );
623 slow_jobs.spawn("CHUNK_DROP", move || {
625 drop(deferred_updates);
626 });
627 }
628}