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