veloren_voxygen/session/
mod.rs

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