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        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            // Go back to registered state (char selection screen)
92            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                // Correct client if its requested VD is too high.
108                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                    // Skip respawn if client entity is alive
124                    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                                // TODO: Make this an exclusive check on the upper bound of the AABB
167                                // Vek defaults to inclusive which is not optimal
168                                .filter(|aabb| aabb.contains_point(pos))
169                                .and_then(|_| terrain.get(pos).ok())
170                            {
171                                let new_block = old_block.into_vacant();
172                                // Take the rare writes lock as briefly as possible.
173                                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                                // TODO: Make this an exclusive check on the upper bound of the AABB
197                                // Vek defaults to inclusive which is not optimal
198                                .filter(|aabb| aabb.contains_point(pos))
199                                .is_some()
200                            {
201                                // Take the rare writes lock as briefly as possible.
202                                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                // FIXME: How do we want to handle the error?  Probably not by swallowing it.
220                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/// This system will handle new messages from clients
285#[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        // NOTE: stdlib mutex is more than good enough on Linux and (probably) Windows,
357        // but not Mac.
358        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            // NOTE: Required because Specs has very poor work splitting for sparse joins.
379            .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                    // If an `ExitInGame` message is received this is set to `None` allowing further
405                    // ingame messages to be ignored.
406                    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                            // Reminder: review these frequently to ensure they're reasonable
451                            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                                // How far the player is permitted to stray from the correct position (perhaps due to
463                                // latency problems).
464                                const POSITION_THRESHOLD: f32 = 16.0;
465
466                                // The position can either be sensible with respect to either the old or the new
467                                // velocity such that we don't punish for edge cases after a sudden change
468                                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                                        // Determine whether the change in position is broadly consistent with both
473                                        // the magnitude and direction of the velocity, with appropriate thresholds.
474                                        LineSegment3 {
475                                            start: Vec3::zero(),
476                                            end: ref_vel * dt.0,
477                                        }
478                                            .projected_point(rpos)
479                                            // + 1.5 accounts for minor changes in position without corresponding
480                                            // velocity like block hopping/snapping
481                                            .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                                // Checks that are only relevant if the position changed
489                                if new_pos.0 != old_pos.0 {
490                                    // Reject updates that would move the entity into terrain
491                                    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                            // TODO: Log when false positives aren't generated often
513                            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                            /*
521                            // Perhaps this is overzealous?
522                            if let Some(mut setting) = new_player_physics_setting.as_mut() {
523                                setting.server_force = true;
524                                warn!("Switching player {alias:?} to server-side physics");
525                            }
526                            */
527
528                            // Reject the change and force the server's view of the physics state
529                            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                    // Ensure deferred view distance changes are applied (if the
538                    // requsite time has elapsed).
539                    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                    // Return the possibly modified skill set, and possibly modified server physics
545                    // settings.
546                    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                    // NOTE: Since we pass Option<&mut _> rather than &mut Option<_> to
551                    // handle_client_in_game_msg, and the new player was initialized to the same
552                    // value as the old setting , we know that either both the new and old setting
553                    // are Some, or they are both None.
554                    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            // NOTE: Would be nice to combine this with the map_init somehow, but I'm not sure if
561            // that's possible.
562            .filter(|(x, y)| x.is_some() || y.is_some())
563            // NOTE: I feel like we shouldn't actually need to allocate here, but hopefully this
564            // doesn't turn out to be important as there shouldn't be that many connected clients.
565            // The reason we can't just use unzip is that the two sides might be different lengths.
566            .collect::<Vec<_>>();
567        let player_physics_settings = &mut *player_physics_settings_;
568        // Deferred updates to skillsets and player physics.
569        //
570        // NOTE: It is an invariant that there is at most one client entry per player
571        // uuid; since we joined on clients, it follows that there's just one update
572        // per uuid, so the physics update is sound and doesn't depend on evaluation
573        // order, even though we're not updating directly by entity or uid (note that
574        // for a given entity, we process messages serially).
575        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                    // We know this exists, because we already iterated over it with the skillset
580                    // lock taken, so we can ignore the error.
581                    //
582                    // Note that we replace rather than just updating.  This is in order to avoid
583                    // dropping here; we'll drop later on a background thread, in case skillsets are
584                    // slow to drop.
585                    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                    // We don't necessarily know this exists, but that's fine, because dropping
591                    // player physics is a no op.
592                    player_physics_settings
593                        .settings
594                        .insert(uuid, player_physics_setting);
595                }
596            });
597        // Finally, drop the deferred updates in another thread.
598        slow_jobs.spawn("CHUNK_DROP", move || {
599            drop(deferred_updates);
600        });
601    }
602}