veloren_voxygen/session/
mod.rs

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