veloren_voxygen/session/
mod.rs

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