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