veloren_server/sys/msg/
in_game.rs

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
33// NOTE: These writes are considered "rare", meaning (currently) that they are
34// admin-gated features that players shouldn't normally access, and which we're
35// not that concerned about the performance of when two players try to use them
36// at once.
37//
38// In such cases, we're okay putting them behind a mutex and penalizing the
39// system if they're actually used concurrently by lots of users.  Please do not
40// put less rare writes here, unless you want to serialize the system!
41struct 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            // Go back to registered state (char selection screen)
91            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                // Correct client if its requested VD is too high.
107                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                    // Skip respawn if client entity is alive
123                    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                                // TODO: Make this an exclusive check on the upper bound of the AABB
166                                // Vek defaults to inclusive which is not optimal
167                                .filter(|aabb| aabb.contains_point(pos))
168                                .and_then(|_| terrain.get(pos).ok())
169                            {
170                                let new_block = old_block.into_vacant();
171                                // Take the rare writes lock as briefly as possible.
172                                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                                // TODO: Make this an exclusive check on the upper bound of the AABB
196                                // Vek defaults to inclusive which is not optimal
197                                .filter(|aabb| aabb.contains_point(pos))
198                                .is_some()
199                            {
200                                // Take the rare writes lock as briefly as possible.
201                                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                // FIXME: How do we want to handle the error?  Probably not by swallowing it.
219                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/// This system will handle new messages from clients
278#[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        // NOTE: stdlib mutex is more than good enough on Linux and (probably) Windows,
350        // but not Mac.
351        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            // NOTE: Required because Specs has very poor work splitting for sparse joins.
372            .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                    // If an `ExitInGame` message is received this is set to `None` allowing further
398                    // ingame messages to be ignored.
399                    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                            // Reminder: review these frequently to ensure they're reasonable
444                            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                                // How far the player is permitted to stray from the correct position (perhaps due to
456                                // latency problems).
457                                const POSITION_THRESHOLD: f32 = 16.0;
458
459                                // The position can either be sensible with respect to either the old or the new
460                                // velocity such that we don't punish for edge cases after a sudden change
461                                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                                        // Determine whether the change in position is broadly consistent with both
466                                        // the magnitude and direction of the velocity, with appropriate thresholds.
467                                        LineSegment3 {
468                                            start: Vec3::zero(),
469                                            end: ref_vel * dt.0,
470                                        }
471                                            .projected_point(rpos)
472                                            // + 1.5 accounts for minor changes in position without corresponding
473                                            // velocity like block hopping/snapping
474                                            .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                                // Checks that are only relevant if the position changed
482                                if new_pos.0 != old_pos.0 {
483                                    // Reject updates that would move the entity into terrain
484                                    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                            // TODO: Log when false positives aren't generated often
506                            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                            /*
514                            // Perhaps this is overzealous?
515                            if let Some(mut setting) = new_player_physics_setting.as_mut() {
516                                setting.server_force = true;
517                                warn!("Switching player {alias:?} to server-side physics");
518                            }
519                            */
520
521                            // Reject the change and force the server's view of the physics state
522                            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                    // Ensure deferred view distance changes are applied (if the
531                    // requsite time has elapsed).
532                    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                    // Return the possibly modified skill set, and possibly modified server physics
538                    // settings.
539                    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                    // NOTE: Since we pass Option<&mut _> rather than &mut Option<_> to
544                    // handle_client_in_game_msg, and the new player was initialized to the same
545                    // value as the old setting , we know that either both the new and old setting
546                    // are Some, or they are both None.
547                    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            // NOTE: Would be nice to combine this with the map_init somehow, but I'm not sure if
554            // that's possible.
555            .filter(|(x, y)| x.is_some() || y.is_some())
556            // NOTE: I feel like we shouldn't actually need to allocate here, but hopefully this
557            // doesn't turn out to be important as there shouldn't be that many connected clients.
558            // The reason we can't just use unzip is that the two sides might be different lengths.
559            .collect::<Vec<_>>();
560        let player_physics_settings = &mut *player_physics_settings_;
561        // Deferred updates to skillsets and player physics.
562        //
563        // NOTE: It is an invariant that there is at most one client entry per player
564        // uuid; since we joined on clients, it follows that there's just one update
565        // per uuid, so the physics update is sound and doesn't depend on evaluation
566        // order, even though we're not updating directly by entity or uid (note that
567        // for a given entity, we process messages serially).
568        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                    // We know this exists, because we already iterated over it with the skillset
573                    // lock taken, so we can ignore the error.
574                    //
575                    // Note that we replace rather than just updating.  This is in order to avoid
576                    // dropping here; we'll drop later on a background thread, in case skillsets are
577                    // slow to drop.
578                    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                    // We don't necessarily know this exists, but that's fine, because dropping
584                    // player physics is a no op.
585                    player_physics_settings
586                        .settings
587                        .insert(uuid, player_physics_setting);
588                }
589            });
590        // Finally, drop the deferred updates in another thread.
591        slow_jobs.spawn("CHUNK_DROP", move || {
592            drop(deferred_updates);
593        });
594    }
595}