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