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(&mut global_state.audio);
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                    audio_cpu_usage: global_state.audio.get_cpu_usage(),
1644                }
1645            });
1646
1647            let inverted_interactable_map = self.interactables.inverted_map();
1648
1649            // Extract HUD events ensuring the client borrow gets dropped.
1650            let mut hud_events = self.hud.maintain(
1651                &self.client.borrow(),
1652                global_state,
1653                &debug_info,
1654                self.scene.camera(),
1655                global_state.clock.get_stable_dt(),
1656                HudInfo {
1657                    is_aiming,
1658                    active_mine_tool,
1659                    is_first_person: matches!(
1660                        self.scene.camera().get_mode(),
1661                        camera::CameraMode::FirstPerson
1662                    ),
1663                    viewpoint_entity,
1664                    mutable_viewpoint,
1665                    target_entity: self.target_entity,
1666                    selected_entity: self.selected_entity,
1667                    persistence_load_error: self.metadata.skill_set_persistence_load_error,
1668                    key_state: &self.key_state,
1669                },
1670                inverted_interactable_map,
1671            );
1672
1673            // Maintain egui (debug interface)
1674            #[cfg(feature = "egui-ui")]
1675            if global_state.settings.interface.egui_enabled() {
1676                let settings_change = global_state.egui_state.maintain(
1677                    &mut self.client.borrow_mut(),
1678                    &mut self.scene,
1679                    debug_info.map(|debug_info| EguiDebugInfo {
1680                        frame_time: debug_info.frame_time,
1681                        ping_ms: debug_info.ping_ms,
1682                    }),
1683                    &global_state.settings,
1684                );
1685
1686                if let Some(settings_change) = settings_change {
1687                    settings_change.process(global_state, self);
1688                }
1689            }
1690
1691            // Look for changes in the localization files
1692            if global_state.i18n.reloaded() {
1693                hud_events.push(HudEvent::SettingsChange(
1694                    ChangeLanguage(Box::new(global_state.i18n.read().metadata().clone())).into(),
1695                ));
1696            }
1697
1698            let mut has_repaired = false;
1699            let sfx_triggers = self.scene.sfx_mgr.triggers.read();
1700            // Maintain the UI.
1701            for event in hud_events {
1702                match event {
1703                    HudEvent::SendMessage(msg) => {
1704                        // TODO: Handle result
1705                        self.client.borrow_mut().send_chat(msg);
1706                    },
1707                    HudEvent::SendCommand(name, args) => {
1708                        match run_command(self, global_state, &name, args) {
1709                            Ok(Some(info)) => {
1710                                self.hud.new_message(ChatType::CommandInfo.into_msg(info))
1711                            },
1712                            Ok(None) => {}, // Server will provide an info message
1713                            Err(error) => {
1714                                self.hud.new_message(ChatType::CommandError.into_msg(error))
1715                            },
1716                        };
1717                    },
1718                    HudEvent::CharacterSelection => {
1719                        global_state.audio.stop_all_music();
1720                        global_state.audio.stop_all_ambience();
1721                        global_state.audio.stop_all_sfx();
1722                        self.client.borrow_mut().request_remove_character()
1723                    },
1724                    HudEvent::Logout => {
1725                        self.client.borrow_mut().logout();
1726                        // Stop all sounds
1727                        // TODO: Abstract this behavior to all instances of PlayStateResult::Pop
1728                        // somehow
1729                        global_state.audio.stop_all_ambience();
1730                        global_state.audio.stop_all_sfx();
1731                        return PlayStateResult::Pop;
1732                    },
1733                    HudEvent::Quit => {
1734                        return PlayStateResult::Shutdown;
1735                    },
1736
1737                    HudEvent::RemoveBuff(buff_id) => {
1738                        self.client.borrow_mut().remove_buff(buff_id);
1739                    },
1740                    HudEvent::LeaveStance => self.client.borrow_mut().leave_stance(),
1741                    HudEvent::UnlockSkill(skill) => {
1742                        self.client.borrow_mut().unlock_skill(skill);
1743                    },
1744                    HudEvent::UseSlot {
1745                        slot,
1746                        bypass_dialog,
1747                    } => {
1748                        let mut move_allowed = true;
1749
1750                        if !bypass_dialog {
1751                            if let Some(inventory) = self
1752                                .client
1753                                .borrow()
1754                                .state()
1755                                .ecs()
1756                                .read_storage::<comp::Inventory>()
1757                                .get(self.client.borrow().entity())
1758                            {
1759                                match slot {
1760                                    Slot::Inventory(inv_slot) => {
1761                                        let slot_deficit = inventory.free_after_equip(inv_slot);
1762                                        if slot_deficit < 0 {
1763                                            self.hud.set_prompt_dialog(PromptDialogSettings::new(
1764                                                global_state.i18n.read().get_content(
1765                                                    &Content::localized_with_args(
1766                                                        "hud-bag-use_slot_equip_drop_items",
1767                                                        [(
1768                                                            "slot_deficit",
1769                                                            slot_deficit.unsigned_abs() as u64,
1770                                                        )],
1771                                                    ),
1772                                                ),
1773                                                HudEvent::UseSlot {
1774                                                    slot,
1775                                                    bypass_dialog: true,
1776                                                },
1777                                                None,
1778                                            ));
1779                                            move_allowed = false;
1780                                        }
1781                                    },
1782                                    Slot::Equip(equip_slot) => {
1783                                        // Ensure there is a free slot that is not provided by the
1784                                        // item being unequipped
1785                                        let free_slots =
1786                                            inventory.free_slots_minus_equipped_item(equip_slot);
1787                                        if free_slots > 0 {
1788                                            let slot_deficit =
1789                                                inventory.free_after_unequip(equip_slot);
1790                                            if slot_deficit < 0 {
1791                                                self.hud
1792                                                    .set_prompt_dialog(PromptDialogSettings::new(
1793                                                    global_state.i18n.read().get_content(
1794                                                        &Content::localized_with_args(
1795                                                            "hud-bag-use_slot_unequip_drop_items",
1796                                                            [(
1797                                                                "slot_deficit",
1798                                                                slot_deficit.unsigned_abs() as u64,
1799                                                            )],
1800                                                        ),
1801                                                    ),
1802                                                    HudEvent::UseSlot {
1803                                                        slot,
1804                                                        bypass_dialog: true,
1805                                                    },
1806                                                    None,
1807                                                ));
1808                                                move_allowed = false;
1809                                            }
1810                                        } else {
1811                                            move_allowed = false;
1812                                        }
1813                                    },
1814                                    Slot::Overflow(_) => {},
1815                                }
1816                            };
1817                        }
1818
1819                        if move_allowed {
1820                            self.client.borrow_mut().use_slot(slot);
1821                        }
1822                    },
1823                    HudEvent::SwapEquippedWeapons => {
1824                        self.client.borrow_mut().swap_loadout();
1825                    },
1826                    HudEvent::SwapSlots {
1827                        slot_a,
1828                        slot_b,
1829                        bypass_dialog,
1830                    } => {
1831                        let mut move_allowed = true;
1832                        if !bypass_dialog {
1833                            if let Some(inventory) = self
1834                                .client
1835                                .borrow()
1836                                .state()
1837                                .ecs()
1838                                .read_storage::<comp::Inventory>()
1839                                .get(self.client.borrow().entity())
1840                            {
1841                                match (slot_a, slot_b) {
1842                                    (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1843                                    | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1844                                        if !inventory.can_swap(inv_slot, equip_slot) {
1845                                            move_allowed = false;
1846                                        } else {
1847                                            let slot_deficit =
1848                                                inventory.free_after_swap(equip_slot, inv_slot);
1849                                            if slot_deficit < 0 {
1850                                                self.hud.set_prompt_dialog(
1851                                                    PromptDialogSettings::new(
1852                                                        global_state.i18n.read().get_content(
1853                                                            &Content::localized_with_args(
1854                                                                "hud-bag-swap_slots_drop_items",
1855                                                                [(
1856                                                                    "slot_deficit",
1857                                                                    slot_deficit.unsigned_abs()
1858                                                                        as u64,
1859                                                                )],
1860                                                            ),
1861                                                        ),
1862                                                        HudEvent::SwapSlots {
1863                                                            slot_a,
1864                                                            slot_b,
1865                                                            bypass_dialog: true,
1866                                                        },
1867                                                        None,
1868                                                    ),
1869                                                );
1870                                                move_allowed = false;
1871                                            }
1872                                        }
1873                                    },
1874                                    _ => {},
1875                                }
1876                            }
1877                        }
1878                        if move_allowed {
1879                            self.client.borrow_mut().swap_slots(slot_a, slot_b);
1880                        }
1881                    },
1882                    HudEvent::SelectExpBar(skillgroup) => {
1883                        global_state.settings.interface.xp_bar_skillgroup = skillgroup;
1884                    },
1885                    HudEvent::SplitSwapSlots {
1886                        slot_a,
1887                        slot_b,
1888                        bypass_dialog,
1889                    } => {
1890                        let mut move_allowed = true;
1891                        if !bypass_dialog {
1892                            if let Some(inventory) = self
1893                                .client
1894                                .borrow()
1895                                .state()
1896                                .ecs()
1897                                .read_storage::<comp::Inventory>()
1898                                .get(self.client.borrow().entity())
1899                            {
1900                                match (slot_a, slot_b) {
1901                                    (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1902                                    | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1903                                        if !inventory.can_swap(inv_slot, equip_slot) {
1904                                            move_allowed = false;
1905                                        } else {
1906                                            let slot_deficit =
1907                                                inventory.free_after_swap(equip_slot, inv_slot);
1908                                            if slot_deficit < 0 {
1909                                                self.hud
1910                                                    .set_prompt_dialog(PromptDialogSettings::new(
1911                                                    global_state.i18n.read().get_content(
1912                                                        &Content::localized_with_args(
1913                                                            "hud-bag-split_swap_slots_drop_items",
1914                                                            [(
1915                                                                "slot_deficit",
1916                                                                slot_deficit.unsigned_abs() as u64,
1917                                                            )],
1918                                                        ),
1919                                                    ),
1920                                                    HudEvent::SwapSlots {
1921                                                        slot_a,
1922                                                        slot_b,
1923                                                        bypass_dialog: true,
1924                                                    },
1925                                                    None,
1926                                                ));
1927                                                move_allowed = false;
1928                                            }
1929                                        }
1930                                    },
1931                                    _ => {},
1932                                }
1933                            }
1934                        };
1935                        if move_allowed {
1936                            self.client.borrow_mut().split_swap_slots(slot_a, slot_b);
1937                        }
1938                    },
1939                    HudEvent::DropSlot(x) => {
1940                        let mut client = self.client.borrow_mut();
1941                        client.drop_slot(x);
1942                        if let Slot::Equip(EquipSlot::Lantern) = x {
1943                            client.disable_lantern();
1944                        }
1945                    },
1946                    HudEvent::SplitDropSlot(x) => {
1947                        let mut client = self.client.borrow_mut();
1948                        client.split_drop_slot(x);
1949                        if let Slot::Equip(EquipSlot::Lantern) = x {
1950                            client.disable_lantern();
1951                        }
1952                    },
1953                    HudEvent::SortInventory => {
1954                        self.client.borrow_mut().sort_inventory();
1955                    },
1956                    HudEvent::ChangeHotbarState(state) => {
1957                        let client = self.client.borrow();
1958
1959                        let server_name = &client.server_info().name;
1960                        // If we are changing the hotbar state this CANNOT be None.
1961                        let character_id = match client.presence().unwrap() {
1962                            PresenceKind::Character(id) => Some(id),
1963                            PresenceKind::LoadingCharacter(id) => Some(id),
1964                            PresenceKind::Spectator => {
1965                                unreachable!("HUD adaption in Spectator mode!")
1966                            },
1967                            PresenceKind::Possessor => None,
1968                        };
1969
1970                        // Get or update the ServerProfile.
1971                        global_state.profile.set_hotbar_slots(
1972                            server_name,
1973                            character_id,
1974                            state.slots,
1975                        );
1976
1977                        global_state
1978                            .profile
1979                            .save_to_file_warn(&global_state.config_dir);
1980
1981                        info!("Event! -> ChangedHotbarState")
1982                    },
1983                    HudEvent::TradeAction(action) => {
1984                        self.client.borrow_mut().perform_trade_action(action);
1985                    },
1986                    HudEvent::Ability(i, state) => {
1987                        self.client.borrow_mut().handle_input(
1988                            InputKind::Ability(i),
1989                            state,
1990                            default_select_pos,
1991                            self.target_entity,
1992                        );
1993                    },
1994
1995                    HudEvent::RequestSiteInfo(id) => {
1996                        self.client.borrow_mut().request_site_economy(id);
1997                    },
1998
1999                    HudEvent::CraftRecipe {
2000                        recipe_name: recipe,
2001                        craft_sprite,
2002                        amount,
2003                    } => {
2004                        let slots = {
2005                            let client = self.client.borrow();
2006
2007                            let s = if let Some(inventory) = client
2008                                .state()
2009                                .ecs()
2010                                .read_storage::<comp::Inventory>()
2011                                .get(client.entity())
2012                            {
2013                                let rbm =
2014                                    client.state().ecs().read_resource::<RecipeBookManifest>();
2015                                if let Some(recipe) = inventory.get_recipe(&recipe, &rbm) {
2016                                    recipe.inventory_contains_ingredients(inventory, 1).ok()
2017                                } else {
2018                                    None
2019                                }
2020                            } else {
2021                                None
2022                            };
2023                            s
2024                        };
2025                        if let Some(slots) = slots {
2026                            self.client.borrow_mut().craft_recipe(
2027                                &recipe,
2028                                slots,
2029                                craft_sprite,
2030                                amount,
2031                            );
2032                        }
2033                    },
2034
2035                    HudEvent::CraftModularWeapon {
2036                        primary_slot,
2037                        secondary_slot,
2038                        craft_sprite,
2039                    } => {
2040                        self.client.borrow_mut().craft_modular_weapon(
2041                            primary_slot,
2042                            secondary_slot,
2043                            craft_sprite,
2044                        );
2045                    },
2046
2047                    HudEvent::CraftModularWeaponComponent {
2048                        toolkind,
2049                        material,
2050                        modifier,
2051                        craft_sprite,
2052                    } => {
2053                        let additional_slots = {
2054                            let client = self.client.borrow();
2055                            let item_id = |slot| {
2056                                client
2057                                    .inventories()
2058                                    .get(client.entity())
2059                                    .and_then(|inv| inv.get(slot))
2060                                    .and_then(|item| {
2061                                        item.item_definition_id().itemdef_id().map(String::from)
2062                                    })
2063                            };
2064                            if let Some(material_id) = item_id(material) {
2065                                let key = recipe::ComponentKey {
2066                                    toolkind,
2067                                    material: material_id,
2068                                    modifier: modifier.and_then(item_id),
2069                                };
2070                                if let Some(recipe) = client.component_recipe_book().get(&key) {
2071                                    client.inventories().get(client.entity()).and_then(|inv| {
2072                                        recipe.inventory_contains_additional_ingredients(inv).ok()
2073                                    })
2074                                } else {
2075                                    None
2076                                }
2077                            } else {
2078                                None
2079                            }
2080                        };
2081                        if let Some(additional_slots) = additional_slots {
2082                            self.client.borrow_mut().craft_modular_weapon_component(
2083                                toolkind,
2084                                material,
2085                                modifier,
2086                                additional_slots,
2087                                craft_sprite,
2088                            );
2089                        }
2090                    },
2091                    HudEvent::SalvageItem { slot, salvage_pos } => {
2092                        self.client.borrow_mut().salvage_item(slot, salvage_pos);
2093                    },
2094                    HudEvent::RepairItem { item, sprite_pos } => {
2095                        let slots = {
2096                            let client = self.client.borrow();
2097                            let slots = (|| {
2098                                if let Some(inventory) = client.inventories().get(client.entity()) {
2099                                    let item = match item {
2100                                        Slot::Equip(slot) => inventory.equipped(slot),
2101                                        Slot::Inventory(slot) => inventory.get(slot),
2102                                        Slot::Overflow(_) => None,
2103                                    }?;
2104                                    let repair_recipe =
2105                                        client.repair_recipe_book().repair_recipe(item)?;
2106                                    repair_recipe
2107                                        .inventory_contains_ingredients(item, inventory)
2108                                        .ok()
2109                                } else {
2110                                    None
2111                                }
2112                            })();
2113                            slots.unwrap_or_default()
2114                        };
2115                        if !has_repaired {
2116                            let sfx_trigger_item = sfx_triggers
2117                                .get_key_value(&SfxEvent::from(&InventoryUpdateEvent::Craft));
2118                            global_state.audio.emit_ui_sfx(sfx_trigger_item, None);
2119                            has_repaired = true
2120                        };
2121                        self.client
2122                            .borrow_mut()
2123                            .repair_item(item, slots, sprite_pos);
2124                    },
2125                    HudEvent::InviteMember(uid) => {
2126                        self.client.borrow_mut().send_invite(uid, InviteKind::Group);
2127                    },
2128                    HudEvent::AcceptInvite => {
2129                        self.client.borrow_mut().accept_invite();
2130                    },
2131                    HudEvent::DeclineInvite => {
2132                        self.client.borrow_mut().decline_invite();
2133                    },
2134                    HudEvent::KickMember(uid) => {
2135                        self.client.borrow_mut().kick_from_group(uid);
2136                    },
2137                    HudEvent::LeaveGroup => {
2138                        self.client.borrow_mut().leave_group();
2139                    },
2140                    HudEvent::AssignLeader(uid) => {
2141                        self.client.borrow_mut().assign_group_leader(uid);
2142                    },
2143                    HudEvent::ChangeAbility(slot, new_ability) => {
2144                        self.client.borrow_mut().change_ability(slot, new_ability);
2145                    },
2146                    HudEvent::SettingsChange(settings_change) => {
2147                        settings_change.process(global_state, self);
2148                    },
2149                    HudEvent::AcknowledgePersistenceLoadError => {
2150                        self.metadata.skill_set_persistence_load_error = None;
2151                    },
2152                    HudEvent::MapMarkerEvent(event) => {
2153                        self.client.borrow_mut().map_marker_event(event);
2154                    },
2155                    HudEvent::Dialogue(target, dialogue) => {
2156                        self.client.borrow_mut().perform_dialogue(target, dialogue);
2157                    },
2158                    HudEvent::SetBattleMode(mode) => {
2159                        self.client.borrow_mut().set_battle_mode(mode);
2160                    },
2161                }
2162            }
2163
2164            {
2165                let client = self.client.borrow();
2166                let scene_data = SceneData {
2167                    client: &client,
2168                    state: client.state(),
2169                    viewpoint_entity,
2170                    mutable_viewpoint: mutable_viewpoint || self.free_look,
2171                    // Only highlight if interactable
2172                    target_entities: &self.interactables.entities,
2173                    loaded_distance: client.loaded_distance(),
2174                    terrain_view_distance: client.view_distance().unwrap_or(1),
2175                    entity_view_distance: client
2176                        .view_distance()
2177                        .unwrap_or(1)
2178                        .min(global_state.settings.graphics.entity_view_distance),
2179                    tick: client.get_tick(),
2180                    gamma: global_state.settings.graphics.gamma,
2181                    exposure: global_state.settings.graphics.exposure,
2182                    ambiance: global_state.settings.graphics.ambiance,
2183                    mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable,
2184                    sprite_render_distance: global_state.settings.graphics.sprite_render_distance
2185                        as f32,
2186                    particles_enabled: global_state.settings.graphics.particles_enabled,
2187                    weapon_trails_enabled: global_state.settings.graphics.weapon_trails_enabled,
2188                    flashing_lights_enabled: global_state
2189                        .settings
2190                        .graphics
2191                        .render_mode
2192                        .flashing_lights_enabled,
2193                    figure_lod_render_distance: global_state
2194                        .settings
2195                        .graphics
2196                        .figure_lod_render_distance
2197                        as f32,
2198                    is_aiming,
2199                    interpolated_time_of_day: self.scene.interpolated_time_of_day,
2200                };
2201
2202                // Runs if either in a multiplayer server or the singleplayer server is unpaused
2203                if !global_state.paused() {
2204                    self.scene.maintain(
2205                        global_state.window.renderer_mut(),
2206                        &mut global_state.audio,
2207                        &scene_data,
2208                        &client,
2209                        &global_state.settings,
2210                    );
2211
2212                    // Process outcomes from client
2213                    for outcome in outcomes {
2214                        self.scene
2215                            .handle_outcome(&outcome, &scene_data, &mut global_state.audio);
2216                        self.hud
2217                            .handle_outcome(&outcome, scene_data.client, global_state);
2218                    }
2219                }
2220            }
2221
2222            // Clean things up after the tick.
2223            self.cleanup();
2224
2225            PlayStateResult::Continue
2226        } else if client_registered && client_presence.is_none() {
2227            // If the client cannot enter the game but spectate, pop the play state instead
2228            // of going back to character selection.
2229            if client_type.can_spectate() && !client_type.can_enter_character() {
2230                // Go back to the main menu state
2231                return PlayStateResult::Pop;
2232            } else {
2233                PlayStateResult::Switch(Box::new(CharSelectionState::new(
2234                    global_state,
2235                    Rc::clone(&self.client),
2236                    Rc::clone(&self.message_backlog),
2237                )))
2238            }
2239        } else {
2240            error!("Client not in the expected state, exiting session play state");
2241            PlayStateResult::Pop
2242        }
2243    }
2244
2245    fn name(&self) -> &'static str { "Session" }
2246
2247    fn capped_fps(&self) -> bool { false }
2248
2249    fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
2250
2251    /// Render the session to the screen.
2252    ///
2253    /// This method should be called once per frame.
2254    fn render(&self, drawer: &mut Drawer<'_>, settings: &Settings) {
2255        span!(_guard, "render", "<Session as PlayState>::render");
2256
2257        let client = self.client.borrow();
2258
2259        let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
2260
2261        let scene_data = SceneData {
2262            client: &client,
2263            state: client.state(),
2264            viewpoint_entity,
2265            mutable_viewpoint,
2266            // Only highlight if interactable
2267            target_entities: &self.interactables.entities,
2268            loaded_distance: client.loaded_distance(),
2269            terrain_view_distance: client.view_distance().unwrap_or(1),
2270            entity_view_distance: client
2271                .view_distance()
2272                .unwrap_or(1)
2273                .min(settings.graphics.entity_view_distance),
2274            tick: client.get_tick(),
2275            gamma: settings.graphics.gamma,
2276            exposure: settings.graphics.exposure,
2277            ambiance: settings.graphics.ambiance,
2278            mouse_smoothing: settings.gameplay.smooth_pan_enable,
2279            sprite_render_distance: settings.graphics.sprite_render_distance as f32,
2280            figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32,
2281            particles_enabled: settings.graphics.particles_enabled,
2282            weapon_trails_enabled: settings.graphics.weapon_trails_enabled,
2283            flashing_lights_enabled: settings.graphics.render_mode.flashing_lights_enabled,
2284            is_aiming: self.is_aiming,
2285            interpolated_time_of_day: self.scene.interpolated_time_of_day,
2286        };
2287
2288        // Render world
2289        self.scene.render(
2290            drawer,
2291            client.state(),
2292            viewpoint_entity,
2293            client.get_tick(),
2294            &scene_data,
2295        );
2296
2297        if let Some(mut volumetric_pass) = drawer.volumetric_pass() {
2298            // Clouds
2299            prof_span!("clouds");
2300            volumetric_pass.draw_clouds();
2301        }
2302        if let Some(mut transparent_pass) = drawer.transparent_pass() {
2303            // Trails
2304            prof_span!("trails");
2305            if let Some(mut trail_drawer) = transparent_pass.draw_trails() {
2306                self.scene
2307                    .trail_mgr()
2308                    .render(&mut trail_drawer, &scene_data);
2309            }
2310        }
2311        // Bloom (call does nothing if bloom is off)
2312        {
2313            prof_span!("bloom");
2314            drawer.run_bloom_passes()
2315        }
2316        // PostProcess and UI
2317        {
2318            prof_span!("post-process and ui");
2319            let mut third_pass = drawer.third_pass();
2320            third_pass.draw_postprocess();
2321            // Draw the UI to the screen
2322            if let Some(mut ui_drawer) = third_pass.draw_ui() {
2323                self.hud.render(&mut ui_drawer);
2324            }; // Note: this semicolon is needed for the third_pass borrow to be
2325            // dropped before it's lifetime ends
2326        }
2327    }
2328
2329    fn egui_enabled(&self) -> bool { true }
2330}
2331
2332fn find_shortest_distance(arr: &[Option<f32>]) -> Option<f32> {
2333    arr.iter()
2334        .filter_map(|x| *x)
2335        .min_by(|d1, d2| OrderedFloat(*d1).cmp(&OrderedFloat(*d2)))
2336}
2337
2338// TODO: Can probably be exported in some way for AI, somehow
2339fn auto_glide(
2340    fluid: Fluid,
2341    vel: Vel,
2342    free_look: bool,
2343    dir_forward_xy: Vec2<f32>,
2344    dir_right: Vec3<f32>,
2345) -> Option<Dir> {
2346    let Vel(rel_flow) = fluid.relative_flow(&vel);
2347
2348    let is_wind_downwards = rel_flow.z.is_sign_negative();
2349
2350    let dir = if free_look {
2351        if is_wind_downwards {
2352            Vec3::from(-rel_flow.xy())
2353        } else {
2354            -rel_flow
2355        }
2356    } else if is_wind_downwards {
2357        dir_forward_xy.into()
2358    } else {
2359        let windwards = rel_flow * dir_forward_xy.dot(rel_flow.xy()).signum();
2360        Plane::from(Dir::new(dir_right)).projection(windwards)
2361    };
2362
2363    Dir::from_unnormalized(dir)
2364}