veloren_voxygen/session/
mod.rs

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