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