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