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