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 terrain_target,
701 &self.scene,
702 ) {
703 Ok(input_map) => {
704 let entities = input_map
705 .values()
706 .filter_map(|(_, interactable)| {
707 if let Interactable::Entity { entity, .. } = interactable {
708 Some(*entity)
709 } else {
710 None
711 }
712 })
713 .collect::<HashSet<_>>();
714 self.interactables = interactable::Interactables {
715 input_map,
716 entities,
717 };
718 },
719 Err(error) => {
720 tracing::trace!(?error, "Getting interactables failed");
721 self.interactables = Default::default()
722 },
723 }
724
725 drop(client);
726
727 self.maybe_auto_zoom_lock(
728 global_state.settings.gameplay.zoom_lock,
729 global_state.settings.gameplay.zoom_lock_behavior,
730 );
731
732 if presence == PresenceKind::Spectator {
733 let mut client = self.client.borrow_mut();
734 if client.spectate_position(cam_pos) {
735 let server_name = &client.server_info().name;
736 global_state.profile.set_spectate_position(
737 server_name,
738 Some(self.scene.camera().get_focus_pos()),
739 );
740 }
741 }
742
743 let nearest_block_dist = find_shortest_distance(&[
745 mine_target
746 .filter(|_| active_mine_tool.is_some())
747 .map(|t| t.distance),
748 build_target.filter(|_| can_build).map(|t| t.distance),
749 ]);
750 let nearest_scene_dist = find_shortest_distance(&[
752 nearest_block_dist,
753 collect_target
754 .filter(|_| active_mine_tool.is_none())
755 .map(|t| t.distance),
756 ]);
757 self.inputs.break_block_pos = if let Some(mt) = mine_target
759 .filter(|mt| active_mine_tool.is_some() && nearest_scene_dist == Some(mt.distance))
760 {
761 self.scene.set_select_pos(Some(mt.position_int()));
762 Some(mt.position)
763 } else if let Some(bt) =
764 build_target.filter(|bt| can_build && nearest_scene_dist == Some(bt.distance))
765 {
766 self.scene.set_select_pos(Some(bt.position_int()));
767 None
768 } else if let Some(ct) =
769 collect_target.filter(|ct| nearest_scene_dist == Some(ct.distance))
770 {
771 self.scene.set_select_pos(Some(ct.position_int()));
772 None
773 } else {
774 self.scene.set_select_pos(None);
775 None
776 };
777
778 let default_select_pos = terrain_target.map(|tt| tt.position);
780
781 self.target_entity = entity_target.map(|t| t.kind.0);
783
784 let controlling_char = self.controlling_char();
785
786 for event in events {
788 {
790 let client = self.client.borrow();
791 let inventories = client.inventories();
792 let inventory = inventories.get(client.entity());
793 if self
794 .hud
795 .handle_event(event.clone(), global_state, inventory)
796 {
797 continue;
798 }
799 }
800 match event {
801 Event::Close => {
802 return PlayStateResult::Shutdown;
803 },
804 Event::InputUpdate(input, state)
805 if state != self.inputs_state.contains(&input) =>
806 {
807 if !self.inputs_state.insert(input) {
808 self.inputs_state.remove(&input);
809 }
810 match input {
811 GameInput::Primary => {
812 self.walking_speed = false;
813 let mut client = self.client.borrow_mut();
814 if let Some(build_target) = build_target.filter(|bt| {
818 state && can_build && nearest_block_dist == Some(bt.distance)
819 }) {
820 client.remove_block(build_target.position_int());
821 } else {
822 client.handle_input(
823 InputKind::Primary,
824 state,
825 default_select_pos,
826 self.target_entity,
827 );
828 }
829 },
830 GameInput::Secondary => {
831 self.walking_speed = false;
832 let mut client = self.client.borrow_mut();
833 if let Some(build_target) = build_target.filter(|bt| {
834 state && can_build && nearest_block_dist == Some(bt.distance)
835 }) {
836 let selected_pos = build_target.kind.0;
837 client.place_block(
838 selected_pos.map(|p| p.floor() as i32),
839 self.selected_block,
840 );
841 } else {
842 client.handle_input(
843 InputKind::Secondary,
844 state,
845 default_select_pos,
846 self.target_entity,
847 );
848 }
849 },
850 GameInput::Block => {
851 self.walking_speed = false;
852 self.client.borrow_mut().handle_input(
853 InputKind::Block,
854 state,
855 None,
856 self.target_entity,
857 );
858 },
859 GameInput::Roll => {
860 self.walking_speed = false;
861 let mut client = self.client.borrow_mut();
862 if can_build {
863 if state {
864 if let Some(block) = build_target.and_then(|bt| {
865 client
866 .state()
867 .terrain()
868 .get(bt.position_int())
869 .ok()
870 .copied()
871 }) {
872 self.selected_block = block;
873 }
874 }
875 } else if controlling_char {
876 client.handle_input(
877 InputKind::Roll,
878 state,
879 None,
880 self.target_entity,
881 );
882 }
883 },
884 GameInput::GiveUp => {
885 self.key_state.give_up = state.then_some(0.0).filter(|_| {
886 let client = self.client.borrow();
887 comp::is_downed(
888 client.current().as_ref(),
889 client.current().as_ref(),
890 )
891 });
892 },
893 GameInput::Respawn => {
894 self.walking_speed = false;
895 self.stop_auto_walk();
896 if state {
897 self.client.borrow_mut().respawn();
898 }
899 },
900 GameInput::Jump => {
901 self.walking_speed = false;
902 self.client.borrow_mut().handle_input(
903 InputKind::Jump,
904 state,
905 None,
906 self.target_entity,
907 );
908 },
909 GameInput::SwimUp => {
910 self.key_state.swim_up = state;
911 },
912 GameInput::SwimDown => {
913 self.key_state.swim_down = state;
914 },
915 GameInput::Sit => {
916 if state && controlling_char {
917 self.stop_auto_walk();
918 self.client.borrow_mut().toggle_sit();
919 }
920 },
921 GameInput::Crawl => {
922 if state && controlling_char {
923 self.stop_auto_walk();
924 self.client.borrow_mut().toggle_crawl();
925 }
926 },
927 GameInput::Dance => {
928 if state && controlling_char {
929 self.stop_auto_walk();
930 self.client.borrow_mut().toggle_dance();
931 }
932 },
933 GameInput::Greet => {
934 if state {
935 self.client.borrow_mut().utter(UtteranceKind::Greeting);
936 }
937 },
938 GameInput::Sneak => {
939 let is_trading = self.client.borrow().is_trading();
940 if state && !is_trading && controlling_char {
941 self.stop_auto_walk();
942 self.client.borrow_mut().toggle_sneak();
943 }
944 },
945 GameInput::CancelClimb => {
946 if state && controlling_char {
947 self.client.borrow_mut().cancel_climb();
948 }
949 },
950 GameInput::MoveForward => {
951 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
952 self.stop_auto_walk();
953 }
954 self.key_state.up = state
955 },
956 GameInput::MoveBack => {
957 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
958 self.stop_auto_walk();
959 }
960 self.key_state.down = state
961 },
962 GameInput::MoveLeft => {
963 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
964 self.stop_auto_walk();
965 }
966 self.key_state.left = state
967 },
968 GameInput::MoveRight => {
969 if state && global_state.settings.gameplay.stop_auto_walk_on_input {
970 self.stop_auto_walk();
971 }
972 self.key_state.right = state
973 },
974 GameInput::Glide => {
975 self.walking_speed = false;
976 let is_trading = self.client.borrow().is_trading();
977 if state && !is_trading && controlling_char {
978 if global_state.settings.gameplay.stop_auto_walk_on_input {
979 self.stop_auto_walk();
980 }
981 self.client.borrow_mut().toggle_glide();
982 }
983 },
984 GameInput::Fly => {
985 self.key_state.fly ^= state;
991 self.client.borrow_mut().handle_input(
992 InputKind::Fly,
993 self.key_state.fly,
994 None,
995 self.target_entity,
996 );
997 },
998 GameInput::ToggleWield => {
999 if state && controlling_char {
1000 let mut client = self.client.borrow_mut();
1001 if client.is_wielding().is_some_and(|b| !b) {
1002 self.walking_speed = false;
1003 }
1004 client.toggle_wield();
1005 }
1006 },
1007 GameInput::SwapLoadout => {
1008 if state && controlling_char {
1009 self.client.borrow_mut().swap_loadout();
1010 }
1011 },
1012 GameInput::ToggleLantern if state && controlling_char => {
1013 let mut client = self.client.borrow_mut();
1014 if client.is_lantern_enabled() {
1015 client.disable_lantern();
1016 } else {
1017 client.enable_lantern();
1018 }
1019 },
1020 GameInput::Mount if state && controlling_char => {
1021 let mut client = self.client.borrow_mut();
1022 if client.is_riding() {
1023 client.unmount();
1024 } else if let Some((_, interactable)) =
1025 self.interactables.input_map.get(&GameInput::Mount)
1026 {
1027 match interactable {
1028 Interactable::Block { volume_pos, .. } => {
1029 client.mount_volume(*volume_pos)
1030 },
1031 Interactable::Entity { entity, .. } => {
1032 client.mount(*entity)
1033 },
1034 }
1035 }
1036 },
1037 GameInput::StayFollow if state => {
1038 let mut client = self.client.borrow_mut();
1039 let player_pos = client
1040 .state()
1041 .read_storage::<Pos>()
1042 .get(client.entity())
1043 .copied();
1044
1045 let mut close_pet = None;
1046 if let Some(player_pos) = player_pos {
1047 let positions = client.state().read_storage::<Pos>();
1048 close_pet = client.state().ecs().read_resource::<CachedSpatialGrid>().0
1049 .in_circle_aabr(player_pos.0.xy(), MAX_MOUNT_RANGE)
1050 .filter(|e|
1051 *e != client.entity()
1052 )
1053 .filter(|e|
1054 matches!(client.state().ecs().read_storage::<comp::Alignment>().get(*e),
1055 Some(comp::Alignment::Owned(owner)) if Some(*owner) == client.uid())
1056 )
1057 .filter(|e|
1058 client.state().ecs().read_storage::<Is<Mount>>().get(*e).is_none()
1059 )
1060 .min_by_key(|e| {
1061 OrderedFloat(positions
1062 .get(*e)
1063 .map_or(MAX_MOUNT_RANGE * MAX_MOUNT_RANGE, |x| {
1064 player_pos.0.distance_squared(x.0)
1065 }
1066 ))
1067 });
1068 }
1069 if let Some(pet_entity) = close_pet
1070 && client
1071 .state()
1072 .read_storage::<Is<Mount>>()
1073 .get(pet_entity)
1074 .is_none()
1075 {
1076 let is_staying = client
1077 .state()
1078 .read_storage::<CharacterActivity>()
1079 .get(pet_entity)
1080 .is_some_and(|activity| activity.is_pet_staying);
1081 client.set_pet_stay(pet_entity, !is_staying);
1082 }
1083 },
1084 GameInput::Interact => {
1085 if state {
1086 let mut client = self.client.borrow_mut();
1087 if let Some((_, interactable)) =
1088 self.interactables.input_map.get(&GameInput::Interact)
1089 {
1090 match interactable {
1091 Interactable::Block {
1092 volume_pos,
1093 block,
1094 interaction,
1095 ..
1096 } => {
1097 match interaction {
1098 BlockInteraction::Collect { .. }
1099 | BlockInteraction::Unlock { .. } => {
1100 if block.is_collectible(
1101 client
1102 .state()
1103 .terrain()
1104 .sprite_cfg_at(volume_pos.pos),
1105 ) {
1106 match volume_pos.kind {
1107 common::mounting::Volume::Terrain => {
1108 client.collect_block(volume_pos.pos);
1109 }
1110 common::mounting::Volume::Entity(_) => {
1111 },
1113 }
1114 }
1115 },
1116 BlockInteraction::Craft(tab) => {
1117 self.hud.show.open_crafting_tab(
1118 *tab,
1119 block
1120 .get_sprite()
1121 .map(|s| (*volume_pos, s)),
1122 )
1123 },
1124 BlockInteraction::Mine(_)
1125 | BlockInteraction::Mount => {},
1126 BlockInteraction::Read(content) => {
1127 match volume_pos.kind {
1128 common::mounting::Volume::Terrain => {
1129 self.hud.show_content_bubble(
1130 volume_pos.pos.as_()
1131 + Vec3::new(
1132 0.5,
1133 0.5,
1134 block.solid_height()
1135 * 0.75,
1136 ),
1137 content.clone(),
1138 )
1139 },
1140 common::mounting::Volume::Entity(_) => {
1143 },
1144 }
1145 },
1146 BlockInteraction::LightToggle(enable) => {
1147 client.toggle_sprite_light(
1148 *volume_pos,
1149 *enable,
1150 );
1151 },
1152 }
1153 },
1154 Interactable::Entity {
1155 entity,
1156 interaction,
1157 ..
1158 } => {
1159 match interaction {
1161 EntityInteraction::HelpDowned => {
1162 client.help_downed(*entity)
1163 },
1164 EntityInteraction::PickupItem => {
1165 client.pick_up(*entity)
1166 },
1167 EntityInteraction::ActivatePortal => {
1168 client.activate_portal(*entity)
1169 },
1170 EntityInteraction::Pet => {
1171 client.do_pet(*entity)
1172 },
1173 EntityInteraction::Talk => {
1174 client.npc_interact(*entity)
1175 },
1176 EntityInteraction::CampfireSit
1177 | EntityInteraction::Trade
1178 | EntityInteraction::StayFollow
1179 | EntityInteraction::Mount => {},
1180 }
1181 },
1182 }
1183 }
1184 }
1185 },
1186 GameInput::Trade => {
1187 if state && controlling_char {
1188 if let Some((_, Interactable::Entity { entity, .. })) =
1189 self.interactables.input_map.get(&GameInput::Trade)
1190 {
1191 let mut client = self.client.borrow_mut();
1192 if let Some(uid) =
1193 client.state().ecs().uid_from_entity(*entity)
1194 {
1195 let name = client
1196 .player_list()
1197 .get(&uid)
1198 .map(|info| info.player_alias.clone())
1199 .unwrap_or_else(|| {
1200 let stats =
1201 client.state().read_storage::<Stats>();
1202 stats.get(*entity).map_or(
1203 format!("<entity {:?}>", uid),
1204 |e| {
1205 global_state
1206 .i18n
1207 .read()
1208 .get_content(&e.name)
1209 },
1210 )
1211 });
1212
1213 self.hud.new_message(ChatType::Meta.into_msg(
1214 Content::localized_with_args(
1215 "hud-trade-invite_sent",
1216 [("playername", name)],
1217 ),
1218 ));
1219
1220 client.send_invite(uid, InviteKind::Trade)
1221 };
1222 }
1223 }
1224 },
1225 GameInput::FreeLook => {
1226 let hud = &mut self.hud;
1227 global_state.settings.gameplay.free_look_behavior.update(
1228 state,
1229 &mut self.free_look,
1230 |b| hud.free_look(b),
1231 );
1232 },
1233 GameInput::AutoWalk => {
1234 let hud = &mut self.hud;
1235 global_state.settings.gameplay.auto_walk_behavior.update(
1236 state,
1237 &mut self.auto_walk,
1238 |b| hud.auto_walk(b),
1239 );
1240
1241 self.key_state.auto_walk =
1242 self.auto_walk && !self.client.borrow().is_gliding();
1243 },
1244 GameInput::ZoomIn => {
1245 if state {
1246 if self.zoom_lock {
1247 self.hud.zoom_lock_reminder();
1248 } else {
1249 self.scene.handle_input_event(
1250 Event::Zoom(-30.0),
1251 &self.client.borrow(),
1252 );
1253 }
1254 }
1255 },
1256 GameInput::ZoomOut => {
1257 if state {
1258 if self.zoom_lock {
1259 self.hud.zoom_lock_reminder();
1260 } else {
1261 self.scene.handle_input_event(
1262 Event::Zoom(30.0),
1263 &self.client.borrow(),
1264 );
1265 }
1266 }
1267 },
1268 GameInput::ZoomLock => {
1269 if state {
1270 global_state.settings.gameplay.zoom_lock ^= true;
1271
1272 self.hud
1273 .zoom_lock_toggle(global_state.settings.gameplay.zoom_lock);
1274 }
1275 },
1276 GameInput::CameraClamp => {
1277 let hud = &mut self.hud;
1278 global_state.settings.gameplay.camera_clamp_behavior.update(
1279 state,
1280 &mut self.camera_clamp,
1281 |b| hud.camera_clamp(b),
1282 );
1283 },
1284 GameInput::CycleCamera if state => {
1285 let camera = self.scene.camera_mut();
1292 let client = self.client.borrow();
1293 camera.next_mode(
1294 client.is_moderator(),
1295 (client.presence() != Some(PresenceKind::Spectator))
1296 || self.viewpoint_entity.is_some(),
1297 );
1298 },
1299 GameInput::Select => {
1300 if !state {
1301 self.selected_entity =
1302 self.target_entity.map(|e| (e, std::time::Instant::now()));
1303 }
1304 },
1305 GameInput::AcceptGroupInvite if state => {
1306 let mut client = self.client.borrow_mut();
1307 if client.invite().is_some() {
1308 client.accept_invite();
1309 }
1310 },
1311 GameInput::DeclineGroupInvite if state => {
1312 let mut client = self.client.borrow_mut();
1313 if client.invite().is_some() {
1314 client.decline_invite();
1315 }
1316 },
1317 GameInput::SpectateViewpoint if state => {
1318 let mut client = self.client.borrow_mut();
1319 if self.viewpoint_entity.is_some() {
1320 client.stop_spectate_entity();
1321 self.viewpoint_entity = None;
1322 self.scene.camera_mut().set_mode(CameraMode::Freefly);
1323 let mut ori = self.scene.camera().get_orientation();
1324 ori.z = 0.0;
1327 self.scene.camera_mut().set_orientation(ori);
1328 } else if let Some(target_entity) = entity_target {
1329 if self.scene.camera().get_mode() == CameraMode::Freefly {
1330 client.start_spectate_entity(target_entity.kind.0);
1333
1334 self.viewpoint_entity = Some(target_entity.kind.0);
1335 self.scene.camera_mut().set_mode(CameraMode::FirstPerson);
1336 }
1337 }
1338 },
1339 GameInput::ToggleWalk if state => {
1340 global_state
1341 .settings
1342 .gameplay
1343 .walking_speed_behavior
1344 .update(state, &mut self.walking_speed, |_| {});
1345 },
1346 _ => {},
1347 }
1348 },
1349 Event::AnalogGameInput(input) => match input {
1350 AnalogGameInput::MovementX(v) => {
1351 self.key_state.analog_matrix.x = v;
1352 },
1353 AnalogGameInput::MovementY(v) => {
1354 self.key_state.analog_matrix.y = v;
1355 },
1356 other => {
1357 self.scene.handle_input_event(
1358 Event::AnalogGameInput(other),
1359 &self.client.borrow(),
1360 );
1361 },
1362 },
1363
1364 Event::ScreenshotMessage(screenshot_msg) => self
1366 .hud
1367 .new_message(ChatType::CommandInfo.into_plain_msg(screenshot_msg)),
1368
1369 Event::Zoom(delta) if self.zoom_lock => {
1370 if delta.abs() > ZOOM_LOCK_SCROLL_DELTA_INTENT {
1372 self.hud.zoom_lock_reminder();
1373 }
1374 },
1375
1376 event => {
1378 self.scene.handle_input_event(event, &self.client.borrow());
1379 }, }
1381 }
1382
1383 if let Some(viewpoint_entity) = self.viewpoint_entity
1384 && !self
1385 .client
1386 .borrow()
1387 .state()
1388 .ecs()
1389 .read_storage::<Pos>()
1390 .contains(viewpoint_entity)
1391 {
1392 self.client.borrow_mut().stop_spectate_entity();
1393 self.viewpoint_entity = None;
1394 self.scene.camera_mut().set_mode(CameraMode::Freefly);
1395 }
1396
1397 let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
1398
1399 let input_vec = self.key_state.dir_vec();
1401 let (axis_right, axis_up) = (input_vec[0], input_vec[1]);
1402 let dt = global_state.clock.get_stable_dt().as_secs_f32();
1403
1404 if let Some(ref mut timer) = self.key_state.give_up {
1405 *timer += dt;
1406
1407 if *timer > crate::key_state::GIVE_UP_HOLD_TIME {
1408 self.client.borrow_mut().give_up();
1409 }
1410 }
1411
1412 if mutable_viewpoint {
1413 if let Some(dir) = self
1415 .auto_walk
1416 .then_some(self.client.borrow())
1417 .filter(|client| client.is_gliding())
1418 .and_then(|client| {
1419 let ecs = client.state().ecs();
1420 let entity = client.entity();
1421 let fluid = ecs
1422 .read_storage::<comp::PhysicsState>()
1423 .get(entity)?
1424 .in_fluid?;
1425 let vel = *ecs.read_storage::<Vel>().get(entity)?;
1426 let free_look = self.free_look;
1427 let dir_forward_xy = self.scene.camera().forward_xy();
1428 let dir_right = self.scene.camera().right();
1429
1430 auto_glide(fluid, vel, free_look, dir_forward_xy, dir_right)
1431 })
1432 {
1433 self.key_state.auto_walk = false;
1434 self.inputs.move_dir = Vec2::zero();
1435 self.inputs.look_dir = dir;
1436 } else {
1437 self.key_state.auto_walk = self.auto_walk;
1438 if !self.free_look {
1439 self.walk_forward_dir = self.scene.camera().forward_xy();
1440 self.walk_right_dir = self.scene.camera().right_xy();
1441
1442 let client = self.client.borrow();
1443
1444 let holding_ranged = client
1445 .inventories()
1446 .get(player_entity)
1447 .and_then(|inv| inv.equipped(EquipSlot::ActiveMainhand))
1448 .and_then(|item| item.tool_info())
1449 .is_some_and(|tool_kind| {
1450 matches!(
1451 tool_kind,
1452 ToolKind::Bow
1453 | ToolKind::Staff
1454 | ToolKind::Sceptre
1455 | ToolKind::Throwable
1456 )
1457 })
1458 || client
1459 .current::<CharacterState>()
1460 .is_some_and(|char_state| {
1461 matches!(char_state, CharacterState::Throw(_))
1462 });
1463
1464 let dir = if is_aiming
1465 && holding_ranged
1466 && self.scene.camera().get_mode() == CameraMode::ThirdPerson
1467 {
1468 let ray_start = self.scene.camera().get_focus_pos();
1473 let entity_ray_end = ray_start + cam_dir * 1000.0;
1474 let terrain_ray_end = ray_start + cam_dir * 1000.0;
1475
1476 let aim_point = {
1477 let entity_dist =
1479 ray_entities(&client, ray_start, entity_ray_end, 1000.0).0;
1480 let terrain_ray_distance = client
1481 .state()
1482 .terrain()
1483 .ray(ray_start, terrain_ray_end)
1484 .max_iter(1000)
1485 .until(Block::is_solid)
1486 .cast()
1487 .0;
1488
1489 ray_start + cam_dir * entity_dist.min(terrain_ray_distance)
1491 };
1492
1493 let ori = client
1495 .state()
1496 .read_storage::<comp::Ori>()
1497 .get(player_entity)
1498 .copied()
1499 .unwrap();
1500 let scale = client
1502 .state()
1503 .read_storage::<comp::Scale>()
1504 .get(player_entity)
1505 .copied()
1506 .unwrap_or(comp::Scale(1.0));
1507 let body = client
1509 .state()
1510 .read_storage::<comp::Body>()
1511 .get(player_entity)
1512 .copied()
1513 .unwrap();
1514 let body_offsets = body.projectile_offsets(ori.look_vec(), scale.0);
1515
1516 let player_pos = client
1518 .state()
1519 .read_storage::<Pos>()
1520 .get(player_entity)
1521 .copied()
1522 .unwrap();
1523
1524 drop(client);
1525 aim_point - (player_pos.0 + body_offsets)
1526 } else {
1527 cam_dir + aim_dir_offset
1528 };
1529
1530 self.inputs.look_dir = Dir::from_unnormalized(dir).unwrap();
1531 }
1532 }
1533 self.inputs.strafing = matches!(
1534 self.scene.camera().get_mode(),
1535 camera::CameraMode::FirstPerson
1536 );
1537
1538 if global_state.settings.gameplay.auto_camera
1540 && matches!(
1541 self.scene.camera().get_mode(),
1542 camera::CameraMode::ThirdPerson | camera::CameraMode::FirstPerson
1543 )
1544 && input_vec.magnitude_squared() > 0.0
1545 {
1546 let camera = self.scene.camera_mut();
1547 let ori = camera.get_orientation();
1548 camera.set_orientation_instant(Vec3::new(
1549 ori.x
1550 + input_vec.x
1551 * (3.0 - input_vec.y * 1.5 * if is_aiming { 1.5 } else { 1.0 })
1552 * dt,
1553 std::f32::consts::PI * if is_aiming { 0.015 } else { 0.1 },
1554 0.0,
1555 ));
1556 }
1557
1558 self.inputs.move_z =
1559 self.key_state.swim_up as i32 as f32 - self.key_state.swim_down as i32 as f32;
1560 }
1561
1562 match self.scene.camera().get_mode() {
1563 CameraMode::FirstPerson | CameraMode::ThirdPerson => {
1564 if mutable_viewpoint {
1565 self.inputs.move_dir =
1569 self.walk_right_dir * axis_right + self.walk_forward_dir * axis_up;
1570 }
1571 },
1572 CameraMode::Freefly => {
1573 const FREEFLY_SPEED: f32 = 50.0;
1576 const FREEFLY_SPEED_BOOST: f32 = 5.0;
1577
1578 let forward = self.scene.camera().forward().with_z(0.0).normalized();
1579 let right = self.scene.camera().right().with_z(0.0).normalized();
1580 let up = Vec3::unit_z();
1581 let up_axis = self.key_state.swim_up as i32 as f32
1582 - self.key_state.swim_down as i32 as f32;
1583
1584 let dir = (right * axis_right + forward * axis_up + up * up_axis).normalized();
1585
1586 let speed = FREEFLY_SPEED
1587 * if self.inputs_state.contains(&GameInput::SpectateSpeedBoost) {
1588 FREEFLY_SPEED_BOOST
1589 } else {
1590 1.0
1591 };
1592
1593 let pos = self.scene.camera().get_focus_pos();
1594 self.scene
1595 .camera_mut()
1596 .set_focus_pos(pos + dir * dt * speed);
1597
1598 self.inputs.move_dir = Vec2::zero();
1600 },
1601 };
1602
1603 let mut outcomes = Vec::new();
1604
1605 if !global_state.paused() {
1607 match self.tick(
1609 global_state.clock.get_stable_dt(),
1610 global_state,
1611 &mut outcomes,
1612 ) {
1613 Ok(TickAction::Continue) => {}, Ok(TickAction::Disconnect) => return PlayStateResult::Pop, Err(Error::ClientError(error)) => {
1616 error!("[session] Failed to tick the scene: {:?}", error);
1617 global_state.info_message =
1618 Some(get_client_msg_error(error, None, &global_state.i18n.read()));
1619
1620 return PlayStateResult::Pop;
1621 },
1622 Err(err) => {
1623 global_state.info_message = Some(
1624 global_state
1625 .i18n
1626 .read()
1627 .get_msg("common-connection_lost")
1628 .into_owned(),
1629 );
1630 error!("[session] Failed to tick the scene: {:?}", err);
1631
1632 return PlayStateResult::Pop;
1633 },
1634 }
1635 }
1636
1637 if self.walking_speed {
1638 self.key_state.speed_mul = global_state.settings.gameplay.walking_speed;
1639 } else {
1640 self.key_state.speed_mul = 1.0;
1641 }
1642
1643 self.scene
1645 .camera_mut()
1646 .compute_dependents(&self.client.borrow().state().terrain());
1647
1648 let debug_info = global_state.settings.interface.toggle_debug.then(|| {
1652 let client = self.client.borrow();
1653 let ecs = client.state().ecs();
1654 let client_entity = client.entity();
1655 let coordinates = ecs.read_storage::<Pos>().get(viewpoint_entity).cloned();
1656 let velocity = ecs.read_storage::<Vel>().get(viewpoint_entity).cloned();
1657 let ori = ecs
1658 .read_storage::<comp::Ori>()
1659 .get(viewpoint_entity)
1660 .cloned();
1661 let look_dir = if viewpoint_entity == client_entity {
1664 self.inputs.look_dir
1665 } else {
1666 ecs.read_storage::<comp::Controller>()
1667 .get(viewpoint_entity)
1668 .map(|c| c.inputs.look_dir)
1669 .unwrap_or_default()
1670 };
1671 let in_fluid = ecs
1672 .read_storage::<comp::PhysicsState>()
1673 .get(viewpoint_entity)
1674 .and_then(|state| state.in_fluid);
1675 let character_state = ecs
1676 .read_storage::<comp::CharacterState>()
1677 .get(viewpoint_entity)
1678 .cloned();
1679
1680 DebugInfo {
1681 tps: global_state.clock.stats().average_tps,
1682 frame_time: global_state.clock.stats().average_busy_dt,
1683 ping_ms: self.client.borrow().get_ping_ms_rolling_avg(),
1684 coordinates,
1685 velocity,
1686 ori,
1687 look_dir,
1688 character_state,
1689 in_fluid,
1690 num_chunks: self.scene.terrain().chunk_count() as u32,
1691 num_lights: self.scene.lights().len() as u32,
1692 num_visible_chunks: self.scene.terrain().visible_chunk_count() as u32,
1693 num_shadow_chunks: self.scene.terrain().shadow_chunk_count() as u32,
1694 num_figures: self.scene.figure_mgr().figure_count() as u32,
1695 num_figures_visible: self.scene.figure_mgr().figure_count_visible() as u32,
1696 num_particles: self.scene.particle_mgr().particle_count() as u32,
1697 num_particles_visible: self.scene.particle_mgr().particle_count_visible()
1698 as u32,
1699 current_track: self.scene.music_mgr().current_track(),
1700 current_artist: self.scene.music_mgr().current_artist(),
1701 active_channels: global_state.audio.get_num_active_channels(),
1702 audio_cpu_usage: global_state.audio.get_cpu_usage(),
1703 }
1704 });
1705
1706 let inverted_interactable_map = self.interactables.inverted_map();
1707
1708 let mut hud_events = self.hud.maintain(
1710 &self.client.borrow(),
1711 global_state,
1712 &debug_info,
1713 self.scene.camera(),
1714 global_state.clock.get_stable_dt(),
1715 HudInfo {
1716 is_aiming,
1717 active_mine_tool,
1718 is_first_person: matches!(
1719 self.scene.camera().get_mode(),
1720 camera::CameraMode::FirstPerson
1721 ),
1722 viewpoint_entity,
1723 mutable_viewpoint,
1724 target_entity: self.target_entity,
1725 selected_entity: self.selected_entity,
1726 persistence_load_error: self.metadata.skill_set_persistence_load_error,
1727 key_state: &self.key_state,
1728 },
1729 inverted_interactable_map,
1730 );
1731
1732 #[cfg(feature = "egui-ui")]
1734 if global_state.settings.interface.egui_enabled() {
1735 let settings_change = global_state.egui_state.maintain(
1736 &mut self.client.borrow_mut(),
1737 &mut self.scene,
1738 debug_info.map(|debug_info| EguiDebugInfo {
1739 frame_time: debug_info.frame_time,
1740 ping_ms: debug_info.ping_ms,
1741 }),
1742 &global_state.settings,
1743 );
1744
1745 if let Some(settings_change) = settings_change {
1746 settings_change.process(global_state, self);
1747 }
1748 }
1749
1750 if global_state.i18n.reloaded() {
1752 hud_events.push(HudEvent::SettingsChange(
1753 ChangeLanguage(Box::new(global_state.i18n.read().metadata().clone())).into(),
1754 ));
1755 }
1756
1757 let mut has_repaired = false;
1758 let sfx_triggers = self.scene.sfx_mgr.triggers.read();
1759 for event in hud_events {
1761 match event {
1762 HudEvent::SendMessage(msg) => {
1763 self.client.borrow_mut().send_chat(msg);
1765 },
1766 HudEvent::SendCommand(name, args) => {
1767 match run_command(self, global_state, &name, args) {
1768 Ok(Some(info)) => {
1769 self.hud.new_message(ChatType::CommandInfo.into_msg(info))
1770 },
1771 Ok(None) => {}, Err(error) => {
1773 self.hud.new_message(ChatType::CommandError.into_msg(error))
1774 },
1775 };
1776 },
1777 HudEvent::CharacterSelection => {
1778 global_state.audio.stop_all_music();
1779 global_state.audio.stop_all_ambience();
1780 global_state.audio.stop_all_sfx();
1781 self.client.borrow_mut().request_remove_character()
1782 },
1783 HudEvent::Logout => {
1784 self.client.borrow_mut().logout();
1785 global_state.audio.stop_all_ambience();
1789 global_state.audio.stop_all_sfx();
1790 return PlayStateResult::Pop;
1791 },
1792 HudEvent::Quit => {
1793 return PlayStateResult::Shutdown;
1794 },
1795
1796 HudEvent::RemoveBuff(buff_id) => {
1797 self.client.borrow_mut().remove_buff(buff_id);
1798 },
1799 HudEvent::LeaveStance => self.client.borrow_mut().leave_stance(),
1800 HudEvent::UnlockSkill(skill) => {
1801 self.client.borrow_mut().unlock_skill(skill);
1802 },
1803 HudEvent::UseSlot {
1804 slot,
1805 bypass_dialog,
1806 } => {
1807 let mut move_allowed = true;
1808
1809 if !bypass_dialog {
1810 if let Some(inventory) = self
1811 .client
1812 .borrow()
1813 .state()
1814 .ecs()
1815 .read_storage::<comp::Inventory>()
1816 .get(self.client.borrow().entity())
1817 {
1818 match slot {
1819 Slot::Inventory(inv_slot) => {
1820 let slot_deficit = inventory.free_after_equip(inv_slot);
1821 if slot_deficit < 0 {
1822 self.hud.set_prompt_dialog(PromptDialogSettings::new(
1823 global_state.i18n.read().get_content(
1824 &Content::localized_with_args(
1825 "hud-bag-use_slot_equip_drop_items",
1826 [(
1827 "slot_deficit",
1828 slot_deficit.unsigned_abs() as u64,
1829 )],
1830 ),
1831 ),
1832 HudEvent::UseSlot {
1833 slot,
1834 bypass_dialog: true,
1835 },
1836 None,
1837 ));
1838 move_allowed = false;
1839 }
1840 },
1841 Slot::Equip(equip_slot) => {
1842 let free_slots =
1845 inventory.free_slots_minus_equipped_item(equip_slot);
1846 if free_slots > 0 {
1847 let slot_deficit =
1848 inventory.free_after_unequip(equip_slot);
1849 if slot_deficit < 0 {
1850 self.hud
1851 .set_prompt_dialog(PromptDialogSettings::new(
1852 global_state.i18n.read().get_content(
1853 &Content::localized_with_args(
1854 "hud-bag-use_slot_unequip_drop_items",
1855 [(
1856 "slot_deficit",
1857 slot_deficit.unsigned_abs() as u64,
1858 )],
1859 ),
1860 ),
1861 HudEvent::UseSlot {
1862 slot,
1863 bypass_dialog: true,
1864 },
1865 None,
1866 ));
1867 move_allowed = false;
1868 }
1869 } else {
1870 move_allowed = false;
1871 }
1872 },
1873 Slot::Overflow(_) => {},
1874 }
1875 };
1876 }
1877
1878 if move_allowed {
1879 self.client.borrow_mut().use_slot(slot);
1880 }
1881 },
1882 HudEvent::SwapEquippedWeapons => {
1883 self.client.borrow_mut().swap_loadout();
1884 },
1885 HudEvent::SwapSlots {
1886 slot_a,
1887 slot_b,
1888 bypass_dialog,
1889 } => {
1890 let mut move_allowed = true;
1891 if !bypass_dialog {
1892 if let Some(inventory) = self
1893 .client
1894 .borrow()
1895 .state()
1896 .ecs()
1897 .read_storage::<comp::Inventory>()
1898 .get(self.client.borrow().entity())
1899 {
1900 match (slot_a, slot_b) {
1901 (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1902 | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1903 if !inventory.can_swap(inv_slot, equip_slot) {
1904 move_allowed = false;
1905 } else {
1906 let slot_deficit =
1907 inventory.free_after_swap(equip_slot, inv_slot);
1908 if slot_deficit < 0 {
1909 self.hud.set_prompt_dialog(
1910 PromptDialogSettings::new(
1911 global_state.i18n.read().get_content(
1912 &Content::localized_with_args(
1913 "hud-bag-swap_slots_drop_items",
1914 [(
1915 "slot_deficit",
1916 slot_deficit.unsigned_abs()
1917 as u64,
1918 )],
1919 ),
1920 ),
1921 HudEvent::SwapSlots {
1922 slot_a,
1923 slot_b,
1924 bypass_dialog: true,
1925 },
1926 None,
1927 ),
1928 );
1929 move_allowed = false;
1930 }
1931 }
1932 },
1933 _ => {},
1934 }
1935 }
1936 }
1937 if move_allowed {
1938 self.client.borrow_mut().swap_slots(slot_a, slot_b);
1939 }
1940 },
1941 HudEvent::SelectExpBar(skillgroup) => {
1942 global_state.settings.interface.xp_bar_skillgroup = skillgroup;
1943 },
1944 HudEvent::SplitSwapSlots {
1945 slot_a,
1946 slot_b,
1947 bypass_dialog,
1948 } => {
1949 let mut move_allowed = true;
1950 if !bypass_dialog {
1951 if let Some(inventory) = self
1952 .client
1953 .borrow()
1954 .state()
1955 .ecs()
1956 .read_storage::<comp::Inventory>()
1957 .get(self.client.borrow().entity())
1958 {
1959 match (slot_a, slot_b) {
1960 (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1961 | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1962 if !inventory.can_swap(inv_slot, equip_slot) {
1963 move_allowed = false;
1964 } else {
1965 let slot_deficit =
1966 inventory.free_after_swap(equip_slot, inv_slot);
1967 if slot_deficit < 0 {
1968 self.hud
1969 .set_prompt_dialog(PromptDialogSettings::new(
1970 global_state.i18n.read().get_content(
1971 &Content::localized_with_args(
1972 "hud-bag-split_swap_slots_drop_items",
1973 [(
1974 "slot_deficit",
1975 slot_deficit.unsigned_abs() as u64,
1976 )],
1977 ),
1978 ),
1979 HudEvent::SwapSlots {
1980 slot_a,
1981 slot_b,
1982 bypass_dialog: true,
1983 },
1984 None,
1985 ));
1986 move_allowed = false;
1987 }
1988 }
1989 },
1990 _ => {},
1991 }
1992 }
1993 };
1994 if move_allowed {
1995 self.client.borrow_mut().split_swap_slots(slot_a, slot_b);
1996 }
1997 },
1998 HudEvent::DropSlot(x) => {
1999 let mut client = self.client.borrow_mut();
2000 client.drop_slot(x);
2001 if let Slot::Equip(EquipSlot::Lantern) = x {
2002 client.disable_lantern();
2003 }
2004 },
2005 HudEvent::SplitDropSlot(x) => {
2006 let mut client = self.client.borrow_mut();
2007 client.split_drop_slot(x);
2008 if let Slot::Equip(EquipSlot::Lantern) = x {
2009 client.disable_lantern();
2010 }
2011 },
2012 HudEvent::SortInventory(sort_order) => {
2013 self.client.borrow_mut().sort_inventory(sort_order);
2014 },
2015 HudEvent::ChangeHotbarState(state) => {
2016 let client = self.client.borrow();
2017
2018 let server_name = &client.server_info().name;
2019 let character_id = match client.presence().unwrap() {
2021 PresenceKind::Character(id) => Some(id),
2022 PresenceKind::LoadingCharacter(id) => Some(id),
2023 PresenceKind::Spectator => {
2024 unreachable!("HUD adaption in Spectator mode!")
2025 },
2026 PresenceKind::Possessor => None,
2027 };
2028
2029 global_state.profile.set_hotbar_slots(
2031 server_name,
2032 character_id,
2033 state.slots,
2034 );
2035
2036 global_state
2037 .profile
2038 .save_to_file_warn(&global_state.config_dir);
2039
2040 info!("Event! -> ChangedHotbarState")
2041 },
2042 HudEvent::TradeAction(action) => {
2043 self.client.borrow_mut().perform_trade_action(action);
2044 },
2045 HudEvent::Ability { idx, state } => {
2046 self.client.borrow_mut().handle_input(
2047 InputKind::Ability(idx),
2048 state,
2049 default_select_pos,
2050 self.target_entity,
2051 );
2052 },
2053
2054 HudEvent::RequestSiteInfo(id) => {
2055 self.client.borrow_mut().request_site_economy(id);
2056 },
2057
2058 HudEvent::CraftRecipe {
2059 recipe_name: recipe,
2060 craft_sprite,
2061 amount,
2062 } => {
2063 let slots = {
2064 let client = self.client.borrow();
2065
2066 let s = if let Some(inventory) = client
2067 .state()
2068 .ecs()
2069 .read_storage::<comp::Inventory>()
2070 .get(client.entity())
2071 {
2072 let rbm =
2073 client.state().ecs().read_resource::<RecipeBookManifest>();
2074 if let Some(recipe) = inventory.get_recipe(&recipe, &rbm) {
2075 recipe.inventory_contains_ingredients(inventory, 1).ok()
2076 } else {
2077 None
2078 }
2079 } else {
2080 None
2081 };
2082 s
2083 };
2084 if let Some(slots) = slots {
2085 self.client.borrow_mut().craft_recipe(
2086 &recipe,
2087 slots,
2088 craft_sprite,
2089 amount,
2090 );
2091 }
2092 },
2093
2094 HudEvent::CraftModularWeapon {
2095 primary_slot,
2096 secondary_slot,
2097 craft_sprite,
2098 } => {
2099 self.client.borrow_mut().craft_modular_weapon(
2100 primary_slot,
2101 secondary_slot,
2102 craft_sprite,
2103 );
2104 },
2105
2106 HudEvent::CraftModularWeaponComponent {
2107 toolkind,
2108 material,
2109 modifier,
2110 craft_sprite,
2111 } => {
2112 let additional_slots = {
2113 let client = self.client.borrow();
2114 let item_id = |slot| {
2115 client
2116 .inventories()
2117 .get(client.entity())
2118 .and_then(|inv| inv.get(slot))
2119 .and_then(|item| {
2120 item.item_definition_id().itemdef_id().map(String::from)
2121 })
2122 };
2123 if let Some(material_id) = item_id(material) {
2124 let key = recipe::ComponentKey {
2125 toolkind,
2126 material: material_id,
2127 modifier: modifier.and_then(item_id),
2128 };
2129 if let Some(recipe) = client.component_recipe_book().get(&key) {
2130 client.inventories().get(client.entity()).and_then(|inv| {
2131 recipe.inventory_contains_additional_ingredients(inv).ok()
2132 })
2133 } else {
2134 None
2135 }
2136 } else {
2137 None
2138 }
2139 };
2140 if let Some(additional_slots) = additional_slots {
2141 self.client.borrow_mut().craft_modular_weapon_component(
2142 toolkind,
2143 material,
2144 modifier,
2145 additional_slots,
2146 craft_sprite,
2147 );
2148 }
2149 },
2150 HudEvent::SalvageItem { slot, salvage_pos } => {
2151 self.client.borrow_mut().salvage_item(slot, salvage_pos);
2152 },
2153 HudEvent::RepairItem { item, sprite_pos } => {
2154 let slots = {
2155 let client = self.client.borrow();
2156 let slots = (|| {
2157 if let Some(inventory) = client.inventories().get(client.entity()) {
2158 let item = match item {
2159 Slot::Equip(slot) => inventory.equipped(slot),
2160 Slot::Inventory(slot) => inventory.get(slot),
2161 Slot::Overflow(_) => None,
2162 }?;
2163 let repair_recipe =
2164 client.repair_recipe_book().repair_recipe(item)?;
2165 repair_recipe
2166 .inventory_contains_ingredients(item, inventory)
2167 .ok()
2168 } else {
2169 None
2170 }
2171 })();
2172 slots.unwrap_or_default()
2173 };
2174 if !has_repaired {
2175 let sfx_trigger_item = sfx_triggers
2176 .get_key_value(&SfxEvent::from(&InventoryUpdateEvent::Craft));
2177 global_state.audio.emit_ui_sfx(sfx_trigger_item, None, None);
2178 has_repaired = true
2179 };
2180 self.client
2181 .borrow_mut()
2182 .repair_item(item, slots, sprite_pos);
2183 },
2184 HudEvent::InviteMember(uid) => {
2185 self.client.borrow_mut().send_invite(uid, InviteKind::Group);
2186 },
2187 HudEvent::AcceptInvite => {
2188 self.client.borrow_mut().accept_invite();
2189 },
2190 HudEvent::DeclineInvite => {
2191 self.client.borrow_mut().decline_invite();
2192 },
2193 HudEvent::KickMember(uid) => {
2194 self.client.borrow_mut().kick_from_group(uid);
2195 },
2196 HudEvent::LeaveGroup => {
2197 self.client.borrow_mut().leave_group();
2198 },
2199 HudEvent::AssignLeader(uid) => {
2200 self.client.borrow_mut().assign_group_leader(uid);
2201 },
2202 HudEvent::ChangeAbility(slot, new_ability) => {
2203 self.client.borrow_mut().change_ability(slot, new_ability);
2204 },
2205 HudEvent::SettingsChange(settings_change) => {
2206 settings_change.process(global_state, self);
2207 },
2208 HudEvent::AcknowledgePersistenceLoadError => {
2209 self.metadata.skill_set_persistence_load_error = None;
2210 },
2211 HudEvent::MapMarkerEvent(event) => {
2212 self.client.borrow_mut().map_marker_event(event);
2213 },
2214 HudEvent::Dialogue(target, dialogue) => {
2215 self.client.borrow_mut().perform_dialogue(target, dialogue);
2216 },
2217 HudEvent::SetBattleMode(mode) => {
2218 self.client.borrow_mut().set_battle_mode(mode);
2219 },
2220 }
2221 }
2222
2223 {
2224 let client = self.client.borrow();
2225 let scene_data = SceneData {
2226 client: &client,
2227 state: client.state(),
2228 viewpoint_entity,
2229 mutable_viewpoint: mutable_viewpoint || self.free_look,
2230 target_entities: &self.interactables.entities,
2232 loaded_distance: client.loaded_distance(),
2233 terrain_view_distance: client.view_distance().unwrap_or(1),
2234 entity_view_distance: client
2235 .view_distance()
2236 .unwrap_or(1)
2237 .min(global_state.settings.graphics.entity_view_distance),
2238 tick: client.get_tick(),
2239 gamma: global_state.settings.graphics.gamma,
2240 exposure: global_state.settings.graphics.exposure,
2241 ambiance: global_state.settings.graphics.ambiance,
2242 mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable,
2243 sprite_render_distance: global_state.settings.graphics.sprite_render_distance
2244 as f32,
2245 particles_enabled: global_state.settings.graphics.particles_enabled,
2246 weapon_trails_enabled: global_state.settings.graphics.weapon_trails_enabled,
2247 flashing_lights_enabled: global_state
2248 .settings
2249 .graphics
2250 .render_mode
2251 .flashing_lights_enabled,
2252 figure_lod_render_distance: global_state
2253 .settings
2254 .graphics
2255 .figure_lod_render_distance
2256 as f32,
2257 is_aiming,
2258 interpolated_time_of_day: self.scene.interpolated_time_of_day,
2259 };
2260
2261 if !global_state.paused() {
2263 self.scene.maintain(
2264 global_state.window.renderer_mut(),
2265 &mut global_state.audio,
2266 &scene_data,
2267 &client,
2268 &global_state.settings,
2269 );
2270
2271 for outcome in outcomes {
2273 self.scene
2274 .handle_outcome(&outcome, &scene_data, &mut global_state.audio);
2275 self.hud.handle_outcome(&outcome, &scene_data, global_state);
2276 }
2277 }
2278 }
2279
2280 self.cleanup();
2282
2283 PlayStateResult::Continue
2284 } else if client_registered && client_presence.is_none() {
2285 if client_type.can_spectate() && !client_type.can_enter_character() {
2288 return PlayStateResult::Pop;
2290 } else {
2291 PlayStateResult::Switch(Box::new(CharSelectionState::new(
2292 global_state,
2293 Rc::clone(&self.client),
2294 Rc::clone(&self.hud.persisted_state),
2295 )))
2296 }
2297 } else {
2298 error!("Client not in the expected state, exiting session play state");
2299 PlayStateResult::Pop
2300 }
2301 }
2302
2303 fn name(&self) -> &'static str { "Session" }
2304
2305 fn capped_fps(&self) -> bool { false }
2306
2307 fn globals_bind_group(&self) -> &GlobalsBindGroup { self.scene.global_bind_group() }
2308
2309 fn render(&self, drawer: &mut Drawer<'_>, settings: &Settings) {
2313 span!(_guard, "render", "<Session as PlayState>::render");
2314
2315 let client = self.client.borrow();
2316
2317 let (viewpoint_entity, mutable_viewpoint) = self.viewpoint_entity();
2318
2319 let scene_data = SceneData {
2320 client: &client,
2321 state: client.state(),
2322 viewpoint_entity,
2323 mutable_viewpoint,
2324 target_entities: &self.interactables.entities,
2326 loaded_distance: client.loaded_distance(),
2327 terrain_view_distance: client.view_distance().unwrap_or(1),
2328 entity_view_distance: client
2329 .view_distance()
2330 .unwrap_or(1)
2331 .min(settings.graphics.entity_view_distance),
2332 tick: client.get_tick(),
2333 gamma: settings.graphics.gamma,
2334 exposure: settings.graphics.exposure,
2335 ambiance: settings.graphics.ambiance,
2336 mouse_smoothing: settings.gameplay.smooth_pan_enable,
2337 sprite_render_distance: settings.graphics.sprite_render_distance as f32,
2338 figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32,
2339 particles_enabled: settings.graphics.particles_enabled,
2340 weapon_trails_enabled: settings.graphics.weapon_trails_enabled,
2341 flashing_lights_enabled: settings.graphics.render_mode.flashing_lights_enabled,
2342 is_aiming: self.is_aiming,
2343 interpolated_time_of_day: self.scene.interpolated_time_of_day,
2344 };
2345
2346 self.scene.render(
2348 drawer,
2349 client.state(),
2350 viewpoint_entity,
2351 client.get_tick(),
2352 &scene_data,
2353 );
2354
2355 if let Some(mut volumetric_pass) = drawer.volumetric_pass() {
2356 prof_span!("clouds");
2358 volumetric_pass.draw_clouds();
2359 }
2360 if let Some(mut transparent_pass) = drawer.transparent_pass() {
2361 prof_span!("trails");
2363 if let Some(mut trail_drawer) = transparent_pass.draw_trails() {
2364 self.scene
2365 .trail_mgr()
2366 .render(&mut trail_drawer, &scene_data);
2367 }
2368 }
2369 {
2371 prof_span!("bloom");
2372 drawer.run_bloom_passes()
2373 }
2374 {
2376 prof_span!("post-process and ui");
2377 let mut third_pass = drawer.third_pass();
2378 third_pass.draw_postprocess();
2379 if let Some(mut ui_drawer) = third_pass.draw_ui() {
2381 self.hud.render(&mut ui_drawer);
2382 }; }
2385 }
2386
2387 fn egui_enabled(&self) -> bool { true }
2388}
2389
2390fn find_shortest_distance(arr: &[Option<f32>]) -> Option<f32> {
2391 arr.iter()
2392 .filter_map(|x| *x)
2393 .min_by(|d1, d2| OrderedFloat(*d1).cmp(&OrderedFloat(*d2)))
2394}
2395
2396fn auto_glide(
2398 fluid: Fluid,
2399 vel: Vel,
2400 free_look: bool,
2401 dir_forward_xy: Vec2<f32>,
2402 dir_right: Vec3<f32>,
2403) -> Option<Dir> {
2404 let Vel(rel_flow) = fluid.relative_flow(&vel);
2405
2406 let is_wind_downwards = rel_flow.z.is_sign_negative();
2407
2408 let dir = if free_look {
2409 if is_wind_downwards {
2410 Vec3::from(-rel_flow.xy())
2411 } else {
2412 -rel_flow
2413 }
2414 } else if is_wind_downwards {
2415 dir_forward_xy.into()
2416 } else {
2417 let windwards = rel_flow * dir_forward_xy.dot(rel_flow.xy()).signum();
2418 Plane::from(Dir::new(dir_right)).projection(windwards)
2419 };
2420
2421 Dir::from_unnormalized(dir)
2422}