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