veloren_voxygen/session/
mod.rs

1pub mod interactable;
2pub mod settings_change;
3mod target;
4
5use std::{cell::RefCell, collections::HashSet, rc::Rc, result::Result, time::Duration};
6
7use itertools::Itertools;
8#[cfg(not(target_os = "macos"))]
9use mumble_link::SharedLink;
10use ordered_float::OrderedFloat;
11use specs::WorldExt;
12use tracing::{error, info};
13use vek::*;
14
15use client::{self, Client};
16use common::{
17    CachedSpatialGrid,
18    comp::{
19        self, CharacterActivity, CharacterState, ChatType, Content, Fluid, InputKind,
20        InventoryUpdateEvent, Pos, PresenceKind, Stats, UtteranceKind, Vel,
21        inventory::slot::{EquipSlot, Slot},
22        invite::InviteKind,
23        item::{ItemDesc, tool::ToolKind},
24    },
25    consts::MAX_MOUNT_RANGE,
26    event::UpdateCharacterMetadata,
27    link::Is,
28    mounting::{Mount, VolumePos},
29    outcome::Outcome,
30    recipe::{self, RecipeBookManifest},
31    terrain::{Block, BlockKind},
32    trade::TradeResult,
33    util::{Dir, Plane},
34    vol::ReadVol,
35};
36use common_base::{prof_span, span};
37use common_net::{msg::server::InviteAnswer, sync::WorldSyncExt};
38
39use crate::{
40    Direction, GlobalState, PlayState, PlayStateResult,
41    audio::sfx::SfxEvent,
42    cmd::run_command,
43    error::Error,
44    game_input::GameInput,
45    hud::{
46        AutoPressBehavior, DebugInfo, Event as HudEvent, Hud, HudCollectFailedReason, HudInfo,
47        LootMessage, PersistedHudState, PromptDialogSettings,
48    },
49    key_state::KeyState,
50    menu::{char_selection::CharSelectionState, main::get_client_msg_error},
51    render::{Drawer, GlobalsBindGroup},
52    scene::{CameraMode, DebugShapeId, Scene, SceneData, camera},
53    session::target::ray_entities,
54    settings::Settings,
55    window::{AnalogGameInput, Event},
56};
57use hashbrown::HashMap;
58use interactable::{BlockInteraction, EntityInteraction, Interactable, get_interactables};
59use settings_change::Language::ChangeLanguage;
60use target::targets_under_cursor;
61#[cfg(feature = "egui-ui")]
62use voxygen_egui::EguiDebugInfo;
63
64/** The zoom scroll delta that is considered an "intent"
65    to zoom, rather than the accidental zooming that Zoom Lock
66    is supposed to help.
67    This is used for both [AutoPressBehaviors::Toggle] and [AutoPressBehaviors::Auto].
68
69    This value should likely differ between trackpad scrolling
70    and various mouse wheels, but we just choose a reasonable
71    default.
72
73    All the mice I have can only scroll at |delta|=15 no matter
74    how fast, I guess the default should be less than that so
75    it gets seen. This could possibly be a user setting changed
76    only in a config file; it's too minor to put in the GUI.
77    If a player reports that their scroll wheel is apparently not
78    working, this value may be to blame (i.e. their intent to scroll
79    is not being detected at a low enough scroll speed).
80*/
81const ZOOM_LOCK_SCROLL_DELTA_INTENT: f32 = 14.0;
82
83/// The action to perform after a tick
84enum TickAction {
85    // Continue executing
86    Continue,
87    // Disconnected (i.e. go to main menu)
88    Disconnect,
89}
90
91#[derive(Default)]
92pub struct PlayerDebugLines {
93    pub chunk_normal: Option<DebugShapeId>,
94    pub wind: Option<DebugShapeId>,
95    pub fluid_vel: Option<DebugShapeId>,
96    pub vel: Option<DebugShapeId>,
97}
98
99pub struct SessionState {
100    scene: Scene,
101    pub(crate) client: Rc<RefCell<Client>>,
102    metadata: UpdateCharacterMetadata,
103    pub(crate) hud: Hud,
104    key_state: KeyState,
105    inputs: comp::ControllerInputs,
106    inputs_state: HashSet<GameInput>,
107    selected_block: Block,
108    walk_forward_dir: Vec2<f32>,
109    walk_right_dir: Vec2<f32>,
110    free_look: bool,
111    freecam_pos: Vec3<f32>,
112    auto_walk: bool,
113    walking_speed: bool,
114    camera_clamp: bool,
115    zoom_lock: bool,
116    is_aiming: bool,
117    pub(crate) target_entity: Option<specs::Entity>,
118    pub(crate) selected_entity: Option<(specs::Entity, std::time::Instant)>,
119    pub(crate) viewpoint_entity: Option<specs::Entity>,
120    interactables: interactable::Interactables,
121    #[cfg(not(target_os = "macos"))]
122    mumble_link: SharedLink,
123    hitboxes: HashMap<specs::Entity, DebugShapeId>,
124    lines: PlayerDebugLines,
125    tracks: HashMap<Vec2<i32>, Vec<DebugShapeId>>,
126    gizmos: Vec<(DebugShapeId, common::resources::Time, bool)>,
127}
128
129/// Represents an active game session (i.e., the one being played).
130impl SessionState {
131    /// Create a new `SessionState`.
132    pub fn new(
133        global_state: &mut GlobalState,
134        metadata: UpdateCharacterMetadata,
135        client: Rc<RefCell<Client>>,
136        persisted_state: Rc<RefCell<PersistedHudState>>,
137    ) -> Self {
138        // Create a scene for this session. The scene handles visible elements of the
139        // game world.
140        let mut scene = Scene::new(
141            global_state.window.renderer_mut(),
142            &mut global_state.lazy_init,
143            &client.borrow(),
144            &global_state.settings,
145        );
146        scene
147            .camera_mut()
148            .set_fov_deg(global_state.settings.graphics.fov);
149        client
150            .borrow_mut()
151            .set_lod_distance(global_state.settings.graphics.lod_distance);
152        #[cfg(not(target_os = "macos"))]
153        let mut mumble_link = SharedLink::new("veloren", "veloren-voxygen");
154        {
155            let mut client = client.borrow_mut();
156            client.request_player_physics(global_state.settings.networking.player_physics_behavior);
157            client.request_lossy_terrain_compression(
158                global_state.settings.networking.lossy_terrain_compression,
159            );
160            #[cfg(not(target_os = "macos"))]
161            if let Some(uid) = client.uid() {
162                let identiy = if let Some(info) = client.player_list().get(&uid) {
163                    format!("{}-{}", info.player_alias, uid)
164                } else {
165                    format!("unknown-{}", uid)
166                };
167                mumble_link.set_identity(&identiy);
168                // TODO: evaluate context
169            }
170        }
171        let hud = Hud::new(global_state, persisted_state, &client.borrow());
172        let walk_forward_dir = scene.camera().forward_xy();
173        let walk_right_dir = scene.camera().right_xy();
174
175        Self {
176            scene,
177            client,
178            key_state: KeyState::default(),
179            inputs: comp::ControllerInputs::default(),
180            inputs_state: HashSet::new(),
181            hud,
182            selected_block: Block::new(BlockKind::Misc, Rgb::broadcast(255)),
183            walk_forward_dir,
184            walk_right_dir,
185            free_look: false,
186            freecam_pos: Vec3::zero(),
187            auto_walk: false,
188            walking_speed: false,
189            camera_clamp: false,
190            zoom_lock: false,
191            is_aiming: false,
192            target_entity: None,
193            selected_entity: None,
194            viewpoint_entity: None,
195            interactables: Default::default(),
196            #[cfg(not(target_os = "macos"))]
197            mumble_link,
198            hitboxes: HashMap::new(),
199            metadata,
200            tracks: HashMap::new(),
201            lines: Default::default(),
202            gizmos: Vec::new(),
203        }
204    }
205
206    fn stop_auto_walk(&mut self) {
207        self.auto_walk = false;
208        self.hud.auto_walk(false);
209        self.key_state.auto_walk = false;
210    }
211
212    /// Possibly lock the camera zoom depending on the current behaviour, and
213    /// the current inputs if in the Auto state.
214    fn maybe_auto_zoom_lock(
215        &mut self,
216        zoom_lock_enabled: bool,
217        zoom_lock_behavior: AutoPressBehavior,
218    ) {
219        if let AutoPressBehavior::Auto = zoom_lock_behavior {
220            // to add Analog detection, update the condition rhs with a check for
221            // MovementX/Y event from the last tick
222            self.zoom_lock = zoom_lock_enabled && self.should_auto_zoom_lock();
223        } else {
224            // it's intentional that the HUD notification is not shown in this case:
225            // refresh session from Settings HUD checkbox change
226            self.zoom_lock = zoom_lock_enabled;
227        }
228    }
229
230    /// Gets the entity that is the current viewpoint, and a bool if the client
231    /// is allowed to edit it's data.
232    fn viewpoint_entity(&self) -> (specs::Entity, bool) {
233        self.viewpoint_entity
234            .map(|e| (e, false))
235            .unwrap_or_else(|| (self.client.borrow().entity(), true))
236    }
237
238    fn controlling_char(&self) -> bool {
239        self.viewpoint_entity.is_none()
240            && self
241                .client
242                .borrow()
243                .presence()
244                .is_some_and(|p| p.controlling_char())
245    }
246
247    /// Tick the session (and the client attached to it).
248    fn tick(
249        &mut self,
250        dt: Duration,
251        global_state: &mut GlobalState,
252        outcomes: &mut Vec<Outcome>,
253    ) -> Result<TickAction, Error> {
254        span!(_guard, "tick", "Session::tick");
255
256        let mut client = self.client.borrow_mut();
257        self.scene.maintain_debug_hitboxes(
258            &client,
259            &global_state.settings,
260            &mut self.hitboxes,
261            &mut self.tracks,
262            &mut self.gizmos,
263        );
264        self.scene.maintain_debug_vectors(&client, &mut self.lines);
265        let pos = client.position().unwrap_or_default();
266
267        #[cfg(not(target_os = "macos"))]
268        {
269            // Update mumble positional audio
270            let ori = client
271                .state()
272                .read_storage::<comp::Ori>()
273                .get(client.entity())
274                .map_or_else(comp::Ori::default, |o| *o);
275            let front = ori.look_dir().to_vec();
276            let top = ori.up().to_vec();
277            // converting from veloren z = height axis, to mumble y = height axis
278            let player_pos = mumble_link::Position {
279                position: [pos.x, pos.z, pos.y],
280                front: [front.x, front.z, front.y],
281                top: [top.x, top.z, top.y],
282            };
283            self.mumble_link.update(player_pos, player_pos);
284        }
285
286        for event in client.tick(self.inputs.clone(), dt)? {
287            match event {
288                client::Event::Chat(m) => {
289                    self.hud.new_message(m);
290                },
291                client::Event::GroupInventoryUpdate(item, uid) => {
292                    self.hud.new_loot_message(LootMessage {
293                        amount: item.amount(),
294                        item,
295                        taken_by: uid,
296                    });
297                },
298                client::Event::InviteComplete {
299                    target,
300                    answer,
301                    kind,
302                } => {
303                    let target_name = match client.player_list().get(&target) {
304                        Some(info) => info.player_alias.clone(),
305                        None => match client.state().ecs().entity_from_uid(target) {
306                            Some(entity) => {
307                                let stats = client.state().read_storage::<Stats>();
308                                stats
309                                    .get(entity)
310                                    .map_or(format!("<entity {}>", target), |e| {
311                                        global_state.i18n.read().get_content(&e.name)
312                                    })
313                            },
314                            None => format!("<uid {}>", target),
315                        },
316                    };
317
318                    let msg_key = match (kind, answer) {
319                        (InviteKind::Group, InviteAnswer::Accepted) => "hud-group-invite-accepted",
320                        (InviteKind::Group, InviteAnswer::Declined) => "hud-group-invite-declined",
321                        (InviteKind::Group, InviteAnswer::TimedOut) => "hud-group-invite-timed_out",
322                        (InviteKind::Trade, InviteAnswer::Accepted) => "hud-trade-invite-accepted",
323                        (InviteKind::Trade, InviteAnswer::Declined) => "hud-trade-invite-declined",
324                        (InviteKind::Trade, InviteAnswer::TimedOut) => "hud-trade-invite-timed_out",
325                    };
326
327                    let msg = global_state
328                        .i18n
329                        .read()
330                        .get_msg_ctx(msg_key, &i18n::fluent_args! { "target" => target_name });
331
332                    self.hud.new_message(ChatType::Meta.into_plain_msg(msg));
333                },
334                client::Event::TradeComplete { result, trade: _ } => {
335                    self.hud.clear_cursor();
336                    self.hud
337                        .new_message(ChatType::Meta.into_msg(Content::localized(match result {
338                            TradeResult::Completed => "hud-trade-result-completed",
339                            TradeResult::Declined => "hud-trade-result-declined",
340                            TradeResult::NotEnoughSpace => "hud-trade-result-nospace",
341                        })));
342                },
343                client::Event::InventoryUpdated(inv_events) => {
344                    let sfx_triggers = self.scene.sfx_mgr.triggers.read();
345
346                    for inv_event in inv_events {
347                        let sfx_trigger_item =
348                            sfx_triggers.0.get_key_value(&SfxEvent::from(&inv_event));
349
350                        global_state.audio.emit_ui_sfx(sfx_trigger_item, None, None);
351
352                        match inv_event {
353                            InventoryUpdateEvent::BlockCollectFailed { pos, reason } => {
354                                self.hud.add_failed_block_pickup(
355                                    // TODO: Possibly support volumes.
356                                    VolumePos::terrain(pos),
357                                    HudCollectFailedReason::from_server_reason(
358                                        &reason,
359                                        client.state().ecs(),
360                                    ),
361                                );
362                            },
363                            InventoryUpdateEvent::EntityCollectFailed {
364                                entity: uid,
365                                reason,
366                            } => {
367                                if let Some(entity) = client.state().ecs().entity_from_uid(uid) {
368                                    self.hud.add_failed_entity_pickup(
369                                        entity,
370                                        HudCollectFailedReason::from_server_reason(
371                                            &reason,
372                                            client.state().ecs(),
373                                        ),
374                                    );
375                                }
376                            },
377                            InventoryUpdateEvent::Collected(item) => {
378                                global_state.profile.tutorial.event_collect();
379                                self.hud.new_loot_message(LootMessage {
380                                    amount: item.amount(),
381                                    item,
382                                    taken_by: client.uid().expect("Client doesn't have a Uid!!!"),
383                                });
384                            },
385                            _ => {},
386                        };
387                    }
388                },
389                client::Event::Dialogue(sender_uid, dialogue) => {
390                    if let Some(sender) = client.state().ecs().entity_from_uid(sender_uid) {
391                        self.hud.dialogue(sender, pos, dialogue, global_state);
392                    }
393                },
394                client::Event::Disconnect => return Ok(TickAction::Disconnect),
395                client::Event::DisconnectionNotification(time) => {
396                    self.hud
397                        .new_message(ChatType::CommandError.into_msg(match time {
398                            0 => Content::localized("hud-chat-goodbye"),
399                            _ => Content::localized_with_args("hud-chat-connection_lost", [(
400                                "time", time,
401                            )]),
402                        }));
403                },
404                client::Event::Notification(n) => {
405                    global_state.profile.tutorial.event_notification(&n);
406                    self.hud.new_notification(n);
407                },
408                client::Event::SetViewDistance(_vd) => {},
409                client::Event::Outcome(outcome) => {
410                    global_state
411                        .profile
412                        .tutorial
413                        .event_outcome(&client, &outcome);
414                    outcomes.push(outcome);
415                },
416                client::Event::CharacterCreated(_) => {},
417                client::Event::CharacterEdited(_) => {},
418                client::Event::CharacterError(_) => {},
419                client::Event::CharacterJoined(_) => {
420                    self.scene.music_mgr.reset_track(&mut global_state.audio);
421                },
422                client::Event::MapMarker(event) => {
423                    self.hud
424                        .persisted_state
425                        .borrow_mut()
426                        .location_markers
427                        .update(event);
428                },
429                client::Event::StartSpectate(spawn_point) => {
430                    let server_name = &client.server_info().name;
431                    let spawn_point = global_state
432                        .profile
433                        .get_spectate_position(server_name)
434                        .unwrap_or(spawn_point);
435
436                    client
437                        .state()
438                        .ecs()
439                        .write_storage()
440                        .insert(client.entity(), Pos(spawn_point))
441                        .expect("This shouldn't exist");
442
443                    self.scene.camera_mut().force_focus_pos(spawn_point);
444                },
445                client::Event::SpectatePosition(pos) => {
446                    self.scene.camera_mut().force_focus_pos(pos);
447                },
448                client::Event::PluginDataReceived(data) => {
449                    tracing::warn!("Received plugin data at wrong time {}", data.len());
450                },
451                client::Event::Gizmos(gizmos) => {
452                    self.gizmos.retain(|gizmos| {
453                        let keep = gizmos.2;
454                        if !keep {
455                            self.scene.debug.remove_shape(gizmos.0);
456                        }
457                        keep
458                    });
459                    for gizmos in gizmos {
460                        let mut add_shape = |shape, pos: Vec3<f32>| {
461                            let id = self.scene.debug.add_shape(shape);
462                            self.scene.debug.set_context(
463                                id,
464                                pos.with_w(0.0).into_array(),
465                                gizmos.color.map(|c| c as f32 / 255.0).into_array(),
466                                [0.0, 0.0, 0.0, 1.0],
467                            );
468                            self.gizmos.push((
469                                id,
470                                gizmos.end_time.unwrap_or(common::resources::Time(
471                                    client.state().get_time() + 1.0,
472                                )),
473                                gizmos.end_time.is_some(),
474                            ));
475                        };
476                        match gizmos.shape {
477                            comp::gizmos::Shape::Sphere(sphere) => {
478                                add_shape(
479                                    crate::scene::DebugShape::CapsulePrism {
480                                        p0: Vec2::zero(),
481                                        p1: Vec2::zero(),
482                                        radius: sphere.radius,
483                                        // Well, we need to put something in
484                                        // there...
485                                        head_ratio: 1.0,
486                                        height: sphere.radius * 2.0,
487                                    },
488                                    sphere.center,
489                                );
490                            },
491                            comp::gizmos::Shape::LineStrip(lines) => {
492                                for (a, b) in lines.into_iter().tuple_windows::<(_, _)>() {
493                                    add_shape(
494                                        crate::scene::DebugShape::Line([Vec3::zero(), b - a], 0.1),
495                                        a,
496                                    );
497                                }
498                            },
499                        }
500                    }
501                },
502            }
503        }
504
505        Ok(TickAction::Continue)
506    }
507
508    /// Clean up the session (and the client attached to it) after a tick.
509    pub fn cleanup(&mut self) { self.client.borrow_mut().cleanup(); }
510
511    fn should_auto_zoom_lock(&self) -> bool {
512        let inputs_state = &self.inputs_state;
513        for input in inputs_state {
514            match input {
515                GameInput::Primary
516                | GameInput::Secondary
517                | GameInput::Block
518                | GameInput::MoveForward
519                | GameInput::MoveLeft
520                | GameInput::MoveRight
521                | GameInput::MoveBack
522                | GameInput::Jump
523                | GameInput::WallJump
524                | GameInput::Roll
525                | GameInput::Sneak
526                | GameInput::AutoWalk
527                | GameInput::SwimUp
528                | GameInput::SwimDown
529                | GameInput::SwapLoadout
530                | GameInput::ToggleWield
531                | GameInput::Slot1
532                | GameInput::Slot2
533                | GameInput::Slot3
534                | GameInput::Slot4
535                | GameInput::Slot5
536                | GameInput::Slot6
537                | GameInput::Slot7
538                | GameInput::Slot8
539                | GameInput::Slot9
540                | GameInput::Slot10
541                | GameInput::CurrentSlot
542                | GameInput::SpectateViewpoint
543                | GameInput::SpectateSpeedBoost => return true,
544                _ => (),
545            }
546        }
547        false
548    }
549}
550
551impl PlayState for SessionState {
552    fn enter(&mut self, global_state: &mut GlobalState, _: Direction) {
553        // Trap the cursor.
554        global_state.window.grab_cursor(true);
555
556        self.client.borrow_mut().clear_terrain();
557
558        // Send startup commands to the server
559        if global_state.settings.send_logon_commands {
560            for cmd in &global_state.settings.logon_commands {
561                self.client.borrow_mut().send_chat(cmd.to_string());
562            }
563        }
564
565        #[cfg(feature = "discord")]
566        {
567            // Update the Discord activity on client initialization
568            #[cfg(feature = "singleplayer")]
569            let singleplayer = global_state.singleplayer.is_running();
570            #[cfg(not(feature = "singleplayer"))]
571            let singleplayer = false;
572
573            if singleplayer {
574                global_state.discord.join_singleplayer();
575            } else {
576                global_state
577                    .discord
578                    .join_server(self.client.borrow().server_info().name.clone());
579            }
580        }
581    }
582
583    fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
584        span!(_guard, "tick", "<Session as PlayState>::tick");
585        // TODO: let mut client = self.client.borrow_mut();
586        // TODO: can this be a method on the session or are there borrowcheck issues?
587        let (client_presence, client_type, client_registered) = {
588            let client = self.client.borrow();
589            (
590                client.presence(),
591                *client.client_type(),
592                client.registered(),
593            )
594        };
595
596        if let Some(presence) = client_presence {
597            let camera = self.scene.camera_mut();
598
599            // Clamp camera's vertical angle if the toggle is enabled
600            if self.camera_clamp {
601                let mut cam_dir = camera.get_orientation();
602                let cam_dir_clamp =
603                    (global_state.settings.gameplay.camera_clamp_angle as f32).to_radians();
604                cam_dir.y = cam_dir.y.clamp(-cam_dir_clamp, cam_dir_clamp);
605                camera.set_orientation(cam_dir);
606            }
607
608            let client = self.client.borrow();
609            let player_entity = client.entity();
610
611            let dt = global_state.clock.get_stable_dt().as_secs_f32();
612
613            #[cfg(feature = "discord")]
614            if global_state.discord.is_active()
615                && let Some(chunk) = client.current_chunk()
616                && let Some(location_name) = chunk.meta().name()
617            {
618                global_state
619                    .discord
620                    .update_location(location_name, client.current_site());
621            }
622
623            if global_state.settings.gameplay.bow_zoom {
624                let mut fov_scaling = 1.0;
625                if let Some(comp::CharacterState::ChargedRanged(cr)) = client
626                    .state()
627                    .read_storage::<comp::CharacterState>()
628                    .get(player_entity)
629                    && cr.charge_frac() > 0.5
630                {
631                    fov_scaling -= 3.0 * cr.charge_frac() / 5.0;
632                }
633                camera.set_fixate(fov_scaling);
634            } else {
635                camera.set_fixate(1.0);
636            }
637
638            // Compute camera data
639            camera.compute_dependents(&client.state().terrain());
640            let camera::Dependents {
641                cam_pos, cam_dir, ..
642            } = self.scene.camera().dependents();
643            let focus_pos = self.scene.camera().get_focus_pos();
644            let focus_off = focus_pos.map(|e| e.trunc());
645            let cam_pos = cam_pos + focus_off;
646
647            let (is_aiming, aim_dir_offset) = {
648                let is_aiming = client
649                    .state()
650                    .read_storage::<comp::CharacterState>()
651                    .get(player_entity)
652                    .map(|cs| cs.is_wield())
653                    .unwrap_or(false);
654
655                (
656                    is_aiming,
657                    if is_aiming && self.scene.camera().get_mode() == CameraMode::ThirdPerson {
658                        Vec3::unit_z() * 0.025
659                    } else {
660                        Vec3::zero()
661                    },
662                )
663            };
664            self.is_aiming = is_aiming;
665
666            let can_build = client
667                .state()
668                .read_storage::<comp::CanBuild>()
669                .get(player_entity)
670                .map_or_else(|| false, |cb| cb.enabled);
671
672            let active_mine_tool: Option<ToolKind> = if client.is_wielding() == Some(true) {
673                client
674                    .inventories()
675                    .get(player_entity)
676                    .and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
677                    .and_then(|item| item.tool_info())
678                    .filter(|tool_kind| matches!(tool_kind, ToolKind::Pick | ToolKind::Shovel))
679            } else {
680                None
681            };
682
683            // Check to see whether we're aiming at anything
684            let (build_target, collect_target, entity_target, mine_target, terrain_target) =
685                targets_under_cursor(
686                    &client,
687                    cam_pos,
688                    cam_dir,
689                    can_build,
690                    active_mine_tool,
691                    self.viewpoint_entity().0,
692                );
693
694            match get_interactables(
695                &client,
696                collect_target,
697                entity_target,
698                mine_target,
699                terrain_target,
700                &self.scene,
701            ) {
702                Ok(input_map) => {
703                    for (_, inter) in input_map.values() {
704                        global_state.profile.tutorial.event_find_interactable(inter);
705                    }
706
707                    let entities = input_map
708                        .values()
709                        .filter_map(|(_, interactable)| {
710                            if let Interactable::Entity { entity, .. } = interactable {
711                                Some(*entity)
712                            } else {
713                                None
714                            }
715                        })
716                        .collect::<HashSet<_>>();
717                    self.interactables = interactable::Interactables {
718                        input_map,
719                        entities,
720                    };
721                },
722                Err(error) => {
723                    tracing::trace!(?error, "Getting interactables failed");
724                    self.interactables = Default::default()
725                },
726            }
727
728            drop(client);
729
730            self.maybe_auto_zoom_lock(
731                global_state.settings.gameplay.zoom_lock,
732                global_state.settings.gameplay.zoom_lock_behavior,
733            );
734
735            if presence == PresenceKind::Spectator {
736                let mut client = self.client.borrow_mut();
737                if client.spectate_position(cam_pos) {
738                    let server_name = &client.server_info().name;
739                    global_state.profile.set_spectate_position(
740                        server_name,
741                        Some(self.scene.camera().get_focus_pos()),
742                    );
743                }
744            }
745
746            // Nearest block to consider with GameInput primary or secondary key.
747            let nearest_block_dist = find_shortest_distance(&[
748                mine_target
749                    .filter(|_| active_mine_tool.is_some())
750                    .map(|t| t.distance),
751                build_target.filter(|_| can_build).map(|t| t.distance),
752            ]);
753            // Nearest block to be highlighted in the scene (self.scene.set_select_pos).
754            let nearest_scene_dist = find_shortest_distance(&[
755                nearest_block_dist,
756                collect_target
757                    .filter(|_| active_mine_tool.is_none())
758                    .map(|t| t.distance),
759            ]);
760            // Set break_block_pos only if mining is closest.
761            self.inputs.break_block_pos = if let Some(mt) = mine_target
762                .filter(|mt| active_mine_tool.is_some() && nearest_scene_dist == Some(mt.distance))
763            {
764                self.scene.set_select_pos(Some(mt.position_int()));
765                Some(mt.position)
766            } else if let Some(bt) =
767                build_target.filter(|bt| can_build && nearest_scene_dist == Some(bt.distance))
768            {
769                self.scene.set_select_pos(Some(bt.position_int()));
770                None
771            } else if let Some(ct) =
772                collect_target.filter(|ct| nearest_scene_dist == Some(ct.distance))
773            {
774                self.scene.set_select_pos(Some(ct.position_int()));
775                None
776            } else {
777                self.scene.set_select_pos(None);
778                None
779            };
780
781            // filled block in line of sight
782            let default_select_pos = terrain_target.map(|tt| tt.position);
783
784            // Throw out distance info, it will be useful in the future
785            self.target_entity = entity_target.map(|t| t.kind.0);
786
787            let controlling_char = self.controlling_char();
788
789            // Handle window events.
790            for event in events {
791                // Pass all events to the ui first.
792                {
793                    let client = self.client.borrow();
794                    let inventories = client.inventories();
795                    let inventory = inventories.get(client.entity());
796                    if self
797                        .hud
798                        .handle_event(event.clone(), global_state, inventory)
799                    {
800                        continue;
801                    }
802                }
803                match event {
804                    Event::Close => {
805                        return PlayStateResult::Shutdown;
806                    },
807                    Event::InputUpdate(input, state)
808                        if state != self.inputs_state.contains(&input) =>
809                    {
810                        if !self.inputs_state.insert(input) {
811                            self.inputs_state.remove(&input);
812                        }
813                        match input {
814                            GameInput::Primary => {
815                                self.walking_speed = false;
816                                let mut client = self.client.borrow_mut();
817                                // Mine and build targets can be the same block. make building
818                                // take precedence.
819                                // Order of precedence: build, then mining, then attack.
820                                if let Some(build_target) = build_target.filter(|bt| {
821                                    state && can_build && nearest_block_dist == Some(bt.distance)
822                                }) {
823                                    client.remove_block(build_target.position_int());
824                                } else {
825                                    client.handle_input(
826                                        InputKind::Primary,
827                                        state,
828                                        default_select_pos,
829                                        self.target_entity,
830                                    );
831                                }
832                            },
833                            GameInput::Secondary => {
834                                self.walking_speed = false;
835                                let mut client = self.client.borrow_mut();
836                                if let Some(build_target) = build_target.filter(|bt| {
837                                    state && can_build && nearest_block_dist == Some(bt.distance)
838                                }) {
839                                    let selected_pos = build_target.kind.0;
840                                    client.place_block(
841                                        selected_pos.map(|p| p.floor() as i32),
842                                        self.selected_block,
843                                    );
844                                } else {
845                                    client.handle_input(
846                                        InputKind::Secondary,
847                                        state,
848                                        default_select_pos,
849                                        self.target_entity,
850                                    );
851                                }
852                            },
853                            GameInput::Block => {
854                                self.walking_speed = false;
855                                self.client.borrow_mut().handle_input(
856                                    InputKind::Block,
857                                    state,
858                                    default_select_pos,
859                                    self.target_entity,
860                                );
861                            },
862                            GameInput::Roll => {
863                                self.walking_speed = false;
864                                let mut client = self.client.borrow_mut();
865                                if can_build {
866                                    if state
867                                        && let Some(block) = build_target.and_then(|bt| {
868                                            client
869                                                .state()
870                                                .terrain()
871                                                .get(bt.position_int())
872                                                .ok()
873                                                .copied()
874                                        })
875                                    {
876                                        self.selected_block = block;
877                                    }
878                                } else if controlling_char {
879                                    global_state.profile.tutorial.event_roll();
880                                    client.handle_input(
881                                        InputKind::Roll,
882                                        state,
883                                        default_select_pos,
884                                        self.target_entity,
885                                    );
886                                }
887                            },
888                            GameInput::GiveUp => {
889                                self.key_state.give_up = state.then_some(0.0).filter(|_| {
890                                    let client = self.client.borrow();
891                                    comp::is_downed(
892                                        client.current().as_ref(),
893                                        client.current().as_ref(),
894                                    )
895                                });
896                            },
897                            GameInput::Respawn => {
898                                self.walking_speed = false;
899                                self.stop_auto_walk();
900                                if state && self.client.borrow_mut().respawn() {
901                                    global_state.profile.tutorial.event_respawn();
902                                }
903                            },
904                            GameInput::Jump => {
905                                self.walking_speed = false;
906                                global_state.profile.tutorial.event_jump();
907                                self.client.borrow_mut().handle_input(
908                                    InputKind::Jump,
909                                    state,
910                                    default_select_pos,
911                                    self.target_entity,
912                                );
913                            },
914                            GameInput::WallJump => {
915                                self.walking_speed = false;
916                                self.client.borrow_mut().handle_input(
917                                    InputKind::WallJump,
918                                    state,
919                                    default_select_pos,
920                                    self.target_entity,
921                                );
922                            },
923                            GameInput::SwimUp => {
924                                self.key_state.swim_up = state;
925                            },
926                            GameInput::SwimDown => {
927                                self.key_state.swim_down = state;
928                            },
929                            GameInput::Sit => {
930                                if state && controlling_char {
931                                    self.stop_auto_walk();
932                                    self.client.borrow_mut().toggle_sit();
933                                }
934                            },
935                            GameInput::Crawl => {
936                                if state && controlling_char {
937                                    self.stop_auto_walk();
938                                    self.client.borrow_mut().toggle_crawl();
939                                }
940                            },
941                            GameInput::Dance => {
942                                if state && controlling_char {
943                                    self.stop_auto_walk();
944                                    self.client.borrow_mut().toggle_dance();
945                                }
946                            },
947                            GameInput::Greet => {
948                                if state {
949                                    self.client.borrow_mut().utter(UtteranceKind::Greeting);
950                                }
951                            },
952                            GameInput::Sneak => {
953                                let is_trading = self.client.borrow().is_trading();
954                                if state && !is_trading && controlling_char {
955                                    self.stop_auto_walk();
956                                    self.client.borrow_mut().toggle_sneak();
957                                }
958                            },
959                            GameInput::CancelClimb => {
960                                if state && controlling_char {
961                                    self.client.borrow_mut().cancel_climb();
962                                }
963                            },
964                            GameInput::MoveForward => {
965                                if state && global_state.settings.gameplay.stop_auto_walk_on_input {
966                                    self.stop_auto_walk();
967                                }
968                                self.key_state.up = state
969                            },
970                            GameInput::MoveBack => {
971                                if state && global_state.settings.gameplay.stop_auto_walk_on_input {
972                                    self.stop_auto_walk();
973                                }
974                                self.key_state.down = state
975                            },
976                            GameInput::MoveLeft => {
977                                if state && global_state.settings.gameplay.stop_auto_walk_on_input {
978                                    self.stop_auto_walk();
979                                }
980                                self.key_state.left = state
981                            },
982                            GameInput::MoveRight => {
983                                if state && global_state.settings.gameplay.stop_auto_walk_on_input {
984                                    self.stop_auto_walk();
985                                }
986                                self.key_state.right = state
987                            },
988                            GameInput::Glide => {
989                                self.walking_speed = false;
990                                let is_trading = self.client.borrow().is_trading();
991                                if state && !is_trading && controlling_char {
992                                    if global_state.settings.gameplay.stop_auto_walk_on_input {
993                                        self.stop_auto_walk();
994                                    }
995                                    self.client.borrow_mut().toggle_glide();
996                                    global_state.profile.tutorial.event_open_glider();
997                                }
998                            },
999                            GameInput::Fly => {
1000                                // Not sure where to put comment, but I noticed
1001                                // when testing flight.
1002                                //
1003                                // Syncing of inputs between mounter and mountee
1004                                // broke with controller change
1005                                self.key_state.fly ^= state;
1006                                self.client.borrow_mut().handle_input(
1007                                    InputKind::Fly,
1008                                    self.key_state.fly,
1009                                    default_select_pos,
1010                                    self.target_entity,
1011                                );
1012                            },
1013                            GameInput::ToggleWield => {
1014                                if state && controlling_char {
1015                                    let mut client = self.client.borrow_mut();
1016                                    if client.is_wielding().is_some_and(|b| !b) {
1017                                        self.walking_speed = false;
1018                                    }
1019                                    client.toggle_wield();
1020                                }
1021                            },
1022                            GameInput::SwapLoadout => {
1023                                if state && controlling_char {
1024                                    self.client.borrow_mut().swap_loadout();
1025                                }
1026                            },
1027                            GameInput::ToggleLantern if state && controlling_char => {
1028                                let mut client = self.client.borrow_mut();
1029                                if client.is_lantern_enabled() {
1030                                    client.disable_lantern();
1031                                } else {
1032                                    global_state.profile.tutorial.event_lantern();
1033                                    client.enable_lantern();
1034                                }
1035                            },
1036                            GameInput::Mount if state && controlling_char => {
1037                                let mut client = self.client.borrow_mut();
1038                                if client.is_riding() {
1039                                    client.unmount();
1040                                } else if let Some((_, interactable)) =
1041                                    self.interactables.input_map.get(&GameInput::Mount)
1042                                {
1043                                    match interactable {
1044                                        Interactable::Block { volume_pos, .. } => {
1045                                            client.mount_volume(*volume_pos)
1046                                        },
1047                                        Interactable::Entity { entity, .. } => {
1048                                            client.mount(*entity)
1049                                        },
1050                                    }
1051                                }
1052                            },
1053                            GameInput::StayFollow if state => {
1054                                let mut client = self.client.borrow_mut();
1055                                let player_pos = client
1056                                    .state()
1057                                    .read_storage::<Pos>()
1058                                    .get(client.entity())
1059                                    .copied();
1060
1061                                let mut close_pet = None;
1062                                if let Some(player_pos) = player_pos {
1063                                    let positions = client.state().read_storage::<Pos>();
1064                                    close_pet = client.state().ecs().read_resource::<CachedSpatialGrid>().0
1065                                        .in_circle_aabr(player_pos.0.xy(), MAX_MOUNT_RANGE)
1066                                        .filter(|e|
1067                                            *e != client.entity()
1068                                        )
1069                                        .filter(|e|
1070                                            matches!(client.state().ecs().read_storage::<comp::Alignment>().get(*e),
1071                                                Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid())
1072                                        )
1073                                        .filter(|e|
1074                                            client.state().ecs().read_storage::<Is<Mount>>().get(*e).is_none()
1075                                        )
1076                                        .min_by_key(|e| {
1077                                            OrderedFloat(positions
1078                                                .get(*e)
1079                                                .map_or(MAX_MOUNT_RANGE * MAX_MOUNT_RANGE, |x| {
1080                                                    player_pos.0.distance_squared(x.0)
1081                                                }
1082                                            ))
1083                                        });
1084                                }
1085                                if let Some(pet_entity) = close_pet
1086                                    && client
1087                                        .state()
1088                                        .read_storage::<Is<Mount>>()
1089                                        .get(pet_entity)
1090                                        .is_none()
1091                                {
1092                                    let is_staying = client
1093                                        .state()
1094                                        .read_storage::<CharacterActivity>()
1095                                        .get(pet_entity)
1096                                        .is_some_and(|activity| activity.is_pet_staying);
1097                                    client.set_pet_stay(pet_entity, !is_staying);
1098                                }
1099                            },
1100                            GameInput::Interact => {
1101                                if state {
1102                                    let mut client = self.client.borrow_mut();
1103                                    if let Some((_, interactable)) =
1104                                        self.interactables.input_map.get(&GameInput::Interact)
1105                                    {
1106                                        match interactable {
1107                                            Interactable::Block {
1108                                                volume_pos,
1109                                                block,
1110                                                interaction,
1111                                                ..
1112                                            } => {
1113                                                match interaction {
1114                                                    BlockInteraction::Collect { .. }
1115                                                    | BlockInteraction::Unlock { .. } => {
1116                                                        if block.is_directly_collectible() {
1117                                                            match volume_pos.kind {
1118                                                                common::mounting::Volume::Terrain => {
1119                                                                    client.collect_block(volume_pos.pos);
1120                                                                }
1121                                                                common::mounting::Volume::Entity(_) => {
1122                                                                    // TODO: Do we want to implement this?
1123                                                                },
1124                                                            }
1125                                                        }
1126                                                    },
1127                                                    BlockInteraction::Craft(tab) => {
1128                                                        self.hud.show.open_crafting_tab(
1129                                                            *tab,
1130                                                            block
1131                                                                .get_sprite()
1132                                                                .map(|s| (*volume_pos, s)),
1133                                                        )
1134                                                    },
1135                                                    BlockInteraction::Mine(_)
1136                                                    | BlockInteraction::Mount => {},
1137                                                    BlockInteraction::Read(content) => {
1138                                                        match volume_pos.kind {
1139                                                            common::mounting::Volume::Terrain => {
1140                                                                self.hud.show_content_bubble(
1141                                                                    volume_pos.pos.as_()
1142                                                                        + Vec3::new(
1143                                                                            0.5,
1144                                                                            0.5,
1145                                                                            block.solid_height()
1146                                                                                * 0.75,
1147                                                                        ),
1148                                                                    content.clone(),
1149                                                                )
1150                                                            },
1151                                                            // Signs on volume entities are not
1152                                                            // currently supported
1153                                                            common::mounting::Volume::Entity(_) => {
1154                                                            },
1155                                                        }
1156                                                    },
1157                                                    BlockInteraction::LightToggle(enable) => {
1158                                                        client.toggle_sprite_light(
1159                                                            *volume_pos,
1160                                                            *enable,
1161                                                        );
1162                                                    },
1163                                                }
1164                                            },
1165                                            Interactable::Entity {
1166                                                entity,
1167                                                interaction,
1168                                                ..
1169                                            } => {
1170                                                // NOTE: Keep this match exhaustive.
1171                                                match interaction {
1172                                                    EntityInteraction::HelpDowned => {
1173                                                        client.help_downed(*entity)
1174                                                    },
1175                                                    EntityInteraction::PickupItem => {
1176                                                        client.pick_up(*entity)
1177                                                    },
1178                                                    EntityInteraction::ActivatePortal => {
1179                                                        client.activate_portal(*entity)
1180                                                    },
1181                                                    EntityInteraction::Pet => {
1182                                                        client.do_pet(*entity)
1183                                                    },
1184                                                    EntityInteraction::Talk => {
1185                                                        client.npc_interact(*entity)
1186                                                    },
1187                                                    EntityInteraction::CampfireSit
1188                                                    | EntityInteraction::Trade
1189                                                    | EntityInteraction::StayFollow
1190                                                    | EntityInteraction::Mount => {},
1191                                                }
1192                                            },
1193                                        }
1194                                    }
1195                                }
1196                            },
1197                            GameInput::Trade => {
1198                                if state
1199                                    && controlling_char
1200                                    && let Some((_, Interactable::Entity { entity, .. })) =
1201                                        self.interactables.input_map.get(&GameInput::Trade)
1202                                {
1203                                    let mut client = self.client.borrow_mut();
1204                                    if let Some(uid) = client.state().ecs().uid_from_entity(*entity)
1205                                    {
1206                                        let name = client
1207                                            .player_list()
1208                                            .get(&uid)
1209                                            .map(|info| info.player_alias.clone())
1210                                            .unwrap_or_else(|| {
1211                                                let stats = client.state().read_storage::<Stats>();
1212                                                stats.get(*entity).map_or(
1213                                                    format!("<entity {:?}>", uid),
1214                                                    |e| {
1215                                                        global_state
1216                                                            .i18n
1217                                                            .read()
1218                                                            .get_content(&e.name)
1219                                                    },
1220                                                )
1221                                            });
1222
1223                                        self.hud.new_message(ChatType::Meta.into_msg(
1224                                            Content::localized_with_args(
1225                                                "hud-trade-invite_sent",
1226                                                [("playername", name)],
1227                                            ),
1228                                        ));
1229
1230                                        client.send_invite(uid, InviteKind::Trade)
1231                                    };
1232                                }
1233                            },
1234                            GameInput::FreeLook => {
1235                                let hud = &mut self.hud;
1236                                global_state.settings.gameplay.free_look_behavior.update(
1237                                    state,
1238                                    &mut self.free_look,
1239                                    |b| hud.free_look(b),
1240                                );
1241                                let camera = self.scene.camera_mut();
1242                                let ori = camera.get_orientation();
1243                                if self.free_look {
1244                                    self.freecam_pos = ori;
1245                                } else {
1246                                    camera.set_orientation_instant(self.freecam_pos);
1247                                }
1248                            },
1249                            GameInput::AutoWalk => {
1250                                let hud = &mut self.hud;
1251                                global_state.settings.gameplay.auto_walk_behavior.update(
1252                                    state,
1253                                    &mut self.auto_walk,
1254                                    |b| hud.auto_walk(b),
1255                                );
1256
1257                                self.key_state.auto_walk =
1258                                    self.auto_walk && !self.client.borrow().is_gliding();
1259                            },
1260                            GameInput::ZoomIn => {
1261                                if state {
1262                                    if self.zoom_lock {
1263                                        self.hud.zoom_lock_reminder();
1264                                    } else {
1265                                        self.scene.handle_input_event(
1266                                            Event::Zoom(-30.0),
1267                                            &self.client.borrow(),
1268                                        );
1269                                    }
1270                                }
1271                            },
1272                            GameInput::ZoomOut => {
1273                                if state {
1274                                    if self.zoom_lock {
1275                                        self.hud.zoom_lock_reminder();
1276                                    } else {
1277                                        self.scene.handle_input_event(
1278                                            Event::Zoom(30.0),
1279                                            &self.client.borrow(),
1280                                        );
1281                                    }
1282                                }
1283                            },
1284                            GameInput::ZoomLock => {
1285                                if state {
1286                                    global_state.settings.gameplay.zoom_lock ^= true;
1287
1288                                    self.hud
1289                                        .zoom_lock_toggle(global_state.settings.gameplay.zoom_lock);
1290                                }
1291                            },
1292                            GameInput::CameraClamp => {
1293                                let hud = &mut self.hud;
1294                                global_state.settings.gameplay.camera_clamp_behavior.update(
1295                                    state,
1296                                    &mut self.camera_clamp,
1297                                    |b| hud.camera_clamp(b),
1298                                );
1299                            },
1300                            GameInput::CycleCamera if state => {
1301                                // Prevent accessing camera modes which aren't available in
1302                                // multiplayer unless you are an
1303                                // admin. This is an easily bypassed clientside check.
1304                                // The server should do its own filtering of which entities are
1305                                // sent to clients to
1306                                // prevent abuse.
1307                                let camera = self.scene.camera_mut();
1308                                let client = self.client.borrow();
1309                                camera.next_mode(
1310                                    client.is_moderator(),
1311                                    (client.presence() != Some(PresenceKind::Spectator))
1312                                        || self.viewpoint_entity.is_some(),
1313                                );
1314                            },
1315                            GameInput::Select => {
1316                                if !state {
1317                                    self.selected_entity =
1318                                        self.target_entity.map(|e| (e, std::time::Instant::now()));
1319                                }
1320                            },
1321                            GameInput::AcceptGroupInvite if state => {
1322                                let mut client = self.client.borrow_mut();
1323                                if client.invite().is_some() {
1324                                    client.accept_invite();
1325                                }
1326                            },
1327                            GameInput::DeclineGroupInvite if state => {
1328                                let mut client = self.client.borrow_mut();
1329                                if client.invite().is_some() {
1330                                    client.decline_invite();
1331                                }
1332                            },
1333                            GameInput::SpectateViewpoint if state => {
1334                                let mut client = self.client.borrow_mut();
1335                                if self.viewpoint_entity.is_some() {
1336                                    client.stop_spectate_entity();
1337                                    self.viewpoint_entity = None;
1338                                    self.scene.camera_mut().set_mode(CameraMode::Freefly);
1339                                    let mut ori = self.scene.camera().get_orientation();
1340                                    // Remove any roll that could have possibly been set to the
1341                                    // camera as a result of spectating.
1342                                    ori.z = 0.0;
1343                                    self.scene.camera_mut().set_orientation(ori);
1344                                } else if let Some(target_entity) = entity_target
1345                                    && self.scene.camera().get_mode() == CameraMode::Freefly
1346                                {
1347                                    // Notify the server that we start spectating an entity so
1348                                    // we get viewpoint specific component packages.
1349                                    client.start_spectate_entity(target_entity.kind.0);
1350
1351                                    self.viewpoint_entity = Some(target_entity.kind.0);
1352                                    self.scene.camera_mut().set_mode(CameraMode::FirstPerson);
1353                                }
1354                            },
1355                            GameInput::ToggleWalk if state => {
1356                                global_state
1357                                    .settings
1358                                    .gameplay
1359                                    .walking_speed_behavior
1360                                    .update(state, &mut self.walking_speed, |_| {});
1361                            },
1362                            _ => {},
1363                        }
1364                    },
1365                    Event::AnalogGameInput(input) => match input {
1366                        AnalogGameInput::MovementX(v) => {
1367                            self.key_state.analog_matrix.x = v;
1368                        },
1369                        AnalogGameInput::MovementY(v) => {
1370                            self.key_state.analog_matrix.y = v;
1371                        },
1372                        other => {
1373                            self.scene.handle_input_event(
1374                                Event::AnalogGameInput(other),
1375                                &self.client.borrow(),
1376                            );
1377                        },
1378                    },
1379
1380                    // TODO: Localise
1381                    Event::ScreenshotMessage(screenshot_msg) => self
1382                        .hud
1383                        .new_message(ChatType::CommandInfo.into_plain_msg(screenshot_msg)),
1384
1385                    Event::Zoom(delta) if self.zoom_lock => {
1386                        // only fire this Hud event when player has "intent" to zoom
1387                        if delta.abs() > ZOOM_LOCK_SCROLL_DELTA_INTENT {
1388                            self.hud.zoom_lock_reminder();
1389                        }
1390                    },
1391
1392                    // Pass all other events to the scene
1393                    event => {
1394                        if let Event::Zoom(delta) = &event {
1395                            global_state.profile.tutorial.event_zoom(*delta);
1396                        }
1397
1398                        self.scene.handle_input_event(event, &self.client.borrow());
1399                    }, // TODO: Do something if the event wasn't handled?
1400                }
1401            }
1402
1403            // Talk to entities when we are in dialogue with them
1404            if let Some(tgt) = self.hud.current_dialogue() {
1405                let mut client = self.client.borrow_mut();
1406                client.do_talk(Some(tgt));
1407                // Turn to face the entity when in first-person mode
1408                if matches!(
1409                    self.scene.camera().get_mode(),
1410                    camera::CameraMode::FirstPerson
1411                ) && let Some(activity) = client
1412                    .state()
1413                    .read_storage::<CharacterActivity>()
1414                    .get(client.entity())
1415                    && let Some(dir) = activity.look_dir
1416                {
1417                    let ori = Vec3::new(dir.x.atan2(dir.y), -dir.z.atan(), 0.0);
1418                    self.scene.camera_mut().lerp_toward(ori, dt, 2.5);
1419                }
1420            }
1421
1422            if let Some(viewpoint_entity) = self.viewpoint_entity
1423                && !self
1424                    .client
1425                    .borrow()
1426                    .state()
1427                    .ecs()
1428                    .read_storage::<Pos>()
1429                    .contains(viewpoint_entity)
1430            {
1431                self.client.borrow_mut().stop_spectate_entity();
1432                self.viewpoint_entity = None;
1433                self.scene.camera_mut().set_mode(CameraMode::Freefly);
1434            }
1435
1436            let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
1437
1438            // Get the current state of movement related inputs
1439            let input_vec = self.key_state.dir_vec();
1440            if input_vec.magnitude_squared() > 0.5f32.powi(2) {
1441                global_state.profile.tutorial.event_move()
1442            }
1443            let (axis_right, axis_up) = (input_vec[0], input_vec[1]);
1444
1445            if let Some(ref mut timer) = self.key_state.give_up {
1446                *timer += dt;
1447
1448                if *timer > crate::key_state::GIVE_UP_HOLD_TIME {
1449                    self.client.borrow_mut().give_up();
1450                }
1451            }
1452
1453            if mutable_viewpoint {
1454                // If auto-gliding, point camera into the wind
1455                if let Some(dir) = self
1456                    .auto_walk
1457                    .then_some(self.client.borrow())
1458                    .filter(|client| client.is_gliding())
1459                    .and_then(|client| {
1460                        let ecs = client.state().ecs();
1461                        let entity = client.entity();
1462                        let fluid = ecs
1463                            .read_storage::<comp::PhysicsState>()
1464                            .get(entity)?
1465                            .in_fluid?;
1466                        let vel = *ecs.read_storage::<Vel>().get(entity)?;
1467                        let free_look = self.free_look;
1468                        let dir_forward_xy = self.scene.camera().forward_xy();
1469                        let dir_right = self.scene.camera().right();
1470
1471                        auto_glide(fluid, vel, free_look, dir_forward_xy, dir_right)
1472                    })
1473                {
1474                    self.key_state.auto_walk = false;
1475                    self.inputs.move_dir = Vec2::zero();
1476                    self.inputs.look_dir = dir;
1477                } else {
1478                    self.key_state.auto_walk = self.auto_walk;
1479                    if !self.free_look {
1480                        self.walk_forward_dir = self.scene.camera().forward_xy();
1481                        self.walk_right_dir = self.scene.camera().right_xy();
1482
1483                        let client = self.client.borrow();
1484
1485                        let holding_ranged = client
1486                            .inventories()
1487                            .get(player_entity)
1488                            .and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
1489                            .and_then(|item| item.tool_info())
1490                            .is_some_and(|tool_kind| {
1491                                matches!(
1492                                    tool_kind,
1493                                    ToolKind::Bow
1494                                        | ToolKind::Staff
1495                                        | ToolKind::Sceptre
1496                                        | ToolKind::Throwable
1497                                )
1498                            })
1499                            || client
1500                                .current::<CharacterState>()
1501                                .is_some_and(|char_state| {
1502                                    matches!(char_state, CharacterState::Throw(_))
1503                                });
1504
1505                        let dir = if is_aiming
1506                            && holding_ranged
1507                            && self.scene.camera().get_mode() == CameraMode::ThirdPerson
1508                        {
1509                            // Shoot ray from camera focus forwards and get the point it hits an
1510                            // entity or terrain. The ray starts from the camera focus point
1511                            // so that the player won't aim at things behind them, in front of the
1512                            // camera.
1513                            let ray_start = self.scene.camera().get_focus_pos();
1514                            let entity_ray_end = ray_start + cam_dir * 1000.0;
1515                            let terrain_ray_end = ray_start + cam_dir * 1000.0;
1516
1517                            let aim_point = {
1518                                // Get the distance to nearest entity and terrain
1519                                let entity_dist =
1520                                    ray_entities(&client, ray_start, entity_ray_end, 1000.0).0;
1521                                let terrain_ray_distance = client
1522                                    .state()
1523                                    .terrain()
1524                                    .ray(ray_start, terrain_ray_end)
1525                                    .max_iter(1000)
1526                                    .until(Block::is_solid)
1527                                    .cast()
1528                                    .0;
1529
1530                                // Return the hit point of whichever was smaller
1531                                ray_start + cam_dir * entity_dist.min(terrain_ray_distance)
1532                            };
1533
1534                            // Get player orientation
1535                            let ori = client
1536                                .state()
1537                                .read_storage::<comp::Ori>()
1538                                .get(player_entity)
1539                                .copied()
1540                                .unwrap();
1541                            // Get player scale
1542                            let scale = client
1543                                .state()
1544                                .read_storage::<comp::Scale>()
1545                                .get(player_entity)
1546                                .copied()
1547                                .unwrap_or(comp::Scale(1.0));
1548                            // Get player body offsets
1549                            let body = client
1550                                .state()
1551                                .read_storage::<comp::Body>()
1552                                .get(player_entity)
1553                                .copied()
1554                                .unwrap();
1555                            let body_offsets = body.projectile_offsets(ori.look_vec(), scale.0);
1556
1557                            // Get direction from player character to aim point
1558                            let player_pos = client
1559                                .state()
1560                                .read_storage::<Pos>()
1561                                .get(player_entity)
1562                                .copied()
1563                                .unwrap();
1564
1565                            drop(client);
1566                            aim_point - (player_pos.0 + body_offsets)
1567                        } else {
1568                            cam_dir + aim_dir_offset
1569                        };
1570
1571                        self.inputs.look_dir = Dir::from_unnormalized(dir).unwrap();
1572                    }
1573                }
1574                self.inputs.strafing = matches!(
1575                    self.scene.camera().get_mode(),
1576                    camera::CameraMode::FirstPerson
1577                );
1578
1579                // Auto camera mode
1580                if global_state.settings.gameplay.auto_camera
1581                    && matches!(
1582                        self.scene.camera().get_mode(),
1583                        camera::CameraMode::ThirdPerson | camera::CameraMode::FirstPerson
1584                    )
1585                    && input_vec.magnitude_squared() > 0.0
1586                {
1587                    let camera = self.scene.camera_mut();
1588                    let ori = camera.get_orientation();
1589                    camera.set_orientation_instant(Vec3::new(
1590                        ori.x
1591                            + input_vec.x
1592                                * (3.0 - input_vec.y * 1.5 * if is_aiming { 1.5 } else { 1.0 })
1593                                * dt,
1594                        std::f32::consts::PI * if is_aiming { 0.015 } else { 0.1 },
1595                        0.0,
1596                    ));
1597                }
1598
1599                self.inputs.move_z =
1600                    self.key_state.swim_up as i32 as f32 - self.key_state.swim_down as i32 as f32;
1601            }
1602
1603            match self.scene.camera().get_mode() {
1604                CameraMode::FirstPerson | CameraMode::ThirdPerson => {
1605                    if mutable_viewpoint {
1606                        // Move the player character based on their walking direction.
1607                        // This could be different from the camera direction if free look is
1608                        // enabled.
1609                        self.inputs.move_dir =
1610                            self.walk_right_dir * axis_right + self.walk_forward_dir * axis_up;
1611                    }
1612                },
1613                CameraMode::Freefly => {
1614                    // Move the camera freely in 3d space. Apply acceleration so that
1615                    // the movement feels more natural and controlled.
1616                    const FREEFLY_SPEED: f32 = 50.0;
1617                    const FREEFLY_SPEED_BOOST: f32 = 5.0;
1618
1619                    let forward = self.scene.camera().forward().with_z(0.0).normalized();
1620                    let right = self.scene.camera().right().with_z(0.0).normalized();
1621                    let up = Vec3::unit_z();
1622                    let up_axis = self.key_state.swim_up as i32 as f32
1623                        - self.key_state.swim_down as i32 as f32;
1624
1625                    let dir = (right * axis_right + forward * axis_up + up * up_axis).normalized();
1626
1627                    let speed = FREEFLY_SPEED
1628                        * if self.inputs_state.contains(&GameInput::SpectateSpeedBoost) {
1629                            FREEFLY_SPEED_BOOST
1630                        } else {
1631                            1.0
1632                        };
1633
1634                    let pos = self.scene.camera().get_focus_pos();
1635                    self.scene
1636                        .camera_mut()
1637                        .set_focus_pos(pos + dir * dt * speed);
1638
1639                    // Do not apply any movement to the player character
1640                    self.inputs.move_dir = Vec2::zero();
1641                },
1642            };
1643
1644            let mut outcomes = Vec::new();
1645
1646            // Runs if either in a multiplayer server or the singleplayer server is unpaused
1647            if !global_state.paused() {
1648                // Perform an in-game tick.
1649                match self.tick(
1650                    global_state.clock.get_stable_dt(),
1651                    global_state,
1652                    &mut outcomes,
1653                ) {
1654                    Ok(TickAction::Continue) => {}, // Do nothing
1655                    Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu
1656                    Err(Error::ClientError(error)) => {
1657                        error!("[session] Failed to tick the scene: {:?}", error);
1658                        global_state.info_message =
1659                            Some(get_client_msg_error(error, None, &global_state.i18n.read()));
1660
1661                        return PlayStateResult::Pop;
1662                    },
1663                    Err(err) => {
1664                        global_state.info_message = Some(
1665                            global_state
1666                                .i18n
1667                                .read()
1668                                .get_msg("common-connection_lost")
1669                                .into_owned(),
1670                        );
1671                        error!("[session] Failed to tick the scene: {:?}", err);
1672
1673                        return PlayStateResult::Pop;
1674                    },
1675                }
1676            }
1677
1678            if self.walking_speed {
1679                self.key_state.speed_mul = global_state.settings.gameplay.walking_speed;
1680            } else {
1681                self.key_state.speed_mul = 1.0;
1682            }
1683
1684            // Recompute dependents just in case some input modified the camera
1685            self.scene
1686                .camera_mut()
1687                .compute_dependents(&self.client.borrow().state().terrain());
1688
1689            // Generate debug info, if needed
1690            // (it iterates through enough data that we might
1691            // as well avoid it unless we need it).
1692            let debug_info = global_state.settings.interface.toggle_debug.then(|| {
1693                let client = self.client.borrow();
1694                let ecs = client.state().ecs();
1695                let client_entity = client.entity();
1696                let coordinates = ecs.read_storage::<Pos>().get(viewpoint_entity).cloned();
1697                let velocity = ecs.read_storage::<Vel>().get(viewpoint_entity).cloned();
1698                let ori = ecs
1699                    .read_storage::<comp::Ori>()
1700                    .get(viewpoint_entity)
1701                    .cloned();
1702                // NOTE: at the time of writing, it will always output default
1703                // look_dir in Specate mode, because Controller isn't synced
1704                let look_dir = if viewpoint_entity == client_entity {
1705                    self.inputs.look_dir
1706                } else {
1707                    ecs.read_storage::<comp::Controller>()
1708                        .get(viewpoint_entity)
1709                        .map(|c| c.inputs.look_dir)
1710                        .unwrap_or_default()
1711                };
1712                let in_fluid = ecs
1713                    .read_storage::<comp::PhysicsState>()
1714                    .get(viewpoint_entity)
1715                    .and_then(|state| state.in_fluid);
1716                let character_state = ecs
1717                    .read_storage::<comp::CharacterState>()
1718                    .get(viewpoint_entity)
1719                    .cloned();
1720
1721                DebugInfo {
1722                    tps: global_state.clock.stats().average_tps,
1723                    frame_time: global_state.clock.stats().average_busy_dt,
1724                    ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
1725                    coordinates,
1726                    velocity,
1727                    ori,
1728                    look_dir,
1729                    character_state,
1730                    in_fluid,
1731                    num_chunks: self.scene.terrain().chunk_count() as u32,
1732                    num_lights: self.scene.lights().len() as u32,
1733                    num_visible_chunks: self.scene.terrain().visible_chunk_count() as u32,
1734                    num_shadow_chunks: self.scene.terrain().shadow_chunk_count() as u32,
1735                    num_figures: self.scene.figure_mgr().figure_count() as u32,
1736                    num_figures_visible: self.scene.figure_mgr().figure_count_visible() as u32,
1737                    num_particles: self.scene.particle_mgr().particle_count() as u32,
1738                    num_particles_visible: self.scene.particle_mgr().particle_count_visible()
1739                        as u32,
1740                    current_track: self.scene.music_mgr().current_track(),
1741                    current_artist: self.scene.music_mgr().current_artist(),
1742                    active_channels: global_state.audio.get_num_active_channels(),
1743                    audio_cpu_usage: global_state.audio.get_cpu_usage(),
1744                }
1745            });
1746
1747            let inverted_interactable_map = self.interactables.inverted_map();
1748
1749            // Extract HUD events ensuring the client borrow gets dropped.
1750            let mut hud_events = self.hud.maintain(
1751                &self.client.borrow(),
1752                global_state,
1753                &debug_info,
1754                self.scene.camera(),
1755                global_state.clock.get_stable_dt(),
1756                HudInfo {
1757                    is_aiming,
1758                    active_mine_tool,
1759                    is_first_person: matches!(
1760                        self.scene.camera().get_mode(),
1761                        camera::CameraMode::FirstPerson
1762                    ),
1763                    viewpoint_entity,
1764                    mutable_viewpoint,
1765                    target_entity: self.target_entity,
1766                    selected_entity: self.selected_entity,
1767                    persistence_load_error: self.metadata.skill_set_persistence_load_error,
1768                    key_state: &self.key_state,
1769                },
1770                inverted_interactable_map,
1771            );
1772
1773            // Maintain egui (debug interface)
1774            #[cfg(feature = "egui-ui")]
1775            if global_state.settings.interface.egui_enabled() {
1776                let settings_change = global_state.egui_state.maintain(
1777                    &mut self.client.borrow_mut(),
1778                    &mut self.scene,
1779                    global_state.window.window(),
1780                    debug_info.map(|debug_info| EguiDebugInfo {
1781                        frame_time: debug_info.frame_time,
1782                        ping_ms: debug_info.ping_ms,
1783                    }),
1784                    &global_state.settings,
1785                );
1786
1787                if let Some(settings_change) = settings_change {
1788                    settings_change.process(global_state, self);
1789                }
1790            }
1791
1792            // Look for changes in the localization files
1793            if global_state.i18n.reloaded() {
1794                hud_events.push(HudEvent::SettingsChange(
1795                    ChangeLanguage(Box::new(global_state.i18n.read().metadata().clone())).into(),
1796                ));
1797            }
1798
1799            let mut has_repaired = false;
1800            let sfx_triggers = self.scene.sfx_mgr.triggers.read();
1801            // Maintain the UI.
1802            for event in hud_events {
1803                match event {
1804                    HudEvent::SendMessage(msg) => {
1805                        // TODO: Handle result
1806                        self.client.borrow_mut().send_chat(msg);
1807                    },
1808                    HudEvent::SendCommand(name, args) => {
1809                        match run_command(self, global_state, &name, args) {
1810                            Ok(Some(info)) => {
1811                                self.hud.new_message(ChatType::CommandInfo.into_msg(info))
1812                            },
1813                            Ok(None) => {}, // Server will provide an info message
1814                            Err(error) => {
1815                                self.hud.new_message(ChatType::CommandError.into_msg(error))
1816                            },
1817                        };
1818                    },
1819                    HudEvent::CharacterSelection => {
1820                        global_state.audio.stop_all_music();
1821                        global_state.audio.stop_all_ambience();
1822                        global_state.audio.stop_all_sfx();
1823                        self.client.borrow_mut().request_remove_character()
1824                    },
1825                    HudEvent::Logout => {
1826                        self.client.borrow_mut().logout();
1827                        // Stop all sounds
1828                        // TODO: Abstract this behavior to all instances of PlayStateResult::Pop
1829                        // somehow
1830                        global_state.audio.stop_all_ambience();
1831                        global_state.audio.stop_all_sfx();
1832                        return PlayStateResult::Pop;
1833                    },
1834                    HudEvent::Quit => {
1835                        return PlayStateResult::Shutdown;
1836                    },
1837
1838                    HudEvent::RemoveBuff(buff_id) => {
1839                        self.client.borrow_mut().remove_buff(buff_id);
1840                    },
1841                    HudEvent::LeaveStance => self.client.borrow_mut().leave_stance(),
1842                    HudEvent::UnlockSkill(skill) => {
1843                        self.client.borrow_mut().unlock_skill(skill);
1844                    },
1845                    HudEvent::UseSlot {
1846                        slot,
1847                        bypass_dialog,
1848                    } => {
1849                        let mut move_allowed = true;
1850
1851                        if !bypass_dialog
1852                            && let Some(inventory) = self
1853                                .client
1854                                .borrow()
1855                                .state()
1856                                .ecs()
1857                                .read_storage::<comp::Inventory>()
1858                                .get(self.client.borrow().entity())
1859                        {
1860                            match slot {
1861                                Slot::Inventory(inv_slot) => {
1862                                    let slot_deficit = inventory.free_after_equip(inv_slot);
1863                                    if slot_deficit < 0 {
1864                                        self.hud.set_prompt_dialog(PromptDialogSettings::new(
1865                                            global_state.i18n.read().get_content(
1866                                                &Content::localized_with_args(
1867                                                    "hud-bag-use_slot_equip_drop_items",
1868                                                    [(
1869                                                        "slot_deficit",
1870                                                        slot_deficit.unsigned_abs() as u64,
1871                                                    )],
1872                                                ),
1873                                            ),
1874                                            HudEvent::UseSlot {
1875                                                slot,
1876                                                bypass_dialog: true,
1877                                            },
1878                                            None,
1879                                        ));
1880                                        move_allowed = false;
1881                                    }
1882                                },
1883                                Slot::Equip(equip_slot) => {
1884                                    // Ensure there is a free slot that is not provided by the
1885                                    // item being unequipped
1886                                    let free_slots =
1887                                        inventory.free_slots_minus_equipped_item(equip_slot);
1888                                    if free_slots > 0 {
1889                                        let slot_deficit = inventory.free_after_unequip(equip_slot);
1890                                        if slot_deficit < 0 {
1891                                            self.hud.set_prompt_dialog(PromptDialogSettings::new(
1892                                                global_state.i18n.read().get_content(
1893                                                    &Content::localized_with_args(
1894                                                        "hud-bag-use_slot_unequip_drop_items",
1895                                                        [(
1896                                                            "slot_deficit",
1897                                                            slot_deficit.unsigned_abs() as u64,
1898                                                        )],
1899                                                    ),
1900                                                ),
1901                                                HudEvent::UseSlot {
1902                                                    slot,
1903                                                    bypass_dialog: true,
1904                                                },
1905                                                None,
1906                                            ));
1907                                            move_allowed = false;
1908                                        }
1909                                    } else {
1910                                        move_allowed = false;
1911                                    }
1912                                },
1913                                Slot::Overflow(_) => {},
1914                            }
1915                        };
1916
1917                        if move_allowed {
1918                            self.client.borrow_mut().use_slot(slot);
1919                        }
1920                    },
1921                    HudEvent::SwapEquippedWeapons => {
1922                        self.client.borrow_mut().swap_loadout();
1923                    },
1924                    HudEvent::SwapSlots {
1925                        slot_a,
1926                        slot_b,
1927                        bypass_dialog,
1928                    } => {
1929                        let mut move_allowed = true;
1930                        if !bypass_dialog
1931                            && let Some(inventory) = self
1932                                .client
1933                                .borrow()
1934                                .state()
1935                                .ecs()
1936                                .read_storage::<comp::Inventory>()
1937                                .get(self.client.borrow().entity())
1938                        {
1939                            match (slot_a, slot_b) {
1940                                (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1941                                | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1942                                    if !inventory.can_swap(inv_slot, equip_slot) {
1943                                        move_allowed = false;
1944                                    } else {
1945                                        let slot_deficit =
1946                                            inventory.free_after_swap(equip_slot, inv_slot);
1947                                        if slot_deficit < 0 {
1948                                            self.hud.set_prompt_dialog(PromptDialogSettings::new(
1949                                                global_state.i18n.read().get_content(
1950                                                    &Content::localized_with_args(
1951                                                        "hud-bag-swap_slots_drop_items",
1952                                                        [(
1953                                                            "slot_deficit",
1954                                                            slot_deficit.unsigned_abs() as u64,
1955                                                        )],
1956                                                    ),
1957                                                ),
1958                                                HudEvent::SwapSlots {
1959                                                    slot_a,
1960                                                    slot_b,
1961                                                    bypass_dialog: true,
1962                                                },
1963                                                None,
1964                                            ));
1965                                            move_allowed = false;
1966                                        }
1967                                    }
1968                                },
1969                                _ => {},
1970                            }
1971                        }
1972                        if move_allowed {
1973                            self.client.borrow_mut().swap_slots(slot_a, slot_b);
1974                        }
1975                    },
1976                    HudEvent::SelectExpBar(skillgroup) => {
1977                        global_state.settings.interface.xp_bar_skillgroup = skillgroup;
1978                    },
1979                    HudEvent::SplitSwapSlots {
1980                        slot_a,
1981                        slot_b,
1982                        bypass_dialog,
1983                    } => {
1984                        let mut move_allowed = true;
1985                        if !bypass_dialog
1986                            && let Some(inventory) = self
1987                                .client
1988                                .borrow()
1989                                .state()
1990                                .ecs()
1991                                .read_storage::<comp::Inventory>()
1992                                .get(self.client.borrow().entity())
1993                        {
1994                            match (slot_a, slot_b) {
1995                                (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1996                                | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1997                                    if !inventory.can_swap(inv_slot, equip_slot) {
1998                                        move_allowed = false;
1999                                    } else {
2000                                        let slot_deficit =
2001                                            inventory.free_after_swap(equip_slot, inv_slot);
2002                                        if slot_deficit < 0 {
2003                                            self.hud.set_prompt_dialog(PromptDialogSettings::new(
2004                                                global_state.i18n.read().get_content(
2005                                                    &Content::localized_with_args(
2006                                                        "hud-bag-split_swap_slots_drop_items",
2007                                                        [(
2008                                                            "slot_deficit",
2009                                                            slot_deficit.unsigned_abs() as u64,
2010                                                        )],
2011                                                    ),
2012                                                ),
2013                                                HudEvent::SwapSlots {
2014                                                    slot_a,
2015                                                    slot_b,
2016                                                    bypass_dialog: true,
2017                                                },
2018                                                None,
2019                                            ));
2020                                            move_allowed = false;
2021                                        }
2022                                    }
2023                                },
2024                                _ => {},
2025                            }
2026                        };
2027                        if move_allowed {
2028                            self.client.borrow_mut().split_swap_slots(slot_a, slot_b);
2029                        }
2030                    },
2031                    HudEvent::DropSlot(x) => {
2032                        let mut client = self.client.borrow_mut();
2033                        client.drop_slot(x);
2034                        if let Slot::Equip(EquipSlot::Lantern) = x {
2035                            client.disable_lantern();
2036                        }
2037                    },
2038                    HudEvent::SplitDropSlot(x) => {
2039                        let mut client = self.client.borrow_mut();
2040                        client.split_drop_slot(x);
2041                        if let Slot::Equip(EquipSlot::Lantern) = x {
2042                            client.disable_lantern();
2043                        }
2044                    },
2045                    HudEvent::SortInventory(sort_order) => {
2046                        self.client.borrow_mut().sort_inventory(sort_order);
2047                    },
2048                    HudEvent::ChangeHotbarState(state) => {
2049                        let client = self.client.borrow();
2050
2051                        let server_name = &client.server_info().name;
2052                        // If we are changing the hotbar state this CANNOT be None.
2053                        let character_id = match client.presence().unwrap() {
2054                            PresenceKind::Character(id) => Some(id),
2055                            PresenceKind::LoadingCharacter(id) => Some(id),
2056                            PresenceKind::Spectator => {
2057                                unreachable!("HUD adaption in Spectator mode!")
2058                            },
2059                            PresenceKind::Possessor => None,
2060                        };
2061
2062                        // Get or update the ServerProfile.
2063                        global_state.profile.set_hotbar_slots(
2064                            server_name,
2065                            character_id,
2066                            state.slots,
2067                        );
2068
2069                        global_state
2070                            .profile
2071                            .save_to_file_warn(&global_state.config_dir);
2072
2073                        info!("Event! -> ChangedHotbarState")
2074                    },
2075                    HudEvent::TradeAction(action) => {
2076                        self.client.borrow_mut().perform_trade_action(action);
2077                    },
2078                    HudEvent::Ability { idx, state } => {
2079                        self.client.borrow_mut().handle_input(
2080                            InputKind::Ability(idx),
2081                            state,
2082                            default_select_pos,
2083                            self.target_entity,
2084                        );
2085                    },
2086
2087                    HudEvent::RequestSiteInfo(id) => {
2088                        self.client.borrow_mut().request_site_economy(id);
2089                    },
2090
2091                    HudEvent::CraftRecipe {
2092                        recipe_name: recipe,
2093                        craft_sprite,
2094                        amount,
2095                    } => {
2096                        let slots = {
2097                            let client = self.client.borrow();
2098
2099                            if let Some(inventory) = client
2100                                .state()
2101                                .ecs()
2102                                .read_storage::<comp::Inventory>()
2103                                .get(client.entity())
2104                            {
2105                                let rbm =
2106                                    client.state().ecs().read_resource::<RecipeBookManifest>();
2107                                if let Some(recipe) = inventory.get_recipe(&recipe, &rbm) {
2108                                    recipe.inventory_contains_ingredients(inventory, 1).ok()
2109                                } else {
2110                                    None
2111                                }
2112                            } else {
2113                                None
2114                            }
2115                        };
2116                        if let Some(slots) = slots {
2117                            self.client.borrow_mut().craft_recipe(
2118                                &recipe,
2119                                slots,
2120                                craft_sprite,
2121                                amount,
2122                            );
2123                        }
2124                    },
2125
2126                    HudEvent::CraftModularWeapon {
2127                        primary_slot,
2128                        secondary_slot,
2129                        craft_sprite,
2130                    } => {
2131                        self.client.borrow_mut().craft_modular_weapon(
2132                            primary_slot,
2133                            secondary_slot,
2134                            craft_sprite,
2135                        );
2136                    },
2137
2138                    HudEvent::CraftModularWeaponComponent {
2139                        toolkind,
2140                        material,
2141                        modifier,
2142                        craft_sprite,
2143                    } => {
2144                        let additional_slots = {
2145                            let client = self.client.borrow();
2146                            let item_id = |slot| {
2147                                client
2148                                    .inventories()
2149                                    .get(client.entity())
2150                                    .and_then(|inv| inv.get(slot))
2151                                    .and_then(|item| {
2152                                        item.item_definition_id().itemdef_id().map(String::from)
2153                                    })
2154                            };
2155                            if let Some(material_id) = item_id(material) {
2156                                let key = recipe::ComponentKey {
2157                                    toolkind,
2158                                    material: material_id,
2159                                    modifier: modifier.and_then(item_id),
2160                                };
2161                                if let Some(recipe) = client.component_recipe_book().get(&key) {
2162                                    client.inventories().get(client.entity()).and_then(|inv| {
2163                                        recipe.inventory_contains_additional_ingredients(inv).ok()
2164                                    })
2165                                } else {
2166                                    None
2167                                }
2168                            } else {
2169                                None
2170                            }
2171                        };
2172                        if let Some(additional_slots) = additional_slots {
2173                            self.client.borrow_mut().craft_modular_weapon_component(
2174                                toolkind,
2175                                material,
2176                                modifier,
2177                                additional_slots,
2178                                craft_sprite,
2179                            );
2180                        }
2181                    },
2182                    HudEvent::SalvageItem { slot, salvage_pos } => {
2183                        self.client.borrow_mut().salvage_item(slot, salvage_pos);
2184                    },
2185                    HudEvent::RepairItem { item, sprite_pos } => {
2186                        let slots = {
2187                            let client = self.client.borrow();
2188                            let slots = (|| {
2189                                if let Some(inventory) = client.inventories().get(client.entity()) {
2190                                    let item = match item {
2191                                        Slot::Equip(slot) => inventory.equipped(slot),
2192                                        Slot::Inventory(slot) => inventory.get(slot),
2193                                        Slot::Overflow(_) => None,
2194                                    }?;
2195                                    let repair_recipe =
2196                                        client.repair_recipe_book().repair_recipe(item)?;
2197                                    repair_recipe
2198                                        .inventory_contains_ingredients(item, inventory)
2199                                        .ok()
2200                                } else {
2201                                    None
2202                                }
2203                            })();
2204                            slots.unwrap_or_default()
2205                        };
2206                        if !has_repaired {
2207                            let sfx_trigger_item = sfx_triggers
2208                                .0
2209                                .get_key_value(&SfxEvent::from(&InventoryUpdateEvent::Craft));
2210                            global_state.audio.emit_ui_sfx(sfx_trigger_item, None, None);
2211                            has_repaired = true
2212                        };
2213                        self.client
2214                            .borrow_mut()
2215                            .repair_item(item, slots, sprite_pos);
2216                    },
2217                    HudEvent::InviteMember(uid) => {
2218                        self.client.borrow_mut().send_invite(uid, InviteKind::Group);
2219                    },
2220                    HudEvent::AcceptInvite => {
2221                        self.client.borrow_mut().accept_invite();
2222                    },
2223                    HudEvent::DeclineInvite => {
2224                        self.client.borrow_mut().decline_invite();
2225                    },
2226                    HudEvent::KickMember(uid) => {
2227                        self.client.borrow_mut().kick_from_group(uid);
2228                    },
2229                    HudEvent::LeaveGroup => {
2230                        self.client.borrow_mut().leave_group();
2231                    },
2232                    HudEvent::AssignLeader(uid) => {
2233                        self.client.borrow_mut().assign_group_leader(uid);
2234                    },
2235                    HudEvent::ChangeAbility(slot, new_ability) => {
2236                        self.client.borrow_mut().change_ability(slot, new_ability);
2237                    },
2238                    HudEvent::SettingsChange(settings_change) => {
2239                        settings_change.process(global_state, self);
2240                    },
2241                    HudEvent::AcknowledgePersistenceLoadError => {
2242                        self.metadata.skill_set_persistence_load_error = None;
2243                    },
2244                    HudEvent::MapMarkerEvent(event) => {
2245                        self.client.borrow_mut().map_marker_event(event);
2246                    },
2247                    HudEvent::Dialogue(target, dialogue) => {
2248                        self.client.borrow_mut().perform_dialogue(target, dialogue);
2249                    },
2250                    HudEvent::SetBattleMode(mode) => {
2251                        self.client.borrow_mut().set_battle_mode(mode);
2252                    },
2253                }
2254            }
2255
2256            {
2257                let client = self.client.borrow();
2258                let scene_data = SceneData {
2259                    client: &client,
2260                    state: client.state(),
2261                    viewpoint_entity,
2262                    mutable_viewpoint: mutable_viewpoint || self.free_look,
2263                    // Only highlight if interactable
2264                    target_entities: &self.interactables.entities,
2265                    loaded_distance: client.loaded_distance(),
2266                    terrain_view_distance: client.view_distance().unwrap_or(1),
2267                    entity_view_distance: client
2268                        .view_distance()
2269                        .unwrap_or(1)
2270                        .min(global_state.settings.graphics.entity_view_distance),
2271                    tick: client.get_tick(),
2272                    gamma: global_state.settings.graphics.gamma,
2273                    exposure: global_state.settings.graphics.exposure,
2274                    ambiance: global_state.settings.graphics.ambiance,
2275                    mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable,
2276                    sprite_render_distance: global_state.settings.graphics.sprite_render_distance
2277                        as f32,
2278                    particles_enabled: global_state.settings.graphics.particles_enabled,
2279                    weapon_trails_enabled: global_state.settings.graphics.weapon_trails_enabled,
2280                    flashing_lights_enabled: global_state
2281                        .settings
2282                        .graphics
2283                        .render_mode
2284                        .flashing_lights_enabled,
2285                    figure_lod_render_distance: global_state
2286                        .settings
2287                        .graphics
2288                        .figure_lod_render_distance
2289                        as f32,
2290                    is_aiming,
2291                    interpolated_time_of_day: self.scene.interpolated_time_of_day,
2292                    wind_vel: self.scene.wind_vel,
2293                };
2294
2295                // Runs if either in a multiplayer server or the singleplayer server is unpaused
2296                if !global_state.paused() {
2297                    self.scene.maintain(
2298                        global_state.window.renderer_mut(),
2299                        &mut global_state.audio,
2300                        &scene_data,
2301                        &client,
2302                        &global_state.settings,
2303                    );
2304
2305                    // Process outcomes from client
2306                    for outcome in outcomes {
2307                        self.scene
2308                            .handle_outcome(&outcome, &scene_data, &mut global_state.audio);
2309                        self.hud.handle_outcome(&outcome, &scene_data, global_state);
2310                    }
2311                }
2312            }
2313
2314            // Clean things up after the tick.
2315            self.cleanup();
2316
2317            PlayStateResult::Continue
2318        } else if client_registered && client_presence.is_none() {
2319            // If the client cannot enter the game but spectate, pop the play state instead
2320            // of going back to character selection.
2321            if client_type.can_spectate() && !client_type.can_enter_character() {
2322                // Go back to the main menu state
2323                PlayStateResult::Pop
2324            } else {
2325                PlayStateResult::Switch(Box::new(CharSelectionState::new(
2326                    global_state,
2327                    Rc::clone(&self.client),
2328                    Rc::clone(&self.hud.persisted_state),
2329                )))
2330            }
2331        } else {
2332            error!("Client not in the expected state, exiting session play state");
2333            PlayStateResult::Pop
2334        }
2335    }
2336
2337    fn name(&self) -> &'static str { "Session" }
2338
2339    fn capped_fps(&self) -> bool { false }
2340
2341    fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
2342
2343    /// Render the session to the screen.
2344    ///
2345    /// This method should be called once per frame.
2346    fn render(&self, drawer: &mut Drawer<'_>, settings: &Settings) {
2347        span!(_guard, "render", "<Session as PlayState>::render");
2348
2349        let client = self.client.borrow();
2350
2351        let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
2352
2353        let scene_data = SceneData {
2354            client: &client,
2355            state: client.state(),
2356            viewpoint_entity,
2357            mutable_viewpoint,
2358            // Only highlight if interactable
2359            target_entities: &self.interactables.entities,
2360            loaded_distance: client.loaded_distance(),
2361            terrain_view_distance: client.view_distance().unwrap_or(1),
2362            entity_view_distance: client
2363                .view_distance()
2364                .unwrap_or(1)
2365                .min(settings.graphics.entity_view_distance),
2366            tick: client.get_tick(),
2367            gamma: settings.graphics.gamma,
2368            exposure: settings.graphics.exposure,
2369            ambiance: settings.graphics.ambiance,
2370            mouse_smoothing: settings.gameplay.smooth_pan_enable,
2371            sprite_render_distance: settings.graphics.sprite_render_distance as f32,
2372            figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32,
2373            particles_enabled: settings.graphics.particles_enabled,
2374            weapon_trails_enabled: settings.graphics.weapon_trails_enabled,
2375            flashing_lights_enabled: settings.graphics.render_mode.flashing_lights_enabled,
2376            is_aiming: self.is_aiming,
2377            interpolated_time_of_day: self.scene.interpolated_time_of_day,
2378            wind_vel: self.scene.wind_vel,
2379        };
2380
2381        // Render world
2382        self.scene.render(
2383            drawer,
2384            client.state(),
2385            viewpoint_entity,
2386            client.get_tick(),
2387            &scene_data,
2388        );
2389
2390        if let Some(mut volumetric_pass) = drawer.volumetric_pass() {
2391            // Clouds
2392            prof_span!("clouds");
2393            volumetric_pass.draw_clouds();
2394        }
2395        if let Some(mut transparent_pass) = drawer.transparent_pass() {
2396            // Trails
2397            prof_span!("trails");
2398            if let Some(mut trail_drawer) = transparent_pass.draw_trails() {
2399                self.scene
2400                    .trail_mgr()
2401                    .render(&mut trail_drawer, &scene_data);
2402            }
2403        }
2404        // Bloom (call does nothing if bloom is off)
2405        {
2406            prof_span!("bloom");
2407            drawer.run_bloom_passes()
2408        }
2409        // PostProcess and UI
2410        {
2411            prof_span!("post-process and ui");
2412            let mut third_pass = drawer.third_pass();
2413            third_pass.draw_postprocess();
2414            // Draw the UI to the screen
2415            if let Some(mut ui_drawer) = third_pass.draw_ui() {
2416                self.hud.render(&mut ui_drawer);
2417            }; // Note: this semicolon is needed for the third_pass borrow to be
2418            // dropped before it's lifetime ends
2419        }
2420    }
2421
2422    fn egui_enabled(&self) -> bool { true }
2423}
2424
2425fn find_shortest_distance(arr: &[Option<f32>]) -> Option<f32> {
2426    arr.iter()
2427        .filter_map(|x| *x)
2428        .min_by(|d1, d2| OrderedFloat(*d1).cmp(&OrderedFloat(*d2)))
2429}
2430
2431// TODO: Can probably be exported in some way for AI, somehow
2432fn auto_glide(
2433    fluid: Fluid,
2434    vel: Vel,
2435    free_look: bool,
2436    dir_forward_xy: Vec2<f32>,
2437    dir_right: Vec3<f32>,
2438) -> Option<Dir> {
2439    let Vel(rel_flow) = fluid.relative_flow(&vel);
2440
2441    let is_wind_downwards = rel_flow.z.is_sign_negative();
2442
2443    let dir = if free_look {
2444        if is_wind_downwards {
2445            Vec3::from(-rel_flow.xy())
2446        } else {
2447            -rel_flow
2448        }
2449    } else if is_wind_downwards {
2450        dir_forward_xy.into()
2451    } else {
2452        let windwards = rel_flow * dir_forward_xy.dot(rel_flow.xy()).signum();
2453        Plane::from(Dir::new(dir_right)).projection(windwards)
2454    };
2455
2456    Dir::from_unnormalized(dir)
2457}