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