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                    if let Some(controller) = controller {
117                        controller.inputs.update_with_new(*inputs);
118                    }
119                }
120            },
121            ClientGeneral::ControlEvent(event) => {
122                if presence.kind.controlling_char()
123                    && let Some(controller) = controller
124                {
125                    // 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                    if let Some(controller) = controller {
137                        controller.push_action(event);
138                    }
139                }
140            },
141            ClientGeneral::PlayerPhysics {
142                pos,
143                vel,
144                ori,
145                force_counter,
146            } => {
147                if presence.kind.controlling_char()
148                    && force_update
149                        .is_none_or(|force_update| force_update.counter() == force_counter)
150                    && healths.get(entity).is_none_or(|h| !h.is_dead)
151                    && is_rider.get(entity).is_none()
152                    && is_volume_rider.get(entity).is_none()
153                    && !server_physics_forced
154                    && player_physics_setting
155                        .as_ref()
156                        .is_none_or(|s| !s.server_authoritative_physics_optin())
157                {
158                    *player_physics = Some((pos, vel, ori));
159                }
160            },
161            ClientGeneral::BreakBlock(pos) => {
162                if let Some(comp_can_build) = can_build.get(entity) {
163                    if comp_can_build.enabled {
164                        for area in comp_can_build.build_areas.iter() {
165                            if let Some(old_block) = build_areas
166                                .areas()
167                                .get(*area)
168                                // TODO: Make this an exclusive check on the upper bound of the AABB
169                                // Vek defaults to inclusive which is not optimal
170                                .filter(|aabb| aabb.contains_point(pos))
171                                .and_then(|_| terrain.get(pos).ok())
172                            {
173                                let new_block = old_block.into_vacant();
174                                // Take the rare writes lock as briefly as possible.
175                                let mut guard = rare_writes.lock();
176                                let _was_set =
177                                    guard.block_changes.try_set(pos, new_block).is_some();
178                                #[cfg(feature = "persistent_world")]
179                                if _was_set {
180                                    if let Some(terrain_persistence) =
181                                        guard._terrain_persistence.as_mut()
182                                    {
183                                        terrain_persistence.set_block(pos, new_block);
184                                    }
185                                }
186                            }
187                        }
188                    }
189                }
190            },
191            ClientGeneral::PlaceBlock(pos, new_block) => {
192                if let Some(comp_can_build) = can_build.get(entity) {
193                    if comp_can_build.enabled {
194                        for area in comp_can_build.build_areas.iter() {
195                            if build_areas
196                                .areas()
197                                .get(*area)
198                                // TODO: Make this an exclusive check on the upper bound of the AABB
199                                // Vek defaults to inclusive which is not optimal
200                                .filter(|aabb| aabb.contains_point(pos))
201                                .is_some()
202                            {
203                                // Take the rare writes lock as briefly as possible.
204                                let mut guard = rare_writes.lock();
205                                let _was_set =
206                                    guard.block_changes.try_set(pos, new_block).is_some();
207                                #[cfg(feature = "persistent_world")]
208                                if _was_set {
209                                    if let Some(terrain_persistence) =
210                                        guard._terrain_persistence.as_mut()
211                                    {
212                                        terrain_persistence.set_block(pos, new_block);
213                                    }
214                                }
215                            }
216                        }
217                    }
218                }
219            },
220            ClientGeneral::UnlockSkill(skill) => {
221                // FIXME: How do we want to handle the error?  Probably not by swallowing it.
222                let _ = skill_set
223                    .as_mut()
224                    .map(|skill_set| {
225                        SkillSet::unlock_skill_cow(skill_set, skill, |skill_set| skill_set.to_mut())
226                    })
227                    .transpose();
228            },
229            ClientGeneral::RequestSiteInfo(id) => {
230                emitters.emit(event::RequestSiteInfoEvent { entity, id });
231            },
232            ClientGeneral::RequestPlayerPhysics {
233                server_authoritative,
234            } => {
235                if let Some(setting) = player_physics_setting {
236                    setting.client_optin = server_authoritative;
237                }
238            },
239            ClientGeneral::RequestLossyTerrainCompression {
240                lossy_terrain_compression,
241            } => {
242                presence.lossy_terrain_compression = lossy_terrain_compression;
243            },
244            ClientGeneral::UpdateMapMarker(update) => {
245                emitters.emit(event::UpdateMapMarkerEvent { entity, update });
246            },
247            ClientGeneral::SpectatePosition(pos) => {
248                if let Some(admin) = maybe_admin
249                    && admin.0 >= AdminRole::Moderator
250                    && presence.kind == PresenceKind::Spectator
251                {
252                    if let Some(position) = position {
253                        position.0 = pos;
254                    }
255                }
256            },
257            ClientGeneral::SpectateEntity(uid) => {
258                if let Some(admin) = maybe_admin
259                    && admin.0 >= AdminRole::Moderator
260                {
261                    *spectating_entity = Some(uid);
262                }
263            },
264            ClientGeneral::SetBattleMode(battle_mode) => {
265                emitters.emit(event::SetBattleModeEvent {
266                    entity,
267                    battle_mode,
268                });
269            },
270            ClientGeneral::RequestCharacterList
271            | ClientGeneral::CreateCharacter { .. }
272            | ClientGeneral::EditCharacter { .. }
273            | ClientGeneral::DeleteCharacter(_)
274            | ClientGeneral::Character(_, _)
275            | ClientGeneral::Spectate(_)
276            | ClientGeneral::TerrainChunkRequest { .. }
277            | ClientGeneral::LodZoneRequest { .. }
278            | ClientGeneral::ChatMsg(_)
279            | ClientGeneral::Command(..)
280            | ClientGeneral::Terminate
281            | ClientGeneral::RequestPlugins(_) => {
282                debug!("Kicking possibly misbehaving client due to invalid client in game request");
283                emitters.emit(event::ClientDisconnectEvent(
284                    entity,
285                    common::comp::DisconnectReason::NetworkError,
286                ));
287            },
288        }
289        Ok(())
290    }
291}
292
293/// This system will handle new messages from clients
294#[derive(Default)]
295pub struct Sys;
296impl<'a> System<'a> for Sys {
297    type SystemData = (
298        Entities<'a>,
299        Events<'a>,
300        (
301            ReadExpect<'a, TerrainGrid>,
302            ReadExpect<'a, SlowJobPool>,
303            ReadExpect<'a, EditableSettings>,
304        ),
305        (
306            Read<'a, IdMaps>,
307            Read<'a, DeltaTime>,
308            Read<'a, Settings>,
309            Read<'a, AreasContainer<BuildArea>>,
310        ),
311        ReadStorage<'a, CanBuild>,
312        WriteStorage<'a, ForceUpdate>,
313        ReadStorage<'a, Is<Rider>>,
314        ReadStorage<'a, Is<VolumeRider>>,
315        WriteStorage<'a, SkillSet>,
316        ReadStorage<'a, Health>,
317        ReadStorage<'a, Body>,
318        ReadStorage<'a, Scale>,
319        Write<'a, BlockChange>,
320        WriteStorage<'a, Pos>,
321        WriteStorage<'a, Vel>,
322        WriteStorage<'a, Ori>,
323        WriteStorage<'a, Presence>,
324        WriteStorage<'a, Client>,
325        WriteStorage<'a, Controller>,
326        WriteStorage<'a, SpectatingEntity>,
327        Write<'a, PlayerPhysicsSettings>,
328        TerrainPersistenceData<'a>,
329        ReadStorage<'a, Player>,
330        ReadStorage<'a, Admin>,
331    );
332
333    const NAME: &'static str = "msg::in_game";
334    const ORIGIN: Origin = Origin::Server;
335    const PHASE: Phase = Phase::Create;
336
337    fn run(
338        _job: &mut Job<Self>,
339        (
340            entities,
341            events,
342            (terrain, slow_jobs, editable_settings),
343            (id_maps, dt, settings, build_areas),
344            can_build,
345            mut force_updates,
346            is_rider,
347            is_volume_rider,
348            mut skill_sets,
349            healths,
350            bodies,
351            scales,
352            mut block_changes,
353            mut positions,
354            mut velocities,
355            mut orientations,
356            mut presences,
357            mut clients,
358            mut controllers,
359            mut spectating_entities,
360            mut player_physics_settings_,
361            mut terrain_persistence,
362            players,
363            admins,
364        ): Self::SystemData,
365    ) {
366        let time_for_vd_changes = Instant::now();
367
368        // NOTE: stdlib mutex is more than good enough on Linux and (probably) Windows,
369        // but not Mac.
370        let rare_writes = parking_lot::Mutex::new(RareWrites {
371            block_changes: &mut block_changes,
372            _terrain_persistence: &mut terrain_persistence,
373        });
374
375        let player_physics_settings = &*player_physics_settings_;
376        let mut deferred_updates = (
377            &entities,
378            &mut clients,
379            (&mut presences).maybe(),
380            players.maybe(),
381            admins.maybe(),
382            (&skill_sets).maybe(),
383            (&mut positions).maybe(),
384            (&mut velocities).maybe(),
385            (&mut orientations).maybe(),
386            (&mut controllers).maybe(),
387            (&mut force_updates).maybe(),
388        )
389            .join()
390            // NOTE: Required because Specs has very poor work splitting for sparse joins.
391            .par_bridge()
392            .map_init(
393                || events.get_emitters(),
394                |emitters, (
395                    entity,
396                    client,
397                    mut maybe_presence,
398                    maybe_player,
399                    maybe_admin,
400                    skill_set,
401                    ref mut pos,
402                    ref mut vel,
403                    ref mut ori,
404                    ref mut controller,
405                    ref mut force_update,
406                )| {
407                    let old_player_physics_setting = maybe_player.map(|p| {
408                        player_physics_settings
409                            .settings
410                            .get(&p.uuid())
411                            .copied()
412                            .unwrap_or_default()
413                    });
414                    let mut new_player_physics_setting = old_player_physics_setting;
415                    let is_server_physics_forced = maybe_player.is_none_or(|p| editable_settings.server_physics_force_list.contains_key(&p.uuid()));
416                    // If an `ExitInGame` message is received this is set to `None` allowing further
417                    // ingame messages to be ignored.
418                    let mut clearable_maybe_presence = maybe_presence.as_deref_mut();
419                    let mut skill_set = skill_set.map(Cow::Borrowed);
420                    let mut player_physics = None;
421                    let mut spectating_entity = None;
422                    let _ = super::try_recv_all(client, 2, |client, msg| {
423                        Self::handle_client_in_game_msg(
424                            emitters,
425                            entity,
426                            client,
427                            &mut clearable_maybe_presence,
428                            &terrain,
429                            &can_build,
430                            &is_rider,
431                            &is_volume_rider,
432                            force_update.as_ref(),
433                            &mut skill_set,
434                            &healths,
435                            &rare_writes,
436                            pos.as_deref_mut(),
437                            &mut spectating_entity,
438                            controller.as_deref_mut(),
439                            &settings,
440                            &build_areas,
441                            new_player_physics_setting.as_mut(),
442                            is_server_physics_forced,
443                            &maybe_admin,
444                            time_for_vd_changes,
445                            msg,
446                            &mut player_physics,
447                        )
448                    });
449
450                    if let Some((new_pos, new_vel, new_ori)) = player_physics
451                        && let Some(old_pos) = pos.as_deref_mut()
452                        && let Some(old_vel) = vel.as_deref_mut()
453                        && let Some(old_ori) = ori.as_deref_mut()
454                    {
455                        enum Rejection {
456                            TooFar { old: Vec3<f32>, new: Vec3<f32> },
457                            TooFast { vel: Vec3<f32> },
458                            InsideTerrain,
459                        }
460
461                        let rejection = if maybe_admin.is_some() {
462                            None
463                        } else {
464                            // Reminder: review these frequently to ensure they're reasonable
465                            const MAX_H_VELOCITY: f32 = 75.0;
466                            const MAX_V_VELOCITY: std::ops::Range<f32> = -100.0..80.0;
467
468                            'rejection: {
469                                let is_velocity_ok = new_vel.0.xy().magnitude_squared() < MAX_H_VELOCITY.powi(2)
470                                    && MAX_V_VELOCITY.contains(&new_vel.0.z);
471
472                                if !is_velocity_ok {
473                                    break 'rejection Some(Rejection::TooFast { vel: new_vel.0 });
474                                }
475
476                                // How far the player is permitted to stray from the correct position (perhaps due to
477                                // latency problems).
478                                const POSITION_THRESHOLD: f32 = 16.0;
479
480                                // The position can either be sensible with respect to either the old or the new
481                                // velocity such that we don't punish for edge cases after a sudden change
482                                let is_position_ok = [old_vel.0, new_vel.0]
483                                    .into_iter()
484                                    .any(|ref_vel| {
485                                        let rpos = new_pos.0 - old_pos.0;
486                                        // Determine whether the change in position is broadly consistent with both
487                                        // the magnitude and direction of the velocity, with appropriate thresholds.
488                                        LineSegment3 {
489                                            start: Vec3::zero(),
490                                            end: ref_vel * dt.0,
491                                        }
492                                            .projected_point(rpos)
493                                            // + 1.5 accounts for minor changes in position without corresponding
494                                            // velocity like block hopping/snapping
495                                            .distance_squared(rpos) < (rpos.magnitude() * 0.5 + 1.5 + POSITION_THRESHOLD).powi(2)
496                                    });
497
498                                if !is_position_ok {
499                                    break 'rejection Some(Rejection::TooFar { old: old_pos.0, new: new_pos.0 });
500                                }
501
502                                // Checks that are only relevant if the position changed
503                                if new_pos.0 != old_pos.0 {
504                                    // Reject updates that would move the entity into terrain
505                                    let scale = scales.get(entity).map_or(1.0, |s| s.0);
506                                    let min_z = new_pos.0.z as i32;
507                                    let height = bodies.get(entity).map_or(0.0, |b| b.height()) * scale;
508                                    let head_pos_z = (new_pos.0.z + height) as i32;
509
510                                    if !(min_z..=head_pos_z).any(|z| {
511                                        let pos = new_pos.0.as_().with_z(z);
512
513                                        terrain
514                                            .get(pos)
515                                            .is_ok_and(|block| block.is_fluid())
516                                    }) {
517                                        break 'rejection Some(Rejection::InsideTerrain);
518                                    }
519                                }
520
521                                None
522                            }
523                        };
524
525                        if let Some(rejection) = rejection {
526                            // TODO: Log when false positives aren't generated often
527                            let alias = maybe_player.map(|p| &p.alias);
528                            match rejection {
529                                Rejection::TooFar { old, new } => warn!("Rejected physics for player {alias:?} (new position {new:?} is too far from old position {old:?})"),
530                                Rejection::TooFast { vel } => warn!("Rejected physics for player {alias:?} (new velocity {vel:?} is too fast)"),
531                                Rejection::InsideTerrain => warn!("Rejected physics for player {alias:?}: Inside terrain."),
532                            }
533
534                            /*
535                            // Perhaps this is overzealous?
536                            if let Some(mut setting) = new_player_physics_setting.as_mut() {
537                                setting.server_force = true;
538                                warn!("Switching player {alias:?} to server-side physics");
539                            }
540                            */
541
542                            // Reject the change and force the server's view of the physics state
543                            force_update.as_mut().map(|fu| fu.update());
544                        } else {
545                            *old_pos = new_pos;
546                            *old_vel = new_vel;
547                            *old_ori = new_ori;
548                        }
549                    }
550
551                    // Ensure deferred view distance changes are applied (if the
552                    // requsite time has elapsed).
553                    if let Some(presence) = maybe_presence {
554                        presence.terrain_view_distance.update(time_for_vd_changes);
555                        presence.entity_view_distance.update(time_for_vd_changes);
556                    }
557
558                    // Return the possibly modified skill set, and possibly modified server physics
559                    // settings.
560                    let skill_set_update = skill_set.and_then(|skill_set| match skill_set {
561                        Cow::Borrowed(_) => None,
562                        Cow::Owned(skill_set) => Some((entity, skill_set)),
563                    });
564                    // NOTE: Since we pass Option<&mut _> rather than &mut Option<_> to
565                    // handle_client_in_game_msg, and the new player was initialized to the same
566                    // value as the old setting , we know that either both the new and old setting
567                    // are Some, or they are both None.
568                    let physics_update = maybe_player.map(|p| p.uuid())
569                        .zip(new_player_physics_setting
570                             .filter(|_| old_player_physics_setting != new_player_physics_setting));
571                     let spectating_entity_update = spectating_entity.map(|e| (entity, e));
572                    (skill_set_update, spectating_entity_update, physics_update)
573                },
574            )
575            // NOTE: Would be nice to combine this with the map_init somehow, but I'm not sure if
576            // that's possible.
577            .filter(|(x, y, z)| x.is_some() || y.is_some() || z.is_some())
578            // NOTE: I feel like we shouldn't actually need to allocate here, but hopefully this
579            // doesn't turn out to be important as there shouldn't be that many connected clients.
580            // The reason we can't just use unzip is that the two sides might be different lengths.
581            .collect::<Vec<_>>();
582        let player_physics_settings = &mut *player_physics_settings_;
583        // Deferred updates to skillsets and player physics.
584        //
585        // NOTE: It is an invariant that there is at most one client entry per player
586        // uuid; since we joined on clients, it follows that there's just one update
587        // per uuid, so the physics update is sound and doesn't depend on evaluation
588        // order, even though we're not updating directly by entity or uid (note that
589        // for a given entity, we process messages serially).
590        deferred_updates.iter_mut().for_each(
591            |(skill_set_update, spectating_entity_update, physics_update)| {
592                if let Some((entity, new_skill_set)) = skill_set_update {
593                    // We know this exists, because we already iterated over it with the skillset
594                    // lock taken, so we can ignore the error.
595                    //
596                    // Note that we replace rather than just updating.  This is in order to avoid
597                    // dropping here; we'll drop later on a background thread, in case skillsets are
598                    // slow to drop.
599                    skill_sets
600                        .get_mut(*entity)
601                        .map(|mut old_skill_set| mem::swap(&mut *old_skill_set, new_skill_set));
602                }
603                if let &mut Some((entity, spectating_uid)) = spectating_entity_update {
604                    if let Some(uid) = spectating_uid
605                        && let Some(spectated_entity) = id_maps.uid_entity(uid)
606                    {
607                        // We know this exists, so can ignore the error.
608                        let _ =
609                            spectating_entities.insert(entity, SpectatingEntity(spectated_entity));
610                    } else {
611                        spectating_entities.remove(entity);
612                    }
613                }
614                if let &mut Some((uuid, player_physics_setting)) = physics_update {
615                    // We don't necessarily know this exists, but that's fine, because dropping
616                    // player physics is a no op.
617                    player_physics_settings
618                        .settings
619                        .insert(uuid, player_physics_setting);
620                }
621            },
622        );
623        // Finally, drop the deferred updates in another thread.
624        slow_jobs.spawn("CHUNK_DROP", move || {
625            drop(deferred_updates);
626        });
627    }
628}