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