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, 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
34// NOTE: These writes are considered "rare", meaning (currently) that they are
35// admin-gated features that players shouldn't normally access, and which we're
36// not that concerned about the performance of when two players try to use them
37// at once.
38//
39// In such cases, we're okay putting them behind a mutex and penalizing the
40// system if they're actually used concurrently by lots of users.  Please do not
41// put less rare writes here, unless you want to serialize the system!
42struct 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            // Go back to registered state (char selection screen)
94            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                // Correct client if its requested VD is too high.
110                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                    // Skip respawn if client entity is alive
126                    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                                // TODO: Make this an exclusive check on the upper bound of the AABB
170                                // Vek defaults to inclusive which is not optimal
171                                .filter(|aabb| aabb.contains_point(pos))
172                                .and_then(|_| terrain.get(pos).ok())
173                        {
174                            let new_block = old_block.into_vacant();
175                            // Take the rare writes lock as briefly as possible.
176                            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                                // TODO: Make this an exclusive check on the upper bound of the AABB
198                                // Vek defaults to inclusive which is not optimal
199                                .filter(|aabb| aabb.contains_point(pos))
200                                .is_some()
201                        {
202                            // Take the rare writes lock as briefly as possible.
203                            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                // FIXME: How do we want to handle the error?  Probably not by swallowing it.
218                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/// This system will handle new messages from clients
289#[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        // NOTE: stdlib mutex is more than good enough on Linux and (probably) Windows,
364        // but not Mac.
365        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            // NOTE: Required because Specs has very poor work splitting for sparse joins.
386            .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                    // If an `ExitInGame` message is received this is set to `None` allowing further
412                    // ingame messages to be ignored.
413                    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                            // Reminder: review these frequently to ensure they're reasonable
460                            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                                // How far the player is permitted to stray from the correct position (perhaps due to
472                                // latency problems).
473                                const POSITION_THRESHOLD: f32 = 16.0;
474
475                                // The position can either be sensible with respect to either the old or the new
476                                // velocity such that we don't punish for edge cases after a sudden change
477                                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                                        // Determine whether the change in position is broadly consistent with both
482                                        // the magnitude and direction of the velocity, with appropriate thresholds.
483                                        LineSegment3 {
484                                            start: Vec3::zero(),
485                                            end: ref_vel * dt.0,
486                                        }
487                                            .projected_point(rpos)
488                                            // + 1.5 accounts for minor changes in position without corresponding
489                                            // velocity like block hopping/snapping
490                                            .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                                // Checks that are only relevant if the position changed
498                                if new_pos.0 != old_pos.0 {
499                                    // Reject updates that would move the entity into terrain
500                                    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                            // TODO: Log when false positives aren't generated often
522                            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                            /*
530                            // Perhaps this is overzealous?
531                            if let Some(mut setting) = new_player_physics_setting.as_mut() {
532                                setting.server_force = true;
533                                warn!("Switching player {alias:?} to server-side physics");
534                            }
535                            */
536
537                            // Reject the change and force the server's view of the physics state
538                            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                    // Ensure deferred view distance changes are applied (if the
547                    // requsite time has elapsed).
548                    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                    // Return the possibly modified skill set, and possibly modified server physics
554                    // settings.
555                    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                    // NOTE: Since we pass Option<&mut _> rather than &mut Option<_> to
560                    // handle_client_in_game_msg, and the new player was initialized to the same
561                    // value as the old setting , we know that either both the new and old setting
562                    // are Some, or they are both None.
563                    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            // NOTE: Would be nice to combine this with the map_init somehow, but I'm not sure if
571            // that's possible.
572            .filter(|(x, y, z)| x.is_some() || y.is_some() || z.is_some())
573            // NOTE: I feel like we shouldn't actually need to allocate here, but hopefully this
574            // doesn't turn out to be important as there shouldn't be that many connected clients.
575            // The reason we can't just use unzip is that the two sides might be different lengths.
576            .collect::<Vec<_>>();
577        let player_physics_settings = &mut *player_physics_settings_;
578        // Deferred updates to skillsets and player physics.
579        //
580        // NOTE: It is an invariant that there is at most one client entry per player
581        // uuid; since we joined on clients, it follows that there's just one update
582        // per uuid, so the physics update is sound and doesn't depend on evaluation
583        // order, even though we're not updating directly by entity or uid (note that
584        // for a given entity, we process messages serially).
585        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                    // We know this exists, because we already iterated over it with the skillset
589                    // lock taken, so we can ignore the error.
590                    //
591                    // Note that we replace rather than just updating.  This is in order to avoid
592                    // dropping here; we'll drop later on a background thread, in case skillsets are
593                    // slow to drop.
594                    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                        // We know this exists, so can ignore the error.
603                        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                    // We don't necessarily know this exists, but that's fine, because dropping
611                    // player physics is a no op.
612                    player_physics_settings
613                        .settings
614                        .insert(uuid, player_physics_setting);
615                }
616            },
617        );
618        // Finally, drop the deferred updates in another thread.
619        slow_jobs.spawn("CHUNK_DROP", move || {
620            drop(deferred_updates);
621        });
622    }
623}