Skip to main content

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