veloren_voxygen/session/
mod.rs

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